Commit Graph
11936 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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
Brian Degenhardt cd20f6454d Android: flush memory card writes when the app is backgrounded
A memory card write does not necessarily reach the file system when it happens,
and on Android that is data loss rather than a detail.

A FOLDER card holds writes in an in-memory page cache and flushes two frames
after the last one, counted down by the per-frame tick that runs off vsync. So
pausing does not delay that flush, it stops it ever being reached -- the counter
does not advance at all while the VM is paused.

A FILE card writes through stdio with no flush anywhere in the path. Seeking on
an update stream pushes the previous write out, so a run of writes mostly
self-corrects, but the last write of a save sequence sits in the buffer until the
next card access or fclose.

Either way the pending write is lost if Android reclaims the process while it is
backgrounded, which it may do with no further callback. Save in-game, switch
apps, get reclaimed, and the save was never on disk.

The pause path already handles exactly this shape for the BIOS NVRAM
(cdvdSaveNVRAM, added because "the process is frequently killed while paused"),
so the card flush goes next to it, in both the Running and already-Paused
branches. It runs on the CPU thread, queued after SetPaused, so the console is
stopped and nothing can be written behind it, and it is fire-and-forget -- onPause
is on a deadline and blocking it risks an ANR.

  - FileMcd_Flush() / FileMemoryCard::Flush() / FolderMemoryCardAggregator::Flush()
    write out what is buffered without closing anything, so the console keeps
    playing afterwards.
  - FileMemoryCard::Flush deliberately does NOT stamp the running checksum the way
    Close() does. That value is a change-detector a savestate load compares to
    decide whether the card moved under the console, not an integrity check, and
    m_chkaddr is card data rather than a header field we own. A stale value costs
    one auto-eject on the next savestate load, which is the safe direction, so
    writing to the card on a path upstream never writes on buys nothing.
  - FolderMemoryCard::FlushNow clears the frame countdown so a resumed VM does not
    repeat the work. Flush() is already a no-op when nothing is cached, so calling
    this on a quiet card costs nothing.
  - Save() flushes each sector as it is written.

Also bounds the emulation-thread join in onDestroy. NativeApp.shutdown() already
gives up waiting after 5 s and returns anyway, so an unbounded join inherited a
wedged CPU thread and hung the destroy path until Android force-closed us.
2026-08-20 09:22:18 -07:00
Brian Degenhardt df17121cc1 SIO/Memcard: refuse a card write whose read-back failed
FileMemoryCard::Save is a read-modify-write: it reads the sector being written
into a scratch buffer, ANDs the new data into it (memory card bits only ever go
1->0 without an erase), and writes the result back.

When the read failed it reported the error and then carried straight on into the
merge. m_currentdata is only ever grown, never cleared, so the merge ran against
whatever an earlier -- and possibly completely unrelated -- write had left in the
buffer, and that was what got written to the card. A transient read failure
therefore did not lose a write, it corrupted a sector the console never asked to
change.

Refuse the write instead. The sector keeps its previous contents, which the
console can retry. The return value is discarded by the only caller (Sio.h), so
this reads as "don't write" rather than as an error report -- the same shape as
the two Seek failures already in this function.

Desktop hosts rarely see a read fail on a card file. Android does: the storage
memory cards live on is FUSE-emulated, where this tree already documents libc
calls being denied outright.
2026-08-20 08:26:31 -07:00
Brian Degenhardt dfef534426 UI: the Android internal-resolution set in the Qt and Big Picture pickers
Both desktop resolution pickers ran Native, 2x, 3x ... in whole steps. That is
the wrong shape for a handheld at both ends: below native is unreachable even
though rendering there is a large win and the GS accepts it -- only the top end
is clamped, in GSClampUpscaleMultiplier -- and above native the jump from 1x to
2x is four times the pixels with nothing in between.

Both pickers now carry the set the Android UI has offered since issue #207:

  0.25x  0.5x  0.75x  Native  1.25x  1.5x  1.75x
  2x  2.25x  2.5x  2.75x  3x  3.5x  4x  5x  6x  7x  8x

