Files
psxrecomp/docs/internal/audit_inventory.md
T
Matthew Stanley e6ce5006b4 docs: move root and stale docs into docs/ and docs/internal/
The repository root carried seven markdown files that were not onboarding
material, plus two that had been superseded by the code they described. This
sorts them: reader-facing docs to docs/, working notes to docs/internal/, and
the two superseded ones merged into the docs that replaced them.

Moved to docs/:            WIDESCREEN.md, ENHANCEMENTS.md, TCP_COMMANDS.md
Moved to docs/internal/:   PRINCIPLES.md, ISSUES.md, DEBUG.md, GTE_LM_FIX.md,
                           and from docs/: bios-hardening.md, overlay-status.md,
                           overlay-plan.md, overlay-recompilation-design.md,
                           overlay-discovery.md, TOMBA2_EXC_CONSULT_RESPONSE.md,
                           CASE_A_AOT_GAP.md, audit_inventory.md
Moved to docs/internal/upstream/: UPSTREAM_2026-08-20.md

The root keeps only what belongs there: README.md, CONTRIBUTING.md, LICENSE,
CLAUDE.md, THIRD_PARTY_ATTRIBUTION.md, RELEASE_NOTES.md, the dotfiles and
psxrecomp_cli.py.

MERGES

SPLITGEN_MIGRATION.md folded into docs/BUILDING.md as "Split generated C". Its
migration instructions were stale in a way that mattered: it told every game
repo to hand-edit its CMakeLists and add a file(GLOB ...) over
<serial>_full*.c, but runtime/runtime.cmake now does that itself. GEN_FULL_GLOB
is a multi-value argument of psxrecomp_add_game_runtime(); the loop at
runtime.cmake:1985 makes each pattern absolute against CMAKE_CURRENT_SOURCE_DIR,
expands it with file(GLOB), and concatenates the hits into
GAME_GENERATED_FULL_C. So a title names the pattern and gets however many shards
a regeneration produced. The rewritten section describes that, the two-step
fallback when the globs match nothing (GEN_FULL_FALLBACK, else <serial>_full.c
derived from the dispatch marker by substituting _dispatch.c), and the one
caveat the code actually has: that file(GLOB) is plain, not CONFIGURE_DEPENDS,
so a regeneration which changes the shard COUNT needs a re-configure to be seen.

psxrecomp-toml-readme.md folded into docs/GAME_PROJECT_SETUP.md as "EXE-only
autofill (psxrecomp-toml)", placed after "Disc autofill (probe_disc.py)" and
before "Project Studio" so the two autofill paths sit together. Its documented
flags were checked against the usage text in recompiler/src/main_toml.cpp:28-42
and against the argument parser at :150-165 -- all seven options match with no
drift, so the flag list is reproduced as-is. Added a pointer that probe_disc.py
is preferable when a cue exists, since that tool does the same EXE-header work
plus disc identity and netplay gates.

FUNCTIONAL FIXES

tools/gen_tcp_commands.py wrote to REPO/"TCP_COMMANDS.md". After the move that
path does not exist, so the generator would have created a new stray root file
and --check would have failed against it forever. Retargeted to
REPO/"docs"/"TCP_COMMANDS.md", along with the seven docstring and message
strings that name the path.

Running it surfaced two further problems, both fixed here:

  * The doc was genuinely stale -- pre-existing drift, not introduced by this
    commit (the moved file is byte-identical to origin/master, and no
    debug-server command table is touched here). Four commands registered since
    the last regeneration were missing from the index -- cdrom_timing_dump,
    savestate_status, ws_ui_groups, xprobe_watch -- and scanline had gained a
    backend. Regenerated: 307 -> 311 commands, and the change is confined to the
    AUTOGEN block (the hand-written head and tail are byte-identical to master).
    tools/gen_tcp_commands.py --check now exits 0.
  * DOC.write_text() has no newline argument, so Python translated every newline
    to os.linesep. On Windows that rewrote all 585 lines of the file to CRLF and
    turned a four-line index update into a whole-file diff -- measured: 589 CRLF
    / 0 LF after a run, against 0 CRLF / 585 LF in the repo. Now written through
    an explicit newline="" handle, verified to leave the file at 0 CRLF.

