Commit Graph
100 Commits
Author SHA1 Message Date
jpolo1224 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)
2026-08-24 10:27:37 -04:00
jpolo1224 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.
2026-08-23 10:49:41 -04:00
jpolo1224 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.
2026-08-23 10:49:41 -04:00
jpolo1224 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)
2026-08-23 10:49:41 -04:00
jpolo1224 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.
2026-08-23 10:49:41 -04:00
jpolo1224 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.
2026-08-23 10:49:41 -04:00
jpolo1224 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.
2026-08-23 10:49:41 -04:00
jpolo1224 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.
2026-08-21 23:26:12 -04:00
jpolo1224 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".
2026-08-21 23:09:00 -04:00
jpolo1224 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.
2026-08-21 22:32:15 -04:00
jpolo1224 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. 10f4f73fe8 removed the unconditional
    syncAllEnableLists on Patch Manager open, stopped EnumeratePnachFiles falling
    back to the bundled zip over on-disk files, made delete() drop names from the
    enable list, and stopped D-pad Right arming the row under the cursor.
  - Anyone still carrying a poisoned list can clear it themselves, because
    disabling a patch sticks now. That was the original complaint.

Nothing writes these lists except the player, which is how it should have been.
2026-08-21 22:09:53 -04:00
jpolo1224 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.
2026-08-21 21:24:35 -04:00
jpolo1224 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.

10f4f73fe8 fixed a real bug -- opening the Patch Manager armed every uncommented
group of every on-disk .pnach -- and cleaned up the damage with a one-time purge
of [Patches]/[Cheats] Enable. The purge deleted every entry unconditionally,
including the ones people had turned on deliberately, so updating past 2.6.6
silently switched off everyone's patches and cheats.

It surfaces in the worst possible way. The reporter's mod is a PNACH,
94A82AAA_Modern_Persona_Switch_aem.pnach, and PNACH mods patch game DATA. With it
disabled the font glyphs in GS memory differ, so the texture pack keyed to the
patched font stops matching as well -- and the whole thing reads as "my texture
mods stopped applying". Their logs say it plainly: 18,791 patch lines applied on
2.6.6 against 76 now, same .pnach found in both.

Two changes:

  - The purge now only fires on lists carrying the auto-armed signature.
    syncAllEnableLists persisted every uncommented group across ~4000 bundled
    files, so a poisoned list runs to hundreds of names; a hand-built one is a
    handful. At or under 25 entries it is treated as deliberate and left alone.

  - A one-time notice for the people v1/v2 already hit. Nothing can give them back
    what was deleted, and they have no way to know it happened -- the .pnach is
    still on disk and still found in the log, it is simply no longer enabled.
    Saying so once is the only repair available.

The immediate fix for anyone affected is to re-enable their patches in Patches &
Cheats; the purge is one-shot and will not take them again.
2026-08-21 21:21:13 -04:00
jpolo1224 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.
2026-08-21 21:06:50 -04:00
jpolo1224 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.
2026-08-21 21:01:28 -04:00
jpolo1224 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.
2026-08-21 20:40:17 -04:00
jpolo1224 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.
2026-08-21 19:50:44 -04:00
jpolo1224 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.
2026-08-21 19:11:18 -04:00
jpolo1224 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.
2026-08-21 18:53:47 -04:00
jpolo1224 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.
2026-08-21 18:37:38 -04:00
jpolo1224 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.
2026-08-21 18:25:28 -04:00
jpolo1224 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.
2026-08-21 17:58:03 -04:00
jpolo1224 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.
2026-08-21 17:41:13 -04:00
jpolo1224 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.
2026-08-21 15:26:28 -04:00
jpolo1224 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.
2026-08-21 15:14:13 -04:00
jpolo1224 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.
2026-08-21 15:11:31 -04:00
jpolo1224 88d95a83f5 Merge branch 'pr-566' into jit-android-catchup-gv7 2026-08-21 15:10:24 -04:00
jpolo1224 ff2b0c2155 Merge branch 'pr-593' into jit-android-catchup-gv7 2026-08-21 15:08:40 -04:00
jpolo1224 2c36725e11 Merge branch 'pr-608' into jit-android-catchup-gv7 2026-08-21 15:03:02 -04:00
jpolo1224 9262ea0480 Merge remote-tracking branch 'origin/master' into jit-android-catchup-gv7 2026-08-21 14:57:26 -04:00
jpolo1224 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.
2026-08-21 14:26:59 -04:00
jpolo1224 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.
2026-08-21 13:43:40 -04:00
jpolo1224 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.
2026-08-21 13:19:18 -04:00
jpolo1224 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.
2026-08-21 12:59:05 -04:00
jpolo1224 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 2f3f28faae allows a group through only if it DECLARES
gsaspectratio or gsinterlacemode. That rule was picked to separate a Skip
Cutscenes cheat from a Widescreen patch living in the same pnach, and it does
separate those two. What it does not survive is the rest of the database.

