Commit Graph
100 Commits
Author SHA1 Message Date
Brian Degenhardt 2262c7bf55 Merge remote-tracking branch 'yaps2/main' into jit-transplant
# Conflicts:
#	pcsx2/arm64/BaseblockEx-arm64.h
2026-07-21 19:04:47 -07:00
Brian DegenhardtandClaude 3d71da84a1 arm64: bound the block-link map by owner liveness
Link() inserted into the link multimap unconditionally and nothing ever pruned
it, so entries accumulated for the whole lifetime of the map. Every recompile
of a caller emits its branch at a fresh code address and appended another
entry, and New(pc) then re-patched every site the map had ever seen for that
pc — each with its own 4-byte __builtin___clear_cache.

On Dirge of Cerberus (SLUS-21419) one PC had accumulated 13,372 stale sites,
and 93.5% of EE-thread cycles were inside __aarch64_sync_cache_range, reached
only via recRecompile -> Arm64BaseBlocks::New. The game hot-patches a small
routine ~14x/frame, which is legitimate SMC and legitimately forces recompiles;
the cost blew up quadratically because each recompile made the next one more
expensive. A 4-byte flush measures ~157ns on an Apple M2, where IC IVAU is
broadcast.

Each entry now records the block that emitted it (owner startpc plus that
block's code address), and both Link() and New() drop entries whose owner is
gone or has since been recompiled. Pruning in New() alone is not sufficient: a
destination that is never recompiled again would never have its list revisited,
so it would still leak. Sites judged dead are unreachable code — and a
false-dead would merely leave a site pointing at a removed block's redirect
stub, which routes through the dispatcher.

Two supporting changes:
 - Link() no longer flushes. Its patch site is inside the block being emitted,
   which armEndBlock() already covers with one whole-range flush. AetherSX2
   does the same: its Link() performs no cache maintenance at all.
 - New() coalesces the sites it patches into contiguous ranges and issues one
   flush per run. The per-call cost is the dsb ish / isb pair, not the DC/IC.

Measured, Dirge of Cerberus 3000 frames headless (--liverun, null renderer):
  fps           11.38 -> 238.8
  EE thread     253.44s -> 9.49s CPU
  EE cycles     829.7G -> 30.6G
  EE IPC        0.247 -> 3.64
  link map      grew 87k->188k in 2min, unbounded -> plateaus ~12k, tracking
                the ~6.8k live blocks
R&C UYA (no SMC pathology) is neutral: instructions identical, cycles +0.6%,
inside its own 0.26% run-to-run spread.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 18:57:55 -07:00
Brian DegenhardtandClaude 4d7fd5935e arm64: make BASEBLOCKEX::fnptr authoritative when a block is recompiled in place
recClear resets BLOCK->fnptr across the full extent of the blocks it removes
but deliberately spares the in-progress block's entry, so a startpc can be
recompiled while its BASEBLOCKEX is still live in the array. Both recompilers
handled that by skipping New() entirely (BaseBlockArray::insert() has no dedup,
so calling it would duplicate the entry) and reusing the old BASEBLOCKEX as-is
— leaving fnptr pointing at the compile that was just superseded.

Everything downstream of fnptr was then wrong: x86size is measured from the
dead base, Remove() writes its `B JITCompile` redirect stub over dead code
instead of the live entry, and New()'s pending-link patch never runs, so
callers linked to that pc keep branching into stale code.

Measured on Dirge of Cerberus (SLUS-21419): the reuse path is taken 6000+ times
per 600 frames and fnptr was stale on every single one of them.

New() now creates or re-binds, so both recompilers just call it unconditionally
and fnptr always names the block's current code.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 22:50:18 -07:00
Brian DegenhardtandClaude 3d4c08d378 perf: correct the MQ65 device profile's fan note
The MQ65 does have a fan (gpio_fan hwmon, on/off, thermal-driven with a
65degC active trip); the profile claimed the device was fanless. A
null-renderer codegen_ab session plateaus ~55-57degC and correctly never
engages it, so the measurement protocol is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 17:37:36 -07:00
Brian Degenhardt dbb7eade69 Merge remote-tracking branch 'yaps2/main' 2026-07-20 17:15:41 -07:00
Brian DegenhardtandClaude 8fa3a4db0d perf/census: classify COP2 VU0 macro traffic and 128-bit MMI NEON churn
Add four categories to the EE code census analyzer:
- cop2_vf_ldst(@VU0) / cop2_vu0_other for x24-based VU0 macro-mode
  load/store traffic (off<512 = VF/VI register file).
- neon_gpr_ld/st(q@GPR) for 128-bit GPR-home traffic (MMI NEON
  residency churn), kept out of the scalar pin categories since rd is
  a v-reg number there and pin comparisons would be meaningless.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 17:06:31 -07:00
Brian DegenhardtandClaude 679c230841 ee/fpu: make add/sub guard-bit emulation a toggleable option (default on)
Reintroduces the fpuGuardedAddSub Recompiler option removed in 37bc5e164,
but with the default flipped to ON so the PS2-accurate behavior is what
every game gets unless a title is explicitly opted out. The removal's
concern was per-game GameDB maintenance; a global default-on knob
sidesteps that entirely — no GameDB plumbing is restored, so games can
be flagged later once individually confirmed.

Both JITs gate the single-precision add/sub masking on CHECK_FPU_GUARDED
again (arm64 fpuEmitGuardedAddSub early-out; x86 FPU_ADD/FPU_SUB), which
also restores the eerunner --set fpuGuardedAddSub twindiff A/B knob. The
toggle only affects the fast path: Full clamp mode runs the DOUBLE path,
which guards unconditionally and ignores this bit. It composes
orthogonally with Extra clamp mode's operand clamping (that stays on the
fast path and is honored independently).

Exposed as a checkbox in both the Qt Advanced settings and the
FullscreenUI (handheld) CPU page, with tooltips noting the Full-mode
no-op. Test-side, EeRecTestHarness gains DisableFpuGuarded() and the
guard-bit suite pins the opt-out path
(EeRecFpuGuardBit.DisableEmitsPlainOpMatchingInterp). recompiler_tests:
1405/1405 green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 16:05:59 -07:00
Brian DegenhardtandClaude da22a8b672 Revert "SL-14: retain EE-class NEON through the COP2 sync seam"
This reverts commit 47ec1b7d6.

SD865 codegen_ab (7-run fan-pinned medians, EE-thread-scoped) attributed
a large dynamic regression to SL-14: UYA gameplay EE insns +3.3% /
cycles +11.4% vs the SL-13-only arm. The 25-quad blind save/restore in
the shared sync stubs is static-tiny but runs on every VPU_STAT-taken
seam, and UYA keeps VU0 busy enough that the taken path executes
millions of times per session — insurance for ~2-5 live registers paid
at 52 memory ops per seam. SotC (cold seams) was neutral. The M2 static
census (flagship -124 B) could not see this cost by construction.

SL-13 (clamp-constant residency) survives its own gate and stays:
UYA EE insns -0.69% / cycles noise-level, SotC -0.75% / -1.05%.

Measurement: /storage/pcsx2-profiles/s5{,-attr} on the SD865 device;
scratchpad/ee-surpass-2026-07/s4/SL13-CENSUS-2026-07-20.md.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 15:56:28 -07:00
Brian DegenhardtandClaude 47ec1b7d69 SL-14: retain EE-class NEON through the COP2 sync seam
cop2FlushForConditionalSync freed EVERY in-use NEON register at every
VPU_STAT-conditional sync seam, even though the C call sits behind the
runtime check and is rarely taken — in the hottest UYA EE block
(EE_003F7690, 16 seams) that meant a store->reload round-trip for the
same GPR quads every ~6 guest instructions (61% of its q-loads reloaded
same-block stores; the eejit ours-vs-aether snapshot pinned this as the
flagship's density residual vs aether's whole-block NEON residency).

Extend the S4-2 GPR/FPRC retention policy to the NEON file:

- NEONTYPE_GPRREG quads / FPREG / FPACC are KEPT mapped with no
  writeback. The shared sync stubs blind-save the pool NEON file
  (q0-q7/q10-q24/q27/q28, 25 quads, full 128 bits) around the C calls on
  the TAKEN path only — state-agnostic, no per-site knowledge, and the
  400 B of stack traffic sits on a path about to run a whole VU0
  microprogram. q8/q9 need only their callee-saved low 64 bits; q25/q26
  are re-materialized by the SL-13 Dups.
- NEONTYPE_VFREG still frees WITH writeback (the VU0 micro writes VF, a
  retained mirror would go stale — same reason the VI mirrors free, and
  the EP-2b VF compile-cache flush stays).
- TEMPs still free.

Soundness is the S4-2 invariant: the sync callees (vu0SyncThin /
vu0SyncRunAheadThin / _vu0FinishMicro / _vu0WaitMicro -> CpuVU0->Execute)
never read or write EE GPR memory or fpr/fprc, so stale canonical memory
during the call is unobservable; events raised by the micro are flagged
and handled at block end, where dirty entries write back as today.

M2 census (6000f UYA slot-02, identical 7644-block set, base = SL-13):
flagship 3636 -> 3512 B (GPR-quad loads 78 -> 58), 003F7820 -6.3%,
hot-90 sample-weighted bytes -1.8% — concentrated exactly where the SV-0
sizing said the class lives. Cumulative S5 vs pre-SL-13 base: hot-90
sample-weighted -7.7%, EE corpus -2.0%, clamp-const loads 8398 -> 0.

Gates: 1404/1404 recompiler_tests (3 new EeVu0Cop2SeamSurvival tests:
retention-filter policy probe, dirty-MMI-quad and FPREG riding a
runtime-TAKEN seam); 8000-seed EE fuzz green; UYA slot-02 --stepdiff
STEPDIFF output byte-identical to the SL-13 run (known-benign signature).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 15:36:07 -07:00
Brian DegenhardtandClaude 4fb25f230f SL-13: COP2 clamp-constant broadcast residency (q25/q26)
The hand-rolled COP2 macro FMAC clamps reloaded the ±FLT_MAX bounds from
_cpuRegistersPack.cop2Rec at EVERY clamp site — up to 6 q-loads per guest
op, 8398 static instructions across the UYA EE corpus and 9-21% of every
hot COP2 block (5.1% of EErec exec-weighted instructions on the M2
sample profile). x86 never shows this class because SSE folds the operand
(minps xmm, [mem]); AetherSX2-arm64 keeps the bounds register-resident.

Dedicate q25 = maxFloat.4S / q26 = minFloat.4S, excluded from the EE NEON
allocator pool (like q8/q9) and from the COP2 macro-mode mVU pool
(microRegAlloc::reset(cop2mode), the NEON twin of the x26/x27 EE-pin
gate). Re-materialization is 2 Dups from the pinned callee-saved s8/s9
scalars — no memory access (minFloat[i] == maxFloat[i] | 0x80000000 ==
-FLT_MAX exactly). The clamp itself is now a bare Fminnm+Fmaxnm.

Compile-time validity discipline (s_cop2ClampConstsValid):
- false at block start; first clamp site emits the 2 Dups;
- invalidated by iFlushCall (any flushtype — every real C-call seam);
- NOT invalidated by the VPU_STAT sync seams: the shared sync stubs
  re-materialize unconditionally on their taken path (always sound —
  q25/q26 can hold nothing else; the fast path touches no NEON);
- NOT invalidated by fastmem: vtlbGetLiveRegisterMasks ORs q25/q26 into
  the recorded fpr_bitmask while valid, so a backpatched slowmem thunk
  preserves them like any live register;
- NOT invalidated by the mVU-reuse macro wrappers (pool-gated, no C
  calls);
- forks and superblock side exits carry the flag via BranchCompileState
  and Cop2VfCacheScope.

M2 census (fresh 6000f UYA slot-02 liverun A/B, identical 7644-block
set): clamp-const loads 8398 -> 0, replaced by 498 lazy establishments;
EE corpus -7,446 insns (-1.83%) / -29.1 KiB; hot-90 set bytes -6.0%
sample-weighted; COP2-dense physics blocks -15..-19% (0043C718 0.806,
0043A840 0.853). Flagship 003F7690 unchanged as expected (its density
residual is GPR-quad seam round-trips, the SL-14 item). Unlike the
reverted S4-4 outlining (insns-for-bytes trade, A77-neutral), this
removes instructions AND bytes together on serial clamp->FMAC dependency
chains.

