470 Commits
Author SHA1 Message Date
jpolo1224 ff2b0c2155 Merge branch 'pr-593' into jit-android-catchup-gv7 2026-08-21 15:08:40 -04: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 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 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 99299e556c Merge pull request #590 from pstef/mode-3-4
Widen FPRreg to host double
2026-08-16 11:06:22 -07:00
pstef 5598420cdb Tests: pin the COP2 macro MADD/MSUB clamp sets against the interpreter
Exponent-0xFF operands through VMADDw and VMSUB at every dest mask, one
case per half of MADDw's set. MADDx/y/z runs the same shape with its
broadcast lanes in range as a scope control.
2026-08-16 17:02:58 +02:00
pstef 8ee895a649 EE/FPU: divide by the unit's own recurrence at eeClampMode 4
DIV.S, SQRT.S and RSQRT.S ran on host doubles on every recompiler rung,
which makes them correctly rounded; the EE's divide/square-root unit is
a digit recurrence with no rounding step and is not. The interpreter has
run the recurrence since the model landed, so mode 4 now calls the same
two functions out of line rather than reproducing them. eeDivide and
eeSqrtBits stop being static for it.

RSQRT.S composes the two with an ordinary single in between, as silicon
does, and its intermediate crosses the square root's call through the
island's scratch. Only the value moves: the negative-operand and
zero-divisor arms, and every flag they set, are the code they were.

Modes 1 to 3 keep the host instruction and the FPUDivFPCR swap.

The rsqrt sign table gains a mode-4 leg, which owes every row; the
filter for the twenty rows mode 3 misses by one ULP stays where it is,
now as that mode's own pin. The divide unit's console table gains a
mode-4 leg beside its interpreter one.
2026-08-16 00:11:47 +02:00
pstef ac9ca7c2b7 EE/FPU: pin where O comes from against the console
A standalone probe of the exponent band the FP matrix corpus does not
reach: a large operand at exponent 255 against an addend walked across
the guard mask's erasure boundary and the narrowing boundary just above
it, through all eight add/sub-family forms. Both engines and both rungs
of the DOUBLE path are scored separately, and the table is checked for
rows of each polarity so an engine that never raises O cannot pass it.
2026-08-16 00:11:46 +02:00
pstef 366308d53c EE/FPU: guard-mask ADD/SUB's operands in the wide domain
The mask keys on the exponent difference and clears low mantissa bits,
and a widened slot carries both: the exponent field moves from bits
23..30 to 52..62 and mantissa bit k to bit k+29, so FPU_ADD_SUB_D --
already the form the accumulates use -- is the same law with the shift
29 further left. That leaves nothing wanting the architectural single,
so both operands go straight through SlotToDouble, and the narrowing
pair and the widening's exponent-0xff test go with the single-domain
form. ADD.S and SUB.S drop from 127 host instructions to 93, ADDA.S and
SUBA.S from 132 to 98.

recFPUOp was the only caller reaching the arm that masks ft, which the
console corpus never sampled, so the new witness table covers it against
the interpreter and the console rows now also run through the DOUBLE
path.
2026-08-16 00:11:46 +02:00
pstef a0eaa92c0a EE/FPU: narrow SQRT's root with a plain Fcvt
ToPS2FPU_Full's saturating and flushing arms are for results that leave
the PS2's range, and a root does not, so both sat dead behind their
compares. The body goes from 54 host instructions to 23, under the 26
the single-precision path spends.
2026-08-16 00:11:46 +02:00
pstef 7b133ae02f EE/FPU: pin the multiplier deficit away from exponent 127
Both capture-backed tables read the operands the fpmul3 sweep produced,
which is exponent 127 with both operands positive. Every model of the
deficit here reads the significands and nothing else, and nothing said
so.

Sixteen rows from a probe that moves the other two fields: four sign
combinations across five exponent placements for eight separating
operand pairs, of which the extremes are kept. All 160 rows the probe
ran came back one ULP low and no operand pair changed its verdict.
2026-08-16 00:11:46 +02:00
pstef 9c05af0901 EE/FPU: pin both clamp modes' multiplies against the console
The two rungs differ on two classes of operand and the 8137-case
hardware corpus reaches neither: its zero-tail multiplies all carry ft
mantissas the boundary term cannot read, and its rows inside the array's
band either saturate or come back exact. It scores mode 3 and mode 4 the
same, so nothing in the tree said which of them silicon agrees with.

The fpmul3 capture does. It swept eight fs significands against every
one of the 2^23 ft significands on an SCPH-90000, recording for each
whether the console returned the correctly-rounded product or that
product one ULP low, and those two are the only outcomes it ever saw.
Twenty rows where the boundary term decides and twelve inside the band
are transcribed here with both values, so each asserts mode 4 against
silicon and mode 3 against the rounding it is supposed to keep.

Half of each table has the two values equal, which is the polarity that
stops an emitter that decremented every product from passing.
2026-08-16 00:11:46 +02:00
pstef b0ddecfede EE/UI: Remove the FPU multiply gamefix
It patched one product, 0.25 * pi, from the correctly-rounded 0x3f490fdb
to the 0x3f490fda the EE's multiplier returns, so Tales of Destiny stops
hanging. That product is one sample of the multiplier's one-ULP deficit,
and eeClampMode 3 models the deficit for every operand pair -- including
the asymmetry the gamefix reproduced by comparing fs and ft against
their own constants, which falls out of a predicate that reads ft alone.
The seven serials carry the clamp mode instead, and iFPU-arm64.cpp's
multiply is a bare Fmul again.

