Files
BFM-decomp/docs/psyq-worklist.md
T
Drew T 757bd82a0f feat(phase-31): S79 #4 — scattered-.bss split at link-prepare (psyq_bss_split): SYS.o→libgpu2, VM_F.o→snd12, GS_001.o→libgs8 LINKED; libgpu_used retired
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 🛑.
2026-09-04 17:19:29 -06:00

23 KiB
Raw Blame History

PsyQ library integration worklist (Phase 8)

Byte-confirmed footprint map of the remaining PsyQ 4.0 libraries linked by SLUS_007.26. Generated by psyq_identify.py over the original EXE (extracted/retail/SLUS_007.26, text 0x80010000..0x800629DC); raw per-object placements in .run/survey/<lib>.txt (regenerable: for L in …; do python3 tools/psyq_identify.py .run/obj40/$L --vram-base 0x8000F800 --exe extracted/retail/SLUS_007.26; done). Replaces the Phase-7 ~350-fn estimate (R14). Placement = candidate footprint; per-object byte-identity is confirmed at integration time by psyq_link_region.py --verify before wiring (the real gate, G3/P9).

DONE (Gen1/Phase 7): libcd (18 objs) + libgs (31 objs) linked byte-identical.

Footprint summary (remaining libraries)

Order Library Placed / total Region(s) ~banked B Difficulty notes
1 libgpu 3 / 12 800b2 (0x80058890) ~14.9k ✅ DONE (T4 + S78 #4): EXT+PRIM+SYS linked (raw .run/obj40/libgpu, no curated dir). SYS.o was EXCLUDED Phase 8→P31 S78 as scattered-.bss (§9.1); linked since S78 #4 via the link-prepare .bss split (cookbook §489) as libgpu2.
2 libetc 5 / 7 800 tail (0x8004239C) ~3.3k VSYNC/INTR/INTR_VB/INTR_DMA/VMODE contiguous, ends exactly at libcd1 (0x80043088). Clean.
3 libmcrd 2 / 2 800b2 (0x8005FC68,0x80062888) ~9.0k LIBMCRD.o (2186 ins, huge, holds the 55 LIBMCRD_OBJ_*) + USERFUNC.o — 2 blocks. _card_* h_norm dups collapse inside LIBMCRD.
4 libc2 17 / 46 800b2 (0x8005C2C8–0x5CD98 + STRCAT 0x80061E90) ~3.1k C stdlib (BZERO/MEMCPY/STRCMP/PRINTF/PRNT…); contiguous run + 1 outlier. PRNT.o (418 ins) has an internal jtbl — verify NOLOAD .rodata placement (the PRNT_OBJ_24C rodata note).
5+6 libapi+libcard 22 (800c2) 800c2 (0x80061F38–0x80062888) ~0.7k ✅ DONE (combined apicard region): 22 objs / 4 blocks / +24 fns (tools/make_apicard_used.py, C112 dedup, 0 exclusions). libapi's ~22 objects in the 800c3 region (0x5CE18..) DEFERRED — lowest value, separate resegmentation.
7+8 libspu+libsnd 63 / (38+32) 800 sound (0x8003A444–0x8004239C) ~24k ✅ DONE (combined region; S78 #3/#4): 63 objs / 12 blocks. The two libs interleave, so linked as ONE region (tools/make_snd_used.py dedups + excludes 3 addresses; gen_lib_subsegs.py + window). Excluded: S_R/S_W 0x3C438, S_GRMDT* 0x3D424 (cross-object commons — these objects have NO .bss, so the S78 split cannot apply), S_IH/UT_RON 0x3D94C (false-positive, inside SSSTART). VM_F 0x3FA64 (237 ins) rejoined in S78 #4 (snd12, .bss split at _svm_sreg_buf). SSGM.o 0x1BD80 deferred (isolated in matched-C, 8 ins).
9 libgte 58 / 381 800b (0x8004787C–0x5082C) + libgs gaps ~48k ✅ DONE (T11): 53 objs / 22 blocks linked in 800b (subsegs via gen_lib_subsegs.py; integrate window 0x4787C..0x51804). 5 libgs-gap objects DEFERRED (MTX_05/07/11/REG03/REG11 → gsgap1/2/4/5 stay stubs; gsgap2≠MTX_07 exactly so needs sub-split).

SKIP — zero footprint (recorded, not linked by the EXE): libmath 0/48, libc 0/56, libsn 0/51 (2 tiny ambiguous). BFM links libc2, not libc; no libmath/libsn. (Also unbuilt/no-footprint: libcomb, libds, libgun, libsio, libtap, libpress — never converted, no symbols.)

Region map (where each library lives, relative to the current splat subsegs)

800   (0x800123F0–0x80043088)  game code + libsnd(SSGM @1BD80; 3D454–42374) + libspu(3A444–422E8) + libetc(4239C–43088, tail)
libcd (0x80043088–0x8004787C)  DONE
800b  (0x8004787C–0x80051804)  libgte(4787C–5082C, multi-block) + game-code tail
libgs (0x8005080C–0x80057928)  DONE; gaps gsgap1/2/4/5 are actually libgte (MTX_05/07/11, REG03/11; wired S78 #3 as libgte27-30); gsgap3=GS_001 linked S78 #4 as libgs8 (six-piece `.bss` split)
800b2 (0x80057928–0x800629DC)  game code + libgpu(58890–5Bxxx) + libc2(5C2C8–5CD98,+STRCAT 61E90) + libapi(5CE18–626B8 scattered) + libmcrd(5FC68 + 62888) + libcard(61F38–62808)

Integration order rationale (ascending difficulty)

  • 1–2 (libgpu, libetc): clean single contiguous blocks, minimal interleaving → re-validate the §9 mechanism end-to-end on a fresh library before the hard ones.
  • 3–4 (libmcrd, libc2): few blocks, contained in 800b2; libc2 introduces the PRNT internal-jtbl wrinkle.
  • 5–6 (libcard, libapi): short syscall stubs + cross-library aliases + scatter in 800b2 → need windowed placement + curated _used dirs.
  • 7–8 (libspu, libsnd): the interleaved sound pair in the 800 subseg (which still holds the 43 REAL game-code matches lower down) → split_src_region.py trims, windowed placement, alias disambiguation.
  • 9 (libgte): largest, multi-region (800b + 4 libgs gaps), game-code-interleaved blocks → last.

Per-library integration recipe (each = one byte-gated task, §9.3/§9.5)

Phase 9: all these tools now REQUIRE --vram-base 0x8000F800 --exe extracted/retail/SLUS_007.26 (the EXE's values; psyq_integrate also --symbols config/symbols.us.txt). For a Gen2 binary, pass its own --vram-base/--exe/--symbols (cookbook §9.7, SETUP §6.7).

  1. psyq_link_region.py .run/obj40/<lib> <lo> <hi> --vram-base 0x8000F800 --exe extracted/retail/SLUS_007.26 [--emit <p>] → confirm per-object byte-identity ("per-object .text byte-identical: True") + the block/gap structure (G1: derive blocks empirically, don't trust this table's addresses).
  2. Curate .run/obj40/<lib>_used (drop cross-library aliases / keep the variant whose .data/.rdata also matches) when aliases exist (make_libgs.sh pattern).
  3. Resegment config/splat.us.exe.yaml (one c subseg per contiguous block; non-library gaps stay stubs); split_src_region.py trim the affected game-code .c; make extract.
  4. Makefile: add LIB<X>_ELF/OBJDIR/SYMS + a gated psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) … <stubs> [lo hi] call + -T $(LIB<X>_SYMS) on the ld line.
  5. Add recovered SDK names to config/symbols.us.txt (mirror to Ghidra, R15/G6); update progress.py's LINKED config.
  6. Dual byte-gate: make clean && extract && build && check → 143dbb89… WITH .run/obj40/<lib> present AND with it absent (stub fallback). make report LINKED up. Commit (R20).

Cross-cutting: windowed placement is mandatory for the short-object / interleaved libraries (the libgs GS_106 precedent); cross-library address collisions (C112 libapi/libcard; UT_RON/S_IH; S_R/S_W) mean total distinct objects < the 199 summed placements — byte-verify decides ownership. Any object that won't link byte-identical after §9.1/§9.4 stays a documented honest stub (does not block exit, P9).

⚠️ Boundary gotcha (found at libc2/T6): the game-code subseg boundary AFTER a library block must be the last object's .text SECTION size (8-aligned), NOT its instruction count × 4. psyq_identify reports the instruction count, which omits the object's trailing 8-byte alignment padding (e.g. libc2 SETJMP.o: 30 ins = 0x78, but .text = 0x80 → block ends 8 bytes later). A too-low boundary overlaps the object's padded tail and the relink inserts +N padding, shifting the whole downstream image (pervasive 1-byte reloc diffs + grown file). Verify the boundary = last_obj.vram + readelf .text size for any library whose last object isn't followed object-to-object. Also: when a library block sits at a subseg's START (no < lo prefix to keep), rm the old .c so splat regenerates it under the new boundaries (split_src_region.trim would leave it header-only).

Excluded / deferred objects (§9.1 scattered-.bss) — honest stubs, byte-identical via asm

The standard mechanism places each object's .bss as ONE NOLOAD section at a single base. An object whose .bss commons the original linker scattered to non-contiguous addresses (referenced as .bss+offset by a single section symbol) cannot be reproduced by one base → excluded, kept as an INCLUDE_ASM stub (still byte-identical). Diagnose: psyq_link_region.py <dir> shows N words differ in the object, and its .bss references resolve to >1 base in the EXE. Curate the library's _used dir to drop these.

Object Library Evidence Status
GS_001.o libgs scattered .bss (Phase 7) — SIX bases LINKED S78 #4 (libgs8): psyq_bss_split cuts the section into six single-base pieces at symbol starts (PSDBASEX, CLIP2, PSDBASEY, POSITION, GsDRAWENV); the S77 probe's "5 interleaved bases, NOT splittable" grouped by base instead of by run (cookbook §489)
SYS.o (3109 ins) libgpu .bss+0x150 → 0x800c551c but base recovered 0x80078830; commons scattered 0x80078xxx/0x800c5xxx LINKED S78 #4 (libgpu2): .bss [0,0x144) @0x80078830 + .bss2 = _que @0x800C5510
MTX_05/07/11,REG03,REG11 libgte sit in libgs gaps gsgap1/2/4/5; gsgap2(48B)≠MTX_07(36B) so the gap stub needs a sub-split deferred (T11); small GTE fns; link byte-identical, just need the gsgap region resegmented (low priority)
0x3C438,0x3D424,0x3D94C (S_R/S_GRMDT/S_IH) libspu/snd cross-object commons referenced at a minority address (S_R/S_GRMDT — these objects have NO .bss of their own, so the S78 split does not apply) + a false placement inside SSSTART (S_IH) excluded (sound region); stay stubs (24+4+24 ins). VM_F 0x3FA64 (237 ins) left this row in S78 #4 → snd12
SSGM.o libsnd isolated @0x1BD80, inside the matched-C region (near func_8001Bxxx) deferred; 8 ins; would need a 1-object carve amid matched C
libapi 800c3 cluster (~22 objs) libapi C57..L10/L02/L03 @0x5CE18.. in the 800c3 region (separate from the 800c2 apicard region) deferred; ~22 4-ins BIOS syscall stubs; lowest value; another region resegmentation

If scattered-.bss proves prevalent across libgte/libspu/libsnd, escalate to a Max general fix (split each object's .bss into per-common NOLOAD sections at their EXE-resolved addresses); otherwise excluding the few affected objects is the GS_001-precedent decision. Done in P31 S78 #4 — exactly that fix, twenty-three phases later: tools/psyq_bss_split.py runs inside the link-prepare step of psyq_link / psyq_link_region / psyq_integrate and tiles any such section into per-base NOBITS pieces from the bytes (cookbook §489). All three excluded objects link byte-identical; 235 placed objects across the 9 curated dirs, 0 refusals (R39 negative control).


The RESIDENT has no linkable PsyQ footprint — it is custom engine code (Phase 12 T1, byte-verified)

Pre-Phase-12 assumption (now corrected): because the resident detects as PsyQ 4.7.0 (DetectPsyQ + a DsMix/libsnd signature hit), it was expected to link embedded 4.7 libsnd/libgte/libspu (the Phase-11 close-out + R24 "link the resident's 4.7 libs" opener). The bytes refuted this.

Phase 12 T1 survey ran psyq_identify over the 4.7 objects (tools/psyq/conv47/…/lib/, already ELF — ar x into .run/obj47/<lib>/, no psyq-obj-parser needed) and the 4.0 objects, against the resident (MAIN.CD/FILE_010/1.1, --vram-base 0x800CEDF8), over the code window and the whole file. Result — NIL footprint:

Lib (4.7) placed Lib (4.0) placed
libsnd 1/226 (ut_rev_2.o, 4 ins) libsnd 2/163 (VM_DON.o 5 ins, VM_DOFF.o 4 ins)
libspu 0/134 libspu 0/129
libgte 0/509 libgte 0/381
libgpu 0/61
libcd/libmath/libds 1 each (≤8 ins, two alias the same DsMix addr)

All hits are the §9.5 short-object coincidental class (≤8 ins, masked patterns match by luck). Tool sanity (the negative is real): 4.0 libsnd vs the EXE snd region = 35/163 placed — the tool works and is version-sensitive; the resident genuinely embeds no stock PsyQ object of either version.

Ghidra corroboration (G1, sampled): DsMix decompiles to { FUN_800d1bf8(); return 1; } — a custom 2-line wrapper that ignores its vol arg (NOT the stock libsnd DsMix; the R13 tag in symbols.resident.txt is refuted). Other sampled functions are game logic (global accessors, engine init calling EXE REAL matches, entity-heading math calling the EXE's libgte RATAN). The resident's code makes 61 distinct EXE-range jal calls (vs 37 internal) — it calls the EXE's resident SDK rather than embedding it.

Conclusion / architecture: the PsyQ SDK lives in the EXE (959 LINKED); the resident is ~143 functions of custom engine code that calls the EXE's SDK + engine via fixed addresses (no RAM-wasting SDK duplication in an always-loaded blob). The DetectPsyQ "4.7.0" was a single coincidental DsMix-region signature, not a linked footprint. Phase 12 matches the resident engine by hand (REAL), not by linking (LINKED stays 0). R24's per-binary-provenance principle holds, but for the resident the practical consequence is "nothing to link." (Regenerate: for L in libsnd libspu libgte libgpu; do d=.run/obj47/$L; mkdir -p $d; (cd $d && ar x ../../tools/psyq/conv47/psyq-4_7-converted/lib/$L.a); python3 tools/psyq_identify.py $d 0x800CEDFC 0x800D3408 --vram-base 0x800CEDF8 --exe extracted/retail/MAIN.CD.dir/FILE_010.dir/1.1; done)


S78 (2026-09-04): the 800c3 "wall" band is LIBPAD 4.2.1 + LIBAPI 4.2, and the LINKED residue is now printed by the build

How it was identified (a free oracle nobody had used). ghidra_psx_ldr ships per-version PsyQ signature sets — ~/ghidra_12.1_PUBLIC/Ghidra/Extensions/ghidra_psx_ldr/data/psyq/<ver>/<LIB>.LIB.json, one masked-byte signature + function labels per OBJECT, for 2.6 → 4.7. Matched against the retail EXE bytes (cookbook §487), the 4.2 set places these byte-exact:

object vram ins functions (labels)
LIBAPI COUNTER.OBJ 0x8005CF68 92 SetRCnt · GetRCnt · StartRCnt · StopRCnt · ResetRCnt
LIBAPI C114.OBJ 0x8005CE48 8 _96_remove — the 4.2 Ps stamp (SETUP §5.1 "libnum 0")
LIBPAD PADENTRY.OBJ 0x8005D0D8 300 PadChkVsync · PadStartCom · PadStopCom · PadChkMtap · PadGetState · PadInfoMode · PadInfoAct · PadInfoComb · PadSetActAlign · PadSetMainMode · PadSetAct
LIBPAD PADMAIN.OBJ (4.2.1) 0x8005D588 ~756 PadEnableCom · _padSetVsyncParam · _padChkVsync · _padStartCom · _padStopCom · _padInitSioMode · _padSioRW (+ _padSioRW2/_padClrIntSio0/_padWaitRXready, order-inferred: the 4.2 sig drifts +4/+12 here — the EXE holds the 4.2.1 revision)
LIBAPI L02.OBJ/L03.OBJ 0x8005E168 4+4 SysEnqIntRP · SysDeqIntRP
LIBPAD PADCMD.OBJ 0x8005E188 600 _padSetAct · _padSetCmd · _padSendAtLoadInfo · _padRecvAtLoadInfo · _padGetActSize · _padLoadActInfo · _padSetActAlign · _padSetMainMode · _padCmdParaMode
LIBPAD PADIF.OBJ 0x8005EAE8 ~376 statics only (no public labels): func_8005EAE8/EB28/EC00/ECC0/ED4C
LIBPAD PADPORTD.OBJ 0x8005F0C8 408 PadInitDirect
LIBPAD PADSEQD.OBJ 0x8005F728 288 _padInitDirSeq · _dirFailAuto
LIBPAD WAITRC2.OBJ (4.3 sig) 0x8005FBA8 48 setRC2wait · chkRC2wait
LIBAPI FIRST.OBJ 0x80061FA8 168 firstfile (the 4.2 C wrapper; 4.0's A66.o trampoline at 0x80062248 is firstfile2 in 4.2 naming)
LIBAPI PAD.OBJ 0x80062388 192 SetInitPadFlag · ReadInitPadFlag · PAD_init · InitPAD · StartPAD · StopPAD
LIBAPI PATCH.OBJ 0x800626C8 40 EnablePAD · DisablePAD · _patch_pad
LIBAPI CHCLRPAD.OBJ 0x80062768 28 _remove_ChgclrPAD

The 4.2.1x Ps stamp at 0x80072954 ("libnum 12") stands directly in front of libpad's .data — the callback tables D_8007295C..D_800729D8 the band's functions index. So all 12 of main's open stubs in 800c3 (incl. the four §332 "%lo-in-a-delay-slot" walls: _padInitSioMode, _padStartCom, func_8005ED4C, func_8005F450) and the 8 SDK-C-REORDER verbatims are Sony library code assembled in reorder mode — §332b's mechanism, with its provenance. Names applied to config/symbols.us.txt + Ghidra (46; firstfile after retiring Ghidra's 4.0-sig firstfile at 0x80062248 → firstfile2).

Which archives hold it. Placed with psyq_identify after converting each to ELF: PsyQ 4.0 (lib40/, no LIBPAD at all), 4.6 (lib46/, S78 fetch), 4.7 (conv47/): only PDMAIINI.o (4.6/4.7, 68 ins @0x8005D8B4 = _padStartCom+_padStopCom) is byte-identical in the band; 4.6/4.7 libpad otherwise differs (PADMAIN 884 ins in 4.4+, PADCMD 768). The loader's 4.3 signature set matches PADENTRY/PADCMD/PADPORTD/WAITRC2 but not PADSEQD (292 vs 288). A 4.2.1 or 4.3 LIBPAD.LIB is the archive that would LINK the whole band (task #13); until then the band is matched as C under the REORDER_TUS island with the real names.

The LINKED residue, now a build output. psyq_integrate --yaml (S78) wires only the objects inside each library's stub subsegs and PRINTS the rest as ~~ N located object(s) / M ins OUTSIDE the stub subsegs — the completion contract's "SDK residue empties" line, read straight off make build. At S78 close it reads, for libgte: 13 objects / 1,264 ins — MSC01/02/05/09 (800b, 276 ins, 100% of that "game code" subseg), SMP_00 (800b_2), SMP_05 (800b_3), FGO_01–06 (800b_5, 804 ins, 100%), PATCHGTE (800b_6, 40 ins, 100%). Plus, outside libgte's window: SYS.o (800c, 3,109), 2D_BG0/1 (800b_7, 1,022), VM_NO1 (sgap_7, 305), VM_NOWON (sgap_8, 300), VM_F (sgap_6, 237), libapi's 800c3 trampolines (176), FIRST.o (168). Task #3/#4 wire these.

Why the build was RED at HEAD (found S78). The S77 psyq_identify fix (§485) started locating those in-gap objects, and psyq_integrate.contiguous_blocks() then merged libgte's 22 stub blocks into 3 (3 object blocks but 22 stubs) — every make build BINARY=main with .run/obj40/libgte present died at the link. It passed the S77 gates because the gate worktrees have no .run/obj40 and take the stub fallback. Two fixes, both in psyq_integrate.py: (1) --yaml: stub↔objects by SUBSEG RANGE with an exact-tiling check, residue printed; (2) a library object's DEFINED symbol whose recovered address the curated file names differently is --redefine-sym'd to the curated name (R15) — A66.o firstfile→firstfile2, and it also exposed TOC.o CdGetToc@0x800430B8 mis-curated as DecDCToutCallback (an xdedup-vs-VS mislabel; libcd 4.0's linked object is the stronger oracle).

S78 task #3 — thirteen "game code" subsegs were library objects: wired as LINKED (exact tiles, 0 tokens)

new block was objects ins note
libgte23 800b MSC01, MSC02, MSC05, MSC09 276 GTE macro wrappers (SetRGBfifo…) that had been hand-matched as REAL
libgte24 800b_2 SMP_00 132
libgte9 (re-derived) 800b_3 + libgte9 + 800b_4 SMP_05 (NormalClip) 12 SMP_06 NormalClipS is its nested sub-pattern; the "3-nop NOTCODE-PAD" was the object's alignment
libgte25 800b_5 FGO_01…FGO_06 804
libgte26 800b_6 PATCHGTE 40 .sbss 0x10, single base
libgte27/28/29 gsgap1/2/4 MTX_05 / MTX_07 / MTX_11 20/12/12 the Phase-8 "deferred (T11)" libgs-gap objects
libgte30 gsgap5 REG03 + REG11 76
libgs7 800b_7 2D_BG0 + 2D_BG1 1022 §485's headline find
snd10 sgap_7 VM_NO1 305
snd11 sgap_8 (head) VM_NOWON 300 sgap_8 now starts at 0x80040DE8 with its game C

Result: libgte 70 objects / 30 blocks (was 53/22), libgs 33 / 7, sound 62 / 11; LINKED fns 959 → 1,040; REAL 912 → 886 (26 inline-asm SDK wrappers re-provenanced); VERBATIM 146 → 85; 13 TUs deleted; main 143dbb89 byte-identical with and without the SDK objects (the fallback had been red since S7x: CdReadyCallback was called by its SDK name but the libcd stub carried func_800435B4 — curated now). Remaining located-but-unwired: SYS.o 3,109 + VM_F 237 (both .bss-splittable, task #4), the libpad/libapi band pieces (task #5), SSGM.o 8, and the genuine scattered-.bss walls GS_001 / S_R / S_GRMDT.

The metric correction this exposed (R35). progress.py's "MAIN game-code weighted" line claimed its sig excluded the LINKED objects; it never did — the 2026-08-05 Ghidra sig carried all ~31,000 linked-SDK instructions, whose stub records read as unmatched game code. Honest figure, LINKED now excluded live from the Makefile stub lists + yaml ranges: 91.8% (44,562 / 48,537), not 59.8%; the 3,975-ins remainder equals the sum of main's open stubs in frontier_classify exactly.

new block was object ins .bss pieces (cut → base) note
libgpu2 800c (56 hand-matched SDK fns + 62 verbatim frags) SYS.o 3,109 .bss [0,0x144) → 0x80078830 · .bss2 = _que → 0x800C5510 _que recovered BY NAME from SYS.o's own 4 named refs = 0x800C5510 — the cut is confirmed by an independent oracle
snd12 sgap_6 tail (hand-matched func_8003FA64 = _SsVmFlush) VM_F.o 237 .bss [0,0x508) → 0x80079580 · .bss2 = _svm_sreg_buf → 0x800B9B58 62 other sound objects recover _svm_sreg_buf = 0x800B9B58
libgs8 gsgap3 (hand-matched as game C: func_800525DC…) GS_001.o 384 six pieces: 0x80078810 · PSDBASEX 0x800A4F3C · CLIP2 0x800AE820 · PSDBASEY 0x800A4F40 · POSITION 0x800A5E50 · GsDRAWENV 0x800A6438 the S77 probe called this "5 interleaved bases, NOT splittable" — it grouped by BASE; by RUN it is six symbol-aligned pieces, and the other libgs objects recover all five cut symbols at exactly those addresses

Mechanism (tools/psyq_bss_split.py, cookbook §489): a pure-Python ELF32 REL rewrite that derives each reference's base from the game bytes, walks the references in offset order, cuts at symbol starts between runs, moves the symbols, inserts a LOCAL section symbol per piece, retargets the relocations with the addend rewritten in the instruction immediates, and self-diffs. It runs inside the shared link-prepare step (psyq_link.link_object, psyq_link_region.build_region, psyq_integrate.integrate), derived from the bytes on every build — nothing recorded, nothing to go stale. Negative control (R39): 235 placed objects across the 9 curated dirs, 0 refusals, exactly these 3 splits; an end-of-buffer reference (.bss + size, libcd) caught the first bounds check and is why problems are fatal only when a split is actually needed.

Wiring: LIBGPU_ELF is the raw .run/obj40/libgpu (the libgpu_used dir existed only to exclude SYS.o — retired); libgs 34 objects / 8 blocks (make_libgs.sh +GS_001); sound 63 / 12 (make_snd_used.py exclusions 4 → 3). Three TUs went: src/800c.c, src/gsgap3.c, and the _SsVmFlush body of src/sgap_6.c (its 4-ins game function stays). Main 143dbb89 byte-identical WITH and WITHOUT the SDK objects (fallback from a fresh extract). Remaining located-but-unwired SDK code in main: the libpad/libapi band pieces (task #5), SSGM.o 8 ins, and the two cross-object-common walls S_R/S_W + S_GRMDT* (no .bss of their own — a different class from this one).