Gates: 1401/1401 recompiler_tests incl. 8 new EeVu0Cop2ClampResidency
contract tests (establishment counts, taken-seam clamp correctness, fork
sharing, sync-stub re-Dup byte-scan, EE+mVU pool exclusion probes);
8000-seed EE fuzz green; UYA slot-02 --stepdiff signature identical to
the known-benign baseline (same timer skips, same 0x004c295c terminal).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 15:26:24 -07:00
Brian Degenhardt 15ec992131 Merge remote-tracking branch 'origin/jit-transplant' into jit-transplant 2026-07-20 14:11:28 -07:00
Brian Degenhardt e80d1c8696 Merge remote-tracking branch 'yaps2/main' into jit-transplant 2026-07-20 14:10:34 -07:00
Brian DegenhardtandClaude dc70533148 arm64 COP2 macro: load VSUB operands before claiming the result slot
The True Crime VSUB fix (ac51b16ea) hoisted cop2ResultReg above the
operand fetches. For a full-mask write, cop2ResultReg claims Fd's
VF-cache slot with fill=false — resident but unloaded — violating its
own documented invariant ("call this AFTER fetching the op's cache
operands"): when Fd aliases Fs or Ft (the ubiquitous vsub vfd,vfd,vft),
the subsequent cop2GetVF hits the empty slot and reads garbage instead
of the operand. OutRun 2006 drops cars through the floor; True Crime
NYC's camera-basis kernel (vsub.xyzw vf1,vf2,vf1 — Fd==Ft) halves its
bone-matrix scale rows — the falls-through-floor/clipping residual
previously mis-attributed to surviving June-22 divFlag work. Load
operands first, per branch, like VADD/VMUL.

Pinned by EeVu0Cop2Macro.VsubFullMaskFdAliases{Fs,Ft}ReadsRealOperand
(red on the unfixed emitter, green after); full suite 1393/1393. True
Crime full-RAM three-engine consensus vs ARMSX2-gold drops from 43
divergent words (bone-matrix pages 0x892xxx+) to only the known-benign
pad-worker scheduling class; both titles visually confirmed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 14:08:20 -07:00
Brian Degenhardt 5dd362bc94 Merge remote-tracking branch 'yaps2/main' into jit-transplant 2026-07-20 12:51:28 -07:00
Brian DegenhardtandClaude ac51b16ea7 arm64 COP2 macro: VSUB with Fs==Ft is exact +0 (True Crime black world)
PS2 VU floats have no inf/NaN — exp-FF bit patterns are valid huge
numbers and x - x cancels to +0 exactly. The hand-rolled macro VSUB
emitted a raw Fsub whose NaN - NaN result the unconditional result clamp
turned into +FLT_MAX, corrupting True Crime NYC's VU0-macro camera/bbox
kernel (qmtc2 leaves stale EE-GPR bits in the upper lanes; the game
zeroes W with vsub.w vf1,vf1,vf1): world geometry degenerates and the
scene renders black with a live HUD within 2 frames, bistable with the
EE interp toggle. Interp (vuDouble operand clamping), x86 mVU
(microVU_Upper's (_Ft_ == _Fs_) opCase1 short-circuit), and our arm64
micro-mode port all produce +0 — only the hand-rolled macro op dropped
the corner. Short-circuit Fs==Ft to a zero move like micro mode,
non-broadcast only, matching x86 ("Don't do this with BC's!").

Found via the twindiff cross-build comparator (armsx2-master gold JIT vs
yaps2; three-engine-consensus corruption at 0x58aa54/0x540aa4/0x58ab24)
plus a gdb watchpoint on the fastmem alias. Pinned by
EeVu0Cop2Macro.VsubSameRegNanPattern{MaskedW,FullMask} — red before,
green after; full recompiler_tests suite 1391/1391.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 12:30:33 -07:00
Brian DegenhardtandClaude 9d0f84b12e Revert "S4-4: outline COP2 macro FMAC tails into shared DynGen stubs"
This reverts commit 14c848127.

The COP2 FMAC-tail outlining cut -18.7% static emitted bytes on M2, but the SD865 codegen_ab A/B (7-run fan-pinned medians, uya-gameplay + sotc-01) shows it is cycle-neutral on the A77: EE-thread retired instructions +1.1-1.4% (the bl/ret + mask-setup call overhead) while cycles stay flat (process-wide +0.14% / -0.35%, within run-noise; harness verdict "cycles absorbed by OoO"). The static footprint win is inert on the out-of-order A77 - the density->icache->front-stall thesis does not convert to cycles for this slice - so the change costs executed instructions and codegen complexity for no measured gain on the primary target.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 12:14:24 -07:00
Brian DegenhardtandClaude 14c8481277 S4-4: outline COP2 macro FMAC tails into shared DynGen stubs
The hand-rolled COP2 macro arithmetic ops re-emitted the same ~24-insn
tail at every site: the ±FLT_MAX result clamp, the MAC/status flag
extraction, and (at chain boundaries) the status denormalize/normalize
bodies. These were the top three repeated instruction shapes in the UYA
hot-block corpus — the largest single contributor to the EE hot-core
emission-density gap vs the icache-friendly reference emitters, which
generate this family once and BL to it (design adapted from
neither/LRPS2's emitSharedVUBody, GPLv3, credit pstef).

Seven stubs are emitted once per recompiler reset alongside the S4-2
sync stubs: two in-place clamps (q30/q31), three flag tails
(mac+status / mac-only / status-only; xyzw dest mask in w1, result in
q30 left clamped), and the status-chain denormalize/normalize pair.
FMAC bodies now compute into RQSCRATCH and the per-site tail is a bare
BL (flag-dead) or Mov-mask + BL (flag-live); operand pre-clamps are a
Mov + BL. The flag stubs run the extraction with a single weight-vector
load (new cop2Rec.macPackWeightsRev) instead of two literal-pool loads,
and never clobber the result register, retiring the q28 park dance.
The compute-into-cache-slot path (cop2ResultReg) stays for the
tail-less ops; full-mask FMAC writes pay one Mov at the dest-mask
apply in exchange. The denorm-in-w8 forwarding token is retired — the
normalize stub always reloads (store-forwarded when adjacent).

Flag semantics are unchanged: same clamp order, same lane math, same
denorm-scratch RMW masks, same EP-4 chain gating. Pinned by the
existing EeVu0Cop2MacroLazyStatus / cop2 macro suites (1389 tests
green) and a UYA slot-02 stepdiff A/B (identical benign-only outcome
vs base). Emitted-size A/B on the S4 hot set (same scene, same
startpcs): -18.7% sample-weighted, COP2-dense physics blocks -30..-60%,
whole shared corpus -5.5%.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 11:16:13 -07:00
Brian DegenhardtandClaude 37bc5e1646 ee/fpu: make add/sub guard-bit emulation unconditional
The fpuGuardedAddSub Recompiler option (and its GameDB
clampModes.guardedAddSub override) is gone; both JITs now always mask
the smaller-exponent operand's guard bits on the single-precision
ADD/SUB fast path, matching the Full-mode DOUBLE path's unconditional
guard. Games like True Crime NYC and Jak 3 misrender without the
masking, and per-game flagging proved impractical to maintain — the
failures take cross-build diffing to even attribute. The |exp diff|<=1
early-out keeps the common case at a plain op.

Test-side, EnableFpuGuarded() is removed (the masked behavior is now
the default the guard-bit suite pins directly) and the
DefaultOffEmitsPlainOpMatchingInterp test goes away with the option.
recompiler_tests: 1389/1389 green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 00:22:24 -07:00
Brian DegenhardtandClaude ea62853294 eerunner: --mkstate savestate bootstrapper for twindiff
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 23:45:03 -07:00
Brian DegenhardtandClaude 2ec02fab87 eerunner: twindiff cross-build twin-trace modes + --dump-config/--set
--twindump/--twincompare compare per-frame memhash/regfp/cycle
trajectories across two DIFFERENT builds of this runner (full-interp x2
control + full-JIT passes; interp cells force EE+VU0+VU1+IOP interp so
they are pure shared-C++). Built to triage the ARMSX2 jit-transplant
regressions against their pre-transplant master JIT, but generic: works
for any working-vs-broken pair sharing a savestate format (e.g. x86 vs
arm64 yaps2). --dump-config serializes the effective per-game EmuConfig
(GameDB applied) for cross-build config diffing; --set Section/Key=Value
gives base-layer overrides for A/B (e.g. fpuGuardedAddSub). Guarded with
EERUNNER_LEGACY_CORE so the same file compiles against pre-yaps2 cores.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 23:31:50 -07:00
Brian DegenhardtandClaude 2fb38e2dd8 S4-3: drop the unconditional iFlushCall from fastmem LQC2/SQC2
recLQC2/recSQC2 still paid iFlushCall(FLUSH_CONSTANT_REGS) on the
fastmem path — the legacy q0-detach shape — which frees every
caller-saved GPR/NEON allocator entry mid-block. UYA's hottest COP2
staging block (0x3F7690) pays it four times in a row. Rework both to
the GE-14 recLQ/recSQ shape: address before the const flush (keeps
the const-Rs fold), _flushConstRegs(true) only, and the quad staged
through RQSCRATCH (q30, never allocator-tracked) so no q0 detach is
needed. The softmem/faulting-PC fallback keeps the legacy full-flush
+ q0 shape.

VF memory is current at both sites without any flush: LQC2/SQC2 are
VF-cache-classifier-false, so recompileNextInstruction flushed the
COP2 VF compile cache before the emitter runs, and VF regs are never
EE-allocator-tracked. The fault path is the proven GE-07 live-mask
thunk (handles non-q0 data registers, saves live allocator entries).

Measured (M2 census, UYA 6000f, vs the S4-2 capture): static EE
bytes -0.16%, 146 blocks shrink / zero grow, EE_003F7690 -104B;
exec-weighted ldr_gpr_unpinned 5.04% -> 4.13% (the reload churn the
seam caused downstream), neon q ld/st -0.3pt combined, cop2_vf flat
by design. A compile-time VF-cache event trace + policy resimulation
sized the ledger's original "VF residency across macro chains" idea
at ~0.07% of cycles ceiling (flagship block: zero) — the seam, not
the residency policy, was the prize.

Tests: three new LQC2/SQC2 residency contracts (dirty caller-saved
scalars + const-folded base, macro-result-then-SQC2 ordering, dirty
resident MMI quad across an LQC2/SQC2 pair). Gates: 1390 tests, 8k
fuzz seeds, vucorpus bins catalog-exact, UYA stepdiff signature
identical, SotC stepdiff baseline-identical (its .01 signature
drifted since the S4-2 session but pre/post agree byte-for-byte at
block 0x006f7168 — pre-existing class, still uncharacterized).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 22:09:22 -07:00
Brian DegenhardtandClaude 6ed6c46d04 S4-2: outline the COP2 VU0-sync seam into shared DynGen stubs
The conditional VU0-sync envelope (VPU_STAT gate, cycle-delta
flush/reload, lazy-pin flush/reload, retained-entry reloads, the sync
C calls) was re-emitted inline at every analysis-marked COP2 site —
15-25 instructions each, the dominant per-site byte carrier in
COP2-dense hot blocks. Emit it once per recResetEE instead, as five
shared stubs (one per sync/finish callee combination), and shrink the
per-site sequence to Add-cycles + BL. AetherSX2 4248 ships the same
shape (mVUmacroEmitCOP2_0/1 shared-stub family).

The stub checks VPU_STAT and returns immediately when VU0 is idle. On
the taken path it raw-preserves LR plus the six caller-saved allocator
pool registers on the stack, which replaces the old writeback-keep +
per-site reload protocol for retained GPR/FPRC entries entirely —
sound because the sync callees never read or write EE GPR memory or
fprc, the same invariant the old retain seam already relied on.
_reloadArm64GPR loses its only caller and is removed.

UYA M2 census vs the S4-1 baseline (same scene, 6000 frames): EE
emitted bytes -4.67% (1648 KB -> 1571 KB), 813 blocks shrink, none
grow; the flagship COP2 physics blocks EE_003F7690 / EE_003FA2D8 drop
20% each. Pin-seam reload/flush instruction counts fall 74%/54%; the
taken-sync execution moves to the shared stubs (dispatcher sample
share 0.06% -> 2.42%), net M2 sample share flat — the win is icache
footprint, to be scored on SD865.

Tests: three new taken-sync residency contracts (dirty caller-saved
scalars, dirty tier-2 lazy pins, and the interlocked .I forms — new
encoders — whose exact-sync+wait/finish stubs previously had no test
coverage), green pre- and post-change. Full battery: 1387 tests, 8k
fuzz seeds, vucorpus bins at catalog, UYA stepdiff bit-identical,
SotC stepdiff signature-identical to baseline (the 0x0010ef38 data
divergence reproduces byte-for-byte on pre-S4-2 emitters — pre-existing,
tracked separately).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 21:29:38 -07:00
Brian DegenhardtandClaude 57f00fea21 tests: replay the Jak 3 VU0-macro camera-basis kernel against interp
Faithful straight-line replay of the Naughty Dog camera kernel at guest
0x0061b44c (SCUS-97330): quaternion -> basis via VOPMULA/VOPMSUB cross
products, +1.0 diagonal via VADDw broadcast, per-axis scale via VMULx/y/z,
then the 4x4 transform via the VMULAx/VMADDAy/VMADDAz/VMADDw ACC chain,
stored with SQC2. Encodings verified bit-exact against the game words
(e.g. vmulax.xyzw ACC,vf07,vf02x = 0x4be239bc). Adds local encoders for
the broadcast VADD/VSUB family (upper funct 0x00-0x07) and VMADDAz.

This is the kernel that floods per-object matrices with +fMax in the
Jak 3 artifact repro; the test pins that the emission chain itself is
bit-identical to the interpreter for finite inputs (it is — the live
corruption enters through the kernel's scratchpad quaternion input, not
the codegen), and guards the whole broadcast/OPMULA/ACC-chain shape
against future regressions.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 21:02:52 -07:00
Brian DegenhardtandClaude 738df26e70 S4-1: allocator-based COP2 transfer ops — drop unconditional FLUSH_EVERYTHING
recCOP2_QMFC2/QMTC2/CFC2/CTC2 opened with a compile-time-unconditional
iFlushCall(FLUSH_EVERYTHING) — every allocator entry and NEON quad evicted
at every transfer, even with VU0 idle. Route them through the register
allocator instead (x86 recQMFC2/recQMTC2 shape, AetherSX2 4248 parity):

- QMFC2: dest claims a NEON quad MODE_WRITE when rt is used later (stays
  q-resident for following MMI/QMTC2 consumers); dead dests store straight
  to the canonical image.
- QMTC2: source policy mirrors x86 — force a quad fill only for dirty
  const / dirty scalar rt; reuse a resident quad for free; a clean miss
  reads memory + merges the lazy pin without claiming a slot.
- CFC2: general path rides the coherent dest helpers; the REG_R partial
  UL[0] write flushes rt residency with writeback first (UL[1]/UD[1]
  survive; interp semantics kept — x86's 64-bit zero-extend divergence
  deliberately not copied).
- CTC2: no flush needed at all — the body reads rt via _eeMoveGPRtoR
  (const/scalar/quad/pin aware) and writes only VU0 state.

The analysis-gated sync seam inside cop2EmitConditionalSync (SL-2 retain
shape + runtime VPU_STAT Tbz) is untouched and remains the only flush
point in a transfer.

New hazard suite ee_vu0_cop2_transfer_residency_tests.cpp (19 tests) pins
the interleave contracts: MMI-quad/scalar/pin/const sources into QMTC2/
CTC2, QMFC2/CFC2 dests consumed by MMI/scalar/pin readers, REG_R partial-
write preservation, and residency riding the mid-block VU0 sync seam
(pending-micro VCALLMS recipe). The suite is green on the old emitters too.

Gates: recompiler_tests 1384 green; 8k-seed EE fuzz soak; UYA .02 + SotC
.02 stepdiff signatures identical to baseline (benign timer classes only);
vucorpus rep sweep bins match the documented catalog (the GTA-SA crash
class is EE-independent). M2 census: static EE bytes +0.04% — UYA hot
blocks are transfer-light, so the predicted icache relief lives in the
sync-seam size (S4-2) and VF residency (S4-3); exec-weighted neon_gpr
q-churn −0.4pt.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 21:02:49 -07:00
Brian DegenhardtandClaude 5458d12ca1 eerunner: VU0-aware fingerprints, divergence census, frame-local RAM diff
Three stepdiff triage upgrades built during the Jak 3 artifact hunt:

- EERUNNER_VU0FP=1 mixes VU0 macro-visible state (VF00-31, ACC, the
  STATUS/MAC/CLIP flag VIs) into the divtrace fingerprint and captures it
  in FullSnap/DiffFullSnaps, so the zoom breaks at the EE block whose COP2
  macro emission first produced divergent VU0 state instead of only when
  it later lands in a GPR or memory (the default fingerprint surfaced the
  Jak 3 fMax vertex flood at an unrelated memcpy). VF is memory-resident
  at every block boundary per the COP2 VF-cache seam policy, so the JIT
  block-prologue sample site reads consistent state. Opt-in: the benign
  filters were tuned without VU0 fields, and mVU-vs-interp has by-design
  NaN/clamp corners.

- EERUNNER_CENSUS=1 stops the zoom hard-stopping at the first non-benign
  divergence: each data/control site is reported compactly, ResyncAfter
  walks past TRANSIENT ones (DMA-phase staging buffers, poll-phase
  control-flow splits that reconverge), and the walk stops at the first
  PERSISTENT site. Persistent control-flow sites also dump the JIT-side
  guest regs at the split entry (busy-poll operands), the polled word when
  it resolves to RAM, and both sides' frame-end SPR channel registers.

- The stepdiff candidate-real branch now re-runs the frame once per mode
  from the same checkpoint and byte-diffs EE RAM+scratch (ReportMemDiff,
  max_show now a parameter): with a clean interp control every differing
  byte is a real JIT store difference, at frame-local granularity.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 21:02:39 -07:00
Brian Degenhardt db0939d28b GSDeviceVK: exclude Turnip from the Adreno colorWriteMask-with-depthtest workaround
The 0x801EA000 driverVersion threshold is in the proprietary Qualcomm blob's
version encoding, but Mesa/Turnip reports driverVersion in Mesa's own encoding
(e.g. Mesa 26.1.2 packs to 0x06801002), which is always below the threshold.
Result: the Adreno-5xx blob workaround fired on every Turnip device regardless
of GPU generation, forcing FBMASK draws with depth test through the blend
emulation path unnecessarily (confirmed active on Turnip A650, Mesa 26.1.2,
deviceID 0x06050002). The blob bug does not exist in Mesa, so gate the
workaround off when driverID reports VK_DRIVER_ID_MESA_TURNIP.
2026-07-19 20:15:04 -07:00
Brian Degenhardt c8d736c042 Merge yaps2/main: EE FPU MAX.S/MIN.S operand clamp at eeClampMode>=1 2026-07-19 19:08:36 -07:00
Brian DegenhardtandClaude bdefb9faf1 EE FPU: clamp MAX.S/MIN.S operands to ±fMax at eeClampMode>=1
recMAX_S_xmm/recMIN_S_xmm emitted bare Fmaxnm/Fminnm with no operand
clamp. x86 routes MAX/MIN through recCommutativeOp(op>=2), whose gate
`CHECK_FPU_EXTRA_OVERFLOW || (op>=2)` always fires, so it fpuFloat2-clamps
both operands (sign-preserving inf/NaN -> ±fMax) whenever CHECK_FPU_OVERFLOW
— eeClampMode >= 1, a strictly lower threshold than the arithmetic ops'
>= 2. AetherSX2's shipped arm64 rec gates the same MAX/MIN clamp on
fpuOverflow (options bit 8) vs ADD/SUB's bit 8+9 (verified against the 3606
build disasm). Without the clamp a raw Inf/NaN operand (via MOV.S/LWC1/MTC1)
survives the NaN-eating Fmaxnm/Fminnm as the wrong finite value — the True
Crime: New York City rainbow (BlueTongue engine, SLUS-21106, eeClampMode:2),
a min(max(uv,0),size) UV-clamp idiom producing a corrupt palette index.

Add fpuClampMinMaxOperand (mirror of fpuClampInput, gated CHECK_FPU_OVERFLOW
instead of CHECK_FPU_EXTRA_OVERFLOW) and apply it to both operands.

Pinned by EeRecFpu.Max/MinSClamps* (mode-2 Inf/NaN cases + a mode-1 gate
test + a mode-0 no-clamp lower bound). Interp fp_max/fp_min run on raw bits
with no clamp, so the correct JIT diverges from interp here — the tests use
RunJitNoDiff/GetFprBitsJit (x86 JIT is the FPU-clamp oracle, not interp).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 19:07:31 -07:00
Brian Degenhardt f2027d7b01 iOS W^X: route ArmConstantPool::GetBlob through the RW alias
Second real catch from the forced dual-map CI gate: GetBlob (the SMC
expected-bytes blob emitted by recRecompile's manual-protection chain)
still memcpy'd straight to the RX pointer — byte-write permission fault in
_platform_memmove under dual-mapping. Same alias + write-range protocol as
GetJumpTrampoline/GetLiteral; the pool has no other writers.
2026-07-19 19:06:52 -07:00
Brian Degenhardt 95bdb032f7 iOS W^X: only alias-translate writes inside the dual-mapped JIT region
The forced dual-map CI run caught armGetWritableCodePtr adding the global
RW offset to ANY pointer: Arm64BaseBlocks link tests patch static buffers
in the test binary's data segment, and site + g_code_rw_offset landed in
unmapped space (byte-write translation fault at TestBody()::code + offset,
Arm64BaseBlocksLink.BlFormSurvivesLinkAndRepatch). Bound the translation
to [GetJitBase(), GetJitEnd()) — the SetJitRange arena that vm_remap
actually mirrored. Addresses outside it (test fixtures; everything, when
no region exists) write untranslated. Production sites are all inside the
arena, so iOS behavior is unchanged; the check is two compares, Apple-only.
2026-07-19 18:41:01 -07:00
Brian Degenhardt 4f7465586c CI: self-diagnose forced dual-map recompiler_tests crashes on macOS
The forced dual-map validation step segfaulted after several passing tests
(so alias emission + RX execution fundamentally work); with no local macOS
hardware the failing step must produce its own root-cause data. On failure,
dump the macOS DiagnosticReports .ips crash log (faulting PC, fault address,
exception subtype — distinguishes a KERN_CODESIGN_ERROR exec fault, i.e. a
test-hook mapping problem, from a write fault at an RX address, i.e. an
unrouted code-write path) and rerun under lldb --batch for a live backtrace.
2026-07-19 18:15:09 -07:00
Brian Degenhardt 77d008af1d iOS W^X: route every JIT code write through the dual-map RW alias
Port the W^X protocol from the previous ARMSX2 recompilers (aR*/aVU) onto
the transplanted JIT so it runs under all four DarwinMisc JIT modes:
Simulator (MAP_JIT + pthread_jit_write_protect_np toggle), iOS 26 LuckTXM
and LuckNoTXM (vm_remap dual-mapping, writes at rx + g_code_rw_offset),
and Legacy (mprotect RW/RX toggle, iOS <= 18).

- AsmHelpers: export armGetWritableCodePtr (RX -> RW alias, identity off
  Apple); armStartBlock/armEndBlock switch to BeginCodeWriteRange with a
  1 MiB Legacy write window and construct the MacroAssembler over the RW
  alias while armAsmPtr stays the RX base, so armGetCurrentCodePointer()
  and all displacement math remain in execute space; armEmitJmpPtr and the
  constant-pool trampoline/literal writes go through the alias with their
  own write scopes.
- Arm64BaseBlocks::PatchAtomic (block linking + exception-path unlink) and
  recPatchIslandB store via the alias; displacements/icache flushes stay RX.
- RecStubs fastmem backpatch stores the redirect B via the alias.
- microVU: the persistent per-VU MacroAssembler is built over the alias of
  prog.x86start; ProgCache hydration fixups patch through the alias while
  Rel26/ADRP math keeps using the RX chunk address.
- recExecute re-arms Legacy-mode execute protection via
  DarwinMisc::LegacyEnsureExecutable (mirrors the previous recompiler).
- BeginCodeWrite/EndCodeWrite skip the macOS MAP_JIT toggle when a
  dual-mapping is active (offset != 0), matching the iOS branches.
- CI validation without an iOS device: ARMSX2_FORCE_DUAL_MAP=1 now also
  works on macOS (Memory.cpp routes the code arena through
  DarwinMisc::MmapCodeDualMap, which builds the vm_remap RW alias there),
  and the macOS workflow reruns recompiler_tests under it, forcing every
  emission/patch path through the alias. Production macOS keeps MAP_JIT
  with offset 0, unchanged.

Linux/Android paths compile to identity no-ops. Gates: recompiler_tests
1359/1359, gs_vertex_tests 21/21, mvu_progcache_versioning_tests 13/13.
2026-07-19 17:18:39 -07:00
Brian Degenhardt 616e5900f3 Merge yaps2/main: GV7 GS front/back thread split (17 commits)
Brings the complete GV7 campaign: GSBackQueue SPSC record ring, draw/
transfer/PCRTC/vsync records with inline executors, the GSFrontState
two-object front split, lockstep + pipelined back-thread modes (default
Off), mid-frame MTGS-thread drain seams, back-thread affinity fix, and
the Qt/Big Picture GSBackThreadMode settings UI.

Conflict resolutions:
- pcsx2/GS/GS.cpp: adjacent additions unioned (Android Tekken 5 Mali
  override + GV7 g_gs_front/GSParseTarget). GSAllocateWrappedMemory
  unioned semantically: keeps the iOS-safe HostSys::CreateSharedMemory
  routing AND drops the static fd so two wrapped allocations coexist
  (the GV7 two-object split runs two GSStates, each with a wrapped vm);
  CreateSharedMemory already unlinks/memfds the name and the iOS
  file-backed fallback O_EXCL-retries per-attempt paths, so coexisting
  allocations cannot collide.
- GraphicsAdvancedSettingsTab.ui: take gsBackThreadMode tabstop; drop
  yaps2's stale "rov" tabstop (no such widget in either tree).
- FullscreenUI_Settings.cpp: GS Back Thread setting inserted outside the
  ARMSX2 !__APPLE__ guard around exclusive fullscreen.

Audit: ARMSX2 one-liners in GV7-rewritten files survived
(GSClut CreateFeedbackTarget, GSState SaveTransferImages); ARMSX2's
Merge()-path additions (RetroArch shader chain, FastMAD fallback) run
post-drain on the vsync path, so no unaudited device seams.

Gates on merged tree: recompiler_tests 1359/1359, gs_vertex_tests 21/21
(incl. new gs_backqueue suite), full build incl. armsx2-qt.
2026-07-19 16:19:06 -07:00
Brian DegenhardtandClaude 35add7405c GV7: expose GSBackThreadMode in the Qt and Big Picture settings UIs
Adds a 'GS Back Thread' dropdown to Graphics > Advanced in both frontends
(Disabled / Inline Records / Lockstep / Pipelined, default Disabled). The
setting is in RestartOptionsAreEqual, so changing it mid-session performs a
full GS reopen and takes effect immediately. Placed at sparse form-layout
row 7 so the existing removeRow() position bookkeeping in
GraphicsSettingsWidget is untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 15:49:52 -07:00
Brian DegenhardtandClaude 63c60af149 GV7-2: clear the back thread's inherited affinity mask at spawn
A spawned thread inherits its spawner's affinity, and the back thread is
spawned by the MTGS thread — which EnableThreadPinning pins to a single
core. Whenever the back thread is (re)spawned while pinning is active
(any GSreopen: renderer switch, restart-class settings apply), it would
inherit that one-core mask and front and back would time-slice a single
core, silently re-serializing the split. Clear to all cores at thread
entry; explicit pinning policy for this thread stays a VMManager concern
(future work: a fourth pin slot on 4-big-core targets).

Fresh boots were unaffected (SetEmuThreadAffinities runs after GSopen),
which is why live sessions looked fine.

Gates: gs_vertex_tests 21/21; gsrunner hashes identical, modes 0/3,
vk and sw.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 15:19:58 -07:00
Brian Degenhardt 1e286ad1e4 tooling: include common/Assertions.h in microVU_Divtrace
The Windows EnterMode stub added in ca8cedd83f uses pxFailRel, but
nothing in this TU's include chain declares it on Windows — the Linux
build never compiles that branch, so it got past the local gate and
died only on the Windows CI leg.
2026-07-19 15:02:39 -07:00
Brian DegenhardtandClaude 5aeb3dd8bc GV7-2: drain the back queue at mid-frame MTGS-thread device seams
Sync-point audit fixes for pipelined mode: GSUpdateConfig's non-reopen
branches, SaveSnapshotToMemory, and capture begin/end all touch renderer
or GSDevice state from the MTGS thread while the back thread may be
mid-draw on the same device. Each now drains queued records first (the
front only parses on the MTGS thread, so nothing new queues during the
operation). DrainBackQueue becomes public for the GS.cpp seam.

Audit conclusions (no code needed): WaitGS callers never touch back-owned
state EE-side (state access travels through MTGS ring packets into already
drained seams); SIGNAL/FINISH are GIFRegHandlerNull in GSState — CSR
semantics are entirely EE-side; a back-thread assert failure aborts the
process on Linux, so it cannot deadlock the front.

Gates: gs_vertex_tests 21/21; gsrunner PNG hashes identical to GV-0
baselines, modes 0 and 3, vk and sw.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 14:58:49 -07:00
Brian Degenhardt ca8cedd83f tooling: make the vu_capture and divtrace TUs compile on Windows
Both are yaps2 offline-diagnostic TUs that had never seen a Windows
build (yaps2 is Linux-only); they were the last two compile failures
on the arm64 Windows CI leg.

vu_capture.cpp only needed getpid — now a capture_pid() helper that
uses _getpid/<process.h> on Windows; the probe itself is fully
functional there.

microVU_Divtrace's capture mechanism is SIGTRAP + ucontext (the JIT
emits brk, the handler snapshots vuRegs and advances the faulting PC),
which has no Windows equivalent — the signal machinery compiles out,
while the shared globals and FingerprintRegs stay so the interp/JIT
consumer sites link unchanged. EnterMode pxFailRel's loudly on Windows
so a future replay-driver port can't mistake an unhandled brk for a
JIT crash. Only the offline replay drivers call it, and they don't
build on Windows.

recompiler_tests 1359/1359 on linux-arm64.
2026-07-19 14:41:03 -07:00
Brian DegenhardtandClaude e7736345bb GV7-1d-ii: report the configured mode in the back-thread startup log
The drain policy is the producer's; under the split the back object's own
flag stays lockstep while the front runs pipelined.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 14:36:46 -07:00
Brian DegenhardtandClaude 5e3819a087 GV7-1d-ii-c: flip mode 3 to true pipelining
The front parser object no longer drains after each push — the record ring
and pool backpressure bound the runahead, and the back thread executes
draws while the front parses ahead. Every cross-boundary read is either
record-carried, behind an explicit drain, or per-object:

- Serial counters s_n / s_transfer_n / s_last_transfer_draw_n become
  per-object members (were process statics): the front assigns order and
  carries serials in records, the draw executor installs s_n from the
  record, and the transfer executor counts its own slice stream — so TC
  timestamps and age heuristics on the back thread see the executing
  draw's serial, not the front's runahead position. Qualified static refs
  in the TC/MultiISA/OGL-debug/SW-dump paths now go through the renderer
  object. The front re-syncs s_n and the scanmask after each (drained)
  vsync.
- Kick-time IsCoverageAlphaSupported drains before reading last-flushed-
  draw state (exact AND deterministic: post-drain state is a function of
  the record stream, not thread timing; the alpha clause can read CLUT
  bytes so it is not front-computable). Memoized per (draw serial, live
  ALPHA) => at most one drain per AA1 draw.
- The HOST->LOCAL exec cursor stays back-side: the front skips the inline
  m_tr.x/y mirror, Freeze adopts the drained back cursor before
  serializing, and Defrost seeds it back.
- GSreset resets the front first (flushing pending draws as records) so
  the back's drain executes them before memory/TC reset, like serial
  pre-reset draws.
- Pipelined mode is refused (falls back to single-object lockstep) when
  HWDownloadMode is Unsynchronized: that path reads local memory from the
  EE thread with no drain.

Gates: gs_vertex_tests 21/21; gsrunner PNG hashes bit-identical to GV-0
baselines for modes 0/1/2/3 x vk+sw over all 10 dumps, and mode 3 repeated
3x with identical hashes (pipelining is deterministic by construction).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 14:33:20 -07:00
Brian DegenhardtandClaude 84a1de62b5 GV7-1d-ii-b: two-object front split (GSFrontState + entry-point routing)
Instantiate a GSFrontState parser object under GSBackThreadMode::Pipelined
(SEAM-AUDIT.md $7): it owns all parse state and emits records into the back
renderer's channel; the back object executes them, installing record state
into its own members so the HW look-ahead heuristics read the same names
they always did. Mode 3 still drains per record (lockstep) — the pipelined
flip is the next commit.

- GS.cpp routes GIF transfers, SoftReset, CSR, readbacks, savestates, and
  the vsync PCRTC digestion to the front; present/TC/settings stay on the
  renderer. The front is created only when the back thread engaged, and is
  destroyed first (it drains the shared channel the back owns).
- Drained seams reach authoritative memory through m_mem_target: readback
  ReadImageX/SaveBMP, InvalidateLocalMem, savestate vm8 serialize/restore,
  TC readback/purge (now draining), plus back-side Reset/CLUT-reset and a
  PCRTC re-sync on Defrost.
- The draw executor on a split back aims m_draw_env/PRIM/m_context around
  the tail exactly as FlushDraw does on the front, and restores after.
- m_channel_shuffle_finish is written on both sides; the front's ApplyTEX0
  set becomes a one-shot edge OR-ed into the back-owned flag (a level
  install clobbered the draw path's own sets/clears — FlatOut 2 lost its
  channel-shuffle skip, caught by the vk hash gate).
- Kick-time IsCoverageAlphaSupported reproduces single-object mixed
  semantics: live PRIM/ALPHA from the front, last-executed-draw primclass/
  cached-ctx/alpha-minmax from the back (IsRTWrittenLive split).
- GSAllocateWrappedMemory drops its process-global singleton (close the
  fd/handle once the views are mapped) so two GSStates can each own a
  wrapped vm; also fixes a handle leak in the Windows free path.
- s_transfer_n moves to the submit side: transfer serials are
  front-assigned, and the vsync idle-frame check reads them on the MTGS
  thread.

Gates: gs_vertex_tests 21/21; gsrunner PNG hashes bit-identical to GV-0
baselines for modes 0/1/2/3 x vk+sw over all 10 dumps (mode 3 exercises
Defrost + readback seams through the front object).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 14:20:49 -07:00
Brian Degenhardt e6fdc666f2 common: drop __vectorcall from the arm64 r128 calling-convention macros
__vectorcall is an x86-ism; AAPCS64 already passes/returns 128-bit
vectors in SIMD registers under the default calling convention, and
the macro expands to nothing on non-Windows anyway. On aarch64-windows
clang-cl folds it to an explicit default-CC (cdecl) attribute, which
conflicts with the preserve_most annotation on the vtlb r128
dispatchers ('preserve_most and cdecl attributes are not compatible',
8 errors across every TU including vtlb.h) while changing nothing
about how r128 is actually passed. The x86 branch keeps __vectorcall
untouched.

recompiler_tests 1359/1359 on linux-arm64.
2026-07-19 14:13:55 -07:00
Brian Degenhardt 54995264fc tests: mVU digest pins apply only on the Linux/libstdc++ harvest ABI
The pinned digests hash emitted code that bakes guest-state field
offsets, and those offsets move with the C++ standard library's struct
layout: libc++ containers are smaller than libstdc++'s, so on macOS
every kPins row drifts wholesale (all 7 probes, first macOS CI run
2026-07-19). The emitted code is internally consistent per platform —
nothing is miscompiled — the pin VALUES are just platform-ABI-specific.

Skip the pin comparison (visibly, GTEST_SKIP) outside desktop
Linux/libstdc++ where development happens and pins are harvested.
Every platform still computes digests, asserts them nonzero, and runs
EmittedShapeIndependentOfPriorCompile (which passes on macOS).
Maintaining a per-platform pin table would mean transcribing hex from
CI logs on every emitter change and would re-drift with Apple SDK
updates — the tripwire's job (catching unintended emitter drift during
development) doesn't need it.
2026-07-19 13:56:29 -07:00
Brian Degenhardt b1c4cf9358 vixl: suppress clang-cl C++11-narrowing errors from MS enum semantics
Under the MSVC ABI, enums without a fixed underlying type keep int for
MSVC compatibility, so vixl's >=0x80000000 instruction-encoding
enumerators wrap negative and their use as case labels against the
unsigned Instr type is a narrowing error clang-cl enforces (two TUs:
cpu-features-auditor, disasm). MSVC itself compiles the identical
semantics silently -- upstream PCSX2 ships vixl on Windows arm64 MSVC
-- and the 32-bit patterns are unchanged, so downgrading the
diagnostic is behavior-correct. PUBLIC like the existing
deprecated-enum-enum-conversion suppression, since the same
enum-vs-Instr switches appear in TUs including vixl headers.
2026-07-19 13:52:55 -07:00
Brian Degenhardt 7d79b9d8f4 android: define the libretro-headers target for VKLibretro.cpp
pcsx2's Vulkan GS source list includes VKLibretro.cpp unconditionally
(runtime-gated outside the libretro frontend) and links the header-only
libretro-headers INTERFACE target for libretro_vulkan.h. The Android
thin CMake never added root 3rdparty/libretro, and CMake silently
treats an unknown name in target_link_libraries as a raw linker flag,
so the miss surfaced only as a fatal missing-header compile error in
the APK build. iOS is unaffected (USE_VULKAN forced OFF there).
2026-07-19 13:44:07 -07:00
Brian Degenhardt 57f0f6bb00 CI: package-sdl.sh stages armsx2-sdl, not yaps2-sdl
The script came over from yaps2 verbatim; the SDL job's build step
already produces bin/armsx2-sdl (rebranded frontend), so packaging
died at 'cannot stat .../yaps2-sdl' after an otherwise clean build.
2026-07-19 13:44:07 -07:00
Brian Degenhardt 1d7cd1a3f3 mVU persist: resolve the executable image range on Darwin
ResolveImageRange was #ifdef __linux__ (/proc/self/maps parse) with a
hard false elsewhere, so on macOS the recorder classified every baked
global address as unclassifiable and dropped every episode — recording
silently recorded nothing, failing MvuAbiDigest/MvuPersistRoundTrip on
the macOS CI leg (first platform to ever run these suites outside
Linux).

The Darwin path takes the union of the main image's LC_SEGMENT_64
ranges (excluding __PAGEZERO) plus the ASLR slide via dyld. Unlike
Linux non-PIE these addresses vary per run, so cross-process payloads
recorded on macOS never revalidate — the existing hydration-side
imageAnchor check rejects them and the program recompiles, which is
the intended fail-safe. In-process recording (digest pins, round
trips, and eventually an iOS in-session cache) works fully.

Linux side re-verified: recompiler_tests 1359/1359.
2026-07-19 13:43:58 -07:00
Brian DegenhardtandClaude 94de4fd55c GV7-1d-ii-a: extract the front<->back channel from GSState
Move the record ring, wake semaphore, and both pool arenas/free rings into
GSBackQueue::Channel. Each GSState owns channel storage and works through a
m_chan pointer (defaulting to its own storage), so the upcoming two-object
pipelined split can aim a front parser object at the back object's channel
without touching any record or pool logic. DrainBackQueue keys on the
channel's consumer_running flag instead of the producer flag, making drains
work from either side; payload node-0 adoption becomes an explicit
AdoptTransferBuffer() run by the staging object. The destructor frees only
its own channel storage. No behavior change in any mode.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 13:42:51 -07:00
Brian Degenhardt ad4ed68e06 CI: build Windows arm64 pcsx2 with clang-cl for preserve_most
MSVC has no __attribute__((preserve_most)), which the arm64 recompiler
requires on the vtlb dispatchers (hard #error in pcsx2/vtlb.h since
f7a039e870 -- the emitted JIT code depends on the x9-x15 preservation
contract, so a compiler without the attribute produces a miscompile,
not a slowdown). clang-cl targeting aarch64-pc-windows-msvc compiles
the attribute with correct codegen (verified: caller keeps a live
value in x9 across the annotated call, matching the linux target).

Only the emulator configure step changes: clang-cl uses the MSVC ABI,
so the deps stage stays on MSVC unchanged and keeps its cache, and
vcvars still provides the SDK, armasm64 (FastJmp), and link.exe. The
CMake side already supports clang-cl upstream (USE_CLANG_CL). The
runner image ships a native arm64 LLVM; the locate step prints the
triple so a wrong-arch install fails loudly at configure.
2026-07-19 13:36:21 -07:00
Brian DegenhardtandClaude 6d998af9ff GV7-1d: lockstep drain spins before sleeping
Live MQ65 lockstep run measured 30->6 fps (GS frame 32ms->111ms): the
per-record WaitForEmpty futex round-trip at thousands of records per
frame is almost all of it. Records usually execute in microseconds, so
WaitForEmptyWithSpin catches nearly every drain on the spin path.
Lockstep remains per-record synchronization — the bisect rung, not a
shipping mode; the throughput answer is pipelined mode.

Gates: gs_vertex_tests 21/21; mode-2 gsrunner hashes bit-identical to
GV-0 baselines, all 10 dumps, vk + sw.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 13:28:29 -07:00
Brian Degenhardt 10fab243e7 CI: install the Rust Android target for the librashader cargo build
The Android APK job fails (also on master - the job is
continue-on-error so it never blocked) because librashader's cargo
build targets aarch64-linux-android and the runner's rustup only has
the host std: error[E0463] can't find crate for 'core'. One rustup
target add before the gradle build fixes it. Candidate to send
upstream to master independently of this branch.
2026-07-19 13:11:01 -07:00
Brian Degenhardt f7a039e870 vtlb: make the preserve_most requirement a hard compile-time check
The arm64 recompiler emits calls to the vtlb dispatchers assuming the
preserve_most contract (x9-x15 spared) and skips pin spill/reload
around them. Compilers without the attribute break that contract
silently: GCC warns and ignores unknown attributes, producing a binary
that corrupts the EE pin registers at every MMIO slow path; MSVC
errors on the attribute syntax by accident (C2065, the Windows arm64
CI failure in run 29700912398).

Replace both outcomes with an explicit #error naming the requirement.
clang supports preserve_most on all arm64 targets we build for,
including aarch64-pc-windows-msvc (verified: caller keeps live values
in x9 across the call, no diagnostic), so the Windows leg can go green
by building with clang; until then it fails with a clear message
instead of a mystery identifier error.
2026-07-19 13:10:28 -07:00
Brian Degenhardt 7dd2b50df0 arm64: route in-place patch icache flushes through HostSys
Three sites (Arm64BaseBlocks::PatchAtomic, recPatchIslandB,
armEmitJmpPtr) invalidated the patched word with __builtin___clear_cache
directly. On Darwin that builtin lowers to a call to compiler-rt's
___clear_cache, which the iOS app link does not provide - the iOS CI
leg failed with 'Undefined symbols: ___clear_cache' (run 29700912398).

HostSys::FlushInstructionCache is the existing portable wrapper
(sys_icache_invalidate on Apple, the builtin elsewhere) and is already
used for the identical 4-byte patch flush in the RecStubs fastmem
backpatch, including from the SIGSEGV handler, so signal-safety
follows existing precedent.

recompiler_tests 1359/1359, core_test 86/86 local.
2026-07-19 13:09:15 -07:00
Brian Degenhardt 55828f1938 CI: restore build-dependencies-runner.sh for the SDL/libretro jobs
The libretro + SDL handheld jobs wired into build-all.yml (fabccaa4fb)
call .github/workflows/scripts/linux/build-dependencies-runner.sh, but
the merge resolved .github/workflows/ wholesale to the ARMSX2 side and
dropped the script. Both jobs failed at Build Dependencies with exit
127 in run 29700912398. Its common/shaderc-changes.patch reference is
already present in this tree.
2026-07-19 13:07:48 -07:00
Brian Degenhardt c581e58eb8 tests: port arm64_emit_test to the transplanted JIT's AsmHelpers
The Arm64EmitEE suite (185 tests) unit-tested the replaced backend's
per-op emit helpers (armEmitEffectiveAddr, RESTATEPTR state layout,
EE_*_OFFSET macros) which were deleted with that backend; the semantics
they pinned (EE ALU/load-store per-op behavior) are covered end-to-end
by the recompiler_tests JIT-vs-interp oracle suite (1359 tests). The
Arm64Emit toolchain suite (4 tests: MAP_JIT emit lifecycle, 64-bit
immediate materialization, in-place branch patch + icache flush) is
backend-independent and kept - it is exactly the machinery the iOS
W^X port will stress.

armEmitJmpPtr was declared in AsmHelpers.h but its definition was
deleted with the old backend's AsmHelpers.cpp; reimplement it with the
same atomic-word-store + __builtin___clear_cache protocol as
Arm64BaseBlocks::PatchAtomic / recPatchIslandB.

Fixes the Linux arm64 (4k/16k) and macOS CI legs:
arm64_emit_test.cpp:18 fatal error: 'arm64/aR5900.h' file not found
(run 29700912398). core_test 86/86, recompiler_tests 1359/1359 local.
2026-07-19 13:07:28 -07:00
Brian DegenhardtandClaude 1b5ea8f159 GV7-1d: the back thread — lockstep mode live
Spawn the GS back thread (WorkSema loop over the SPSC RecordRing) when
GSBackThreadMode >= Lockstep. Every Submit* seam now routes: queued
modes push the record into the ring (consumer dispatches by tag through
ExecRecordSlot and releases draw nodes after the tail); modes 0/1 keep
the direct/inline paths untouched. Lockstep drains after every push,
which is what makes executing against the shared single-object state
safe — true pipelining needs the front-object split, so Pipelined runs
lockstep until that lands.

VSYNC records are never queued: SubmitVsync drains and presents on the
MTGS thread, keeping the back thread off the GSDevice on present paths
entirely. Queued modes engage only for Vulkan HW or SW renderers (a GL
device is context-bound to the MTGS thread; SW never touches the device
off the vsync path) — anything else warns and falls back to inline
records. ExecVsyncRecord becomes a GSState virtual for the dispatch
switch; GSRenderer's implementation overrides it.

Drain seams added (no-ops in lockstep, load-bearing under pipelining):
Reset, SoftReset, InitReadFIFO, Read, ReadLocalMemoryUnsync, Freeze,
Defrost, and StopBackThread (drain + exit + join) at destruction.

Gates: gs_vertex_tests 21/21; gsrunner PNG hashes bit-identical to GV-0
baselines on all 10 dumps, vk + sw, modes 0, 1 AND 2; mode 3 verified
falling back to lockstep.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 13:05:11 -07:00
Brian DegenhardtandClaude 4f20fc6f4c GV7-1c: transfer payload pool — TRANSFER records own their staging bytes
In record modes, m_tr.buff aliases a pooled 4MB node (the ctor adopts
GSTransferBuffer's own allocation as node 0). TRANSFER records reference
slices of the current node; slices of one logical transfer share it —
front appends and consumer reads touch disjoint ranges, so that's safe
under pipelining. At the next transfer Init (TRXDIR) after any record
referenced the buffer, the front rotates to a fresh node and emits a
RELEASE_PAYLOAD record behind the slices — FIFO ordering guarantees
they were consumed by the time the release returns the node to the
pool. Readback Inits rotate too, since ReadImageX writes into the
staging buffer.

The whole-packet Write fast path stages through the pooled buffer under
record modes (GIF packet memory is transient — a queued consumer would
read freed data); mode 0 keeps today's zero-copy reference.

Pool: 8 nodes / 32MB cap, same free-ring + arena + backpressure shape
as the draw-node pool. Mode 0 is untouched.

Gates: gs_vertex_tests 21/21; gsrunner PNG hashes bit-identical to GV-0
baselines on all 10 dumps, vk + sw, mode 0 AND mode 1.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 12:56:35 -07:00
Brian DegenhardtandClaude d779cfcec4 GV7-1c: draw-node pool — DRAW records own their vertex/index arrays
On the record path, FlushPrim now hands the live heap arrays to a pool
node before building the DRAW record: the buffer structs are snapshotted
into the node and the heap arrays exchanged, so the parse slot
(m_vertex_buffers[i]) takes the node's recycled arrays as fresh buffers
and keeps its xy-ring/counter state untouched — every array-indexed
consumer (PushBuffer, FlushBuffers, CheckWriteOverlap) is unaffected.
The record references the node's structs, valid until the consumer
releases the node (inline modes: FlushPrim right after the executor
returns; the back thread takes over that release when it lands).

Pool: free-list SpscRing (back producer / front consumer) + front-owned
arena capped at 64 nodes = ring capacity, so Release can never fail and
Acquire past the cap becomes the pipelined backpressure wait. Node
arrays are allocated to the current buffer's maxcount and float
organically through the swaps afterwards.

Off path (mode 0) untouched — no pool, no record, direct tail.

Gates: gs_vertex_tests 21/21; gsrunner PNG hashes bit-identical to GV-0
baselines on all 10 dumps, vk + sw, mode 0 AND mode 1.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 12:51:57 -07:00
Brian DegenhardtandClaude 370fe6e29a GV7-1b: GSBackThreadMode config + record-off fast path
Add the GV-7 mode ladder as a restart-required GS option
(EmuCore/GS GSBackThreadMode: 0=off, 1=inline-records, 2=lockstep,
3=pipelined; default 0). Restart-required means the mode can never
change under a live GSState, so it's sampled once at construction.

Off now skips the DRAW record round-trip entirely: every field the
record carries is captured from live state and installed back over the
same live state, an identity — FlushPrim calls the executor tail
directly instead. ExecDrawRecord splits into the install block +
DrawRecordTail(draw_serial), which is the shared tail for both paths
(and closes the ~2 env memcpys/draw the GV7-0d inline path was paying;
GV7-3's bool-off profile verifies against the GV-6b baseline).

Mode >= 1 keeps the GV7-0 build+execute-inline shape (modes 2/3 fall
back to it until the thread lands). gsrunner grows -backthread <mode>
so the gate matrix can pin both rungs.

Gates: gs_vertex_tests 21/21; gsrunner PNG hashes bit-identical to
GV-0 baselines on all 10 dumps, vk + sw, in BOTH mode 0 and mode 1
(record path confirmed active via the new startup log line).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 12:45:24 -07:00
Brian DegenhardtandClaude e59ad346e0 GV7-1a: front->back SPSC record ring + unit suite
Add the GV-7 queue primitive to GSBackQueue.h: a single-producer/
single-consumer ring templated over slot type and power-of-two count,
using free-running u32 cursors with acquire/release ordering only (no
RMW, armv8.0-safe). Records are built directly in the ring slot via
BeginPush/CommitPush, so queued mode adds no intermediate copy on top
of the record build itself.

RecordSlot is the tagged variant sized/aligned for the largest record
(DRAW, 1632B); all record types are statically asserted trivially
copyable so slots recycle without destructor bookkeeping. RecordRing =
512 slots (~860KB).

New gs_backqueue_tests.cpp (in the gs_vertex_tests binary): FIFO +
capacity + backpressure edge, wraparound, tag round-trip through
RecordSlot, and a two-thread spin stress (1M values, 64-slot ring)
checking exact in-order delivery.

Nothing production-side consumes the ring yet — that lands with the
mode switch behind the GSBackThread config bool.

Gates: gs_vertex_tests 21/21; gsrunner PNG hashes bit-identical to
GV-0 baselines, all 10 dumps, vk + sw.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 12:39:09 -07:00
Brian Degenhardt f78a296b94 GSDeviceVK: keep push descriptors and provoking vertex on Turnip Adreno
Two pre-transplant Adreno gates were tuned against the old backend's binding
code and hurt the transplanted backend on Turnip (MQ65/Adreno 610 measures the
GS thread well behind the yaps2 build of the same backend, which ships both
features on Turnip):

- Push descriptors were disabled for every non-proprietary Adreno driver,
  forcing the per-draw descriptor-set alloc/update/bind fallback for the 7 TFX
  textures. Allow Turnip alongside the proprietary driver; keep the disable
  only for unknown Adreno drivers.
- VK_EXT_provoking_vertex was stripped vendor-wide (Eden's rule targets the
  proprietary driver), pushing flat-shading conversion onto the GS thread in
  software. Restrict the strip to the proprietary driver.
2026-07-19 11:53:59 -07:00
Brian DegenhardtandClaude 7e612f4750 GV7-0e: PCRTC_SYNC + VSYNC records with inline executors
PCRTCDisplays is not vsync-only state — the HW Draw() heuristics read it
per draw — so under the split it is duplicated front/back and refreshed
by a PCRTC_SYNC record carrying the whole digested GSPCRTCRegs plus the
pre-decrement scanmask counter. GSvsync now digests (unchanged), submits
the PCRTC record, flushes, then submits a VSYNC record carrying
field/registers_written/idle_frame; the executor runs the whole VSync()
body (Merge, present, capture). Record order reproduces today's
semantics: vsync-flushed draws see the fresh display state, mid-frame
draws the previous frame's. Merge's scanmask decrement stays back-side;
the front mirrors it at enqueue once the copies are distinct (GV7-1).

GSPCRTCRegs hoists to GSBackQueue.h as the record payload type; GSState
keeps an alias.

Gate: gs_vertex_tests 17/17; gsrunner PNG hashes bit-identical to the
GV-0 baselines on all dumps, Vulkan and SW.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 11:53:04 -07:00
Brian DegenhardtandClaude f7816e589c GV7-0d: GS draw seam — self-contained DRAW records + inline executor
FlushPrim splits at the audit's seam line. The front half captures the
carry-over window (pre-rounding, as today), updates the texture-flush
flag (front-computable, front-read), assigns the draw serial, and builds
a self-contained DrawRecord: the staged draw environment, the live-env/
m_v next-draw peek the HW look-ahead heuristics read, temp_draw_rect,
flush reason, channel-shuffle-finish and packed-UV flags, serial, and
the vertex/index buffer set. ExecDrawRecord installs the record and runs
the old tail — sprite-blit frame-rate detection, scissor update, vertex
trace, texel rounding, Draw(), perfmon — reading the draw serial from
the record. The buffer reset + carry-over rebuild stay front-side, after
the executor.

GSVertexBuff/GSIndexBuff hoist to GSBackQueue.h (VertexBuff/IndexBuff)
as the record payload types; GSState keeps aliases. The GV7-1 pool will
hand ownership of these across the thread boundary.

Gate: gs_vertex_tests 17/17; gsrunner PNG hashes bit-identical to the
GV-0 baselines on all dumps, Vulkan and SW.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 11:48:28 -07:00
Brian Degenhardt 49ff97d0e7 GSDeviceVK: fix double-destroy of frame descriptor pools from merge duplication
The jit-transplant merge resolution of DestroyResources() kept both parents'
per-frame descriptor pool teardown lines (they order command-pool-first, yaps2
descriptor-pool-first), destroying each frame's descriptor pool twice. The
second vkDestroyDescriptorPool call dereferences the freed pool inside the
driver — segfault in libvulkan_freedreno on every GS device reopen (renderer
switch / settings apply) on turnip. Keep the upstream order, destroy once.

Found via coredump on MQ65 (Adreno 610/turnip); occurrence-count sweep of all
vkDestroy/vmaDestroy/vkFree targets across HEAD vs both merge parents confirms
this was the only duplicated teardown in the Vulkan backend.
2026-07-19 11:45:18 -07:00
Brian DegenhardtandClaude 33a329fdb7 GV7-0c: split GSClut::Write into decision state and palette load
The CLUT write decision chain (WriteTest / CanLoadCLUT / InvalidateRange
dirty tracking) is purely register/address-based; only the load itself
reads palette bytes from local memory. Split Write() at that seam:
WriteDecision updates m_write/m_CBP (front side), WriteLoad sets
m_read.dirty and dispatches the m_wc loader (back-executable).

ApplyTEX0 now routes through SubmitClutLoad, which updates the decision
state at submit time and builds a self-contained ClutLoadRecord executed
inline by ExecClutLoadRecord — the CLUTLOAD leg of the GV-7 record
stream.

Gate: gs_vertex_tests 17/17; gsrunner PNG hashes bit-identical to the
GV-0 baselines on all dumps, Vulkan and SW.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 11:36:56 -07:00
Brian DegenhardtandClaude 134c571f29 GV7-0b: GS transfer/move seam — self-contained records + inline executors
Introduce GSBackQueue.h record types for the GS front/back split (GV-7):
TransferRecord (one HOST->LOCAL slice: register snapshots, payload span,
partial-end fixup inputs, cursor init) and MoveRecord (LOCAL->LOCAL blit
registers). FlushWrite and the GSState::Write whole-packet fast path now
build a TransferRecord and hand it to ExecTransferRecord; the TRXDIR
local->local case builds a MoveRecord via SubmitMove and ExecMoveRecord
installs it and runs the unchanged virtual Move chain (HW hack -> TC
move -> software blit). Records are executed inline today; GV7-1 moves
execution to the back thread.

Back-owned effects move into the executors: the m_draw_transfers
upload-queue push (from Write first-packet time to first-slice execution
— order-equivalent because every consumer path passes through FlushWrite
first: TRXDIR handler, FlushDraw, Flush), s_last_transfer_draw_n
stamping, InvalidateVideoMem, the wi() local-memory write, and the
Swizzle perfmon stat (stat_len preserves the fast path's raw-packet
counting). The executor owns the write cursor across slices
(m_exec_tr_x/y), mirrored back into m_tr.x/y inline for savestate
coherence. The staged path invalidates via the live m_env.BITBLTBUF
while the fast path uses m_tr.m_blit — captured per-record in env_blit,
preserving both behaviors exactly.

Gate: gs_vertex_tests 17/17; gsrunner PNG hashes bit-identical to the
GV-0 baselines for all 10 dumps on both vk and sw renderers.

Seam classification: scratchpad/gv7-2026-07/SEAM-AUDIT.md.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 11:26:57 -07:00
Brian Degenhardt d499e0600a Merge yaps2: GV-6b per-vertex divide fold + GV-CLOSE scaffolding strip
Pulls GV-6b (divide per vertex at accumulate time — the fused FindMinMax
covers varying Q), the ROV-heuristic/SW-sync-log warning cleanup, and
GV-CLOSE (GS_VERTEX_CROSSCHECK campaign scaffolding removed, including
the orphaned CMake option this tree carried).

The warning-cleanup hunks in GSRendererHW resolved to this tree's newer
upstream ROV shape, where the dead locals it removed (two_pass_alpha and
friends) never existed and full_barrier must stay mutable.
2026-07-19 10:43:52 -07:00
Brian Degenhardt fabccaa4fb libretro: rebrand core to armsx2_libretro, wire libretro+SDL CI into build-all
Rename the imported yaps2 libretro core (output .so, .info, core-option
prefixes, ini name, Vulkan app/engine identity) to ARMSX2. The core is
still gated behind ENABLE_LIBRETRO (default OFF) and needs X11_API=OFF
WAYLAND_API=OFF (headless Vulkan-context-negotiation build, same as the
CI job). Exports remain retro_* only via link.T.

The yaps2 nightly_release.yml is dropped — ARMSX2 has its own release
process — and the reusable libretro/SDL build workflows are instead
invoked from build-all.yml, so PR runs cover them. armsx2-sdl keeps the
kmsdrm handheld frontend buildable for Rocknix-style downstreams.
2026-07-19 10:37:07 -07:00
Brian Degenhardt b1dbb0af84 Remove the replaced backend's interpreter-fallback glue
The yaps2 recompilers compile every EE opcode natively, so the
per-instruction interpreter fallback the previous arm64 backend relied
on is dead code: drop intExecuteOneInst (Interpreter.cpp/R5900.h) and
the AndroidEEOpHist fallback-opcode histogram. EEDiffVerify stays — its
per-op hooks are worth re-emitting from the new recompiler later.

Also hoist the Qt metatype declarations shared by moc'd headers into
QtMetaTypes.h: with the debugger sources now optional the autogen bucket
layout shifted, exposing a specialization-after-instantiation error when
moc_MainWindow preceded moc_QtHost in mocs_compilation.
2026-07-19 10:32:53 -07:00
Brian Degenhardt 3e077eff9b Merge yaps2: arm64 JIT transplant + test/perf/libretro infrastructure
Merges yaps2/main (github.com/yaps2/yaps2, c16b88cb7) into ARMSX2,
replacing the arm64 recompiler family with the yaps2 JITs and importing
the yaps2 testing, perf, and libretro infrastructure. Common ancestor is
upstream PCSX2 342db5152 (2026-06-19); git auto-merged all but 38 files.

Replaced (deleted in this merge, recoverable from history):
- arm64/aR5900*, aR3000A*, aVU* -> arm64/iR5900*/iR3000A*/microVU*-arm64:
  EE static-pin register file with lazy dirty tracking, dual-residence
  allocator, IOP block linking, native COP2 macro ops, inline unaligned
  fastmem, persisted VU program cache, call-ret shadow ring, VU0 spin
  fast-forward.
- MVU_DIFF shadow-run hooks in shared VU interpreter TUs (superseded by
  the offline vurunner JIT-vs-interp oracle).

Imported from yaps2:
- tests/ctest/core/recompilers: ~80 gtest suites (EE/IOP/VU differential
  harnesses, fuzzers, ABI digest tripwire, capture format pins) plus the
  gs_vertex_tests kernel oracle.
- pcsx2-vurunner / pcsx2-eerunner headless capture-replay runners.
- tools/perf counter-based A/B rigs, perf jitdump productionization,
  PmuCounters, clang-perf/clang-handheld presets.
- pcsx2-libretro core (ENABLE_LIBRETRO, default OFF; rename pending).
- GS vertex-kick fast path (GV series): TBL-based packed parse,
  register-resident kick, scalar-outcode cull, fused draw-rect/FindMinMax.
- Null renderer, VK_KHR_display direct WSI, swapchain PresentStats.
- SPU2 NEON mixer vectorization, EE timer read clamp (NFL 2K5 hang),
  IOP ioman signed-compare fix, assorted UB fixes.

Kept from ARMSX2 in the both-touched files:
- iOS dual-map W^X and fastmem-unavailable resilience (Memory, HostSys,
  vtlb). The split data/code area model is retained; both areas now take
  fixed VA hints so cached VU JIT code stays deterministic on Linux.
- Android thread-affinity model, VMState shutdown early-outs, all
  platform frontends, branding, CI, RetroAchievements identity/policy.
- GSDeviceVK: ARMSX2's push-descriptor decision logic (Mali crash gate,
  proprietary-vs-turnip Adreno split) merged with yaps2's descriptor-pool
  exhaustion recovery (flush + render-pass restart instead of dropped
  binds). Vendor feature policy is the union: Mali fbfetch policy with
  MediaTek/G57/Xclipse gates from ARMSX2; Adreno stencil/ROV/
  test-and-sample-depth hang avoidance and no_ps2_z_quantization from
  yaps2.

Build-system notes:
- The Qt debugger is now gated behind ENABLE_QT_DEBUGGER (default off on
  arm64) so handheld builds drop the KDDockWidgets dependency.
- GSDeviceNone and remaining yaps2 GS code were ported to the newer
  upstream GSTexture Usage-flags API.

The replaced backend's interpreter-fallback glue (intExecuteOneInst,
AndroidEEOpHist) and the EEDiffVerify runtime differ are retained for
now; dead pieces will be removed in a follow-up commit.
2026-07-19 10:24:29 -07:00
Brian DegenhardtandClaude d52ea81dd4 GV-CLOSE: strip GS_VERTEX_CROSSCHECK campaign scaffolding
The dual-path crosscheck (parse, scalar cull, fused FindMinMax vs their
legacy kernels per vertex/prim/draw over live replays) did its job — it
caught the fan-class FindMinMax coverage bug that the property sweeps
could not see — and the campaign is measured and closed. The gtest
oracle suite (gs_vertex_tests, 17 tests) remains the standing gate.
Recover the crosscheck machinery from git history if a divergence hunt
ever needs it again.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 10:17:10 -07:00
Brian DegenhardtandClaude 431b623763 GS: clean up compiler warnings in ROV heuristic and SW sync log
- GSRendererHW: drop the dead heuristic inputs (colormask/atst/afail/
  blend/date/ztst derivations) left from an earlier iteration of the ROV
  cost model — none feed multipass_color/depth. Recover from git if a
  fuller model returns. depth_to_color is only consumed by GL_PUSH,
  which compiles out of non-debug builds — mark [[maybe_unused]].
- GSRendererSW: s_n is u64; use PRIu64 in the LOG fprintf.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 10:10:52 -07:00
Brian DegenhardtandClaude 7601f25a52 GV-6b: divide per vertex at accumulate time — fused FMM covers varying Q
The MQ65 verification profile showed FindMinMax<TRIANGLE,iip,tme,!fst>
unchanged at 6.3% of the GS thread: GV-6's constant-Q gate declined the
dominant draw shape — perspective-projected meshes have per-vertex Q, so
STQ draws almost never have one constant Q — and we paid the accumulate
AND the legacy walk.

Replace the constant-Q fold with a single-vertex transcription of the
legacy STQ step at accumulate time: build {S/Q, T/Q, Q, Q}, blend-mask
NaN lanes out of the min/max chains, accumulate tnan. One 4-lane FDIV
per unique vertex vs the legacy walk's one per index-list pair (strips
reference vertices up to 3x). Per-lane the scalars go through the same
IEEE ops as the legacy pair-wise walk, and blend-masked min/max is
idempotent/assoc/comm, so the result is bit-exact with no decline cases
— FmmFinish is now unconditional and the monotonicity gates are gone.

