Commit Graph
24975 Commits
Author SHA1 Message Date
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 2.6.6.7 nightly-20260817 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
Brian Degenhardt da25cb84cc Android: call eeClampMode 4 Exact, the name the other frontends use
The desktop and Big Picture pickers landed the same tier as Exact, and
GameIndex.md documents it under that name. Android was the only frontend
calling it something else, which made the same setting look like two
different ones depending on which screen the user was on.

The translation key moves with the label rather than keeping the old name
for a value it no longer matches, so the map stays alphabetical and there
is nothing left to mislead the next reader.
2026-08-16 11:18:41 -07:00
Brian Degenhardt 541b1abfaf Android: offer eeClampMode 4 as Ludicrous in the clamping pickers
The tier existed in the core but nowhere in the UI, on any frontend, so
reaching it meant hand-editing the settings file. Both Android pickers —
the Performance tab and the in-game pause menu — now carry a fifth
option, and the settings layer packs it.

The packing is the part that matters. emucore validates the four clamp
booleans as a cascade and silently resets an inconsistent set to the
defaults rather than rejecting it, so writing fpuExactMode without its
three predecessors would not fail loudly, it would quietly land the user
back on Normal. applyTo therefore writes all four cumulatively, and
readFromIni reads them back highest-first.

readFromIni treats a missing fpuExactMode as an older core rather than as
mode 3: a build without the key never wrote it, and inferring 3 there
would demote a Ludicrous setting every time the settings were reloaded
under a mixed pair of builds.

The chip row already scrolls horizontally, so a fifth option needs no
layout change.

⚠️ Not addressed here, and worth a decision: the GameDB overwrites the
whole tier for any title carrying an eeClampMode entry, and an entry
below 4 clears the exact bit outright. On those ~115 titles the new
option is inert unless game fixes are off — which is most of the titles
whose users would want it. The setting description says so; whether the
core should let a user's choice raise the database's is a separate call.
2026-08-16 11:09:47 -07:00
Brian Degenhardt 99299e556c Merge pull request #590 from pstef/mode-3-4
Widen FPRreg to host double
2026-08-16 11:06:22 -07: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
refractionpcsx2 bc268a7e83 GameDB: Add Prioritize Lower depth fix to Steambot Chronicles
(cherry picked from commit cde2448a6cd0d27f6651c59b43102d27dcb45e48)
2026-08-16 12:46:34 -04:00
refractionpcsx2 34333fe6bd GS/HW: Avoid detecting shuffles as source of truth for target format
(cherry picked from commit ded263f98aa2862b56439ea3c0b552748e4e64f0)
2026-08-16 12:46:34 -04:00
refractionpcsx2 0d558ce69f GS/TC: Improve single pixel overlap detection in RT in RT
(cherry picked from commit ffa569065638312d9c01a47f760130a2109ce7cc)
2026-08-16 12:46:34 -04:00