Files
psxrecomp/docs/internal/audit_inventory.md
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

253 lines
12 KiB
Markdown

# 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.