From cbf5bae0437af950d1dd9754ea04f6a1e12cc2e6 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:56:28 -0600 Subject: [PATCH] =?UTF-8?q?feat(main):=20unblock=20main's=20switch=20funct?= =?UTF-8?q?ions=20=E2=80=94=20the=20rodata=20span=20carve=20+=20derived=20?= =?UTF-8?q?jtbl=20pads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main's gate could only ever say "got X want Y". S71 read 7 such verdicts as body rejects and recorded 11 functions as "PROVEN gate-rejects, §376 in its purest form". They are not: all 11 are switch functions, and the blocker is that main has had exactly ONE rodata carve since Phase 7 (LZSS's jtbl_80072A38). Every other main jump table stayed raw in the tail data, so a drafted switch DOUBLE-EMITTED its table, the image grew (+28/+52/+76/+84 measured), and all 238 symbols above 0x80072A4C shifted. * tools/main_diff_locate.py (NEW) — turns a red image into a named list of divergent symbols via the linker map; per-byte attribution, self-test flips a byte at a known address and asserts the containing symbol (plus the identical-pair direction). * gate_main.py — PRESERVES the red image + map before the R40 baseline control rebuilds over it, and auto-localizes: BODY REJECT vs PLUMBING REJECT vs MIXED. Also -j on the build (was single-threaded) and the §376 drop list written to .run/gate_main_dropped.json with the reconciliation chain. * splat.us.exe.yaml — the .rodata carve extends from the LZSS table alone to the whole contiguous game-jtbl span 0x80072A38-0x80072C70 (12 tables, one 800.o run). Byte-neutral with no drafts substituted (probed first). * jtbl_rodata_pads.py — --derive now works for main: one file-0-vram expression makes both address->bytes and yaml-piece->address correct for the EXE's 0x800 header and leaves flat overlays unchanged. Makefile arms it for BINARY=main. Banked byte-identical: func_8001A114, func_8001AAD0, func_8001AF34 — three of the eleven. 25 of main's 59 frontier functions (6,215 of 12,912 instructions) are in this class; the remaining spans need src/800.c split at the TU boundaries the spans reveal. --- Makefile | 4 +- config/splat.us.exe.yaml | 17 ++- src/800.c | 264 +++++++++++++++++++++++++++++++++- tools/gate_main.py | 95 ++++++++++++- tools/jtbl_rodata_pads.py | 37 ++++- tools/main_diff_locate.py | 289 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 693 insertions(+), 13 deletions(-) create mode 100644 tools/main_diff_locate.py diff --git a/Makefile b/Makefile index 5d5699a29..a95813f38 100644 --- a/Makefile +++ b/Makefile @@ -659,7 +659,7 @@ ifeq ($(BINARY),main) # 6324C.data. Idempotent; keyed off splat's exact output (re-run = no-op). # EXE-only (overlays have no rodata island) — gated to BINARY=main; --front/--tail # name the sandwich .data objects (cookbook §8). - $(PYTHON) tools/ld_interleave.py --front 53198.data.o --tail 6324C.data.o $(LD_SCRIPT) + $(PYTHON) tools/ld_interleave.py --front 53198.data.o --tail 63470.data.o $(LD_SCRIPT) endif # Phase-26 §8: overlays that carve a jr-function's jtbl into a dotted .rodata subseg run # ld_interleave to place the migrated .rodata between the pre/post data-tail chunks (the @@ -718,7 +718,7 @@ ASFLAGS_REORDER := -Iinclude -march=r3000 -mtune=r3000 -no-pad-sections -O2 -G0 build/src/%.o: src/%.c @mkdir -p $(dir $@) @echo " CC $@" - @set -o pipefail; $(CPP) $(CPPFLAGS) -MMD -MP -MT $@ -MF $(@:.o=.d) $< | $(CC1_PSX) $(CC1FLAGS) | $(if $(filter $*,$(REORDER_TUS)),$(VENV_PY) tools/reorder_passthrough.py | $(AS) $(ASFLAGS_REORDER) -o $@,$(VENV_PY) $(MASPSX) --aspsx-version=$(ASPSX_VERSION) $(MASPSX_FLAGS) $(if $(JTBL_PADS),| $(VENV_PY) tools/jtbl_rodata_pads.py --pads $(JTBL_PADS),$(if $(filter md_%,$(BINARY)),| $(VENV_PY) tools/jtbl_rodata_pads.py --derive $(BINARY) --tu $(notdir $*))) | $(AS) $(ASFLAGS) -o $@) + @set -o pipefail; $(CPP) $(CPPFLAGS) -MMD -MP -MT $@ -MF $(@:.o=.d) $< | $(CC1_PSX) $(CC1FLAGS) | $(if $(filter $*,$(REORDER_TUS)),$(VENV_PY) tools/reorder_passthrough.py | $(AS) $(ASFLAGS_REORDER) -o $@,$(VENV_PY) $(MASPSX) --aspsx-version=$(ASPSX_VERSION) $(MASPSX_FLAGS) $(if $(JTBL_PADS),| $(VENV_PY) tools/jtbl_rodata_pads.py --pads $(JTBL_PADS),$(if $(filter md_% main,$(BINARY)),| $(VENV_PY) tools/jtbl_rodata_pads.py --derive $(BINARY) --tu $(notdir $*))) | $(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 diff --git a/config/splat.us.exe.yaml b/config/splat.us.exe.yaml index 1773a5fa5..221fa5b49 100644 --- a/config/splat.us.exe.yaml +++ b/config/splat.us.exe.yaml @@ -204,6 +204,19 @@ segments: # splat mis-detect it as func_80062998 (shadowing D_80062998). Carving it as the head of the # front-data subseg forces the data labels deterministically. (ld_interleave FRONT_DATA matches.) - [0x53198, data, 53198] # data table + front data (vram 0x80062998-0x80072A38) - - [0x63238, .rodata, 800] # LZSS jtbl_80072A38 ONLY (vram 0x80072A38-0x80072A4C) -> migrates into LzssDecodeSector - - [0x6324C, data, 6324C] # tail data: rest of island (raw) + globals (vram 0x80072A4C-0x80074800) + # P31 S72 — SPAN EXTENSION. The Phase-7 carve stopped at the LZSS table because a FULL + # island migration hits the interleaved game data and the library jtbls. But the island + # opens with a CONTIGUOUS run of game tables owned entirely by subseg 800: + # 0x80072A38 jtbl (LzssDecodeSector, matched, cc1-emitted) + # 0x80072A4C A7C A94 AB4 ADC B0C B24 B3C B64 B88 BFC (11 tables, 8 stubbed owners) + # 0x80072C70 loadDestPtrTable <- first non-table datum, the span's hard end + # One code object may contribute exactly ONE contiguous .rodata run, and this whole run is + # 800.o's, in address order = src/800.c source order. So the span carves as one piece and + # the 3-piece data->rodata->data sandwich is unchanged in SHAPE, only in where it splits. + # This is what blocked every main switch function: gcc emits the drafted function's table + # into .rodata while the raw copy stayed here, so the image GREW (measured +28/+52/+76/+84 + # on four drafts) and all 238 symbols above 0x80072A4C shifted. 25 of main's 59 frontier + # functions (6,215 of 12,912 instructions) are in that class. + - [0x63238, .rodata, 800] # LZSS jtbl + the 11 contiguous game jtbls (vram 0x80072A38-0x80072C70) + - [0x63470, data, 63470] # tail data: loadDestPtrTable onward (vram 0x80072C70-0x80074800) - [0x65000] diff --git a/src/800.c b/src/800.c index aafdfb1e4..073b4527f 100644 --- a/src/800.c +++ b/src/800.c @@ -7054,7 +7054,120 @@ void func_8001A0FC(void) { D_800AE70C = 0; } -INCLUDE_ASM("asm/nonmatchings/800", func_8001A114); + +/* CD error-recovery state machine (0x8001A114), stepped from CdReadStateMachine's state 10. + * One step per call; returns 1 only when the path table has been re-resolved (recovery done). + * 0: CdFlush-ish reset + CdlNop -> 1 1: CdSync poll (2 -> advance, 0x10 -> restart) + * 2: CdlSetmode(0x80) 3: burn 3 frames 4: CdSync poll + * 5: re-run CdSearchFile on the path entry (up to 16 tries) -> done / restart + * + * §ADD-8 (re-tie barrier): the two constant arguments of the state-0 CdControl must issue + * BEFORE the `la $s0,cdReq_cdResult`; sched1 otherwise ranks the address load first (it feeds + * $a2 and so has the longer chain). The zero-byte `__volatile__` re-ties pin them to source + * order. §20/§243 (held-pointer): cdReq_retry in state 3 and D_800AE6F4 on the shared + * "advance" tail are spelled through a named pointer — that is what turns their %hi/%lo pairs + * into a single base register, and keeping the two tails textually distinct is what stops + * cross-jumping from merging .L8001A260 with .L8001A2AC. */ + +extern void func_800434BC(void); +extern int func_80043830(int com, u8 *param, u8 *result); +extern int func_80046630(int mode); +extern int func_8004674C(void); + +extern s32 D_800AE6F4; /* recovery state */ +extern u8 cdReq_cdResult; /* CdControl status byte (bit 0x10 = error) */ +extern u8 D_800AE740; /* CdlSetmode mode-byte buffer; cdReq_cdResult is at -8 */ +extern int cdReq_retry; +extern CdlFILE D_80063028; /* CdPathTable entry; its name string sits at -0x14 */ + +int func_8001A114(void) { + int ret; + u8 *p; + int r; + int i; + CdlFILE *fp; + int *rp; + s32 *sp; + int c0; + int c1; + + ret = 0; + switch (D_800AE6F4) { + case 0: + func_800434BC(); + c0 = 1; + __asm__ __volatile__("" : "=r"(c0) : "0"(c0)); + c1 = 0; + __asm__ __volatile__("" : "=r"(c1) : "0"(c1)); + p = &cdReq_cdResult; + func_80043830(c0, (u8 *)c1, p); + if ((*p & 0x10) != 0) { + break; + } + D_800AE6F4 = D_800AE6F4 + 1; + /* fallthrough */ + case 1: + r = func_80046630(0); + if (r == 2) { + goto bump; + } + if (r != 0x10) { + break; + } + reset: + D_800AE6F4 = 0; + break; + case 2: + p = &D_800AE740; + *p = 0x80; + if (func_80043830(0xE, p, p - 8) == 0) { + break; + } + if ((p[-8] & 0x10) != 0) { + goto reset; + } + cdReq_retry = 0; + D_800AE6F4 = D_800AE6F4 + 1; + break; + case 3: + rp = &cdReq_retry; + *rp = *rp + 1; + if (*rp < 3) { + break; + } + *rp = 0; + D_800AE6F4 = D_800AE6F4 + 1; + break; + case 4: + r = func_8004674C(); + if ((r == 1) || (r == 0x10) || (r == 0)) { + D_800AE6F4 = 0; + } + if (r != 2) { + break; + } + bump: + sp = &D_800AE6F4; + *sp = *sp + 1; + break; + case 5: + i = 0; + fp = &D_80063028; + do { + r = (int)CdSearchFile(fp, (char *)fp - 0x14); + if (r != -1) { + break; + } + i = i + 1; + } while (i < 0x10); + if ((u32)(r + 1) < 2) { + goto reset; + } + ret = 1; + break; + } + return ret; +} #ifdef NON_MATCHING typedef struct { short x, y, w, h; } RECT; /* libgpu RECT (VRAM rectangle) */ @@ -7312,7 +7425,61 @@ void func_8001AAA0(s32 arg0) { func_8001ABBC(1, arg0, 0, 0, 0); } -INCLUDE_ASM("asm/nonmatchings/800", func_8001AAD0); +extern s32 D_800BA1B4; +extern u8 D_80062C38; +extern s32 func_8001ABBC(s32 a0, s32 a1, s32 a2, s32 a3, s32 a4); + +void func_8001AAD0(s32 arg0, s32 arg1) { + s32 idx; + + switch (arg0) { + case 0: + idx = 9; + break; + case 1: + idx = 10; + break; + case 2: + idx = 11; + break; + case 3: + idx = 12; + break; + case 4: + idx = 13; + break; + case 5: + idx = 14; + break; + case 6: + idx = 15; + break; + case 7: + idx = 16; + break; + case 8: + idx = 17; + break; + case 9: + idx = 18; + break; + case 10: + idx = 19; + break; + case 11: + idx = 20; + break; + default: + idx = 0; + break; + } + + if (D_800BA1B4 == 5) { + D_800BA1B4 = 0; + } + + func_8001ABBC(3, arg1, (s32)(&D_80062C38 + idx * 0x30), 0, 0); +} void func_8001ABB4(void) { } @@ -7463,7 +7630,98 @@ s32 func_8001AF04(void) { return 2; } -INCLUDE_ASM("asm/nonmatchings/800", func_8001AF34); +extern s32 D_800BA1B4; +extern s32 D_800AE6E4; +extern s32 D_800BA318; +extern s32 D_800C6D2C; +extern s32 D_800A6550; +extern s32 cdReq_curSector; +extern void *cdReq_dest; +extern s32 cdReq_size; +extern void *cdReq_cdlFile; +extern s32 D_800AE724; +extern s32 D_800AE720; +extern s32 cdReq_result; +extern s32 D_800AE640; +extern s32 D_800A6430; +extern s16 D_800A6430_h __asm__("D_800A6430"); +extern s32 D_800747E4; +extern s32 CdQueueBusy(void); +extern void CdReadStateMachine(int); +extern s32 func_8001B394(s32); +extern s32 func_8001B7C4(void *); +extern s32 func_8001B0D4(void *, s32); + +void func_8001AF34(void) { + s32 *cdlFile; + s32 dest; + s32 size; + s32 mode; + s32 sector; + s32 ret; + + CdQueueBusy(); + switch (D_800BA1B4) { + case 0: + return; + case 1: + cdlFile = (s32 *)D_800AE6E4; + dest = D_800BA318; + size = D_800C6D2C; + mode = D_800A6550; + if (CdQueueBusy() != 0) { + ret = 0; + goto chk; + } + if (cdReq_curSector == 0) { + sector = *cdlFile; + } else { + sector = *cdlFile; + if (sector != cdReq_curSector) { + ret = 0; + goto chk; + } + } + cdReq_dest = (void *)dest; + cdReq_size = size; + cdReq_cdlFile = (void *)cdlFile; + D_800AE724 = mode; + D_800AE720 = 0; + cdReq_curSector = sector; + if (mode == 0) { + D_800AE720 = 1; + } + CdReadStateMachine(0); + ret = cdReq_result; + chk: + if (ret == 0) { + return; + } + D_800BA1B4 = 3; + return; + case 2: + if (func_8001B394(D_800AE640) == 0) { + return; + } + D_800BA1B4 = 3; + return; + case 3: + return; + case 4: + if (func_8001B7C4((void *)D_800AE6E4) == 0) { + return; + } + D_800BA1B4 = 3; + return; + case 5: + if (func_8001B0D4((void *)D_800AE6E4, D_800A6430_h) == 0) { + return; + } + D_800747E4 = 0; + D_800BA1B4 = 3; + return; + } +} INCLUDE_ASM("asm/nonmatchings/800", func_8001B0D4); diff --git a/tools/gate_main.py b/tools/gate_main.py index e31f3de40..c5c786970 100644 --- a/tools/gate_main.py +++ b/tools/gate_main.py @@ -563,7 +563,12 @@ def clean_build(): can pass without building is worse than no verifier.""" run("rm -f build/us/SLUS_007.26") run("make extract BINARY=main") - r = run("make build BINARY=main") + # `-j`. `make build BINARY=main` without it is SINGLE-THREADED on a 32-core box; the + # Makefile's own JOBS knob is parallelism ACROSS binaries, which a one-binary build never + # reaches (memory `pass-j-to-every-build`, measured 6.1x elsewhere and byte-identical). A gate + # is run hundreds of times a session, so this is the difference between a probe you take and a + # probe you talk yourself out of. + r = run(f"make build BINARY=main -j{os.cpu_count() or 8}") if r.returncode != 0: # `make build BINARY=main` runs the SHA check itself, so rc!=0 does NOT mean "no # binary": a linked-but-MISMATCHED build also exits nonzero. Returning None here routed @@ -578,12 +583,83 @@ def clean_build(): return None, r # build truly failed -> no hash, and never a pass return sha(), r +FAILDIR = '.run/gate_main_fail' + + +def _preserve_and_localize(entries, got): + """Snapshot the RED image + its map, then name the symbols that actually diverged. + + WHY THIS EXISTS (P31 S72). Every red verdict this gate has ever produced was two hashes and + nothing else -- and the R40 baseline control that runs immediately after a failure REBUILDS + THE TREE GREEN, overwriting `build/us/SLUS_007.26` and its map. The one artifact that could + say WHERE the image moved was destroyed, every time, before anyone could look at it. + + That is not a cosmetic gap. S71 substituted 11 main drafts one at a time, saw 7 come back with + a different hash, and recorded all 11 as "PROVEN gate-rejects". A hash cannot distinguish + "your body is wrong" from "your body is perfect and the substitution changed a CALLER" -- the + §376 shape, where the TU keeps a stale `extern void f(void*)` while the definition is + `void f(s32)`, so every call site's argument codegen moves. Six of those eleven are that + class, and this gate's own pre-check names them (see resolve_conflicts) -- but the four that + reached a build were judged with no instrument that could tell the two apart. + + So: copy the image and the map aside FIRST, attribute per byte, and print the verdict. With a + single-entry slate the verdict is the routing decision (body reject vs plumbing reject).""" + try: + import main_diff_locate as MDL + except Exception as e: # never let diagnostics sink a gate + print(f" (diff localization unavailable: {e})") + return + tag = entries[0]['fn'] if len(entries) == 1 else f"batch{len(entries)}" + d = os.path.join(FAILDIR, f"{tag}_{(got or 'nobin')[:8]}") + os.makedirs(d, exist_ok=True) + for f in ('build/us/SLUS_007.26', 'build/us/SLUS_007.26.map'): + if os.path.exists(f): + run(f"cp {f} {d}/") + built = os.path.join(d, 'SLUS_007.26') + mp = os.path.join(d, 'SLUS_007.26.map') + if not (os.path.exists(built) and os.path.exists(mp) and os.path.exists(MDL.REF)): + print(f" (red image preserved at {d}, but localization inputs are incomplete)") + return + try: + sections, syms = MDL.parse_map(mp) + per, ndiff, _sz = MDL.attribute(open(built, 'rb').read(), open(MDL.REF, 'rb').read(), + sections, syms) + except Exception as e: + print(f" (red image preserved at {d}; localization failed: {e})") + return + rows = sorted(per.values(), key=lambda x: -x['bytes']) + print(f" RED IMAGE PRESERVED -> {d}") + print(f" {ndiff} differing byte(s) across {len(rows)} symbol(s):") + for e in rows[:12]: + a = f"0x{e['first_addr']:08x}" if e['first_addr'] is not None else '?' + print(f" {e['bytes']:>6} {a} {e['symbol']}") + if len(rows) > 12: + print(f" ... {len(rows)-12} more ({sum(x['bytes'] for x in rows[12:])} bytes)") + if len(entries) == 1: + fn = entries[0]['fn'] + inside = per.get(fn, {}).get('bytes', 0) + outside = ndiff - inside + if inside and not outside: + print(f" VERDICT {fn}: BODY REJECT — divergence confined to the function itself.") + elif outside and not inside: + print(f" VERDICT {fn}: PLUMBING REJECT — the function is BYTE-IDENTICAL; all " + f"{outside} differing bytes are elsewhere. Route to the §376/§378 chain " + f"(fix_arity_callers --any-proto -> cast_self_callers -> re-gate); do NOT " + f"record this as a body reject.") + elif inside and outside: + print(f" VERDICT {fn}: MIXED — {inside} bytes inside, {outside} outside. The body " + f"verdict is UNPROVEN until the outside bytes are fixed and it is re-gated.") + + def try_batch(entries): run("git checkout -- " + " ".join(main_tus())) run("make extract BINARY=main") # regenerate .s for the reverted stubs (hazard 2) substitute(entries) got, r = clean_build() - return got == GOOD, got, r + ok = got == GOOD + if not ok and got is not None and entries: + _preserve_and_localize(entries, got) + return ok, got, r def main(): ap = argparse.ArgumentParser() @@ -694,6 +770,21 @@ def main(): if dropped: print(" (dropped drafts are usually CORRECT -- recover with a cast-at-use: adopt the") print(" other declaration verbatim and adapt at the use site, e.g. (&D_x)[i].)") + # A DROP IS A ROUTE, NOT A VERDICT (P31 S72). This list is the §376 pile: the draft's + # definition disagrees with a forward declaration the TU already carries, which is a + # PLUMBING problem with a named fix chain -- not evidence about the body. Printed-only, + # it kept getting read as a rejection: S71 recorded six of these as "PROVEN gate-rejects, + # §376 in its purest form -- do not re-slate", and they were never re-slated. Writing it + # to disk with the chain spelled out makes the recovery the obvious next command instead + # of a paragraph someone has to remember. + json.dump(dropped, open('.run/gate_main_dropped.json', 'w'), indent=1) + print(f" -> .run/gate_main_dropped.json ({len(dropped)} to reconcile). The chain is:") + print(f" tools/fix_arity_callers.py --apply --any-proto --funcs " + f"{','.join(d['fn'] for d in dropped)} \\\n" + f" --drafts