Commit Graph
24982 Commits
Author SHA1 Message Date
Brian Degenhardt 112bc73c4c Android: take a card snapshot at launch, and offer the restore
Wires MemoryCardBackup into the app.

The snapshot is taken immediately before the emulation thread starts, in
MainActivityRuntime.start() and startBios(). At that instant the card file is not
open, so the copy cannot catch a half-finished write and there is no thread
timing to reason about. It also means the copy holds the card as it stood when
the player last finished successfully -- if this session is the one that breaks
things, the snapshot is clean by construction. Restoring then loses the current
session's saves, which is the trade a save-state slot already makes.

The BIOS boot gets one too: its memory card manager can format a card or delete
saves off it, so that session is worth a copy for the same reason a game is.

Launch also checks the cards it is about to mount. If one will not read AND a
verified backup exists, the boot is HELD and the prompt offers to put it back
before the game starts. That ordering is not cosmetic: once the console has
mounted a card it caches its own picture of the directory in guest memory, and a
restore underneath would be written straight over. "Start anyway" stays available
-- some people will want to format fresh -- and is remembered only for the launch
it was answered for.

The memory card screen gets a per-card Backups panel: the snapshots with their
date, size and the game that was running, a verified-or-suspect badge, restore,
back up now, and the automatic-backups switch. That manual path is the one that
actually matters, because the automatic offer cannot fire for the failure players
hit most -- a card that verifies perfectly while the save inside it is damaged.
Recognising that would mean understanding each game's save format.

Restore is refused while a game is running, for the cache reason above, and says
so rather than failing quietly.

A suspect snapshot is listed rather than hidden: the pre-restore copy of a broken
card is exactly what someone may need back.
2026-08-20 09:24:01 -07:00
Brian Degenhardt 8e07613231 Android: rolling per-card memory card snapshots
Nothing today keeps a previous version of a memory card. A file card is written
straight through to disk as the game plays, so the card on storage is always the
only card there is; anything that interrupts a write leaves the real card
damaged, and the player finds out when they load.

MemoryCardBackup keeps a small rotation of snapshots per card so that is
recoverable. This commit is the engine only -- nothing calls it yet.

Two rules do the real work:

  - A card that fails verify() is never snapshotted. There is nothing worth
    saving and a rotation slot to lose. That same check is what detects the
    user's problem, so it is reported rather than swallowed.
  - prune() never deletes the newest snapshot that passed verification. Without
    that, the rotation is a shredder: the card breaks, the player relaunches five
    times trying to work out why, and every good copy has been overwritten with a
    copy of the broken card.

Retention is the newest 3, plus the newest from each of the preceding 4 distinct
days, plus always the newest verified copy however old. Three copies from one
afternoon only protect against that afternoon.

verify() is the same test the core trusts (FileMcd_IsMemoryCardFormatted): the
PS2 format signature at the head of the card, or the folder card's superblock
marker. Deliberately a size FLOOR rather than a table of exact sizes -- strict
about the signature, permissive about the size, so it refuses damaged cards
without refusing unusual ones that work.

Snapshots are content-hashed, so relaunching without saving costs nothing. They
are written to a .part file and renamed into place, so a process killed mid-write
leaves no truncated archive that would list as valid -- the same class of bug
this feature exists to undo. Restore stages and swaps through a sidelined copy,
outside the cards folder: a folder card is a directory, and the card list shows
every directory under memcards/ as a card, so staging there would flash a phantom
card mid-restore and leave one for good if the process died.

Restore takes a snapshot of the live card first, unconditionally, including a
broken one marked as such. Restoring the wrong copy must not be the act that
destroys the evidence.

Written against the Java file APIs rather than the core on purpose. The storage
memory cards live on is FUSE-emulated on some Android configurations, where
libc's file-creation call is denied outright -- a problem this tree has hit twice
and worked around both times from Java. A snapshot writer in the core would fail
silently on exactly the devices that need it.

The rotation joins the whole-app export: a card restored onto a new phone from
that archive is just as likely to be the broken one, so the history that can undo
it has to travel with it.
2026-08-20 09:22:21 -07: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.
nightly-20260820
2026-08-19 19:35:22 -07:00
Brian Degenhardt 7f0ae7a6c6 GameDB: internal FPS by DISPFB blit for NASCAR Thunder 2002 through NASCAR 06
The register-write detector reads these engines as producing a new image every
vblank, so duplicate-frame skipping never fires and every repeat pays for a full
present. Counting blits into the displayed framebuffer restores it.