Gates: gs_vertex_tests 17/17 (the four FMM sweeps now require bit-exact
match on ALL Q/ST configurations, including varying Q and NaN/inf);
GS_VERTEX_CROSSCHECK replay of all 10 dumps clean; sw+vk frame hashes
bit-identical to pre-campaign baselines.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 10:07:28 -07:00
Brian DegenhardtandClaude c16b88cb76 GV-6: fuse FindMinMax into vertex kick emission
GSVertexTraceFMM::FindMinMax re-walks the draw's index list at flush
(strip vertices up to 3x redundant) with a non-pipelined FDIV per vertex
pair — 6.6% of the GS thread on the MQ65 UYA profile. Accumulate the
min/max at index-emission time instead, where the vertex is
register/L1-hot, and consume the accumulator in GSVertexTrace::Update.

- Per-buffer FmmAcc (position/texture/color pairs) in GSVertexBuff,
  maintained by VertexKickDirect for triangle strips/lists. A watermark
  dedups already-folded vertices (past strip warmup only the register-
  resident new vertex accumulates); rewind/compaction sites clamp it so
  rewritten positions re-accumulate. Fan emissions poison the draw's
  fused state — the fan head doesn't fit the watermark model and
  FlushPrim can rebuild fan indices (caught by GS_VERTEX_CROSSCHECK on
  the UYA dump corpus).
