- per-file -O0 mechanism: split text c-subseg into boot@-O0 (src/boot.c) + 800@-O2 via splat resegmentation + Makefile target-specific CC1FLAGS; the boot/main/ game-mode-dispatch module (0x80010000-0x800123F0) is -O0, not the pinned -O2 (per-module compiler mixing, SETUP §5.5). Regression-gated byte-identical. - 4 byte-matches (38->42 real, make check BYTE-IDENTICAL throughout): GameModeDispatch (-O0 register-ptr far member), DebugMenuHandler (-O0 reserved-slot local), CdQueueBusy (-O2 if/else order + branch polarity), CdReadRequest (-O2 early-return fall-through) - PsyQ infra: include/psyq/libcd.h (CdlLOC/CdlFILE from the .gdt) + 4 named symbols (CdSearchFile/CdPosToInt/CdIntToPos/VSync) in symbols.us.txt; func_* stubs renamed - loader cluster non-jtbl COMPLETE (6 matched + 2 drafted): NON_MATCHING drafts of LoaderInitFileTable + ResourceLoadStateMachine (logically faithful, residuals in-source) - tooling: progress.py/difficulty.py fixed for the multi-file split (glob src/*.c, all asm dirs, skip forward-decls); deterministic; 42 real / 4 NM - flywheel: matching-cookbook §6 (-O0 detection + idioms), §7 (PsyQ types/symbols), T4 - build byte-identical 143dbb89f34491258bbc27810d0a12ec8b43a8dd from a full clean cycle
8.8 KiB
CURRENT PHASE — Phase 7: Matching loop at scale + Gen1 close-out
Status: Plan APPROVED 2026-06-14. REORDERED 2026-06-14 (Drew): bank non-switch wins first; the
rodata-island foundation + LZSS match are DEFERRED to a focused sub-project after the easier tasks.
Full plan: ~/.claude/plans/confirmed-plan-mode-enabled-jolly-lemon.md. Project version: 1.6.0 → 1.7.0 at PhaseEnd.
In-phase crash/compaction recovery log (P3 step 4). Update after every task.
Locked decisions (developer-confirmed)
- ≥25 = real substantive matches (the 42 splat-auto empties do NOT count). 14 real now → need ≥11 more.
- Loader cluster = match-tractable / draft-hard (NON_MATCHING-draft the hard state machines).
- REORDER (2026-06-14): rodata foundation hit a structural wall (see below); do reports + harvest + non-switch loader FIRST, then a focused LZSS/rodata sub-project. LZSS is still required for Gen1 exit.
Verified baseline (grounded; R14 corrections)
- Build byte-identical (
143dbb89f34491258bbc27810d0a12ec8b43a8dd), reproducible. Only change from committed Phase-6 = one R15 symbol line (func_80047CAC = 0x80047CAC; // data) — fixes a LATENT NON-REPRODUCIBILITY: spimdisasm 1.41.0 auto-detection of that 8-byte inter-fn blob is unstable across clean extracts; declaring it makesmake clean && make extract && make builddeterministic. (Note for PhaseEnd.) - Switch cohort ~46 funcs / 53 tables (51 in-image, single-ref/migratable). LzssDecodeSector 0x80018730 = gcc switch via
jtbl_80072A38(write-free rodata island 0x80072A38–0x80074750 / file 0x63238–0x64F50). - All 13 loader-cluster funcs in text region (< 0x800629DC). Authoritative counts: 14 real / 42 empty / 2 NON_MATCHING / 1998 stubs / 2050 matchable.
Task checklist (REORDERED — execution order top to bottom)
- Task 3 — Report machinery ✓ DONE.
tools/{progress,difficulty,dup_report}.py+make report(deterministic) +make sig-refresh. Authoritative baseline 14 real / 42 empty / 2 NM / 1992 stubs / 2050 matchable (REAL/matchable=0.68%); empties audit 42/42 clean; dup leverage 8 h_exact + 44 h_norm redundant; harvest queue 1119 non-jtbl leaves + 313 trivial. Digests →docs/{progress,difficulty,duplicates}.md. - Task 4 — Harvest to ≥25 real ✓ DONE. 36 real matches (14 + 22 trivial accessor leaves: getters/setters of globals, one mask, one two-store), build BYTE-IDENTICAL → all 22 byte-perfect. ≥25 bar PASS (margin 36/25).
.run/harvest.py= the applier. Dedup-collapse skipped (the sig dup groups are mostly PsyQ library fragments/epilogues, not real funcs). LZSS still required (Task 2′). - Task 5.5 — Per-file -O0 split mechanism ✓ DONE (session B, Drew-approved). MAJOR FINDING: per-module opt mixing is real (§5.5). The boot/main/game-mode-dispatch module @ vram 0x80010000–0x800123F0 (~50 funcs incl. start/main/GameModeDispatch/DebugMenuHandler) is -O0, not the pinned -O2; everything ≥0x800123F0 is -O2. Mechanism: split text c-subseg →
[0x800,c,boot]→src/boot.c(-O0 via Makefile target-specific CC1FLAGS) +[0x2BF0,c,800]→src/800.c(-O2, matched fns untouched). Regression gate PASS (byte-identical at 100% INCLUDE_ASM). Cookbook §6 added (detect via21F0A003frame-ptr sig;register-pointer far-member idiom). Reusable for every future flag-divergent module. - Task 5.6 — PsyQ CD struct header ✓ DONE (session B).
include/psyq/libcd.h(CdlLOC 4B + CdlFILE 24B, offsets from Ghidra/LIBCD.H, G1) + 3 libcd protos; added 4 PsyQ symbols toconfig/symbols.us.txt(CdSearchFile/CdPosToInt/CdIntToPos/VSync — Ghidra-confirmed, R15) and renamed theirfunc_*stubs in src/800.c. Build BYTE-IDENTICAL (label-only); header compiles, sizeof asserts hold. Unlocks the file-loader cluster (LoaderInitFileTable, ResourceLoadStateMachine, the jtbl loaders). Cookbook §7 added (PsyQ types/symbols pattern). NB symbols.us.txt now has 983 lines (4 appended, un-sorted — re-merge note for R15). - [~] Task 5 — Loader cluster PARTIAL (42 real total; non-jtbl 5/7 matched). Matched (score 0, byte-identical): ResourceGetCdLoc (15), LoaderResetReadState (32), GameModeDispatch (29, -O0
register-ptr far member), DebugMenuHandler (30, -O0, reserved-slot local), CdQueueBusy (35, -O2 — if/else order + branch polarity), CdReadRequest (53, -O2 — early-return fall-through). Triage of the rest:- jtbl → Task 2′ (rodata-gated): CdReadStateMachine (385), CdReadSectorReadyCB (424, sole LZSS caller), SaveLoadRoutine (1139), StreamLoadStateMachine (459).
- non-jtbl COMPLETE (6 matched + 2 drafted): LoaderInitFileTable (133) + ResourceLoadStateMachine (211) → NON_MATCHING-drafted (logically faithful, libcd.h-typed; per-function residual notes in src/800.c — LoaderInit: regalloc/loop-invariant hoisting + name=base-0x14; ResourceLoad: block placement (state 0→1→2→3 layout vs nested-if) + reserved-local frame 0x38 vs 0x28 + func_8002D4C8 arg & 0xFFFF). NON_MATCHING count now 4. Byte-match both in a later structural/permuter pass.
- Report tooling fixed for the multi-file split:
tools/{progress,difficulty}.pynow glob allsrc/*.c+ search allasm/nonmatchings/*subdirs, andprogress.pyskipsextern …(…);forward-declarations (they were swallowing the next fn + double-counting). Deterministic; 42 real / 1964 stubs consistent across both. (Note for PhaseEnd.) - Task 2′ — Focused LZSS + rodata-island sub-project (DEFERRED hard task; Gen1-exit LZSS gate). Max. See investigation findings below.
- Task 6 — Gen1-exit close-out (README, checklist, ≥3-session zero-regression evidence). xHigh.
- Task 7 — PhaseEnd_Phase7 (Gen1 synthesis, milestone gate). Max · Tier 1.
Current task
Task 5 — Loader cluster (match-tractable non-switch / draft-hard). Then Task 2′ (LZSS), Task 6, Task 7.
NOTE: Gen1 exit needs ≥3 SESSIONS of green make check — cannot complete this session regardless.
Per-session make check green log (≥3 sessions needed for the milestone)
- 2026-06-14 (session A):
make check→143dbb89… BYTE-IDENTICAL✓ — baseline restored + reproducibility fix, reports built, 38 real matches (22 accessor leaves + ResourceGetCdLoc + LoaderResetReadState), build byte-identical throughout. [need ≥2 more sessions] - 2026-06-14 (session B):
make check→143dbb89… BYTE-IDENTICAL✓ — per-file -O0 split mechanism (src/boot.c + Makefile per-file flags); 4 real matches (GameModeDispatch, DebugMenuHandler, CdQueueBusy, CdReadRequest) → 42 real; PsyQ libcd.h infra (CdlLOC/CdlFILE + 4 named symbols, unlocks the loader cluster); LoaderInitFileTable + ResourceLoadStateMachine NON_MATCHING-drafted (→ 4 NM) — Task 5 non-jtbl loaders COMPLETE (6 matched + 2 drafted); report tooling fixed (multi-file); cookbook §6/§7/T4. Build byte-identical throughout. [need ≥1 more session]
Rodata-island foundation — investigation findings (DEFERRED, for Task 2′)
Attempted the 3-way data split ([data front][.rodata island][data tail] + ld_legacy_generation: True).
What works: dotted .rodata sibling named 800 → spimdisasm MIGRATES each jump table into its owning
function's .s (jtbl_80072A38 lands inside LzssDecodeSector.s with .section .rodata/.section .text),
references resolve intra-800.o, build LINKS. Non-migrated multi-ref rodata becomes INCLUDE_RODATA (70 lines)
in a FRESH-regenerated 800.c.
What blocks byte-identity (the structural wall):
- Adding any
rodatasubseg turns on global jumptable analysis → merges 56 over-split switch fragments (good) but DROPS an 8-byte inter-fn blob (thefunc_80047CACissue, now fixed via explicit symbol). - A separate rodata object can't link to text-local
.L/jlabeljumptable targets → must migrate (co-locate). - splat places sections CONTIGUOUSLY (no explicit
. = addr), so any size drift shifts the whole image. Observed a 24-byte.textoverrun (main_TEXT_END0x800629F4 vs 0x800629DC) → +24B size, 85288 bytes differ. Root cause of the 24B NOT fully pinned. Candidate fix for Task 2′: explicit linker addresses (Makefile post-extract.ld-patch placing.text@0x80010000, front.data@0x800629DC,800.o(.rodata)@0x80072A38, tail.data@0x80074750) + scope migration so only island tables land in800.o(.rodata). Alternative: sotn-style per-file split (Gen2-scale). Reproduce with the migration config (in git stash / reconstruct from this log).
Blockers / open items
- Task 2′ ld-placement mechanism (above). MCP-mode batching: sig-refresh needs MCP stopped, LZSS needs MCP live.
Notes
- Commits accumulate UNCOMMITTED; one phase-end commit by the developer (R8/R6).
.run/merge_matches.py= the regenerate-800.c + re-apply-matches helper (reusable for Task 2′).