Everything above 8x moves behind Extended Upscaling Multipliers, which used to
unlock 13x and up. That checkbox was dead on ordinary hardware: the GPU cap is
max texture size / 1280, so a 16K-texture part reports exactly 12x and the
"supports extended" test wanted more than 12x. It now unlocks anything past 8x,
which is the first time it does something on a normal GPU, and 9x-25x still
appear only as far as the GPU can actually go.

Big Picture indexed two parallel arrays as "slot i means multiplier i+1", which
a non-uniform list breaks. Replaced with one table carrying the label, the INI
string and the multiplier, filtered against the cap. The quarter steps are all
exact in binary, so matching a saved multiplier by equality is safe.

Two things fixed in passing, both of which the new list would otherwise have
broken. The Qt global tab keyed "nothing found" off index 0 being Native, and
index 0 is now 0.25x. And Big Picture passed "1.000000" as its default while
the INI is written by StringUtil::ToChars, which produces "1" -- so with no
key set the picker read back a value matching no entry and displayed Unknown.
2026-08-19 19:35:22 -07:00
J1coding 05d94ed82d iOS: download the RetroArch shader collection from inside the app
867 presets over 27 categories, one manifest and one zip each, and no third
request anywhere. A tester asked for what Manic EMU has: a button that
fetches the collection instead of making people find a zip and side-load it.
The closure resolution and the licence sign-off landed first; this is the
phone half.

Order is the whole safety argument, because a remote manifest is attacker-
controlled if the host is. The stated size is refused before the transfer
rather than after -- the manifest carries it, so the refusal costs nothing.
The received byte count and the SHA-256 are both compared before the
importer is called, and the hash is streamed rather than read whole. The
relative path is validated before it becomes a URL, because .. and / both
survive percent-encoding. Then the fenced extractor does the writing,
unchanged, so there is no second containment guard to get wrong.

The manifest is 8 MB raw and 312 KB gzipped, and 96% of those bytes are the
per-file array. The entry type does not declare that key, so it is skipped:
the zip carries its own hash and that covers every file inside it. What
lands in the cache is this build's own projection rather than the served
bytes, which is also what makes browsing work with no network -- a failed
refresh ages the list instead of emptying it.

Two things the import path never had. Staging files are swept at launch,
because defer does not run when iOS kills a backgrounded app mid-download,
which is the ordinary outcome and not an edge case. And cancelling removes
the pack if the extract already began, which is not the same as stopping it;
the comment says so rather than implying otherwise.

The importer returns the name it installed instead of only publishing it.
One property on a shared object is fine for one caller and wrong for a
screen with 867 rows and no reason to install them one at a time: two
installs overwrite each other's answer, which would write one entry's marker
into the other's folder and make cancelling one delete the other.

Three fixes in the code around it, from the same review. A loaded chain
owned a render target and a pipeline per pass and nothing freed any of it
when the player turned shaders off, because DestroyShaderChain had exactly
two callers, a preset change and device teardown. The Metal frame path
flushed on success and returned on failure, though a chain that failed
partway has already encoded passes into the same command buffer and needs
the submit for the same reason the success path does. And the pack extractor
held every file's bytes resident to the 32 MB cap, because the autoreleased
data was never drained inside the loop.

The catalogue is not published yet. The base URL is one constant, and an INI
key no UI writes can repoint it, accepting only https and file -- which is
how a simulator reads a local emit, since ATS refuses plain HTTP and there
is no reason to weaken it for a test.

Eleven checks in the new fence, six mutations run against the real source
and all six caught, each restored byte for byte. Still open and written down
rather than left to be rediscovered: the extractor's per-entry decompressed
cap is applied after the entry is fully inflated, so a crafted zip can spend
up to that cap before the refusal. Bounding it earlier needs a streaming
inflate.

The extractor's own fence gains an ordering claim. It asserted that a
canonical resolve appears somewhere in the method, which passes for a
resolve whose answer is discarded; deleting the entire containment refusal
left it green. It anchors on resolvedParent now, because the body carries
several refusal sites naming the same constants and anything looser is
satisfied by a neighbouring refusal that has nothing to do with containment
-- which is the same trap the first attempt at this fix fell into.
2026-08-18 23:41:53 +02:00
J1coding f23ddabf1e iOS: run RetroArch shader chains on the Metal renderer
librashader built for arm64 and pinned, wired into GSDeviceMTL, and a preset
library behind it that can name a file the same way twice across a
reinstall.