- FmmFinish reproduces the legacy tail bit-exactly or declines: STQ
  requires one constant, normal, nonzero Q (min(s/q) == min(s)/q by
  monotone IEEE division; negative Q swaps; FLT_MAX sentinels folded at
  quotient level) and no inf/NaN S/T — legacy masks NaN quotients per
  lane and reports vt.nan, which a min/max summary can't reproduce.
  Declined draws run the legacy FindMinMax unchanged.
- aarch64-only: NaN detection relies on FMIN/FMAX propagation (sticky in
  the raw accumulator); SSE min/max can drop a NaN again. x86 keeps the
  legacy walk everywhere.
- TME/FST/IIP are stable across one draw's emissions (TestDrawChanged
  flushes or buffer-switches on any draw-affecting PRIM change), so
  kick-time PRIM flags select the accumulation policy.

Gates: gs_vertex_tests +4 property sweeps (600k draws vs a transcription
of the legacy walk: benign must fuse and match bit-exactly, special
Q/ST must decline or match); GS_VERTEX_CROSSCHECK replay of all 10 local
dumps clean; sw+vk frame hashes bit-identical to pre-campaign baselines;
recompiler_tests 1359/1359.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 09:49:24 -07:00
Brian DegenhardtandClaude f346103ef7 GV-3b: only refresh the cull mirror when the cull rect actually changes
RefreshKickMirror ran unconditionally from UpdateScissor, which fires on
every context switch — measured at 1.87% of the MQ65 GS thread on UYA.
Cache the cull rect the bounds were derived from (poison-initialized)
and skip the bounds re-derive + mirror refresh when it is unchanged;
context switches with an identical scissor become a single vector
compare. The buffer-reactivation refresh stays unconditional (copied
entries carry outcodes from the source buffer's bounds). Entries are
only ever read by the prim class that wrote them (ApplyPRIM resets the
strip window), so skipping on class-only changes is safe.

Gates: gs_vertex_tests 13/13; gsrunner frame hashes bit-identical for
all 15 dumps on sw and vulkan; GS_VERTEX_CROSSCHECK replay clean.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 09:12:14 -07:00
Brian DegenhardtandClaude 9c7921a58f GV-4: accumulate the draw rect across fused batches
Every accepted prim updated temp_draw_rect through memory: load, union,
scissor clamp (with its own scissor.in load), store. Accumulate the
per-prim rects in the batch cursor instead and fold them into
temp_draw_rect with a single union + clamp at each cursor seam.

Exact by two properties: rintersect is monotone and idempotent, so one
clamp over the union equals the legacy per-prim clamp-then-union chain;
and the draw's first prim (which replaces temp_draw_rect rather than
unioning) can only be the first prim accumulated after a seam, because
the index buffer only empties behind flush seams. All temp_draw_rect
readers (CheckFlushes/SetDrawBuffDirty, Flush and autoflush analysis,
CheckOverlapVertsSlow, the transfer paths) run behind cursor seams or
outside vertex batches, so the deferral is unobservable.

