chore(phase-30 S45 II.2): retirements (R33) + SETUP module recipe

- DELETED: disc_code_sweep.py (superseded by disc_audit/make audit-disc), reconcile_decls.py
  (superseded by reconcile_tu; incumbent row removed from cdecl audit_differential — the
  differential existed to prove this deletion safe), rollout_801457a4_o0/rollout_whale_o0/
  rollout_o0_cluster one-shots (rollout_o0.py is the live generic), ImportOverlay.java +
  VerifyOverlay.java (ghidra_import_raw.sh is the live path)
- reference check first (R14): the plan's 'zero build refs' was wrong for 3 — comment refs
  annotated, the one LIVE import (cdecl) reworked; audit-cdecl + tools-health re-proven green
- SETUP §6.7: module-class recipe (TEXT_LO derivation, paired-.rodata hdr carve, A4 symbol-
  window law, ELF-seeded sig-modules) + new_binary.sh inventory row + 3 RETIRED rows (R21);
  disc-completeness Reproduce marked retired
This commit is contained in:
Drew T
2026-08-06 13:34:50 -06:00
parent 4cadac4e11
commit a0f07d629e
12 changed files with 46 additions and 872 deletions
+2 -2
View File
@@ -661,7 +661,7 @@ build/src/ov_SC01_077/ov_SC01_077_o0.o: CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=
# Phase-24: the whale func_80144B9C is a 2nd -O0 region (0x80144B9C..0x801457A4) present in EVERY
# overlay (reach-134), carved into its own object <ov>_o0b by each overlay's splat config; the
# struct-assign memcpy matches only at -O0. One wildcard rule -O0-compiles all overlays' _o0b.o
# (the ×134 rollout; tools/rollout_whale_o0.py). All share src/shared/func_80144B9C.h.
# (the ×134 rollout; tools/rollout_whale_o0.py — one-shot, retired S45). All share src/shared/func_80144B9C.h.
#
# P30 T2: the glob is `_o0?` (was `_o0b`) so ANY lettered -O0 sub-split is covered by this one rule.
# A 4th -O0 region was found inside an -O2 jr split (0x80183CF0..0x80184920, 15 contiguous fns in
@@ -674,7 +674,7 @@ WHALE_O0B_OBJS := $(patsubst src/%.c,build/src/%.o,$(wildcard src/ov_*/ov_*_o0?.
$(WHALE_O0B_OBJS): CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
# Phase-29 T2 Arm A: the -O0 cluster (0x8013B568..0x8013C98C) carved per single-file overlay into
# <ov>_o0.o (tools/rollout_o0_cluster.py) — same per-file -O0 mechanism so its h_seq family members
# <ov>_o0.o (tools/rollout_o0_cluster.py — one-shot, retired S45; the generic driver is tools/rollout_o0.py) — same per-file -O0 mechanism so its h_seq family members
# bank whole-binary (the Task-1 swing verdict: they masked-MATCH only at -O0). One wildcard rule
# -O0-compiles every overlay's _o0.o; ov_SC01_077_o0.o already has its explicit rule above (filtered
# out to avoid a duplicate target-specific assignment). `*_o0.c` never matches the whale's `*_o0b.c`.
+25 -3
View File
@@ -571,6 +571,27 @@ The reusable **flat-blob recipe** (every Gen2 overlay follows it):
- Per-binary `<bin>_GHIDRA_PROG` → `make sig-refresh BINARY=<bin>`; `diff_settings.py` + the three
report scripts gain a `<bin>` entry; `make expected` is per-binary-safe (merge-copy, no sibling clobber).
**Module-class binaries — `md_*` (P30 S44/S45; the §S44 loader table, `docs/memory-map.md`):**
the small type-1 payloads (per-actor modules, the SC07 endgame pair) load at their OWN statically
derived slots (A `0x800CAE08` · B `0x800CCB1C` · boot/resident `0x800CEDF8` · SC07 `0x801A00D8`),
not the shared overlay slot. Onboard with **`tools/new_binary.sh <alias> <payload> <VRAM>
[TEXT_LO]`** (the generalized `new_overlay.sh`; registry `config/modules.mk` / `MODULE_BINARIES`).
Module-specific facts the recipe encodes:
- **TEXT_LO ≠ 0** (the §154 module-id law: payload word0 is a global module id, sometimes followed
by a fn-ptr table and/or data): derive per payload from the first-prologue scan (`27BDxxxx`) and
the min fn-ptr-table target — NOT min-table alone (functions can precede the lowest table entry:
the SC07 pair's real code start is 0xFC/0x158, their min table targets 0x930/0x370).
- **The header carve is a dot-typed `.rodata` PAIRED with the c segment** (same name), never a
standalone `rodata, hdr` object and never `bin`: a module header can hold a function's JUMP
TABLE, whose `.L` labels only resolve when jtbl and function assemble in the SAME object (the
EXE `[0x63238,.rodata,800]` precedent); `bin` assets link in the data block (wrong placement).
- **A4 symbol-window law:** a module whose window lies INSIDE another binary's symbol region must
NOT stack that binary's symbol file — the boot trio (`0x800CEDF8`) omits `symbols.resident.txt`
(DsMix @0x800D1BD8 minted a phantom fn boundary in md_MAIN_011 before this).
- **`make sig-modules`** signs every module at its own vram/TEXT_LO, seeding from the built ELF's
`func_*` symbols when a build exists (bootstrap's linear partition glues adjacent functions
around jtbl dispatch); fresh-clone fallback is `--bootstrap`, self-healing on the next run.
### §6.8 Cross-binary dedup & code-sharing (Phase 11) — "one match unlocks many"
Full how-to in `docs/matching-cookbook.md` §11. Command crib:
- **`make sig-overlays`** — Ghidra-FREE sign all 134 location overlays (`SCxx 0.4.dec`) at the shared overlay
@@ -642,8 +663,8 @@ Every script under `tools/` (plus the two report make-targets), grouped by purpo
| | `ExportSymbols.java` | Dump curated symbols (feeds `config/symbols.us.txt`, R15). |
| | `DumpProgramInfo.java` | Dump program metadata (loader, language, ImageBase, function count). |
| | `DumpFunctionSignatures.java` | Dump function signatures (feeds `make sig-refresh`). |
| | `ImportOverlay.java` | Import an overlay segment into the project. |
| | `VerifyOverlay.java` | Verify an imported overlay against expected bytes. |
| | `ImportOverlay.java` | **RETIRED (S45, R33)** — 1-overlay-era hardcoded import; `tools/ghidra_import_raw.sh` is the live path. |
| | `VerifyOverlay.java` | **RETIRED (S45, R33)** — companion of ImportOverlay.java; retired with it. |
| | `GetSymbolAt.java` | Read the symbol at a given address (scripted lookup). |
| | `DecompileAt.java` | Decompile the function at a given address (scripted scaffold). |
| | `DefineFunctions.java` | Disassemble + create functions at splat's validated entry points (`.run/<prog>_funcs.txt`) — completes a raw-blob program's function set (Phase 10). |
@@ -664,7 +685,8 @@ Every script under `tools/` (plus the two report make-targets), grouped by purpo
| | `tools/permuter/` | decomp-permuter harness (PERM_ recipes/weights) for stubborn near-misses. |
| | `diff_settings.py` *(repo root)* | asm-differ config (arch `mipsel`, object mode vs `expected/`). |
| | `tools/new_overlay.sh` | One-command location-overlay onboarding: `<SCxx> <FILE_nnn> [ENTRY]` — instantiate `config/splat.<ov>.yaml` from the template (+ non-4-aligned `bin` carve), register the binary in `config/overlays.mk` + the report/diff dicts, `make extract && build` byte-check. Idempotent (Phase 13, cookbook §13). **Phase-27 T7:** the optional `ENTRY` arg (default `0.4`) reaches a non-`0.4.dec` payload — the 4 SC07 overlays put code at PAC entry 1 (`1.4`). difficulty.py dropped from the insertion set (it derives now, T6). |
| | `tools/disc_code_sweep.py` | **(Phase-27 T7)** Disc-completeness audit: decode every extracted PAC payload (reusing `sig_image.make_insn`) and flag code-bearing ones by BOTH `valid ≥ 0.90` AND `jr $ra` density `≥ 0.01` (the `jr $ra` gate is decisive — type-0/2 data decodes ~100% "valid" but has 0 returns). Reconciles the onboarded set against the disc — the R34 answer to "what code did nobody onboard". Findings → `docs/disc-completeness.md` (138/138 type-4 complete; **39 un-onboarded type-1 resident-class modules** pending load-address RE). |
| | `tools/disc_code_sweep.py` | **RETIRED (S45, R33)** — superseded by `tools/disc_audit.py` / `make audit-disc` (whole-payload, BOTH raw+LZSS layers, residue-0 partition, claimed-by derived from `config/check.<bin>.sha`). The sweep read only the RAW layer through a 4,096-word window and had no notion of a claim; its historical findings are preserved in `docs/disc-completeness.md`. |
| | `tools/new_binary.sh` | **(P30 S44/S45)** One-command onboarding for ANY flat-blob binary class: `<alias> <payload> <VRAM> [TEXT_LO]` — ov_* (overlay slot, registry `overlays.mk`) or md_* (own §S44 slot, registry `modules.mk`). Signs (TEXT_LO-aware §154), instantiates the shared template (non-zero TEXT_LO ⇒ paired-`.rodata` header carve), writes check.sha + symbols, registers in the registry + the 3 report/diff dicts (sentinel-anchored, ast-checked), extract+build byte-check. Idempotent. `new_overlay.sh` is now a thin wrapper over it. §6.7 module recipe. |
| **PsyQ library linking** (cookbook §8/§9) | `tools/psyq_lib_split.py` | Split a PsyQ `.LIB` into per-object members. |
| | `tools/psyq_build_libs.sh` | Build the PsyQ libs from split members. |
| | `tools/psyq_identify.py` | Identify which SDK objects a region's functions belong to. |
+6 -2
View File
@@ -82,7 +82,11 @@ mostly-unmatched code to the denominator; every "×134" family propagation is no
honest direction — the prior number was measured over an incomplete disc.
## Reproduce
> **⚠️ S45: `tools/disc_code_sweep.py` is RETIRED (R33)** — superseded by `make audit-disc`
> (`tools/disc_audit.py`): whole-payload classification at BOTH the raw and LZSS layers, a
> residue-0 partition over the disc, and `claimed-by` derived from `config/check.<bin>.sha`.
> The commands below are kept for provenance of the 2026-07-15 findings; they no longer run.
```
tools/disc_code_sweep.py # the table above + the ranked hidden list
tools/disc_code_sweep.py --types 1 # just the type-1 code modules
tools/disc_code_sweep.py # (retired) the table above + the ranked hidden list
tools/disc_code_sweep.py --types 1 # (retired) just the type-1 code modules
```
+11 -1
View File
@@ -224,7 +224,17 @@ stub on a named wall/behemoth/queue ledger** — 140/140 byte-identical througho
(12,682,962/13,487,439) · 95.96% fn-count · 87.6% distinct (78,596/90,143 uniq).** The drop
from S44's 94.4% is the denominator growing +52k module ins (honest direction, P27/S39/S44
precedent).
- **II.2** (retirements + SETUP module recipe) — next.
- **II.2 ✅ — retirements (R33) + SETUP module recipe.** Deleted: `disc_code_sweep.py` (superseded
by `disc_audit.py`/`make audit-disc`), `reconcile_decls.py` (superseded by `reconcile_tu`;
its incumbent row removed from cdecl's differential — the audit's purpose was to enable exactly
this deletion), the 3 one-shot rollout drivers (`rollout_801457a4_o0` / `rollout_whale_o0` /
`rollout_o0_cluster` — `rollout_o0.py` is the live generic), `ImportOverlay.java` +
`VerifyOverlay.java` (`ghidra_import_raw.sh` is the live path). Reference check first: the
plan's "zero build refs" was wrong for 3 of them (comments + one live cdecl import — handled,
gate re-proven green). SETUP §6.7 gained the **module-class recipe** (TEXT_LO derivation law,
paired-.rodata header carve, A4 symbol-window law, ELF-seeded sig-modules) + inventory rows
(R21); disc-completeness Reproduce marked retired; Makefile comments annotated.
- **II.3** (metrics re-baseline + roadmap delta + decision-log) — next.
---
+2 -2
View File
@@ -1239,7 +1239,8 @@ def audit_differential():
tool. The reverse (symbols only this one sees) is the measured size of the hole."""
sys.path.insert(0, os.path.join(REPO, 'tools'))
import gen_harvest_targets as ght
import reconcile_decls as rd
# reconcile_decls RETIRED (S45, R33): its DATA_DECL_LINE_RE row proved the superset for
# 26-A..S44; the incumbent is deleted, so the differential now compares the survivors only.
import sig_unify as su
ec = os.path.join(REPO, 'src/shared/engine_core.h')
@@ -1268,7 +1269,6 @@ def audit_differential():
mine_d = {n for n in mine if n.startswith('D_')}
rows = []
for name, rx in (('gen_harvest_targets.DATA_DECL_RE', ght.DATA_DECL_RE),
('reconcile_decls.DATA_DECL_LINE_RE', rd.DATA_DECL_LINE_RE),
('sig_unify.DATA_DECL_RE', su.DATA_DECL_RE)):
theirs = set()
for m in rx.finditer(text):
-166
View File
@@ -1,166 +0,0 @@
#!/usr/bin/env python3
"""disc_code_sweep.py — find code-bearing PAC payloads the onboarding may have missed (Phase-27 T7).
The disc-completeness question the byte-gate is structurally blind to (R34): the build only touches
binaries someone ONBOARDED, so a code payload no one onboarded is invisible to `make check-all` no
matter how green it is. The Phase-27 audit found four such overlays (SC07 FILE_006/007/010/011, code
at PAC entry 1 not 0). This sweeps EVERY extracted PAC payload and reports which decode as MIPS code,
so the onboarded set can be reconciled against the disc rather than trusted.
Method (reuses sig_image's exact rabbitizer decode): decode the first `--window` words of each raw
payload as MIPS-LE, report the fraction rabbitizer calls valid. Overlay/EXE code runs ~0.97-1.00
valid; data/graphics/audio sit far lower. A payload above `--threshold` that is NOT already onboarded
is a candidate the audit must explain (onboard it, or record why it is not a build binary — e.g. a
type-1 blob that loads at its own address like the resident, needing load-address analysis first).
COVERAGE-ASSERTED (R32): every extracted `{index}.{type}` payload is classified; the onboarded set is
cross-checked so a hit that is already a binary is labelled, not re-flagged.
tools/disc_code_sweep.py # sweep all types, print the report
tools/disc_code_sweep.py --types 1,6,7 # only the non-type-4 unknowns
"""
import argparse
import glob
import os
import re
import struct
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "bfm_extract"))
import sig_image # make_insn — the shared rabbitizer decode (GTE-aware)
import lzss # decompress — the SAME game-semantics decoder the extractor uses
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# a raw payload is <dir>/<index>.<type> with NO .dec suffix; type-4 also has a .dec sibling
_PAYLOAD = re.compile(r"/(\d+)\.(\d+)$")
def onboarded_payloads():
"""The set of payload paths already wired as a build binary (main, resident, every overlay).
Read from the Makefile/overlays.mk *_EXE assignments — the single source of truth (R33).
KEYED BY THE RAW PAYLOAD PATH (Phase-28 T7). A type-4 overlay's `_EXE` is the DECOMPRESSED path
(`…/0.4.dec`), but this sweep iterates the RAW payloads (`…/0.4`) so it can decode the compressed
layer itself. Store the `.dec`-stripped form too, or all 138 type-4 overlays — now correctly seen
as code via decompression — would fail the onboarded match and flag as false HIDDEN hits."""
paths = set()
for mk in ("Makefile", "config/overlays.mk"):
p = os.path.join(REPO, mk)
if not os.path.exists(p):
continue
for m in re.finditer(r"^\w+_EXE\s*:=\s*(\S+)", open(p).read(), re.M):
norm = os.path.normpath(m.group(1))
paths.add(norm)
if norm.endswith(".dec"):
paths.add(norm[:-len(".dec")]) # the raw payload this .dec came from
return paths
_JR_RA = 0x03E00008 # `jr $ra` — a function return; the discriminator rabbitizer.isValid() lacks
def _signals(data, window):
n = min(window, len(data) // 4)
if n == 0:
return 0.0, 0.0, 0
ok = jr = 0
for k in range(n):
word = struct.unpack_from("<I", data, k * 4)[0]
if sig_image.make_insn(word, 0x80000000 + k * 4).isValid():
ok += 1
if word == _JR_RA:
jr += 1
return ok / n, jr / n, n
def code_signals(path, window):
"""(valid_ratio, jr_ra_density, nwords, layer). isValid() alone is too permissive — structured
DATA decodes ~100% valid (type-0/type-2 blobs hit 1.00 with ZERO returns). Real MIPS code carries
a `jr $ra` roughly once per function (~2.9-3.4% of words across the onboarded overlays + resident);
data carries ~0%. Requiring BOTH separates code from valid-looking data.
DECODES BOTH LAYERS (Phase-28 T7 — the fix). The original decoded only the RAW payload, which is
STRUCTURALLY BLIND to compressed code: type-4 overlay code is LZSS-compressed, so raw bytes are
noise and every type-4 row read "code 0" — a VACUOUS row for 138 known-code binaries. That is not
a curiosity: this tool is the disc-completeness oracle (R34), and it could NOT have found the 4
hidden SC07 overlays (their code is compressed like every type-4) — they were caught by
hand-reconciling 138-vs-134. It found the 39 type-1 modules ONLY because those happen to be
uncompressed. So decode both and take the stronger signal:
* uncompressed code (type-1 resident-class): lives in the RAW bytes.
* compressed code (type-4 overlays): lives in the LZSS-DECOMPRESSED bytes.
A payload is code if EITHER layer is code; `layer` records which, so the report is auditable."""
raw = open(path, "rb").read()
r_ratio, r_jr, r_n = _signals(raw, window)
best = (r_ratio, r_jr, r_n, "raw")
try:
res = lzss.decompress(raw)
dec = res.data if hasattr(res, "data") else bytes(res)
except Exception:
dec = b""
if len(dec) >= 64:
d_ratio, d_jr, d_n = _signals(dec, window)
# "stronger" = the layer that clears the code bar, or (if neither/both) the higher jr density
# (the discriminator). This never downgrades a raw-code hit to a decompressed non-hit.
if (d_jr, d_ratio) > (best[1], best[0]):
best = (d_ratio, d_jr, d_n, "dec")
return best
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--root", default="extracted/retail")
ap.add_argument("--types", default=None, help="comma list of PAC types to sweep (default: all)")
ap.add_argument("--window", type=int, default=4096, help="words decoded per payload")
ap.add_argument("--threshold", type=float, default=0.90, help="valid-ratio floor")
ap.add_argument("--jr-min", type=float, default=0.01, help="jr-$ra density floor (code ~0.03, data ~0)")
a = ap.parse_args()
want = set(a.types.split(",")) if a.types else None
onboard = onboarded_payloads()
rows, seen = [], 0
for p in sorted(glob.glob(os.path.join(REPO, a.root, "**"), recursive=True)):
if not os.path.isfile(p) or p.endswith(".dec"):
continue
m = _PAYLOAD.search(p)
if not m:
continue
typ = m.group(2)
if want and typ not in want:
continue
seen += 1
rel = os.path.relpath(p, REPO)
ratio, jr, nwords, layer = code_signals(p, a.window)
is_code = ratio >= a.threshold and jr >= a.jr_min # BOTH signals (jr is the discriminator)
rows.append((typ, ratio, jr, nwords, rel, os.path.normpath(rel) in onboard, is_code, layer))
assert len(rows) == seen, f"classified {len(rows)} of {seen} payloads — a silent skip (R32)"
hidden = [r for r in rows if r[6] and not r[5]]
by_type = {}
for typ, ratio, jr, _, _, onb, is_code, _layer in rows:
d = by_type.setdefault(typ, [0, 0, 0])
d[0] += 1
d[1] += is_code
d[2] += is_code and onb
print(f"disc code sweep — {seen} payloads under {a.root}"
+ (f" (types {a.types})" if want else "")
+ f", window={a.window}w, valid>={a.threshold}, jr_ra>={a.jr_min}")
print(f"{'type':>4} {'payloads':>9} {'code':>5} {'onboarded':>9} {'HIDDEN':>7}")
for typ in sorted(by_type):
tot, c, onb = by_type[typ]
print(f"{typ:>4} {tot:>9} {c:>5} {onb:>9} {c - onb:>7}")
print(f"\nHIDDEN code-bearing payloads (valid>={a.threshold} AND jr_ra>={a.jr_min}, NOT onboarded): {len(hidden)}")
for typ, ratio, jr, nwords, rel, _, _, layer in sorted(hidden, key=lambda r: -r[2]):
print(f" type {typ} valid {ratio:5.1%} jr_ra {jr:5.2%} ({nwords}w, {layer}) {rel}")
if not hidden:
print(" (none — the onboarded set accounts for every code-bearing payload the sweep sees)")
return 0 if not hidden else 3
if __name__ == "__main__":
sys.exit(main())
-62
View File
@@ -1,62 +0,0 @@
// ImportOverlay.java — headless postScript: load the T6b-proven resident blob + one location
// overlay into the program at their byte-verified vaddrs, so the overlay/resident code becomes
// disassemblable in Ghidra. The MCP tools cannot create memory blocks / bulk-load file bytes, so
// this is done headless (analyzeHeadless opens the project directly and saves on completion).
//
// ~/ghidra_12.1_PUBLIC/support/analyzeHeadless ~/bfm-decomp/ghidra bfm \
// -process SLUS_007.26 -noanalysis \
// -scriptPath ~/bfm-decomp/tools/ghidra_scripts -postScript ImportOverlay.java
//
// Idempotent: re-runs write into the blocks created on the first run.
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.mem.Memory;
import ghidra.program.model.mem.MemoryBlock;
import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
public class ImportOverlay extends GhidraScript {
public void run() throws Exception {
String home = System.getProperty("user.home");
// {block name, vaddr, extracted file}
String[][] targets = {
{"residentBlob", "0x800CEDF8", home + "/bfm-decomp/extracted/MAIN.CD.dir/FILE_010.dir/1.1"},
{"ovl_tutForest", "0x80128158", home + "/bfm-decomp/extracted/SC01.CD.dir/FILE_077.dir/0.4.dec"},
};
Memory mem = currentProgram.getMemory();
println("BFMOVL == existing memory blocks ==");
for (MemoryBlock b : mem.getBlocks())
println("BFMOVL " + b.getName() + " " + b.getStart() + "-" + b.getEnd()
+ " init=" + b.isInitialized());
for (String[] t : targets) {
String name = t[0];
Address addr = toAddr(t[1]);
byte[] bytes = Files.readAllBytes(Paths.get(t[2]));
MemoryBlock existing = mem.getBlock(addr);
if (existing == null) {
mem.createInitializedBlock(name, addr, new ByteArrayInputStream(bytes),
bytes.length, monitor, false);
println("BFMOVL CREATED " + name + " @ " + addr + " len=" + bytes.length);
} else {
println("BFMOVL into existing block '" + existing.getName()
+ "' init=" + existing.isInitialized());
if (!existing.isInitialized())
mem.convertToInitialized(existing, (byte) 0);
mem.setBytes(addr, bytes);
println("BFMOVL WROTE " + bytes.length + " bytes @ " + addr);
}
}
// sanity: read back first 8 bytes at each vaddr
for (String[] t : targets) {
Address a = toAddr(t[1]);
byte[] c = new byte[8];
mem.getBytes(a, c);
StringBuilder sb = new StringBuilder();
for (byte x : c) sb.append(String.format("%02x", x));
println("BFMOVL VERIFY " + a + " first8=" + sb);
}
println("BFMOVL done.");
}
}
-19
View File
@@ -1,19 +0,0 @@
// VerifyOverlay.java — read-only R9 check that the imported overlay/resident bytes persisted.
// analyzeHeadless ... -process SLUS_007.26 -noanalysis -readOnly -postScript VerifyOverlay.java
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
public class VerifyOverlay extends GhidraScript {
public void run() throws Exception {
String[][] t = { {"residentBlob","0x800CEDF8"}, {"ovl_tutForest","0x80128158"} };
for (String[] e : t) {
Address a = toAddr(e[1]);
byte[] c = new byte[8];
currentProgram.getMemory().getBytes(a, c);
StringBuilder sb = new StringBuilder();
for (byte x : c) sb.append(String.format("%02x", x));
boolean init = currentProgram.getMemory().getBlock(a).isInitialized();
println("BFMVERIFY " + e[0] + " @" + a + " init=" + init + " first8=" + sb);
}
}
}
-259
View File
@@ -1,259 +0,0 @@
#!/usr/bin/env python3
"""RETIRED (Phase 26-A, R33) — superseded by tools/reconcile_tu.py. DO NOT RE-WIRE.
This tool asked the wrong question, and no regex fix could have saved it:
reconcile_decls asks "what does the FLEET call this symbol?"
C asks "what does THIS TRANSLATION UNIT declare?"
The engine is loosely typed — the same address is legitimately declared with incompatible types in
different overlays — so a single fleet-wide answer is **wrong for some TU by construction**. And it
is worse than a silent skip: it writes an ACTIVELY WRONG declaration into the draft, which then
collides with the very TU it was meant to conform to. Measured across ov_SC01_077's 12 TUs:
agrees with the TU's own declaration ....... 2883
CONFLICTS with it (cc1 REJECTS the result) . 548 <- 16%
TU declares it, this oracle has NO answer .. 357
and it was live on both banking paths: it rewrote 60 of 196 drafts in gate_stage, and every sibling
in the ×134 jtbl_family_bank sweep — the project's economic engine.
Its own docstring already knew the symbols are PER-OVERLAY. That is exactly why a FLEET oracle
could never have been right.
KEPT AS EVIDENCE, NOT AS CODE. Its `data_access_subs` also has no fn-ptr kind, so teaching its parser
to see `extern void (*D_x[])(void);` would have ARMED it to rewrite a call-through `D_x[i]()` into
`((u8 *)D_x)[i]()` — a dormant transform that "fixing the regex" would have detonated. reconcile_tu
handles the kind natively, which is why it SUPERSEDES this rather than patching it.
Original docstring follows.
"""
"""reconcile_decls.py — the DATA-symbol analog of cast_call_sites.py (Phase 24 T7b, cookbook §33).
A freshly-matched giant / wave draft byte-matches STANDALONE with its own guessed decls, but to bank
×1 in the overlay TU (and then propagate ×134 via a shared `DEFINE_func_*` macro) its callee/data
externs must be FLEET-CANONICAL — otherwise `conflicting types` vs engine_core.h / sibling decls
(a COMPILE error, not a byte miss). `cast_call_sites.py` handles this for callee FUNCTIONS
(decl->canonical + fn-ptr cast at the call). This tool does the same for DATA symbols `D_XXXX`:
rewrite the draft's `extern <T_draft> D_x...;` -> the fleet-canonical decl, and inject a byte-neutral
cast at each ACCESS so the load/store opcode is unchanged. gcc-2.7.2 folds the compile-time
pointer/scalar cast of a known symbol, so the body bytes are identical.
This is a PURE draft-text transform (like sig_unify / cast_call_sites / canon_resident_calls): the
whole-binary `harvest_verify` byte-gate remains the sole arbiter (G3/P9) — a wrong reconcile just
fails the gate and reverts, so it trial-and-errors safely. It does NOT touch callee FUNCTION decls
(that's cast_call_sites/sig_unify — compose them). It changes only the draft's OWN data externs +
the access sites; it never edits the fleet's canonical decls.
Byte-neutral cast taxonomy (proven by hand on func_80129CF8; the intended type is what the DRAFT
declared, the canonical is the fleet's — cast every use to reproduce the intended access):
draft array Ed[] vs canonical array Ec[] -> decl Ec[]; D_x[i] -> ((Ed*)D_x)[i]; D_x -> (Ed*)D_x
draft array Ed[] vs canonical struct/scalar -> decl canon; D_x -> (Ed*)&D_x (address-of the object)
draft scalar Td vs canonical scalar Tc -> decl Tc; D_x -> *(Td*)&D_x (forces the Td-width opcode)
draft ptr P* D_x vs canonical scalar Tc -> decl Tc; write D_x=v -> D_x=(Tc)(v); read D_x -> (P*)D_x
Pipeline (cookbook §33):
draft -> canon_resident_calls -> cast_call_sites -> reconcile_decls -> sig_unify -> harvest_verify --chunk 1
(callee funcs) (data symbols) (def sig) (the byte-gate)
Usage:
tools/reconcile_decls.py --overlay ov_SC01_077 --src-file src/ov_SC01_077/ov_SC01_077_a.c \
--in .run/drafts-giant --out .run/drafts-giant-rc
tools/reconcile_decls.py --print-canon D_80126948 # inspect the oracle's pick for one symbol
"""
import argparse, os, re, glob, importlib.util, collections
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _load(mod, rel):
spec = importlib.util.spec_from_file_location(mod, os.path.join(REPO, rel))
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
return m
_ght = _load('ght', 'tools/gen_harvest_targets.py') # reuse collect_data_decls / the D_ regexes
# `extern <type> D_XXXX;` or `extern <type> D_XXXX[];` (optional trailing /* comment */). <type> may
# carry a trailing `*` (pointer) which we fold into the element type.
DATA_DECL_LINE_RE = re.compile(
r'^([ \t]*)extern\s+([A-Za-z_][\w \t]*?)\s*(\*?)\s*\b(D_[0-9A-Fa-f]+)\b\s*(\[\s*\])?\s*;'
r'[ \t]*(?:/\*[^\n]*\*/)?[ \t]*$')
# ---------------------------------------------------------------- the canonical-type ORACLE (T2)
def canonical_data_map(extra_src=None):
"""Fleet-majority canonical decl per data symbol. Returns {D_name: 'extern <decl>;'}.
Precedence: an engine_core.h macro decl is AUTHORITATIVE (it's the shared/propagated set every
overlay co-instantiates — conforming to it is what avoids the per-overlay macro conflict). Else
the plurality vote across all overlays + resident (ties -> the lexicographically-first spelling,
deterministic). This is the picker the design-doc flagged as missing (gen_harvest_targets read
only engine_core.h + ONE overlay, first-seen-wins, and never saw a fleet disagreement)."""
ec = os.path.join(REPO, 'src/shared/engine_core.h')
# 1. engine_core.h — authoritative (a symbol declared inside any DEFINE_ macro)
ec_decls = _ght.collect_data_decls([ec]) # {D_name: 'extern ...;'} first-seen
# 2. plurality across the fleet (all overlays + resident) for symbols engine_core.h lacks
votes = collections.defaultdict(collections.Counter)
fleet = (glob.glob(os.path.join(REPO, 'src/ov_*/*.c'))
+ [os.path.join(REPO, 'src/resident/resident.c')])
if extra_src:
fleet.append(os.path.join(REPO, extra_src))
for p in fleet:
if not os.path.exists(p):
continue
txt = open(p).read()
for m in _ght.DATA_DECL_RE.finditer(txt):
decl = re.sub(r'\s+', ' ', m.group(1)).strip()
nm = re.search(r'D_[0-9A-Fa-f]+', decl)
if nm:
votes[nm.group(0)][f'extern {decl};'] += 1
canon = {}
names = set(ec_decls) | set(votes)
for nm in names:
if nm in ec_decls:
canon[nm] = ec_decls[nm] # authoritative
else:
# plurality; tie -> lexicographically-first decl spelling (deterministic)
best = sorted(votes[nm].items(), key=lambda kv: (-kv[1], kv[0]))[0][0]
canon[nm] = best
return canon
def parse_data_decl(decl):
"""'extern u8 D_x[];' -> (name, elem='u8', is_array=True, is_ptr=False).
'extern struct BigCopy D_x;' -> (name, 'struct BigCopy', False, False).
'extern s32 *D_x;' -> (name, 's32', False, is_ptr=True)."""
m = DATA_DECL_LINE_RE.match(decl) or DATA_DECL_LINE_RE.match(' ' + decl.strip())
if not m:
return None
_indent, base, star, name, arr = m.group(1), m.group(2), m.group(3), m.group(4), m.group(5)
return name, re.sub(r'\s+', ' ', base).strip(), bool(arr), bool(star)
def _norm_type(elem, is_array, is_ptr):
"""Spelling-insensitive key for 'do these decls differ' — collapse int-family aliases (same
width+conv, casting them is a no-op), normalise whitespace. Array vs ptr vs scalar are distinct."""
e = re.sub(r'\s+', ' ', elem).strip()
e = re.sub(r'\b(s32|u32|int|unsigned int|unsigned|long|unsigned long|u_long)\b', 'int', e)
kind = 'arr' if is_array else ('ptr' if is_ptr else 'sca')
return (e, kind)
def _base_ptr_expr(name, ielem, canon_is_array):
"""The draft's intended array/base pointer, expressed under the canonical storage decl.
canonical array -> the array decays: (Ed*)D_x
canonical struct/scalar -> take its address: (Ed*)&D_x"""
return f'(({ielem} *){name})' if canon_is_array else f'(({ielem} *)&{name})'
def data_access_subs(name, idecl, cdecl):
"""One (compiled-regex, replacement-fn) that reproduces the DRAFT's intended access to `name`
under the CANONICAL storage decl in a SINGLE pass (re.sub never re-scans its own output, so a
symbol appearing in several forms on one line can't double-wrap). [] if type-compatible, None if
unparseable. The regex captures an optional leading `&` and a following `[` to pick the form."""
ip = parse_data_decl(idecl)
cp = parse_data_decl(cdecl)
if not ip or not cp:
return None # unparseable -> caller logs, leaves as-is
_, ielem, iarr, iptr = ip
_, celem, carr, cptr = cp
if _norm_type(ielem, iarr, iptr) == _norm_type(celem, carr, cptr):
return [] # already compatible
rx = re.compile(rf'(&?)\b{re.escape(name)}\b(\s*\[)?')
if iarr:
base = _base_ptr_expr(name, ielem, carr) # (Ed*)D_x or (Ed*)&D_x (canon array vs not)
def repl(m):
amp, idx = m.group(1), m.group(2)
if idx: # D_x[i] / &D_x[i] -> [&]base[i]
return f'{amp}{base}{idx}'
return base # bare D_x / &D_x -> base (array address IS the base ptr)
else:
# scalar (signedness/width) OR ptr-vs-scalar: force the intended-width/type access via a
# cast-lvalue `*(<intended> *)&D_x` (valid as both lvalue and rvalue; ptr-intended -> `ielem **`).
star = ' *' if iptr else ''
acc = f'(*({ielem}{star} *)&{name})'
def repl(m):
amp, idx = m.group(1), m.group(2)
return f'{amp}{acc}{idx or ""}'
return [(rx, repl)]
# ---------------------------------------------------------------- the transform (T3)
def transform(text, canon):
"""Return (new_text, n_data_reconciled, notes). For each data symbol the draft declares with a
type differing from the fleet-canonical: rewrite the decl -> canonical + cast every access."""
lines = text.split('\n')
plan = {} # D_name -> (canonical_decl_line, [subs])
decl_idx = {} # D_name -> line index of its decl
notes = []
for i, ln in enumerate(lines):
m = DATA_DECL_LINE_RE.match(ln)
if not m:
continue
indent, name = m.group(1), m.group(4)
idecl = ln.strip()
if name not in canon:
continue # no fleet decl -> draft's is the only one, keep
cdecl = canon[name]
subs = data_access_subs(name, idecl, cdecl)
if subs is None:
notes.append(f'{name}: UNPARSEABLE decl, left as-is'); continue
if not subs:
continue # already compatible -> nothing to do
plan[name] = (f'{indent}{cdecl}', subs)
decl_idx[name] = i
if not plan:
return text, 0, notes
out = []
for i, ln in enumerate(lines):
dname = next((n for n, j in decl_idx.items() if j == i), None)
if dname is not None:
out.append(plan[dname][0]) # replace the decl line with the canonical decl
continue
if DATA_DECL_LINE_RE.match(ln): # some OTHER data decl -> never cast in a decl
out.append(ln); continue
for name, (_c, subs) in plan.items(): # body line -> rewrite this symbol's accesses
for rx, rep in subs:
ln = rx.sub(rep, ln)
out.append(ln)
return '\n'.join(out), len(plan), notes
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--overlay', default='ov_SC01_077')
ap.add_argument('--src-file', dest='src_file', default=None,
help='overlay .c whose local decls also vote (default: none; the fleet scan '
'already covers all overlays)')
ap.add_argument('--in', dest='indir')
ap.add_argument('--out', dest='outdir')
ap.add_argument('--print-canon', metavar='D_XXXX', help='print the oracle pick for one symbol and exit')
a = ap.parse_args()
canon = canonical_data_map(a.src_file)
if a.print_canon:
print(canon.get(a.print_canon, f'(no fleet decl for {a.print_canon})'))
return
if not (a.indir and a.outdir):
ap.error('need --in and --out (or --print-canon)')
os.makedirs(os.path.join(REPO, a.outdir), exist_ok=True)
drafts = touched = total = 0
for p in sorted(glob.glob(os.path.join(REPO, a.indir, '*.c'))):
new, k, notes = transform(open(p).read(), canon)
open(os.path.join(REPO, a.outdir, os.path.basename(p)), 'w').write(new)
drafts += 1
if k:
touched += 1; total += k
for n in notes:
print(f' [{os.path.basename(p)}] {n}')
print(f'canonical data decls: {len(canon)}; drafts: {drafts}; '
f'reconciled: {touched} draft(s), {total} data symbol(s)')
if __name__ == '__main__':
main()
-133
View File
@@ -1,133 +0,0 @@
#!/usr/bin/env python3
"""Bank func_801457A4 across the fleet by moving its DEFINITION into <ov>_o0b.c (the -O0 object).
§116: `func_801457A4` is an -O0 function. In ov_SC01_077 its definition lives in
ov_SC01_077_o0b.c, which the Makefile's WHALE_O0B_OBJS wildcard compiles -O0. In every other
overlay the same function's stub sits in <ov>_after.c, which is -O2 — so `family_sweep --hseq`
templated the -O0-matched body into an -O2 TU and the byte-gate correctly rejected all 137.
WHY THIS TOOL EXISTS AND family_sweep CANNOT DO IT (Phase 29 T80, the refuted shortcut):
the obvious move is to relocate the member's `INCLUDE_ASM(...)` line into <ov>_o0b.c and let the
existing sweep stage there. That is NOT byte-neutral — it is unbuildable. splat emits
`asm/<ov>/nonmatchings/<seg>/<fn>.s` for EXACTLY the functions the segment's own .c marks with
INCLUDE_ASM (measured: 12 lines <-> 12 .s files, identical sets). Delete the line from
<ov>_after.c and the .s stops being generated, so the relocated reference cannot assemble.
`asm/` follows the SEGMENT; object membership follows the .c FILE.
So the substitution has to be ATOMIC ACROSS TWO FILES — append the remapped body to <ov>_o0b.c
AND drop the INCLUDE_ASM from <ov>_after.c in one edit — which `harvest_verify`/`family_sweep`
do not do (they substitute a draft for a stub *in the stub's own file*). Hence this driver.
Deliberately no splat change: a re-carve is the Phase-29 Arm-A wall (+0x20 data-symbol shift on
3 of 4 sampled overlays).
The whole-binary byte-gate stays the sole arbiter (G3/P9): per overlay, build and compare against
config/check.<ov>.sha; on any mismatch BOTH files are restored from their snapshots.
tools/rollout_801457a4_o0.py [--apply] [--limit N] [--jobs N]
Out of scope: ov_SC07_{006,007,010,011} have no _o0b.c (onboarded in Phase 27, never whale-carved).
"""
import argparse
import concurrent.futures as futures
import glob
import hashlib
import os
import re
import subprocess
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(REPO, "tools"))
import family_remap as FR # noqa: E402
FN = "func_801457A4"
ADDR = 0x801457A4
EX_OV = "ov_SC01_077"
STUB_RE = re.compile(rf'^INCLUDE_ASM\("([^"]+)",\s*{FN}\);\s*$', re.M)
NOTE = (f"/* {FN} (@0x{ADDR:08X}) is an -O0 function; its definition lives in {{o0b}} (the -O0\n"
f" * whale object, whose .text ends exactly at this address). Mirrors ov_SC01_077. §116 */")
def sha1(p):
h = hashlib.sha1()
with open(p, "rb") as fh:
for b in iter(lambda: fh.read(1 << 20), b""):
h.update(b)
return h.hexdigest()
def good_sha(ov):
return open(os.path.join(REPO, f"config/check.{ov}.sha")).read().split()[0]
def build_ok(ov):
r = subprocess.run(["make", "build", f"BINARY={ov}"], cwd=REPO,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
out = os.path.join(REPO, f"build/{ov}/{ov}")
return r.returncode == 0 and os.path.exists(out) and sha1(out) == good_sha(ov)
def candidates():
out = []
for after in sorted(glob.glob(os.path.join(REPO, "src/ov_*/ov_*_after.c"))):
ov = os.path.basename(os.path.dirname(after))
o0b = os.path.join(REPO, f"src/{ov}/{ov}_o0b.c")
if not os.path.exists(o0b):
continue # never whale-carved (the 4 SC07 tail overlays)
if not STUB_RE.search(open(after).read()):
continue # already banked, or no stub here
out.append((ov, after, o0b))
return out
def attempt(ov, after, o0b, apply):
draft, info = FR.remap_hseq(ADDR, EX_OV, ov, ADDR)
if draft is None:
return ov, "remap-refused", str(info)[:60]
a_txt, b_txt = open(after).read(), open(o0b).read()
m = STUB_RE.search(a_txt)
new_after = a_txt[:m.start()] + NOTE.format(o0b=os.path.basename(o0b)) + a_txt[m.end():]
new_o0b = b_txt.rstrip("\n") + "\n\n" + draft.rstrip("\n") + "\n"
if not apply:
return ov, "would-try", ""
open(after, "w").write(new_after)
open(o0b, "w").write(new_o0b)
if build_ok(ov):
return ov, "BANKED", ""
open(after, "w").write(a_txt) # restore BOTH, always (§61)
open(o0b, "w").write(b_txt)
return ov, "gate-reject", ""
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--apply", action="store_true", help="write + gate (default: dry run)")
ap.add_argument("--limit", type=int, default=0)
ap.add_argument("--jobs", type=int, default=1, help="overlays gated in parallel (distinct binaries)")
a = ap.parse_args()
cands = candidates()
if a.limit:
cands = cands[:a.limit]
print(f"in scope: {len(cands)} overlays", flush=True)
res = []
if a.jobs > 1 and a.apply:
with futures.ThreadPoolExecutor(max_workers=a.jobs) as ex:
for r in ex.map(lambda c: attempt(*c, a.apply), cands):
res.append(r)
print(f" {r[0]}: {r[1]} {r[2]}", flush=True)
else:
for c in cands:
r = attempt(*c, a.apply)
res.append(r)
print(f" {r[0]}: {r[1]} {r[2]}", flush=True)
from collections import Counter
print(Counter(r[1] for r in res))
return 0
if __name__ == "__main__":
sys.exit(main())
-132
View File
@@ -1,132 +0,0 @@
#!/usr/bin/env python3
"""Phase-29 T2 Arm A: carve the -O0 cluster (vram 0x8013B568..0x8013C98C, file 0x13410..0x14834)
into its own -O0 object per single-file overlay, so the h_seq family members of those 9 matched
-O0 exemplars can bank whole-binary (they masked-MATCH only at -O0 — the Task-1 swing verdict).
Background (the swing verdict, 2026-07-16): `family_sweep --hseq` compiled the -O0 cluster members
at -O2 (member stub files are -O2), so their remapped bodies masked-MATCH but fail the whole-binary
gate. gcc-2.7.2 has no per-function optimize pragma (opt is per-FILE), so — exactly like the whale
(rollout_whale_o0.py, §38) and ov_SC01_077's own _o0 split (Phase-19 §18) — each overlay must
compile the cluster in its own -O0 TU. This tool builds that split; the family sweep then fills it.
The cluster is a contiguous run of 16 fns (all INCLUDE_ASM stubs in an unswept overlay). We carve
<ov>.c into 3 objects (a single object's .text can't be split around a middle object):
- <ov>.c = before the cluster (0x0..0x13410), keeps name + nonmatchings/<ov> asm paths
- <ov>_o0.c = the -O0 cluster (0x13410..0x14834), asm paths -> nonmatchings/<ov>_o0
- <ov>_o2b.c = after the cluster (0x14834..data tail), asm paths -> nonmatchings/<ov>_o2b
The Makefile O0_CLUSTER_OBJS wildcard -O0-compiles every <ov>_o0.o. Byte-NEUTRAL by construction:
INCLUDE_ASM pastes the ORIGINAL bytes regardless of -O0/-O2, so a freshly-carved (all-stub) overlay
stays byte-identical — gate it before sweeping. Idempotent (skips carved overlays). The whole-binary
byte-gate (make check-all) is the sole arbiter (G3/P9). Usage:
rollout_o0_cluster.py <ov> [<ov> ...] # single-file overlays only (files=1); whale/jr-carved
# overlays keep the cluster inside jr_801380E0 (not handled)
"""
import sys, re, os
BASE = 0x80128158
LO_FOFF, HI_FOFF = 0x13410, 0x14834 # cluster vram 0x8013B568 .. 0x8013C98C
START_FN = "func_8013B568" # first fn of the cluster (0x13410)
LAST_FN = "func_8013C964" # last fn of the cluster (just below 0x14834)
CLUSTER_FNS = [ # the 16 contiguous -O0 fns (from ov_SC01_077_o0.c)
"func_8013B568", "func_8013B598", "func_8013B6A0", "func_8013B7AC",
"func_8013B7F4", "func_8013B83C", "func_8013BC7C", "func_8013BCDC",
"func_8013BD34", "func_8013BD74", "func_8013C08C", "func_8013C0F8",
"func_8013C360", "func_8013C414", "func_8013C938", "func_8013C964",
]
def header_end(lines):
"""Header = the leading #include block (+ trailing blanks). Stops at the first non-#include,
non-blank line (a DEFINE_func macro / decl), so the low-address dedup macros stay in `before`."""
last_inc = -1
for i, l in enumerate(lines):
s = l.strip()
if s.startswith("#include"):
last_inc = i
elif s and last_inc >= 0:
break
he = last_inc + 1
while he < len(lines) and lines[he].strip() == "":
he += 1
return he
def unit_start(lines, fn):
"""Line index where `fn`'s definition/stub begins (an INCLUDE_ASM stub or a `... fn(` def line)."""
for i, l in enumerate(lines):
if re.search(rf'INCLUDE_ASM\("[^"]*",\s*{fn}\)\s*;', l):
return i, "stub"
if re.search(rf'\b{fn}\s*\(', l) and not l.lstrip().startswith(("//", "*", "extern")) \
and "INCLUDE_ASM" not in l and (l.rstrip().endswith("{") or ")" in l and ";" not in l):
return i, "def"
return None, None
def split_overlay(ov):
cdir = f"src/{ov}"
main_c = f"{cdir}/{ov}.c"
o0_c, o2b_c = f"{cdir}/{ov}_o0.c", f"{cdir}/{ov}_o2b.c"
yaml = f"config/splat.{ov}.yaml"
if os.path.exists(o0_c):
return f"SKIP {ov} (o0 exists)"
if not (os.path.exists(main_c) and os.path.exists(yaml)):
return f"SKIP {ov} (missing .c or yaml)"
lines = open(main_c).read().split("\n")
# every cluster fn must be present in THIS file (single-file overlay) — else the cluster is
# carved elsewhere (jr-embedded overlay) and this tool must not touch it.
missing = [fn for fn in CLUSTER_FNS if not any(re.search(rf'\b{fn}\b', l) for l in lines)]
if missing:
return f"SKIP {ov} (cluster fns not in base .c: {missing[:3]}... — jr-embedded?)"
si, _ = unit_start(lines, START_FN)
li, lkind = unit_start(lines, LAST_FN)
if si is None or li is None:
return f"ERROR {ov}: boundary fn not found (start={si}, last={li})"
if lkind != "stub":
return f"SKIP {ov} ({LAST_FN} is matched inline — cluster not all-stub; carve by hand)"
# end = first non-blank line after the LAST_FN stub (keeps the after-region fn's forward externs
# with the after-region, not the cluster).
ei = li + 1
while ei < len(lines) and lines[ei].strip() == "":
ei += 1
if not (si < li < ei):
return f"ERROR {ov}: bad boundary order si={si} li={li} ei={ei}"
header = "\n".join(lines[:header_end(lines)]).rstrip("\n")
before, cluster, after = lines[:si], lines[si:ei], lines[ei:]
OLD = f'nonmatchings/{ov}"'
open(main_c, "w").write("\n".join(before).rstrip("\n") + "\n")
cluster_body = "\n".join(cluster).replace(OLD, f'nonmatchings/{ov}_o0"')
open(o0_c, "w").write(header + "\n\n" + cluster_body.rstrip("\n") + "\n")
after_body = "\n".join(after).replace(OLD, f'nonmatchings/{ov}_o2b"')
open(o2b_c, "w").write(header + "\n\n" + after_body.rstrip("\n") + "\n")
# carve the yaml code subseg [0x0, c, <ov>] -> before / o0 cluster / o2b after
y = open(yaml).read()
pat = re.compile(r'^(\s*)- \[0x0, c, ' + re.escape(ov) + r'\].*$', re.M)
m = pat.search(y)
if not m:
return f"ERROR {ov}: '- [0x0, c, {ov}]' not found in yaml"
ind = m.group(1)
repl = (f'{ind}- [0x0, c, {ov}] # -O2 before -O0 cluster (file 0x0..0x{LO_FOFF:X})\n'
f'{ind}- [0x{LO_FOFF:X}, c, {ov}_o0] # -O0 cluster 0x8013B568..0x8013C98C (0x{LO_FOFF:X}..0x{HI_FOFF:X})\n'
f'{ind}- [0x{HI_FOFF:X}, c, {ov}_o2b] # -O2 after -O0 cluster (0x{HI_FOFF:X}..data tail)')
open(yaml, "w").write(pat.sub(lambda _m: repl, y, count=1))
ncl = sum(1 for l in cluster if "INCLUDE_ASM" in l)
return f"OK {ov}: before {len(before)} / cluster {len(cluster)} ({ncl} stubs) / after {len(after)} lines"
if __name__ == "__main__":
targets = sys.argv[1:]
if not targets:
print(__doc__)
sys.exit(2)
n_ok = 0
for ov in targets:
r = split_overlay(ov)
if r.startswith("OK"):
n_ok += 1
print(r)
print(f"--- {n_ok}/{len(targets)} carved ---")
-91
View File
@@ -1,91 +0,0 @@
#!/usr/bin/env python3
"""Phase-24 W9: roll out the -O0 whale (func_80144B9C) to single-file overlays for the ×134 bank.
The whale is byte-identical in all 134 overlays (reach-134) but only matches at -O0 (gcc-2.7.2 has
no per-function optimize pragma), so each overlay must compile it in its own -O0 TU. Per overlay
(<ov>.c is splat-emitted in vram order, so a line-split at the whale is safe):
- carve the code subseg into before / o0b(-O0 whale) / after in config/splat.<ov>.yaml
- <ov>.c = before-whale (keeps name + nonmatchings/<ov> asm paths)
- <ov>_o0b.c = a thin wrapper: #include common.h + the shared src/shared/func_80144B9C.h
- <ov>_after.c = header (includes) + after-whale, asm paths -> nonmatchings/<ov>_after
The Makefile WHALE_O0B_OBJS wildcard rule -O0-compiles every <ov>_o0b.o. Idempotent (skips carved
overlays). The whole-binary byte-gate (make check-all) is the sole arbiter. Usage:
rollout_whale_o0.py <ov> [<ov> ...] or rollout_whale_o0.py --all
"""
import sys, re, os, glob
WHALE = "func_80144B9C"
WHALE_FOFF = 0x1CA44 # vram 0x80144B9C - base 0x80128158
AFTER_FOFF = 0x1D64C # vram 0x801457A4 (next fn) - base 0x80128158
def header_end(lines):
"""Header = the leading #include block (+ trailing blanks). Single-file overlays have no
file-scope shared externs (each fn declares its own), so this captures common.h + engine_core.h."""
last_inc = -1
for i, l in enumerate(lines):
s = l.strip()
if s.startswith("#include"):
last_inc = i
elif s and last_inc >= 0:
break
he = last_inc + 1
while he < len(lines) and lines[he].strip() == "":
he += 1
return he
def split_overlay(ov):
cdir = f"src/{ov}"
main_c, o0b_c, after_c = f"{cdir}/{ov}.c", f"{cdir}/{ov}_o0b.c", f"{cdir}/{ov}_after.c"
yaml = f"config/splat.{ov}.yaml"
if os.path.exists(o0b_c):
return f"SKIP {ov} (o0b exists)"
if not (os.path.exists(main_c) and os.path.exists(yaml)):
return f"SKIP {ov} (missing .c or yaml)"
lines = open(main_c).read().split("\n")
wi = [i for i, l in enumerate(lines) if WHALE in l and "INCLUDE_ASM" in l]
if len(wi) != 1:
return f"SKIP {ov} (whale INCLUDE_ASM count={len(wi)} — not a single-file stub)"
wi = wi[0]
he = header_end(lines)
header = "\n".join(lines[:he]).rstrip("\n")
before, after = lines[:wi], lines[wi + 1:]
OLD = f'nonmatchings/{ov}"'
open(main_c, "w").write("\n".join(before).rstrip("\n") + "\n")
open(o0b_c, "w").write('#include "common.h"\n#include "../shared/func_80144B9C.h"\n')
after_body = "\n".join(after).replace(OLD, f'nonmatchings/{ov}_after"')
open(after_c, "w").write(header + "\n\n" + after_body.rstrip("\n") + "\n")
# carve the yaml code subseg
y = open(yaml).read()
pat = re.compile(r'^(\s*)- \[0x0, c, ' + re.escape(ov) + r'\].*$', re.M)
m = pat.search(y)
if not m:
return f"ERROR {ov}: '- [0x0, c, {ov}]' not found in yaml"
ind = m.group(1)
repl = (f'{ind}- [0x0, c, {ov}] # -O2 before whale (file 0x0..0x{WHALE_FOFF:X})\n'
f'{ind}- [0x{WHALE_FOFF:X}, c, {ov}_o0b] # -O0 whale func_80144B9C (0x{WHALE_FOFF:X}..0x{AFTER_FOFF:X})\n'
f'{ind}- [0x{AFTER_FOFF:X}, c, {ov}_after] # -O2 after whale (0x{AFTER_FOFF:X}..data tail)')
open(yaml, "w").write(pat.sub(lambda _m: repl, y, count=1))
return f"OK {ov}: before {len(before)} / after {len(after)} lines"
def all_overlays():
ovs = []
for p in sorted(glob.glob("config/splat.ov_*.yaml")):
ov = os.path.basename(p)[len("splat."):-len(".yaml")]
if ov != "ov_SC01_077": # already carved (the harvest source, different structure)
ovs.append(ov)
return ovs
if __name__ == "__main__":
args = sys.argv[1:]
targets = all_overlays() if args == ["--all"] else args
n_ok = 0
for ov in targets:
r = split_overlay(ov)
if r.startswith("OK"):
n_ok += 1
print(r)
print(f"--- {n_ok}/{len(targets)} carved ---")