2 Commits
Author SHA1 Message Date
l fce905de94 arm64: the COP2 flag hack needs a horizon, not adjacency
The arm64 run analysis decided whether to keep every status write in
a macro run live by peeking exactly one word before the run start and
one after the run end, looking for CTC2-to-Status or CFC2-from-Status.
x86's COP2FlagHackPass instead tracks a horizon: a CTC2 opens it, the
matching CFC2 closes it, and every status write in between stays live
even when the macro ops are split into several runs by a store, a NOP
or an interp-pinned VU op. Adjacency misses exactly those cases, and
the sticky bits the CFC2 reads never accumulate.

The bracket now scans, bounded at 64 instructions each way, staying
inside mapped RAM because a compile-time vtlb miss is not an option.
It stops backwards at an earlier CFC2 that already closed the horizon
and forwards at a later CTC2 that reopens it, which is what x86's
single cfc2_pc cursor expresses.

This is the second half of the pair the differential test was written
to find. The x86 side had a clobbered cpuRegs.code in the same
analysis (fixed earlier); this is the arm64 side, same family --
a cheap local test standing in for a range.

Verification. The differential test in tests/analysis is the gate
here, not the soak: it compares the two implementations
instruction-for-instruction over 218k comparisons, and its harness is
regenerated from the patched source rather than hand-edited.

                                          before    after
    arm64 conservative (safe)             10,759   10,759
    arm64 superset (safe)                 31,972   43,373
    x86 marks a flag arm64 does not        1,613        0

The class that can lose sticky VU0 status is gone; the class that
grew is the safe direction, arm64 keeping more status writes live
than strictly needed. Identical figures whether the harness is built
for x86_64 or cross-built and run under qemu-aarch64.

Runtime: the aarch64 core cross-builds and runs Tekken for 200 frames
under qemu with a SIGSEGV forensics handler attached and no faults,
and the 2048 homebrew for 600 frames. x86_64 is untouched by
construction and confirmed so -- gates 36/36, byte suites green
(95,692 + 112,118 + 27,337, zero divergent), Tekken clean at 3000
real-BIOS frames, all four JIT region hashes identical.

Caveat, because the soak numbers invite more confidence than they
earn: qemu runs Tekken at about 1 fps here, so 200 frames is a
shallow test in wall-clock terms, and a flag-accuracy bug can hide
behind a correct-looking picture regardless. The differential test is
what actually covers this change. Hardware validation is still the
honest requirement before calling arm64 flag handling correct.
2026-08-20 16:24:44 +02:00
l 8cd99be227 tests: differential test for the COP2 flag-hack analysis, x86 vs arm64
The two recompilers decide independently which VU0-macro instructions
must compute and commit MAC / status / clip flags: x86 runs
COP2FlagHackPass over a whole block, arm64 analyses a RUN of
consecutive native macro ALU ops as it emits. Nothing tested that
they agree, and the cpuRegs.code clobber fixed in the previous commit
showed what a divergence looks like when nobody is comparing.

The harness compiles BOTH implementations verbatim out of the
recompiler sources -- cop2flags and COP2FlagHackPass_Run from the x86
tree, Cop2AnalyzeRun and the mode-0 classifier from the arm64 tree,
extracted rather than retyped, with the emitter function pointers
reduced to a boolean -- backs them with a synthetic instruction
memory, and compares the five flag-hack bits instruction for
instruction over generated blocks: pure macro runs of every length,
runs bracketed by the Tekken CTC2/CFC2 pattern, and mixed blocks
where runs are separated by stores, VCALLMS, VCLIP and transfers.
218k instruction comparisons.

Three classes of difference come out, and the harness classifies them
rather than just counting:

  * arm64 conservative (10,759): a run preceded by CTC2-to-Status with
    no following CFC2. arm64 keeps every status write live, x86 does
    not. Extra work, never less. Safe.
  * arm64 superset (31,972): arm64's denormalize/normalize bracket
    spans a run, x86's spans a block, so arm64 re-brackets at every
    run boundary. Also extra work only. Safe.
  * x86 marks a flag arm64 does not (1,613): the direction that can
    lose sticky VU0 status. This is the harness's failure condition.

Delta-debugging the corpus produces a six-instruction reproduction,
included as known_case():

    CTC2 Status ; NOP ; MUL ; RSQRT ; CTC2 Status ; CFC2 Status

x86 sets STATUS on the MUL because its cfc2_pc horizon runs from the
CTC2 to the matching CFC2 and the MUL is inside it. arm64 checks only
the instruction immediately before the run and immediately after it;
here a NOP and a second CTC2 sit in those slots, the bracket test
fails, and the MUL's status contribution is skipped -- so the CFC2
reads sticky bits that never accumulated. It is the same shape of bug
as the x86 one just fixed: an adjacency test standing in for a
horizon.

I am not patching arm64 on that basis. This container cannot build or
run the arm64 recompiler, the fix is a design choice (widen the
bracket to a bounded backward/forward scan for CTC2/CFC2, matching
x86's horizon) rather than a typo, and shipping an untested change to
a recompiler I cannot execute is exactly the move this project does
not make. The test documents the gap, fails on it, and is ready to
verify a fix when someone can run one.
2026-08-20 09:28:06 +02:00