Gates: gs_vertex_tests 13/13; gsrunner frame hashes bit-identical to
the pre-campaign baseline for all 15 dumps on sw and vulkan;
GS_VERTEX_CROSSCHECK replay clean.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 08:19:14 -07:00
Brian DegenhardtandClaude Fable 5 321c9dcb9b GV-3: scalar-outcode accept/cull for the GS vertex kick
The per-prim accept/cull decision was the largest slab of the fused
packed handlers (~30% on the MQ65 annotate): a NEON bbox build whose
verdict crossed to scalar through 3x umaxv + 2x uminp + 5x fmov
serialized into a ccmp chain - all exposed latency on in-order cores.

Replace it, for the hot shapes (point/line always; triangle strips and
lists and sprites at native res without AA1 expansion), with an exact
scalar reformulation over per-vertex precomputed metadata:

- Scissor reject: the bbox is the min/max of the vertices, so "bbox
  outside an edge" == "every vertex outside that edge". For
  triangle/sprite at native res the ceil16/floor16 interior rounding
  and the cull rect's +/-8 fold into pixel-band bounds (band(v) =
  (v-1)>>4 vs (cull.x+14)>>4 etc.); point/line compare raw 12.4 coords
  against cull. The per-prim test is an AND of 4-bit outcodes.