Reported-by: yuasasa
nightly-20260819
2026-08-18 20:04:16 -07:00
J1coding be72a8e1eb iOS: put Download Shaders under Preset, where it can be reached
It shipped as its own Section on the settings page, which put it below the
parameter list. With crt-aperture selected that is twenty-two sliders and
about thirty-nine swipes, on the one control a tester had asked for by name.
Found by walking the screen in the simulator rather than by reading it,
which is the only way this kind of thing turns up.

It moves into ShaderChainSection, directly under Preset and above Install
Shader Pack, so the three ways to get a preset sit together in the order you
would try them: pick one you have, download one, install one from a file.

That also puts it in the in-game pause panel, which is a gain rather than a
side effect, and it works only because GameScreenView wraps the shared
section in its own NavigationStack. Without that every NavigationLink in the
section is dead on tap, Preset included, so the fence now checks for the
stack in the text immediately around the mount.

Immediately around, and not anywhere earlier in the file, because the first
version of that check searched backwards from the mount through the whole of
GameScreenView and any one of its several other NavigationStacks satisfied
it. Deleting the one that matters left the suite green. That is the third
time in this branch an assertion has been satisfied by a neighbour, and the
only reason any of the three were caught is that each new check was run
against a deliberately broken source before being trusted.

The row sits outside the enabled gate, which the first attempt at this move
got wrong. Every other row in that section is behind `if enabled`, and
putting the download row there too hid it whenever the chain was off -- so a
first run had nothing to select, no way to fetch anything, and no hint that
the toggle came first. Caught by relaunching with the chain off and looking,
one screenshot after the change built.
2026-08-18 23:41:53 +02:00
J1coding 4fa820435d iOS: decode a launch link's filename once, not twice
queryValue returns its answer already percent-decoded on both of its paths:
URLComponents decodes for the ordinary case, and the raw-query fallback
decodes by hand for callbacks that arrive unencoded. launchGame then decoded
it again.

A second pass reads a literal percent in the value as the start of a new
escape. 100%.iso percent-encodes to 100%25.iso, the first decode gives back
100%.iso, and the second sees % followed by .i, which is not hex, so
removingPercentEncoding returns nil. The guard falls through and tells the
player the link is missing a game filename, which is the one thing it
plainly carries. Only the launch route did this; exportLibrary reads the
same helper and does not.

The contract now sits on queryValue rather than being something each caller
has to know, since knowing it is what went wrong.

The fence pins more than the fix, because armsx2://launch?game= is not an
internal detail. libraryPayload hands that string to other frontends, which
store it and replay it much later, so the verb, the parameter name and the
encoding are a contract with software this repository does not control. It
also compares the schemes the handler accepts against the ones Info.plist
registers, in both directions: a scheme in code but not in the plist fails
silently, because iOS never routes the URL and the handler that would have
accepted it is never reached.

Six mutations run against the real source, all six caught, each restored
byte for byte. The scheme check needed the second direction to catch the
sixth; the first version iterated the known list and could not see an
addition.
2026-08-18 23:41:53 +02: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 af30d18304 iOS: three defects the branch review found, two of them silent
Seven lenses over the branch, each finding then handed to a skeptic told to
refute it rather than confirm it. Thirty-one were raised. These three
survived and matter, and two of them fail without saying anything, which is
why none of them turned up in a device pass over a green suite.

Per-game write() set the enabled key straight from the picker, before the
guard that needs the preset to resolve. Choose On, delete the pack the
preset came from, then save any unrelated row on that game: the file keeps
the chain enabled and loses both preset keys, and an absent key in the game
layer falls through to the base layer. That game then renders the GLOBAL
preset. The type's own first comment says this never happens and boot-time
repair has always got it right; write did not. It is the worst kind of wrong
because it is invisible -- one CRT shader looks like another, so the player
sees a filter and assumes it is theirs.

A per-game preset never received its saved parameter values at all.
SettingsStore pushes the global tier's overrides at launch and on every
change, but a per-game preset is chosen in a file SettingsStore never reads,
so the game rendered the shader author's defaults and every value saved
against that preset was ignored. The boot repair already resolves that token
before bootISO, which is the one place that knows both the token and the
timing; pushStored is nonisolated now so it can be called from there without
hopping actors and losing the ordering.

