From 8ffb7f0607bb9bbbebb3013b91125e9e2e93acfd Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:37:05 -0600 Subject: [PATCH] =?UTF-8?q?feat(phase-7):=20-O0=20split=20+=204=20matches?= =?UTF-8?q?=20(42=20real)=20+=20libcd.h=20=E2=80=94=20session=20B=20checkp?= =?UTF-8?q?oint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Makefile | 8 + config/splat.us.exe.yaml | 9 +- config/symbols.us.txt | 4 + docs/difficulty.md | 4 +- docs/matching-cookbook.md | 89 ++++++++ docs/progress.md | 14 +- include/psyq/libcd.h | 34 ++++ phase-ends/CURRENT_PHASE.md | 8 +- src/800.c | 392 +++++++++++++++++++++++++----------- src/boot.c | 142 +++++++++++++ tools/difficulty.py | 38 ++-- tools/progress.py | 88 ++++---- 12 files changed, 652 insertions(+), 178 deletions(-) create mode 100644 include/psyq/libcd.h create mode 100644 src/boot.c diff --git a/Makefile b/Makefile index 06168c7c8..c40f4fe56 100644 --- a/Makefile +++ b/Makefile @@ -208,6 +208,14 @@ build/src/%.o: src/%.c @echo " CC $@" @set -o pipefail; $(CPP) $(CPPFLAGS) $< | $(CC1_PSX) $(CC1FLAGS) | $(VENV_PY) $(MASPSX) --aspsx-version=$(ASPSX_VERSION) $(MASPSX_FLAGS) | $(AS) $(ASFLAGS) -o $@ +# Per-module optimization override (SETUP §5.5 — per-module compiler mixing). The boot/ +# main/game-mode-dispatch module (src/boot.c, vram 0x80010000-0x800123F0) was compiled at +# -O0, NOT the -O2 game-code default: frame-pointer setup + unfolded large-offset loads +# are the evidence (GameModeDispatch byte-matches only at -O0). gcc 2.7.2 has no +# per-function optimize pragma, so opt level is per-file. Target-specific CC1FLAGS (the +# pattern recipe reads $(CC1FLAGS), so this overrides it for just build/src/boot.o): +build/src/boot.o: CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker + # link (the .ld pulls in the .o by path) + objcopy to the raw PS-X EXE image. $(OUT): $(OBJS) $(LD_SCRIPT) @set -e diff --git a/config/splat.us.exe.yaml b/config/splat.us.exe.yaml index cc353501e..cc65a28c8 100644 --- a/config/splat.us.exe.yaml +++ b/config/splat.us.exe.yaml @@ -66,6 +66,13 @@ segments: # no-op at every real boundary here. align: 4 subsegments: - - [0x800, c, 800] # text — Phase 6: c-scaffold (src/800.c INCLUDE_ASM stubs); matched fns replace stubs + # Per-module optimization mixing (SETUP §5.5): the boot/main/game-mode-dispatch + # module (vram 0x80010000-0x800123F0) was compiled at -O0; the rest of the text at + # -O2. Split into two c-subsegments so the Makefile applies per-file flags + # (build/src/boot.o is overridden to -O0). The split is byte-identical at 100% + # INCLUDE_ASM (Phase-7 regression gate) — opt level only affects matched C, not stubs. + # Boundary = func_800123F0 (vram 0x800123F0 -> file vram-0x8000F800 = 0x2BF0). + - [0x800, c, boot] # -O0 boot module -> src/boot.c (vram 0x80010000-0x800123F0) + - [0x2BF0, c, 800] # -O2 remainder -> src/800.c (vram 0x800123F0-0x800629DC); name "800" kept (legacy) so the 38 matched fns + their asm/nonmatchings/800 paths don't migrate; NB start is now 0x2BF0, not 0x800 - [0x531DC, data, 531DC] # data — psxexeinfo boundary; round-trips byte-identical - [0x65000] diff --git a/config/symbols.us.txt b/config/symbols.us.txt index c572b6c0d..9bdf80f26 100644 --- a/config/symbols.us.txt +++ b/config/symbols.us.txt @@ -977,3 +977,7 @@ resLoad_result = 0x800C6D10; // data resLoad_curId = 0x800C6D34; // data lzss_state = 0x800C7D24; // data listCdBuffer = 0x80180000; // data +CdIntToPos = 0x80043A18; // func PsyQ libcd (Ghidra-confirmed, Phase 7) +CdPosToInt = 0x80043B1C; // func PsyQ libcd (Ghidra-confirmed, Phase 7) +CdSearchFile = 0x80045374; // func PsyQ libcd (Ghidra-confirmed, Phase 7) +VSync = 0x8004239C; // func PsyQ libetc (Ghidra-confirmed, Phase 7) diff --git a/docs/difficulty.md b/docs/difficulty.md index a9fbfdb1b..de92f8e4c 100644 --- a/docs/difficulty.md +++ b/docs/difficulty.md @@ -1,8 +1,8 @@ # Unmatched difficulty inventory (generated by tools/difficulty.py — harvest queue) -unmatched functions : 1968 +unmatched functions : 1962 trivial (<=5 ins) : 291 -non-jtbl leaves : 1095 (best harvest targets) +non-jtbl leaves : 1093 (best harvest targets) jump-table funcs : 84 (deferred — need the rodata-island workflow, Task 2') ## Easiest 120 unmatched (score asc) — the work queue diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md index 434dd7553..87e33f740 100644 --- a/docs/matching-cookbook.md +++ b/docs/matching-cookbook.md @@ -73,6 +73,21 @@ Open example: `func_80015A74` residual (counter init vs hoisted magic constant). `lb` (sign-extend). Pick the load width/sign that matches the asm, then layer `& 0xff` (I2) / casts as needed. +### T4 — Branch polarity: invert the source condition to flip gcc's chosen branch +Two source forms can be logically identical but emit **opposite** branches: +`if (x & m) return A; return B;` vs `if ((x & m) == 0) return B; return A;`. gcc -O2 picks one +polarity (beqz vs bnez); it may be the opposite of the target. Symptom in asm-differ: the right +structure but a lone **beqz↔bnez flip with the two return constants swapped** between the branch +and its `j`/delay slot. Fix: rewrite the condition with the other polarity. Also: which arm of an +`if/else` becomes the fall-through follows source order — put the target's fall-through block in the +`if`, the branched-to block in the `else` (e.g. `if (a != b){…} else {…}` if the `==` block sits +last). Example: `CdQueueBusy` (1405 → 210 via if/else order, 210 → 0 via the 0x20 polarity flip). +Likewise multi-exit functions: write the **success/main return LAST** (it becomes the fall-through +into the shared epilogue) and error cases as **early `return`s** (they branch in). Reversing this — +`if (ok) { … return good; } return 0;` — makes `return 0` the fall-through and duplicates the +`j epilogue`/`move v0,zero` tail. Example: `CdReadRequest` (305 → 0 by flipping to `if (busy) return 0; +… return cdReq_result;`). + --- ## §3 When a diff is pure scheduling → decomp-permuter (harness built, Phase 6) @@ -118,3 +133,77 @@ as decomp-permuter candidates rather than hand-grinding. - **Hoisted-invariant vs IV-init ordering** — a loop-invariant load scheduled before/after the counter init; not reachable by C-source changes (permuter stuck at base). Needs `PERM_*` or insight. Example: `func_80015A74` (uint→BCD). See §3. + +--- + +## §6 Per-module optimization mixing — the -O0 boot module (Phase 7) +**Finding (2026-06-14):** the EXE mixes optimization levels per original translation unit (the +§5.5 Xenogears-style mixing, now concrete). The **boot/main/game-mode-dispatch module** — a clean +contiguous block at **vram 0x80010000–0x800123F0** (~50 funcs: `start`, `main`, `GameModeDispatch`, +`DebugMenuHandler`, the game-mode handlers) — was compiled at **-O0**. Everything from 0x800123F0 +onward (every match so far + the file-loader cluster) is **-O2**. **Always opt-fingerprint a new +function before writing C** — the pinned `-O2` is NOT global. + +### Detecting the opt level (do this first) +gcc 2.7.2 **-O0 keeps a frame pointer**: `addu $fp,$sp,$zero` (`21F0A003`) in the prologue + +`addu $sp,$fp,$zero` in the epilogue; **-O2 omits it**. Grep the target `.s`: +`grep -l 21F0A003 asm/nonmatchings//.s` → hit = **-O0**, miss = -O2. Module scan: classify +every `.s` by that signature, sort by address; the contiguous -O0 run is the module (the boot block +is the one early -O0 run). Other -O0 tells: redundant `move`/`addu rd,rs,$zero` copies; a `nop` after +every load (no load-delay scheduling); single-use values parked in callee-saved `s0..`; large +constant member offsets left **unfolded** (`la $reg,sym` + `lhu off($reg)`), where -O2 folds +`sym+off` into one load. + +### -O0 idiom — far struct member via a `register` base pointer +Target: `lui s0,%hi(BASE); addiu s0,s0,%lo(BASE); lui at,1; addu at,s0,at; lhu v0,-0x5c52(at)` = +load a u16 at `BASE + 0xA3AE`. The `lui 1 / addu / -0x5c52` is just `as` expanding a register-relative +load whose offset (0xA3AE) exceeds 0x7FFF (%hi=1, %lo=-0x5C52). C — a **`register`-qualified pointer** +to the base, then offset-deref: +```c +extern u8 BASE[]; +register u8 *p = BASE; +... *(u16 *)(p + 0xA3AE) ... /* base stays in a callee-saved reg; offset left unfolded */ +``` +`register` is **load-bearing**: drop it and -O0 spills the pointer (extra sw/lw, bigger frame); and +-O1/-O2 fold it all back to `lhu sym+off` (no base reg, no frame pointer). Plain `BASE[idx]` or +`((struct*)BASE)->m` also **fold** at -O0 → wrong. Example: `GameModeDispatch` (0x80010B40) = +`gameModeHandlerTable[*(u16*)(p+0xA3AE)]()` — byte-exact (asm-differ 0). + +### -O0 idiom — a reserved (unstored) local sets the frame size +A named local the original declares but our toolchain wouldn't store (e.g. a call result the +original checks directly) still **reserves its 8-byte stack slot** at -O0, enlarging the frame. +If a near-match differs **only** by frame size + a uniform save-offset shift (every instruction +identical), add the missing local as a **declaration-only** `int x;` + `(void)x;` — no store, no +load, no `-Wall` noise, no code, just the slot. (Assigning the result to the local instead emits a +`sw`/`lw` pair the target lacks.) Example: `DebugMenuHandler` (0x80011144) — `if (CdReadRequest(...) +!= 0)` with a reserved `int iVar1;` → frame 0x20 (score 42 → 0). + +### Build mechanism — per-file opt override (splat resegmentation) +One original .c = one opt level; you can't mix within a compile unit, and gcc 2.7.2 has no +per-function optimize pragma. **Split the module into its own splat c-subsegment** and give that +object its flags: +- `config/splat.us.exe.yaml`: split the text subseg at the module boundary (a function start; file + off = vram − 0x8000F800). Boot module = `[0x800, c, boot] → src/boot.c`; the rest stays + `[0x2BF0, c, 800] → src/800.c` (name kept to avoid migrating matched C). +- `Makefile`: target-specific override — `build/src/boot.o: CC1FLAGS := …-O0…` (the pattern recipe + reads `$(CC1FLAGS)`, so this overrides just that object). +- **Regression gate:** the split must rebuild **byte-identical at 100% INCLUDE_ASM** before any -O0 C + is added (opt level only affects matched C, not stubs). Verified for the boot split. +Reuse this hook for any future module whose flags differ (another opt level, bare `divu`, etc.). + +--- + +## §7 PsyQ SDK types & symbols (the library-call prerequisite) +A function that calls PsyQ library routines needs both the SDK **types** and the library **symbols**: +- **Types:** pull the EXACT layout from Ghidra's imported `.gdt` (`mcp__ghidra__types get `, + category e.g. `/LIBCD.H`) — never guess offsets (G1). Declare in `include/psyq/.h` (it + `#include "common.h"` for u8/u32, guard-safe). Verify sizes with a compile-time assert: + `typedef char a[sizeof(T)==N ? 1 : -1];`. +- **Symbols:** PsyQ fns are already named in the Ghidra DB but absent from our exported + `config/symbols.us.txt`, so the build shows them as `func_`. Add `Name = 0xADDR; // func` + to `symbols.us.txt` (R15) AND rename the matching `INCLUDE_ASM("…", func_)` stub(s) in + `src/` to the canonical name (splat won't rewrite a committed `.c`). Re-extract → + **byte-identical** (label-only change). No matched C may already reference the old name. +- Done Phase 7: `include/psyq/libcd.h` (CdlLOC 4B, CdlFILE 24B + CdSearchFile/CdPosToInt/CdIntToPos + protos) + the 4 libcd/libetc symbols — unlocks the file-loader cluster. Same pattern for + libgpu/libgte/libspu as they come up. diff --git a/docs/progress.md b/docs/progress.md index 30a65b060..2a35387b3 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -1,17 +1,17 @@ # BFM matching progress (generated by tools/progress.py — authoritative) -REAL substantive matches : 38 <- the Gen1-exit >=25 bar counts THIS -NON_MATCHING (near-miss) : 2 +REAL substantive matches : 42 <- the Gen1-exit >=25 bar counts THIS +NON_MATCHING (near-miss) : 4 splat-auto empty no-ops : 42 -INCLUDE_ASM stubs : 1968 +INCLUDE_ASM stubs : 1962 data blobs (excluded) : 4 ---------------------------------------- matchable functions : 2050 -REAL / matchable : 38 / 2050 = 1.85% -incl. empties : 80 / 2050 = 3.90% +REAL / matchable : 42 / 2050 = 2.05% +incl. empties : 84 / 2050 = 4.10% -REAL matches: LoaderResetReadState ResourceGetCdLoc func_80012AB0 func_800168B4 func_80018F20 func_80019018 func_80019198 func_80019378 func_80019388 func_80019398 func_800193A8 func_8001AA78 func_8001AA88 func_8001B22C func_8001B374 func_8001B384 func_8001B85C func_8001BE20 func_8001BFA0 func_8001BFE8 func_8001D0E8 func_80029254 func_80029264 func_80029504 func_80029514 func_8002953C func_8002954C func_8002957C func_8002958C func_80029FD4 func_8002A26C func_8002A27C func_8002A4B8 func_8002A4C8 func_8002A728 func_8002A738 func_8002A998 func_8002A9A8 -NON_MATCHING: func_80015A74 func_80016714 +REAL matches: CdQueueBusy CdReadRequest DebugMenuHandler GameModeDispatch LoaderResetReadState ResourceGetCdLoc func_80012AB0 func_800168B4 func_80018F20 func_80019018 func_80019198 func_80019378 func_80019388 func_80019398 func_800193A8 func_8001AA78 func_8001AA88 func_8001B22C func_8001B374 func_8001B384 func_8001B85C func_8001BE20 func_8001BFA0 func_8001BFE8 func_8001D0E8 func_80029254 func_80029264 func_80029504 func_80029514 func_8002953C func_8002954C func_8002957C func_8002958C func_80029FD4 func_8002A26C func_8002A27C func_8002A4B8 func_8002A4C8 func_8002A728 func_8002A738 func_8002A998 func_8002A9A8 +NON_MATCHING: LoaderInitFileTable ResourceLoadStateMachine func_80015A74 func_80016714 build SHA1: 143dbb89f34491258bbc27810d0a12ec8b43a8dd (byte-identical) diff --git a/include/psyq/libcd.h b/include/psyq/libcd.h new file mode 100644 index 000000000..c054a4c58 --- /dev/null +++ b/include/psyq/libcd.h @@ -0,0 +1,34 @@ +/* psyq/libcd.h — PSY-Q libcd (CD-ROM) types & prototypes used by the file loader. + * + * Struct layouts are the authoritative ones from the imported PsyQ 4.0 .gdt + * (Ghidra category /LIBCD.H) — verified offsets/sizes, not guessed (G1). Function + * prototypes use the canonical PsyQ signatures; their symbols are pinned in + * config/symbols.us.txt (and already named in the Ghidra DB). Grow this as more + * libcd surface is matched. Companion to common.h (provides the u8/u32 typedefs). + */ +#ifndef PSYQ_LIBCD_H +#define PSYQ_LIBCD_H + +#include "common.h" + +/* CD logical location — minute/second/sector are BCD. 4 bytes. (/LIBCD.H) */ +typedef struct { + u8 minute; /* +0x00 */ + u8 second; /* +0x01 */ + u8 sector; /* +0x02 */ + u8 track; /* +0x03 */ +} CdlLOC; + +/* CD file descriptor (CdSearchFile result / directory entry). 24 bytes. (/LIBCD.H) */ +typedef struct { + CdlLOC pos; /* +0x00 file start location */ + u32 size; /* +0x04 file size in bytes */ + char name[16]; /* +0x08 file name */ +} CdlFILE; + +/* libcd prototypes (symbols in config/symbols.us.txt). */ +extern CdlFILE *CdSearchFile(CdlFILE *fp, char *name); /* 0x80045374 */ +extern int CdPosToInt(CdlLOC *p); /* 0x80043B1C */ +extern CdlLOC *CdIntToPos(int i, CdlLOC *p); /* 0x80043A18 */ + +#endif /* PSYQ_LIBCD_H */ diff --git a/phase-ends/CURRENT_PHASE.md b/phase-ends/CURRENT_PHASE.md index 69db21f78..4f5fb2af2 100644 --- a/phase-ends/CURRENT_PHASE.md +++ b/phase-ends/CURRENT_PHASE.md @@ -23,9 +23,12 @@ rodata-island foundation + LZSS match are DEFERRED to a focused sub-project afte ## Task checklist (REORDERED — execution order top to bottom) - [x] **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`. - [x] **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 — Loader cluster** PARTIAL. Matched: **ResourceGetCdLoc** (15, table lookup), **LoaderResetReadState** (32, store seq) → byte-identical. Triage of the rest: +- [x] **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 via `21F0A003` frame-ptr sig; `register`-pointer far-member idiom). Reusable for every future flag-divergent module. +- [x] **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 to `config/symbols.us.txt` (CdSearchFile/CdPosToInt/CdIntToPos/VSync — Ghidra-confirmed, R15) and renamed their `func_*` 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, need Ghidra+iteration:** GameModeDispatch (29, base+offset addr + callee-saved across jalr), DebugMenuHandler (30), CdReadRequest (53), LoaderInitFileTable (133), ResourceLoadStateMachine (211), CdQueueBusy (35). Match or NON_MATCHING-draft in continuation. + - **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}.py` now glob all `src/*.c` + search all `asm/nonmatchings/*` subdirs, and `progress.py` skips `extern …(…);` 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.** @@ -36,6 +39,7 @@ NOTE: Gen1 exit needs ≥3 SESSIONS of green `make check` — cannot complete th ## 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] --- diff --git a/src/800.c b/src/800.c index 7bb78f71b..8844caaea 100644 --- a/src/800.c +++ b/src/800.c @@ -1,114 +1,5 @@ #include "common.h" - -INCLUDE_ASM("asm/nonmatchings/800", start); - -INCLUDE_ASM("asm/nonmatchings/800", func_800100A0); - -INCLUDE_ASM("asm/nonmatchings/800", __do_global_dtors); - -INCLUDE_ASM("asm/nonmatchings/800", main); - -INCLUDE_ASM("asm/nonmatchings/800", func_8001096C); - -INCLUDE_ASM("asm/nonmatchings/800", func_8001099C); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010A08); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010A98); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010AE0); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010B10); - -INCLUDE_ASM("asm/nonmatchings/800", GameModeDispatch); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010BB4); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010C7C); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010CEC); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010D60); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010DA0); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010DE0); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010E14); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010E48); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010E7C); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010ED4); - -INCLUDE_ASM("asm/nonmatchings/800", func_80010F80); - -INCLUDE_ASM("asm/nonmatchings/800", func_800110CC); - -INCLUDE_ASM("asm/nonmatchings/800", DebugMenuHandler); - -INCLUDE_ASM("asm/nonmatchings/800", func_800111BC); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011220); - -INCLUDE_ASM("asm/nonmatchings/800", func_8001125C); - -INCLUDE_ASM("asm/nonmatchings/800", func_800112A8); - -INCLUDE_ASM("asm/nonmatchings/800", func_800112C8); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011320); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011350); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011380); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011680); - -INCLUDE_ASM("asm/nonmatchings/800", func_800116E0); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011778); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011818); - -INCLUDE_ASM("asm/nonmatchings/800", func_800118AC); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011928); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011998); - -INCLUDE_ASM("asm/nonmatchings/800", func_800119F0); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011A3C); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011ADC); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011B7C); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011C10); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011C8C); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011CFC); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011D54); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011DA0); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011DCC); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011DF4); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011E24); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011E84); - -INCLUDE_ASM("asm/nonmatchings/800", func_80011EB4); - -INCLUDE_ASM("asm/nonmatchings/800", func_800120DC); - -INCLUDE_ASM("asm/nonmatchings/800", func_8001212C); +#include "psyq/libcd.h" INCLUDE_ASM("asm/nonmatchings/800", func_800123F0); @@ -631,7 +522,113 @@ void func_800193A8(s32 arg0) { INCLUDE_ASM("asm/nonmatchings/800", func_800193B8); +#ifdef NON_MATCHING +extern void LoaderResetReadState(void); +extern int VSync(int mode); +extern CdlFILE D_80063058; +extern char cdpath_DEBUG_BIN[]; +extern u8 D_80062C38; /* path table entry[0].file (CdlFILE), entries stride 0x30; names at -0x14 */ +extern u8 D_80062C68; /* path entry[1].file.pos (CdlLOC), the 8 .CD files, stride 0x30 */ +extern s32 debugBinPresent; +extern s32 D_800747D4; +extern s32 D_80063074; +extern s32 D_800747D8; +extern u8 cdFileLocTable[]; /* out: {CdlLOC pos; u32 size} per sub-file, 8B stride */ +extern u8 D_800AE834; /* cdFileLocTable + 4 (the size field) */ +extern s32 listCdBuffer; /* LIST.CD content: 8B records {value; size} */ +extern u8 D_80180004; /* listCdBuffer + 4 (the size field) */ +/* file-loader directory resolver (boot @0x800101fc): probe \DEBUG.BIN;1, resolve the 21 + * CdPathTable entries via CdSearchFile, read LIST.CD (0xE40 B) into listCdBuffer, then build + * cdFileLocTable (CdlLOC+size per sub-file) over the 8 .CD files. Phase 3 T2. + * NON_MATCHING: logically faithful, structurally close (138 vs 133 ins) but not byte-exact. + * Residual is register allocation / loop-invariant hoisting — the target keeps &D_80063058, + * &D_80062C38, &D_80062C68 in callee-saved regs and derives the name arg as (base - 0x14) + * rather than a separate symbol; a multi-iteration / decomp-permuter target for a later pass. */ +void LoaderInitFileTable(void) { + CdlFILE *res; + int tries; + int pathOff; + int pathN; + int base; + int n; + int fileIdx; + int bufIdx; + int j; + int outer; + int locOff; + int dstOff; + int srcOff; + int *pCount; + int *pOff; + CdlLOC *p; + + LoaderResetReadState(); + tries = 0; + do { + res = CdSearchFile(&D_80063058, cdpath_DEBUG_BIN); + tries++; + if (res != (CdlFILE *)-1) { + break; + } + } while (tries < 0x10); + pathN = 0; + pathOff = 0; + debugBinPresent = (res != (CdlFILE *)0); + D_800747D4 = 1; + D_80063074 = 0; + D_800747D8 = 0; + do { + do { + res = CdSearchFile((CdlFILE *)(&D_80062C38 + pathOff), (char *)(&D_80062C38 - 0x14 + pathOff)); + } while ((u32)((int)res + 1) < 2); + pathN++; + pathOff += 0x30; + } while (pathN < 0x15); + do { + fileIdx = 0; + n = CdReadRequest(&D_80062C38, &listCdBuffer, 0xE40, 0); + if (n != 0) { + break; + } + VSync(0); + } while (1); + bufIdx = 0; + outer = 0; + locOff = 0; + pCount = &listCdBuffer; + do { + base = CdPosToInt((CdlLOC *)(&D_80062C68 + locOff)); + n = *pCount; + pCount += 2; + bufIdx++; + j = 0; + if (n > 0) { + dstOff = fileIdx * 8; + p = (CdlLOC *)(cdFileLocTable + dstOff); + srcOff = bufIdx * 8; + pOff = &listCdBuffer + bufIdx * 2; + do { + if (*pOff != 0) { + CdIntToPos(*pOff + base, p); + *(s32 *)(&D_800AE834 + dstOff) = *(s32 *)(&D_80180004 + srcOff); + } + pOff += 2; + srcOff += 8; + pCount += 2; + bufIdx++; + p += 2; + dstOff += 8; + j++; + fileIdx++; + } while (j < n); + } + outer++; + locOff += 0x30; + } while (outer < 8); +} +#else INCLUDE_ASM("asm/nonmatchings/800", LoaderInitFileTable); +#endif INCLUDE_ASM("asm/nonmatchings/800", func_80019930); @@ -668,7 +665,44 @@ void LoaderResetReadState(void) { INCLUDE_ASM("asm/nonmatchings/800", func_80019A10); -INCLUDE_ASM("asm/nonmatchings/800", CdReadRequest); +extern s32 CdQueueBusy(void); +extern void CdReadStateMachine(int); +extern s32 cdReq_curSector; +extern void *cdReq_dest; +extern s32 cdReq_size; +extern void *cdReq_cdlFile; +extern s32 cdReq_result; +extern s32 D_800AE720; +extern s32 D_800AE724; +/* Read-request dispatcher (Phase 3 T2). Refuse while CdQueueBusy(); dedup on the request's + * start sector (*cdlFile) vs the in-flight cdReq_curSector; stash dest/size/cdlFile/mode into + * the control block, set the "first read" flag D_800AE720 = (mode == 0), drive the state + * machine, return cdReq_result. */ +s32 CdReadRequest(int *cdlFile, void *dest, s32 size, s32 mode) { + s32 sector; + if (CdQueueBusy() != 0) { + return 0; + } + if (cdReq_curSector == 0) { + sector = *cdlFile; + } else { + sector = *cdlFile; + if (sector != cdReq_curSector) { + return 0; + } + } + cdReq_dest = dest; + cdReq_size = size; + cdReq_cdlFile = cdlFile; + D_800AE724 = mode; + D_800AE720 = 0; + cdReq_curSector = sector; + if (mode == 0) { + D_800AE720 = 1; + } + CdReadStateMachine(0); + return cdReq_result; +} INCLUDE_ASM("asm/nonmatchings/800", CdReadStateMachine); @@ -739,7 +773,107 @@ void func_8001B384(void) { INCLUDE_ASM("asm/nonmatchings/800", func_8001B394); +#ifdef NON_MATCHING +extern int func_8001A114(void); +extern void func_8001B710(void); +extern void func_8002D4C8(int arg0, int arg1); +extern void func_80036D58(int arg0); +extern int StreamLoadStateMachine(int arg0, void *loc, int n); +extern s32 CdQueueBusy(void); /* defined later in this file */ +extern s32 ResourceGetCdLoc(s16 arg0); /* defined later in this file */ +extern s32 resLoad_state; +extern s32 resLoad_curId; +extern s32 resLoad_lastId; +extern s32 resLoad_loadedFileIdx; +extern s32 resLoad_result; +extern s32 cdReq_curSector; +extern u8 resourceIdMap[]; /* 6B entries {s16 fileIdx; s16 D_8006313A; s16 D_8006313C} */ +extern u8 D_8006313A; +extern u8 D_8006313C; +extern u8 cdFileLocTable[]; +extern s32 D_800AE6F4; +extern s32 D_800AE70C; +/* loads a resource by resLoad_curId via resourceIdMap; load-once cache + * (resLoad_lastId/resLoad_loadedFileIdx). field0<0 -> non-CD path func_80036D58; else + * ResourceGetCdLoc -> StreamLoadStateMachine -> func_8002D4C8 post-process. Polled on + * resLoad_state; done flag resLoad_result. Phase 3 T3. + * NON_MATCHING: logically faithful (Ghidra-derived), body close but not byte-exact. Residuals: + * (1) block placement — the target lays state blocks 0->1->2->3 in order under a top beq-chain + * dispatch (order 1,0,2,3); this nested-if emits the state-2/3 block inline. (2) frame 0x38 vs + * 0x28 (~16B reserved locals the original keeps). (3) func_8002D4C8 arg is field2 & 0xFFFF. + * A switch() risks a rodata jump table (target has none). A later structural / permuter pass. */ +void ResourceLoadStateMachine(void) { + int result; + + result = 0; + if (resLoad_state != 1) { + if (resLoad_state != 0) { + if (resLoad_state == 2) { + D_800AE6F4 = 0; + D_800AE70C = 0; + resLoad_state = 3; + } else if (resLoad_state != 3) { + goto done; + } + if (func_8001A114() != 0) { + resLoad_state = 0; + } + goto done; + } + if (resLoad_curId == resLoad_lastId) { + resLoad_result = 1; + return; + } + if (*(s16 *)(resourceIdMap + resLoad_curId * 6) == resLoad_loadedFileIdx && + *(s16 *)(&D_8006313C + resLoad_curId * 6) != 0) { + func_8002D4C8(*(s16 *)(&D_8006313C + resLoad_curId * 6), 0); + resLoad_lastId = resLoad_curId; + resLoad_result = 1; + return; + } + if (CdQueueBusy() != 0) { + goto done; + } + if (*(s16 *)(resourceIdMap + resLoad_curId * 6) < 0) { + func_80036D58(*(s16 *)(&D_8006313A + resLoad_curId * 6)); + resLoad_result = 1; + return; + } + if (ResourceGetCdLoc((s16)resLoad_curId) == 0) { + resLoad_result = 1; + return; + } + if (cdReq_curSector != 0 && + *(s32 *)(cdFileLocTable + *(s16 *)(resourceIdMap + resLoad_curId * 6) * 8) != cdReq_curSector) { + goto done; + } + cdReq_curSector = *(s32 *)(cdFileLocTable + *(s16 *)(resourceIdMap + resLoad_curId * 6) * 8); + resLoad_state++; + } + result = StreamLoadStateMachine(*(s16 *)(&D_8006313A + resLoad_curId * 6), + cdFileLocTable + *(s16 *)(resourceIdMap + resLoad_curId * 6) * 8, 0x10); + if (result == 2) { + resLoad_state++; + result = 0; + } +done: + if (result != 0) { + resLoad_loadedFileIdx = *(s16 *)(resourceIdMap + resLoad_curId * 6); + if (*(s16 *)(&D_8006313C + resLoad_curId * 6) > 0) { + func_8002D4C8(*(s16 *)(&D_8006313C + resLoad_curId * 6), 0); + resLoad_lastId = resLoad_curId; + if (resLoad_curId == 0x3D) { + func_8001B710(); + } + } + resLoad_state = 0; + cdReq_curSector = 0; + } + resLoad_result = result; +} +#else INCLUDE_ASM("asm/nonmatchings/800", ResourceLoadStateMachine); +#endif INCLUDE_ASM("asm/nonmatchings/800", func_8001B710); @@ -1652,7 +1786,33 @@ INCLUDE_ASM("asm/nonmatchings/800", func_80034B0C); INCLUDE_ASM("asm/nonmatchings/800", func_80034B3C); -INCLUDE_ASM("asm/nonmatchings/800", CdQueueBusy); +extern s32 D_8006AEF8; +extern s32 D_8006AEFC; +extern u8 D_80076214; +extern u8 D_8007620C; +/* CD read-queue status: head==tail (D_8006AEF8/FC equal) => idle; flags D_80076214 (a + * pending/error byte) and D_8007620C (bit7/bit5) classify the busy/result state. + * Returns 0=idle done, 8=had pending, 2/4/1=busy variants. */ +s32 CdQueueBusy(void) { + if (D_8006AEF8 != D_8006AEFC) { + if (D_80076214 == 0) { + if (D_8007620C & 0x80) { + return 2; + } + if (D_8007620C & 0x20) { + return 4; + } + return 1; + } + } else { + D_8007620C = 0; + if (D_80076214 == 0) { + return 0; + } + } + D_80076214 = 0; + return 8; +} INCLUDE_ASM("asm/nonmatchings/800", func_80034C24); @@ -2350,7 +2510,7 @@ INCLUDE_ASM("asm/nonmatchings/800", func_800422E8); INCLUDE_ASM("asm/nonmatchings/800", func_80042374); -INCLUDE_ASM("asm/nonmatchings/800", func_8004239C); +INCLUDE_ASM("asm/nonmatchings/800", VSync); INCLUDE_ASM("asm/nonmatchings/800", VSYNC_OBJ_84); @@ -2490,9 +2650,9 @@ INCLUDE_ASM("asm/nonmatchings/800", func_800439D4); INCLUDE_ASM("asm/nonmatchings/800", func_800439F8); -INCLUDE_ASM("asm/nonmatchings/800", func_80043A18); +INCLUDE_ASM("asm/nonmatchings/800", CdIntToPos); -INCLUDE_ASM("asm/nonmatchings/800", func_80043B1C); +INCLUDE_ASM("asm/nonmatchings/800", CdPosToInt); INCLUDE_ASM("asm/nonmatchings/800", func_80043B9C); @@ -2558,7 +2718,7 @@ INCLUDE_ASM("asm/nonmatchings/800", callback); INCLUDE_ASM("asm/nonmatchings/800", BIOS_OBJ_1728); -INCLUDE_ASM("asm/nonmatchings/800", func_80045374); +INCLUDE_ASM("asm/nonmatchings/800", CdSearchFile); INCLUDE_ASM("asm/nonmatchings/800", ISO9660_OBJ_F8); diff --git a/src/boot.c b/src/boot.c new file mode 100644 index 000000000..ddeaff35e --- /dev/null +++ b/src/boot.c @@ -0,0 +1,142 @@ +#include "common.h" + +INCLUDE_ASM("asm/nonmatchings/boot", start); + +INCLUDE_ASM("asm/nonmatchings/boot", func_800100A0); + +INCLUDE_ASM("asm/nonmatchings/boot", __do_global_dtors); + +INCLUDE_ASM("asm/nonmatchings/boot", main); + +INCLUDE_ASM("asm/nonmatchings/boot", func_8001096C); + +INCLUDE_ASM("asm/nonmatchings/boot", func_8001099C); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010A08); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010A98); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010AE0); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010B10); + +/* (*gameModeHandlerTable[gameMode])() — 18 handlers @0x800629F4 (entry [7] = + * DebugMenuHandler). gameMode is a u16 at D_800AF630 + 0xA3AE (= 0x800B99DE) in the + * main state block. NOTE: `register` is REQUIRED to match — at -O0 (this module) it + * keeps the struct base in a callee-saved reg with no stack spill, and the 0xA3AE + * (>0x7FFF) member offset then assembles to the +0x10000/-0x5C52 split. */ +extern void (*gameModeHandlerTable[])(void); +extern u8 D_800AF630[]; +void GameModeDispatch(void) { + register u8 *p = D_800AF630; + gameModeHandlerTable[*(u16 *)(p + 0xA3AE)](); +} + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010BB4); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010C7C); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010CEC); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010D60); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010DA0); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010DE0); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010E14); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010E48); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010E7C); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010ED4); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80010F80); + +INCLUDE_ASM("asm/nonmatchings/boot", func_800110CC); + +extern void func_8001A9F8(int); +extern int CdReadRequest(void *dst, void *src, int arg2, int arg3); +extern void func_80010AE0(int); +extern void func_80011778(void); +extern void func_80015310(void); +extern u8 D_800AE888; +extern void *loadDestPtrTable; +/* gameMode 7 handler (gameModeHandlerTable[7]) — the TCRF L3 debug menu. Streams the + * room-select overlay (cdFileLocTable[11] -> 0x800CEDF8) via CdReadRequest, then inits it + * (func_80011778 / func_80015310). Retail: only room-select works. Phase 3 T8. */ +void DebugMenuHandler(void) { + int iVar1; /* -O0 reserves this local's 8-byte slot (frame 0x20); the original + * checks CdReadRequest's result directly (no store) — assigning iVar1 + * here would emit a store the target lacks, so it stays declaration-only. */ + func_8001A9F8(0); + if (CdReadRequest(&D_800AE888, loadDestPtrTable, 0, 0) != 0) { + func_80010AE0(0x3E0); + func_80011778(); + func_80015310(); + } + (void)iVar1; +} + +INCLUDE_ASM("asm/nonmatchings/boot", func_800111BC); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011220); + +INCLUDE_ASM("asm/nonmatchings/boot", func_8001125C); + +INCLUDE_ASM("asm/nonmatchings/boot", func_800112A8); + +INCLUDE_ASM("asm/nonmatchings/boot", func_800112C8); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011320); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011350); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011380); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011680); + +INCLUDE_ASM("asm/nonmatchings/boot", func_800116E0); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011778); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011818); + +INCLUDE_ASM("asm/nonmatchings/boot", func_800118AC); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011928); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011998); + +INCLUDE_ASM("asm/nonmatchings/boot", func_800119F0); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011A3C); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011ADC); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011B7C); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011C10); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011C8C); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011CFC); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011D54); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011DA0); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011DCC); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011DF4); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011E24); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011E84); + +INCLUDE_ASM("asm/nonmatchings/boot", func_80011EB4); + +INCLUDE_ASM("asm/nonmatchings/boot", func_800120DC); + +INCLUDE_ASM("asm/nonmatchings/boot", func_8001212C); diff --git a/tools/difficulty.py b/tools/difficulty.py index d166d78ae..7aa064b9c 100644 --- a/tools/difficulty.py +++ b/tools/difficulty.py @@ -11,11 +11,17 @@ Usage: tools/difficulty.py [TOP] (TOP = how many easy rows in the md digest, d import re, sys, pathlib ROOT = pathlib.Path(__file__).resolve().parent.parent -SRC = ROOT / "src" / "800.c" -ASM = ROOT / "asm" / "nonmatchings" / "800" +SRCS = sorted((ROOT / "src").glob("*.c")) # every c-segment (src/boot.c, src/800.c, ...) +ASM_ROOT = ROOT / "asm" / "nonmatchings" # per-segment subdirs (boot/, 800/, ...) MD = ROOT / "docs" / "difficulty.md" CSV = ROOT / ".run" / "difficulty.csv" +def find_s(name): + """Locate .s in any asm/nonmatchings// subdir.""" + for p in sorted(ASM_ROOT.glob(f"*/{name}.s")): + return p + return None + INSTR = re.compile(r'^\s*/\*\s*[0-9A-Fa-f]+\s+([0-9A-Fa-f]+)\s+[0-9A-Fa-f]+\s*\*/\s+([a-z][a-z0-9.]*)') BRANCH = re.compile(r'^b(eq|ne|gez|gtz|lez|ltz|nez|eqz|c1t|c1f|gezal|ltzal)?z?$') @@ -24,22 +30,24 @@ def is_data_blob(txt): def unmatched_stubs(): """INCLUDE_ASM names that are real functions (exclude data-blobs) and not inside NON_MATCHING.""" - lines = SRC.read_text().split('\n'); n = len(lines); i = 0; out = [] - while i < n: - s = lines[i].strip() - if s.startswith('#ifdef NON_MATCHING'): - while i < n and not lines[i].strip().startswith('#endif'): - i += 1 - i += 1; continue - m = re.match(r'INCLUDE_ASM\("[^"]+",\s*(\w+)\)', s) - if m: - out.append(m.group(1)) - i += 1 + out = [] + for src in SRCS: + lines = src.read_text().split('\n'); n = len(lines); i = 0 + while i < n: + s = lines[i].strip() + if s.startswith('#ifdef NON_MATCHING'): + while i < n and not lines[i].strip().startswith('#endif'): + i += 1 + i += 1; continue + m = re.match(r'INCLUDE_ASM\("[^"]+",\s*(\w+)\)', s) + if m: + out.append(m.group(1)) + i += 1 return out def analyze(name): - p = ASM / f"{name}.s" - if not p.exists(): return None + p = find_s(name) + if p is None: return None txt = p.read_text() if is_data_blob(txt): return None # not a function nins = branches = ncalls = 0; last_vaddr = None; jtbl = False diff --git a/tools/progress.py b/tools/progress.py index 043eb5a80..f72f9252e 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -13,12 +13,18 @@ Usage: import re, sys, hashlib, pathlib ROOT = pathlib.Path(__file__).resolve().parent.parent -SRC = ROOT / "src" / "800.c" -ASM = ROOT / "asm" / "nonmatchings" / "800" +SRCS = sorted((ROOT / "src").glob("*.c")) # every c-segment (src/boot.c, src/800.c, ...) +ASM_ROOT = ROOT / "asm" / "nonmatchings" # per-segment subdirs (boot/, 800/, ...) OUT = ROOT / "docs" / "progress.md" BUILD = ROOT / "build" / "us" / "SLUS_007.26" CHECK = ROOT / "config" / "check.us.sha" +def find_s(name): + """Locate .s in any asm/nonmatchings// subdir (segments: boot, 800, ...).""" + for p in sorted(ASM_ROOT.glob(f"*/{name}.s")): + return p + return None + INSTR = re.compile(r'^\s*/\*\s*[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s*\*/\s+[a-z]') def strip_comments(s): @@ -27,16 +33,16 @@ def strip_comments(s): def is_data_blob(name): """A .s with a code label (glabel/jlabel) is a function; data-only (dlabel, no code) is a blob.""" - p = ASM / f"{name}.s" - if not p.exists(): + p = find_s(name) + if p is None: return False txt = p.read_text() return ('glabel' not in txt and 'jlabel' not in txt and 'dlabel' in txt) def asm_is_trivial(name): """True iff the function's asm is exactly {jr, nop} (the empty-no-op shape splat emits void{} for).""" - p = ASM / f"{name}.s" - if not p.exists(): + p = find_s(name) + if p is None: return None mnem = [] for ln in p.read_text().splitlines(): @@ -48,37 +54,49 @@ def asm_is_trivial(name): SIG = re.compile(r'^\s*[A-Za-z_][\w \t\*]*\b([A-Za-z_]\w*)\s*\(') def classify(): - lines = SRC.read_text().split('\n') - n = len(lines); i = 0 real, empty, nonmatching, stubs, blobs = [], [], [], [], [] - while i < n: - s = lines[i].strip() - if s.startswith('#ifdef NON_MATCHING'): - blk = [] - while i < n and not lines[i].strip().startswith('#endif'): - blk.append(lines[i]); i += 1 - i += 1 - m = re.search(r'INCLUDE_ASM\("[^"]+",\s*(\w+)\)', '\n'.join(blk)) - if m: nonmatching.append(m.group(1)) - continue - m = re.match(r'INCLUDE_ASM\("[^"]+",\s*(\w+)\)', s) - if m: - (blobs if is_data_blob(m.group(1)) else stubs).append(m.group(1)); i += 1; continue - if s.startswith('INCLUDE_RODATA'): - i += 1; continue - fm = SIG.match(lines[i]) - if fm and '(' in lines[i]: - start = i; depth = 0; opened = False - while i < n: - c = strip_comments(lines[i]); depth += c.count('{') - c.count('}') - if '{' in c: opened = True + for src in SRCS: + lines = src.read_text().split('\n') + n = len(lines); i = 0 + while i < n: + s = lines[i].strip() + if s.startswith('#ifdef NON_MATCHING'): + blk = [] + while i < n and not lines[i].strip().startswith('#endif'): + blk.append(lines[i]); i += 1 i += 1 - if opened and depth <= 0: break - body = '\n'.join(lines[start:i]) - a, b = body.index('{'), body.rindex('}') - (real if strip_comments(body[a+1:b]).strip() else empty).append(fm.group(1)) - continue - i += 1 + m = re.search(r'INCLUDE_ASM\("[^"]+",\s*(\w+)\)', '\n'.join(blk)) + if m: nonmatching.append(m.group(1)) + continue + m = re.match(r'INCLUDE_ASM\("[^"]+",\s*(\w+)\)', s) + if m: + (blobs if is_data_blob(m.group(1)) else stubs).append(m.group(1)); i += 1; continue + if s.startswith('INCLUDE_RODATA'): + i += 1; continue + fm = SIG.match(lines[i]) + if fm and '(' in lines[i]: + # Definition ({ ... }) vs forward declaration (ends ;)? Scan to the first { or ;. + # extern/prototype lines (e.g. `extern s32 CdQueueBusy(void);`) are NOT functions. + j = i; kind = None + while j < n: + c = strip_comments(lines[j]) + br = c.find('{'); sm = c.find(';') + if br != -1 and (sm == -1 or br < sm): kind = 'def'; break + if sm != -1: kind = 'decl'; break + j += 1 + if kind != 'def': + i = j + 1; continue # skip the declaration + start = i; depth = 0; opened = False + while i < n: + c = strip_comments(lines[i]); depth += c.count('{') - c.count('}') + if '{' in c: opened = True + i += 1 + if opened and depth <= 0: break + body = '\n'.join(lines[start:i]) + a, b = body.index('{'), body.rindex('}') + (real if strip_comments(body[a+1:b]).strip() else empty).append(fm.group(1)) + continue + i += 1 return real, empty, nonmatching, stubs, blobs def main():