- Interior-empty: (v+15)>>4 == ((v-1)>>4)+1 identically, so
  ceil16(min) > floor16strict(max) <=> all vertices share one band on
  that axis - a pure equality test (EOR/TST) on packed bands.
- Degenerate triangle: the legacy 128-bit eq on {x,y,x,y} window
  entries is xy equality - one u64 compare on the packed position.

Each kick appends a CullMirrorEntry (packed window xy + 28-bit bands +
outcode, one 16-byte slot) to a scalar mirror of the xy ring inside
GSVertexBuff, computed from the raw vertex XY on the scalar side where
it dual-issues against the NEON parse. The mirror is maintained
wherever the xy ring is written (kick, draw-buffer compaction,
PushBuffer copy, buffer-reactivation copy, FlushPrim fan rebuild), and
outcodes are re-derived from the stored positions on every scissor /
context / draw-buffer-env change (RefreshKickMirror; bands and
positions are bounds-independent). Bands are 28-bit so any s32 window
coord packs exactly - games that write junk in XYOFFSET pad bits (the
full 32-bit lane is subtracted, matching the NEON ring) cannot alias.

Rejected prims now never touch NEON; accepted prims compute the bbox
via the factored ComputeCullBBox (bit-identical to the legacy path)
feeding the draw_rect update. Fans, AA1, upscale, and the staged
piecemeal path keep the legacy CullTest - its extraction tower now
sits only on that fallback branch.

Gates: gs_vertex_tests 13/13 including 3M-case scalar-vs-legacy
property sweeps over GS-shaped scissors with band/edge-snapped coords;
GS_VERTEX_CROSSCHECK replay of all 15 dumps asserts scalar == legacy
per prim, clean; gsrunner frame hashes bit-identical to the
pre-campaign baseline on sw and vulkan; recompiler_tests 1359/1359.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:12:53 -07:00
Brian Degenhardt bc33ef1d09 arm64: attribute yaps2 authorship in SPDX copyright headers
Add "yaps2 Dev Team" copyright to the files we authored. Net-new files
(all 42 pcsx2/arm64/ codegen/ProgCache/persist sources, the recompiler
test suite + harness, and the vurunner/eerunner tools) never existed
upstream, so they carry yaps2 sole credit. RecStubs.cpp predates the
fork and was heavily extended, so it keeps PCSX2 credit and adds yaps2.

The five pre-existing arm64 files we only lightly touched (AsmHelpers,
Vif_Dynarec, Vif_UnpackNEON) stay PCSX2-only. GPL-3.0+ license lines are
unchanged throughout; this is authorship attribution only.
2026-07-19 07:37:15 -07:00
Brian DegenhardtandClaude Fable 5 c25e1a58cc GV-2: GS vertex kick: register-cache buffer state across fused batches
VertexKickDirect round-tripped every hot buffer field through memory per
vertex: m_vertex/m_index pointer loads plus head/tail/next/xy_tail/itail
loads and stores each kick, all loop-carried through store-forwarding on
in-order cores. The MQ65 annotate put this bookkeeping at ~15% of the
handler (m_vertex reloaded 7x, m_index 4x per vertex).

Introduce VertexKickCursor: the fused packed handlers load
{vb,ib,vbuff,ibuff,head,tail,next,xy_tail,maxcount,itail} into locals
once per batch and pass the cursor through VertexKickDirect, so the
fields live in registers across the whole GIF batch. The cursor is
stored back before - and reloaded after - every callee that can flush,
grow or switch draw buffers (CheckOverlapVertsSlow + Flush,
HandleAutoFlush, GrowVertexBuffer, Flush(VERTEXCOUNT)); GrowVertexBuffer
in particular reads tail/itail for its preserved-copy sizes. The
env-backup block (memcpys + SetDrawBufferEnv) touches no buffer state
and rides through cursor-resident. The staged VertexKick wrapper keeps
piecemeal handlers at their previous load-once/store-once shape.

Also hoist the depth-clamp decision out of the per-vertex path:
GetDepthClampMode() resolves config + renderer kind + ZBUF bpp once per
batch (all invariant across a fused batch) and ApplyDepthClampMode
applies the resolved mode, replacing 3 GSConfig loads and a
GSIsHardwareRenderer() call per vertex with one register compare - which
also removes the only warm-path call from the loop CFG.

Verified: the built <4u,false> handler loads the cursor once at entry
and after the two flush seams only; stores appear only on the overlap
slow path; depth-clamp is two cmp/branch on the disabled default.

Gates: gs_vertex_tests 9/9; gsrunner frame hashes bit-identical to the
pre-campaign baseline for all 15 dumps on sw and vulkan;
GS_VERTEX_CROSSCHECK replay of all dumps clean; recompiler_tests
1359/1359.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 07:35:20 -07:00
Brian DegenhardtandClaude Fable 5 64ff1d5a62 GV-1b: TBL-based packed vertex parse on aarch64
Replace the SSE-translated parse chain with byte-permute kernels: one
vqtbl2q_u8 builds m[0] (S/T/RGBA/Q gathered from the STQ+RGBAQ qwords —
the legacy RGBA pack alone was ~11 NEON ops), and for XYZF2 a shared USHR #4
plus one vqtbl2q_u8 over {r2, r2>>4} builds m[1], with out-of-range TBL
indices providing the 24-bit Z and 8-bit F masks for free. XYZ2 takes a
single-register TBL for the low half and inserts {UV, FOG} as one 64-bit
lane. The Q==+0.0 -> FLT_MIN rewrite folds to CMEQ+AND+ORR against a
lane-3-only constant.

Handlers call the new _Fast dispatchers: aarch64 takes the TBL kernels
(GS_VERTEX_CROSSCHECK builds run the legacy kernels alongside and
pxAssertRel bit-equality per vertex); x86 keeps the legacy path unchanged.

Gates: gs_vertex_tests 9/9 including 2M-case NEON-vs-scalar-model sweeps;
gsrunner frame hashes bit-identical to the GV-0 baseline (15 dumps, sw +
vulkan); crosscheck build replayed all 15 dumps with zero divergence
assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 07:02:23 -07:00
Brian DegenhardtandClaude Fable 5 44f5e503dc GV-1a: kick parsed vertices straight from registers, write m_v once per batch
Split VertexKick into a staged-m_v wrapper (piecemeal reg handlers, autoflush)
and VertexKickDirect, which stores the incoming vertex to the buffer from the
values still in registers. The fused packed STQRGBAXYZF2/STQRGBAXYZ2 handlers
(auto_flush=false instantiations) now parse into locals, kick directly, and
write m_v/m_q once at batch exit — removing two 16-byte staging stores, the
store-forwarded reload pair, and the loop-carried UV/FOG reload per vertex
(packed XYZF2/XYZ2 never write UV/FOG, so they are loop-invariant).

