mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-27 05:56:00 -04:00
757bd82a0f
The §9.1 "scattered .bss commons" exclusion class (Phase 8 → P31) is closed 3/3. New tools/psyq_bss_split.py (own ELF32 REL reader/writer) cuts an object's packed .bss into per-base NOBITS pieces: bases derived from the game bytes per HI16/LO16 pair, references walked in offset order into single-base runs, cuts snapped to symbol starts (the linker scattered SYMBOLS), symbols moved, a LOCAL section symbol per piece inserted, relocs retargeted with the addend rewritten in the immediates, self-diffed. It runs inside the one prepare step shared by psyq_link.link_object / psyq_link_region.build_region / psyq_integrate.integrate (prepare_object before classify), re-derived every build. GS_001.o was certified "5 interleaved bases, NOT splittable" by the S77 probe, which grouped by BASE; by RUN it is six symbol-aligned pieces. All seven cuts across the three objects are confirmed by the other objects' by-name recoveries (_que 0x800C5510, _svm_sreg_buf 0x800B9B58, PSDBASEX/CLIP2/PSDBASEY/POSITION/GsDRAWENV). R39 negative control: 235 placed objects across 9 curated dirs, 0 refusals, exactly 3 splits (a libcd .bss+size end pointer refused the first build → reference problems are fatal only when a split is needed). Wiring: yaml 800c→libgpu2, sgap_6→sgap_6+snd12, gsgap3→libgs8 (comments rewritten); LIBGPU_ELF := .run/obj40/libgpu (curated libgpu_used retired); libgs 34 objs/8 blocks (make_libgs.sh +GS_001); snd 63/12 (make_snd_used.py exclusions 4→3). src/800c.c and src/gsgap3.c removed (Sony code hand-matched as REAL/verbatim), sgap_6.c keeps only func_8003FA54; splat-emitted libgpu2.c/libgs8.c/snd12.c stubs for the no-SDK fallback. Verified: main 143dbb89f34491258bbc27810d0a12ec8b43a8dd WITH the SDK objects and WITHOUT them from a fresh extract; make tools-health OK; R22 fleet clean extract-all 212/212 + check-all 213/213. Metrics: main REAL 886→839, LINKED 1,040→1,150, VERBATIM 85→29, stubs 29 (unchanged); game-code weighted 91.1% (40,895/44,870) — both terms lost the 3,667 SDK ins; the remainder is still exactly the 3,975-ins open-stub sum. Verbatim manifest --update 200→33 rows (subtractive). Docs: cookbook §489 (+index), psyq-worklist rows + "S78 task #4", SETUP S79 R21 table, decision-log S79 addendum, accelerators S79, CURRENT_PHASE S79 FINAL 🛑.
139 lines
7.1 KiB
Python
139 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
||
"""psyq_bss_probe.py — can this PsyQ object's scattered `.bss` be SPLIT and placed? (P31 S77; S78 #4: yes, and it is)
|
||
|
||
THE WALL THIS INTERROGATES. `config/splat.us.exe.yaml` had excluded library objects from the
|
||
LINKED build since Phase 8 with one recorded reason:
|
||
|
||
SYS.o EXCLUDED — scattered-.bss commons (the GS_001 class: SYS references .bss by section+offset
|
||
but the original linker scattered the commons across 0x80078xxx/0x800c5xxx, so no single NOLOAD
|
||
base reproduces it)
|
||
|
||
That reason is CORRECT — §9.2's "weaken the `.bss` symbol and `--defsym` it" cannot reach these
|
||
sites, because a `R_MIPS_HI16/LO16` against the bare `.bss` SECTION has no name to defsym. But "no
|
||
single base" is not the same claim as "unlinkable", and nobody had asked the next question: **can the
|
||
section be cut into pieces that each have ONE base?** This probe asked it (S77) and
|
||
`tools/psyq_bss_split.py` (S78 #4) answers it at link-prepare time for every object in the build.
|
||
|
||
WHAT "SPLITTABLE" MEANS NOW (the S77 test was too strict — corrected S78). The S77 probe grouped the
|
||
references BY BASE and required the per-base offset ranges to be disjoint. That verdict was right
|
||
for SYS.o and VM_F.o (two bases, two ranges) and WRONG for GS_001.o, which it called "5 interleaved
|
||
bases": walked in OFFSET order, GS_001's references form six single-base RUNS, and every cut between
|
||
two runs lands on a symbol start (PSDBASEX, CLIP2, PSDBASEY, POSITION, GsDRAWENV) — exactly what
|
||
"the linker scattered the commons" predicts. The linker scattered SYMBOLS, so the right question is
|
||
whether the runs can be tiled at symbol boundaries without a sized symbol straddling a cut. All
|
||
three objects can; all three link byte-identical through the split (psyq_link.py PASS).
|
||
|
||
HOW IT DERIVES THE BASE — from the bytes, not from a config. For each HI16/LO16 pair against the
|
||
section symbol, the OBJECT's immediates encode the addend and the GAME's immediates encode the
|
||
resolved address; `base = resolved − addend`. Grouping by base shows the scatter; the runs give the
|
||
cut points. The object's placement in the game is found by masking every relocated field and
|
||
searching for the unique match, so a wrong `--vram` cannot fake a clean answer (and the search
|
||
doubles as proof the object is really there). The analysis itself lives in `psyq_bss_split.analyze`
|
||
(R33: one implementation; this file only extracts, locates and reports).
|
||
|
||
tools/psyq_bss_probe.py --archive tools/psyq/lib40_elf/LIBGPU.a --object SYS.o
|
||
tools/psyq_bss_probe.py --archive ... --object GS_001.o --vram 0x800525DC
|
||
"""
|
||
import argparse
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
from psyq_bss_split import Elf32, SHT_NOBITS, R_MIPS_HI16, R_MIPS_LO16, SplitRefused, analyze, report # noqa: E402
|
||
|
||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
EXE = os.path.join(REPO, "extracted/retail/SLUS_007.26")
|
||
EXE_VRAM0 = 0x8000F800 # file offset 0 -> vram (the 0x800 PS-X EXE header sits below .text)
|
||
R_MIPS_26, R_MIPS_32 = 4, 2
|
||
|
||
|
||
def find_vram(e, game):
|
||
"""The unique game offset where this object's .text sits, with every relocated field masked.
|
||
|
||
Masking is what makes the search honest: relocated immediates DIFFER between the archive object
|
||
and the linked game, so an exact byte search would find nothing and a loose one would find noise.
|
||
A unique hit is also proof the object is present at all (R14)."""
|
||
ti = e.section_index(".text")
|
||
text = bytes(e.data[ti])
|
||
mask = bytearray(b"\xff" * len(text))
|
||
for ri, rl in e.rels.items():
|
||
if e.sh[ri]["info"] != ti:
|
||
continue
|
||
for off, _sym, typ in rl:
|
||
if off + 4 <= len(mask):
|
||
if typ in (R_MIPS_HI16, R_MIPS_LO16):
|
||
mask[off:off + 2] = b"\x00\x00" # low half-word (little-endian immediate)
|
||
elif typ in (R_MIPS_26, R_MIPS_32):
|
||
mask[off:off + 4] = b"\x00\x00\x00\x00"
|
||
tb, mb = text, bytes(mask)
|
||
hits = []
|
||
for base in range(0, len(game) - len(tb), 4):
|
||
ok = True
|
||
for i in range(0, len(tb), 4):
|
||
if mb[i] == 0 and mb[i + 1] == 0 and mb[i + 2] == 0xff: # HI/LO: compare top half only
|
||
if game[base + i + 2:base + i + 4] != tb[i + 2:i + 4]:
|
||
ok = False; break
|
||
elif mb[i] == 0:
|
||
continue
|
||
elif game[base + i:base + i + 4] != tb[i:i + 4]:
|
||
ok = False; break
|
||
if ok:
|
||
hits.append(base)
|
||
if len(hits) > 1:
|
||
break
|
||
return hits
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||
ap.add_argument("--archive", required=True)
|
||
ap.add_argument("--object", required=True)
|
||
ap.add_argument("--vram", default=None, help="skip the search; the object's .text vram")
|
||
a = ap.parse_args()
|
||
|
||
with tempfile.TemporaryDirectory(dir=os.path.join(REPO, ".run")) as td:
|
||
subprocess.run(["ar", "x", os.path.abspath(a.archive), a.object], cwd=td, check=True)
|
||
obj = os.path.join(td, a.object)
|
||
e = Elf32(open(obj, "rb").read())
|
||
names = [e.secname(i) for i in range(len(e.sh))]
|
||
if ".text" not in names:
|
||
sys.exit("%s: no .text section" % a.object)
|
||
nobits = [i for i, s in enumerate(e.sh) if s["type"] == SHT_NOBITS]
|
||
tsize = e.sh[e.section_index(".text")]["size"]
|
||
if not nobits:
|
||
# NOT AN ERROR, AND SAYING SO MATTERS. The first version exited "no .text/.bss" here,
|
||
# which reads as a malformed object; it actually means the scattered-.bss wall CANNOT
|
||
# apply. Measured: 2D_BG0.o and VM_NO1.o both land here, and both were excluded in
|
||
# splat.us.exe.yaml under a reason that is about .bss. (R61a: a tool must not report a
|
||
# clean answer as a failure.)
|
||
print("%s: .text %d ins, NO .bss SECTION — the scattered-.bss wall does not apply to "
|
||
"this object; whatever excludes it is a different reason." % (a.object, tsize // 4))
|
||
return 0
|
||
game = open(EXE, "rb").read()
|
||
if a.vram:
|
||
goff = int(a.vram, 0) - EXE_VRAM0
|
||
else:
|
||
hits = find_vram(e, game)
|
||
if len(hits) != 1:
|
||
sys.exit("%s: masked .text search found %d placements — pass --vram" % (a.object, len(hits)))
|
||
goff = hits[0]
|
||
print("%s: .text %d ins @ vram %#010x (%s)"
|
||
% (a.object, tsize // 4, goff + EXE_VRAM0,
|
||
", ".join("%s %d bytes" % (e.secname(i), e.sh[i]["size"]) for i in nobits)))
|
||
try:
|
||
plans = analyze(obj, goff + EXE_VRAM0, game, EXE_VRAM0, elf=e)
|
||
except SplitRefused as ex:
|
||
print(" SPLITTABLE: NO — %s" % ex)
|
||
return 1
|
||
if not plans:
|
||
print(" no references through a NOBITS section symbol — nothing blocking a NOLOAD placement here")
|
||
return 0
|
||
print("\n".join(report(plans, a.object).splitlines()[1:])) # report()'s first line repeats the header
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|