Measured across the shipped 4437 files, the groups it blocked were overwhelmingly
not cheats:

    305  50 FPS
    177  60 FPS
    168  50/60 FPS
    102  Remove Blackbars      <- a widescreen patch
    100  NTSC Mode
     31  480p Mode

Almost every real presentation patch changes the picture by writing EE memory and
declares nothing, so the declaration test caught the tail and missed the bulk.
The original commit sampled Car select and Auto-activate analogs and concluded
the blocked set read like cheats; across the whole database it does not.

So the name is now consulted too, matched case-insensitively against the labels
the database actually uses. Matched on the EFFECT rather than the verb — Remove
Blur, Disable Blur and No Blur are the same patch, and listing verbs caught one
spelling while blocking the others.

After: 4673 groups stay by declaration, 1206 more stay by name, 495 remain
blocked. What still blocks reads the way it should — Adjusted triggers
sensitivity, Trigger control mappings, GT3 Chase Camera, and Skip Cutscenes
itself, which is the case the gate was written for.

An unrecognised name still fails CLOSED, which is the safe direction: a new cheat
is blocked by default and only an understood presentation class is let through.

★ Frame-rate patches are the judgement call here. They are in the allowlist
because they are what was reported broken and desktop PCSX2 permits them, but
they are the entry RetroAchievements is most likely to object to. If RA rules
them out, deleting three strings from ALLOWED is the whole change.
2026-08-21 12:16:27 -04:00
jpolo1224 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.
2026-08-21 12:04:09 -04:00
jpolo1224 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.
2026-08-21 11:51:32 -04:00
jpolo1224 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 9a6db38395), then
'mods do not apply at all' once failed uploads correctly fell back. Removing the
cap entirely just traded the crash back.

A FRACTION of RAM is the wrong shape, and that is the actual mistake. It scales
the RESERVE with total memory, when what has to be reserved is roughly constant:
the EE/GS allocations, the JIT, Android itself. On 8 GB, RAM/2 holds back 4 GB
to protect something needing about 1.5 GB.

A fixed 5 GB is wrong too, and specifically so: the pack measures 5.0 GB, so a
5 GB cap sits exactly on the boundary and evicts anyway.

So reserve a constant 2.5 GB and give the rest to textures:

   6 GB ->  3.5 GB   (was 3.0)   GoW1 HD 2.97 GB fits
   8 GB ->  5.5 GB   (was 4.0)   P3 FES HD 5.0 GB fits
  12 GB ->  9.5 GB   (was 6.0)

The 8 GB row is the reported case, and it now holds the pack whole with headroom
rather than missing by a gigabyte.

The budget is logged at startup with the physical RAM it came from, and the
over-budget warning now says the pack does not fit rather than only quoting the
budget — the actionable question is which is bigger, and the old wording did not
let anyone answer it.
2026-08-21 11:36:43 -04:00
jpolo1224 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 9a6db38395 a failed upload was injected anyway
with UNDEFINED contents, which is the green blocky corruption in the original
screenshots. After 9a6db38395 a failed upload correctly drops the texture, so
the same cause changed appearance to 'the mods do not apply at all'.

No budget satisfies both cases: 5 GB does not fit inside any fraction of 8 GB
that also leaves the emulator room. So this is a choice about which failure the
user gets, not a bug with a correct answer. An oversized pack now behaves as it
did before — it loads, and the device is allowed to run out of memory if it
genuinely cannot hold it. Half a pack applied with nothing on screen explaining
why is the worse outcome; it reads as broken rather than as too big.