Save as New Preset could destroy the preset it was saving from. The
reference it writes is relative to My Presets and the sheet pre-fills the
base's own name, so selecting a saved preset, nudging a value and accepting
the default replaced that file with one whose only reference is its own
filename. Nothing resolves that, and the values it held are gone. It refuses
now, in the write path, which is the only place that can see both the target
and the base.
2026-08-18 23:41:53 +02:00
J1coding 30d9816eda iOS: resolve the shader catalogue off-device, and sign what it may ship
The half of the downloader that cannot run on a phone: a generator that
turns any preset in a pinned slang-shaders tree into a complete, path-safe,
licence-classified file closure, and refuses to emit anything until a person
has signed the rules it would be built from.

Resolving a closure means walking includes and references across a
5,000-file tree. Over the GitHub API that costs two to five requests per
preset against a 60-per-hour limit keyed to the originating IP rather than
to the app, so every user behind one carrier NAT shares one budget. On a
local clone it costs fifteen seconds of CPU and no network at all. That
asymmetry is the whole design.

emit refuses without a signed rules file recording the pin, so the catalogue
cannot physically exist before the nine class questions were answered. Six
were confirmations of rules the bundled sixteen already ran under. Three had
never been decided and were worth 577 presets between them, and the one that
mattered was whether a LICENSE file governs the directory it sits in --
worth 552 on its own, and exactly the inference the standing rule exists to
refuse. Admitted, with the reasoning in the signed document rather than
here.

Of 2,553 presets in the tree, 867 are offered: 13 dropped on upstream
defects, 8 on an extension the extractor will not write, and 1,665 excluded
by class. Fourteen presets the earlier hand audit had measured agree row for
row on file count and on upstream bytes, which is the free correctness check
on all of it.

The whole-tree run found the divide-by-zero prescale in ten more files than
the two bundled ones, refusing 98 presets. Twelve sites and not ten, because
the scanner reports one per file and clamping the first in crt-potato and
ultra_potato made a second visible in each; the scan was re-run until it
came back empty.

All twelve now carry a notice in the file itself saying it changed and when.
ATTRIBUTION.md covers the bundle and covers nothing once the same file
travels in a zip on its own, which is where GPL section 2(a) asks for the
notice anyway. The first wording of that notice said the change was "one
max() and nothing else", and the guard test looked for max() anywhere in the
file -- so the comment describing the fix satisfied the test that checks the
fix exists. Both were changed: the notice says clamp, and the test now
requires the guard on a line that actually matches the prescale pattern.
2026-08-18 23:41:53 +02:00
J1coding e2b1bcaea2 iOS: let one game keep its own shader preset
A preset was a single global value, so picking crt-geom for a 2D fighter
also applied it to the next 3D game booted. The per-game subsystem is the
right home; the blocker was that its bridge exposed Int, Bool and Float and
no String, while a preset is a string token.

Four String accessors added, in the forISO and the current-game shapes the
twenty existing per-game settings already use. A Shaders section on the per-
game Graphics tab, where the global Shader Chain section sits, on the same
tri-state sentinel every other control there uses: use global, off, on.

Scope is preset only, decided with both arms in front of the developer.
Parameter values stay global and stay keyed by preset token, and the panel
says so on screen rather than leaving it to be discovered. A preset exposes
up to twenty-two values and per-game copies of those would multiply the
storage and the UI.

The identity is the same root token the global tier uses, re-rooted at boot
before bootISO reads the file, so a per-game choice survives a reinstall for
the same reason a global one does. ShaderPresetLibrary.resolve stays the
only token-to-path resolver; nothing here reimplements containment.

The rule that matters, and the one the fence exists for: a token that no
longer names a file turns the chain off for that game rather than falling
through to the global preset. A different CRT shader looks like a CRT
shader, so a substitution is invisible -- the player sees a filter, assumes
it is theirs, and never learns their choice is gone.

Six source checks over the six files the selection lives in, and four
mutations run against the real source with every restore byte identical.

Also here, because it landed in the same wave: the prescale fence widens to
.inc and .h. A .slangp names its stages, but a stage includes whatever it
likes, so that bug can sit in a header and never appear in a .slang -- which
is exactly where the whole-tree catalogue run found it.
2026-08-18 23:41:53 +02:00
J1coding 7e8f7f1955 iOS: give the shader controls their own page and a pause-menu route
Four things a tester asked for after playing the first build, and the two
defects found while building them.

Shaders are their own settings page rather than a section inside Graphics.
The section already took its persistence from the caller, so this is a move
and a root row.

