diff --git a/docs/HOST_OPTIMIZATION_CONTRACT.md b/docs/HOST_OPTIMIZATION_CONTRACT.md new file mode 100644 index 00000000..29146544 --- /dev/null +++ b/docs/HOST_OPTIMIZATION_CONTRACT.md @@ -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: +Commits/artifacts: A= B= selector= +Workload: +Primary metric: +Run order: A1 B1 A2 B2 A3 B3 +Raw A: , , +Raw B: , , +Min-of-3: A= B= change= +Medians: A= B= change= +Correctness: faithful= forced-on= result= +Contamination: +Complexity/ABI: +Decision: ACCEPT/REJECT - +``` + +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. diff --git a/docs/UPSTREAM_2026-08-20.md b/docs/UPSTREAM_2026-08-20.md new file mode 100644 index 00000000..d69b5cca --- /dev/null +++ b/docs/UPSTREAM_2026-08-20.md @@ -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 |