The byte accounting is kept and now warns ONCE past physical RAM/2, naming the
footprint. It is a breadcrumb, not a limit: nothing is refused and nothing is
evicted, but if the process is killed the log says what was resident. The LRU
ordering is kept dormant for the same reason — if OOM reports return,
reinstating eviction is one line in GetReplacementCacheBudget.

The genuine fix is on the pack side: block-compressed (BC7/BC3) it would be
several times smaller and fit comfortably.
2026-08-21 11:26:36 -04:00
jpolo1224 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.
2026-08-21 11:07:04 -04:00
jpolo1224 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.
2026-08-21 11:04:52 -04:00
jpolo1224 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.
2026-08-21 11:00:55 -04:00
jpolo1224 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.
2026-08-21 10:56:53 -04:00
jpolo1224 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.
2026-08-21 10:47:35 -04:00
jpolo1224 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.
2026-08-21 10:38:07 -04:00
jpolo1224 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.
2026-08-21 10:30:15 -04:00
jpolo1224 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.
2026-08-21 10:23:26 -04:00
jpolo1224 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.
2026-08-21 01:14:30 -04:00
jpolo1224 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.
2026-08-21 00:53:19 -04:00
jpolo1224 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.
2026-08-21 00:25:08 -04:00
jpolo1224 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.
2026-08-21 00:22:31 -04:00
jpolo1224 2a98726692 Merge remote-tracking branch 'origin/master' into jit-android-catchup-gv7 2026-08-16 14:22:55 -04:00
jpolo1224 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.
2026-08-16 14:22:55 -04:00
jpolo1224 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.
2026-08-16 13:08:05 -04:00
jpolo1224 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.
2026-08-16 12:49:03 -04:00
jpolo1224 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.
2026-08-16 12:11:33 -04:00
jpolo1224 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.
2026-08-16 11:13:54 -04:00
jpolo1224 7fde8ce980 Merge remote-tracking branch 'origin/master' into jit-android-catchup-gv7 2026-08-16 10:52:15 -04:00
jpolo1224 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.
2026-08-16 10:40:50 -04:00
jpolo1224 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.
2026-08-16 10:38:25 -04:00
jpolo1224 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.
2026-08-14 12:52:39 -04:00
jpolo1224 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.
2026-08-14 12:21:01 -04:00
jpolo1224 cdb7310a74 Merge branch 'pr539' into jit-android-catchup-gv7 2026-08-04 21:35:10 -04:00
jpolo1224 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.
2026-08-04 21:34:01 -04:00
jpolo1224 156a778cf9 Android: import external save-state files into a slot 2026-08-03 09:45:34 -04:00
jpolo1224 12a7e37682 Android: 2D fallback background, portrait status layout, Clear Shader Cache placement, memcard delete wording 2026-07-30 18:20:12 -04:00
jpolo1224 1a45319a6a Android: fix Auto Progressive Scan hold, and correct Samsung QHD touch offset 2026-07-30 18:20:12 -04:00
jpolo1224 9d735f9ccb GameDB: no readbacks for Need for Speed Underground 1 & 2 2026-07-30 18:20:12 -04:00
jpolo1224 036eeea43e Patch: apply cheats filed under the generic all-CRC name 2026-07-30 18:20:12 -04:00
jpolo1224 e0f39849ce GS: keep Adreno framebuffer-fetch on by default, gate Snapdragon 8 Elite off 2026-07-30 18:20:12 -04:00
jpolo1224 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.
2026-07-30 02:43:59 -04:00
jpolo1224 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.
2026-07-30 02:31:55 -04:00
jpolo1224 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).
2026-07-30 02:19:56 -04:00
jpolo1224 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.
2026-07-30 02:04:58 -04:00
jpolo1224 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.
2026-07-30 01:45:56 -04:00
jpolo1224 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.
2026-07-30 01:31:39 -04:00
jpolo1224 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.
2026-07-30 01:18:56 -04:00
jpolo1224 3cb1e88029 Merge remote-tracking branch 'origin/master' into jit-android-catchup-gv7
# Conflicts:
#	tests/ctest/core/gs/CMakeLists.txt
2026-07-30 01:10:19 -04:00
jpolo1224 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.
2026-07-30 01:09:39 -04:00
jpolo1224 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.
2026-07-30 01:09:12 -04:00
jpolo1224 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.
2026-07-30 01:08:54 -04:00
jpolo1224 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.
2026-07-30 01:08:39 -04:00
jpolo1224 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.
2026-07-28 12:50:06 -04:00
jpolo1224 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.
2026-07-28 12:50:06 -04:00
jpolo1224 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.
2026-07-28 12:49:48 -04:00
jpolo1224 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.
2026-07-28 12:49:30 -04:00
jpolo1224 1eafb9971d GS/VK: check device features exist before asking for them
An advertised extension does not guarantee its feature bit, and requesting a
feature the driver does not have fails vkCreateDevice outright with
VK_ERROR_FEATURE_NOT_PRESENT. So a driver that offers, say, VK_EXT_line_
rasterization while reporting bresenhamLines as false did not cost us one
optional nicety -- it took Vulkan down completely, and the renderer refused to
start with "Failed to create render device".