The same controls reach the in-game Quick Menu, under Game Tools rather than
Quick Actions. Not a drop-in: the settings section is Section-shaped and
embeds a push, while the Quick Menu is card-shaped with no navigation stack,
so it routes out to a sheet the way the speed panel does. A test holds it to
that shape, because the shape is the thing that works rather than an
implementation detail.

Parameter rows use NumberRow, the control eleven other settings files
already use, so a value can be typed instead of only dragged. Detents, units
and the reset affordance come with it.

Every label is translated into the nine languages beside English.

Then the two defects. A preset's saved values reached the core only when a
shader screen was open, so a cold launch rendered the author's defaults
until the player visited the page -- proven by measuring frame luminance
across a launch rather than by reading the code. And the guard that keeps
SettingsStore.init off SettingsStore.shared was recovered from an orphaned
commit and turned out to be broken: a plus-or-minus 400 character window let
an allowlist entry cover its neighbour, so the test would not have caught
the crash it was written for. It requires the match to span the access now.
2026-08-18 23:41:53 +02:00
J1coding d24e9d76ed iOS: clamp a shader's prescale so upscaling cannot blacken the frame
crt-aperture and sharp-bilinear each derive a whole-number prescale from
output height over source height and then divide by it. RetroArch only ever
feeds them a small console framebuffer being scaled up, so that ratio never
falls below one. PCSX2 renders internally at up to 8x: past roughly 1.5x on
a phone the source is taller than the screen, the ratio drops under one,
floor() returns zero and the divide yields NaN. The whole frame goes black.

Reported on an iPhone SE 2 with a 1334x750 window, where 1.5x rendered and
2x did not. Two hypotheses were wrong first -- push-constant placement, then
parameter placement -- and both were refuted by tester data before the
reporter supplied the actual trigger, which was the internal resolution and
not the preset. Reproduced in the simulator at 3x and fixed there.

The clamp is what the sibling sharp-bilinear-simple already carries as
max(floor(...), vec2(1.0)) and what crt-geom carries as clamp(floor(...),
1.0, 2.0). Nine of the eleven bundled presets never divide by a derived
scale and were unaffected.

These files are otherwise byte-verbatim copies of a pinned upstream commit,
so the divergence is a reversible patch beside the librashader one and a
note in ATTRIBUTION.md. The test fails if either guard is dropped, which is
what a re-sync from upstream would otherwise do silently.
2026-08-18 23:41:53 +02:00
J1coding 16b571cf72 iOS: put the shader chain and its parameters in settings
A section in Graphics after Shade Boost, matching pipeline order, a folder-
at-a-time preset browser, and every parameter a preset declares on screen.

The section is absent rather than disabled in a build without librashader,
gated on a bridge capability, so a cargo-less build does not advertise a
feature it cannot run.

Every number in a preset's parameter block is the shader author's, so every
number is treated as hostile. Absent, non-finite, inverted ranges and a zero
step all occur in the published collection. A parameter whose range cannot
be made sense of is dropped rather than rendered as a control that does
nothing.

Pushing a value sends the effective value of every parameter, not only the
changed one. librashader has no unset call, so a name dropped from the
override map would leave the chain on whatever was pushed last and a reset
would never take.

A tweaked preset can be saved as its own file: a #reference to the base plus
the changed values, written into My Presets inside the scanned root so it
becomes selectable with no extra plumbing. The reference is relative while
the base is in Documents, so the pair survives the container moving; a
bundled base gets a path instead, which a reinstall breaks, and the sheet
says so.

The naming sheet is .sheet(item:) rather than .sheet(isPresented:), because
the parent's body invalidating tears the content down and takes keyboard
focus with it, which is the failure this codebase has a rule about.

Also here: the once-cached name lists are owned rather than read after free,
and librashader builds for the simulator as well as the device, which is
what makes any of this testable without hardware.
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 040104142a Tests: pin the I immediate against a rewrite of micro memory
Both polarities of the gamefix. With it on, a rewritten immediate has to
reach the block already compiled; with it off, the rewrite has to force
a recompile. Each row asserts the compile count as well as the result,
so neither can pass by recompiling behind the value it checks.

