Files
BFM-decomp/tools/split_src_region.py
T
Drew T 3788a89b3c feat(phase-7): libcd linked into byte-identical build (58 SDK funcs) — session D checkpoint
The first real PsyQ library is sourced from real SDK objects in the byte-identical
build, replacing the libcd-region asm stubs. Tasks 2'.1/2'.2/2'.3 — the whole
PsyQ-library-into-build mechanism, proven on the simplest library (libcd).

- psyq_link.py: per-object byte-link engine — recovers externals from the EXE's
  resolved relocations (R_MIPS_26 / HI16+LO16); weakens psyq-obj-parser's
  mislabelled common-style .bss globals so a strong --defsym wins
- psyq_link_lib.py: whole-library verify — 18/18 libcd objects byte-identical
- psyq_link_region.py: region link via NOLOAD data placement (no carving) +
  weaken-all-.bss; gap-aware per-object placement
- psyq_integrate.py: build wiring — splat resegment swap + per-object .text at
  vram + NOLOAD data; external resolution (func_<addr> aliases, symbols.us.txt
  jump-table names, recovered St*/CD_* globals, all weakened .bss commons);
  idempotent; conditional on tools/psyq/ (fresh clone builds via stubs)
- split_src_region.py: H5-safe src/*.c split at vram boundaries (preserves
  matched C + NON_MATCHING blocks)
- splat.us.exe.yaml: 5-way text split [pre][libcd1][gap][libcd2][post]
- src/800.c trimmed to <0x80043088; src/{800b,gap,libcd1,libcd2}.c splat stubs
- Makefile: libcd integration in the $(OUT) recipe (conditional + idempotent)
- cookbook §9.1/9.2/9.3 (the reusable mechanism for libgs/Gen2); CURRENT_PHASE
  session-D log; ≥3-session green bar A/B/C/+D
- make clean && extract && build && check -> 143dbb89... BYTE-IDENTICAL
2026-06-14 23:13:39 -06:00

158 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Split a splat src/*.c at vram boundaries, preserving matched C and NON_MATCHING blocks (H5).
When a text subsegment is resegmented (e.g. to swap a library region's stubs for real objects),
the existing curated .c must be trimmed and its matched functions relocated — splat will NOT
overwrite an existing .c, so a stale .c would place functions at the wrong addresses. This tool
partitions the file's top-level items (INCLUDE_ASM stubs, real C functions, and #ifdef
NON_MATCHING blocks) by each item's vram:
trim <src.c> <lo> <hi> <move.frag>
rewrite <src.c> keeping only items with vram < lo (the header is preserved verbatim);
items in [lo, hi) are dropped (superseded by the linked objects); real-C items with
vram >= hi are written to <move.frag> for injection into the post-region file.
inject <dst.c> <move.frag>
in the freshly splat-generated <dst.c>, replace each moved function's INCLUDE_ASM stub
with its real C body (matched by function name).
Item vram comes from a func_XXXXXXXX name, else a name looked up in config/symbols.us.txt.
Brace matching is naive (counts {}); the build's SHA1 check is the backstop if an item with
string-literal braces is mis-split.
"""
import re, sys
SYMS_PATH = "config/symbols.us.txt"
def load_syms():
s = {}
for ln in open(SYMS_PATH):
m = re.match(r"(\w+)\s*=\s*0x([0-9A-Fa-f]+)", ln)
if m:
s[m.group(1)] = int(m.group(2), 16)
return s
def item_name(text):
m = re.search(r"INCLUDE_ASM\([^,]+,\s*(\w+)\)", text)
if m:
return m.group(1)
m = re.search(r"^\s*(?:static\s+)?[\w\*]+[\s\*]+(\w+)\s*\(", text, re.M)
return m.group(1) if m else None
def item_addr(text, syms):
name = item_name(text)
if name:
m = re.match(r"func_([0-9A-Fa-f]{8})$", name)
if m:
return int(m.group(1), 16)
if name in syms:
return syms[name]
return None
def parse(src):
"""Return (header, [item_text, ...]) splitting top-level items."""
lines = src.split("\n")
n = len(lines)
i = 0
while i < n and (lines[i].startswith("#include") or lines[i].strip() == ""):
i += 1
header = "\n".join(lines[:i]).rstrip("\n")
items = []
while i < n:
if lines[i].strip() == "":
i += 1
continue
start = i
# leading line/block comments belong to the following item
while i < n and (lines[i].lstrip().startswith("//") or lines[i].lstrip().startswith("/*")
or lines[i].lstrip().startswith("*")):
if lines[i].lstrip().startswith("/*") and "*/" not in lines[i]:
while i < n and "*/" not in lines[i]:
i += 1
i += 1
if i >= n:
break
l = lines[i]
if l.startswith("#ifdef NON_MATCHING"):
while i < n and not lines[i].startswith("#endif"):
i += 1
i += 1
elif l.lstrip().startswith("INCLUDE_ASM"):
i += 1
else:
depth = 0
seen = False
while i < n:
depth += lines[i].count("{") - lines[i].count("}")
if "{" in lines[i]:
seen = True
i += 1
if seen and depth <= 0:
break
items.append("\n".join(lines[start:i]))
return header, items
def is_real_c(text):
return "INCLUDE_ASM" not in text or text.startswith("#ifdef NON_MATCHING")
def trim(srcpath, lo, hi, movepath):
syms = load_syms()
header, items = parse(open(srcpath).read())
keep, drop, move = [], 0, []
for it in items:
a = item_addr(it, syms)
if a is None:
sys.exit(f"trim: cannot resolve address of item:\n{it[:80]}")
if a < lo:
keep.append(it)
elif a < hi:
drop += 1
else:
if is_real_c(it):
move.append((a, item_name(it), it))
open(srcpath, "w").write(header + "\n\n" + "\n\n".join(keep) + "\n")
move.sort()
with open(movepath, "w") as f:
f.write("\n\n".join(it for _, _, it in move) + "\n")
print(f"trim {srcpath}: kept {len(keep)} (<0x{lo:X}), dropped {drop} stubs/superseded "
f"[0x{lo:X},0x{hi:X}), moved {len(move)} real-C funcs (>=0x{hi:X}) -> {movepath}")
for a, nm, _ in move:
print(f" move 0x{a:08X} {nm}")
def inject(dstpath, movepath):
_, moved = parse(open(movepath).read())
by_name = {item_name(it): it for it in moved}
dst = open(dstpath).read()
done = []
for name, body in by_name.items():
# replace the whole stub line (optionally wrapped) for this function
pat = re.compile(r"^INCLUDE_ASM\([^,]+,\s*" + re.escape(name) + r"\);\s*$", re.M)
if pat.search(dst):
dst = pat.sub(lambda m: body, dst, count=1)
done.append(name)
open(dstpath, "w").write(dst)
print(f"inject {dstpath}: replaced {len(done)} stubs with matched C: {done}")
missing = [n for n in by_name if n not in done]
if missing:
sys.exit(f"inject: stubs not found for {missing}")
def main():
if len(sys.argv) >= 5 and sys.argv[1] == "trim":
trim(sys.argv[2], int(sys.argv[3], 0), int(sys.argv[4], 0), sys.argv[5])
elif len(sys.argv) == 4 and sys.argv[1] == "inject":
inject(sys.argv[2], sys.argv[3])
else:
sys.exit(__doc__)
if __name__ == "__main__":
main()