Reported on a PowerVR BXM-8-256, which is neither Adreno nor Mali and so had
never been through this path. Deterministic: three identical failures in a row,
and the only way out was switching to OpenGL.

The file already knew about this trap. A comment above the depth-ROAA probe spells
it out exactly -- but the lesson had been applied to that one sub-feature and
nowhere else, so the other six were still requested blind. ProcessDeviceExtensions
does perform the same reconcile, and would have caught it, except that it runs
AFTER vkCreateDevice and can therefore only ever describe the failure.

Probe all six up front in one vkGetPhysicalDeviceFeatures2 call -- provoking
vertex, line rasterization, ROAA colour and depth, feedback-loop layout,
swapchain maintenance1, fragment shader interlock -- and drop whatever is not
really there. Dropping is logged WITH THE FEATURE NAME, because "Vulkan works but
one thing is off" is a completely different bug report from "Vulkan does not
start", and the next person needs to know which feature the driver misrepresented
rather than guessing from a bare error code.

Confirmed fixed on the reporting device.
2026-07-28 11:29:59 -04:00
jpolo1224 f98371ddcd Android: license the Discord helper MIT, not GPL
The process split was only half done. discord_bridge.cpp still carried PCSX2's
"GPL-3.0+ / PCSX2 Dev Team" header -- wrong on both counts, since it is neither
PCSX2's code nor compatible with what it links -- and the four helper Kotlin/Java
files carried no header at all, so they inherited the repository's GPL by
default. A GPL file that links Discord's proprietary SDK is exactly the defect
the separate process exists to prevent; the boundary has to hold in the licence
headers as well as in the linker.

Everything on the helper side of that boundary is now MIT with accurate
copyright: the bridge, the native wrapper, the service, the auth activity and
the shared IPC definitions. MIT rather than Apache-2.0 because it has to work in
both directions -- the emulator side (DiscordPresence and the Friends UI, which
stay GPL) consumes the shared IPC definitions, and that only holds if this side
is permissive.

Each header says why, so it does not get "fixed" back to the PCSX2 boilerplate
by someone tidying up.

No functional change: comments only, both sides still compile.
2026-07-28 01:52:41 -04:00
jpolo1224 b762cc01de Android: make the bundled patches visible, and add All on / All off
Two reports from Rei Ayanami, one of which turned out to be a real gap rather
than a misunderstanding.

A game could report "3 game patches are active" with an empty patch list and
nothing anywhere to turn them off. Those patches come from the ~2 MB patches.zip
we ship, and the manager only ever listed files in the patches folder, so
everything inside the zip applied invisibly. Worse, the core auto-applies any
group with no [Name] ("we auto enable anything that's not labelled" --
Patch.cpp), and a group with no name has nothing for a toggle to hang off.