The chain runs from DoApplyShaderChain, after ShadeBoost and before present,
on the same ping-pong the FXAA path uses. Two things about it are load-
bearing rather than incidental. EndRenderPass comes first, because the chain
opens its own passes and Metal aborts if ours is still encoding. And
FlushEncoders comes last, because librashader recycles its per-frame objects
over a ring shallower than our deferred-submit window, so a chain frame has
to end the batch. A failure latches on the preset that caused it, or a
preset that will not compile recompiles every frame forever.

Static archive rather than dylib, decided by building both against a working
tracer and measuring, and the loser was deleted rather than left as an option.

The library underneath is where the reinstall problem lives. Both preset
roots sit under a container UUID that changes on every sideload, so a
selection stored as an absolute path is stale within days. A preset is
stored as a marker plus a root-relative path -- bundle: or data: -- and re-
rooted at launch. The separator is a colon because Files refuses one in a
name and it is not a path separator, so the relative half never needs
escaping.

Packs come in as a picked zip or folder through an extractor that keeps the
directory tree, because a .slangp names its stages by relative path and the
tree is part of the pack rather than an arrangement of it. That is the
opposite of the skin extractor's flattening policy, so a test fences the two
apart. Sixteen presets ship in the app, each cleared against its own header
rather than a blanket grant.

librashader's own cache goes to Library/Caches through XDG_CACHE_HOME, set
before anything loads it. Latent today because the Metal runtime never
reaches that cache, but a pin bump that adds caching would otherwise put a
disposable file somewhere iOS can neither purge nor keep out of a backup.
2026-08-18 23:41:53 +02:00
pstef 5b616729f1 EE rec: stop raising TLB misses on unknown MMIO too
The _ext_mem* fallbacks raise a TLB exception when a registered region
gets an access its device has no case for. Under a recompiler that is
the defect just removed from vtlb_Miss by another route: nothing diverts
the block, so the raise only latches Status.EXL.

Raise on the interpreter alone. Recompilers report instead, which is new
- MEM_LOG is devbuild-only, so the raise was all a release build left.
2026-08-18 08:00:56 -07:00
pstef 19cb54586f microVU: read the I immediate at run time under IbitHack
x86's doIbit picks between folding the immediate into the block and
loading it from micro memory; arm64 only ever folded.

Scarface (SLUS-21111), the game the gamefix is listed for, writes a
per-object transform into VU1 micro memory as I immediates. Under the
recompiler its trees and much of the geometry around them took whichever
object's transform compiled first. The interpreter re-reads the word on
every dispatch and was unaffected.
2026-08-18 08:00:56 -07:00
pstef c5ad9ddd4d EE rec: stop raising TLB misses, matching x86
vtlb_Miss raised the exception under the arm64 recompiler and returned,
on the assumption that the rec would pick cpuRegs.pc up at the next
dispatch. Nothing picks it up: the block runs on and its tail writes its
own branch target over the vector PC, while cpuException has already
latched Status.EXL. cpuException leaves EPC alone whenever EXL is
already set, so from the first swallowed miss onward every exception
keeps its predecessor's EPC, and the guest kernel's syscall epilogue
erets to an address belonging to the fault.

Report the miss and continue instead, as the x86 rec already does. The
guest's handler still does not run, so a title that needs demand paging
still cannot work under the rec; a stray miss now stays local to the
instruction. ee_rec_tlb_divert_tests.cpp describes the divert and stays
disabled.

With nothing left to set s_recTlbMissOccurred, the poll after every
interpreter call goes too. It could not have covered the raise anyway:
only the interpreter-call sites had it, so a flag set at an inline
access sat there until an unrelated later recCall consumed it and
diverted on a pc belonging to neither.

This gives up the one case the rec did handle, a miss in a branch delay
slot reaching the vector through the cpuRegs.branch bracket epilogue.
Its recompiler half moves to the disabled file; the interpreter half
stays live, beside a new test pinning the rec's behaviour. The bracket
itself stays: the tlb_fallback_* handlers raise from a delay slot on
their own.
2026-08-18 08:00:56 -07:00