docs: add host optimization and upstream audit notes

This commit is contained in:
Matthew Stanley
2026-08-23 13:09:19 -07:00
parent 13a56626d3
commit 7a872bc018
2 changed files with 465 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
# Host optimization contract
This document is the acceptance contract for changes intended to make
psxrecomp run faster on the host. It applies to generated code, the runtime,
the flat CPS trampoline, overlay dispatch and validation, graphics and audio
support code, and host-side caches or fast paths.
Correctness fixes and deliberate guest-visible enhancements still need focused
verification, but the 5% performance retention gate below applies specifically
to optimizations.
## Non-negotiable rules
1. Measure on a quiet host with zero compiler processes. Finish all AOT,
overlay, shader, and helper compilation before the timed region. Confirm
that no compiler, linker, build driver, or autocompile worker is alive while
sampling. A run in which one appears is contaminated and is discarded, not
explained away.
2. Interleave A and B. Never measure all baseline runs and then all candidate
runs. The minimum acceptable order for three pairs is `A1 B1 A2 B2 A3 B3`;
reversing the first member of alternate campaigns is encouraged. Keep the
same binary and use a forced runtime selector when practical.
3. Report every raw run, the min-of-N for A and B, the percentage change
between those minima, both medians, and the percentage change between the
medians. N is at least three. State whether lower or higher is better.
4. Discard a pair when either member is contaminated. Reasons include a
compiler process, overlay compilation, shader-cache construction, a changed
power or thermal state, unexpected foreground load, a different overlay
population, or a different pacing owner. Record the discarded pair and its
reason in the experiment ledger.
5. A candidate that adds complexity, persistent state, generated-code ABI,
configuration or debug ABI, or another maintained fast path is retained
only when the agreed primary metric improves by at least 5%. A pure
simplification may be retained at flat cost, but its measured numbers must
still be recorded. Correctness fixes are judged on correctness, not this
speed threshold; do not disguise an optimization as a correctness fix.
6. With the optimization forced on for every eligible operation, guest-visible
state must be byte-identical to the faithful path at deterministic
checkpoints. An automatic fallback is not proof: it can hide the exact case
the fast path mishandles.
7. The faithful reference computation remains linked and selectable. A fast
path is an implementation of that same computation, not a replacement model
that merely looks plausible for one title.
8. Do not copy another console project's opcode handlers. It is valid to reuse
its experimental process, evidence format, test structure, or review
checklist. Implement R3000A behavior from PlayStation evidence and
psxrecomp's own faithful path.
## Preparing comparable psxrecomp runs
Prefer one executable containing both paths and an explicit selector with
three states: faithful, candidate forced on, and normal policy. Record the
selector and assert in telemetry that the requested state took effect. If two
binaries are unavoidable, build both before the campaign and record their
commits, toolchains, options, and artifact hashes.
Static recompilation makes build quietness part of the benchmark definition.
Pre-generate the game and BIOS images, warm every overlay needed by the
workload, and prevent runtime autocompile during the measured interval. Record
the static image identity and the exact overlay inventory. A benchmark is not
comparable if A interprets an overlay while B uses a compiled shard.
Content-verified overlay variants require an additional check. A and B must
use the same captured bytes, identity ranges, flavor, cache generation, and
selected variant at each checkpoint. Pin and report the overlay flavor. A fast
identity check may avoid a CRC or range scan, but the faithful verifier must
remain callable and both must select the same content-verified variant.
The flat CPS trampoline is part of observable execution semantics. Compare the
published continuation PC, dispatch result, exception redirect, dispatch depth,
and same-site resume consumption at block and function boundaries. A host
function returning with `cpu->pc == 0` is not equivalent to tail-publishing the
next guest PC, even if common images never cross that boundary.
Use a deterministic workload with fixed inputs and a fixed capture or replay
when possible. Keep renderer, audio backend, window state, display refresh,
vsync/pacer ownership, CPU overclock, CRTC multiplier, mods, RAM size, and
instrumentation identical. Warm caches outside the timed region unless cold
behavior is the stated metric.
## Correctness proof
The forced-candidate run must exercise the candidate, and its checkpoints must
match a run forced through the faithful reference. Hash or serialize all state
that can affect later guest execution, including:
- GPR, HI/LO, PC, COP0, GTE registers and completion timestamps;
- main RAM, scratchpad, VRAM, SPU RAM, and relevant device registers;
- the R3000A cycle counter, deferred cycle charges, event deadlines, IRQ state,
load-delay state, and pending exception continuation;
- dispatch and overlay identity state that changes which guest bytes execute;
- deterministic mod-owned guest state and savestate-visible sections.
Host-only counters may be excluded only when they cannot influence selection,
timing, invalidation, or later guest state. Document every exclusion.
Compare at more than the final frame. Include checkpoints before the candidate
first fires, immediately after it fires, at overlay or exception boundaries it
touches, and after a long soak. For caches and memos, force hits, misses,
invalidation, wrap or saturation, and savestate/rewind restore. For generated
code, cover the game emitter, BIOS full-function emitter, compiled overlays,
static overlay bakes, and the dirty interpreter wherever the contract crosses
execution tiers.
For cycle-affecting work, use `tools/cycle_testrom/measure.py` as the existing
measurement seam. Run the same test-ROM anchors against the native compiled
path, `PSX_FORCE_INTERP=1`, and the Beetle oracle as applicable. The script's
per-iteration deltas establish guest-cycle correctness; they do not replace a
separate interleaved wall-time or throughput campaign for host performance.
The faithful per-instruction R3000A cycle model remains authoritative: an
optimization may reduce host work but may not remove base charges, wait states,
interlocks, GTE completion timing, I-cache behavior, or peripheral deadlines.
## Performance campaign and evidence
Choose the primary metric before running the campaign. Frame or workload time
is preferred when the work is bounded; throughput is acceptable for a fixed
steady-state loop. Report secondary metrics, but do not change the retention
decision after seeing the results.
The evidence block for every candidate contains:
```text
Candidate: <short name>
Commits/artifacts: A=<hash> B=<hash> selector=<setting>
Workload: <capture, interval, overlay inventory, backend and host>
Primary metric: <name and unit; lower/higher is better>
Run order: A1 B1 A2 B2 A3 B3
Raw A: <a1>, <a2>, <a3>
Raw B: <b1>, <b2>, <b3>
Min-of-3: A=<min-a> B=<min-b> change=<percent>
Medians: A=<median-a> B=<median-b> change=<percent>
Correctness: faithful=<hashes> forced-on=<hashes> result=<identical/not identical>
Contamination: <none, or discarded pairs and reasons>
Complexity/ABI: <none, or exact added surface>
Decision: ACCEPT/REJECT - <reason>
```
Worked example, using workload time in milliseconds where lower is better:
```text
Run order: A1 B1 A2 B2 A3 B3
Raw A: 12.000, 11.000, 13.000 ms
Raw B: 9.744, 9.493, 10.100 ms
Min-of-3: A=11.000 B=9.493; improves 13.7%
Medians: A=12.000 B=9.744; improves 18.8%
Correctness: faithful=7a... forced-on=7a...; byte-identical
Complexity/ABI: two per-variant words plus one generated helper call
Decision: ACCEPT - both summaries exceed the 5% retention gate
```
Do not report only a profiler share, a best screenshot, an FPS range, or a
sequential before/after observation. Those are useful for finding candidates,
not for accepting them.
## Experiment ledger
Every investigated candidate gets a ledger row, including rejected,
superseded, reverted, and flat candidates. The ledger is append-only evidence,
not a highlights page. Keep it beside the optimization or campaign notes and
include links to raw data when available.
Required fields are:
| Field | Required contents |
|---|---|
| ID and date | Stable candidate ID and campaign date |
| Change | Commit(s), artifact hashes, selector, and concise hypothesis |
| Scope | Workload, renderer/audio backend, host, toolchain, config, overlay inventory and flavor |
| Correctness | Faithful and forced-on checkpoint hashes; exact exclusions |
| Runs | Interleaved order, all raw accepted runs, and every discarded pair with reason |
| Summary | A/B min-of-N and medians with percentage changes |
| Cost | Added code, state, ABI, configuration, maintenance burden, or simplification |
| Verdict | Accepted or rejected, threshold used, and reason |
Accepted and rejected results are equally visible. A compact ledger index may
look like this, with each row linking to its full evidence block:
| ID | Min-of-N change | Median change | State result | Cost | Verdict |
|---|---:|---:|---|---|---|
| EX-A | improves 13.7% | improves 18.8% | byte-identical | two per-variant words and one helper call | ACCEPT - clears 5% |
| EX-R | improves 3.0% | improves 1.0% | byte-identical | new cache and invalidation path | REJECT - below 5% |
Use an explicit rejected entry even when the candidate was reverted before a
formal campaign. Record the numbers that motivated the experiment and state
which required evidence is missing. Do not silently delete failed ideas: the
ledger prevents the same attractive but invalid optimization from being tried
again without new evidence.
## Review checklist
- Host was quiet and had zero compiler processes during every retained run.
- A/B was interleaved; contaminated pairs were discarded and recorded.
- Raw runs, min-of-N, and medians are present.
- The forced candidate and faithful reference produced byte-identical
guest-visible state.
- The faithful path remains linked and selectable.
- Overlay identities and execution tiers were held constant and reported.
- Cycle-model changes were checked through the cycle-test-ROM seam.
- Added complexity or ABI clears 5%; flat pure simplifications have numbers.
- Accepted and rejected candidates are both in the experiment ledger.
- R3000A implementation was derived independently; no other console's opcode
handlers were copied.
+262
View File
@@ -0,0 +1,262 @@
# Upstream contribution audit — 2026-08-20
## Summary
This worktree is a cross-cutting psxrecomp framework hardening and host-performance pass discovered while bringing up WipEout 3 SE: it fixes overlay-DLL generation and byte-variant identity, makes cycle/IRQ/cache shortcuts preserve guest-observable timing, closes several PGXP/GTE provenance holes, extends HD-texture tracking to copied VRAM, separates presentation content policy from the configured outer canvas, and adds reusable profiling/test surfaces. The mechanisms are generally applicable to recompiled PS1 titles; the few title-specific observations and enablement conclusions are isolated below and should not be upstreamed as framework policy.
## Audit basis and limits
- The audited delta is the **current working tree against local `HEAD` `4f0b07b1491e` on `feat/ws-cull-widen`**, because that is the boundary that matches the reported session body: 58 tracked files (`3,068` insertions, `871` deletions) plus 16 meaningful untracked source/doc/test files = **74 meaningful paths**. Untracked `build-pgxp-focused/` and `build-texpack-focused/` are build artifacts and were excluded.
- `fork/feat/ws-cull-widen` is `e27eac73663f`, an ancestor 27 commits behind local `HEAD`. Diffing to that remote ref would mix those already-committed changes into this working-tree contribution and produce 81 meaningful paths instead of the reported approximately 76.
- Per instruction, no build, test, benchmark, or executable was run during this audit. “Covered” below means the test source exists and targets the invariant; it is **not** a fresh pass result.
- A number is called “measured” only when the audited files contain an actual observation/result. Loop counts and cache capacities are reported as designed coverage/structure, not as performance results. Where the delta has no result, the measurement is **UNKNOWN**.
## Corrections to the supplied headlines
1. **Overlay shim anchor: mechanism supported; manifestation counts unverified.** `patch_generated_c` now anchors after the unconditional `#include "psx_runtime.h"` and fails if it is missing. The delta does not contain a linker transcript or result establishing “~10k undefined runtime symbols” or “zero overlay DLLs”; those numbers are **UNKNOWN** here.
2. **Overlay stale-PC/variant identity: supported.** An executed-word overwrite retires the whole page epoch, incoherent capture roots are rejected against current bytes, and static requests are keyed by `(load address, size, bytes)`.
3. **Cycle specialization: supported with a qualification.** Generation selects arity 0/1/2/3 helpers, but masks with more than three nonzero dependencies deliberately retain the generic mask helper. Main-RAM load charges and the pre-deadline fast path are present.
4. **LTO: feature supported; quoted deltas unsupported.** `PSX_ENABLE_LTO`, the no-inline cycle wrappers, and a deterministic benchmark target exist. No checked-in output contains `-45.92%`, `+10.45%`, or `+32.69%`; all three are **UNKNOWN** for this audit.
5. **PGXP/GTE: the correctness changes are supported; “fake never-incremented counters removed” is not.** The old `s_nclip_corrected` counter was incremented on both sign-disagreement branches, and the removed `s_texcorr.z_interp` field was incremented when missing Z values were mean-filled. The change converts NCLIP to non-mutating disagreement telemetry, removes partial-Z interpolation, and adds real primitive/provenance counters; no demonstrably never-incremented counter removal was found.
6. **HD texture pack: supported.** Tile occupancy gates invalidation scans, and VRAM copies can become upload-equivalent candidates, with deferred readback for unresolved copies.
7. **HD text gate: headline mechanism is wrong.** The change uses an atomic aggregate boolean (`g_dispatch_armed`) derived from capture, string-table, and pending-patch state. It is **not** a monotonic mutation epoch. The separate native-text guard does gain a monotonic epoch, but that is a different subsystem.
8. **Presentation: supported as an opt-in capability.** With `fixed_outer_aspect=true`, scene classification selects only the inner content rect; the outer rect comes from configured aspect. Legacy behavior remains when the option is false.
9. **Card-write diagnostic: only true for release builds.** `card_data_writes_check` and `card_data_writes_arm` preprocess away under `PSX_NO_DEBUG_TOOLS`; debug builds still execute them.
10. **GTE bridge narrowing: supported.** The command exporter stops rewriting inputs/control registers and its source comment counts **40 redundant stores removed per COP2 command**; per-write whole-state canonicalization is removed from data/control writes.
11. **Widescreen `items_join`: not in this delta.** `runtime/src/ws_ui_group.c` and its test are unchanged against local `HEAD`. Submission-order joining was implemented earlier in ancestor commit `572b921`; it must be summarized from that commit separately, not claimed as part of this worktree.
Significant changes missing from the supplied list are the exact IRQ-pending cache and writer audit, I-cache replay isolation, two generated-dispatch caches, cycle-observer flushes, live CRTC cadence reconciliation/config exposure, PGXP rectangle-state disarming, release diagnostic pruning, and new host-optimization/keyboard regression surfaces.
## Improvement inventory
Tests named in the last column were inspected but not executed.
### Correctness bugs
| ID | Title | Files touched | What it fixes | Why it is general | Test(s) that cover it |
|---|---|---|---|---|---|
| C1 | Overlay shim anchored to an unconditional include | `tools/compile_overlays.py`; `runtime/include/{cpu_state.h,psx_cycles.h}`; overlay shim tests | A conditional include could make the whole DLL callback shim disappear under `PSX_OVERLAY_DLL_BUILD`, leaving unresolved direct runtime calls. The new anchor is the emitter-guaranteed unconditional runtime include and absence is fatal. | Any generated overlay can acquire conditional headers; no title address or symbol is involved. | `recompiler/tests/test_overlay_store_barrier_codegen.py`; `test_overlay_shim_compile.py` |
| C2 | Overlay execution epoch and exact byte-variant identity | `runtime/src/memory.c`; `tools/compile_overlays.py`; overlay discovery/perf-guard tests | Prevents an outgoing image's recorded PCs from being compiled against a later image's bytes; current-byte CFG rejection and exact-image static requests fail closed to interpretation. | Self-modifying code, streamed overlays, and same-address variants are generic PS1 behaviors. | `recompiler/tests/test_aot_overlay_discovery.py`; `runtime/tests/test_interpreter_perf_guards.py` |
| C3 | Exact cached INTC output and Cause.IP2 ownership | `runtime/include/interrupts.h`; `runtime/src/{interrupts,boot_state,memory,overlay_loader,sio,psx_interpreter}.c`; IRQ tests | Replaces repeated `i_stat & i_mask` hot reads with one cache while making a single refresh owner update both the full pending value and Cause.IP2 after every writer. | Every title uses the PS1 INTC; correctness depends on hardware-register mutation, not game logic. | `runtime/tests/test_cause_ip2_combinational.c`; `test_perf_round3_guards.py` |
| C4 | Deferred-cycle observers flush before reading time | `runtime/src/psx_cycles.c`; cycle tests | MUL/DIV and GTE issue/read/stall paths no longer observe a guest clock that is still sitting in the deferred batch. | These are architectural R3000A/GTE completion timestamps shared by all guests. | `runtime/tests/test_psx_cycle_event_boundaries.c`; `test_perf_round3_guards.py` |
| C5 | I-cache replay cannot mutate tags or charge misses | `runtime/include/psx_icache.h`; `runtime/tests/test_psx_icache_fastpath.c` | Replay now suppresses both tag evolution and miss charges while preserving normal cold initialization, hit/miss behavior, and the disabled-cache giveback path. | Lockstep/replay correctness is independent of title code. | `runtime/tests/test_psx_icache_fastpath.c` |
| C6 | Primitive-atomic PGXP position/depth and frame-canonical endpoints | `runtime/include/pgxp_vertex_cache.h`; `runtime/src/{pgxp_vertex_cache,gpu}.c`; `ENHANCEMENTS.md`; cache test/CMake | Eliminates corrected/native cracks on shared raster endpoints, prevents the two halves of a quad from making different decisions, and rejects incomplete depth instead of mean-filling missing Z. First use of `(frame, final native x, final native y)` owns the endpoint for the frame. | Shared-edge identity, quad splitting, and incomplete projection depth occur in any PGXP-enabled title. | `runtime/tests/test_pgxp_vertex_cache.c`; depth completeness is exercised indirectly by existing GTE/PGXP tests, but **no focused incomplete-Z primitive test was found.** |
| C7 | Per-component PGXP projection identity | `runtime/src/pgxp.cpp`; `runtime/include/pgxp.h`; PGXP tests | Identical packed bits from different projections can no longer be mistaken for coherent X/Y/Z provenance, including after halfword reconstruction. | Packed-word aliasing is a generic provenance problem in MIPS dataflow. | `runtime/tests/test_pgxp.cpp` |
| C8 | Destructive PGXP writes invalidate stale provenance everywhere | `recompiler/src/{pgxp_hook_emitter.cpp,pgxp_hook_emitter.h}`; `runtime/include/{pgxp.h,pgxp_hooks.h,overlay_dispatch_preamble.c.inc}`; `runtime/src/pgxp.cpp`; `runtime/src/{memory,psx_interpreter,dirty_ram_interp,overlay_loader}.c`; emitter/PGXP coverage tests | AND/XOR/NOR/SLT families, link writes, COP0/COP2 destinations, and raw RAM/scratch stores now kill old provenance even when the resulting integer bytes are unchanged. Sparse GPR/page-live gates avoid paying the callback when no value is live. | All recompilers/interpreters must agree on writer semantics; no opcode handling is title-specific. | `recompiler/tests/full_function_emitter_test.cpp`; `runtime/tests/test_pgxp.cpp`; `test_pgxp_destructive_write_coverage.py` |
| C9 | SXYP shadow FIFO follows architectural FIFO movement | `runtime/src/{gte.cpp,pgxp.cpp}`; `runtime/include/pgxp.h`; GTE test | A write to register 15 shifts shadows `12 <- 13`, `13 <- 14`, then invalidates the new `14/15`, instead of retaining stale SXY0/SXY1 provenance. | SXYP alias/FIFO semantics are architectural GTE behavior. | `runtime/tests/test_gte_register_access.cpp` |
| C10 | Precise NCLIP is telemetry-only and guest-bit-exact | `runtime/src/{gte.cpp,gpu.c,debug_server.c}`; `runtime/include/cpu_state.h`; `recompiler/src/config_loader.h`; GTE test | Exact 16.16 winding is still observed, but it never changes guest MAC0 or face visibility; the old saturation-rail clamp rescue can no longer smuggle far-off precise coordinates into rasterization. | Host precision enhancements must not alter guest branches or bypass native GTE saturation in any title. | `runtime/tests/test_gte_register_access.cpp::test_precise_nclip_never_flips_guest_sign` |
| C11 | Polygon-to-rectangle boundary clears triangle-only PGXP state | `runtime/src/{gpu.c,gpu_render.c}` | PGXP-armed polygon fast paths are no longer collapsed to rectangles, and native GP0 rectangles clear any prior triangle precision, perspective, or depth state. | Renderer state leakage and primitive fast-path conversion can affect any sequence mixing polygon and rectangle paths. | **No focused test found.** |
| C12 | Live CRTC changes reconcile host pacing at VBlank | `runtime/include/gpu.h`; `runtime/src/gpu.c`; `runtime/src/main.cpp`; `runtime/tests/test_host_refresh_sanitize.py` | Host pacing/swap policy is no longer startup-latched: a GP1(08h), restore, or plugin period change resets old pacing debt and is applied before waits; banners defer until the guest programs display mode. | PAL/NTSC switching, restores, and timing plugins are framework concerns. | `runtime/tests/test_host_refresh_sanitize.py` |
### Performance
| ID | Title | Files touched | What it fixes | Why it is general | Test(s) that cover it |
|---|---|---|---|---|---|
| P1 | Generation-time cycle dependency arity | `recompiler/src/{code_generator,full_function_emitter}.cpp`; `runtime/include/psx_cyc.h`; `runtime/src/psx_cyc_steps.c`; emitter/cycle tests | Removes the runtime dependency-mask loop for the common 0–3 register cases while retaining a generic fallback for larger masks; shared no-inline wrappers bound LTO cloning. | Every generated instruction uses the load-delay/cycle dependency machinery. | `runtime/tests/test_psx_cyc_batch.c`; `recompiler/tests/full_function_emitter_test.cpp`; `runtime/tests/test_perf_round3_guards.py` |
| P2 | Fused load charge and exact pre-deadline fast charge | `runtime/include/{psx_cyc.h,psx_cycles.h}`; `runtime/src/{memory,psx_cycles,psx_interpreter,dirty_ram_interp}.c`; cycle/perf tests | Main-RAM loads publish base + read fudge + wait/completion as one exact charge; eligible CPU charges advance directly only through `next_service_cycle - 1`, including exact Q16 overclock remainder handling. | Load timing, event deadlines, replay, and overclock transforms are common runtime machinery. | `runtime/tests/test_psx_cyc_batch.c`; `test_psx_cycle_event_boundaries.c`; `test_interpreter_perf_guards.py` |
| P3 | Exact-address compiled dispatch caches | `recompiler/src/{full_function_emitter,main_psx}.cpp`; emitter test | Adds a 256-slot TLS cache in generated static dispatch and a 1,024-slot TLS positive/negative cache in game lookup; every hit rechecks the full canonical address before bypassing binary search. | BIOS services, tail-call loops, and large dispatch tables occur across recompiled games. | `recompiler/tests/full_function_emitter_test.cpp` covers the generated-dispatch cache; **no focused test found for the game lookup cache.** |
| P4 | Two-level native-text guard epoch | `runtime/src/memory.c`; structural guard test | Stable no-write intervals return after one epoch comparison; after a mutation, the existing exact page-generation sum decides whether the cached verdict remains valid. | Dirty-RAM/native-code guards are shared by every title using mutable text. | `runtime/tests/test_perf_round3_guards.py` |
| P5 | Aggregate text-translation dispatch gate | `runtime/include/text_xlate.h`; `runtime/src/{text_xlate.cpp,fntrace.c}`; structural guard test | Avoids invoking the translation callback at every dispatch once capture is off and no string-table or unsettled RAM patch needs it. This is an atomic boolean, not an epoch. | Capture, shipped translations, and pending patch classes are framework state. | `runtime/tests/test_perf_round3_guards.py` |
| P6 | Tile-indexed texture invalidation | `runtime/include/tex_pack_rect_index.h`; `runtime/src/tex_pack.cpp`; index test/CMake | VRAM writes first query conservative 16×16-tile occupancy instead of always linearly scanning up to 8,192 upload/palette/pending-copy records; uncertainty returns “maybe” and preserves the slow exact scan. | VRAM invalidation volume and upload count vary by title, while the VRAM geometry is common. | `runtime/tests/test_tex_pack_rect_index.cpp` |
| P7 | Narrow GTE command export and incremental canonicalization | `runtime/src/gte.cpp`; `runtime/tests/test_gte_register_access.cpp`; `runtime/CMakeLists.txt` | Commands export only architectural results (`D7..D29`, `C31`) and writes maintain aliases/masks incrementally instead of sweeping the entire backing state after every write. | COP2 register semantics are architectural and shared. | `runtime/tests/test_gte_register_access.cpp` |
| P8 | Debug-only store diagnostics compile out of release | `runtime/src/{memory,sio}.c`; structural guard test | Removes universal RAM-store calls to card-write and debug trace consumers, plus SIO arming, when `PSX_NO_DEBUG_TOOLS` has no consumer. Debug behavior is retained. | This is build-mode ownership, not a title exception. | `runtime/tests/test_perf_round3_guards.py` |
### New capability
| ID | Title | Files touched | What it fixes/adds | Why it is general | Test(s) that cover it |
|---|---|---|---|---|---|
| N1 | Opt-in CMake IPO/LTO and generated-hotpath benchmark | `runtime/runtime.cmake`; `runtime/CMakeLists.txt`; `runtime/tests/bench_psx_runtime_hotpath.c`; `runtime/src/psx_cyc_steps.c` | Adds toolchain-checked `PSX_ENABLE_LTO` (default off), applies IPO to runtime targets, and provides a semantic-checksummed generated-style benchmark. | Toolchain optimization and code-shape regression measurement apply to every game target. | Benchmark is deliberately not registered in CTest; **no result is checked in or run here.** |
| N2 | HD replacements from VRAM-to-VRAM copies | `runtime/include/tex_pack.h`; `runtime/src/{tex_pack.cpp,gpu_render.c}`; copy test/CMake | Copy destinations can reuse an exact tracked source preimage or become a lazily read-back candidate; CPU upload and copy records share the same position/size/hash semantics and replacement lookup. | Many PS1 engines stage textures in VRAM rather than uploading final destinations directly. | `runtime/tests/test_tex_pack_copy.cpp` |
| N3 | Configurable CRTC refresh multiplier | `recompiler/src/{config_loader.cpp,config_loader.h}`; `runtime/include/{gpu.h,mod_plugins.h}`; `runtime/src/gpu.c`; `runtime/src/main.cpp`; host refresh test | Exposes `[video] crtc_refresh_multiplier` in the range 1..8 as the baseline timing setting, with activation plugins allowed to override it. | It turns an existing runtime timing control into a validated per-project framework option. | `runtime/tests/test_host_refresh_sanitize.py` covers live x2 cadence ownership, not parser bounds 1..8. |
| N4 | Stable configured outer aspect with scene-specific inner content | `WIDESCREEN.md`; `recompiler/src/{config_loader.cpp,config_loader.h}`; `runtime/include/{gpu_gl_renderer.h,gpu_vk_renderer.h,present_ring.h,ws_present_layout.h}`; `runtime/src/{gpu.c,gte.cpp,main.cpp,gpu_gl_renderer.c,gpu_vk_renderer.c,ws_present_layout.c}`; layout test/CMake | Adds opt-in `fixed_outer_aspect`: menus/FMV can remain centered 4:3 content without changing the outer configured canvas; GL, Vulkan, interpolation, hold-last, and CPU paths use the separated policy. | Outer user preference and content correction are independent concepts for any widescreen title. | `runtime/tests/test_ws_present_layout.c` |
### Diagnostics and verification surfaces
| ID | Title | Files touched | What it fixes/adds | Why it is general | Test(s) that cover it |
|---|---|---|---|---|---|
| D1 | Actionable PGXP and copied-texture telemetry | `runtime/include/{cpu_state.h,pgxp.h}`; `runtime/src/{gpu.c,gte.cpp,pgxp.cpp,debug_server.c,tex_pack.cpp}` | Reports primitive armed/rejected/mixed decisions, endpoint promotion/demotion/cache overflow, memory invalidations, mixed projection rejects, NCLIP sign disagreements, and copy queued/resolved/tracked/invalidated/dropped counts. | These counters diagnose framework coverage and fail-closed behavior without game addresses. | Counter transitions are partially asserted by `runtime/tests/{test_pgxp.cpp,test_gte_register_access.cpp,test_tex_pack_copy.cpp}`. |
| D2 | Host optimization acceptance contract | `docs/HOST_OPTIMIZATION_CONTRACT.md` | Defines an interleaved A/B protocol, semantic equivalence requirement, raw-run retention, and a 5% acceptance gate for complexity/ABI-bearing host optimizations. | It is a reusable maintainer process rather than title behavior. | Documentation only. |
| D3 | Keyboard Select+Start chord regression | `runtime/tests/test_keyboard_pad_chord.c`; `runtime/CMakeLists.txt` | Adds a focused test for default bindings, simultaneous and consecutive polls, release, and re-press behavior. It does not change production keybind code. | Multi-button active-low pad words are common input behavior. | `runtime/tests/test_keyboard_pad_chord.c` |
## Concrete evidence and measurements
### Correctness evidence
**C1 — overlay shim anchor.** The root cause and new invariant are stated at `tools/compile_overlays.py:2078-2089`: the syntactically last include may be inside the per-instruction I-cache `#ifdef/#else`, and `PSX_OVERLAY_DLL_BUILD` would preprocess away the callback definitions. The insertion now uses the exact unconditional include at `tools/compile_overlays.py:2084-2091`; absence raises `ValueError`. `recompiler/tests/test_overlay_store_barrier_codegen.py:109-118` asserts the flush definition precedes the DLL conditional, and `test_overlay_shim_compile.py:43-59` compiles the expanded PGXP hook surface with the DLL macros when that test is run. Windows exports for the cycle barrier are at `runtime/include/cpu_state.h:76-81` and `runtime/include/psx_cycles.h:192-198`. **Measured result:** UNKNOWN. The supplied ~10k unresolved-symbol and zero-linked-DLL claims are absent from the audited delta.
**C2 — overlay epoch/identity.** `runtime/src/memory.c:1080-1114` preserves evidence for data-only writes but clears both full-page PC bitmaps and the page gate when any executed word is replaced. `tools/compile_overlays.py:1546-1572` rejects capture-derived roots whose current capped CFG reaches invalid instructions; `tools/compile_overlays.py:2867-2930` keys static demand by `(load_addr, size, data)` and resolves it only against current-byte range identities; isolated generation also reports `INCOHERENT_VARIANT_BYTES` at `tools/compile_overlays.py:3074-3083`. Focused regressions begin at `recompiler/tests/test_aot_overlay_discovery.py:340` and `:366`. **Measured result:** UNKNOWN; no shard ok/failed count or coverage percentage for this change is present.
**C3 — IRQ cache.** The owner computes `pending = i_stat & i_mask`, stores the full value, and then derives Cause.IP2 at `runtime/src/interrupts.c:149-160`. The structural audit at `runtime/tests/test_perf_round3_guards.py:22-46` currently finds exactly **19** direct `I_STAT`/`I_MASK` mutations across five writer files and requires a refresh near each; it also verifies the cache is stored before the nullable Cause pointer can return (`:55-63`). `runtime/tests/test_cause_ip2_combinational.c:97` repeatedly asserts exact cache equality. **Measured result:** UNKNOWN; 19 is a static writer-set count, not a runtime speed result.
**C4 — cycle observers.** Flushes precede cycle-dependent work in `psx_muldiv_set`, `psx_muldiv_stall`, `psx_gte_read`, `psx_gte_set`, and `psx_gte_stall` at `runtime/src/psx_cycles.c:696-795`. `runtime/tests/test_psx_cycle_event_boundaries.c` includes explicit delayed GTE read/stall assertions and ends by checking cross-device `D-1 + 1` causality. **Measured result:** UNKNOWN.
**C5 — I-cache replay.** `runtime/include/psx_icache.h:20-32` separates active-state initialization from replay and calls the miss path only outside replay. Warmed-hit and would-be-miss replay cases are at `runtime/tests/test_psx_icache_fastpath.c:79-103`. **Measured result:** UNKNOWN.
**C6 — primitive/canonical endpoint.** The cache is a fixed **65,536-entry** table (`runtime/include/pgxp_vertex_cache.h:21-35`) and resolves frame/native-coordinate identity at `runtime/src/pgxp_vertex_cache.c:10-38`. Whole primitive resolution, including all four quad vertices, starts at `runtime/src/gpu.c:3434-3609`; depth arms only when `depth_count == count` (`:3570-3605`), replacing the old mean-filled partial-Z path. The focused cache test covers corrected→native, native→corrected, transform identity, and frame identity; its native→corrected candidate is deliberately **0.75 px**, above the old 0.5 workaround (`runtime/tests/test_pgxp_vertex_cache.c:12-64`). **Measured result:** no performance or in-game crack count is recorded; UNKNOWN.
**C7 — component IDs.** `PGXPValue` gains independent X/Y/Z projection IDs at `runtime/src/pgxp.cpp:62-68`; coherence predicates require matching nonzero IDs at `:287-307`, and reconstructed halves propagate their component's ID around `:450-526`. `runtime/tests/test_pgxp.cpp:205-220` asserts that mixed projections are rejected even when packed integer geometry can otherwise look usable. **Measured result:** UNKNOWN.
**C8 — destructive writes.** The hot GPR macro consults `g_pgxp_gpr_live_mask` before calling the invalidator (`runtime/include/pgxp_hooks.h:93-101`); the invalidator itself is at `runtime/src/pgxp.cpp:396-403`. Raw word invalidation and partial-half handling begin at `runtime/src/pgxp.cpp:371`, while six central RAM/scratch write branches are gated in `runtime/src/memory.c:1968-2520`. `runtime/tests/test_pgxp_destructive_write_coverage.py:69-77` requires two 1-byte, two 2-byte, and two 4-byte paths plus both interpreter/emitter surfaces. `runtime/tests/test_pgxp.cpp:137-203` includes byte-identical GPR and RAM writes. The existing source comment says the earlier broad ALU-hook gate accounted for **3.6% of the emulation thread** in a high-refresh workload (`runtime/src/pgxp.cpp:91-101`), but there is no measured delta for the new sparse invalidation gates. **Measured result for this change:** UNKNOWN.
**C9 — SXYP.** Architectural backing shifts at `runtime/src/gte.cpp:2027-2047`; the shadow shift/reset is implemented in `runtime/src/pgxp.cpp:1033-1053`. `runtime/tests/test_gte_register_access.cpp:501-526` checks the precise FIFO before and after register 15 and all SXY write classes. **Measured result:** UNKNOWN.
**C10 — NCLIP.** The old double-based sign replacement is removed; `runtime/src/gte.cpp:962-983` computes an exact `int64_t` determinant, increments disagreement telemetry, and always writes native `out` to MAC0. The GPU-side comment at `runtime/src/gpu.c:3644-3650` makes NCLIP audit-only and removes the saturation-rail rescue route to far-off precise raster coordinates. The regression constructs native `+1` versus exact negative and requires guest MAC0 to remain `+1` while the disagreement counter increments (`runtime/tests/test_gte_register_access.cpp:575-602`). The removed code incremented `s_nclip_corrected` in both mismatch branches, and the removed partial-Z path incremented `s_texcorr.z_interp`, so the “never-incremented” description is false. **Measured result:** one deterministic disagreement fixture; no field count or performance result.
**C11 — rectangle boundary.** PGXP-aware polygon fast paths require position/depth to be unarmed before collapsing a quad to a rectangle (`runtime/src/gpu.c:4049-4054`, `:4287-4330`). `runtime/src/gpu_render.c:135-141` clears precise position, perspective, and depth state; each flat/textured/scaled rect entry invokes it at `:270`, `:288`, and `:296`. **Test/measurement:** no focused test found; UNKNOWN.
**C12 — live CRTC cadence.** Guest display-mode observation is recorded on GP1(08h) and restore at `runtime/src/gpu.c:6018` and `:6202`. `refresh_live_crtc_cadence` (`runtime/src/main.cpp:2916-2949`) keys transitions by exact guest period, resets old wall-clock debt, reapplies present cadence, and logs only after guest programming; it is called before frame performance/pacing work at `runtime/src/main.cpp:6250`. The structural test's exact arithmetic establishes PAL×2 = **100.0 Hz** matches a 100 Hz panel while NTSC×2 = **120.0 Hz** does not (`runtime/tests/test_host_refresh_sanitize.py:73-86`). These are formula assertions, not a live-run result. **Measured runtime result:** UNKNOWN.
### Performance evidence
**P1 — arity helpers.** Both emitters choose specialized helpers and retain the generic fallback at `recompiler/src/code_generator.cpp:51-69` and `recompiler/src/full_function_emitter.cpp:35-53`. The shared wrappers and deliberate no-inline boundary are at `runtime/src/psx_cyc_steps.c:5-68`; overlay DLLs retain inline equivalents at `runtime/include/psx_cyc.h:219-247`. `runtime/tests/test_psx_cyc_batch.c:244-264` defines **98,304 oracle comparisons** (`4,096 seeds × 6 modes × 4 arities`) and compares every `CPUState` byte plus deferred cycles. **Measured performance:** UNKNOWN.
**P2 — fused/fast charge.** Main-RAM word and half loads compute a single `charge + fudge + 5` publication (`wait 3 + completion 2`) at `runtime/include/psx_cyc.h:280-337`; slow paths carry the same base charge through `runtime/src/memory.c:2316-2383`. `psx_cycles_try_fast_charge` previews and commits both scaled cycles and Q16 remainder only below the published limit (`runtime/include/psx_cycles.h:110-139`), which is set to `next_service_cycle - 1` at `runtime/src/psx_cycles.c:189-194` and invalidated at MMIO/exact/restore boundaries. Tests define **4,096 fused-load oracle cases** (`2,048 × 2 overclock modes`, `runtime/tests/test_psx_cyc_batch.c:267-307`) and **1,152 fast-charge oracle cases** (`2 scales × 16 accumulators × 3 charges × 12 deadline rooms`, `:311-334`). **Measured performance:** UNKNOWN.
**P3 — dispatch caches.** Generated static dispatch emits the 256-slot table and exact-address hit check at `recompiler/src/full_function_emitter.cpp:1834-1865`; the game lookup emits 1,024 TLS slots and caches both hits and misses at `recompiler/src/main_psx.cpp:1594-1629`. `recompiler/tests/full_function_emitter_test.cpp:199-204` checks the generated helper and address validation. **Measured result:** UNKNOWN; no hit rate, comparison count, or benchmark delta is present.
**P4 — native-text guard epoch.** The monotonic epoch and global mutation helper are at `runtime/src/memory.c:617-624`; the one-compare hit and page-generation fallback are at `:772-829`. The existing source comment records a motivating profile in which `psx_game_text_native_ok` was **6.8% of the emulation thread** with the older memo never hitting (`runtime/src/memory.c:625-632`). That is a baseline attribution, not a measured post-change gain. **Measured gain:** UNKNOWN.
**P5 — text-translation aggregate gate.** `refresh_dispatch_armed` derives one atomic gate from capture, string-table, and three pending patch counts (`runtime/src/text_xlate.cpp:441-453`); `fntrace_record` checks it at `runtime/src/fntrace.c:170-171`. The source records two historical baselines: capture scanning at roughly **33k dispatches/frame**, **18.6% of samples**, with capture disabled yielding **+22% fps over guest frames 800–2200** (`runtime/src/text_xlate.cpp:423-435`), and the otherwise-useless fixes-only argument scan at **4.2% of the emulation thread** (`:898-905`). None is a direct A/B of the new outer aggregate gate. **Measured gain:** UNKNOWN.
**P6 — texture tile index.** The conservative grid uses 16×16 tiles over a 2,048×1,024 domain and returns “maybe” for unindexed/uncertain rectangles (`runtime/include/tex_pack_rect_index.h:15-83`). `invalidate_locked` checks upload, pending-copy, and palette occupancy before any vector scan (`runtime/src/tex_pack.cpp:562-620`); the tracker cap remains **8,192** records (`runtime/src/tex_pack.cpp:54`). The deterministic test runs **20,000 randomized add/remove/query iterations** and asserts there are no false negatives (`runtime/tests/test_tex_pack_rect_index.cpp:47-77`). **Measured performance:** UNKNOWN.
**P7 — GTE bridge.** `runtime/src/gte.cpp:1702-1729` exports only command results and explicitly counts **40 redundant stores removed per command**. Data/control writes now maintain their own masked/aliased state at `:2012-2081`; full canonicalization remains only at the raw import boundary (`:2099`). The oracle suite covers **2,112 command cases** (`22 functions × 96`), **256** MVMVA selector combinations, and **8,192** stateful stream steps (`64 × 128`) at `runtime/tests/test_gte_register_access.cpp:382-473`, in addition to register-write tests. **Measured throughput:** UNKNOWN.
**P8 — release diagnostics.** Declarations and call sites are guarded around `runtime/src/memory.c:1298-1301`, `:2006-2012`, `:2142-2150`, and `:2485-2493`; SIO arming receives the same build guard. `runtime/tests/test_perf_round3_guards.py:65-73` scans every card diagnostic call for a release guard. **Measured performance:** UNKNOWN.
### Capability evidence
**N1 — LTO/benchmark.** `runtime/runtime.cmake:26-44` defines default-off, C/C++ toolchain-checked `PSX_ENABLE_LTO`; target IPO is applied at `:937-939`. The wrappers resist whole-program cloning at `runtime/src/psx_cyc_steps.c:28-38`. The benchmark models **32 guest instructions/block**, defaults to **4,000,000 blocks** (**128,000,000 guest instructions/sample**), takes **7 samples**, reports median/min/max, and rejects checksum drift (`runtime/tests/bench_psx_runtime_hotpath.c:25-28`, `:227-284`). It is intentionally not a CTest (`runtime/CMakeLists.txt:46-58`). **Measured result:** UNKNOWN; the supplied `-45.92%`, `+10.45%`, and `+32.69%` values are not in the audited repository state.
**N2 — VRAM copies.** `gr_copy_rect` classifies mask/wrap/overlap preservation and reports the copy before backend mutation (`runtime/src/gpu_render.c:143-156`). `tex_pack_on_copy` (`runtime/src/tex_pack.cpp:817-893`) shares exact full-source preimages, hashes subrects, invalidates the destination in the original order, and otherwise queues a fail-closed candidate. Draw-time query/resolution is at `:896-989`; CPU and copy records meet in the shared builder at `:625-657`. `runtime/tests/test_tex_pack_copy.cpp` checks CPU/copy key parity, invalidation, and replacement lookup. Its optional, unregistered benchmark is designed for **200,000 copies**, **5,000,000 no-pending queries**, and **50,000 hashes** (`:168-208`), but no output is checked in or run. **Measured performance:** UNKNOWN.
**N3 — CRTC config.** Parsing and range rejection are at `recompiler/src/config_loader.cpp:573-579`; the default/range contract is at `recompiler/src/config_loader.h:412-414`. Config is applied before activation plugins at `runtime/src/main.cpp:11534-11538`. The available test checks x2 live cadence only; it does not test TOML parser bounds or multipliers 3–8. **Measured compatibility/coverage for 3–8:** UNKNOWN.
**N4 — outer/content layout.** The API contract explicitly forbids scene classification from changing the outer rect when fixed mode is enabled (`runtime/include/ws_present_layout.h:20-32`); the independent rect calculation is at `runtime/src/ws_present_layout.c:33-60`. GL uses it at `runtime/src/gpu_gl_renderer.c:3452-3461`; Vulkan uses it at `runtime/src/gpu_vk_renderer.c:1688-1706`; scene classification was renamed/content-scoped in `runtime/src/gpu.c:324-390` and `runtime/src/main.cpp:7022-7391`. The focused test checks exact 32:9-in-16:9 and 21:9 layouts plus unchanged legacy behavior (`runtime/tests/test_ws_present_layout.c:14-43`). **Measured visual/performance result:** UNKNOWN.
### Diagnostic evidence
**D1 — telemetry.** Primitive counters are defined and exported at `runtime/src/gpu.c:3460-3498`; PGXP memory/mixed-projection counters are maintained in `runtime/src/pgxp.cpp`; NCLIP disagreements are at `runtime/src/gte.cpp:941-983`; debug JSON publishes them at `runtime/src/debug_server.c:5216-5276`. Copy lifecycle counters are defined at `runtime/src/tex_pack.cpp:204-207` and published at `:1465-1487`. **Measured field values:** UNKNOWN; the change adds counters but the delta contains no captured report.
**D2 — optimization contract.** `docs/HOST_OPTIMIZATION_CONTRACT.md:29-44` defines the 5% retention and semantic-equivalence gates; `:103-110` requires a separate interleaved performance campaign; `:185-200` specifies evidence retention. The example percentages at `:181-182` are explicitly examples, not results from this work. **Measured result:** not applicable.
**D3 — keyboard chord test.** `runtime/tests/test_keyboard_pad_chord.c:24-50` checks default Return/RShift bindings and six pad states: released, Select held, first and consecutive Select+Start polls, Start release, and Start re-press. `runtime/CMakeLists.txt:86-95` registers it. **Production change/measurement:** none; this is regression coverage only, and it was not run here.
## RISK / REVIEW FOCUS
1. **Overlay byte identity and shim placement (C1/C2) — highest review priority.** Preserve three invariants: (a) the callback preamble is outside every conditional and missing the guaranteed runtime include is a hard failure; (b) a compiled body satisfies a request only when load address, size, and exact bytes/range identities agree; (c) overwriting an executed word retires all PC/dispatch evidence for that page's outgoing image, while an unrelated data-only write does not erase valid execution evidence. False positives execute wrong bytes; overly broad invalidation silently destroys AOT coverage.
2. **Guest cycle exactness and device deadlines (C4/P1/P2) — highest review priority.** Specialized helpers must be byte-for-byte equivalent to the generic dependency mask, preserve GPR0, read-absorb and load-delay ownership, and retain COSIM/replay/conservative/device-service fallbacks. A fused load must apply base, read fudge, wait, completion, and Q16 overclock remainder exactly once. Fast charge must never reach or cross `next_service_cycle`, overflow the clock, bypass a local/deferred batch, or leave stale eligibility after MMIO, exact service, or restore. Every observer of absolute guest time must flush first.
3. **IRQ pending cache and live CRTC timing (C3/C12/N3) — highest review priority.** Every direct `I_STAT`/`I_MASK` writer, including init, restore, acknowledgment, and temporary CD/SIO masking, must call the refresh owner. The full cached value must remain exactly `i_stat & i_mask`; only Cause.IP2 is masked to the architectural 11 bits. Cached snapshots must be refreshed after any idle skip/device service. CRTC period changes must be reconciled before the next host wait without changing plugin override precedence or inheriting pacing debt from the old period. Adding a twentieth writer without extending the structural audit is a correctness bug.
4. **PGXP provenance and guest visibility (C6–C11).** NCLIP telemetry must never write a host-corrected sign to guest MAC0. Projection IDs must survive only operations that prove component identity; every destructive writer must invalidate across both interpreters, generated code, and overlay DLLs. Endpoint-cache collision/overflow behavior must remain deterministic and fail to native geometry. Rect paths must clear every triangle-only backend latch.
5. **VRAM copy tracking (P6/N2).** Masked, wrapping, or overlapping copies must never claim a pre-copy source hash as the post-copy destination. Pending readback must use the authoritative post-copy backend, be invalidated by later writes/restores, and preserve CPU-upload key semantics including position. Tile indices may have false positives but must never have false negatives.
6. **LTO/code-shape claims (P1/N1).** Review generated symbol visibility on MSVC/Clang/GCC and ensure IPO does not clone the guarded wrappers or drop DLL exports. Do not accept the quoted LTO percentages without raw commands, compiler/linker versions, interleaved samples, and semantic checksums; none is present in this delta.
7. **Presentation backends (N4).** Fixed mode must produce the same outer rect for gameplay, menu, FMV, interpolation, hold-last, native VRAM, CPU, and wide paths; scene state may change only the inner rect. Legacy mode must remain byte/geometry compatible for projects that do not opt in, especially Vulkan's pre-existing 4:3 policy.
## NOT UPSTREAMABLE
- The WipEout-specific conclusion in `ENHANCEMENTS.md:879-883` that `pgxp_tolerance = 1.0` is valid for WipEout 3 is title validation/configuration, not a framework default. Upstream the canonical-endpoint mechanism and neutral explanation; keep the title's chosen value in the game repository.
- The explanatory phrase “the WipEout 8 MB payload depends on that” at `runtime/src/memory.c:1090` should be generalized before submission. The whole-page execution-epoch mechanism itself is upstreamable.
- `docs/internal/upstream/WIPEOUT3_SESSION_ENGINE_DRAFTS_2026-08-20.md` is a WipEout session/commit audit, not framework product documentation; do not include it in an upstream code PR.
- `build-pgxp-focused/` and `build-texpack-focused/` are generated build trees and must not be committed.
- Enabling `fixed_outer_aspect`, a non-stock CRTC multiplier, PGXP tolerance 1.0, or any other title profile belongs in the game repository. The parser/runtime capabilities and default-off framework behavior are upstreamable.
- No new WipEout-specific executable address hook was found in the working-tree delta. Existing title references outside the added lines are not evidence that the general mechanisms above are per-game hacks.
- The `items_join` UI grouping change is potentially upstreamable, but it is **not part of this delta**; submit/review it from ancestor commit `572b921` with its own history and evidence rather than folding it into this worktree summary.
## Suggested PR split and landing order
1. **Overlay DLL shim placement/export fix (C1).** Small, independently testable, and prerequisite to trusting any overlay-DLL validation.
2. **Overlay byte-identity correctness (C2).** Land after the shim fix so generated overlay tests exercise a valid DLL surface. Keep the page-epoch producer fix and byte-keyed consumer/static-request logic together because either half alone leaves a stale-image path.
3. **IRQ pending-cache ownership (C3).** Separate high-risk PR. Include all current writers and the 19-writer structural audit in the same change.
4. **Cycle exactness foundation (C4, C5, P2 without code-shape claims).** Land observer flushes, replay-safe I-cache behavior, fused load accounting, deadline publication/invalidation, and oracle tests together. This establishes equivalence before performance-oriented code generation changes.
5. **Cycle arity specialization and LTO surface (P1, N1).** Depend on PR 4. Keep the benchmark and no-inline rationale with the specialization; publish new raw benchmark results instead of the unsupported percentages in the supplied headline.
6. **Dispatch/text/release hot-path pruning (P3, P4, P5, P8).** These are independent of guest timing after PR 4. Prefer three reviewable commits/PRs if maintainers want narrower invalidation reasoning: dispatch caches; text guards/gates; debug-only store hooks.
7. **GTE/PGXP provenance correctness (C7–C10, P7).** Land component IDs, destructive-write coverage, SXYP FIFO, NCLIP telemetry-only behavior, and narrowed architectural export first. These define trustworthy provenance and guest-bit-exact boundaries.
8. **PGXP primitive/canonical endpoint and renderer boundary (C6, C11, D1 PGXP portion).** Depend on PR 7. Add a focused rectangle-state regression before submission; keep the WipEout tolerance conclusion out of the framework PR.
9. **Texture-pack index, then copy candidates (P6 followed by N2 and copy telemetry).** The copy queue uses the conservative index and shared upload storage, so land/test the no-false-negative index first. Review copy/mask/wrap/overlap and savestate invalidation as a separate capability PR.
10. **Presentation layout separation (N4).** Independent UI/rendering PR covering the shared layout helper and both GL/Vulkan paths. Preserve default-off legacy behavior.
11. **CRTC configuration and live cadence (C12/N3).** Separate timing-sensitive PR after IRQ/cycle work. Add parser tests for 1, 8, 0, and 9 and either validate or narrow the advertised 1..8 range before landing.
12. **Maintainer diagnostics (D2/D3).** The optimization contract can land as documentation if the project accepts that policy; the keyboard regression can land independently. Neither should block correctness PRs.
---
## Appendix: session measurements (recorded 2026-08-20)
The audit above correctly marks several deltas UNKNOWN because no result transcript is
checked in. These numbers WERE observed live during the session that produced this work.
They are recorded here so a reviewer can see the basis; they are single-machine
observations, not a portable benchmark suite.
**Host:** AMD Ryzen 7 9800X3D (8C/16T, 4.7 GHz, X3D cache), Windows 11, pinned
retcomm cmake-clang-v1 (Clang 22.1.8). Title: WipEout 3 SE (PAL, SCES-02845) with
hueponik's official PAL 100 fps patch, 8 MB RAM, max in-game detail,
`cpu_overclock = 900`, `crtc_refresh_multiplier = 2` (PAL 50 Hz x2 = 100 Hz).
### LTO / codegen benchmark
`runtime/tests/bench_psx_runtime_hotpath.c`, median-of-three rotated runs, seven samples
each, 256M guest instructions per sample. EVERY variant produced identical results
(guest cycles 33,774,169; oc accumulator 60,416; services 0; digest 3d88e743440954f1),
which is the equivalence evidence for this workload.
| Variant | Guest instructions/s | vs baseline |
|---|---:|---:|
| Baseline `-O3` | 451,062,216 | - |
| Plain ThinLTO | 243,919,125 | -45.92% |
| Plain full LTO | 585,251,881 | +29.75% |
| ThinLTO + guarded wrappers | 498,195,210 | +10.45% |
| **Full LTO + guarded wrappers** | **598,526,036** | **+32.69%** |
| `-march=native` only | 457,336,000 | +1.39% |
| `-march=znver5 -mtune=znver5` | 441,476,740 | -2.13% |
| Helpers moved to headers/unity | 162,246,886 | -64.03% |
Plain ThinLTO REGRESSES badly by cloning hot interlock bodies; the `noinline` guard at
`runtime/src/psx_cyc_steps.c:27` is what makes LTO a win. ThinLTO executable size fell
97,148,928 -> 86,249,472 bytes (-11.22%) once guarded, confirming the over-cloning.
### End-to-end guest speed (identical BIOS boot phase each run, so directly comparable)
| Build state | Guest speed |
|---|---:|
| Session start (no static bake; overlay autocompile degraded) | 0.36x |
| + partial static bake (25/62 shards) | 0.42x |
| + clean static bake (26 shards, 0 failed) | 0.49x |
| + overlay DLL link fix; PGXP hooks and rewind disarmed | 0.58x |
| + cycle-accounting specialization, PGXP RE-ENABLED, 100% texture pack | 0.50x |
| **+ full LTO** | **0.68-0.76x** |
LTO's end-to-end contribution on this identical phase was ~+35%, consistent with the
+32.69% synthetic result. 3D attract-mode scenes were observed sustaining
`VBlank 100/s 1.00x, render 100 fps`.
### Overlay pipeline
| Metric | Before | After |
|---|---:|---:|
| Overlay DLL link errors | 10,395 diagnostics / 20 symbol names | 0 |
| Overlay DLLs produced by autocompile | 0 | 1,442 in cache |
| Static bake shards | ok=25 failed=23 skipped=14 | ok=26 failed=0 skipped=14 |
| Runtime dispatch | `disp_native=0`, `disp_interp=4,456,079` | native dispatch active |
The `disp_native=0` figure is from the runtime's own `psx_last_run_report.json`, which also
carried `autocompile_degraded=1` with the reason string naming the cause.
### HD texture pack
| Metric | Before | After |
|---|---:|---:|
| Requested (texture,palette) pairs covered | 39 / 439 (8.88%) | 439 / 439 (100%) |
| Pack entries | 292 | 445 generated (692 live incl. legacy) |
| PNG payload | 2.61 MiB | 4.32 MiB |
Verification on the generated set: alpha subset of source 402/402, RGB subset 402/402,
all dimensions <= 4096, F500 font atlases byte-identical (untouched).
### PGXP
Hook cost measured by the 1 kHz IP sampler on the emulation thread, in race:
`psx_pgxp_load` 7.66% + `psx_pgxp_alu` 3.63% + `psx_pgxp_store` 2.35% +
`psx_pgxp_cop2` 1.20% = **14.84%** while both correction features were DISABLED, because
`precise_nclip` and `pgxp_cpu_mode` arm the hook surface independently. After disarming,
the same buckets fell to 0.69% total.
### Font atlases (game-side asset work, listed for completeness)
| Metric | Before | After |
|---|---:|---:|
| Detached ink components (ringing lobes) | 986 | 0 |
| Dark ink pixels (black halo) | 87,650 | 0 |
| Alpha masks | - | byte-identical before/after |