Files
BFM-decomp/tools/jtbl_rodata_pads.py
T
Drew T 5e78dccf07 feat(phase-29 §8e): jtbl 8-align pad-spec filter — the 4-giant unblock tooling (fleet-neutral)
- ROOT CAUSE PINNED (the session-2 half-pin was INVERTED; both probes were vacuous, R35):
  cc1 emits .align 3 before EVERY jump table; maspsx passes it VERBATIM (the :435 'drop' is
  an inventory-only pass); as bakes the pad SECTION-RELATIVE; link placement was never guilty
  (SUBALIGN(2) + ALIGN(.,4) place 4-mod-8 carve starts tight). Merging originally-separate
  TUs fires an intra-TU align where the original packed tight -> +4 at rodata 0xCC ->
  image-wide %lo shift. Honest probes persisted: .run/probe_jtbl/ (verdict.md + objdumps).
- NEW tools/jtbl_rodata_pads.py: post-maspsx filter replaces each rodata .align 3 with the
  ORIGINAL's exact pad bytes per a JTBL_PADS spec; fail-loud on table-count drift /
  non-align-3 / non-jtbl rodata content. Byte-proven: verbatim 0xE4 pad-at-0xCC ->
  filtered 0xE0 tight (= the merged carve span).
- jtbl_carve.py: spec-aware same-subseg merge (gap 0 or 4-with-zero-payload-word; else
  NON-CONTIGUOUS -> isolate), interval-arithmetic pad specs (committed values CARRIED,
  never re-derived), JTBL_PADS target-var emission into overlays.mk + revert() restore +
  stale-.o invalidation; the false 'maspsx drops .align' docstring corrected (H5).
- Makefile: $(if $(JTBL_PADS),| jtbl_rodata_pads.py ...) stage in build/src/%.o + file-scope
  empty default (env-shield). jtbl_family_bank.stub_file: duplicate-stub fail-loud (the
  earlier 'ladder failure' was a wrong-TU splice into a stale _a.c stub, byte-witnessed).
- R22 clean-fleet WITH the fix wired: 140/140 byte-identical, tools-health green
  (dedup 1846/0, C1 234205/234205), ZERO new banks -- fleet-neutral by construction.
- cookbook §8e (the jtbl alignment law) + §8a/§8a-pad corrections; decision-log R31 entry;
  SETUP.md tool row; .gitignore allowlist for the probe verdict artifacts.
2026-07-18 02:03:48 -06:00

105 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Phase-29 §8e: reproduce the ORIGINAL's jump-table padding in a multi-table .rodata carve.
The mechanism (byte-proven, .run/probe_jtbl/verdict.md):
- cc1 (Sony gcc 2.7.2) emits `.rdata` + `.align 3` + label before EVERY switch jump table.
- maspsx passes `.align` through VERBATIM (the maspsx/__init__.py:435 `continue` is in an
inventory-only pass; the output path re-emits `.align` unchanged).
- GNU as bakes the align pad into the section SECTION-RELATIVE — the linker can never remove it
(SUBALIGN(2) caps only the section's *external* placement alignment).
- The ORIGINAL image packs originally-separate TUs' tables TIGHT (the PSX linker 4-aligned
section placement), while intra-TU consecutive tables carry REAL zero-word pads where the
previous table ends ≡4 mod 8.
So when a decomp TU holds MULTIPLE matched jr-functions (originally separate TUs merged), cc1's
`.align 3` fires at boundaries where the original packed tight -> a +4 interior pad -> every
downstream data symbol shifts -> %lo relocs break image-wide. And conversely a table's alignment
pad must appear exactly where the original HAS one. `.align` cannot express this (it is relative
to the section start, whose vram parity varies per overlay sibling) — only explicit pad bytes can.
This filter sits between maspsx and as (Makefile `build/src/%.o` recipe, armed per-object by a
`JTBL_PADS` target-specific make var written by tools/jtbl_carve.py). It REPLACES each rodata
`.align` with the exact pad the original has at that table boundary:
--pads 0,4,0 -> table 1: no pad (specs[0] is always 0 — the table starts the section),
table 2: one `.word 0` pad, table 3: no pad.
Pads are derived by jtbl_carve from the carve intervals (pad[K] = start[K] - end[K-1], values in
{0,4}), so each overlay sibling gets its own spec from its own addresses. Unfiltered objects
(no JTBL_PADS var — every single-table carve) keep today's byte-identical pipeline.
Fail-loud guards (R32) — each converts a silent downstream SHA1 mismatch into an immediate,
attributable build error:
- spec count != rodata `.align` count (table-count drift: a sibling's switch compiled
to a different number of tables)
- a rodata `.align` other than `.align 3` (unknown emission shape)
- rodata content other than $L labels / `.word $L...` / blank (strings, floats — the carve
model does not cover them)
- pads[0] != 0, or any pad not in {0, 4}
"""
import argparse
import re
import sys
LABEL_RE = re.compile(r"^\$L\d+:$")
WORD_RE = re.compile(r"^\.word\s+\$L\d+$")
def run(pads, lines, out):
in_rodata = False
consumed = 0
for line in lines:
s = line.strip()
if s == ".section .rodata":
in_rodata = True
out.write(line)
continue
if in_rodata and (s == ".text" or s.startswith(".section")):
in_rodata = False
out.write(line)
continue
if in_rodata:
if s.startswith(".align"):
parts = s.split()
if len(parts) != 2 or parts[1] != "3":
sys.exit(f"jtbl_rodata_pads: unexpected rodata align {s!r} "
f"(cc1 emits `.align 3` per jump table — see verdict.md)")
if consumed >= len(pads):
sys.exit(f"jtbl_rodata_pads: more rodata .align directives than pad specs "
f"({len(pads)}) — table-count drift vs the carve")
if pads[consumed] == 4:
out.write(".word 0 # jtbl_rodata_pads: original inter-table .align 3 pad\n")
consumed += 1
continue
if s == "" or LABEL_RE.match(s) or WORD_RE.match(s):
out.write(line)
continue
sys.exit(f"jtbl_rodata_pads: unexpected rodata content {s!r} — "
f"the carve model covers only jump tables ($L labels + .word entries)")
out.write(line)
if consumed != len(pads):
sys.exit(f"jtbl_rodata_pads: consumed {consumed} rodata .align(s) but {len(pads)} pad "
f"spec(s) given — table-count drift vs the carve")
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--pads", required=True,
help="comma list of pad BYTES before each rodata jump table, in emission "
"order; pads[0] must be 0")
a = ap.parse_args()
try:
pads = [int(x) for x in a.pads.split(",")]
except ValueError:
sys.exit(f"jtbl_rodata_pads: malformed --pads {a.pads!r}")
if not pads or pads[0] != 0:
sys.exit(f"jtbl_rodata_pads: pads[0] must be 0 (the first table starts the section): {pads}")
if any(p not in (0, 4) for p in pads):
sys.exit(f"jtbl_rodata_pads: every pad must be 0 or 4: {pads}")
run(pads, sys.stdin, sys.stdout)
if __name__ == "__main__":
main()