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.
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.
arm64 hand-rolls these ops instead of going through the mVU emitter, so
microVU's per-op clamp argument is retyped in iCOP2-arm64.cpp. The two
rows conditional on isCOP2, MADDw's cACC|cFt|cFs and MSUB's cFs, were
missing.
Shadow of Destiny moved its player character a fraction of a step per
frame under the recompiler, at every eeClampMode, on the MADDw row.
Clamping ACC also changes results against the interpreter for values
above FLT_MAX, but is what the x86 COP2 path already does.
eeFprSyncSlotFormat keys on CHECK_FPU_FULL, so the relocated file is
what both of iFPUd's rungs compute over, not mode 3 alone. The comments
that name a mode where they mean the tier are renamed to the tier.
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.
The multiply array's out-of-line call carries its own spill of the
allocator's live caller-saved homes and its own pin flush/reload pair.
The divide unit needs the same frame around calls of its own, so the two
halves become emitIslandEnter/emitIslandLeave and the multiply is
rewritten on top of them.
Enter takes a byte count for scratch above the saved registers, which
nothing needs yet at zero and RSQRT.S will: its dividend has to outlive
the square root's call.
Both narrowing helpers compared the wide result against kEeFpuMax and
saturated on anything above it. A result can be above it and still
narrow back onto it, and then nothing saturated and O must stay clear;
ADD/SUB and the accumulates reach that band whenever the guard mask
leaves the smaller operand its leading bit and nothing else. Masking the
low 29 mantissa bits before the comparison is that narrowing, under the
arithmetic FPCR. DIV/SQRT/RSQRT round to nearest and pass flags=false,
so their test is unchanged.
The mask sits on the arm that has already branched away for 2^128 and
up, so the in-range path is untouched.
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.
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.
The same three moves as DIV: the sign test reads bit 63, Fcmp against
zero reads the slot, and the saturated result assembles from
kEeFprSignBit and kEeFprMaxBits. |ft| becomes unconditional, since it is
a no-op on the positive arm and is also the copy that keeps the guest
slot intact — which is what narrowSrc was there for.
108 host instructions to 75.
narrowSrc, ToDouble and ToDoubleFrom are down to one caller between
them: the ADD/SUB guard mask, which rewrites the word.
recDIVhelper1 narrowed both operands so it could compare them against
zero and xor their signs in the word domain. Fcmp against zero reads the
slot the same way — FZ flushes an EE denormal there too — and the sign
sits at bit 63, so the saturated result assembles from kEeFprSignBit and
kEeFprMaxBits without a word ever being formed. Both arms leave a slot
now, which costs the caller a register move where it paid for a widen.
102 host instructions to 69, and four temp NEON registers to two.
The body narrowed ft to the architectural single, tested the sign bit
there, then widened that single back to a double through ToDouble's
exponent-0xff branch. The slot carries the sign at bit 63 and holds the
value scaled by a power of two, so the test reads the slot as it stands
and the widening is SlotToDouble's one exact multiply.
71 host instructions to 54.
The double tier narrowed both slots to words, wrapped each in a 64-bit
double pattern, ordered them with Fmax/Fmin and widened the winner back.
The relocation is order-preserving, so the ordering key the fast path
already uses reads the slot unchanged, and Csel picks between the two
untouched registers.
24 host instructions to 14, and two temp NEON registers to none. What is
left over the fast path's 12 is the fprc[31] memory RMW this tier owes.
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.
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.
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.
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.
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.
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.
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.
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.
The recompiler already has a policy for arriving at address zero. A fetch
at PC=0 raises an Address Error and the BIOS handler takes over (AX-11),
because PS1 mode drives the IOP there through a register jump often
enough to be worth modelling rather than asserting on.
The immediate form of the same event never got there. Emitting a jump
whose target is zero asserted instead, so the two ways of reaching the
same address behaved differently: through a register it is emulated and
the guest carries on, through `j 0` it aborts a Devel build one
instruction before the handler would have seen it. Dropping the assert
routes the immediate form into the existing path — the tail stores pc,
links the block at zero, and the dispatcher hands it to psxRecompile,
which raises the Address Error.
Unlike the EE, nothing here compiles a jump the guest does not take: the
IOP scanner ends every block at the first branch, so an unresolved weak
symbol's guarded `jal 0` is never emitted. Reaching this needs the guest
to genuinely jump to zero — an unguarded weak call, a branch target that
computes to zero in low RAM, or a corrupted code word.
The test runs the JIT arm alone, which is what the new harness mode is
for: the interpreter has no PC=0 model at all, so the arms are meant to
disagree here and the differential harness has nothing to say.
A call to an unresolved weak symbol links as `jal 0`, guarded by a null
test on the symbol's address that always skips it. PS2SDK's libc glue
ships four such sites, so every homebrew ELF built against it carries the
shape, and the recompiler asserted the moment it met one.
It meets one because SL-03 continuation compiles the skipped path: the
guard branch becomes a continuation site, the scan runs on through the
dead call, and the emitter is handed a zero target for code that never
executes. The assert (inherited from the x86 recompiler, which aborts on
the same ELF) then takes down any Devel build before the program starts.
Nothing needs to happen at that target. If something did jump there,
address zero resolves like every other address — a block in RAM page 0,
or the unmapped-page handler — so the three tails just emit it.
The shape only reaches the emitter when the guard cannot be resolved at
compile time; a constant address folds the branch and the dead call is
never emitted, which is why an ELF carrying it can run clean until one
block boundary lands between the address materialization and the test.
The tests pin the reachable half.
71 commits from 474ad59818 to 2cf8dabe6b, triaged rather than taken wholesale.
Declined, resolved to ours:
- AGENTS.md: upstream's AI-agent instructions; we carry our own and do not
want a second, conflicting policy file.
- CI deps bump (setup-node, labeler): both target workflows are absent here,
and the labeler job is gated on the repository being PCSX2/pcsx2.
- KDDockWidgets 2.4.1: two of the six files do not exist here; we already
build 2.4.0 against a 2.3.0 floor, so there is nothing to gain.
- The FullscreenUI Achievements-layout realignment: our section already
carries the same settings, and ours is the branded copy.
- The GS draw/vertex-buffer cluster (7887919e74, b2fa00844e, 99cfbb49c1,
5c611f85e1, 9945046a49, af48193ebb, d88510e3a6, 8c1bb5742e). Our vertex
kick is an ARM64 rewrite of the same hot path -- register-resident cursor,
fused min/max with a rewind watermark, and a scalar cull mirror that
dual-issues against the NEON parse -- so upstream's generic pointer-logic
optimisation is a variant of work already banked here, and their growth
restructure replaces per-buffer capacity with a single global value, which
the pooled draw-node model cannot express. Two of the four August commits
in that cluster repair regressions the July rewrite introduced, and the
third's genuine fix (staging arrays sized from an unrelated buffer) we had
already made independently.
Taken with adjustment:
- EATAN coefficients (aae9438f98). Upstream relabelled mVU_Globals so the
names match the powers; we had fixed the same defect by ordering the arm64
call sites by power instead. Both fixes are correct alone and CANCEL when
combined, so the arm64 call sites move to plain ascending order in the same
commit. The values never moved, so this emits an identical instruction
sequence. Their fix also repairs the x86 mVU we still carry.
- Shader cache version: upstream numbered their tfx.glsl change 109, which is
below our 110. Taking their value would hand every user a stale blob, so
this lands as 111.
- FullscreenUI: took the two readback-spin toggles, placed outside our
non-Apple guard rather than inside upstream's unguarded run.
- Restored tools/generate_fullscreen_ui_translation_strings.py, dropped by
431ca0c063, and regenerated both string areas. That also registers the Big
Picture setup-wizard strings, which had never been extractable.
GameDB: the three serials upstream gave gsHWFixes (SLES-53869, PAPX-90020,
SCPS-15064) are absent from the mobile overlay, so no fix is silently erased
on handhelds.