runtime/include/debug_server.h:9 and runtime/src/debug_server.c:8 cited "TCP.md
and DEBUG.md". TCP.md has never existed in this repo under that name; both now
name docs/TCP_COMMANDS.md and docs/internal/DEBUG.md.

LINK AND REFERENCE FIXES

Every inbound reference to a moved file was located by grepping each basename
across the whole tracked tree, not just the markdown. Markdown hrefs in
README.md, CONTRIBUTING.md, docs/ARCHITECTURE.md, docs/COMPILING_OVERLAYS.md,
docs/AOT_OVERLAY_PLAN.md, docs/RELOCATION_MANIFEST_FORMAT.md,
docs/config_schema.md, docs/OVERLAY_XREF.md, docs/TCP_COMMANDS.md and several
docs/internal/ files now point at the new locations; where two files became
siblings under docs/internal/ the path was shortened rather than lengthened
(bios-hardening.md's reference to overlay-discovery.md, for instance).
docs/COMPILING_OVERLAYS.md's "in this folder" phrasing was wrong once two of the
three files it listed moved, so it now names internal/ explicitly.

24 source-comment mentions of ENHANCEMENTS.md sections G1.x across 19 files
(recompiler, runtime, cmake) were retargeted to docs/ENHANCEMENTS.md with the
section numbers untouched. docs/SHADOW_ENHANCEMENTS.md is a DIFFERENT file and
was deliberately excluded.

20 rationale strings citing docs/CASE_A_AOT_GAP.md across the four
recompiler/seeds/*.json files were updated. Done as a byte-level substring
replacement rather than a JSON round-trip: two of those files are stored with
CRLF endings and two with LF, and re-serializing them reformatted 5,700 lines.
The byte-level version changes exactly 20 lines and each file still parses.

References that look similar but point elsewhere were deliberately left alone,
and are worth naming so a future pass does not "fix" them:

  * "recomp-template PRINCIPLES.md" / "NES recomp template PRINCIPLES.md" in
    CLAUDE.md:43, docs/SHADOW_ENHANCEMENTS.md:28,
    docs/internal/HLE_SCHEDULER_CARVEOUT_PLAN.md:5,
    runtime/src/debug_server.c:4934 and tools/parity_diff.py:8 are a different
    repository's file. Only the five that mean this repo's PRINCIPLES.md were
    updated.
  * Every remaining bare ISSUES.md mention is a GAME repo's tracker
    (MegaManX6Recomp 2/7/8/9, TombaRecomp 6), not this repo's.
  * docs/disc-speed.md:4,54,56 and docs/internal/bios-hardening.md:5 name
    overlay-discovery as a BRANCH, not a file, so no path applies.

INDEXES

README.md's "New here?" table gains rows for docs/WIDESCREEN.md,
docs/ENHANCEMENTS.md and docs/TCP_COMMANDS.md. docs/internal/README.md gains
sections indexing all fourteen documents that moved in, plus an "Upstream
contributor reviews" section describing upstream/ and the new sweep file, plus a
"Removed" bullet for CYCLE_TIMING_ARCH.md (deleted in the previous commit)
pointing at FAITHFUL_TIMING_PLAN.md, CYCLE_MODEL_BEETLE.md and
PRECISE_IRQ_SLICE.md -- following the SLJIT_PERSIST_CACHE.md precedent already
in that file.

docs/ENHANCEMENTS.md:7 claimed framework bugs live in "game repos' ISSUES.md
until a framework tracker exists". A framework tracker does exist; it now says
so and names the Beads epic.

VERIFICATION

A link checker was written to scan every tracked *.md for relative links,
resolving each against the tree being scanned. Baseline at origin/master: 129
files, 137 relative links, 3 broken. After both commits: 127 files, 159 relative
links, 3 broken -- the SAME three, all pre-existing and untouched here:

    docs/DATA_SHARDS.md -> INSTANT_LOADS_PARKED.md   (never existed)
    docs/DATA_SHARDS.md -> INSTANT_LOADS_SPIKE2.md   (never existed)
    docs/NETPLAY.md     -> ../lib/recomp-net/README.md  (inside a submodule)

Zero new broken links. The checker's own coverage was checked against a raw grep
for markdown link openers: the 3-link difference is C++ lambdas inside code
blocks, correctly not treated as links.

Also verified: py_compile over all 113 tracked tools/*.py plus psxrecomp_cli.py;
all four seed JSONs still parse; the test-registration guard still reports 123
test files all registered; recompiler/tests/test_coverage_vault.py still passes;
and no file in this commit had its line endings flipped (checked mechanically
across all 50 modified files, after two files were caught and redone).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 07:39:07 -07:00

12 KiB

Audit Inventory — psxrecomp-v4

Baseline as of 2026-05-12. Captures what audit / test tooling exists, what scope it covers, what each tool currently reports, and where the gaps are.

This document is the input for Phase B (codegen-audit expansion) and Phase C (L2-semantic execute tier).

Tier structure

Layer 5  Runtime behavioral differential vs Beetle (continuous RAM diff)    NONE
Layer 4  Hardware-sim conformance (SIO/DMA/MDEC/GTE vs no$psx)              PARTIAL
Layer 3  Codegen / discovery audits (static analysis of generated C)        PARTIAL
Layer 2b L2-semantic — execute emitted code and verify register state       NONE
Layer 2a L2-structural — emit pattern matching on generated C               HAVE
Layer 1  Per-instruction decode correctness vs Rabbitizer                   HAVE
Layer 0  Build & link integrity                                             HAVE

Tools inventory

Layer 1 — Decode conformance

recompiler/tests/l1_decoder_test.cpp (built: recompiler/build/l1_decoder_test.exe)

  • Scope: BIOS only. Code regions Boot 0x00000-0x0DC60, Kernel 0x10000-0x16760, Shell 0x18000-0x42800.
  • Compares MipsDecoder::decode() mnemonic vs Rabbitizer for every word in those regions.
  • Buckets mismatches by oracle_mnem -> our_mnem.

Baseline result: widespread mismatches. Notable buckets:

Count Rabbitizer says We say First addr Comment
181 cache ??? 0xBFC04300 Real gap. CACHE (op 0x2F) is an R3000A instruction we don't decode.
31 mvmva gte 0xBFC34FF8 Granularity. We map all GTE cmds to generic gte.
13 rtpt gte 0xBFC35DF0 Same.
13 nclip gte 0xBFC361B4 Same.
11 tge special? 0xBFC10190 R4000 trap — likely data-in-code or kernel trap encoding.
10 dadd special? 0xBFC101D4 64-bit (R4000+), likely data.
9 rtps gte 0xBFC35DAC GTE granularity.
~ mov.d, mtc1, lwc1, add.d ??? 0xBFC02358 FP — R3000A has no FPU; likely data-in-code in Boot region.

Two distinct classes:

  • Real decode gap: cache (op 0x2F). 181 sites. Decoder needs a case.
  • Granularity gap: GTE sub-mnemonics. Not a correctness bug — our emit treats all GTE cmds via gte_execute. But audit clarity benefits from matching Rabbitizer's specificity.
  • False positives (data-in-code): dadd, dmult, dsll32, mov.d, mtc1, c.eq.d, etc. These are R4000+ / FPU instructions that don't exist on R3000A. They appear because we scan the entire region without skipping inline data or jump tables. Filter improvement, not a decoder bug.

Layer 2a — Structural emit conformance

recompiler/tests/l2_structural_test.cpp (built: l2_structural_test.exe)

  • For each MIPS instruction form, build word, translate via StrictTranslator, verify emitted C contains expected patterns (e.g., gpr[2], gpr[4], << 1u).
  • Snesrecomp-style. Note in source: "does NOT verify semantic correctness (that requires L2-semantic with actual execution)."

Baseline result: 42/42 ok.

Gap: 42 forms / ~150 R3000A instructions ≈ 28% coverage. No coverage for: load-delay-slot semantics, mult/div HI-LO timing, LWL/ LWR/SWL/SWR unaligned partial-word, JALR same-source/dest, shift-by-32 edge case, COP0 RFE / SR-bit semantics.

Layer 2b — Semantic execute conformance

Status: does not exist.

The gap. To know "does our emit do the right thing when run", we'd:

  1. Build a tiny test harness that takes MIPS bytes + initial CPUState, runs the recompiler → C → execute, returns final CPUState.
  2. For each instruction, supply known input registers and assert output matches the R3000A spec.
  3. Cross-check against mednafen-PSX's CPU as a reference impl.

This is Phase C.

Layer 3 — Codegen / discovery audits

tools/_codegen_audit.py (BIOS only)

  • 6 passes against generated/SCPH1001_full.c + _dispatch.c:
    1. Predicate-init audit (verifies 2026-05-04 fix).
    2. Direct psx_dispatch(cpu, 0xXXXX) targets in dispatch table.
    3. Indirect psx_dispatch(cpu, gpr[N]) distribution.
    4. Tail-call cpu->pc = ...; return; targets in dispatch table.
    5. Chained-branch-with-delay-slot-label population.
    6. Highest-frequency missing dispatch targets.

Baseline result:

  • Predicate decl audit: buggy=0 zero_init=6104 assigns=6104 ✓
  • Direct psx_dispatch: 796 unique, 0 missing ✓
  • Indirect sites: 88 (across many registers)
  • Tail-call missing: 1 site → 0x00000CF0 — the install-at-runtime SIO data-byte handler stub. Expected, handled by dirty-RAM interpreter. Not a regression.
  • Chained-branch with delay-slot label: 27 sites, all protected ✓

tools/gte_audit.py (BIOS only)

  • Scans ROM for COP2/GTE instructions, compares against generated code.

Baseline result: "missing" hits in 0xBFC75xxx..0xBFC7Fxxx. These are in the BIOS DATA region (code ends at 0xBFC42800). All false positives from scanning data bytes that happen to decode as COP2 ops. Need to filter by code region.

tools/scan_branch_delay_hazards.py (BIOS only)

  • For every conditional branch in walked code, checks whether the delay slot writes to the branch's rs/rt operand.
  • Verifies strict_translator.cpp uses the snapshot pattern.

Baseline result:

  • Branches scanned: 4617
  • Delay slot writes branch operand: 441 (intrinsic, informational)
  • Incorrect in current translator: 0 ✓
  • Delay-slot unreachable in walk: 0 ✓

tools/check_dispatch.py — one-off probe with hardcoded address list. Not generally useful. Skip from active inventory.

Layer 4 — Hardware-sim conformance

runtime/tests/test_sio_card_protocol.c

  • Drives sio.c TX/RX state machine through a 137-byte memcard READ command, asserts each RX byte matches no$psx.
  • Not run as part of this Phase A pass (built but not invoked here).

Gap: other MMIO regions — DMA, MDEC, GPU command stream, timers, IRQ controller — have no protocol conformance tests.

Layer 5 — Runtime differential

Does not exist. Both psx-runtime (port 4370) and psx-beetle (port 4380) speak the same wire protocol, so the infrastructure for paired wtrace queries is present; what's missing is the diff-and-cluster tool itself.

Tool generalization status

All five Layer 1-3 tools are hardcoded to BIOS paths. They were written during Phase 1-3 BIOS bring-up and have not been generalized to game code. For Phase B, generalize them via:

  • Argument: --full-c PATH --dispatch-c PATH --rom PATH (or read from game.toml-style config).
  • Run each tool once for BIOS, once for Tomba.

Phase A findings — what to do with them

Finding Class Priority Phase
L1: cache (op 0x2F) not decoded — 181 sites Real decoder gap High Phase B
L1: GTE sub-mnemonic granularity Audit clarity Low Phase B
L1: FPU / 64-bit FPs in data regions Audit filter Low Phase B
L2: 42 forms / ~150 instructions = 28% coverage Test gap High Phase C
L2-semantic tier does not exist Architectural High Phase C
All audits BIOS-only Tool scope High Phase B
gte_audit.py false-positives in data region Audit filter Low Phase B
Tail-call miss at 0x00000CF0 Expected stub None —

Progress log

Phase B2 (2026-05-12) — First audit pass against Tomba

Ran all three Python audits against Tomba's SCUS_942.36. Mixed outcome — tool gaps surfaced and real bugs surfaced.

Initial run findings:

  • codegen_audit.py returned all zeros — its regexes are BIOS-emit- specific and don't match the game full_function_emitter shape (direct func_X(cpu) calls, switch-based dispatch, _bc_X branch conds). Required a sibling tool.
  • gte_audit.py reported 29 findings in Tomba text segment (8 GTE + 21 LWC2/SWC2). See TombaRecomp ISSUES Issue #2.
  • scan_branch_delay_hazards.py correctly skipped walker (no function_starts file for Tomba) and ran translator-source check.

codegen_audit_game.py created (per the principle: separate tool sharing patterns only at the margins). Audits the 6 passes that matter for game-emit style. After fixing my dispatch-table regex (Tomba uses case 0xADDRu: func_X(cpu); return 1; not BIOS's array form) and adding code-region filtering for call_by_address literals (331 of 333 are RAM-loaded overlays handled by dirty_ram_dispatch, expected):

Pass Result
1. Direct func_X(cpu) calls 2,973 calls, 840 unique → all resolve ✓
2. Literal call_by_address 333 unique → 2 in-code misses ⚠
3. Indirect call_by_address 360 sites (informational)
4. Tail-call cpu->pc=X 0 sites (game emit doesn't use this idiom) ✓
5. Branch-cond declarations 6,708 (informational)
6. goto block_X resolution 20,172 sites, all resolve ✓

Real bug surfaced: Issue #5 — mid-function split targets not registered in dispatch table. Three emit sites in code_generator.cpp (lines 970, 981, 998) emit call_by_address for mid-func continuations without calling register_cross_function_target. This is the same bug class as today's jump-table fix. Tomba has 2 manifestations (0x800905E4, 0x80090600 inside func_800905DC). B2's first pass against Tomba found a real recompiler bug.

Phase B0 (2026-05-12) — Audit-config schema

Defined a TOML schema for audit configs. Both BIOS and games consume the same shape; field names are tolerant ([program] or [game], rom or exe).

Artifacts:

  • tools/audit_config.py — shared loader.
  • tools/verify_audit_schema.py — schema validator.
  • bios/SCPH1001.toml — BIOS config (3 regions, 2 address remaps).
  • TombaRecomp/game.toml — extended with [audit] block (1 region, 0 remaps).

Phase B1 (2026-05-12) — Python audit generalization

Migrated three Python audits to consume the audit-config schema. All three pass BIOS regression with output matching the Phase A baseline.

Tool Status BIOS regression
tools/codegen_audit.py migrated identical to Phase A (CLEAN, 0 real findings)
tools/gte_audit.py migrated 73 false-positives eliminated by code-region filter; 7 real findings surfaced
tools/scan_branch_delay_hazards.py migrated identical to Phase A (CLEAN); also runs translator-source check for non-BIOS configs that lack a function_starts file
recompiler/tests/l1_decoder_test.cpp deferred C++ TOML parsing not yet wired; the test still runs against hardcoded BIOS kRegions[]. Generalize in a later sub-phase.
recompiler/tests/l2_structural_test.cpp not needed no per-program input; already generic.

Side effects of generalization:

  • gte_audit.py surfaced 7 previously-masked findings in Shell code: 4 missing gte_execute emits at 0xBFC34FF8, 0xBFC3502C, 0xBFC35064, 0xBFC350C4 (all 0x4A480012 = MVMVA), plus 3 missing LWC2/SWC2 emits. None of these PCs appear in generated/SCPH1001_full.c — either a discovery gap (function containing them not recompiled) or an emit gap. Investigate in Phase B3 or B4.

Methodology going forward

Every new bug class encountered should follow:

  1. Reproduce / characterize the class (not the single instance).
  2. Build the audit FIRST — find every instance of the class in the codebase via tooling, not by manual repro of each.
  3. Fix the underlying recompiler / runtime issue.
  4. Re-run the audit, confirm class is now empty.
  5. Add the audit to the standing audit suite so the class can't regress.

The audit suite forms the regression net: "0 findings, audited N sites" is the only acceptable state for a known class.