71 commits from 474ad59818 to 2cf8dabe6b, triaged rather than taken wholesale.
Declined, resolved to ours:
- AGENTS.md: upstream's AI-agent instructions; we carry our own and do not
want a second, conflicting policy file.
- CI deps bump (setup-node, labeler): both target workflows are absent here,
and the labeler job is gated on the repository being PCSX2/pcsx2.
- KDDockWidgets 2.4.1: two of the six files do not exist here; we already
build 2.4.0 against a 2.3.0 floor, so there is nothing to gain.
- The FullscreenUI Achievements-layout realignment: our section already
carries the same settings, and ours is the branded copy.
- The GS draw/vertex-buffer cluster (7887919e74, b2fa00844e, 99cfbb49c1,
5c611f85e1, 9945046a49, af48193ebb, d88510e3a6, 8c1bb5742e). Our vertex
kick is an ARM64 rewrite of the same hot path -- register-resident cursor,
fused min/max with a rewind watermark, and a scalar cull mirror that
dual-issues against the NEON parse -- so upstream's generic pointer-logic
optimisation is a variant of work already banked here, and their growth
restructure replaces per-buffer capacity with a single global value, which
the pooled draw-node model cannot express. Two of the four August commits
in that cluster repair regressions the July rewrite introduced, and the
third's genuine fix (staging arrays sized from an unrelated buffer) we had
already made independently.
Taken with adjustment:
- EATAN coefficients (aae9438f98). Upstream relabelled mVU_Globals so the
names match the powers; we had fixed the same defect by ordering the arm64
call sites by power instead. Both fixes are correct alone and CANCEL when
combined, so the arm64 call sites move to plain ascending order in the same
commit. The values never moved, so this emits an identical instruction
sequence. Their fix also repairs the x86 mVU we still carry.
- Shader cache version: upstream numbered their tfx.glsl change 109, which is
below our 110. Taking their value would hand every user a stale blob, so
this lands as 111.
- FullscreenUI: took the two readback-spin toggles, placed outside our
non-Apple guard rather than inside upstream's unguarded run.
- Restored tools/generate_fullscreen_ui_translation_strings.py, dropped by
431ca0c063, and regenerated both string areas. That also registers the Big
Picture setup-wizard strings, which had never been extractable.
GameDB: the three serials upstream gave gsHWFixes (SLES-53869, PAPX-90020,
SCPS-15064) are absent from the mobile overlay, so no fix is silently erased
on handhelds.
The loadstate/savestate positional was named "slot", which is also the
global option selecting the PINE socket. argparse shares one namespace,
so the positional overwrote it and `gsctl.py loadstate 1` dialled
pcsx2.sock.1 instead of the emulator's socket, failing to connect.
Give the positional its own dest and keep "slot" as the metavar, so the
command line is unchanged.
A settings query answered the wrong question. GameDB hardware fixes are applied
to the live config after the settings load and are never written back to the
file, so on any game carrying them the persisted value and the running value
disagree — and the query only ever knew about the first. On Rogue Galaxy it
reported autoflush off and preload off while the renderer was running autoflush
at 2 and preload on. That cost real time during the Rogue Galaxy work.
The confusion is the smaller half. The real damage is to measurement: a settings
A/B that writes a key to some value measures the GameDB value in BOTH arms,
because GameDB re-applies it after every settings load, while the two arms
report two different settings. That is a wrong answer with no symptom, on
exactly the titles worth investigating.
So add an opcode that reports both values side by side. Effective values come
from serialising the live config back out through the same wrapper that writes
the INI, which means they land under the identical section/key names a caller
already uses and every setting is covered without a key map — a hand-written map
would need extending by every future setting, and the one that got missed would
be the one somebody trusted. It also fixes a smaller lie: keys absent from the
INI came back as empty strings, reading as "unset" rather than as their default.
The reply says the two strings differ; it does not say why, because from here
that is not knowable. A GameDB fix, safe-mode masking and a settings layer this
query does not read are indistinguishable at the point of comparison, and naming
one of them would be inventing the reason.
The existing read is left alone, so anything speaking the old opcode keeps
working. gsctl's `get` now reports the running value, prints the discrepancy to
stderr where a human cannot miss it and a pipeline does not have to care, and
keeps the on-disk value available behind --persisted.
USE_COVERAGE instruments the build with clang's -fprofile-instr-generate
-fcoverage-mapping, exposed as the clang-coverage preset (build-coverage/,
inheriting clang-devel so the dev asserts stay on, Qt off since nothing in
the test path needs it).
The instrumentation is tree-wide rather than scoped to pcsx2/arm64: much of
the JIT is inline code living in headers that get pulled into core and common
translation units, so narrowing at build time would drop counters for exactly
the code we care about. tools/coverage.sh narrows at report time instead,
where the filter is exact.
The script builds the five gtest binaries, runs them with per-process profile
files, merges, and reports scoped to pcsx2/arm64/ (--scope all widens to the
other ARM64-only sources). Two hazards it defends against:
- cmake --preset takes the source dir from the working directory and ignores
-S, so running this through the /home/bmd/ARMSX2 symlink bakes the
symlinked path into every coverage mapping. It cds first.
- llvm-cov does not error when --sources matches nothing; it reports every
file it has data for, which reads as a plausible whole-tree number. The
filter prefix is read back from CMAKE_HOME_DIRECTORY so it always matches
what the compiler recorded, and a row-count tripwire fails the run if the
report escapes its scope anyway.
Baseline for pcsx2/arm64/: 74.65% lines, 79.94% functions, 77.70% regions.
Debugging a GS performance problem meant restarting the emulator and reloading a
savestate for every "did you try setting X?", and the only way to read the
statistics that drive that decision was to look at the OSD. PINE already
provides a unix socket, a thread, framing, a config key and the RunOnCPUThread
marshalling pattern, but its opcodes stop at guest-RAM peek/poke plus savestates
and game identity -- no host statistics, no settings.
Adds four ARMSX2-local opcodes at 0x10+ (upstream PINE ends at 0xF, so a generic
client will never send them):
MsgGetStats PerformanceMetrics, all GSPerfMon counters and the texture
cache memory figures, as JSON.
MsgGetSetting read a setting by section/key.
MsgSetSetting write a setting, apply it, and report whether the key forces a
GS device reopen.
MsgFrameAdvance step a paused VM.
MsgSetSetting writes the persisted key rather than poking EmuConfig directly,
because a direct poke is silently reverted by the next ApplySettings, which
re-derives EmuConfig from the INI layer stack. The restart_required answer comes
from a new GSOptions::IsRestartOption, sitting next to RestartOptionsAreEqual so
the two lists stay in sync.
Statistics are gathered on the PINE thread. PerformanceMetrics and g_perfmon are
benign scalar reads, but GSgetMemoryStats dereferences g_texture_cache and
g_gs_device, which are GS-thread owned, so that one is marshalled through
RunOnGSThread.
tools/gsctl.py is a stdlib-only client emitting JSON on stdout.
Verified against a headless gsrunner replay: toggling accurate_blending_unit
between 0 and 5 over the socket moves barriers 1.0 <-> 91.5 and draw calls
55.5 <-> 101.5, repeatably, with no restart.
tools/perf is the yaps2 profiling bench: harness scripts plus per-device
profiles for a specific set of handhelds, scene configs pointing at a
private game library, and dated campaign write-ups that carry LAN
addresses. It rode in with the JIT transplant merge; none of it is
buildable content and none of it is usable as-is by anyone else.
It now lives in the umbrella repo at devs/bmdhacks/perf/, kept intact so
codegen_ab.sh and profile_run.sh still resolve devices/ and scenes/
relative to their own directory.
A few source comments still cite tools/perf/<file> as the provenance of a
measurement. Those paths remain valid in the yaps2 tree the comments came
from, so they are left alone rather than churned into merge conflicts.
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>
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>
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.
- devices/mq65.env: MANGMI Air X (Kryo 260: cpu4-7 Gold/A73 part 0x800,
cpu0-3 Silver/A53 part 0x801 — note 0x800 is the BIG cluster), 4K pages,
ARMv8.0 no-LSE, no fan; the primary density gate for EE codegen work.
- codegen_ab.sh: parse @THREADPERF@ "CPU Thread" instructions/cycles as the
PRIMARY metric (EE-scoped, undiluted by GS/MTVU, excludes boot; max across
same-named threads). Whole-process rows kept as secondary. New fast-fail:
a missing @THREADCPU@ line means the VM never booted (GS init failure) —
startup alone can retire billions of insns and sail past the 1e8 floor,
which is exactly what the first mq65 run did.
- scenes: fall back .iso -> .chd (mq65 carries CHD rips).
mq65 self-vs-self sanity (uya-gameplay, 600 frames, runs=2): EE-thread
insns spread 0.008%, delta -0.01% insns / -0.06% cycles — noise floor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both closed-campaign A/B compile toggles have shipped default-ON and
their measure phases are done (EE-SRA 3 lazy-dirty, vindicated on
RK3562 + SD865; P2-2 call-ret ring). Hardcode both to the ON path and
remove the =0 legacy branches, the toggle #defines, and the test
self-default scaffolding:
- EE_PIN_LAZY_DIRTY: drop the write-through guards; lazy-dirty is now
the unconditional pin-write policy
- EE_CALLRET_STACK: drop the "#if !..." Jump path and the guard
wrappers around the ring push/pop emission and reset
Also delete tools/perf/callret_ab.sh + callret_ab_report.py, whose
-DEE_CALLRET_STACK=0 A/B baseline can no longer be built.
Behavior is identical (both already defaulted to 1); recoverable from
history if the write-through / pre-ring baseline is ever needed again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Env-var gates are testing-only and the A/B is done. The off-arm never
needed an in-tree gate: prctl(PR_SET_THP_DISABLE) survives execve, so
fx15_thp_ab.sh now disables THP from outside the process (both arms exec
through a symmetric python3 wrapper). Verified on-device: zero arena
hugepages under the wrapper vs 8MB without.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ITLB_WALK -15% to -35% and L1I_TLB_REFILL -4% to -59% across 4 titles,
but cycles/wall flat (A77 baseline iTLB pressure already negligible;
1280-entry L2 TLB covers the live JIT footprint in 4K pages). THP
madvise stays default-ON: zero cost, no regression, in-order A53 upside.
Adds the reusable A/B harness (env-toggle arms + in-run smaps THP probe).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five-title census complete. FX-16 killed on collapsed per-site saving
(pins ride through tails, short blocks skip the event check, dynamic
allocator near-empty at tails => 0.05-0.3% EE-thread ceiling vs high
emitter complexity). FX-07 killed on five real-workload eviction nils.
FX-03 closed applied via FX-03a. FX-15 iTLB A/B is the remaining item.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three compile-gated telemetry channels for the FEX-mine ceiling
measurements (annotations/_fex/triage), all OFF by default and absent
from default builds (verified: no symbols in gates-off libpcsx2.a,
1149/1149 recompiler_tests both ways, corpus A/B byte-identical
gates-on vs gates-off on all 19 game dirs):
- EE_SMC_TELEM: emitted Ldp/Add/Add/Stp bump at every ProtMode_Manual
check head, split real-SMC vs contains_thread_stack-forced pages;
5s words/s rate line via recEventTest. Emits real instructions —
keep OFF in A/B timing binaries (EE_CALLRET_TELEM precedent).
- EE_BRSHAPE_CENSUS: pure compile-time classification of block-ending
forward conditional skips in the recRecompile scanner (distance
buckets, likely/link variants, skipped-region fusion safety,
diamond tails, 4K crossings) — the FX-16 fusable share.
- JIT_ALLOC_CENSUS: live-value eviction counters at _getFreeArm64GPR
(ee/iop/other + dirty), _getFreeArm64NEON (dead vs live tiers), and
mVU findFreeNeon/findFreeGPR (VF/VI, dirty); 30s tick + exit-time
fallback reporter for vurunner/tests.
Session protocol + first SotC smoke numbers: tools/perf/fx-telem-2026-07-11.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 games x 2 arms x 4 ABBA rounds x 3000 frames on the A77 cluster:
cycles SotC -0.95% / Katamari -0.65% / OutRun -0.42% / UYA flat; IPC up
and BrMis/kinsn down in all four; no regression. Mechanism validated at
98.6% ring hit rate. RK3562 wallclock still owed before calling it
shipped (default-ON on main).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/perf/callret_ab.sh: savestate-anchored pcsx2-eerunner --liverun
--renderer null A/B (two binaries from the same tip, -DEE_CALLRET_STACK=0
vs default), ABBA-interleaved rounds, perf stat around every run
(cycles,instructions,r21,r22,r78,r79 — no multiplexing on A77), fan pin
with auto-restore trap + performance governors when a pwmfan hwmon exists,
EERUNNER_SYNCMTGS=0 per run. Decision metric: PerfLog wall s/kframe;
mechanism metric: r22/kinsn. callret_ab_report.py: per-game paired-round
medians + deltas; tolerates Apple-PMU event naming for M2 shakeouts.
Shaken out on the M2 rig (UYA+SotC, byte-stable 4.50/7.00 s/kframe both
arms — M2 is a wash as expected; the A77 is the test). brmis-2026-07.md
records the user's build-anyway decision, the landed commits, the A/B
protocol, and the pre-existing stepdiff divergence note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read FEX's call-ret shadow stack + per-site inline L1 lookup (BranchOps.cpp,
Dispatcher.cpp, ThreadManager.cpp; clone at /home/bmd/pcsx2/FEX, untracked).
Production MAMBO-X64 shape: adr+stp{guestRA,landing}+bl at calls, ldp+cbz+ret
at returns with EVERY exit path ending in ret to keep the hardware RAS
balanced (corrects the miss-path B in our dismissed Step-1 sketch), balanced
xzr pushes for unknown continuations, guard-page+SIGSEGV recentering instead
of bounds checks, DONTNEED wipe on invalidation, and no shared dispatcher
mega-Br at all -- per-site flat-L1 probe + per-site br/blr. Blueprint if
P2-2 reopens; gate arithmetic unchanged (call-sparse EE workload).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bmd challenged the Step-0 dismissal. Recheck (brmis_recheck.py) found the
entry8 window (2 insns) is far narrower than the real r22 skid (regularly
>64 insns; 76% of EE-block samples land on non-branch IPs, 0.86% on b.cond)
so the sub-block attribution was interpretation, not measurement. Corrected
skid-honest funnel bounds: UYA <=21.5% of mispredicts / 0.79% of EE-thread
cycles, SotC <=28.0% / 1.29% (absurd ceiling 36%/1.67%). G1 still fails --
UYA at every bound, SotC at the honest bound -- so the RSB stays dismissed,
but the margin is honest now. Native ~54% confirmed genuine by function-level
symbolization against the deployed binary: spu2Mix, vifTransferLoop<1>,
outline-atomics thunks (MTVU/MTGS rings), Gif_Unit::Execute,
_cpuEventTest_Shared; plus recClearIOP + mVUlookupProg_VU0 top-10 in UYA.
Raw capture data preserved at scratchpad/brmis-2026-07-10/ (untracked).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Real-gameplay r22 attribution on SD865 (UYA 558k + SotC 981k samples):
~54% of EE-thread mispredicts are in native C code, ~24-29% guest
conditionals; the guest-indirect dispatcher funnel is 6-13% of mispredicts
= 0.23-0.58% of EE-thread cycles. The MAMBO-X64-style RAS-paired return
cache is dismissed at the gate (needed >=35% share and >=1.5% cycles).
MAMBO-X64's SPEC result doesn't transfer: PS2 EE code is call-sparse
(~12-16 returns/kinsn) and the emulator's own native services eat half
the mispredict pool on the same thread.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Buckets mispredict-event sample IPs (perf record -e r22/r10) into native /
per-region (EE+IOP dispatchers) / JIT blocks by instruction class at the IP,
with block-entry reported separately (target-side skid of incoming indirect
edges). DispatcherReg is pinned by code signature (Ldr w0,[RSTATE,#pc] .. Br
x17) — B-target clustering is printed as a diagnostic only, its hottest
non-event cluster is JITCompile via unlinked link placeholders. EE blocks
are split jr-exit (tail B->DispatcherReg) vs linked as a second cut on the
guest-indirect funnel share.
Step 0b of the P2-2 branch-mispredict campaign (MAMBO-X64-style RAS-paired
return cache); gates G1 per plan.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ee_code_census.py --coldbytes: per-block byte-layout census (smc_preamble
via exact structural match of the ProtMode_Manual inline compare, validated
by disassembly; tail_junk after the last unconditional branch; align_pad
gaps) + sample-weighted executed shares + hot-working-set line utilization,
pooling multiple perf.data files. Jitdump overlap dedupe (full resets rewind
the bump allocator; keep the later record).
Session-2 real-gameplay numbers (UYA, 5x10s 20kHz EE-thread bursts):
- SMC compares: 10.26% of STATIC EE bytes (620KB / 2627 blocks) but only
0.42% of executed samples and 0.41% of hot-set bytes -> L1 stub+table
redesign dismissed at the >=1%-samples/>=3%-bytes gate.
- tail_junk 0.35% + pad 3.8% of hot-line bytes -> L2/L3 dismissed.
- Line utilization of the 90%-coverage set: 0.915-0.927 -> cross-block
hot/cold packing PERMANENTLY CLOSED. Compile-order bump allocation is
already ~92% packed; the entire layout class can recover ~11KB of a
130KB working set. Only emitted-code density still shrinks it.
pstef adjudication complete: mechanism real (working set >> L1I), remedy
(hot/cold arenas) prizeless here -- out-of-line slowpaths + compile-order
emission get the layout for free.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>