There is now a card showing exactly what the bundled zip contributes to this
game, with unlabelled groups marked as always-on, and a button to copy the file
into the patches folder. That is not convenience: the core prefers a pnach on
disk over the zip and explicitly disables the bundled copy when it finds an
unlabelled patch on disk, so extracting both makes the cheats individually
switchable AND takes the invisible copy out of play. It is the only way to turn
an unlabelled bundled patch off at all. The card is hidden when the game already
has a pnach on disk, because then the zip contributes nothing and showing it
would be a lie.

All on / All off sits above the cheat list, not below: a community pnach can run
to a hundred entries, and a control you have to scroll past all of them to reach
does not solve the problem it exists for. It rewrites the file once rather than
once per cheat -- a hundred read-modify-writes is a hundred chances to
half-apply -- and each button disables itself when there is nothing left to do.
2026-07-28 01:38:35 -04:00
jpolo1224andSplaser bf7300e654 Android: show the game CRC next to its serial
A PNACH is named <SERIAL>_<CRC>.pnach, so the two values needed to name one
should not live on separate screens. The CRC now appears in the pause-menu
header, in the game details you get by long-pressing in the library, and as a
row that stays put in the Info tab instead of appearing mid-identification and
shoving everything below it down.

Based on Splaser's PR #459, with three changes:

- The long-press sheet showed "CRC ..." forever for an image that could not be
  identified, because produceState starts at null and RESOLVES to null; those
  two states are indistinguishable. Now tri-state: "..." while identifying, an
  em dash when identified as unknown.
- The VM-then-identify fallback was copy-pasted into a second place. One
  DiscIdentity.resolve now, keeping the serial-match guard -- without it a
  different running game lends its CRC to whatever you long-pressed.
- The pause header cannot trust the live VM CRC alone. On ISO boots the core
  hands ELFLoadingOnCPUThread an empty path, UpdateELFInfo takes its failure
  branch, and s_current_crc stays 0 -- the emulog shows the loader computing the
  real CRC and the VM then reporting 00000000. It falls back to identifying the
  image, same as the other two screens.

Co-authored-by: Splaser <splaser@users.noreply.github.com>
2026-07-28 01:38:35 -04:00
jpolo1224 cc94bc57c0 Android: credit the right people in What's New
Contributors were derived from commit authorship between tags, which is not who
a release credits. Work ported in from other projects lands as commits authored
by whoever did the porting, so the people the notes thank never appeared, while
anyone who happened to commit inside that tag range did -- credited in a release
that says nothing about them.

Read the @mentions in the release notes instead. That is the credit somebody
deliberately wrote. Avatars come from github.com/<login>.png, a plain redirect,
so this costs no API calls at all: no rate limit, nothing to cache, and it works
offline from cached notes. The tag-to-tag comparison it replaces cost one
request per release against an unauthenticated budget of sixty an hour.

Each card also shows the release author's avatar, which was already in the
releases payload and free.
2026-07-28 01:38:34 -04:00
jpolo1224 bc951c84dd Android: run the Discord SDK out of process, and finish the friends UI
ARMSX2 is GPL-3.0+ and the Discord Social SDK is proprietary. Linking them into
one binary would make the emulator a combined work with a library whose
corresponding source cannot be supplied, so the SDK now runs as a separate
program in its own process and the two talk over a deliberately dumb message
interface: a title, a serial, an image URL, a list of names. No emulator type
crosses that line.

libemucore has no DT_NEEDED on the SDK and never loads it; the bridge builds
into its own libarmsx2_discord.so, loaded only in :discord. With the feature
switched off that process does not exist and the library is never mapped at all.
Verify with: llvm-readelf -d libemucore_4k.so | grep -i discord  (must be empty)

The SDK itself is no longer in this repository. Discord's terms permit shipping
it inside a working application but not republishing the raw SDK, so it is an
optional private build input via DISCORD_SDK_DIR. Unset -- every public clone --
the feature compiles out and the UI reports itself unavailable. Deliberately not
a product flavour: one release variant, and the only difference is whether that
directory was present at build time.

Also here, from testing the feature into shape:

- Presence uses the game's own cover art, and our logo (an Art Asset key, not a
  repo URL, so it tracks the current mark) when idle.
- Friends show their avatar AND the cover of what they are playing, never one
  instead of the other -- swapping the face out for box art loses the identity
  exactly when the row gets interesting.
