25057 Commits
Author SHA1 Message Date
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
Brian Degenhardt e1f8fb1c56 Settings: a knob missing from the key table should not be a crash
The tables are meant to be complete and the drift tests are what say so, but
ForCoreKnob returns no section for an enumerator nobody mapped, and both callers
walked straight into strcmp with it.
2026-08-16 14:22:00 -07:00
Brian Degenhardt a6d1e35748 Settings: copying global settings into a game writes only the real decisions
"Copy Global Settings" does not copy the settings you can see. It runs the whole
configuration through a wrapper that writes every key unconditionally, so the file
it leaves behind holds roughly seven hundred of them — network adapters, the
debugger, trace logging, memory cards, sections no settings page ever shows.

That was untidy and no worse, until a key present in a per-game file started
meaning the player claimed it. Now one press of a button whose dialog promises only
that "the configuration for this game will be replaced by the current global
settings" turns off every automatic fix that game had, permanently and silently.

A value is worth writing down only if it decides something, and there are two ways
it can fail to. It can be the stock default, in which case the file carries it as
noise. Or it can be what the game database is going to set anyway, in which case
writing it can only become a claim that suppresses the fix it agrees with. So the
copy now excludes both, and what lands is what the player actually chose.

The comparison goes through the string form rather than the typed value, so a float
or an enum name compares the way it will be stored rather than the way it happens to
sit in memory. That is why the references are built with the same interface class:
same formatting on both sides, exact comparison, one path for every type.

The database reference is a default configuration with the entry applied, not this
one with the entry applied. The question is what the database wants, not where it
would leave the source. It matters for the handful of fixes that clamp rather than
assign, and it errs towards writing the player's value — never towards dropping a
fix, since a value is only skipped when it already equals what the fix would set.

Working the reference out means running the apply functions for an outcome nobody is
going to run with, so they take an apply mode. A hypothetical apply says nothing to
the log, raises none of the recommendation messages, and does not allocate the four
megabyte lookup table that the Goemon TLB fix asks for.

The tests cover the precedence rule and the filter, but the ones that matter are the
drift guards: they assert every gamefix, speedhack and clamp mode has a settings key,
and that the only graphics fixes without one are the six that genuinely have no
setting behind them — three renderer routine selectors and three that only raise a
recommendation. A knob nobody maps is a setting that goes quietly back to being
overridden, with no warning and no failure, and that is what these are here to catch.
2026-08-16 14:21:25 -07:00
Brian Degenhardt 96932405ce Settings: a setting the player set for one game outranks the game database
Change a setting in the per-game settings screen and it does not take. The widget
keeps the new value, the file on disk keeps it, and the emulator runs with
something else — for every game the database has an entry for, which is most of
the ones anyone plays.

The layered read is not the problem; it already puts the per-game file above the
global one. The inversion is that the database writes into EmuConfig afterwards,
from ApplyGameFixes, with no idea where any value came from. So the real order is
global < per-game < database, and the only ways out are two switches that are all
or nothing: EnableGameFixes for the CPU side, Manual Hardware Fixes for graphics.
Either one rescues the setting you wanted by throwing away every other fix the
game had, which usually just trades one glitch for a different one.

So tell the database which settings are not its to write. A key sitting in a
per-game file is a deliberate act: every settings screen represents "use the
global setting" by deleting the key, not by writing the global value. That makes
presence the record we need, with nothing new to store and no frontend to change,
and it works for files already on disk rather than only for edits made from here
on.

PerGameOverrides reads one per-game file — the game layer alone, since the layered
stack cannot tell a per-game choice from a global one and only the per-game one
wins — and reports what it claims. Both apply functions take it and leave those
settings alone, down the road they already had for the global switches, so each
one still gets named in the log and now also on screen.

This generalises the pin that already existed for graphics hacks and had no writer
outside iOS. That mask stays, because MaskUserHacks and any INI already carrying
one speak it, and the derived bits merge into it. But the claim itself is keyed by
hardware-fix id rather than by that mask: there are more fixes than its 32 bits can
name, its width is a persisted format, and the settings players actually change —
mipmapping, trilinear, deinterlacing, texture preloading, blend level, download
mode — were never user hacks and so had no bit at all. That is also why the pin
test moves out from under isUserHackHWFix: nested there, none of them could be
claimed.

The database keeps the last word where it should. The three renderer routine
selectors have no setting and no UI, so nothing can claim them. The BIOS path is
untouched — it forces instant DMA on for a hardware reason and strips hacks
deliberately, and its own comment already says claims get no say there.

The iOS bridge derived the same thing from its own copy of the key table; it now
calls the shared one, so the two cannot drift. Android's live GS apply re-derives
the claims after its reload, or masking would strip a hack the player set.

Drive-by: the division rounding mode logged eeRoundMode's value under its own name,
in both the applied and the skipped message.
2026-08-16 14:12:01 -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
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