Three things the harness needs handling for. The first re-entry through
RunJitPreserveBlockCache compiles a second block variant, because it
enters on the pipeline state the previous run left rather than the
post-Reset one, so the block only goes warm on the re-entry after that.
LoadProgram writes VU.Micro directly and so bypasses the vtlb path that
calls mVUclear, which leaves the stale quick slot serving the old
program unless the test runs the invalidation itself. And the pair that
carries the immediate writes VI[REG_I] again on the way out, as does the
delay slot LoadProgram appends, so the register reads back 0 and the
assertion belongs on the VF result.
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
pstef 249aefdb9b Tests: pin the rec's TLB-miss divert, disabled
A TLB miss on an inline load or store leaves the arm64 EE rec at
exception level in user code. Eight cases, all disabled, because the rec
does not do this yet and the route to making it is staged: x86's
behaviour first as a floor, then the divert built back up with these
dropping their prefix one at a time.

Found from the other end. `3D Pinball Space Cadet (PS2) (3.0) (RA)`
hangs after "Parsing complete. Finalizing...", and the visible fault is
a thread id of 0xff966c22 arriving at a caller whose syscall returned 1.
That is strlen's `subu v0,v0,a0` on the correct v0, reached because the
kernel's syscall epilogue eret'ed into the middle of strlen: 131072 of
the run's next 162766 exceptions were taken with EXL already set, so
none of them updated EPC. Upstream of all of it is one swallowed miss on
a strlen(NULL). Two working hypotheses died on the way — that the
pinned-GPR cache lost v0, and that the call-ret shadow stack mispopped —
both refuted by reading the state at the moment of damage rather than by
reasoning about the emitters.

The tests are the chain in four instructions, plus the load and store
halves of both inline emitter pairs, plus the flush the divert needs.

Three separate defects fall out of the one missing poll. The block runs
past the faulting load. EPC names the instruction after it, because
cpuTlbMiss skips its `pc -= 4` for the rec while the rec's own cursor is
already one instruction ahead outside a delay slot — the delay-slot case
comes out right only because two errors cancel. And Cause is whatever
exception came last, describing a different instruction than EPC does.

Two findings shape the fix rather than the tests, so they are recorded
here.

Reaching the vector with guest state intact costs one writeback, not a
general flush: iFlushCall(FLUSH_VTLB) already precedes every inline
access and frees the caller-saved hosts, leaving only x28, the
allocator's single callee-saved host. A boot with fastmem off puts a
live dirty guest GPR there at 489 sites and nothing else anywhere.

The fastmem backpatch thunk is the part with no clean answer. It is
generated at fault time and cannot name the live guest values of the
block around it, so it cannot divert. A census of the 12411 fastmem
sites emitted during that boot says how much per-site state a precise
one would need: 9355 have nothing live and dirty, 2480 have between one
and seven GPRs, and 576 involve the NEON file.

Lesson, from a guard test that was written wrong first. Three dirty
registers before the faulting load is not enough to make the allocator
reach x28, so that test passed with the writeback deleted — it guarded
nothing. It takes sixteen live guest values at once. A test that guards
a writeback has to create the pressure that puts something in the
register the writeback exists for, and the way to find out is to delete
the code and watch.
2026-08-18 08:00:56 -07:00
pstef 83f2510134 Tests: pin the VU FMAC's range against the console
The VU's largest value is 0x7FFFFFFF, one binade above FLT_MAX, the same
range the EE FPU has. So an exponent-255 word is an ordinary number on
the way in and on the way out, and "overflow" starts above it rather
than above FLT_MAX.

Both engines put the boundary a binade lower. vuDouble() rewrites an
exponent-255 operand as 0x7F7FFFFF and VU_MAC_UPDATE() calls every
exponent-255 result an overflow; the arm64 COP2 macro emitters clamp the
result to +/-FLT_MAX and raise neither O nor U. microVU's per-op operand
clamps approximate the same thing from a list of games rather than a
rule.

68 rows off an SCPH-90000 through VU0 macro mode, scored per engine and
per column, with what each engine cannot yet reproduce recorded per case
so a fix trips the test as loudly as a regression. Nothing is fixed
here.

Two of the rows are structural rather than about range. An overflowed
product does not become 0x7FFFFFFF before the accumulate: an addend of
-0x7FFFFFFF cannot cancel it. An underflowed product does become zero
before it. And the multiplier is the EE's, with the same one-ULP deficit
decided by ft's mantissa alone.

The harness grows two things the rows need: a VADDA encoder, and an
opt-out from Run()'s VU0 JIT-vs-interp auto-diff for tests that score
each engine against a hardware capture instead of against the other
engine.
2026-08-18 08:00:56 -07: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