- A count badge on the Friends entry point, in the drawer and in the in-game
  header, because the point of it is to be seen by someone not looking at the
  friends list.
- Friends moved off the in-game tab rail into a header button with its own
  panel. It was last on a rail that scrolls, so reaching it meant knowing it was
  there. The panel is composed, not a Dialog: a Dialog takes its own focused
  window and swallows gamepad keys before our input plumbing sees them.
- "<friend> is now online" is one Compose banner at the Activity root, replacing
  a split between an emulator OSD message in game and nothing at all in the
  library. The OSD is text-only and could never show an avatar.
- Reconnect after a drop, with backoff. The SDK does not retry and its
  connection does not survive being backgrounded, so a drop used to be terminal
  until the app was restarted.
2026-07-28 01:38:34 -04:00
jpolo1224 069f8a44f3 Android 2.6.5.1: Local Link LAN play, async GS readback, pause/rotation/settings fixes
Crash and correctness
- Fix a crash when backgrounding the app mid-game: onPause flushed the Vulkan
  pipeline cache from the UI thread while the GS thread was creating pipelines
  into the same VkPipelineCache. Vulkan requires that handle to be externally
  synchronised, so this was a driver-level data race and crashed on Adreno and
  Xclipse alike. The flush now runs on the GS thread, posted via the CPU thread
  so it does not race the EE-owned MTGS ring.
- Fix an unbounded out-of-bounds vertex read in the GSRendererHW sprite-merge
  paving path: the inner loop advanced i instead of j, so j stayed loop-invariant
  and the scan walked past m_vertex->tail.
- Fix per-game settings being silently ignored: gamesettings/<serial>_<CRC>.ini
  loads into a higher-priority layer than anything the app writes, and saves made
  from the library never regenerated it, so any key already in that file
  overrode the user permanently. Only the category-Reset path rewrote it, which
  is why Reset appeared to be the only thing that worked.
- Fix screen rotation: the BIOS followed the launcher rotation instead of the
  renderer's (it has no GameInfo, and the tier was keyed on that), and the
  launcher stayed locked in a game's orientation after exit because the cleanup
  lived only inside stop()'s vmRunLoopActive-guarded branch, which loses a race
  against the VM thread's own finally. Rotation tier is now an explicit flag and
  the cleanup runs on every terminal path.
- Discard the Vulkan pipeline blob whenever the SPIR-V cache is discarded. It was
  validated only against the device header (vendor/device/pipelineCacheUUID),
  which is identical across an app update, so a SHADER_CACHE_VERSION bump kept
  every pipeline built from the old shaders and nothing pruned it.
- Make eeRecExitRequested atomic: it was a plain bool written from the JNI thread
  and read on the CPU thread.
- OpenGL: restore GL_PACK_ALIGNMENT after readback, add the missing memory
  barrier after the CAS dispatch, and initialise GLState::depth_mask to GL's
  actual default.
- DEV9: log the GetNetAdapter default: bail and the InitNet skip. Both returned
  silently, so a settings mistake surfaced as missing hardware three layers away.

Local Link (new)
- New DEV9 backend bridging emulated PS2 Ethernet between devices over
  authenticated local UDP, so games with a built-in LAN / System Link mode can
  play together. Ported from EmuCoreX (sashkinbro) with the wire format
  unchanged, so peers remain compatible across both forks.
- Network mode picker (Online / Host / Join), host address readout, auto-derived
  peer ids, generated room codes, hostname support alongside numeric IPv4, and a
  link to the supported-games list. Fully controller-navigable.

Performance
- Asynchronous hardware download mode (experimental, opt-in): non-blocking
  GPU->CPU readback so the EE thread no longer waits on the GS thread. Ported
  from EmuCoreX. Appending Asynchronous to GSHardwareDownloadMode makes the enum
  non-ordered, so the relational comparisons on it are replaced with
  IsHardwareDownloadReadbackEnabled / IsHardwareDownloadEEThreadRead.
- Affinity Control Mode (experimental, opt-in): EE/VU/GS priority orders plus a
  Performance Cores mode. Android otherwise leaves these threads unpinned.
