5.2 KiB
§426 ★★★ — main's SWITCH FUNCTIONS WERE NEVER A CODEGEN WALL: ONE RODATA CARVE HAD BEEN MISSING SINCE PHASE 7 (P31 S72; 3 of the 11 "PROVEN gate-rejects" banked byte-identical in 14 s)
THE VERDICT THAT WAS WRONG. S71 substituted 11 main drafts one at a time, saw the image come
back with a different SHA1, and recorded them as "11 main functions score match_one closeness 0
and are PROVEN gate-rejects — §376 in its purest form — do not re-slate without a TU-level fix."
Every one of the 11 has a gcc switch. None of them was a body reject.
THE MECHANISM. config/splat.us.exe.yaml has carried exactly ONE .rodata carve since Phase 7:
[0x63238, .rodata, 800], covering LZSS's jtbl_80072A38 and nothing else. Every other main jump
table stayed raw in the tail data object. So when a switch function is drafted in C, cc1 emits
its table into .rodata while the original copy is still emitted from 6324C.data.o — two
copies, the image GROWS, and every data symbol above the insertion point shifts:
| draft | image delta | symbols moved | first moved |
|---|---|---|---|
func_8001A114 |
+28 | 238 | jtbl_80072A4C |
func_8001AAD0 |
+52 | 238 | jtbl_80072A4C |
func_8002EED8 |
+76 | 238 | jtbl_80072A4C |
func_8002F248 |
+84 | 238 | jtbl_80072A4C |
Each delta is that function's own table (n+1 entries) plus a .align 3 pad. The drafted function
itself differed by 1–4 bytes — the %lo(jtbl_...) immediate pointing at the table that moved.
THE ORACLE THAT COULD HAVE SAID SO, AND DIDN'T EXIST. A main gate's whole output was two
hashes, and gate_main's R40 baseline control rebuilds the tree green immediately after a
failure, overwriting build/us/SLUS_007.26 and its map — so the one artifact that could localize
the divergence was destroyed every time, before anyone could look at it. tools/main_diff_locate.py
(new) attributes the differing bytes to symbols via the linker map; gate_main now preserves the red
image first and prints FOUR verdicts — BODY REJECT / TABLE REJECT (§405-A, the dominant residual on main's switch functions, §433) / PLUMBING REJECT / MIXED.
THE DERIVED OVERLAY THAT NAMES A SHIFT INSTANTLY. splat names a symbol by its address, so
linked_address != name_address is the shift, with no reference build to diff against:
m = re.fullmatch(r'(?:func_|D_|jtbl_)([0-9A-Fa-f]{8})(?:\.NON_MATCHING)?', name)
if m and int(m.group(1), 16) != linked_addr: # this symbol MOVED
238 moved symbols, one delta, first mover jtbl_80072A4C — that is a layout shift, and no amount of
respelling the body will fix it.
THE FIX, AND WHY IT IS CHEAP. The island opens with a run of game tables that is contiguous
and 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
So the carve just extends to 0x80072A38-0x80072C70 and the 3-piece data→rodata→data sandwich
keeps its shape; only the split point moves (--tail 6324C.data.o → 63470.data.o). Probe it with
NO draft substituted first — the extension must be byte-neutral on its own, and it was.
THE §8e RESIDUAL, AND --derive FOR main. With the span carved, func_8001A114 still came back
+8 with two deltas: cc1 emits .align 3 before every table, and its table at 0x80072A7C (≡4
mod 8) gained a +4 pad the original does not have. That is exactly what JTBL_PADS /
jtbl_rodata_pads.py exists for — and its --derive walker (which already models .included .s
rodata spans interleaved with cc1 tables) needed one change to serve main: return the FILE-0
vram (code vram - code start = 0x80010000 - 0x800) instead of the segment vram, which makes
both raw[a - vram] and vram + <yaml offset> correct for the EXE's header AND leaves flat overlays
byte-identical. Makefile arms --derive for BINARY=main alongside md_%.
THE PRIZE, AND THE NEXT LEVER. 25 of main's 59 frontier functions (6,215 of 12,912 instructions, 48%) are switch functions. Span A unlocks 8 of them. The other three game spans —
| span | tables | owning functions |
|---|---|---|
0x80072E44-0x80073140 |
14 | ~10, incl. func_8003388C (663). SaveLoadRoutine (1165) is in this span but is NOT unlockable by drafting — §434 frame pair, route = §265 pair transcription |
0x800732A0-0x8007344C |
8 | ~8, incl. StreamLoadStateMachine |
0x80073494-0x80073514 |
4 | — |
— each need their own code object, because one object contributes exactly ONE contiguous
.rodata run and 800.o's is now span A. The spans' owner address ranges are disjoint and
ordered (span A owners < span B owners < span C owners): the jtbl spans ARE the original
translation units' rodata, so splitting src/800.c at those two boundaries is both the fix and a
recovery of the game's real TU structure.
DONE, SAME SESSION — see §431 for the method. src/800.c was split at 0x8002B0B4 and
0x80035270, byte-identical with nothing banked, and 7 of the session's 14 main banks were span
B/C functions that could not have banked the day before.