Semantics preserved exactly: the depth-clamp hack is factored into
ApplyDepthClamp and applied by both entry points (hoisting it ahead of the
overlap/autoflush checks is neutral — neither reads XYZ.Z), and the
draw-buffering overlap slow path syncs m_v before running since it reads the
incoming vertex's XY. Autoflush instantiations keep the staged path
(HandleAutoFlush reads m_v).

Gates: gs_vertex_tests 7/7; gsrunner frame hashes bit-identical to the GV-0
baseline across all 15 dumps on both sw and vulkan renderers;
recompiler_tests 1359/1359.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 06:57:25 -07:00
Brian DegenhardtandClaude Fable 5 92c3d5b247 GV-0: factor GS vertex parse + cull kernels, add oracle test suite
Extract the fused packed-vertex parse (STQRGBAXYZF2/STQRGBAXYZ2) and the
per-prim accept/cull decision out of GSState.cpp into pure free functions
(GS/GSVertexKick.h, namespace GSVertexKernels). No behavior change: gsrunner
frame hashes over all 15 local dumps are bit-identical on both the software
and Vulkan renderers.

New gtest target gs_vertex_tests pins the kernels against independent scalar
models of the GIF/GS semantics (plain integer C, no GSVector) over directed
edges (Q==+0.0 vs -0.0, 16-subtexel boundaries, duplicate vertices) plus
3.5M-case randomized sweeps per prim class. Optimized kernel implementations
in the GV campaign must pass the same suite bit-for-bit.

Also adds the GS_VERTEX_CROSSCHECK CMake option (default OFF) that later GV
items use to run legacy+new kernels side by side during dump replays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 06:42:16 -07:00
Brian Degenhardt 4ca00c6088 EE counters: clamp T_COUNT reads until the boundary interrupt is delivered
NFL 2K5 (SLUS-20919) hangs at the boot logo in a 64-bit divide-by-repeated-
subtraction with a huge unsigned dividend. Its 64-bit clock is an overflow-
ISR-maintained wrap accumulator plus a live T0_COUNT read (bus/16, OVFE),
reconciled lock-free with double reads — airtight on hardware, where the
count wrap and the overflow interrupt are the same edge and the ISR preempts
before any later read.

Under the JIT the guest can observe the wrap while the ISR's effects are
still pending, in two phases: (1) the count (derived from the live
cpuRegs.cycle) crosses the boundary before the scheduled rcntUpdate event
runs; (2) rcntUpdate has wrapped the count and raised the INTC, but the
exception waits for the next event test — which our static-linked /
short-block tails defer past the reader's entire load sequence (traced live:
the wrap event fires at the reader's own block-entry event test, and
delivery lands at its jr-ra exit, 30 cycles too late). Either way the game
reads stale-accumulator + wrapped-count, time goes backwards one wrap
period, and the divide runs ~2^48 iterations.

Clamp the read to just-before-the-boundary until the interrupt has actually
been delivered. The deliverability guard (INTC pending & unmasked & Status
EIE/IE, no EXL/ERL) makes this exact: inside the handler or with the source
masked (e.g. the game's DisableIntc reader, which reconciles the raw wrap
itself) the wrapped count stays observable, as on hardware.

Pinned by EeTimerCountReadRace.* in recompiler_tests. Verified live: cold
fastboot reaches attract; previously parked at the divide loop within ~20s.
2026-07-19 00:20:01 -07:00
Brian Degenhardt c4a934e9b9 Merge remote-tracking branch 'yaps2/main' 2026-07-18 19:33:55 -07:00
Brian Degenhardtandpstef 69365b15f9 arm64 mVU: differential test for a clamp after an XGKICK
An XGKICK is the one mid-block C call that can sit between a block's
prologue and a clamp emitter, so it is the only place where a clamp reads
bounds across a call boundary. Nothing in the suite paired the two: every
Clamp/Overflow case is call-free, and every XGKICK case is clamp-free.

The probe fires XGKICK (mVU_XGKICK_DELAY, a real C call), then a VMUL whose
square overflows FLT_MAX under vu1Overflow, and diffs the result against the
interpreter. The interp-side assert on +MAX_FLOAT keeps it honest - it
cannot pass by never clamping.

The test is design-neutral as written: mVUclamp1 reloads the clamp bounds
from mVUglob on every call, which no C call can disturb, so it cannot fail
against today's emitter. It is coverage for the pairing, and it goes red for
any future scheme that carries clamp bounds in a caller-saved register
across the kick.

Cherry-picked from pstef's yaps2 PR #7 (test-only, applies unchanged).

Co-Authored-By: pstef <3462925+pstef@users.noreply.github.com>
2026-07-18 19:16:56 -07:00
Brian Degenhardtandpstef 3d9afe72bc arm64 mVU: finalise E-bit flags instead of eliding them
A flag written but never READ still has to be finalised: mVUendProgram
stores it into VI[REG_MAC/STATUS/CLIP_FLAG], and COP2 reads those back on
VU0.

eBitPass1 already forced needExactMatch|=7 when the E-bit sat in the block
being compiled (State of Emergency 2, Driving Emotion Type-S), but
_mVUflagPass's forward scan hit an E-bit end and just broke out, marking
nothing - while the JR/JALR arm right beside it does force the bits. So a
block whose successor ends the program was told "the successor reads no
flags", and mVUsetFlags' forcing loop therefore never set mFLAG.doFlag: the
JIT emitted no flag writes at all. Finalisation then read a never-written
ring instance, because findFlagInst sees all-(-1) and falls back to slot 0.

Repro (a loop writing MAC 3x/iter, reading only STATUS, E-bit in the
fall-through block) finalised REG_MAC_FLAG as JIT=0x0 vs interp=0x24. Not
MAC-specific: the same hole dropped the low Z/S bits of STATUS (0xc0 vs
0xc3). Both VUs are affected - mVUdispatcherA reloads the ring from
VI[REG_*_FLAG] at program entry, so a stale instance left by one VU1
program is observable by the next one that reads MAC before writing it.

Fixed at both E-bit sites (eBitPass1 and the shortBranchPass lookahead) and
for both VUs, but not via needExactMatch, which upstream uses. That bit
does two jobs: it makes mVUsetFlags emit the tail flag writes (correctness),
and it persists into the successor's pState, forcing exact-match block
lookup and the mVUsetupFlags reorder at every link reaching a program end
(no correctness value). Instead:

  - mVU.needFlagFinalize, compile-scoped, never enters pState, never
    changes block identity.
  - mVUsetFlags forces only the LAST tail FMAC's writes.
  - getLastFlagInst recovers a flag the block never wrote from the incoming
    ring phase - what the exact-match reorder had been implicitly providing.

Pre-existing and shared with upstream x86, whose _mVUflagPass has the
identical break. This diverges arm64 from the x86 JIT deliberately: x86's
behaviour here is a garbage-read of an unwritten ring slot, not a semantic
choice, so matching the interpreter (which agrees with real hardware) is
the correct call. The cheaper mechanism applies to x86 too.

Tests: new vu0_flag_link_reorder_tests.cpp - differential JIT-vs-interp
across an exact-match flag link. Against the unfixed tree these go 6 red
(both VU1 probes, both back-edge probes, the unconsumed-MAC finalisation,
and the delay-slot rotation) / 4 green; all 10 green after the fix. Also
adds vu1BranchToEbit, the first VU1 probe in the ABI-digest backstop: every
probe there compiled on VU0, so a VU1-only emitter change moved no digest.

Emitter shape change -> kMvuCompilerAbiVersion 14->15 (+ mirror + digest
row + the new VU1 pin). Full recompiler_tests 1352/1352.

Cherry-picked/reworked from pstef's yaps2 PR #7 onto our tree; digests
re-harvested on our base (spinLoop column kept as the 6th field, VU1 probe
added as the 7th).

Co-Authored-By: pstef <3462925+pstef@users.noreply.github.com>
2026-07-18 19:16:38 -07:00
Brian Degenhardtandpstef 131c2592b7 arm64 mVU: elide status-flag self-Movs at block links
mVUsetupFlags rebuilds the four status-flag instances (gprF0-F3) on every
exact-match block link. getFlagReg(i) is gprF[i], so Mov(gprFi,
getFlagReg(bStatus[i])) is a no-op when bStatus[i]==i (the identity ring
phase / all-same-instance case). vixl does NOT drop Mov(Wd,Wd)
(kDontDiscardForSameWReg): it emits a real ORR because the 32-bit move
clears bits 63:32. So the old code emitted up to four dead ORRs per link.

Guard every emit in all four permutation branches on dst!=src. The temp
regs gprT1-3 never alias gprF0-3, so the guard only ever elides genuine
self-moves; skipping a dst<-dst Mov is unconditionally correct.

Emitter shape change -> kMvuCompilerAbiVersion 13->14 (+ mirror + digest
row). Only the two status-flag-linked probes move (indirectJump,
condEvilBranch); the others are bit-identical to abi 13.

Cherry-picked/reworked from pstef's yaps2 PR #7 onto our tree (our abi 13
is the SL-12 spin-FF, so this renumbers pstef's 12->13 to 13->14 and keeps
the spinLoop digest column). Digests re-harvested on our base.

Co-Authored-By: pstef <3462925+pstef@users.noreply.github.com>
2026-07-18 19:12:26 -07:00
Brian DegenhardtandClaude Fable 5 69060c328f SL-12: arm64 mVU0: fast-forward all-NOP VI-branch spin-wait loops
UYA-gameplay telemetry (600f, deterministic eerunner): 89% of ALL VU0
micro execution (940M of 1052M cycles) is three EE-handshake busy-wait
loops — a conditional VI branch (IBEQ/IBNE vs vi00) whose entire loop
body is architectural NOPs, spinning until the EE's CTC2 releases the
handshake register. The mVU JIT executed every iteration (~235M/600f)
plus 59M dispatch envelopes, all doing architecturally nothing. This was
also the entire mechanism of the superblock wave's UYA regression: both
S2 event coarsening and SL-08 fork-arm charging inflate the charged
EE-cycle length of the kick-to-handshake windows, and VU0 faithfully
spins to fill them (+118M cycles, +15.7% dispatches, mVU0 host insns
+12% — while total virtual time and program finishes stay identical).

