Records the GIF command stream from a --liverun into a .gs dump, so a GS
workload can be replayed in pcsx2-gsrunner with no emulator in front of it.
This is the only honest way to A/B the GS across two builds. Measuring the
GS inside a live run does not work: both the MTGS ring and the software
rasterizer's job queue spin while waiting for work, so a build with a faster
EE shifts the GS threads' instruction counts without changing a single pixel
of GS work. Measured on Xenosaga with byte-identical GS workloads (same PRIM,
DRW and Mpps), a live run reported the GS threads executing 23% more
instructions while burning 21% fewer cycles -- the signature of spin, not
rasterization. Replaying the same captured dump standalone put the same build
11% ahead on instructions.
Recording arms after --gsdump-at frames (default 30, so the scene has settled)
and stops on its own once the requested frame count has been recorded; the
frame budget is raised automatically if it would truncate the dump.
EE JIT divergence hunts start at "this game miscomputes something under the
EE recompiler" with no idea which emitter is at fault, and the existing
funnel (stepdiff/contmem) localizes to a frame and a RAM region — which
chaotic float amplification makes almost unreadable by end of frame.
Add a harness-only switch that routes whole EE opcode groups through
recCall(interpret) instead of their native emitter, everything else still
JIT. The interpreters are the known-good baseline, so the group that makes
the symptom disappear contains the bug, and each probe is one run instead of
a rebuild per hypothesis. Groups: fpu, cop2, mmi, multdiv, shift, arith,
loadstore, move, cop0, branch, plus all/none. cop2 narrows into cop2move /
cop2vu / cop2ls and then into qmfc2 / cfc2 / qmtc2 / ctc2, and those four
accept a `:<reg>` destination filter (`ctc2:0`) since their fs field names
32 registers with wildly different semantics.
Gated on PCSX2_RECOMPILER_TESTS so it never reaches a shipping build; the
non-hooks path compiles to `constexpr bool forcedInterp = false`. A forced
fallback also feeds delaySlotNeedsBranchBracket, matching the conservatism
already applied to genuine !opcode.recompile fallbacks, so the switch cannot
itself perturb delay-slot exception semantics.
Pairs with `--mkstate --renderer sw`, whose savestate carries a rendered
screenshot, to give a headless visual oracle for graphical bugs. Together
these took the Xenosaga zoom bug from "somewhere in the EE JIT" to the exact
instruction (CTC2 to vi00) in about ten runs.
The self-loop phase-skip only consulted BlockBackwardBranchTarget on
ar.pc (the fall-through), so a self-loop whose divergence is observed
at its exit was never skipped and stopped the walk as a false positive
(this is what pinned Carbon's 0x174c30 byte-fill loop). Also check
ar.prev_pc, the offending self-loop itself.
The zoom report now dumps both streams' GPRs and FPRs at the offending
block head, so identical-entry false positives are visible directly in
the report instead of needing a hand-built harness.
The iOS app renders its own SwiftUI UI, so the shared core's ImGui
FullscreenUI overlay never appears on screen. Before this change every
RetroAchievements event still initialized FullscreenUI and posted the
notification through it, adding per-frame render work for an overlay that
is invisible on iOS, and the native toast layer never saw the events at all.
Add two Host callbacks so a platform can take over notification rendering:
bool Host::HasNativeAchievementNotifications()
void Host::OnAchievementNotification(key, duration, title, message, badge_path)
When HasNativeAchievementNotifications() is true the shared core hands each
RA event (unlocks, mastery, leaderboard start/submit/scoreboard,
login, connect/disconnect, summary) to OnAchievementNotification and skips
ImGuiManager::InitializeFullscreenUI() entirely — in BeginLoadingScreen,
ClientLoadGameCallback, DisplayHardcoreDeferredMessage, and
SetHardcoreMode — so the invisible overlay and its render loop stay down.
The existing ImGui path is unchanged for desktop/Android, which return
false from the new callback. Every frontend (eerunner, gsrunner, libretro,
sdl, qt, android, test stub, macOS stubs) gets a no-op implementation; iOS
provides the real one, posting the notification to its SwiftUI toast layer
through ARMSX2_PostRetroAchievementsNotification. The notification now also
carries the configured display duration.
Also flesh out Achievements::GetCurrentUserStats / GetCurrentGameStats /
GetCurrentAchievementList, which were previously unimplemented stubs
returning false. The iOS bridge already wired these up to the
RetroAchievements panel; they now return the logged-in user's score, the
active game's unlock progress, and a bucket-ordered achievement list so the
native panel has real data instead of an empty state.
Stray RetroAchievements debug fprintf spam in the iOS bridge and overlay
defaults is dropped.
Rewording only, no behaviour change. Four of these are contrast sentences that
a blind search-and-replace would have inverted into nonsense -- e.g. microVU's
"Unlike stock x86/ARMSX2 ... yaps2 inlined it here", where the ARMSX2 being
contrasted against is the pre-transplant line. Those now say "this backend" or
"the pre-transplant line" explicitly.
REFACTOR_STATUS.md keeps its two mentions: it is a dated changelog, and
rewriting it would falsify history.
Copyright headers are untouched.
recompiler_tests 1407/1407.
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>
--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>
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>
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.
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>
Two liverun harness fixes surfaced by the mq65 rig bring-up:
1. --liverun silently promoted --renderer null to VK unless --perf-jitdump
was also set, so every codegen_ab "renderer=null" A/B to date actually
ran Vulkan. Track whether --renderer was passed explicitly and honor an
explicit null (pairs with the new deviceless GSDeviceNone). A defaulted
Null still promotes to VK for the hang-repro use case.
2. @THREADPERF@: per-tid perf_event_open(instructions,cycles) attached
after savestate load and reported at shutdown next to @THREADCPU@.
Gives codegen A/Bs an EE-thread-SCOPED deterministic hardware metric:
whole-process perf stat dilutes an EE-only delta by the GS/MTVU/worker
threads' share (~50% on SotC), and the time-based @THREADCPU@ line is
clock-sensitive. Falls back to user-only counting when
perf_event_paranoid blocks kernel-inclusive counts (unprivileged dev
boxes); devices run as root and count both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The g_cop2ReadHook/g_cop2StateHook externs only exist when
ENABLE_RECOMPILER_TEST_HOOKS=ON, but Main.cpp referenced them
unconditionally — production configs (build-rocknix, hooks OFF) failed
to link eerunner since the vu0diff feature landed. Gate the references
and fail --vu0diff loudly on hooks-OFF builds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wallclock A/B on the SD865 has a ±0.3s noise floor over a 600-frame run —
useless for sub-ms/frame codegen deltas. utime+stime per VM thread (parsed
from /proc/self/task/*/stat while the threads are still alive) isolates e.g.
GS-thread cost from GPU/sync/scheduler jitter; it resolved a 1% GS-thread
delta that wallclock could not.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three harness-only additions used to root-cause the OutRun 2006 readback
serialization on SD865 (follow the existing EERUNNER_* env-gate pattern):
- EERUNNER_HWDL=<0..4> forces EmuCore/GS HWDownloadMode. Unsynchronized (3)
keeps CPU-side copy work but skips the fence wait, isolating the stall
cost of synchronous GPU readbacks. 1-4 are not render-accurate; A/B only.
- EERUNNER_SPINCPU=1 sets HWSpinCPUForReadbacks (spin instead of sleeping
on readback fences, cutting scheduler wake latency).
- --liverun now prints an @GSSTAT@ line at completion (per-frame draws,
render passes, readbacks, texture copies/uploads from g_perfmon) so
scripted device runs can attribute GS-side behavior without a profiler.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 0 of the `neither` cherry-pick funnel: a repeatable, attributable,
wallclock-anchored CPU bottleneck baseline for our own ARM64 port, since the
RK3562-era numbers are stale and the target has shifted to Snapdragon 865.
Built and validated on M2 Max / Asahi first; the same scripts run on SD865 with
a new devices/<label>.env and no code change.
Test-harness change (no production code path, no shipped env gate):
- pcsx2-eerunner --perf-jitdump: emit a Linux perf jitdump under EmuFolders::Cache
so `perf inject --jit` resolves EE_/VU0_/VU1_/IOP_/VIF_ block symbols. The enable
is driven through the EmuCore/Profiler EnablePerfDump config bool (ApplySettings ->
LoadSettings re-applies Perf::SetJitDumpEnabled every apply, so a manual enable
would be reset). SetJitDumpDir is set before the first block compiles. Also honors
an explicit `--renderer null` under --perf-jitdump for the CPU-only diagnostic.
- pcsx2-vurunner --perf-jitdump: same flag for VU-only captures (manual enable; the
runner doesn't go through ApplySettings).
Tooling (pure stdlib + bash):
- tools/perf/bucket_perf.py: parse a `perf report --stdio` dump into a PCSX2
subsystem ranking. Buckets tuned against real M2 R&C UYA / Katamari captures:
JIT-by-prefix (EE/VU0/VU1/IOP/VIF) + VU-glue (mVUlookupProg / dispatch envelope) +
native VIF + GS (incl. GIF decode + XXH3) + a GPU-driver bucket that quarantines
the host Asahi/Vulkan/DRM stack (host-specific, NOT an SD865 proxy) + JIT-other
(unsymbolized continuation blocks) + startup/io + a visible `unattributed`.
- tools/perf/profile_run.sh: one-command wrapper (precondition gate -> perf record
-> inject --jit -> single report dump -> bucket -> median wallclock + median
per-bucket share -> summary.md). Device/scene parameterized.
- tools/perf/devices/m2max-asahi.env: the working M2 incantation (P-core PMU
apple_avalanche_pmu/cycles/, -F 999).
- tools/perf/scenes/*.env: R&C UYA + Katamari cinematic/gameplay scenes (assets are
copyrighted, not checked in; paths reflect this dev box).
Validated on M2: go/no-go #1 (perf+jitdump resolves 277 JIT symbols) and go/no-go #2
(per-bucket shares stable across 3 runs) both green. First finding: on M2 the `vk`
profile is dominated by the Asahi GPU stack (GS thread ~57% of samples) — the CPU
shape comes from `--renderer null`, where VU (bodies+glue) is the largest emulation
cost.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>