- Raise the texture-replacement cache ceiling from 6 to 16 GB; RAM/2 remains the
  real limiter, so this only binds at 12 GB RAM and up.
- Low Latency frame pacing is no longer the default, with a one-time migration
  for installs that took the earlier flip.

Features
- Auto renderer resolves to Vulkan HW on Adreno.
- Auto Progressive Scan (per-game): holds Triangle+Cross through boot.
- OLED black as a modifier over any accent colour, including Custom and RGB.
- Optional system keyboard instead of the built-in on-screen one.

Game compatibility
- Everybody's Golf 4 / Hot Shots Golf Fore! hwDownloadMode across all regions
  (PR #421, XDarkFallenX).
- Delta Force: Black Hawk Down (PR #401, XDarkFallenX).
- Reduced input latency and input handling improvements (PR #403, Splaser).

RetroAchievements
- Inject the client version from a build-time secret kept out of public source,
  with a stock-PCSX2 fallback for secret-less builds, so third parties cannot
  copy the client identity. Covers the iOS token too.
2026-07-25 00:48:56 -04:00
jpolo1224 8e23439b26 Android 2.6.5: Adreno Vulkan default, low-latency default, UI sounds, RA client hardening, GameDB
Rendering
- Auto renderer now resolves to Vulkan HW on Adreno (OpenGL elsewhere).
- Mobile hardware ROV (Phase 0): tile-native depth feedback behind the ROV toggle.

Performance & input
- Low Latency frame pacing is the default on capable devices, with a one-time
  migration for existing installs; low-end devices keep the queued pacing.
- Reduce Android input latency and improve input handling (PR #403, Splaser).
- Experimental CPU clock hint (ADPF) toggle in Performance settings (default off).

Audio & UI
- Pop-up open/close sound cues (info, hardcore confirm, patches & cheats).
- Alternating controller navigation / slider tick sounds.

RetroAchievements
- Inject the RA client version from a build-time secret kept out of public source,
  with a stock-PCSX2 fallback (no hardcore) for secret-less builds. Applies to the
  iOS client token too. Prevents third parties from copying our User-Agent.

Game compatibility
- Delta Force: Black Hawk Down (SLUS-21124 / SLES-53299) GameDB fixes
  (PR #401, XDarkFallenX).
2026-07-24 02:40:17 -04:00
jpolo1224 d856d404e8 Android (github flavor): opt-in auto-check for updates on launch
Adds a "Check on launch" toggle to the App-tab updater panel, default OFF. When
enabled, a silent GitHub check runs once at boot (AutoUpdateGate, mounted at the app
root in setContent, gated on IN_APP_UPDATER) and pops the update prompt only if a
newer release exists -- no "up to date" popup on every launch, and nightly builds are
skipped via checkForUpdate's versionCode guard. Reuses the manual button's exact
check/download/install path.

Github flavor only, like the rest of the updater: AutoUpdateGate is real in src/github
and a no-op stub in src/play, so the boot-check + network code never enters the Play
AAB (build-play-aab.sh still fails closed on REQUEST_INSTALL_PACKAGES).

Requested by takanome9104.
2026-07-23 03:00:02 -04:00
jpolo1224 8f60f14e43 Android (github flavor): in-app GitHub-release updater
Adds a "Check for updates" panel to the top of the App settings tab, github
sideload flavor only. It queries the GitHub releases/latest API, semver-compares
the latest stable tag against the installed build, and offers to download the APK
and hand it to the system installer (progress bar + FileProvider). Nightly builds
(versionCode = Unix seconds, so > 1e6) are always ahead of any stable release, so
they short-circuit to "on the nightly channel" and are never prompted to a stable.

Kept entirely out of the Play build, the same way all-files access is:
- IN_APP_UPDATER BuildConfig flag (true github / false play) gates the App-tab hook.
- The real updater + REQUEST_INSTALL_PACKAGES + the FileProvider live in src/github;
  src/play ships a no-op UpdaterEntry stub so shared code still compiles for play.
- build-play-aab.sh now FAILS CLOSED if REQUEST_INSTALL_PACKAGES appears in the AAB
  (a self-updating app is a hard Play-policy violation).

Verified: the github APK ships the permission + FileProvider + updater code; the play
AAB has neither the permission, the FileProvider, nor the network/install code.
2026-07-23 02:04:46 -04:00
jpolo1224 9ee2b71d7c Android build: adapt the GameDB-from-bin generation to AGP 9.2.1
The generateSharedResources wiring failed the Android build on our pinned
AGP 9.2.1 / Gradle 9.4.1:

- assets.srcDir was passed a Provider (generateSharedResources.map { it... }),
  which AGP 9.2.1 rejects at configuration time ("cannot add Provider instances
  to the Android SourceSet API") -- so the whole Android build was red. Use a
  concrete build-dir path and wire the task dependency explicitly instead.
- the dx11 exclude ("dx11/**") is relative to bin/resources and never matched
  shaders/dx11/, so 8 Windows-only DX11 shaders leaked into the APK. Fixed to
  "**/dx11/**".
- declared generateSharedResources as a dependency of the asset-merge AND lint
  model/analyze tasks (they consume the generated assets dir), so Gradle 9's
  strict implicit-dependency validation passes.

Verified: dual-core release APK builds clean (vc1301); the generated tree carries
bin GameIndex.yaml + the overlay, Dirge hwDownloadMode:2 intact, dx11 trimmed.
2026-07-23 01:44:15 -04:00
jpolo1224 1fde0e4467 Android: achievement filter + RA notification/indicator settings, touch-editor grid snap + movable panel, Back-opens-menu
- RetroAchievements: an All/Unlocked/Locked filter over the achievement list,
  plus DuckStation-style notification settings -- notification and leaderboard
  duration sliders and on-screen position pickers (a 3x3 grid) for both the
  toast notifications and the challenge/progress indicators. Backed by a new
  setAchievementsOptionInt JNI bridge over the existing [Achievements] config.
- Touch layout editor: snap-to-grid (widgets snap by their centre so they line
  up cleanly), and a movable + resizable settings panel -- drag the grip to
  move it, -/+ to resize, double-tap to reset -- with per-orientation placement
  so it stops covering the buttons being edited.
- The Android system Back button / gesture opens the in-game menu (#384),
  toggle in Settings > Hotkeys (default on). Only the system back is routed;
  controller Circle stays the PS2 button.

Requested in #384 by resurrectdev1 and Leunamme30.
2026-07-23 00:15:02 -04:00
jpolo1224 6ce3d9ad7d GS: work around Mali r44p1 device loss on the feedback-loop blend path
The Mali-G615 r44p1 blob loses the rendering context under the in-tile
feedback-loop blend path used for accurate blending: VK_ERROR_DEVICE_LOST on
Vulkan (attachment-feedback-loop) and an equivalent context loss on OpenGL ES
(ARM framebuffer-fetch), crashing effectively every game at any resolution.
It is specific to this driver build -- other Mali blobs, including other
Mali-G615 units on different drivers, run the fast path fine, and NetherSX2's
older renderer is unaffected.

Gate only r44p1 off that path, narrowing by driver version rather than
re-blocking the vendor -- exactly what the extension-select comment above the
Vulkan site anticipated. Vulkan falls back to the per-primitive barrier path
(verified on hardware: no measurable slowdown, God of War holds 60fps);
GLES has no texture-barrier extension so it falls to the framebuffer-copy
path (stable, marginally slower) rather than crashing.

Reported and confirmed on-device by Aryan3472.
2026-07-22 21:50:37 -04:00
jpolo1224 0a017612af Android: library opacity, remove-from-recents, recents export, patches notice
- Library opacity slider (Settings -> App) fades the game rows and cards so the
  wallpaper shows through the list.
- Long-press -> Remove from Recently Played drops a single game from the shelf;
  the game menu now scrolls so no action is clipped in landscape.
- The recently-played list is mirrored to recent_games.json in the data root for
  companion apps, reworked from misantronic's PR #391 to run off the UI thread
  and to update on removal too.
- A notice on the Patches & cheats screen warns that outdated cheats/patches are
  the most common cause of false bug reports.
2026-07-22 15:39:39 -04:00