The fast-forward: at mVUcompile block head, detect the two spin shapes
(4-pair {cond; NOP; B ->head; NOP} and 2-pair self-loop; every non-branch
slot must be the exact NOP encodings 0x000002FF/0x8000033C, so no
flags/Q/P/reg/XGKICK/E/M/D/T/I effects exist by construction) and emit a
3-insn head ahead of mVUtestCycles: evaluate the loop-exit VI compare;
while the spin holds, zero mVU.cycles so the existing budget-exhaust path
consumes the whole remaining grant (VU0.cycle += grant via the exit
stub's totalCycles math) and parks the VE-07 resume at this block. This
is sound because the EE thread is stalled inside Execute for the whole
grant — nothing can write the handshake VI mid-grant, so N identical
iterations collapse to one check plus a cycle skip. Cycle-accounting
observables are unchanged (grants, deltas, and sync decisions are all
EE-side); only the useless iterations disappear.

M2 codegen_ab uya-gameplay (600f x3): EE-thread insns -6.19% / cycles
-4.27% vs SL-11 — more than the whole superblock-wave regression; the
full EP-4->SL-12 stack is now net-negative-cost on UYA too (insns -2.10%
vs the pre-superblock EP-4 baseline). SotC (VU0-light): -0.00% insns,
neutral as expected. vurunner corpus 5818 caps: bin-for-bin and
per-file identical with the FF forced off. UYA stepdiff: divergence
signature line-for-line identical to the FF-off baseline (the three
known-benign timer/cycle-phase MMIO classes, same pcs).

Emitted-shape ABI: kMvuCompilerAbiVersion 12 -> 13 (+ versioning-test
mirror); new spinLoop digest probe pins the FF head emission.

Tests: mvu_spin_ff_tests (whole-grant consumption with zero side
effects + TPC parked at the spin head, release-and-complete, both
shapes, and a side-effectful-body negative that must NOT fast-forward);
full recompiler_tests 1342/1342.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:16:40 -07:00
Brian DegenhardtandClaude Fable 5 634b5d6f09 SL-11: shared compact tail for cold side exits
Each cold side exit ended with a full SetBranchImm tail (pc materialize+store,
cycle update, far event check, linked B). Factor the invariant part into one
per-generation stub emitted with the dispatchers: Str pc / Adds RECCYCLE,x1 /
B.ge DispatcherEvent / Ret. A cold exit is now Mov pc, Mov cycles, BL stub,
linked B — the BL/RET pair stays hardware-RAS-balanced on the no-event path,
and the event path discards the link register (pc is already stored, re-entry
comes through the dispatcher). Adds with a zero cycles register sets N/Z from
RECCYCLE itself, preserving emitCycleUpdateAndEventCheck's zero-cycles Cmp
semantics. Falls back to SetBranchImm for its special-case shapes (resident
back-edge, WaitLoop FF), which cannot occur at a forward continuation site.

Cold-exit bodies drop to ~5 insns for the no-delay-slot case (the 4-site UYA
physics superblock's exits: 84 bytes total). recompiler_tests 1339/1339.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:15:11 -07:00
Brian DegenhardtandClaude Fable 5 df7d7ae3d3 SL-10: outline superblock side exits into a cold arena
S2's taken-arm side exits were emitted inline after each block's tail, which
interleaves cold bytes into the hot compile-order stream — the SD865 S2 A/B
measured +5.4% EErec icache-miss density and +4.5% branch-miss density from
exactly this (net cycles neutral: the footprint cost ate the −1.7% executed-
volume win). The 2026-07-10 icache campaign closed hot/cold layout because
compile-order emission was already 92%-packed hot; S2 was the first change to
break that invariant, so the arena remedy is re-motivated for these bytes.

An 8MB cold arena is carved from the top of the EE cache between the code
region and the constant pool (same 64KB-slop discipline as the pool boundary,
and still inside the EE region so fastmem fault range checks and perf
bucketing are unaffected). A side exit's in-block footprint is now a single
far-B island bound at the tail (the site's short-range Tbz/Cbz/B.cond keeps
targeting the island); the bodies emit in a second session into the arena
after the hot block finalizes, and the islands are patched to reach them
(same single-word B rewrite + cache maintenance as link patching). The arena
recycles on full cache reset, and exhaustion triggers the same deferred
reset as the code region (with a diagnostic log line naming which region
filled — this session showed full-reset causes are otherwise invisible).

Deterministic 600f UYA contmem A/B (same 7613-block set): hot-stream EE
bytes 1.7351 → 1.6379 MB (−5.60%). recompiler_tests 1339/1339; EeFuzz 4×2000
seeds; contmem 60f/600f byte-identical to baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:14:41 -07:00
Brian DegenhardtandClaude Fable 5 71fdc2f03b SL-09: offline bisection tooling for EE superblock triage
The instrumentation that localized SL-07, kept for the next hunt:

eerunner stepdiff timer-taint classifier:
- Bound the taint walk at the observation pc: the offending block can end
  mid-disasm (e.g. at an ei), and walking on into the NEXT block's jr delay
  slot let an untainted overwrite erase taint that was live at the
  observation point (a v0 timer read classified as a real divergence).
- Seed the const-tracker with the interpreter's register file snapped at
  the offending block's entry (one extra frame re-run), so load addresses
  materialized in earlier blocks resolve - the software-virtual-clock
  accumulator shape (lui 0x1000 in one block, ori 0x800 in the next) was
  under-tainting. Every unmodeled GPR-writing op now clears known[] so a
  seeded value can never go stale into an over-taint.
- Widen the benign class from the four EE timer COUNTs to cycle-phase MMIO
  (0x1000xxxx hardware regs + GS privileged bank): DMAC CHCR tag fields
  etc. legitimately read back a different transfer phase under JIT-vs-
  interp cycle granularity. The ResyncAfter reconvergence gate remains the
  safety net against masking real bugs.

EE rec testing-only env knobs (no production effect when unset):
- YAPS2_EESB: bitmask of continuation-site families (bit0 BEQ/BNE, bit1
  BLEZ/BGTZ, bit2 REGIMM BLTZ/BGEZ; 0 = pre-superblock block formation).
- YAPS2_EESB_LO/HI: guest-pc window for sites - the binary search over
  this window is what pinned SL-07 to one branch at 0x3f84c8.
- YAPS2_EESB_DUMP=<hex pc>: disassemble any block covering that pc after
  finalize (needs INCLUDE_DISASSEMBLER; no-op otherwise).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:49:48 -07:00
Brian DegenhardtandClaude Fable 5 88f6b2159d SL-08: arm64 EE rec: charge both fork arms the full block cycle accrual
The two-arm conditional fork (recBEQ/BNE_process, recBranchSingle,
recBranchLink) relies on LoadBranchState to restore s_nBlockCycles for the
second SetBranchImm - but the TrySwapDelaySlot fast path skips the
Save/Load pair entirely, so scaleblockcycles_clear's consume in the taken
arm left the fallthrough arm charging the clamped minimum of 1 cycle
instead of the block's accrual (x86 parity: its scaleblockcycles does not
clear, so both tails see the full count there). Pre-existing since the
cycle-delta rework; superblocks amplify the loss since a continuation
block accrues more per tail. Surfaced by the SL-07 emitted-code audit of
UYA's 0x3f8448 block: taken arm +13, fallthrough arm +1.

Capture s_nBlockCycles before the first tail and restore it after the
label bind, unconditionally (harmless on the !swap path, where
LoadBranchState overwrites it right after). Verified in the re-dumped
emission: both arms of the same branch now charge equally (+3/+3).
recompiler_tests 1339/1339. No harness cycle-charge introspection exists,
so this is pinned by the emitted-shape audit rather than a unit test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:49:35 -07:00
Brian DegenhardtandClaude Fable 5 989f1eb822 SL-07: arm64 EE rec: fix instinfo misalignment after swapped-slot continuation
At a superblock continuation site whose delay slot TrySwapDelaySlot hoists
ahead of the compare, recompileNextInstruction(swapped_delay_slot=true)
restores g_pCurInstInfo to the branch's entry. That restore is x86 heritage
from a world where a conditional branch always ended the block, so nothing
downstream consumed the pointer. A continuation keeps compiling, and the
main loop advances g_pCurInstInfo by exactly one per instruction - so every
subsequent op in the superblock read its PREDECESSOR's EEINST: wrong
liveness, wrong const bits, and (the visible symptom) wrong COP2 flag-hack
bits. In R&C UYA's VU0-macro physics routine at 0x3f8448 the block's last
MAC/status writer read the delay slot's info, elided its whole flag body
under vuFlagHack, and downstream MAC-flag polls acted on stale flags -
Ratchet falls through the floor from the 45FE0CC4 .02 savestate.

Fix: when the slot was swapped, advance g_pCurInstInfo to the slot's entry
before returning to the main loop (the !swap path is already aligned by the
inline delay-slot recompile). Localized by a guest-pc-range A/B bisection to
the single BLTZ site at 0x3f84c8 (932KB of contmem divergence from that one
site; back to baseline with the fix), then pinned by diffing the emitted
host code against the sites-disabled emission.

Test: EeVu0Cop2MacroFlagHack.LastWriteCommitsAfterSwappedDelaySlotContinuation
(red before, green after). Full recompiler_tests 1339/1339; UYA sstate
contmem 60f back to SL-02 baseline (29KB class, was 1.23MB); 6k-seed fuzz
spot check clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:49:00 -07:00
Brian DegenhardtandClaude Fable 5 c57dbb6293 SL-06: EE fuzzer: add branch-into-delay-slot loop mix
The existing mixes never emit a branch targeting another branch's delay
slot — exactly the blind spot that let the SL-05 zero-length-block wedge
(guest dcache-flush idiom) reach live UYA past 30k seeds. The new
DelaySlotTargetLoopMix generates counted loops shaped `beqz/blez exit;
addiu ctr,-1; body; bgtz ctr, <the addiu>`: the head branch is a
superblock continuation site whose delay slot is a backward-split target,
covering the split clamp's degenerate (site at program entry, loop 0) and
non-degenerate cases plus block re-entry at a delay-slot address. Both
runners execute the identical trace (ctr is focus-excluded and strictly
decreasing). 12k-seed sharded soak clean; suite 1338/1338.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 09:34:49 -07:00
Brian DegenhardtandClaude Fable 5 c0de1e9f31 SL-05: arm64 EE rec: fix zero-length-block wedge from the superblock split clamp
The SL-03 backward-split clamp ends a block before a continuation branch
when the split target is that branch's delay slot. When the branch is the
block's FIRST instruction — the branch-into-delay-slot loop idiom, e.g.
R&C UYA's dcache-flush routine (beqz exit; addiu t2,-1; cache ops; bgtz
t2, <the addiu>) — the clamp produced s_nEndBlock == startpc: a
zero-length block whose short tail compiles to an unconditional
self-linked B with no event check, wedging the VM (guest pc parked, cycle
counter racing; UYA hung during boot under --stepdiff). Fall back to not
splitting at all in the degenerate case (always correct — the target gets
its own block when branched to), and assert s_nEndBlock > startpc.

Test: HeadBranchDelaySlotLoopDoesNotWedge (hangs on regression, plus a
formation assertion); recompiler_tests 1337/1337.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 09:28:59 -07:00
Brian DegenhardtandClaude Fable 5 6c05faa72b SL-04: arm64 EE/IOP rec: keep the code region out of the constant pool
The EE rec's code-full boundary was recPtrEnd = cache end - 64KB, but the
constant pool (manual-check snapshot blobs + the dispatcher stubs' far-call
veneers) is carved from the LAST 256KB of the same region — so the legal
code region overlapped the pool by 192KB, plus the 64KB slop a single
compile may overhang past recPtrEnd. Once a session emitted enough code to
reach cache-end minus 256KB, block emission overwrote the pool — whose
first bytes are the DispatcherEvent/JITCompile bl-veneers — and the next
event dispatch jumped through a corrupted veneer (observed as JITCompile
re-entered for an already-compiled pc; the fuzz soak's
BackwardLoopResidencyMix hit it deterministically once SL-03 superblocks
grew per-block emission enough to fill the cache into the overlap). IOP had
the boundary-case variant: recPtrEnd == poolBase exactly, so only the slop
overhang could reach the pool.

Fix: compute the pool first and set recPtrEnd = poolBase - 64KB on both
recs, with a pxAssertRel pinning the invariant. Repro gate: the 30k-seed
sharded EE fuzz soak (aborted before, green after); full recompiler_tests
1336/1336.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 09:11:47 -07:00
Brian DegenhardtandClaude Fable 5 151b93fde9 SL-03: arm64 EE rec: superblocks — forward conditionals become continuation sites
The block scanner no longer ends a block at a forward conditional branch
(BEQ/BNE/BLEZ/BGTZ/BLTZ/BGEZ; non-likely, non-link, non-BCx): it records a
continuation site and keeps scanning at the fallthrough, so the not-taken
path compiles as one straight line — no pc store, no event check, no
linked-B, no next-head reload; register and constant residency ride through
the former boundary. The taken arm becomes a cold side exit outlined after
the block tail: it snapshots the compile state at the branch
(BranchCompileState, per pending exit), and its emission restores the
snapshot, compiles the taken-path delay slot (unless TrySwapDelaySlot
already hoisted it), and ends with the normal SetBranchImm flush + event +
linked-B tail. Compare shapes mirror recSetBranchEQ/recSetBranchL with the
sense inverted (branch out when TAKEN), const fast paths included.

Analysis stays exactly as conservative as today's block ends at each former
boundary: the liveness backward pass merges all-live at the branch and its
delay slot (the taken path leaves the block there), and the COP2
deferred-commit passes run per segment delimited at sites. A backward-split
landing on a site's delay slot clamps to the branch instead (a split pair
would leave the delay slot outside the analyzed range). Event-check
coarsening equals today's straight-line blocks: one check per exit, range
still capped by the 4K page. Caps: 8 sites, 128 insns per block.

Composition with SL-01: loops whose body contains a forward conditional
were previously split at it and could never form a self-loop — now they
fuse into one block and the loop-residency preheader/back-edge applies
(pinned by LoopWithInternalForwardBranchBecomesResident).

Deliberate non-sites: backward branches (loops keep the SL-01 shape),
likely variants (taken-only delay slot — different continuation shape),
BEQ rs==rt (unconditional idiom), branch-class delay slots, compile-time
const-resolved-taken. BNE rs==rt and const-resolved-not-taken continue
with no side exit.

Tests: ee_rec_superblock_tests (formation via recEeBlockGuestSize, both
runtime paths vs interp, delay-slot-both-paths, dirty-state flush at taken
exits incl. NEON quads, const propagation, multi-site, cap, SMC in the
fused range, memory traffic across a site); full recompiler_tests
1336/1336.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:34:06 -07:00
Brian DegenhardtandClaude Fable 5 8c8dc89e2b SL-03a: arm64 EE rec: factor branch fork state into BranchCompileState
Pure refactor: SaveBranchState/LoadBranchState's seven parallel statics
become one struct with capture()/restore(), so the superblock side-exit
machinery (SL-03b) can hold one snapshot per pending exit instead of the
single global fork buffer. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:22:16 -07:00
Brian DegenhardtandClaude Fable 5 63f882312e SL-02: arm64 EE rec: retain GPR/FPRC residency across the COP2 sync seam
cop2EmitConditionalSync paid iFlushCall(FLUSH_FREE_XMM|FLUSH_FREE_VU0) on
the UNCONDITIONAL path, evicting every caller-saved allocator entry even
though the C call it protects sits behind the runtime VPU_STAT Tbz and is
skipped whenever VU0 is idle. That eviction was the only remaining
mid-body seam in COP2-heavy self-loops — it forced SL-01's back-edge
reconcile to reload the loop pins every iteration and evicted the
block-resident FCR31 at every sync-marked COP2 op in straight-line code.

Replace it with a retain seam (cop2FlushForConditionalSync):
- GPR/FPRC entries: writeback-keep (_flushArm64GPRregs) — memory stays
  current for the callee, values stay resident on the skip path, and the
  sync path reloads them inside the Tbz via cop2ReloadRetainedAfterSync
  (new _reloadArm64GPR, the Ldr inverse of _writebackArm64GPR). The sync
  callees (vu0SyncThin/RunAheadThin/_vu0FinishMicro -> CpuVU0->Execute)
  have no path that writes EE GPRs or fprc, so a retained mapping cannot
  go stale.
- VIREG entries: freed WITH writeback — VU0 execution writes VU0.VI.
- TEMP/PCWRITEBACK: freed (transient, no reloadable home).
- NEON + VF compile cache: unchanged policy (freed — 128-bit classes
  cannot ride a C call and the macro body wants the file).

Applies to both the interlock and non-interlock sites, and to LQC2/SQC2
via their shared cop2EmitConditionalSync call.

Gates: recompiler_tests 1320/1320; UYA frames-2 + SotC frames-20
--stepdiff signatures identical to the SL-01 baselines (UYA .02 runs with
VU0 live, so the sync-taken reload path is exercised); M2 static census
+0.08% (cold-path reloads inside the conditional).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 07:39:31 -07:00
Brian DegenhardtandClaude Fable 5 852cfef30a SL-01: arm64 EE rec: loop-carried residency for self-loop blocks
A block whose terminal branch targets its own startpc (the scanner's
backward-split rule makes loop heads block starts) now compiles with a
register-resident back-edge instead of the per-iteration full flush +
linked-B + full reload round trip:

- Preheader: the <=5 most-used loop GPRs (EEINST_USED counts; pin-table
  guests excluded) are allocated MODE_READ|MODE_WRITE (dirty-pessimized so
  body-emitted evictions always write back), marked loop-pinned (LRU
  eviction avoids them but may still take them - allocation never fails on
  a pin), and the loop-top label binds after it.
- Back-edge (taken arm via SetBranchImm -> SetBranchBackedge): a two-phase
  reconcile to the loop-top snapshot (VF-cache flush + constant
  materialization + writeback/free of non-snapshot entries, then reload of
  displaced pins from memory), the cycle Adds + b.ge event check
  side-exiting to a cold spill stub (spill pinned set + Str pc +
  DispatcherEvent), then a single B to the loop-top. Dirty values ride
  host registers across iterations.
- Mid-body C seams stay correct without any candidacy analysis:
  iFlushCall frees caller-saved entries coherently and the reconcile
  restores the snapshot, so a seam only localizes the win away.
- recClear safety: the internal back-edge B bypasses the entry redirect
  stub, so it is registered on BASEBLOCKEX (backedge_site/backedge_stub)
  and Arm64BaseBlocks::Remove() atomically repoints it to the spill stub
  (flat-array reads + PatchAtomic; the signal-safety contract holds).
  Without this a cleared self-loop would run stale code until the next
  event.
- Excluded: manual/SMC-checked blocks (entry check must run per
  iteration), wait-loop-FF blocks, and JAL/JR/AL-link tails.

Motivation: the S0 prize map shows 41% of EErec cycles in loop-shaped
blocks (16.6% self-loops) and the tail-only boundary floor already
exceeds the whole remaining EErec gap vs the reference.

Gates: recompiler_tests 1320/1320 (9 new ee_rec_loop_residency_tests
incl. event-mid-loop, const rematerialization, 128-bit MMI carry, SMC
recompile, back-edge repoint); 30k-seed sharded EE fuzz soak clean; UYA
frames-2 + SotC frames-20 --stepdiff signatures identical to EP-4
baselines (JIT block entries -9.4%/-4.4% = iterations riding the
resident back-edge); M2 static census +0.32%, confined to the ~209 UYA
self-loop blocks (preheader + cold stub replace the 12-insn tail; the
win is dynamic).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 07:34:29 -07:00