Files
BFM-decomp/tools/gen_lib_subsegs.py
T
Drew T cb7a280994 refactor(phase-9): T8a — de-default the EXE-curation helpers (thread --exe/--vram-base)
- make_snd_used.py + make_apicard_used.py: argparse --exe/--vram-base (default the
  EXE's); thread into psyq_identify subprocess + link_object (required post-T8b).
  RLO/RHI/EXCLUDE_ADDR kept as the EXE's region data (EXE-specific, documented)
- gen_lib_subsegs.py: --vram-base/--exe (default EXE); threaded into psyq_identify
  and the pos->fileoff arithmetic (module VRAM_BASE constant removed)
- split_src_region.py: --symbols overrides SYMS_PATH (default the EXE's symbols.us.txt)
- checks: snd_used (60) + apicard_used (22) regenerate BYTE-IDENTICAL; gen_lib_subsegs
  libgte -> 22 blocks/53 objs; clean build -> 143dbb89
2026-06-15 20:55:28 -06:00

125 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Generate splat subseg lines + the psyq_integrate stub list for a MULTI-BLOCK PsyQ library.
A library's objects are usually scattered across a game-code region in several contiguous blocks
(libgte = 26 blocks, libsnd = 16, libspu = 10). Hand-resegmenting the splat yaml for each block is
tedious and trips the boundary gotcha. This tool does it mechanically:
- place the library's objects with psyq_identify (optional window for short/ambiguous objects),
- group them into contiguous blocks (next.vram == prev.vram + prev.ins*4),
- END each block at its last object's `.text` SECTION size (8-aligned), NOT its instruction count
— psyq_identify reports ins count, which omits trailing align pad (the libc2/T6 gotcha that
shifted the whole image),
- within the containing region [region_lo, region_hi) emit, in vram order, a game-code fragment
subseg for each gap and a `c` subseg for each library block,
- print the comma-joined stub list for the psyq_integrate.py call.
Game-code fragments are named <gamebase>, <gamebase>_2, <gamebase>_3, …; library blocks <libbase>1,
<libbase>2, …. Objects placed OUTSIDE [region_lo, region_hi) (a library's stragglers in another
region, e.g. libgte's 4 objects in the libgs gaps) are listed as SKIPPED for separate handling.
Usage:
gen_lib_subsegs.py <elf_dir> <libbase> <gamebase> <region_vram_lo> <region_vram_hi> [win_lo win_hi]
Example (libgte in the 800b region, no window):
gen_lib_subsegs.py .run/obj40/libgte libgte 800b 0x8004787C 0x80051804
"""
import os, re, subprocess, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from psyq_link import section_table # noqa: E402
def placements(elf_dir, win, vram_base, exe):
cmd = ["python3", "tools/psyq_identify.py", elf_dir]
if win:
cmd += [f"0x{win[0]:X}", f"0x{win[1]:X}"]
cmd += ["--vram-base", hex(vram_base), "--exe", exe]
objs = []
for ln in subprocess.check_output(cmd, text=True).splitlines():
m = re.match(r"\s+0x([0-9A-Fa-f]+)\s+(\S+\.o)\s+\((\d+) ins\)", ln)
if m:
objs.append((int(m.group(1), 16), m.group(2), int(m.group(3))))
return sorted(objs)
def contiguous_blocks(objs):
blocks, cur = [], [objs[0]]
for o in objs[1:]:
if o[0] == cur[-1][0] + cur[-1][2] * 4:
cur.append(o)
else:
blocks.append(cur)
cur = [o]
blocks.append(cur)
return blocks
def main():
import argparse
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("elf_dir")
ap.add_argument("libbase")
ap.add_argument("gamebase")
ap.add_argument("region_lo")
ap.add_argument("region_hi")
ap.add_argument("window", nargs="*", help="optional placement window: win_lo win_hi")
ap.add_argument("--vram-base", default="0x8000F800", help="fileoff->vram delta (EXE default)")
ap.add_argument("--exe", default="extracted/retail/SLUS_007.26", help="target binary (EXE default)")
a = ap.parse_args()
elf_dir, libbase, gamebase = a.elf_dir, a.libbase, a.gamebase
rlo, rhi = int(a.region_lo, 0), int(a.region_hi, 0)
win = (int(a.window[0], 0), int(a.window[1], 0)) if len(a.window) >= 2 else None
vram_base = int(a.vram_base, 0)
objs = placements(elf_dir, win, vram_base, a.exe)
inreg = [o for o in objs if rlo <= o[0] < rhi]
skipped = [o for o in objs if not (rlo <= o[0] < rhi)]
if not inreg:
sys.exit(f"no {libbase} objects placed in [0x{rlo:X},0x{rhi:X})")
blocks = contiguous_blocks(inreg)
ranges = []
for b in blocks:
lo = b[0][0]
last = b[-1]
tsize = section_table(os.path.join(elf_dir, last[1]))[".text"][0]
hi = last[0] + tsize # section-size end (the gotcha fix)
ranges.append((lo, hi, b))
out, stubs = [], []
pos, gi, li = rlo, 0, 0
def gname():
nonlocal gi
gi += 1
return gamebase if gi == 1 else f"{gamebase}_{gi}"
for lo, hi, b in ranges:
if lo > pos:
nm = gname()
out.append(f" - [0x{pos - vram_base:X}, c, {nm}]"
f" # game code (vram 0x{vram_base + (pos - vram_base):08X}-0x{lo:08X})")
li += 1
sub = f"{libbase}{li}"
stubs.append(sub)
names = f"{b[0][1]}..{b[-1][1]}" if len(b) > 1 else b[0][1]
out.append(f" - [0x{lo - vram_base:X}, c, {sub}]"
f" # {libbase} block {li}: {len(b)} obj ({names}) vram 0x{lo:08X}-0x{hi:08X}")
pos = hi
if pos < rhi:
nm = gname()
out.append(f" - [0x{pos - vram_base:X}, c, {nm}]"
f" # game code (vram 0x{pos:08X}-0x{rhi:08X})")
print("\n".join(out))
print()
print(f"# {len(blocks)} blocks / {len(inreg)} objects in region; integrate stub list:")
print(",".join(stubs))
if skipped:
print(f"\n# SKIPPED — {len(skipped)} object(s) placed OUTSIDE [0x{rlo:X},0x{rhi:X}) "
f"(handle separately): {[s[1] for s in skipped]}")
if __name__ == "__main__":
main()