The toggle goes from the Qt, FullscreenUI, Android and iOS front ends,
along with the GameDB schema enum and the harness helper that set it.

Fix_FpuMultiply and the config bit stay: vu_capture's on-disk gamefix
mask is bit-indexed by GamefixId, so dropping ordinal 0 would
reinterpret every capture already recorded. tbl_GamefixNames keeps the
name, so a GameDB that still lists it parses and does nothing.
2026-08-16 00:11:21 +02:00
pstef 94c4fa72ea EE/FPU: split iFPUd's multiplier deficit into a fourth clamp mode
Both iFPUd modes emitted the multiplier deficit in full: the Booth term,
the boundary predicate over it, and an out-of-line call to the multiply
array for what neither decides. eeClampMode 3 now emits the Booth term
alone, three instructions off ft's mantissa; a new eeClampMode 4 keeps
the other two.  Nothing else differs between them.

Mode 4 reaches the config through the GameDB and the INI and has no
picker entry, but the front ends still write the bit: ApplySanityCheck
rejects a config whose bits are not a whole mode, and a rejected config
falls back to the default rather than to the mode that was picked.

The harness's clamp-mode helpers set whole modes for the same reason.
2026-08-15 23:57:53 +02:00
pstef abd6a73bc2 EE/FPU: call the multiply array for the rest of the deficit
A product whose tail is non-zero but smaller than the array's 2^15
borrow still loses an ULP, and ft alone does not say which of those it
is; only reconstructing the truncated low columns does. Mode 3 guards
for that band and calls eeMulOneUlpLow, which the interpreter already
reaches inline.

The call is plain AAPCS from inside a block, and the only one this file
emits, so it carries an island: the allocator's live caller-saved homes
are spilled around it and the EE pin mirrors go through their
flush/reload pair.
2026-08-15 23:57:43 +02:00
pstef 03da2331ae Tests: give an EE snapshot the FPR format it was captured in
A slot does not say which format it holds, and the global that does
moves with eeClampMode, so a snapshot decoded under a mode other than
the one it was taken in returns a different number for every non-zero
word. Two harnesses alive at once is enough to reach it: an interpreter
leg read after a mode-3 leg has run.

The format now travels with the bytes, the FPRs are read through the
snapshot rather than through FPRreg::Word(), and a restore re-encodes
them for the file it is going into.
2026-08-15 23:57:35 +02:00
pstef 7b8e56a144 EE/FPU: give mode 3 the multiplier predicate's boundary term
The predicate was a Cmtst against a mask parked in d10, which can
express the Booth term and nothing else, so iFPUd came back one ULP high
wherever the boundary term at the truncation column was the deciding one
-- a class the interpreter models. Building the predicate in GPRs
instead admits both terms, and turns the old shape's implicit tail test,
which worked only because FPUFPCR holds round-toward-zero, into an
explicit one.

Nothing reads d10 after that, so it leaves the reserved list and the
allocator has its callee-saved slot back.
2026-08-15 23:57:30 +02:00
pstef 8542a9c38a EE/FPU: compute mode 3 in the relocated domain
iFPUd widened every operand with ToDoubleFrom: eleven instructions
branching on exponent field 0xff, since a PS2 single with that exponent
is an ordinary large number IEEE reads as Inf or NaN. Against a
relocated slot the widening is one Fmul, and a compare goes from 34.75
host instructions to 8.75. CVT.W loses its NaN fix-up the same way,
leaving Fcvtzs.

Which format the file is in follows the clamp mode, so modes 0-2 keep
the architectural word in the low half. FPRreg's accessors read the
format and the emitters read the mode; the two engines meet at the
boundaries the word is still observable at -- MFC1, MTC1, MOV.S, the two
conversions, LWC1, SWC1 and the allocator's fill and spill.

The unscale factor takes q11 and d10's mask moves to the relocated
layout, so the allocator's callee-saved range is q12-q15.
2026-08-15 23:57:24 +02:00
pstef c5183278e5 EE/arm64: measure the callee-saved NEON budget
The header called the range's size the minimum because a 3-operand MMI
op needs three homes at once. The floor is three; the slots above it
keep an FPU multiply-accumulate's four FPR homes inside the
call-surviving range.

EeFuzz.CalleeSavedNeonBudget reads high-water marks of the per-op needed
set from a test-build probe in _clearNeededNEONregs.
2026-08-15 23:57:20 +02:00
pstef 39319f35b4 EE/FPU: read and write an FPR through an accessor
The slot is 64 bits and the architectural register is 32, so a word view
of the union is only right while the two coincide. Word() and SetWord()
replace f/UL/SL and are the identity, so nothing that runs changes; what
it buys is that the compiler names every place that reads a slot as a
word, which is the set the next commit has to relocate.

The x86 tier is not built here and still reads the members directly.
2026-08-15 23:57:13 +02:00
pstef 8b0494b51f EE/FPU: add the FPR word <-> host double relocation
EeFpuFormat.h defines the format and AsmHelpers.h emits it. Widening a
slot already held in it is one Fmul against a parked 2^896, where mode
3's ToDoubleFrom branches on exponent field 0xFF. Nothing computes in it
yet.

Arm64JitBuffer.h is arm64_emit_test.cpp's MAP_JIT buffer, unchanged,
moved out so the relocation's emit tests can call the code they emit
too.
2026-08-15 23:57:09 +02:00
pstef ce6484d947 EE/FPU: widen the FPR file's slots to 64 bits
The word stays in the slot's low half. Nothing but the stride moves.

fpuRegisters was itself the savestate's wire form; fpuRegistersWire is
now, and the freeze boundary converts.
2026-08-15 23:57:03 +02:00