Files
psxrecomp/docs/HOST_OPTIMIZATION_CONTRACT.md
T
Shokunin 1e6e43639a Title timing constant: [timing] guest_cycle_scale (and _gated)
No CPU is emulated: recompiled code charges the guest clock a fixed cost
per instruction. guest_cycle_scale = N charges 1/N of it (instruction,
load, mul/div, GTE latencies, overlay DLL batches) while VBlank, timers,
CD, SPU and DMA keep hardware time, so the game never runs out of frame
time. A title constant in game.toml (1 = faithful, 1..64), never a player
setting; guest_cycle_scale_gated applies it only while the title's mod
opens the gate (psx_guest_cycle_scale_gate_open). PSX_GUEST_CYCLE_SCALE
overrides for testing. A scale other than 1 is folded into the netplay
content fingerprint as a safety net.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:41:34 -07:00

204 lines
10 KiB
Markdown

# 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, guest cycle scale, 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.