mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
d46c512d79b159c32051a813ef229b60100c33a3
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d46c512d79 |
SGSR: reach it from the in-game menu too
The upscaler control exists TWICE -- once in the Renderer settings tab and once in the in-game menu -- and only the first one learned about SGSR, so the option was unreachable from the place people actually change these mid-game. Both are pickers now rather than an FSR1 on/off switch. The in-game one was a MenuSwitchRow, which was fine while FSR1 was the only upscaler and is the wrong shape for three mutually exclusive ones. The settings search index pointed at renderer.fsr1.label, which is no longer a row anywhere; searching for it would have landed on nothing. |
||
|
|
001ca40803 |
GS: add SGSR upscaling (Qualcomm Snapdragon Game Super Resolution 1)
A third display upscaler beside FSR1, and the first one written for the hardware this app actually runs on: FSR1's two compute passes are the expensive outlier on mobile, while SGSR is a single pass Qualcomm designed for Adreno. The filter is Qualcomm's, BSD-3-Clause, unchanged in substance. What differs is the shape around it -- theirs is a fragment shader over a fullscreen triangle, this is a compute pass, because that is what GSDevice already knows how to schedule. So the interpolated texcoord becomes a UV computed from the invocation id and the fragment output becomes an imageStore. The crop handling and the widened 0..2 sharpness range come from the Eden Emulator Project's port (GPL-3.0-or-later, compatible with PCSX2's GPL-3.0+); PCSX2 hands the pass a display rectangle inside a larger target, which is the same problem FsrEasuConOffset solves for FSR1. Deliberately a strict subset of what FSR1 already requires -- same descriptor types, same rgba8 storage image, textureGather with a constant component and no offset, which is core Vulkan 1.0 and needs no optional feature. So any driver already running FSR1 can run this, Turnip included, and there is no vendor gate on either. A driver that cannot compile the pipeline clears Features().sgsr and the renderer falls back to plain bilinear with an OSD notice, rather than failing. The Android upscaler control becomes a picker rather than an on/off toggle: three mutually exclusive upscalers expressed as two toggles that silently switch each other off is a worse way to say it than one list. FSR1 and SGSR share the existing sharpness slider -- the number means different things to each, but it is the same intent, and a second slider would only invite the two to disagree. ★ The Settings.kt clamp on the persisted enum was still bounded at UPSCALER_FSR1, and would have silently rewritten any SGSR selection back to Off. That clamp's own comment warns about exactly this failure; it still had to be updated by hand. Worth remembering the next time the enum grows. Suggested by CamilleLaVey, who authored the upstream changes (eden-emu #4293). |
||
|
|
cf5577b773 |
Thermals: the no-reading sentinel was a positive number
Float.MIN_VALUE is the smallest POSITIVE float in Kotlin and Java (1.4e-45); the most negative is -Float.MAX_VALUE. The overlay hides a figure by testing `value > ARMSX2_THERMAL_NONE` against -1000.0f, so the sentinel sailed through as a real reading and printed as "0°". Both sides are -1000.0f now. Every use is an exact assignment or comparison against the constant, and the plausibility gate (-20..150) means a real reading can never collide with it. Worth noting when this was visible: never, on a device whose zones all resolve, because the sentinel is then never sent. It shows up only on the devices the fallback exists for -- a readable CPU zone and no GPU zone would print "CPU 47° GPU 0°". Found by reading the code during the ARMSX3 port, not by testing, which is the only way it could have been found. |
||
|
|
bec41db7d3 |
Second screen: size the cover cell against the display, not the artwork
Giving the cover cell the art's own 1.4 ratio at the column width produced 869px on a 1080px-tall panel: one tile taking most of the screen and shoving everything below it off the bottom. The panel is landscape and the art is portrait, so deriving the height from the width was the wrong axis entirely -- it made the cell taller the WIDER the panel got. FIT_CENTER already guarantees the whole cover is visible in a box of any shape, so the box only has to be a reasonable size rather than the art's shape. A third of the panel height reads as a cover without crowding out the tiles people actually press, with a floor for very short displays, and the tile-height slider still overrides it. |
||
|
|
48a8fcefcc |
Second screen: give the cover a real box, say which RA mode is active
FIT_CENTER only helps when there is a box to fit into. The cover cell was WRAP_CONTENT around an image, which gives the cell no definite height, so the art sized itself and spilled past the cell to be clipped by the tile outline -- the same cut-off cover, arrived at a different way. Box art is about 1.4 times as tall as it is wide, so the cell is now given that shape at the column width and the whole cover fits inside it. adjustViewBounds went with it; it fights a definite box rather than helping. The achievement tile showed a hardcore count and a casual count above the total, which reads as three unrelated numbers -- "0 next to the trophy, then another 0/64" -- and says nothing about which mode is actually active, which was the one thing it needed to say. It now names the mode, with the trophy for hardcore and the medal for casual, over unlocked-of-total. |
||
|
|
f0d2a42bb2 |
Second screen: fit the cover, scroll the panel, mark hardcore vs casual
The cover was CENTER_CROP in a grid cell. Box art is portrait and a cell is not, so filling the cell ate the top and bottom of the cover -- the parts carrying the logo and the title. It fits inside the tile now and letterboxes against the tile background. The panel root could not scroll, which was survivable while the tile list was short and stopped being so the moment four more tiles were added to it. The panel has no say in its own height -- the user chooses how many tiles and how tall each one is -- so anything that did not fit was simply clipped off the bottom of the display with nothing to say it was there. "12/40" did not say whether those were earned in hardcore or casual, which is the distinction RetroAchievements cares most about. The counts now carry marks for both. rc_client reports a per-achievement mask where a hardcore unlock also sets the softcore bit, so the casual figure is the softcore-ONLY count rather than the total -- otherwise every hardcore unlock would be counted twice. |
||
|
|
93c6aa25c7 |
Second screen: append the new tiles to existing layouts, not just pristine ones
The first migration only upgraded a layout that was byte-for-byte the old default, reasoning that anything else was a deliberate arrangement to leave alone. That fails for the ordinary case: toggle one tile on and off and the layout is no longer the default, so someone who had barely touched it never saw the new tiles at all. Which is exactly what happened -- a panel that had been poked at during testing kept its old contents and looked like the feature had not shipped. Now version-stamped and additive: whatever the user arranged stays arranged, anything missing from the new set is appended, and the stamp means it runs exactly once -- so removing a tile afterwards sticks. |
||
|
|
c109f45ab0 |
Second screen: cover and RetroAchievements in the default layout
Shipping them as chips nobody would find was the wrong call -- a tile that has to be discovered in a settings screen may as well not exist. Cover, Achievements, RA points and rich presence are now part of the default panel. A saved layout still wins, because that is what saving it is for. But a layout that is byte-for-byte the OLD default is not a choice anyone made, it is just what they were handed, so those upgrade once to the new default. Anything actually arranged is left alone. The cover tile also gets a visible fallback. With no game, or before the art downloads, it was an empty box -- and an empty box is indistinguishable from a broken tile. It now shows "Cover" with no game and the game's title once there is one, with the picture drawn over the top when it arrives. A custom cover file that cannot be decoded falls through to the fetched URL instead of leaving the tile blank. |
||
|
|
42eb718fbd |
Fix crash on launch: achievements polled with no VM
getAchievementsJSON is VM-scoped -- the JNI entry calls Achievements::GetAchievementsAsJSON() with no guard at all, unlike the rich presence getter directly below it which checks HasRichPresence() first. With no game loaded it dereferences null and takes the process down. The previous commit hoisted the unlock tracking out of the Achievements tile's text builder so the new Latest-unlock tile would work on its own. That was right, but it moved the call onto the panel tick, which runs whether or not a game is running -- so the panel crashed the app the moment it appeared, on launch, before anything could be touched. Every other caller of this getter is inside a VM-scoped context by construction; this one was not. Now polled only with a live VM, and the read-outs fall back to a dash otherwise. runCatching was never going to help: a SIGSEGV is not a Throwable. The native getter is still a landmine for any future caller and should get the same guard its neighbour has. |
||
|
|
740f2a8975 |
Second screen: cover art and more of what RetroAchievements knows
Cover art tile. The panel had the game as a line of text, and on a screen sitting beside you the cover is what makes it read as "this game" at a glance. It resolves through the same two sources the library uses -- a user-set custom cover first, then the fetched URL -- so the panel shows what the library shows, including a cover picked by hand. The only tile that is a picture rather than text, and it re-resolves on the tick keyed on the game, so switching game changes it without rebuilding the panel. Three more RetroAchievements read-outs: points earned against the total, which is the figure RA itself leads with; the latest unlock on its own; and the rich presence line, which is the one thing here that says something a number cannot. Two things fixed while adding them. The unlock tracking lived inside the Achievements tile's own text builder, so the new Latest-unlock tile would have read "—" forever unless the Achievements tile happened to be placed as well -- it is now a per-tick step that runs regardless of which tiles exist. And the achievements JSON was parsed per tile, so placing all three would have parsed the same string three times a tick for identical results; it is parsed once now and shared. |
||
|
|
484bd78b51 |
Fix theme changes not reaching the panel; collapsible editor toolbar
The panel reads its colours when it BUILDS its views, so publishing a new scheme changed nothing for a panel that was already open -- it kept the colours it was born with, which is why switching theme appeared to do nothing to it. It now rebuilds when the theme changes, keyed on the user-facing switches rather than on the scheme object: the RGB mode produces a new scheme every hue step, and tearing the panel down and back up sixty times a cycle would be absurd. The editor's auto-dock addressed the wrong half of the problem. Moving the panel out of the way once a widget is selected cannot help you SELECT a widget under the panel, because the obstruction happens before there is anything to react to. The grip row now has a collapse toggle: one tap leaves just the grip and uncovers everything beneath it, one tap brings the controls back. Not persisted, and reset on leaving the editor -- it is a momentary "let me see under this", and opening the editor to a panel with no controls on it would look broken. Auto-dock stays; it still helps once a widget is picked. Device temperatures on the overlay now default ON, confirmed reading real values. It sits with CPU/GPU load, the poll is one file read every couple of seconds, and a device with no readable zone shows nothing rather than something wrong. |
||
|
|
d2a7613fb3 |
Second screen and OSD: the rest of the batch
Custom panel background. Theme / library / black covered three of the four asks; "an own background" needed a picker. Takes the persistable read grant like the library's own picker -- without it the URI works until the process restarts and then resolves to nothing, which reads as the background disappearing on its own. Darkened by the same scrim as the library backdrop, because an arbitrary photo has no obligation to be dark and tile text still has to be readable. Clock and battery move into a status bar across the top instead of being two grid cells. Same information, but it stops the clock competing for space with the things you actually press, and the grid gets two cells back. The rest of the in-game OSD's figures reach the panel: VPS, EE / GS / GPU load and frame time. These were not missing by choice -- getFPS() was the only figure with a way across the JNI boundary, so the panel could show frames and a percentage of nominal and nothing else. PerformanceMetrics already computed all of it for the overlay. Each getter returns 0 with no VM rather than the last value, so an idle panel reads as idle instead of frozen on whatever the last game was doing. Tile height is now settable. Columns already decided width -- tiles split the row equally, so choosing columns IS choosing width, and a second width control would only be a way to disagree with it. Height had no control at all, which is why a panel could only ever be as tall as its text. A display can be told to stay out of it. "The second screen also still appears on the external monitor when connected via usbc" is not a bug by the display-picking rule -- a USB-C monitor is a perfectly good second display -- so this records a preference instead of guessing: the panel's own Not-this- screen tile drops the display it is on, and settings can re-enable them. Keyed by display NAME, since ids are reassigned across replugs. Guessing from internal-vs-external would have been wrong anyway; Android has no stable public display type before API 34. Device temperatures on the performance overlay, which is where they were asked for. The core cannot read a temperature -- there is no portable API, and on Android the only route is a vendor-specific sysfs the app layer already discovers for the panel -- so the app pushes the values in and the overlay draws what it was given. Atomics because the writer is a UI-thread poll and the reader is the GS thread. A sensor that could not be read is omitted rather than drawn as a zero. |
||
|
|
302a66a9bb |
Touch editor: the panel gets out of the way by itself
The editor panel was a floating window pinned to the top of the screen, so every button underneath it had to be uncovered by hand before it could be touched. Dragging it was not an occasional adjustment, it was the price of editing anything in the top half of the layout. It now docks to the opposite half from whatever is selected: pick a button up top and the panel goes to the bottom, pick one at the bottom and it returns. Halves rather than real overlap maths, deliberately -- a panel that darts around as rectangles graze each other is less predictable than one that is simply never on the same side as the thing you are working on. Manual dragging stays for anything unusual. The stored offset means "away from the anchored edge" and so flips with the anchor; without that, a panel the user had nudged down would be nudged straight off the bottom of the screen the moment it docked there. |
||
|
|
f2b092cb6e |
Second screen: follow the app's theme, tile icons, thermals, backgrounds
The panel carried a hand-written palette of neutral greys, on the reasoning that a Presentation sits outside the Compose tree and reading MaterialTheme from a plain View would mean holding a composition alive for six colours. The reasoning was right and the conclusion was not: ARMSX2's night theme is BLUE, so grey was not a neutral choice, it was a different app on the second screen. That is what "still looks quite unpleasant... more like stock android instead of armsx2" was describing. Armsx2Theme now publishes the RESOLVED scheme for code that cannot be a composable, and the panel reads that. No composition is held and there is no second copy of the theme logic to drift, so the panel follows Blue, Purple, OLED, Custom, Material You and the animated RGB mode without knowing any of them exist. Action tiles get a glyph over the label, because a tile has to be recognised from across a desk. Geometric Unicode rather than emoji: emoji bring their own colours and their own house style, which is the stock-Android look this is moving away from, while a glyph takes the accent like everything else. The two tiles that carry state SWAP their glyph rather than appending a line -- Pause shows what the tap will do, and Fast Forward no longer grows when you use it, which was half of the ragged-row problem. Panel background is now a choice: the theme's own ground, the library's backdrop darkened so it reads as the same app as the screen beside it, or solid black for an OLED second display. CPU, GPU and battery temperature tiles, asked for by two people. Android has no supported API -- HardwarePropertiesManager is signature-gated -- so this reads the thermal sysfs, which is permissionless but is not a contract: zone count, naming and even the UNIT are vendor-specific. Zones are discovered once by name, the unit is inferred by magnitude (no phone runs at 1000C and none idles at 0.045C, so the ranges cannot overlap), implausible values are dropped rather than displayed, and a device that exposes nothing shows a dash instead of a wrong number. Polling is on its own interval, 1 to 5 seconds -- that interval is the mitigation asked about, and it is why the panel tick can call it every frame for free. |
||
|
|
7bdcd1be7c |
Second screen: pause that stays, even tiles; on-screen turbo (#619)
Pause did not stick. The stuck-paused backstop resumes a VM that is paused
with nothing covering the screen, reasoning that such a state can only be a
lost resume. That held while every pause came with a frontend over it --
pauseForOverlay() is what the quick menu, the library and backgrounding all
use. The second screen's Pause tile is the only caller of plain pause(), and
it deliberately leaves the game on screen, so the backstop undid it 700ms
later. That is the "pause immediately unpauses itself" two people reported.
A pause the user asked for is now marked as such and the backstop leaves it
alone; resume() clears the mark.
Tiles were different heights. An active tile appends a state line ("\n❚❚" on
Pause, likewise Fast Forward), and with maxLines alone a tile grew the moment
you used it, so the row went ragged -- which is what made Fast Forward the
one people noticed. Every tile now reserves both lines whether or not it is
showing state, which also scales with the text size instead of a fixed
height, and row children stretch to the tallest so a Button's padding cannot
show as a ragged edge against a TextView's.
On-screen buttons get rapid-fire (#619). Physical buttons have had turbo and
the on-screen ones never did, which is the asymmetry the request was about --
and it is the same asymmetry tap-to-hold had in the other direction. Per
button, off by default, cycled from the editor toolbar. It runs on the macro
Frequency timer rather than a second one of its own, so it inherits the
sampling floor that stops the fastest settings from emitting presses the VM
never samples, and it composes with tap-to-hold: set both and a tap starts
the autofire and the next tap stops it.
Feature requested by shinobumaehara (#619)
|
||
|
|
bc18907e65 |
Fix Discord stuck on "Connecting" (crash loop in :discord)
connect() resolves the Discord app through DiscordSocialSdkInit.getEngineActivity() and hands the result straight to Context.getPackageManager() with no null check. That static was only ever set by DiscordAuthActivity, which runs during sign-in -- so every launch with a cached token started the SDK with it null, and :discord died with an NPE. Android restarted the service, which started again and died again. The app process never saw any of it, because the protocol is poll-only: the UI just sat on "Connecting" forever. Binding the SDK only during sign-in was wrong independently of that. The SDK's statics are per-process and Android restarts :discord whenever it likes, so the binding has to be re-established on demand rather than assumed to survive. Handing the Activity over is now separate from opening the browser: DiscordAuthActivity takes an EXTRA_AUTHORIZE flag and always binds, and the service will not call start() until a binding exists, launching the invisible Activity itself when there is none. |
||
|
|
6451202102 |
Savers: make every port_free and port_new path unconditional
Follow-up to 7aa4547e60. Four ports -- flux, plasma, solarwinds and hyperspace -- still opened port_free with "if (!g_started) return;". That is unreachable today, since the JNI only calls destroy after a successful create, but unreachability is a property of the current call graph rather than of the function, and it contradicts the rule the fix established: gl1 is given back on every path out, because whatever it holds belongs to an EGL context that is about to die. All six are now unconditional. Same argument one level up. port_new opened with "if (g_started) return 1", which was defensible when gl1's state was whatever the last saver left, but nativeInit now calls gl1_lost() first -- so gl1 is guaranteed DOWN on entry and reporting success there would hand the caller a saver with no shim under it. A stale run is torn down instead, and gl1_init always runs. No behaviour change on any path reachable today. The point is that a saver added later, or an upstream cleanup that returns early, fails in its own saver instead of poisoning the next one. |
||
|
|
fc5c24f8a3 |
Tap to hold for physical buttons (#612)
The on-screen controls have had tap-to-hold since they existed; physical buttons always followed the button exactly. A game that wants one held while another control is worked -- MGS2 holding R1 to aim -- is then unplayable for anyone who cannot hold two controls at once, which is what the request was about. Modelled as a transform on the event stream rather than a branch beside turbo: a tap becomes a synthetic KeyDown, the next tap a synthetic KeyUp, and everything between is swallowed. Turbo composes with it for free -- flag a button both and a tap toggles autofire on and off. Keyed on the physical code for "is this a fresh press", because ACTION_DOWN auto-repeats while a key is held and each repeat would otherwise toggle, and on the PS2 target for "is it latched", so two physical buttons bound to the same button cannot desync. The state lives in the companion because the boot path that clears it runs there -- a latch must not outlive the game it was set in -- while the dispatch that sets it is an instance method. Changing the setting releases whatever is held: turning it off for a button that is latched down would otherwise strand it pressed, with no second tap left to release it. Stored per action per player like turbo, and off by default. Feature requested by bobo123g (#612) |
||
|
|
1a99ab237e |
Fix animated background locking users out of the app
A saver that dies natively made ARMSX2 unlaunchable. The choice is a persisted pref read on the library screen -- the first screen -- so the crash repeated on every launch and Settings was never reachable to turn it off. The only escape was clearing app data, which takes memory cards and save states with it. A user lost their saves that way. Cause: gl1's state is a file-scope global holding GL object names, and gl1_init() early-returns on g.ready. Skyrocket and Lattice defer initSaver() to port_resize, so a create-then-teardown with no surface size left g_started false and their port_free returned BEFORE gl1_shutdown(); flux, plasma and solarwinds leaked it the same way when initSaver() left readyToDraw clear, since returning 0 means port_free is never called. Either way g.ready stayed set with names from a destroyed EGL context, and the next saver -- new view, new context -- drew against them. Drivers answer that with anything from a black screen to a segfault. Each port now gives gl1 back on every path out, and nativeInit calls gl1_lost() as the invariant: a new context never inherits old GL names. Contained separately, because native GL can always find a new way to die: the setting arms itself with a synchronous commit() before the render thread starts and disarms when that thread exits in an orderly way. Still armed at startup means the last run died with a saver up, so the background switches off and the user is told which one. runCatching was never going to catch a SIGSEGV. Also guards Thread.start(): it asks for a 16MB stack (Skyrocket declares a 3MB starmap as a local) and an OutOfMemoryError there is an uncaught throw on the main thread -- the same lockout with no native crash involved. |
||
|
|
e8d3530f51 |
tools: the Discord check must not pipe into grep -q
It fired on its very first run against an APK that DID contain the library. `unzip -l | grep -q` under `set -o pipefail` is a false-failure generator: grep exits on the first match, SIGPIPEs unzip, and pipefail then reports the pipeline as failed. Capture the listing and match it with `case` instead, which is what the existing notes on this already say to do. |
||
|
|
fb80386cc8 |
tools: fail the release build when Discord is configured but missing
2.6.6.8 shipped without Discord and it was only noticed after publication. The SDK is resolved from $DISCORD_SDK_DIR at configure time and gated on include/discordpp.h existing, so with the variable unset the build quietly omits it -- no error, no warning, nothing in the log to read afterwards. Every check that already runs on these artifacts (both cores, alignment, signing, package, MANAGE_EXTERNAL_STORAGE) would have caught this class of mistake if one had existed. Both scripts now verify libdiscord_partner_sdk.so in the output. With DISCORD_SDK_DIR set and the library absent, that is FATAL -- it means the staged directory was wrong, which is easy to get wrong given the raw SDK download ships an x86-64 .so and only the .aar carries arm64. With the variable unset it warns loudly instead of failing, because a Discord-less build is still a legitimate thing to produce on purpose. |
||
|
|
9111cd617d |
GS: remove the texture-replacement diagnostics
All of it was instrumentation for the Persona 3 FES investigation, and that turned out not to be a texture problem at all -- three named patch groups were failing to enable, and the texture pack stopped matching because the mod they gate patches the game's font data. Removed: the TCPROBE cross-version probe in HashCacheKey::Create, the hit/miss counters and their geometric reporter, the per-miss detail listing what the pack holds for a TEX0, the pack filename samples at map load, the duplicate/shadowed accounting, and the mtime sidecar the palette fallback needed. Kept the pre-existing "N indexed for '<serial>' (scanned <dir>)" line, which predates this and is genuinely useful: a zero there with a path that does not match the user's pack folder is still the fastest read on a pack that does nothing. The cache budget work is untouched. |
||
|
|
20a9238b31 |
Patches: a toggle that changes no patch= lines still has to write the enable list
"I enabled HostFS and it is still off", with the switch sitting on. His game INI
proves it: [Cheats] present but empty, and no [Patches] section at all -- the
enable-list write never ran.
toggleLocalCheat flips the switch optimistically, then bails early when
setBodyEnabled produced an identical body:
if (newBody == target.body) return // pushEnableList never reached
setBodyEnabled only comments or uncomments patch= lines, and community pnach files
ship UNCOMMENTED. So for exactly those files enabling is a no-op on the body, the
early return skips the enable list, and Patch.cpp applies a NAMED group only when
its name is in [Patches]/[Cheats] Enable -- unlabelled groups are the only ones
that auto-enable. Switch on, file already correct, patch never applied, nothing
anywhere saying so.
The two writes are independent. Rewrite the body only when it differs; always push
the enable list.
This is what actually blocked the Persona 3 FES mods. The HostFS loader group could
not be enabled at all, so the mod never loaded, and the texture pack keyed to the
modded font stopped matching as a consequence -- which is how it arrived as "my
texture mods broke".
|
||
|
|
b506fe2683 |
Patch: enabling from the library writes to the game INI, not the base layer
"I enabled HostFS and it is still off." The Patch Manager reads the toggle back as on, and the patch loader never sees it. ReloadEnabledLists reads through LayeredSettingsInterface, which returns the FIRST NON-EMPTY layer with LAYER_GAME ahead of LAYER_BASE. So the moment a game's INI carries any [Patches] Enable entry, the base list is invisible. setEnabledPatches targeted the game INI only when a VM was running -- opened from the library it had nowhere to go but base, where a shadowing game layer swallowed it. The UI then reads the value straight back, which is why it looks like it took. For this reporter that patch is the HostFS loader their Persona 3 FES mod runs on, so it stayed off through repeated attempts to switch it on, the mod never loaded, and the texture pack keyed to the modded font kept missing. Take the serial from the caller -- PatchManagerViewModel already resolves one via bestSerial() -- and resolve the game INI by the same glob gameIniBeginWriteForSerial uses. With no VM and no serial, or no INI for that serial yet, it still falls through to base, which is correct for a game that has no per-game file at all. This is the "wants a serial/CRC parameter" the old comment left as the real fix. |
||
|
|
c91ff32d21 |
Patch: delete the enable-list purge
It was a one-time repair for lists poisoned by patches arming themselves, and it
has done more harm than the thing it repaired. It deleted every [Patches]/[Cheats]
Enable entry, not just the self-armed ones, so anyone who had turned a patch on
deliberately lost it silently on update -- and when the patch in question is the
HostFS loader a mod depends on, the mod stops loading, the texture pack keyed to
the modded data stops matching with it, and the whole thing presents as "my
texture mods broke". That cost eleven builds to chase from the wrong end.
Restricting it to lists that look auto-armed was the first attempt at a fix, and
it was still wrong: the threshold is a guess, and someone with thirty cheats
enabled on one game would have been wiped by it just the same. A narrower
silent-data-loss bug is not a fix.
Removing it outright instead, because its job is finished:
- It is a migration, and it has already run for everyone who updated in the past
month. What remains is a shrinking population and a permanent hazard.
- Nothing re-poisons a list now.
|
||
|
|
2bbe909210 |
Revert the palette fallback and the purge notice
Both existed to work around the wrong diagnosis. The Persona 3 FES mods were not failing in the texture cache at all -- a PNACH mod was switched off by the enable-list purge, and because PNACH mods patch game DATA the texture pack keyed to the patched font stopped matching as a side effect. The palette fallback substituted a different colour variant of a glyph when the exact palette was missing. It raised the hit count but could not be right: the palette IS the colour, so substituted art carries the wrong shade, and it never addressed why the hashes moved. Gone; the replacement path behaves exactly as it always did. The one-time notice goes too, per jpolo1224: the purge no longer takes deliberate lists, so nothing needs announcing going forward. Kept: the diagnostics that actually found this -- files scanned versus indexed versus shadowed, hit and miss counts, and the per-miss detail naming what the pack holds for that texture. Those are what turned "mods do not apply" into a measurement, and they cost nothing when nothing is wrong. |
||
|
|
0dcf53af88 |
Patch: stop the enable-list purge switching off patches people chose
This is what broke Persona 3 FES mods, and it was never the texture cache.
|
||
|
|
4d97b6cdf5 |
GS: fall back to another palette variant when the exact one is absent
A paletted replacement is keyed on TEX0 hash AND palette hash, so a pack only applies while the game asks for a palette its packer happened to dump. Persona 3 FES lands exactly there: the pack carries several palette variants of each glyph and the game asks for one that is not among them, so every glyph misses while the unpaletted art around it replaces fine -- 189 hits against 323 misses, which is why the scene looked right and the text did not. With this, 441 against 71, and none of the remainder are palette mismatches. Lowest palette hash, deliberately. It is arbitrary but STABLE: the same glyph resolves to the same file on every draw and every run, so text renders in one consistent colour. Choosing by file mtime was tried and was worse in a way worth recording -- different glyphs won different variants and the text came out multicoloured. The colour can still be wrong, since the replacement image has the packer's palette baked in and there is no recolouring it. That is the trade, and it is the right way round: a mod that applies in the wrong shade beats one that does not apply at all. This does NOT explain why the pack matched on 2.6.6. That is a separate finding and still open: the same glyph at the same address with the same palette hashes differently now (2.6.6 asks 85076d2a533c0128, current asks f0576dc2f0bb17d5), with TBP0, TBW, PSM, TW/TH, region and lod all identical between them. The bytes in GS local memory differ, which is upstream of texture replacement entirely. |
||
|
|
a4f6fb0bd9 |
GS/TC: split the base-level hash from the mip chain in the probe
Both builds report identical TBP0, TBW, PSM, TW/TH, region and lod for the same font glyph, and identical leading bytes -- yet different TEX0 hashes. Since HashTextureLevel reads straight out of GS local memory, the bytes at that address differ; the shipped hash folds the base level and every mip into one value, so it cannot say which of them moved. Recompute the base level alone for the probed textures and log it beside the combined hash, plus the actual mip range rather than the bool the first probe recorded. Base matching with the combined differing means the mip chain moved; base differing means the glyph data itself did. Those are different bugs. |
||
|
|
6375594405 |
GS/TC: cross-version probe for the texture-hash divergence (diagnostic)
The two builds ask for disjoint TEX0 hashes for the same on-screen font -- nine values on 2.6.6, eight now, no overlap, same palette hash, same PSMT4. Every function feeding the hash is byte-identical between them, so what differs is the DATA, and HashCacheKey records only the region's width and height: never where it starts, never which address it came from, never whether mips were folded in. Print exactly what the key discards -- TBP0, TBW, TW/TH, the full region rect including its origin, whether lod was present, and the first bytes actually hashed -- so two logs can be diffed instead of theorised about. |
||
|
|
7573a3e41f |
GS: drop the palette substitution, keep what it taught us
A paletted glyph's palette IS its colour, so when the game asks for a palette the pack does not carry there is no "close enough" file to stand in for it. Substituting produced multicoloured text: the pack holds eight colour variants of each glyph, all from one extraction seconds apart, and different glyphs won different variants. Both selection rules were wrong for the same reason -- lowest hash preferred the base pack, newest was noise, and neither can be right when the thing being chosen IS the colour. Keep the diagnostic half, which is what actually advanced this: on a paletted miss, list every colour of that TEX0 the pack does hold. That distinguishes "the pack lacks this texture" from "the pack lacks this COLOUR of it", and the second is the real finding here -- the modder dumped eight colours and the game is asking for a ninth. Leaves the replacement path behaving exactly as it did before any of this, with better logging. The open question is unchanged and now isolated to one value: why the game asks for a palette that is not among the dumped eight. |
||
|
|
461bcdc52f |
GS: palette fallback picks the NEWEST variant, and names every candidate
Shadowing was not it: 8386 files, 8339 indexed, only 4 collisions -- and the remaining misses are 640x384 PSMCT32 with no palette, i.e. FMV frames, which hash uniquely per frame and can never match. So the layered mods index fine and the game does ask for them. What actually loses is the choice BETWEEN palette variants. A mod over a base pack supplies the same glyph under a different palette, so the two never collide during indexing -- and the first cut of this fallback took the lowest palette hash, which is arbitrary, and silently preferred the base pack. The player ends up seeing the pack they installed FIRST, which is the opposite of what layering a mod means, and matches the report exactly: base HD text applied, slim font did not. Prefer the newest file instead. Modification time is the one signal that separates "the pack added last" from "the pack it was layered over", and the directory walk already reports it, so this costs no extra I/O. Ties break on the lower hash purely to stay reproducible. Also log every candidate -- palette hash, mtime, full path -- for the first few fallbacks, so the choice is auditable rather than asserted. If the newest file is still the wrong one, that log says so immediately instead of costing another round. Still not shippable: substituted art carries the packer's palette, so tints can be wrong, and mtime is a heuristic rather than an expression of intent. A real fix wants explicit pack precedence. |
||
|
|
367b0b7e14 |
GS: report replacement files shadowed by an earlier file
The palette fallback moved Persona 3 FES from 189 hits / 323 misses to 441 / 71, with ZERO of the remaining misses matching a name under another palette -- so the palette hash is settled, and what is left is textures the map does not hold under that name at all. The base HD pack now applies; the layered mods still do not. emplace does not overwrite, and this scan is RECURSIVE. Two files in different subdirectories that decode to the same texture name collide, and whichever the directory walk reaches first wins -- silently, with nothing to say which of them ended up on screen. That is exactly how a mod layered over a base pack loses: not on intent, on traversal order. Count the collisions, name the first six with both paths, and report files scanned alongside textures indexed. "My mod is not applying" and "my mod is being shadowed by another pack" are indistinguishable to the person reporting it, and until now they were indistinguishable from the log too. Nothing about which file wins changes here. Measure first: if the count is zero, the layered mods are absent rather than shadowed, and that is a different fix. |
||
|
|
d9252de06e |
GS: palette-relaxed replacement fallback (TEST BUILD, not for release)
A paletted texture is looked up by TEX0 hash AND palette hash, so a pack only applies while the game asks for a palette its packer happened to dump. The Persona 3 FES logs land exactly there: the pack carries several palette variants per glyph (ea34cb7b, df582335, b6eebe9e, 99b15967 ...) and the game asks for cf4df1c018862d85, which is none of them. Every glyph misses while the unpaletted art around it replaces fine -- 189 hits alongside 323 misses -- which is why the scene looks right and the font does not. Worth recording what this ISN'T, since each cost a round to eliminate: the cache budget (already present in the working version), the pre-ELF hack strip (transient, ApplyCoreSettings restores it), paltex (it relaxes the hash cache, never the replacement lookup -- CreateTextureName always uses the real palette hash for paletted formats), and name parsing, the hash function and the paltex defaults, all byte-identical to 2.6.6. So when the exact palette is missing but the pack holds this TEX0 under others, take one -- lowest palette hash, so the choice is reproducible rather than dependent on map iteration order. NOT correct as shipped, and deliberately labelled so: the replacement image has the packer's palette baked in, so substituted art can carry the wrong tint. It exists to answer one question. If the mods appear, the palette hash was the only obstacle and the real fix is about WHICH palette to prefer, not about loading at all. If they still do not appear, the pack was never the whole story. |
||
|
|
ddcfd39dab |
GS: print the pack's own filenames, instead of asking the player for them
The previous diagnostic could say "the pack holds this TEX0 under a different
palette" but not which, and the difference is the entire diagnosis:
<tex0>-<clut>-<w>x<h>-<bits>.ext a real, different palette hash -- the palette
contents differ at run time from dump time
<tex0>-<w>x<h>-<bits>.ext no palette field, so it indexed with
CLUTHash=0 and can only match under paltex
Answering that meant asking someone to list a directory on their own device. The
people who hit this are players; that is not a reasonable thing to ask, and it is
information the emulator already has in memory.
So print it: up to two matching entries per miss, with their CLUT hash and
filename, plus three sample names at map load so the pack's naming style is on
the record even when nothing misses.
Bounded the same way as the rest of this logging -- at most eight misses reported,
at most two entries each -- because it walks the filename map, and this game's
pack has 8339 entries in it.
|
||
|
|
0fe5715336 |
GS: make the replacement diagnostics able to answer the question
The first version could not. LookupReplacementTexture runs once per NEWLY HASHED
texture, not per draw, so a whole session can be a few hundred calls -- and a
report threshold of 20k therefore printed once, at the very first lookup, and
never again. Two tester logs came back reading "0 hits, 1 misses" after 139 and
195 seconds of play, which cannot distinguish one lookup from twenty thousand.
That ambiguity was the whole answer they were supposed to give.
Three changes:
- Report on a geometric schedule (1, 2, 4, 8 ... then every 4096) rather than a
fixed threshold. Bounded whatever the rate, and dense at the start, which is
where the answer usually is.
- Print the first eight misses in full: the TEX0 hash, CLUT hash, dimensions
and PSM actually asked for, plus whether the pack holds that same TEX0 under
a DIFFERENT CLUT hash. That is the difference between "the pack does not have
this texture" and "it has it, under another palette hash" -- and the latter is
the usual answer for paletted UI art, which is what a Persona 3 FES font and
menu panel are.
- Log preloading, paltex, async and upscale once at map load. Those decide
whether a lookup is ever ATTEMPTED rather than whether it matches; a pack that
indexes thousands of files and is then never consulted looks identical from
outside to one that misses every time.
|
||
|
|
b5b2ed24e1 |
GS: say whether replacement lookups are hitting, not just how many indexed
"My texture pack does nothing" has two failure stages and the log only covered the first. The indexed count proves the FILES were found and their names parsed; it says nothing about whether any draw ever asks for one. A pack that indexes thousands of textures and misses every lookup is a hash problem -- wrong dump settings, paltex vs CLUT, wrong upscale -- and from outside it looks exactly like a pack that never loaded at all. Both read as "the mods are not applying". Count hits and misses in LookupReplacementTexture and summarise one line per 20k lookups. Summarised rather than logged per lookup because this runs per draw, and log volume on its own is enough to stall the emulator. Also counts how many misses WOULD have matched with the CLUT hash zeroed. That separates "the pack does not contain this texture" from "it does, but the palette hash differs", which is the usual answer for paletted UI art -- fonts and menu panels, the exact things a Persona 3 FES mod replaces. Counters reset in ReloadReplacementMap: carried over from a previous game, a stale hit count reads as a healthy pack. Also -Wno-missing-braces on the savers target. Welsh's sources initialise nested aggregates without inner braces throughout, which -Wall diagnoses once per site per TU: ~5 million lines and an 842 MB build log, which is how it was noticed. Upstream code we do not restyle, so the diagnostic has nothing to tell us. Purely a diagnostic flag; no codegen change. |
||
|
|
81d0a3e510 |
Library: five more screensaver backgrounds, from ARMSX3
ARMSX2 had Flurry alone. This brings over the Really Slick Screensavers tree
ARMSX3 already runs -- Flux, Plasma, SolarWinds, Hyperspace, Lattice and
Skyrocket -- as one libsavers.so beside libflurry.so.
Terry Welsh's savers, GPL-2.0-OR-LATER, which this tree may take under the "or
later" clause. Nothing here comes from rss-glx: that Linux port is GPL-2.0-only
and cannot go into ARMSX2 at all, which is why Lattice and Skyrocket were
hand-ported from the Windows sources upstream rather than taken from the neutral
versions rss-glx already has. The per-file header is what decides this, not the
repository's LICENSE file; they disagree.
The saver sources build with RS_XSCREENSAVER, selecting their platform-neutral
path, against the GLES2 shim in savers/compat and savers/gl1.c rather than being
rewritten -- so they stay re-pullable and recognisably his code.
Four things the tree carries that are not obvious from the sources:
- The GL thread takes a 16MB stack. Skyrocket's World constructor declares a
1024x1024x3 starmap as a local, and a default stack is a SIGSEGV inside
memset before the first frame draws.
- Each saver is compiled through a *_unit.cpp that wraps it in a namespace.
They all declare draw/idleProc/cleanUp/readyToDraw, because each was built as
its own executable, and two in one .so collide at link. The shared libraries
must be pre-included OUTSIDE the namespace or the declarations never match
their definitions -- which compiles clean and fails at link.
- The lifecycle is serialised by a mutex and a generation token: gl1 keeps its
state in one global, so a view may only free the run it started. Without it
the first selection works and every later one is black.
- gl1_frame_begin() masks alpha off after the first frames. These savers fade
the previous frame rather than clearing, and that fade writes destination
alpha, dragging a composited surface transparent -- which reaches the screen
as full-screen TV static.
Hyperspace runs with dShaders = 0 (its ARB shader path needs objects GLES2 does
not have; upstream exposes this as -shaders 0 and falls back to it itself), and
Skyrocket with dSound = 0 (upstream drives OpenAL and bakes ~7MB of samples into
headers; soundEngine.h is a stub here). Hyperspace and Skyrocket ship no presets
upstream, so neither shows a preset picker -- invented ranges are what made
earlier ports fail to start.
SaverGlView replaces FlurryGlView, which it supersedes: it hosts Flurry and the
Really Slick savers behind one SaverSpec, and keeping both would have declared
FlurryNative twice in one package. Helios is deliberately absent -- it rendered
but animated wrongly and the cause was never found.
|
||
|
|
c3f859270f |
Credit the PR contributors in the code, not only the release notes
Merging preserves authorship in git history, but nobody reads git log to find out who wrote a file. The tree already had the convention -- "Feature contributed by misantronic (PR #391)" on exportRecentGamesPublic -- it just was not applied to the three PRs merged here. MemoryCardBackup.kt bmdhacks (PR #608) PerGameOverrides.{h,cpp} bmdhacks (PR #593) RecentGamesContentProvider.kt misantronic (PR #566) The provider's line also records that the opt-in gate was added on merge, so the gate is not mistaken for part of the contribution. |
||
|
|
416eb4c668 |
Library sharing: make the recent-games provider opt-in
RecentGamesContentProvider (PR #566) ships android:exported="true" with no android:permission, no readPermission, and no caller check in query(). That is required for the feature to work -- a signature-level permission would only admit apps we sign, and the companion app it exists for is third-party -- but as merged it means every app on the device, holding no permissions at all, can read the recently-played list: titles, serials, last-played times, and the file URIs, which carry the user's folder layout and frequently their real name. No prompt, no way to turn it off. Gate query() on a preference that defaults to OFF, and surface it in App settings, matching how the second-screen panel and Discord presence are handled: anything that exposes data outside the app is the user's decision and starts disabled. The flag lives in the same "ARMSX2" SharedPreferences file the provider already reads for the library itself, so the toggle and the gate are one value rather than two that can drift. Written with commit() and not apply(): the reader is a different process and can be queried the moment the switch returns, and apply() only promises the in-memory value. Returns an empty cursor rather than null when sharing is off -- null is the failure signal a ContentResolver caller has to special-case, and "the user has not enabled this" is a legitimate answer rather than an error. |
||
|
|
88d95a83f5 | Merge branch 'pr-566' into jit-android-catchup-gv7 | ||
|
|
ff2b0c2155 | Merge branch 'pr-593' into jit-android-catchup-gv7 | ||
|
|
2c36725e11 | Merge branch 'pr-608' into jit-android-catchup-gv7 | ||
|
|
9262ea0480 | Merge remote-tracking branch 'origin/master' into jit-android-catchup-gv7 | ||
|
|
19335b2e54 |
LSFG: adaptive pacing captured the wrong target rate
Turning adaptive pacing on silently turned frame generation off: the OSD read FPS 60 / LSFG 60 on a 120Hz panel. The target was captured from Display.getRefreshRate(), which reports the rate the app's window is being driven at, not what the panel can do. Android leaves a window at 60 until something asks for more, so the stored target became 60 -- and the pacer then correctly asked for zero interpolated frames, since a 60fps game at a 60Hz target already satisfies it (desired_outputs = interval * target = 1.0, generations = outputs - 1 = 0). Derive from the highest supported mode at the CURRENT resolution instead, which is what the switch always claimed to mean. Same resolution filter used elsewhere, so the target can never imply a mode switch of its own. Existing installs need the switch toggled off and on to re-capture: the value is stored, not recomputed. Also swap the credit-card glyph on the library's Memory Cards row. |
||
|
|
d9b961eb36 |
Community batch: seven feature requests
Second screen (BrainBeat, NiceRon): - The panel picked its display by "not DEFAULT_DISPLAY", so launching ARMSX2 on the second panel put the panel on top of the running game. Anchor on the display the activity is actually on, and re-pick it on every resume rather than only on a foreground change. - Restyle the panel: it inherits the system dialog theme, not the app's, which is why it looked like a stock Android dialog. Dark ground, rounded tiles, one accent, painted in code since a Presentation is outside the Compose tree. - Customisable grid: SecondScreenTiles declares the tiles (stats, actions, macros, achievements, hide-panel), SecondScreenLayout stores which and in what order, and App settings edits both plus the column count. Stored by tile id, not ordinal. Achievement tile shows collection progress plus whatever unlocked this session -- the snapshot carries no timestamp, so "recent" is the locked-to-unlocked edge on the panel's own tick. Library (Isshin, GBSUPREMO): - Cover region per game, overriding the library-wide choice; "Library" is the absence of a pin, not a fifth region. Both cover components now subscribe to the region state -- game.coverUrl resolves it inside a plain getter, which Compose could not see, so cards kept their old art. - Memory cards reachable from the long-press menu. The card picker already did per-game assignment whenever handed a game; only the in-game menu ever handed it one. In-game (Sizor, Grayy): - Quick menu docks left or right. Alignment, slide direction, rounded corners and inset all move together. - Stats position (four corners), driving PCSX2's own OsdPerformancePos. Stored as the core's enum ordinal so there is no translation table to keep in sync. - Cycle-display-refresh hotkey, using preferredDisplayModeId -- the frame-rate vote in EmulationSurface is a hint the compositor may ignore, which is right for latency and useless as a user-facing toggle. - Analog Sticks section in the quick menu, extracted from PadTab the same way Gyro and Macros already were. Also drop the -fexceptions/no-PCH carve-out on GSLsfg.cpp: it existed because lsfg-vk-android reported failure by throwing, and the Eden port has no throw sites. |
||
|
|
b8f80aee02 |
Fix Reset leaving per-game settings, and Fast Forward (Toggle) on analog triggers
Two reported bugs, unrelated to each other. RESET (takanome9104, confirmed by lugnel): per-game settings such as affinity and GS multithreading survived a full reset. purgeAllSettingsFiles deleted PCSX2-Android.ini and gamesettings/ from currentInitDataRoot — one root. A device with a configured system directory has TWO, and on a device that has been moved between them gamesettings/ exists under both; the surviving copy is re-read on the next launch. Verified on the test device: gamesettings/ present under BOTH the SD root and app-private storage. Every known root is purged now. Deleting a file that is already gone is free, so casting wide costs nothing. This is the same single-root assumption that hid save states from the library's long-press menu earlier today. Worth suspecting wherever this codebase resolves 'the' data directory. The prefs clear also moved from apply() to commit(). restartApp calls Runtime.exit(0) on the next line, and apply() only guarantees the in-memory update — its disk write is asynchronous and an abrupt exit can beat it. A reset that survives the restart is the entire point of the button. FAST FORWARD (SKrazy on an AYN pad, Shmoda12 on a Thor): Fast Forward (Toggle) bound to L2/R2 came on for a frame and then reported OFF. It worked as Hold, it worked on a non-trigger button like R3, and it worked once the pad was switched to digital triggers. Those three facts together say it: some pads report a trigger BOTH as an axis and as a key event, so one pull reaches the hotkey dispatcher twice — once from sendTrigger, once from the key path. For a HOLD that is harmless, since both compute the same state from the same edge. For a TOGGLE the first flips it on and the second immediately flips it back. Digital triggers send only key events, which is why that setting 'fixed' it. The axis path now claims the press and the key path skips its own edge. Scoped to L2/R2 alone so nothing else changes, and cleared on release so the next pull re-arms. sendTrigger already carried a comment about pads that report triggers both ways — the hold path had been made safe against them, the hotkey path had not. |
||
|
|
cc9e58cf4e |
Library: port the Flurry animated background from ARMSX3
Calum Robinson's Flurry screensaver (2002, BSD-3-clause) as a live library backdrop, already shipping in ARMSX3. Source port, not the Windows .scr — every upstream copyright header is intact. Its own SHARED target rather than folded into the core, for two reasons: it is BSD next to GPL and that boundary should be visible, and it is plain C from 2002 that wants none of the C++20 the emulator is built with. gl_compat.c answers the GL 1.x calls the sources make — client-side vertex arrays, a fixed-function ortho, GL_QUADS — with a GLES2 shader, so the renderer stays unmodified. ★ The integration differs from the brief, which surveyed the refresh-experimental checkout. That tree has ui/GamesList.kt and no background system, so it needed a new full-bleed host. This one already has one: Flurry slots in beside XmbGlView under the same libraryBg == null branch and inherits its fallback — if GL cannot come up we get LibraryWaveBackground rather than a hole. onRelease stops the render thread, without which the EGL thread outlives the composition and keeps drawing to a dead surface. ★ add_dependencies had to move BELOW the emucore target. Stated next to the flurry target — which is defined earlier in the file — configure fails outright, because add_dependencies requires a target that already exists. The dependency itself is required: libflurry.so is loaded by System.loadLibrary and never linked, so nothing else in the build would make it, and a missing .so stays invisible until someone switches the background on and the view throws UnsatisfiedLinkError. Off by default, and said plainly in the description. It is a particle simulation rather than a still, and this library shipped a looping video once and lost it in 2.5.9 when the continuous decode turned out to cost real performance. Preset picker included, with random as the default choice. A UI preference, so prefs rather than the twelve-site Settings.kt path — it does not touch the emulator config. Verified: libflurry.so is packaged and exports all six JNI entry points. NOT yet run — Flurry's frame cost has never been measured on either project, and Water spawns nine flurries, so that number is still owed before it is recommended. |
||
|
|
8c0ae12398 |
Patch: stop Hardcore blocking presentation patches
Reported by EddyOP (60 FPS and Widescreen disabled under RetroAchievements
Hardcore) and diagnosed by Jetup, who found that moving the same lines under a
widescreen heading re-enabled them — ARMSX2 issue #541.
The Hardcore gate from
|
||
|
|
66aeaaeb96 |
Patches: cache the repository trees on disk
The other half of the online-browser complaint. Stopping the scan when you leave addressed the heat; this addresses the minutes. Every search downloads four GitHub '?recursive=1' listings — multi-megabyte JSON for repositories holding tens of thousands of pnach files — and regex-scans each for paths. The existing caches are in-memory only, so the first search after every launch paid the full price again, which is why it reads as broken rather than slow. The EXTRACTED PATH LIST is what gets cached, not the JSON: a fraction of the size, and coming back in it skips the expensive regex entirely, which is the CPU half of the cost rather than the network half. Seven-day TTL. These repositories gain files occasionally and the cost of being stale is one newly-added cheat not appearing, against re-downloading megabytes on every cold start. Keyed by a hash of the tree URL rather than a sanitised name, since two repositories can differ only in characters a filesystem folds. The cache directory is handed in rather than discovered: PatchRepo is a context-less object, so until setCacheDir runs it stays memory-only and behaves exactly as before. |
||
|
|
48094495a0 |
Patches: stop the online scan when you leave the browser
Reported by SNAKEATEROP (Helio G99): after using the online cheats/patches
browser, going back to the game left the device heating severely and a game that
had held full speed no longer did. Nothing in the emulator explained it.
The scan was UNSTOPPABLE, not merely slow. PatchRepo's fetch functions were
plain blocking calls with no isActive check, no ensureActive and not even
suspend. Kotlin cancellation is cooperative, so cancelling the scope did
nothing: the work ran to completion no matter what the user did. It walks four
community repositories, each a multi-megabyte GitHub tree that is downloaded and
then regex-scanned for paths — that is the CPU the game was competing with, and
it kept going long after anyone was looking at it.
Three parts:
· PatchRepo's entry points are suspend and check for cancellation between
every repository and every file. Between SOURCES is the one that matters —
that is where the time goes.
· The scan's Job is tracked, so a second search cannot stack on the first, and
the browser cancels it in onDispose. viewModelScope alone was not enough:
the ViewModel is Activity-scoped and shared with the settings tab, so it
does not clear merely because the user went back to the game — which is
exactly the case that was reported.
· The progress text now says it takes a minute or two AND that leaving is
safe. Users assumed it had hung, and several were told to just wait; nobody
should have to sit through it to protect their device.
This does not make the scan faster. It makes it stop, which is the part that was
damaging. Caching the repository trees on disk is the fix for the duration —
they are re-downloaded and re-parsed on every cold start today — and is worth
doing next.
|
||
|
|
879d07209c |
GS: size the texture cache as RAM minus a reserve, not a fraction of it
Third attempt at this budget, so the reasoning is written down properly.
Uncapped OOM-killed Android on a 5 GB uncompressed Persona 3 FES pack. Capping
at RAM/4, then RAM/2, stopped that and broke the same pack on 8 GB devices where
it had been working — 5 GB against a 4 GB budget evicts continuously, each load
dropping the previous one. That is the Persona 3 FES report: corruption first
(a failed upload injected with undefined contents, before
|
||
|
|
7bbe5b2fc1 |
GS: remove the texture-replacement cache cap
Reverses the 2026-07-20 policy. Reported by JustVibin247 for Persona 3 FES: mods worked on 2.6.6 and stopped on 2.6.6.1, first showing as corruption and later as simply not applying. The cap was added to stop a 5 GB uncompressed Persona 3 FES pack OOM-killing Android mid-load, and it did stop that. It also broke every setup where an oversized pack had been working. Budget was RAM/2, so on an 8 GB device that same 5 GB pack sat permanently about 1 GB over and evicted continuously — each load immediately dropping the previous one. That produced both reported symptoms in the order they were reported. The churn means constant re-upload; before |
||
|
|
aba5f201e6 |
Saves: hiding Save must not hide Load
Save and Load shared a single guard, so gating Save on a running VM hid Load with it — and Load is the entire reason the library's long-press menu opens this screen. They have different conditions and now have different guards. Load needs only that the state belongs to the game in context: with no VM it boots the game and loads into it. Save additionally needs a live VM, because there is nothing to snapshot without one. |
||
|
|
2443ad72cf |
Saves: only offer Save when a VM is actually running
Opened from the library's long-press menu the Save Manager showed a Save button next to Load, which cannot mean anything: nothing is booted, so there is no running game to snapshot. The button was gated on canUseWithActiveGame, which means 'this state belongs to the game in context' — a different question, and one that only started answering true here when contextGame was wired up in the previous commit. Saving needs a live VM; loading does not, because it can boot the game first. The two conditions were conflated and the fallback fix exposed it. Now gated on hasActiveVm, read from NativeApp.hasActiveVM() during refresh. Load and Delete are untouched: both are meaningful without a running game. |
||
|
|
208bf68f1b |
Saves: honour contextGame when listing, not just when launching
Long-press -> Load save state opened the Save Manager but nothing could be loaded from it. load() already fell back to contextGame; the LISTING did not. It read currentGame alone, which is null when nothing is booted — so every entry came back canUseWithActiveGame = false, which is what greys out Load, and the serial filter also stopped applying so the screen showed every game's saves at once rather than the one that was long-pressed. contextGame was added for exactly this case and the launch path already used it. The read here had simply never been updated, because until now the only way into this screen was from a running game or the drawer. importSaveStateToNextFreeSlot deliberately still requires currentGame: it resolves destination paths through NativeApp.getGamePathSlot, which answers for the running VM, so a context game would give it nowhere to write. |
||
|
|
146da3d2ef |
Library: open the real Save Manager, and make swipe-to-dismiss actually work
Both from testing feedback. The save-state feature itself worked — slots listed and booting into one loaded correctly — but two things around it did not. ★ The swipe did nothing, and the reason is worth writing down: Compose delivers pointer events to CHILDREN first. Every row in the sheet is clickable, so they consumed the drag in the Main pass and a detector on the parent Box never saw it. Watching PointerEventPass.Initial is the only way a parent wins that. Winning it everywhere would be worse than not having it — it would eat scrolling inside any modal that scrolls — so the gesture is claimed only when it STARTS in the top 64dp, where the drag handle is and where a sheet is grabbed anyway, and only once it has clearly travelled downward. The absorb-taps clickable also moved AFTER the detector; having it first gave it the events. This mattered more than a missing nicety: the game menu is nearly full-height, so there is almost no scrim left to tap, and without the swipe the only way out was the controller. A touch user was stuck. ★ 'Load save state' now opens the Save Manager rather than a bespoke list. That was the request, and it is also the better implementation: the Save Manager already renders slots as a grid with preview thumbnails and carries its own back button, so it cannot trap anyone. contextGame exists for precisely this — it is how the Save Manager already operates on a game that is not currently running — so this is wiring, not new UI. The bespoke picker and its modal are deleted. SaveSlotLookup stays: the menu still needs to know whether a game has any states at all, to decide whether to show the row. |
||
|
|
caaf80749e |
Library: find save states in both data roots, and restore swipe-to-dismiss
Two follow-ups from testing the long-press menu. ★ SaveSlotLookup only searched ONE root. A device with a configured system directory has two — assetCopyRoot resolves to that one (typically the SD card, where ROMs and most saves live) while others stay under getExternalFilesDir. On the test device 13 states sat in one and 5 in the other, so either root alone under-reports, and the failure is silent: it reads as 'this game has no save states' rather than as a bug. This is the same two-root trap that once made a patches investigation report a false 'clean'. Both are searched now, and when a slot exists in both the newer file wins. Swipe-to-dismiss is back for bottom-aligned modals. PadModal replaced ModalBottomSheet because that is its own focused Android window and every row inside it was unreachable by pad; the swipe was the one thing given up in the trade. But a panel that rises from the bottom edge with a rounded top and a drag handle is PROMISING a swipe, so its absence reads as broken rather than as a deliberate omission. The panel now follows the finger and dismisses past a threshold, without giving up focus ownership. Downward only, and only for BottomCenter: dragging a bottom sheet up should not lift it off the edge it is anchored to, and on centred or anchored menus a vertical drag means nothing and would fight scrolling inside them. Worth recording that the originally reported symptom was NOT a bug: God of War II has no save states on the test device, so an absent row was correct. The root bug was real but found by inspection while checking that. |
||
|
|
0f0f719bce |
Library: fix the selection highlight contrast, and offer save states on long-press
bmdhacks' two. Selection highlight was blue on blue. It drew a single ring in the theme's primary, and the library background is themed from the same palette — so on a blue theme the highlight was invisible, which matters because controller navigation is the only way that selection is moved. Now two rings: an outer one derived from inverseSurface, which contrasts with the background whatever hue the user picked, and the accent ring inside it. Whichever the background happens to match, the other still reads. The list rows had the same problem and get a thicker stroke blended toward inverseSurface. Long-press already opened the game menu; it now offers the game's save states and boots straight into one. Most of that already existed and only needed connecting: pendingSlotLoadOnBoot and launchCurrentGameFromSaveSlot have been driving the Save Manager's 'relaunch and load' for a while. The one thing that did not work from the library is that it resolved the game from currentGame/contextGame, and in the library nothing is booted so both are null — it could never have fired there. Split into launchGameFromSaveSlot(game, slot) which names the game explicitly. Enumerating the slots needed new code for the same reason: the in-game picker asks NativeApp.getGamePathSlot, which resolves against the RUNNING VM's serial. SaveSlotLookup reads the files instead, using the layout the save manager already walks — '<serial> (title).NN.p2s' under sstates/ or savestates/. The row only appears when states exist. A 'Load save state' entry that opens onto an empty list is worse than no entry. The picker is a second PadModal rather than a submenu inside the game menu: PadModal owns focus, so nesting one inside another leaves the inner rows unreachable by controller — the same trap that made the game menu a PadModal instead of a ModalBottomSheet in the first place. Both flavours compile. Not yet exercised on device. |
||
|
|
f6ddff79bc |
LSFG: expose adaptive frame pacing, and remove the diagnostic tracing
The pacer shipped in the previous commit but was inert: GSConfig.LsfgTargetRate defaulted to 0, which means "hold the multiplier fixed", and nothing in the Android settings could change it. So the port ran, but the specific problem it was brought over to solve — games that oscillate between 60 and 30fps on a 60Hz panel, where a fixed multiplier presents 120 then 60 and judders at every transition — was still there. Working but inert is the failure mode worth naming: nothing errors, the feature simply does not do the thing it was for. Plumbed through the usual twelve places (the field, INI read and write, the differs chain, toJson/fromJson, both per-game override paths, the reset list, the search index, the strings, and the two call sites), plus the two C++ ones in Pcsx2Config. Presented as a switch rather than a number. The pacer needs a concrete Hz, but picking one by hand is not a decision anyone can make usefully and the only sensible answer is the panel's own refresh rate — so the UI writes that when the toggle goes on, and 0 when it goes off. Off remains the default, so behaviour is unchanged until it is asked for. Also removes the step tracing added while chasing the Turnip crash. It did its job: five rounds of reading the code produced three wrong theories, and the trace produced the answer in two. The reasoning it uncovered is in the comments, which is where it belongs — the instrumentation is not. The new strings live in the github-only table, so the Play split still holds: playDebug has zero class files containing 'Lossless' or 'perf.lsfg', githubDebug has 2 and 4. |
||
|
|
2080bd1c44 |
LSFG: make frame generation actually run on device
Verified working on an Adreno 740, on BOTH the stock Qualcomm driver and Turnip:
an interpolated frame presented for every rendered frame, no crash.
Six real defects between 'compiles' and 'runs', all mine. Recording them because
every one of them compiled cleanly and several looked like somebody else's bug.
★ THE LAST ONE, and the least guessable. Waiting on a semaphore signalled by a
SECOND vkAcquireNextImageKHR within one frame segfaults inside Turnip at
vkQueueSubmit. Stock Qualcomm accepts it; Turnip does not. The extra acquire now
signals a FENCE which we block on before recording, so the submit waits only on
the caller's render-finished semaphore — exactly what a non-generating frame
does, and that shape always worked. It costs a short CPU stall per generated
frame, still far cheaper than the two full device idles per frame the old
implementation paid.
The tell was in the trace, not in the code: working frames submitted
waits=1 signals=1, the frame that died submitted waits=2 signals=2. Everything
else about that frame — the dispatch, the copy, the fences, the presents — was
identical.
The other five:
· __fi on a free function in a header. PCSX2's __forceinline is
__attribute__((always_inline, unused)) with NO inline keyword, so every
including TU emitted its own copy: duplicate symbol at link, from a header
that compiles perfectly alone. Pcsx2Defs.h provides __forceinline_odr for
exactly this and the rest of the renderer only uses __fi inside class
bodies, where members are implicitly inline.
· Acquire budget. Vulkan allows imageCount - minImageCount + 1 images held at
once and the presented frame already holds one, so with min=3 and 3 images
the budget was ZERO. Acquiring anyway is undefined behaviour, not a failed
call. GetImageCount() - 1 was simply the wrong bound.
· Swap chain image count. Asking for base + 1 does nothing: on FIFO the base
is 2, so it clamps straight back up to minImageCount. The request has to be
anchored to minImageCount, or the budget stays zero and frame generation
silently never runs with nothing reporting an error anywhere.
· One command buffer, one semaphore set, no fence. Resetting a buffer that is
still executing and resubmitting one that is still pending are both
undefined. The OLD implementation had the same single-slot arrangement and
got away with it because it called vkQueueWaitIdle twice a frame — removing
those idles is the entire point of this port, and it removed the accidental
serialisation that made reuse legal. Now one slot per swap chain image, each
with its own fence.
· Initialisation order. Moving image allocation into CreateResources without
moving the allocator ahead of it dereferenced an empty std::optional and
killed the GS thread during BIOS boot — before frame generation would ever
have produced a frame, so it presented as an entirely unrelated crash.
Generated frames also now go into images WE own and are copied into the acquired
swap chain image, rather than being dispatched straight into it through a
storage view. That theory did NOT fix the crash — but it is what both Eden and
the old implementation do, it asks nothing unusual of the WSI, and it let the
swap chain drop VK_IMAGE_USAGE_STORAGE_BIT entirely, which removed the
'enable it, then restart the renderer' wart along with it.
Diagnosis was step-tracing the present path, not reading it: five rounds of
reading produced three wrong theories, and the trace produced the answer in two.
The instrumentation is removed; the reasoning is in the comments.
|
||
|
|
0bfefd4b69 |
LSFG: run frame generation on our own device, and delete the old path
Completes the switch to the Eden port. GSLsfg keeps its entire public surface — availability, status text, display FPS, the settings and OSD plumbing all untouched — and only its internals change, so nothing above the renderer had to move. What actually changed on screen: the old implementation ran the interpolator on a SECOND VkDevice and shared images as AHardwareBuffers, and because Android offers no cross-device semaphore (Turnip rejects OPAQUE_FD export on AHB memory) the only barrier available was a full device idle — twice per frame, every frame. That is gone. Generation is now ordinary compute recorded into a command buffer on the device we already have, and interpolated frames are written STRAIGHT into an acquired swap chain image through a storage view, so the intermediate copy is gone too. The pacer comes with it, which is the fix for games that oscillate between 60 and 30fps on a 60Hz panel: the generation count now varies to hold the presented rate near a target instead of blindly multiplying whatever the game produced. ★ ONE submit, N+1 semaphores. All the generation work goes into a single command buffer, submitted once, waiting on the caller's render-finished semaphore plus every acquire, and signalling one semaphore per present that follows. The obvious alternative — a submit per generated frame — walks straight back into the binary-semaphore bug this file was bitten by before, where the real present and the first generated present both want to wait on the semaphore that says the source has been read. A binary semaphore may be waited exactly once. ★ The hook fires AFTER vkQueueSubmit, so FrameGen had to take its command buffer as a parameter. It was written against GSDeviceVK::GetCurrentCommandBuffer(), which at that point is in flight or already belongs to the next frame; recording into it is undefined and the symptom would have been interpolation running a frame late rather than anything resembling an error. Layout bracketing is ours: the ported passes speak Eden's convention where a presentable image lives in GENERAL, and PCSX2 hands them over in PRESENT_SRC_KHR and needs them back in it. The swap chain now requests VK_IMAGE_USAGE_STORAGE_BIT — but only when frame generation is on AND both the surface and the chosen format allow it. Asking unconditionally fails swap chain creation outright on drivers that do not, which would take the whole renderer down for a feature that is switched off. The format half is the easy one to miss: a surface can report STORAGE support while the sRGB format picked for it has no STORAGE_IMAGE feature bit, and that only shows up later as a validation error at image-view creation. Because usage is fixed at creation, switching the feature on mid-session needs a renderer restart; Initialize says so rather than failing silently. DELETED: platforms/android/app/src/main/cpp/3rdparty/lsfg in full — the lsfg-vk-android framegen library, the DXVK dxbc compiler, pe-parse, volk and its 759-symbol collision with VKLoader, the C ABI shim, the version script, the separate .so and the dlopen that found it, and the -fexceptions carve-out they needed. GSLsfg.cpp went from 1259 lines to 654. The ~130 MB configure-time fetch goes with it. build-play-aab.sh's guard was rewritten rather than dropped: it checked for a file that can no longer exist either way, so it would have passed forever without proving anything. It now looks inside the core for a symbol only the ported implementation defines. Verified: all 18 affected translation units compile without errors, with ARMSX2_HAS_LSFG on AND off (the play flavour still compiles the feature out entirely). Not yet run on hardware. |
||
|
|
5e1d979b4e |
LSFG: port Eden's frame generation (passes, pacer, DLL reader)
Ports the frame-generation implementation from Eden (eden-emu PR #4263), which is a substantially better design than the lsfg-vk-android one we currently ship. Why it is better, concretely. Ours runs framegen on its OWN VkDevice, shares images through AHardwareBuffer, and — because Android gives no cross-device semaphore, Turnip rejecting OPAQUE_FD on AHB memory — uses full device idles as its only barrier. Eden's runs as ordinary compute on the device we already have. It also needs none of what ours drags in: no DXVK dxbc compiler (its shader translate is a SPIR-V validate plus a descriptor-binding renumber, because current Lossless.dll ships SPIR-V in its RCDATA resources), no pe-parse, no volk and its 759-symbol collision with VKLoader, no separate .so, no C ABI, no dlopen, and no -fexceptions carve-out. It also brings a real frame PACER, which is the answer to games that oscillate between 60 and 30fps on a 60Hz panel. A fixed multiplier presents 120 then 60 there and judders at every transition; the pacer varies the generation count to hold the OUTPUT near a target instead. New GSConfig.LsfgTargetRate drives it, defaulting to 0 = the existing fixed-multiplier behaviour, so this is opt-in. Nothing is wired up yet — GSLsfg still drives the old path. This commit is the ported library only. ★ The load-bearing decision is LsfgVkCompat. The pass code is written against yuzu's RAII wrapper and its Device/MemoryAllocator, which PCSX2 has no analogue for. Rather than rewrite ~2000 lines of call sites, the slice of that API the code actually uses is reimplemented over PCSX2's raw handles and VMA — it came to five command-buffer methods, three Device queries, two allocator entry points and eight handle types. The result is that every pass body is BYTE-IDENTICAL to Eden's, so upstream fixes stay a readable diff instead of a merge puzzle. Deliberate departures, each commented at the site: · paths are std::string, not std::filesystem — the GS backend uses neither · CityHash -> GSXXH3_64bits, already used elsewhere in GS · the shader cache gained mtime + a flags field so a hit costs a stat() rather than a full read, hash and PE walk of the DLL on every launch; Eden keys on a content hash and so must read the whole file before it may look at the cache. GSLsfg.cpp already validates on size+mtime, so this matches the tree. · Eden's RemoveInstalledLosslessDll() is NOT ported. It deletes the DLL, which is safe there because Eden owns that file; here the path is whatever GSConfig.LsfgDllPath says and nothing checks it points inside our storage. Only the cache half is kept, as ClearShaderCache(). · vk::Buffer gained Flush(). The port initially dropped Eden's flush because the shim had nothing to flush through. That write is the shader's entire uniform block, and the failure mode is not a crash — it is interpolation reading stale constants, which reads as a motion artefact, not a bug. Verified: all 14 translation units compile clean against the real PCSX2 headers under -Wall -Wextra. The reconstructed util.cpp helpers were diffed against the genuine Eden source fetched from the merge commit — the extracted diff hunks in the working copy are PARTIAL, added lines only, so they were not safe to trust. |
||
|
|
517fa69c4c |
OSD: stop any settings change from wiping the active OSD mode
Changing any setting at all — brightness, a speedhack, a controller binding — made the on-screen display disappear. The OSD has two independent controls that both write the same native flags. The per-stat selection in settings, and the MODE picked from the in-game menu or the hotkey (Full / Minimal / Custom / Off). Settings.applyTo() pushes the per-stat osdShow* flags unconditionally, and applyTo runs on EVERY settings change, so it was overwriting whatever mode was active with the Custom flag set. On most setups the Custom set is mostly off, which is why the symptom reads as the OSD vanishing rather than as it changing. The mode STATE was never lost — InGameOverlay.osdMode still said Full, and the in-game menu still showed Full. Only the native flags had been replaced, so the UI and the screen disagreed and nothing looked wrong from the app's side. Fixed at the applyTo choke point rather than at its five call sites: a re-assert that reapplies the mode when it is anything other than Custom. Custom is left alone deliberately — applyTo has just written exactly what Custom means, and re-applying would be a redundant round trip through the CPU thread. This is the same shape as the boot-time applyStoredOsdMode() and the second-display reapplyOsdMode(), which already restore the mode after something else has pushed flags underneath it. applyTo was the third place that needed it and the only one that had no such guard. |
||
|
|
54d2850295 |
LSFG: keep it out of the Play build entirely, not just switched off
Play builds cannot carry LSFG at all, and they did. The gating was a
BuildConfig.LSFG check inside shared files, which is a weaker claim than it
reads as: the rows were never drawn, and all 22 frame-generation strings still
shipped in the Play dex in plain text — including "Lossless Scaling",
"Lossless.dll" and the requirements dialog naming the product, which is exactly
what a text search over the artifact finds. The native half was already
genuinely compiled out (-DARMSX2_ENABLE_LSFG=OFF); only the Kotlin half looked
like it was.
Moved to source sets, which is the arrangement that actually excludes:
LsfgSection.kt main -> github, with a no-op stub in play
the 22 EN strings -> I18nLsfg.kt, real in github and an EMPTY MAP in play
the 5 search rows -> SettingsSearchLsfg.kt, likewise
LsfgEmulationCard new, so the shared pause-menu file no longer even names
the section's string key (SectionCard became internal)
EN is now BASE_EN + LSFG_EN and the search index BASE + LSFG, so whichever
flavour is in scope supplies its half and no caller knows which build it is in.
Splitting the search rows is a behaviour fix as well: in the play build they were
indexed while the section they pointed at was compiled out, so searching would
offer a result that rendered its own key as its title and led nowhere.
The settings FIELDS stay shared on purpose — identifiers rather than product
names, and an identical config schema across flavours is what lets a config move
between builds without losing data.
Verified on compiled output rather than source: playDebug has zero class files
containing 'Lossless' and zero containing 'perf.lsfg'; githubDebug has 2 and 4.
I18nLsfgKt.class is 3633 bytes in github and 833 in play. build-play-aab.sh now
greps the AAB's dex for both strings and fails the build if either appears, so a
later edit to a shared file cannot quietly undo this.
★ That verification first came back clean for BOTH flavours, which was a false
negative: Xcode's strings(1) parses a .class as a Mach-O fat binary, errors, and
prints nothing — indistinguishable from a pass. LC_ALL=C grep -a is what the
check uses, and what the comment in the script warns about.
|
||
|
|
2a98726692 | Merge remote-tracking branch 'origin/master' into jit-android-catchup-gv7 | ||
|
|
5b790427dd |
LSFG/FSR: remove the debug instrumentation
The per-second LSFG branch counters and the FSR gate line were added to find two specific bugs and both did their job — the counters proved generated frames were reaching the screen uncounted (VK_SUBOPTIMAL_KHR treated as failure), and the gate proved all three FSR conditions passed while a misplaced log made the pass look dead. Neither belongs in a release: one printed every second, the other on every state change. What stays is event-driven and diagnostic in the ordinary sense: LSFG's initialise line, shader-cache hits and misses, load and ABI failures, and one FSR line per output-size change. |
||
|
|
c246b9a03c |
LSFG: let the generated frame wait for a display slot; unhide FSR on renderer=auto
Two bugs of mine, both found on-device with everything else working. ★ The zero-timeout acquire disabled frame generation entirely. Forcing FIFO fixed the MAILBOX discard, and the display rate still equalled the real rate. Under FIFO the presentation engine returns an image at a vblank, so at steady state nothing is ever free INSTANTLY — vkAcquireNextImageKHR with a zero timeout returns VK_NOT_READY every frame, the loop breaks, and every interpolated frame is dropped. Silently, because a dropped generated frame is a legitimate outcome and nothing logs it. The reasoning behind the zero was that an interpolated frame is a bonus not worth stalling for. That is backwards: presenting two frames per rendered frame MEANS waiting for the second display slot. Waiting is the mechanism, not the cost. Now a 50ms bound — six vblanks at 120Hz, so it expires only when something is genuinely wrong, while still keeping a lost surface from wedging the GS thread the way an unbounded wait would. ★ The in-game FSR row was gated on renderer == "vulkan", and the default is "auto". "auto" resolves to Vulkan on Android, so the row was hidden from anyone who had not explicitly pinned the renderer — which is nearly everyone, and was the reporter. Gated on the two backends that genuinely cannot run it instead. |
||
|
|
2f1a74c88a |
LSFG: force FIFO presentation, and surface FSR in the in-game menu
★ Frame generation produced nothing on a MAILBOX swapchain, silently. Reported on an Adreno 740: LSFG logged 'active: 1920x1080 x2 frames, 3.1p', cached its 52 shaders, never logged a single failure — and both the FPS and the LSFG display counters read 59. The interpolator was working perfectly and its output was being thrown away. MAILBOX keeps only the most recent image queued for a given refresh. Presenting an interpolated frame and then the real frame immediately after replaces the interpolated one, so it is generated, costs its full GPU time, and is never displayed. IMMEDIATE discards the same way. Nothing errors anywhere along that path, which is why the only symptom is a display rate identical to the real one. The device landed on MAILBOX because vsync was off — 'Immediate not supported for vsync-disabled, using mailbox'. SelectPresentMode now forces FIFO while frame generation is enabled. Eden reached the same conclusion; their setting text reads 'Forces FIFO presentation while enabled'. Gated on the setting rather than GSLsfg::IsAvailable(), which cannot answer at swapchain-creation time: the DLL path only reaches GSLsfg from EndPresent. Also adds the FSR rows to the in-game GraphicsPane. In full settings FSR sits under Display Effects beside CAS, which is the right shelf for a post-effect and the wrong one for finding it — it is an upscaler, so in the quick menu it goes with the internal-resolution controls, which is where you reach while watching the framerate. Vulkan-only, so it is never a dead toggle on OpenGL. |
||
|
|
eeb3affb13 |
GS: FidelityFX Super Resolution 1 as an output-scaling mode
Adds FSR1 (EASU upscale + RCAS sharpen, two compute passes) to the Vulkan backend, so a game rendered below display size can be upscaled properly instead of bilinear-stretched at present. Slots in beside the existing MetalFX branch in GSRenderer rather than introducing a parallel abstraction: GSUpscaler and the non-pure DoXxx virtuals already occupy that design space, and OpenGL and Metal inherit a false return and need no change. FSR1 is MIT (AMD, 2021) and the tree already ships ffx_a.h and ffx_cas.h under the identical grant, so the headers are vendored verbatim with their licence blocks intact. ★ ffx_a.h is NOT replaced. FSR1 wants the 2021 header, ours is 2019, and the 2019 one has been locally patched for Metal Shading Language (A16, A_MSL, A_MAYBE_UNUSED) with ffx_cas.h depending on those. Swapping it would break the Metal backend. The 2021 copy ships alongside as ffx_a_fsr1.h, used only for GPU-side string substitution. The CPU-side FsrEasuConOffset/FsrRcasCon compile against the existing 2019 header — verified by compiling a probe, not by grepping, because AU1_AF1 and AU1_AH2_AF2 are functions and a grep for a #define reports a false negative. Two shader modules, not two specializations. FSR_EASU_F and FSR_RCAS_F are preprocessor gates deciding which function bodies ffx_fsr1.h emits at all, and specialization constants resolve after preprocessing, so CAS's constant_id trick would produce a shader calling undefined functions. Confirmed distinct: disassembly shows EASU with three OpImageGather and RCAS with none. Both passes push the full 80-byte constant block. With all five uvec4 declared so one layout serves both, Sample decorates to byte offset 64 — pushing the 32 bytes RCAS nominally needs would leave it undefined, and Sample gates a gamma-squaring branch, so garbage there squares the image. Binding 0 is a combined image sampler, unlike CAS's plain sampled image, because EASU uses textureGather. The EASU intermediate stays in GENERAL with explicit compute-to-compute barriers. Layout::ShaderReadOnly targets the FRAGMENT stage and TransitionToLayout early-outs when the layout already matches, so neither of the usual tools makes a compute write visible to a compute read. The barrier also covers frame N+1's EASU write against frame N's RCAS read, since the image is parked across frames. FSR and CAS are alternatives, not a chain: RCAS is itself a sharpener. Selecting FSR hides the CAS rows. Pipeline compilation failure is non-fatal and leaves Features().fsr1 false, matching the CAS path that exists because of an Adreno 650 crash. GSUpscaler::FSR1 is appended, not inserted, since the enum is persisted as an integer. Android clamps to the enum's own maximum rather than the count of options its picker shows — clamping to the picker would have rewritten FSR1 back to Off on every save, because MetalFX occupies value 1 and is never displayed. Verified: build clean, no C++ or Kotlin errors; all three resource files packaged into the APK; FSR code present in the core. NOT verified: anything on a GPU. No visual check, no perf numbers, and in particular no confirmation that textureGather in a compute shader works on the Adreno drivers this targets. |
||
|
|
dfd92a4f31 |
LSFG: persist settings and shaders, report status, add flow scale and 3.1p
Five changes, one of which is a plain bug in what shipped. ★ LSFG settings were never persisted. lsfgEnabled, lsfgMultiplier and lsfgDllPath were absent from toJson/fromJson, and that pair IS the persistence format — ConfigStore stores toJson().toString(). So every choice, including the Lossless.dll the user went and found, was discarded on restart. Added to the round-trip, the per-game override diff/merge, and gsDiffersFrom. Translated SPIR-V is now cached to disk. Extraction used to keep raw DXBC and translate inside the shader callback, so all 26 translations re-ran on the GS thread inside EndPresent after every enable, resize or multiplier change. Now ExtractShaders translates eagerly, drops the DXBC, and writes <cache>/lsfg_shaders.bin. The DLL's size and mtime go in the header and a mismatch re-extracts — ARMSX3's equivalent has no invalidation at all. Frame generation can no longer fail invisibly. GetStatusText() feeds one line to the performance overlay, empty ONLY when the user has not enabled it: unavailable / failed / no shaders / starting / a display rate. That rate counts frames actually PRESENTED, real plus generated, because the acquire loop can break early and assuming the multiplier would overstate it. FPS alone cannot show this — frame generation deliberately does not change the emulator's frame rate, so without a separate line 'working', 'broken' and 'unsupported' are all the same absent line. Flow scale and the 3.1p pipeline are exposed. flowScale is a DIVISOR — framegen computes flowExtent = inputExtent / flowScale — so the UI percentage is passed as clamp(100/percent, 1, 4). ARMSX3 passes percent/100, where only the default is right because 1.0 is its own reciprocal and every lower position makes it slower; that inversion is not copied here. 3.1p is a separate shader family with separate device state, so the shim fixes the choice at initialise and dispatches every entry point on it, and the name table gains the p_* resource IDs. Frames the game did not draw are no longer interpolated. PresentWithGeneration captured unconditionally, so pause menus and boot screens got interpolated at full GPU cost. It now takes frame_has_new_content, sourced from the condition GSRenderer already computes (current && !blank_frame) rather than a new heuristic, and consumed with std::exchange because RenderBlankFrame presents without going through BeginPresent. A false also resets the frame history, so the pair either side of a gap is never stitched into one bogus in-between frame. Verified in the built APK: four status states and lsfg_shaders.bin in the core, 26 p_* names in its table, 3.1p linked into the shim. Build clean, no C++ or Kotlin errors. NOT verified: any of it at runtime — no Adreno 7xx here, so the flow-scale direction, the cache round-trip and the capture gate are reasoned, not observed. |
||
|
|
7fde8ce980 | Merge remote-tracking branch 'origin/master' into jit-android-catchup-gv7 | ||
|
|
c66f28a395 |
LSFG: fix binary semaphore misuse and the unbounded acquire
Three defects in the frame-generation present path, found comparing it against ARMSX3's. 1. Binary semaphores signalled without being waited, and waited twice. The loop reassigned the real present's wait to the last post-copy semaphore. That left s_pre_copy_sem signalled and never waited, so the next frame signalled an already-signalled binary semaphore; and it made the last post-copy semaphore the target of two waits, its own present and the real one, when a binary semaphore's signal can be consumed exactly once. Both are spec violations, and the kind that work until a driver decides otherwise. The fix is to delete the reassignment, not to add semaphores. The real present must wait on s_pre_copy_sem specifically, because the pre-copy reads the real image as TRANSFER_SRC and returns it to PRESENT_SRC — presenting before that lands would present an image still being read. The generated presents are independent: different swapchain images, each gated by its own post-copy. Presents issued on one queue are processed in call order, which is what keeps them on screen ahead of the real frame. Every semaphore is now signalled once and waited once. 2. vkAcquireNextImageKHR with UINT64_MAX, on the present path. Two problems at once. It bypassed the bounded ACQUIRE_TIMEOUT_NS that VKSwapChain::AcquireNextImage deliberately adopted so a surface destroyed under the GS thread — background, rotate, fold — does not leave every thread asleep at 0% CPU with nothing in any log. And the swapchain is 2 or 3 images while this loop holds the real one and asks for multiplier-1 more, so at x3/x4 it serialised on a vblank per generated frame, spending exactly the time the feature exists to save. Now a zero timeout: an interpolated frame is a bonus, so if nothing is free the right move is to drop it and get the real frame out. VK_NOT_READY leaves the semaphore unsignalled, so the slot stays clean for the next frame. 3. The fetch was not actually pinned. LSFG_PIN was "release" — a branch — under a comment explaining that an unpinned fetch would let a remote push change what our core links. Now the commit we have actually built and verified against, with GIT_SHALLOW off because a shallow clone carries only branch tips and cannot resolve a SHA. Also caches the structural PE check. GetUnavailableReason() runs once per frame from EndPresent while the feature is on, and it was doing a full fopen/fread/fseek/fread/fclose on the GS thread every frame; the verdict can only change when the path does, which is where it is now invalidated. |
||
|
|
cafb5e758b |
Android: make the legacy tier actually run on ARMv8.0 cores
The legacy APK claimed Android 8 (minSdk 26) while being compiled
-march=armv8.1-a, which lets clang emit LSE atomics inline. Android 8 means
Cortex-A53/A72/A73 — ARMv8.0, no LSE — so the one tier whose entire purpose is
reach did not reach them.
This is not a theoretical concern. BuildParameters.cmake:145 already records it:
'proven by a casal SIGILL on a real A53 device'. The guard written in response
only applies the safe default when nobody passes an -march, and this script
always passes one, so the tier defeated the protection added for it.
Legacy now builds -march=armv8-a -moutline-atomics, which is exactly what that
comment prescribes. Outline atomics keep LSE on cores that have it via a
runtime HWCAP dispatch, so a modern phone loses nothing.
Verified on the built core rather than assumed. The flags reach 3990 and 2036
compile lines respectively, and disassembling an LSE site shows the dispatch:
bti c
adrp x16, ... ; __aarch64_have_lse_atomics
ldrb w16, [x16, #0xc10]
cbz w16, 0x10b7128 ; no LSE -> fall through to LL/SC
cas w0, w1, [x2]
0x10b7128:
ldxr w0, [x2] / cmp / stxr / cbnz
An A53 takes the branch and never reaches the cas. APK minSdk confirmed 26.
Also moves a11/a13/a15 onto ARMSX3's SDK/NDK pairs and pins all four to NDK 29.
The NDK is not a device-compatibility knob — API level and -march gate devices,
and nothing on the device can tell which toolchain built the binary — so one
toolchain across the matrix is what makes a cross-tier comparison mean anything,
and there is no reason to withhold the measured gain from the weakest tier.
Needs a new armsx2.marchExtra gradle property: -moutline-atomics has to be its
own token, and BuildParameters.cmake's escape hatch keys on CMAKE_CXX_FLAGS
matching '-march='.
Artifact renamed to ARMSX2-<VN>-legacy-armv8.0-sdk26.apk. The updater keys on
the -sdkNN suffix, which is unchanged, so no updater change is needed. The Play
AAB is untouched: build.gradle.kts defaults still say minSdk 26 / NDK 28, and
only the APK script ever passes the tier properties.
|
||
|
|
318588f7a6 |
Android: LSFG frame generation (github flavour only)
Drives Lossless Scaling's interpolation from our own Vulkan present path. Upstream's consumer app captures the screen with MediaProjection and composites over the target process, because Android 12+ forbids injecting code into a non-debuggable app. That constraint is not ours: ARMSX2 owns its swapchain, so it hands the library its own images through the AHardwareBuffer entry points. No screen capture, no overlay, no accessibility service. NOTHING PROPRIETARY SHIPS. The interpolation shaders are read at runtime out of the user's own Lossless.dll, supplied through SAF exactly as a PS2 BIOS is. The requirements dialog says so before the toggle commits, not after it silently fails. Only the MIT-licensed lsfg-vk-android framegen library is fetched; its sibling app carries a no-commercial-use licence and is not. framegen is ISOLATED IN ITS OWN .so BEHIND A C ABI. It links volk, which defines 759 globals named vkCreateImage, vkQueueSubmit and so on -- precisely the names VKLoader.cpp defines. In one library that is a duplicate-symbol error at best; at worst the linker merges them and framegen's volkLoadDevice() call, made against its OWN VkDevice, silently repoints every entry point the GS renderer uses, which would present as a driver crash with nothing pointing back at frame generation. libarmsx2_lsfg.so gives volk its own copies, and nm confirms only the eight armsx2_lsfg_* entry points are exported. The interface is C because the CMake project builds ANDROID_STL=c++_static, so an std::vector crossing that boundary would be two unrelated types sharing a name; errors come back as codes, never exceptions. The shader chain (pe-parse over the PE resources, then upstream's DXBC to SPIR-V translator) stays in the core -- neither half touches Vulkan symbols. GSLsfg.cpp is the one PCSX2 translation unit built with exceptions, because that translator throws and the alternative is std::terminate on exactly the paths a wrong DLL takes. Present path mirrors upstream's Android sequence: copy the rendered frame into shared storage, idle, interpolate, idle, present each generated frame, then the real one. The idles are not laziness -- Turnip rejects OPAQUE_FD on AHB-imported memory, so there is no cross-device semaphore and a device idle is the only barrier that exists. Every failure degrades to an ordinary present rather than taking the GS thread down. Gated on Vulkan + Adreno 7xx and newer, asked of the resolved driver profile rather than a GL_RENDERER substring. The UI reports WHY it is unavailable, since 'needs an Adreno 7xx' and 'you have not picked a DLL yet' are the same greyed row otherwise and only one is actionable. Rows live in All Settings > Performance and the in-game performance tab, from one shared section, wired to each host's own settings tier the same way ShaderChainSection is. Play builds compile the whole thing out -- gradle sets ARMSX2_ENABLE_LSFG=OFF, BuildConfig.LSFG is false, and build-play-aab.sh now fails closed if libarmsx2_lsfg.so ever appears in a bundle. Verified: github APK carries libarmsx2_lsfg.so and 13 live @@ANDROID_LSFG@@ strings in the core; the play variant configures with zero references to either. NOT verified: the present path itself, which needs an Adreno 7xx device, a real Lossless.dll and a running game. |
||
|
|
146bce27de |
Android: four release targets, keyed by minSdk suffix
Splits the ARMv8.2 build into three platform tiers instead of two, so the
Android 11 floor gets FP16 + DotProd as well:
legacy minSdk 26 NDK 28 armv8.1-a
a11 minSdk 30 NDK 28 armv8.2-a+fp16+dotprod
a13 minSdk 33 NDK 28 armv8.2-a+fp16+dotprod
a15 minSdk 35 NDK 29 armv8.2-a+fp16+dotprod
Artifacts are now ARMSX2-<VN>-{legacy-armv8.1-sdk26,a11-armv8.2-sdk30,
a13-armv8.2-sdk33,a15-armv8.2-sdk35}.apk, and the updater classifies on the
-sdkNN suffix alone. The old markers were -v82 and -v82-sdk35, where one was a
substring of the other and only a carefully ordered when-branch kept Android 15
devices off the standard build; that hazard grows with every tier. The four sdk
suffixes cannot overlap.
An asset with no recognised marker still counts as legacy, so releases published
before tiering keep resolving.
The release-shape check now requires all four and verifies each name carries
exactly one, distinct sdk marker, and prints the upload-order warning: every
updater up to 2.6.6.6 takes the first .apk asset in a release regardless of
name, so the legacy build has to go up first or those installs are handed an
APK that SIGILLs on its first hot path.
|
||
|
|
cdb7310a74 | Merge branch 'pr539' into jit-android-catchup-gv7 | ||
|
|
7eb414260a |
Android: community batch — stick sprint button, overlays, second screen, save-state delete
Pressure modifier now applies to buttons that are ALREADY held: the range was only read when a press was emitted, so the gesture these games actually use — hold the button, then ease off — did nothing (MGS2 cancels a shot on a half-pressed Square). Macro turbo holds each state for at least 24ms. The pad is sampled on the VM's own schedule, so the fastest frequencies were emitting presses that fell between two samples and never registered, which read as the turbo being dead. Extra button on the on-screen left stick, for sprint/jump. The stick locks the gesture onto the pointer that started on it, so a separate widget could never be reached by a finger gliding up off the stick; the stick hit-tests the zone itself and keeps emitting deflection, making run-and-sprint one thumb motion. Landscape render position (Center/Top), for foldables and clamshell controllers whose screens open downward. Reuses the vertical-align switch that was gated to portrait. Custom internal resolution as a percentage of native, for steps the presets miss. A value matching no preset also stops displaying as "0.25x" while the GS runs something else. RetroArch overlay artwork: import a pack and draw it between the game frame and the touch controls, so it layers with a shader preset and never covers a button. Second-display panel (Ayn Thor, Retroid dual screen): FPS, battery, clock and buttons for save/load state, fast-forward, pause and screenshot. Battery low and temperature warnings, off the sticky battery broadcast. Delete a save state from the in-game picker by long-pressing its slot. Point the PCSX2 CheatDB source at its current home; the old address is gone. |
||
|
|
156a778cf9 | Android: import external save-state files into a slot | ||
|
|
12a7e37682 | Android: 2D fallback background, portrait status layout, Clear Shader Cache placement, memcard delete wording | ||
|
|
1a45319a6a | Android: fix Auto Progressive Scan hold, and correct Samsung QHD touch offset | ||
|
|
9d735f9ccb | GameDB: no readbacks for Need for Speed Underground 1 & 2 | ||
|
|
036eeea43e | Patch: apply cheats filed under the generic all-CRC name | ||
|
|
e0f39849ce | GS: keep Adreno framebuffer-fetch on by default, gate Snapdragon 8 Elite off | ||
|
|
9762b69bc7 |
Pause music: don't play behind a backgrounded app
Swiping out with the pause menu up left the track playing on the OS home screen. Two paths caused it: backgrounding a running game calls InGameOverlay.open() from onPause, which sets overlayVisible = true and re-fires the pause-music LaunchedEffect — that effect then ran start() after onPause had returned, beginning playback while backgrounded — and more generally nothing stopped a late start() from a background thread (a MediaPlayer with USAGE_MEDIA is not auto-paused by the system). Add a foreground guard: onResume sets it true, onPause sets it false (early, before open() flips the overlay state) and pauses any current playback. start() no-ops whenever it is false, so no effect, resume, or toggle path can begin playback behind a backgrounded app. Coming back, onResume clears the guard and restarts the track if a menu is still up. |
||
|
|
110308e92a |
Pause music: slower ambient track, and fade it in
Swap the bundled pause-menu track for the slower, more ambient edit (res/raw/pause_music.ogg -> .mp3; R.raw.pause_music is unchanged), and ease it in instead of starting at full volume — the abrupt onset was the complaint. start() now begins the player silent and fadeIn() ramps to the set volume over ~1.6s, reading the volume each step so a live slider change tracks. stop() mirrors it with a ~0.5s fade-out then release, so resuming the game isn't a hard cut; ownership of the player is handed to the fade coroutine (player = null) up front and released in a finally, so a reopen mid-fade builds a fresh player, the two cross-fade, and nothing leaks. A manual volume change cancels an in-progress fade so the slider never fights it. |
||
|
|
caa1a808ab |
Pause music: play over the silent game stream, default on
The pause-menu track never played. It deferred to active audio the way LibraryMusic does (to stay out of Spotify's way), but that check is wrong here: on an overlay pause the game keeps its audio DEVICE open and just underruns to silence — pauseForOverlay calls setOutputPauseSuppressed(true) so Android does not reclaim the idle stream and stall the resume (#333). So AudioManager reports the game's own stream as active the whole time the menu is up, even though nothing is audible, and start() deferred forever. Drop the isMusicActive() guard: play a second stream over the silent game one. No audio focus is requested either, so the game's stream and its resume are left untouched. Also default the toggle on — the menu was silent and this fills it; the switch turns it off. The effect's retry loop shrinks accordingly (start now plays immediately; a couple of light retries only cover a transient MediaPlayer prepare hiccup). |
||
|
|
79b17a655d |
Present startup blank frames that carry an OSD message or toast
RetroAchievements toasts (and other OSD) were invisible on first boot with Skip BIOS on, until you opened the pause menu — at which point the toast appeared, and vanished again when you backed out. Cause is the Android startup blank-frame suppression (GSPresentationPolicy). With Skip BIOS on there is no boot animation, so the game shows a black screen with no GS output for a while, and RA posts its "achievements loaded" summary toast into exactly that window. ShouldSkipAndroidBlankFrame returns true for every one of those frames (Vulkan + blank + no current output), so the present is skipped — BeginPresentFrame(true) reports FrameSkipped and EndPresentFrame() never runs, which means neither RenderOSD() nor FullscreenUI::Render() (where notifications are drawn) executes. The toast just sits queued in s_notifications. Opening the pause menu forces real presents so it finally draws; closing it resumes the skip. With BIOS on, the boot animation produces GS output before RA posts, so has_current_output is already true and the toast presents normally — which is why the bug only showed with Skip BIOS. Gate the skip on new ImGuiManager::HasPresentableOverlayContent(): an OSD message (pending or active) or an open FullscreenUI window or a queued toast now forces the blank frame down the normal present path, where EndPresentFrame() draws the overlay over black and presents it — exactly what the pause menu already does during boot. Once the game produces its first frame the suppression is moot anyway (has_current_output true), so this only ever presents the few black boot frames that actually have something to show, and is a no-op when there is nothing queued. |
||
|
|
d2608346c9 |
Add in-game pause menu music
The pause menu was silent, and people sit in it for minutes browsing settings, achievements or memory cards mid-game. New PauseMusic object, deliberately separate from LibraryMusic rather than a second gate on it: LibraryMusic refuses to play unless eState == STOPPED, which is the exact opposite condition, and the two have opposite lifetimes. One player serving both would spend its life fighting the other's start conditions. Driven off WindowImpl.overlayVisible || inGameScreen != null rather than from InGameOverlay.open()/close(), for the same reason as the effects around it: many paths reach each state (back, menu button, hotkey, dismissInGameScreen, a boot that force-closes the overlay) and hooking them one by one always misses one. Including inGameScreen matters because openInGameScreen() closes the menu as it opens Settings, and sitting in Settings is the long silence this exists to fill. Start retries for ~3s like the library track: pausing suspends SPU2 but Oboe takes a moment to actually go idle, and start() politely defers while AudioManager still reports audio active, so a single attempt would lose that race every time. Audio focus is deliberately NOT requested — the game's own audio is already suspended, so there is nothing to duck, and grabbing focus for a menu track would stop whatever the player has going in another app. Toggleable and volume-adjustable in App settings like the library music, off by default (audio starting when you open a menu is startling if you didn't ask for it), and a custom track can be imported the same way. onPause/onResume mirror the library track's handling so it never plays behind a backgrounded app; onResume restarts explicitly because the overlay states don't change while backgrounded, so the effect won't re-fire on its own. |
||
|
|
3c6bb8b26f |
Android: expose every emulated USB device, driven by the existing pad
GunCon 2 alone was half the answer. The core registers eighteen USB devices — Buzz buzzers, a Rock Band drum kit, Keyboardmania, BeatMania, a DJ turntable, the Printer, EyeToy, Gametrak, RealPlay, Train controller, mic, headset, HID keyboard/mouse — and none of them were reachable on Android. The list is enumerated FROM RegisterDevice rather than hardcoded, so it cannot drift from what a given build actually supports, and subtypes (different wheels, different turntables) come along with it. Making them USABLE is the real work, and it did not need a second binding editor. Every InputBindingInfo already declares a generic_mapping (Cross, DPadUp, L1, ...), so on attach native builds GenericInputBinding -> bind_index for the device and applyPadButton forwards each press to the matching bind. The player's existing controls — physical pad, on-screen buttons, macros, anything that funnels through that one chokepoint — drive the device with nothing extra to configure. Bindings with no generic equivalent (Gametrak's axes, the printer) get nothing, which is correct: there is no sensible pad button for them. Aiming stays special-cased, because a pointer is not a button: Lightgun owns it, and UsbDevices.setType now tells it when a port changes so the aim layer cannot stay live over a port that has become a drum kit — otherwise every touch would be swallowed as a shot at a device that is no longer attached. |
||
|
|
c7ef124237 |
Android: GunCon 2 lightgun, and gesture controls in the in-game menu
Lightgun: the core already emulates the device (usb_lightgun::GunCon2Device,
DEVTYPE_GUNCON2). What Android lacked were the three things that feed it, because our
input path is bespoke rather than InputManager/SDL:
* device selection -> USB{n}/Type via USB::SetConfigDevice
* aiming -> InputManager::UpdatePointerAbsolutePosition, which is what
GunCon2State reads through GetPointerAbsolutePosition(0) when
it has no relative binds. Window pixels, and our SurfaceView
spans the window, so touch coordinates pass straight through.
* buttons -> USB::SetDeviceBindValue(port, BID_*, 0/1)
Touch aims continuously rather than only on tap, so you can lead a target before
firing, and the aim is pushed BEFORE the trigger because the core samples the pointer
when the trigger goes down. A touch within 6% of a screen edge fires OFF-SCREEN
instead: that is how these games reload, and without it they are unplayable past the
first magazine. A/B/C, Start, Select and Cal (recalibrate) sit down the right edge,
composed above the aim layer so pressing one is a button press and not a shot;
recalibrate matters because several of these games open with a calibration step.
The aim layer consumes its pointers, unlike the gesture layer — with a gun attached a
touch on empty screen IS the shot, so there is nothing else it could belong to. It
still ignores a DOWN a widget already claimed, so the gun buttons and pause still work.
Device type is restart-required and says so: swapping a USB device on a live VM is the
emulated equivalent of yanking the plug from a port the game has already probed.
Gestures also reachable in-game: swipe distance and the Tap/Hold mode are values you
only find by playing, and walking out to the settings tree to nudge them loses the
moment. The six button assignments stay in All Settings — they are set once, and six
pickers would swamp that pane.
|
||
|
|
3cb1e88029 |
Merge remote-tracking branch 'origin/master' into jit-android-catchup-gv7
# Conflicts: # tests/ctest/core/gs/CMakeLists.txt |
||
|
|
3768d442c7 |
Android: gestures, status cluster, texture-pack fixes, working Controls reset
Gesture control (PPSSPP-style): swipes and a double-tap on empty screen area fire a PS2 button. The double-tap takes a Tap/Hold mode — Tap pulses (NFS nitro), Hold latches until you double-tap again (ARPG camera lock). The layer composes below every widget, rejects a DOWN a control already consumed, and never consumes anything itself, so it cannot swallow a press. Pulses hold 40ms because the emulated pad drops an instant down+up. Clock + battery readout in the library toolbar and the in-game menu header. The glyph drains with charge (green/amber/red) and shows a bolt while charging. Time-remaining appears only while charging: Android has computeChargeTimeRemaining() but no public discharge-time API, and inventing an estimate would be worse than omitting it. Texture packs: - write() did delete()+renameTo(); if the rename failed the whole install record was gone, and runCatching swallowed it while still bumping revision. Now an atomic Files.move with a .bak fallback. This is "packs forget being installed after ~57 downloads", and very likely also "delete does nothing" / "still says Installed". - reconcile() refuses to wipe records when a scan reports zero packs but records exist — that is a failed listFiles(), not sixty simultaneous deletions. - refresh() walked every file of every pack on every open (~250k stats at 60 packs). Sizes now come from an mtime-keyed cache. - The catalogue paged 20 rows at a time with a "Show N more" that names the remainder; it cannot be a LazyColumn inside the existing verticalScroll. - Installed packs show game names, and the catalogue sorts by game or serial. Controls reset now exists: resetTunables() hand-listed keys, drifted behind every setting added after it, and had ZERO call sites. Replaced with resetAllControls(), which sweeps by key prefix so it cannot rot, scoped global or per-game. Confirmations are inline overlays claiming an exclusive nav layer, not Compose dialogs — a dialog is its own window and swallows controller keys, so those prompts were touch-only. Adds Clear All for Recently Played and a full app reset; the reset also purges the in-folder settings mirror and gamesettings INIs, without which the next launch would silently restore everything it just wiped. Motion control falls back to the accelerometer where there is no gyroscope, with the tilt limitation stated in the UI (gravity cannot observe yaw). Adds a Motion Recenter hotkey — recenter() previously had no call site at all. |
||
|
|
de7ec8509c |
GS: 20:9/19.5:9/custom aspect, interlace+presentation policies, VK feedback flags
Aspect ratios: added 20:9, 19.5:9 and a user-entered Custom ratio
(GSOptions::CustomAspectRatio, clamped 0.5..5.0). All APPENDED, never inserted —
these values are persisted as raw ints in the ini and in the Android prefs, so
slotting one in mid-enum would silently repoint every saved config at a different
ratio. Also filled in the two ultrawide cases RequestDisplaySize was missing.
Interlace/presentation: ported sashkinbro's EmuCoreX 30799e4. SelectGSInterlaceMode
centralises the mode choice and keeps shader_mode -1 for automatic full-frame output
(a deinterlace pass must not run over progressive output during a video-mode
transition); our formula already agreed, so this is centralisation plus
static_asserts rather than a behaviour change. ShouldSkipAndroidBlankFrame is new
behaviour: Vulkan now suppresses only the startup blank, so a mid-game fade reaches
the normal present path and its recorded command buffer is submitted.
Vulkan: declare the attachment feedback loops on the PIPELINE, not just on the image
layout and render pass. We put attachments into FEEDBACK_LOOP_OPTIMAL without ever
setting VK_PIPELINE_CREATE_{COLOR,DEPTH_STENCIL}_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT,
which the spec requires — undefined behaviour rather than a missed optimisation, and
strict mobile drivers are where undefined shows up as stale attachment reads.
|
||
|
|
10f4f73fe8 |
Patch: stop patches arming themselves, and make disabling one stick
Reported as "patches apply with every patch setting off, and won't turn off" — SOTC/KH2/GOW2. One chain of defects, verified on device: - PatchManagerViewModel.refresh() called syncAllEnableLists() unconditionally, so merely OPENING the Patch Manager persisted every uncommented group of every on-disk .pnach as enabled. Community pnach files ship uncommented, and patches are matched by NAME, so a name like "60 FPS" then armed the same-named group in any of the ~4000 bundled files, for games never opened. Removed; import still registers its own file, which was the only legitimate use. - EnumeratePnachFiles fell back to the bundled zip even when disk files existed, contradicting its own "prefer files on disk" comment. Deleting a pnach silently promoted the identically-named bundled group in its place. - delete() removed the file but never dropped its names from the enable list, so they stayed armed forever. - ReloadPatchAffectingOptions never reset CurrentCustomAspectRatio, which only ever gets set, so 16:9 survived disabling widescreen. - LocalCheatRow and OnlineEntryRow armed the row under the cursor on D-pad Right, so scrolling a cheat list enabled everything you passed. Confirm only now. Patches cannot be un-applied without a reboot: PatchCommand has no original-value field and UnloadPatches never touches guest RAM, so disabling one mid-session only stops it being re-written. |
||
|
|
15538d066d |
GameDB: no readbacks for Guitar Hero II and III
The note-highway render target is never sampled back, so the GPU->CPU download is pure cost on a tiler. Covers GH2 (SLES-54442, SLUS-21447 — the latter is also the serial GH2 Deluxe ships under) and GH3 (SLES-54962, SLES-54974, SLKA-25363, SLKA-25414, SLUS-21672). gsHWFixes is a clear-then-replace map in this overlay, so each GH3 entry restates every upstream key. Dropping one would have silently undone the crowd-texture, bloom and post-processing fixes those entries already carry. |
||
|
|
d4caacf512 |
Android: Exit back in the library menu, Skins next to the control tabs
Exit returns to the library's overflow menu. It had moved to the navigation drawer, which put it below every other destination -- so quitting, one of the most frequent things anyone does from that screen, meant opening the drawer and scrolling to the bottom every time. Reported as issue #460 by shinobumaehara, whose point is simply that frequency should decide placement. It stays in the drawer as well; this is the short path, not a replacement. Smaller than it looked: onExitApp was still a parameter and its confirmation dialog was still wired up. Only the row that reached them had been deleted. LibraryOverflowItem gained optional iconRes/iconTint for it, because the power symbol (U+23FB) is not in the bundled font and rendered as a tofu box -- it now uses the same ic_power drawable and red as the drawer's row, so the two entries match. Every other row keeps the text-glyph path untouched. Skins moves to sit after Shortcuts and before Network. It is controller artwork, so people look for it beside Controls and Shortcuts rather than past On-Screen. Suggested by Isshin. |
||
|
|
363009987a |
ImGui: queue notifications instead of stacking them
Finishing a game submits every leaderboard in the same frame. AddNotification gave each one start_time = current_time, so they all began at once -- Final Fantasy XII posted six, which covered the screen and pushed the mastery unlock out of sight before it could be read. Reported with a screenshot showing exactly that. Three on screen at most now, at least 0.4s apart, and anything past the limit waits its turn. A QUEUED notification does not age while it waits: its duration starts when it actually appears, so nothing expires unseen in the backlog. Also guards the same-key replacement path. It recomputes start_time from elapsed time, which is NEGATIVE for a notification that has not appeared yet -- and Timer::Value is unsigned, so subtracting it would have wrapped and flung the notification years into the future. It now keeps the scheduled start instead. Ordering is still insertion order, so an unlock posted after a batch of leaderboards still comes last. Visible rather than buried, but not prioritised. |
||
|
|
0ddf000e3f |
Android: screenshot button, macro skins, and make grid snap survive Save
Three touch-overlay changes that share the same files. SCREENSHOT is now an on-screen button next to SAVE and LOAD, off by default. It started as a pause-menu entry, which was the wrong place: the core writes the PNG and confirms on the OSD, and the OSD is hidden while the menu is up -- so you tapped it, saw nothing, and only learned it had worked after backing out. Macro buttons can take skin artwork. A macro can already fire any pad input, including L-Stick Left/Right, so a racing layout of steer/steer/accelerate/brake was buildable -- but the four buttons were stuck with the generic M1-M4 labels because there was no skin slot for them. ic_controller_macro1_button.png through macro4, and m1-m4 too, since that is the name David reached for first. That also exposed a silent truncation: the skin import cap was 24 while the key list is now 28, so a complete pack would have had images dropped on import with no error at all -- they simply would not appear. Raised to 40, with a comment tying it to the key count so the next slot added does not repeat it. Grid snap now commits on Save. The editor draws widgets snapped while leaving the underlying fraction raw (snapping live fights the transform gesture's delta accumulation and makes dragging feel stuck), and the commit was supposed to happen on finger-up. But that lived in detectTapGestures' tryAwaitRelease, and an actual DRAG is consumed by the neighbouring detectTransformGestures, which cancels the tap detector and makes tryAwaitRelease return false. So it only ever committed if you tapped without dragging: the layout looked aligned the whole time you were editing and reverted the moment you saved. Reported by David (SSR) and by a user who caught the grid reverting. |
||
|
|
c553aa8acb |
GS: add a 21:9 aspect ratio
Requested by David (SSR), who noted no PS2 emulator offers one: without it the only way to use an ultrawide patch was Stretch, which distorts. Useful on folds, tablets, DeX and anything driving a 21:9 panel. Added to the generic aspect AND the FMV override, since a game that wants ultrawide gameplay usually wants it during cutscenes too. Adding it to the FMV enum also shifted MaxCount, which the name array is sized from -- that array had to grow with it or the last entry would have been a hole. The Kotlin side needed SIX edits for one new enum value, and getting five of them right still left the feature completely dead: RendererTab options list + clamp RendererTab FMV options list + clamp EmulationMenu setAspectRatio clamp EmulationMenuScreen the pause menu's own options list Settings NativeApp.setAspectRatio(coerceIn(0, 4)) <-- the killer Settings INI name<->index, both directions That fifth one clamped on the way to the core, so the picker highlighted 21:9 while the emulator was told 10:7 -- UI correct, nothing happens, no error. The sixth meant the choice would not have survived a reload even once it applied. The NATIVE clamp needed no change at all, because it derives its bound from AspectRatioType::MaxCount instead of hard-coding it. That is the pattern the Kotlin side should follow; four literal 4s in four files is why this was a six-site change instead of a one-site one. |