# Phase 3 all-assembly baseline orchestration. # All original and generated ROM-derived inputs/outputs remain ignored. SHELL := /usr/bin/bash EXE := extracted/SCUS_946.40;1 MANIFEST := extracted/MANIFEST.tsv SPLAT := tools/splat/.venv/bin/splat AS := tools/mipsel-none-elf-binutils/prefix/usr/bin/mipsel-none-elf-as LD := tools/mipsel-none-elf-binutils/prefix/usr/bin/mipsel-none-elf-ld OBJCOPY := tools/mipsel-none-elf-binutils/prefix/usr/bin/mipsel-none-elf-objcopy ASM_ROOT := asm SPLAT_CONFIG := $(ASM_ROOT)/scus_946_40.yaml SPLAT_OUTPUT := $(ASM_ROOT)/generated LINKER_SCRIPT := $(ASM_ROOT)/scus_946_40.ld BUILD := build ELF := $(BUILD)/scus_946_40.elf REBUILT := $(BUILD)/scus_946_40.rebuilt INPUT_REPORT := .run/p3-exe-info.tsv # Phase 5 ordered code build: C regions listed in the tracked registry are # compiled with the identified toolchain and linked in address order between # data fallbacks taken from the original executable. An empty registry # reproduces the all-payload data baseline exactly. REGIONS := config/regions.tsv SYMBOLS := config/symbols.tsv INVENTORY := config/function_inventory.tsv EXTENTS := config/function_extents.tsv DUPES := config/duplicate_bodies.tsv NEGATIVES := config/near_match_negatives.tsv WORKLIST := config/match_worklist.tsv MATCH := tools/sf3_match EXTENTS_TOOL := tools/sf3_extents DUPES_TOOL := tools/sf3_dupes TRIAGE_TOOL := tools/sf3_triage NEGPOOL_TOOL := tools/sf3_negpool # Rows deliberately excluded from dispatch, kept in ONE place: a previous phase excluded each for a # recorded reason (false extent start, shared tail, unmatchable $gp-thunk half, fragment), and the # reason is in the comment block above `worklist`. Both `worklist` and `negpool` are given this # list, because the Phase 12 pools were first built by a one-off script that did not know about it # and **8 of these 12 rows were delivered straight back to the workers as fresh work**. NAMED_EXCLUSIONS := 0x8005DEF8 0x800F3160 0x80108034 0x8010804C 0x80012A10 0x80012AE0 \ 0x80012CFC 0x80012B20 0x8008A198 0x8010080C 0x800C3490 0x8001DC20 # Worker staging for the classified-negatives pools. Empty in a clean checkout, which is correct: # the pools then contain only the rows the tracked index names. NEGPOOL_DIR := .run/p12 NEGPOOL_CLASSIFIED := $(wildcard .run/p12/w-*/negatives.tsv) CODE_OUT := build/code EXPECTED_SHA1 := e173426c157384ebf1b6caf8c6fea18a85a14af9 .PHONY: all validate split assemble link binary code gate extents extents-verify dupes worklist negpool test check clean all: binary validate: @test -f "$(EXE)" @test -f "$(MANIFEST)" @mkdir -p .run @./tools/sf3_extract psx-exe-info "$(EXE)" "$(MANIFEST)" > "$(INPUT_REPORT)" split: validate @test -f "$(SPLAT)" @test -f "$(SPLAT_CONFIG)" @cd "$(ASM_ROOT)" && ../"$(SPLAT)" split "$$(basename "$(SPLAT_CONFIG)")" --disassemble-all assemble: split @test -d "$(SPLAT_OUTPUT)" && test ! -L "$(SPLAT_OUTPUT)" @test -f "$(AS)" @test ! -L "$(BUILD)" @mkdir -p "$(BUILD)" @while IFS= read -r source; do \ rel="$${source#$(ASM_ROOT)/}"; \ object="$(BUILD)/$${rel}.o"; \ mkdir -p "$$(dirname "$$object")"; \ "$(AS)" -march=r3000 -G0 -I "$(ASM_ROOT)/include" -o "$$object" "$$source"; \ done < <(find "$(SPLAT_OUTPUT)" -type f -name '*.s' -print | sort) link: assemble @test -f "$(LINKER_SCRIPT)" @test -f "$(LD)" @rm -f "$(ELF)" @"$(LD)" -T "$(LINKER_SCRIPT)" -o "$(ELF)" binary: link @test -f "$(OBJCOPY)" @rm -f "$(REBUILT)" @"$(OBJCOPY)" -O binary "$(ELF)" "$(REBUILT)" # Ordered code build and full-binary gate (Phase 5). `code` builds only; # `gate` builds and then requires the whole executable to be byte-identical. code: validate @test -f "$(MATCH)" @test -f "$(REGIONS)" @test -f "$(SYMBOLS)" @rm -rf "$(CODE_OUT)" @"$(MATCH)" build --exe "$(EXE)" --regions "$(REGIONS)" --symbols "$(SYMBOLS)" --out "$(CODE_OUT)" gate: validate @test -f "$(MATCH)" @test -f "$(REGIONS)" @test -f "$(SYMBOLS)" @rm -rf "$(CODE_OUT)" @"$(MATCH)" gate --exe "$(EXE)" --regions "$(REGIONS)" --symbols "$(SYMBOLS)" --out "$(CODE_OUT)" \ --expect-sha1 "$(EXPECTED_SHA1)" # Function extents (Phase 7). `extents` regenerates the tracked table from the # inventory; `extents-verify` re-derives it and requires every registered region # to agree, so a region can never silently disagree with the derived extent. extents: validate @test -f "$(EXTENTS_TOOL)" @test -f "$(INVENTORY)" @"$(EXTENTS_TOOL)" scan --exe "$(EXE)" --inventory "$(INVENTORY)" --out "$(EXTENTS)" --force extents-verify: validate @test -f "$(EXTENTS_TOOL)" @test -f "$(INVENTORY)" @test -f "$(EXTENTS)" @"$(EXTENTS_TOOL)" verify --exe "$(EXE)" --inventory "$(INVENTORY)" \ --extents "$(EXTENTS)" --regions "$(REGIONS)" # Duplicate-body census (Phase 7): groups byte-identical bodies so a shared body # is matched once and registered once per address. dupes: validate @test -f "$(DUPES_TOOL)" @test -f "$(EXTENTS)" @"$(DUPES_TOOL)" census --exe "$(EXE)" --extents "$(EXTENTS)" --out "$(DUPES)" --force # Ranked match worklist (Phase 7). The --exclude addresses are recorded # near-misses or known-bad rows deferred by developer direction; naming them here # keeps the exclusion visible instead of burying it in the tool. # 0x8010080C was added in Phase 10 as a FALSE EXTENT START (worker C): its first # instruction is `beq s0,zero` with s0 never written in range, and the walk split # one real 252-byte function (0x801007E0..0x801008DC) at a spurious `jal`-target # boundary. It is now caught earlier by sf3_triage's general `restores_unsaved` # rule (worker A's independent scan re-derived the same row from a different # signal), so this row is retained only as the provenance record for that defect. # # 0x800C3490 was added in Phase 12 as a FRAGMENT, not a function (cookbook 114, # found by worker A and re-confirmed by the coordinator against the registry). It # starts mid-expression, its body is a SHARED TAIL (`addiu sp,sp,48; jr ra`) that # also appears at 0x800C3470-0x800C348C, and it cannot be matched standalone. The # extents table still grades it `exact` with `term=jr_ra`, because the boundary walk # sees a well-formed terminal -- so the tool cannot catch this one. Excluded rather # than left in the dispatch files, where it costs every fresh-band worker a reading # budget before it is recognised again. Worker A identified it for the second time in # Phase 12 and skipped it rather than spending that budget. # # 0x8001DC20 was added in Phase 12 as the SECOND instance of the same class, and it # gives the class a CHECKABLE predicate instead of a vibe. Its first instruction is # `lw t0,0(t5)` (0x8DA80000) and **$13 is written ZERO times in the whole 72-byte # extent** -- t5 is read but never established, so the extent is the tail of a larger # function. Worker D found it and refused to spend GTE spellings on it; the # coordinator re-derived the same conclusion from the bytes before excluding it, so # this row is a two-worker confirmation like 0x800C3490 is. # The general predicate -- "a caller-saved temporary ($8-$15) is READ before any # instruction in the extent WRITES it" -- holds for BOTH rows (`0x800C3490` also # writes $13 zero times) and is sound, because $8-$15 are never incoming o32 # arguments and no compiler emits a read of an uninitialised temporary. It is NOT # yet implemented as an automatic triage rule; until it is, fragments are caught by # a worker reading the row. See the Phase 12 ledger for that open item. worklist: validate @test -f "$(TRIAGE_TOOL)" @test -f "$(EXTENTS)" @test -f "$(DUPES)" @"$(TRIAGE_TOOL)" plan --exe "$(EXE)" --extents "$(EXTENTS)" --inventory "$(INVENTORY)" \ --census "$(DUPES)" --regions "$(REGIONS)" --negatives "$(NEGATIVES)" \ $(foreach a,$(NAMED_EXCLUSIONS),--exclude $(a)) --out "$(WORKLIST)" --force # Classified-negatives worker pools. Regenerating these is cheap and idempotent; handing a worker a # pool that a row was ALREADY removed from is not, so this target owns both filters: # * $(NAMED_EXCLUSIONS) -- rows dispatch must never offer (same list as `worklist`) # * the workers' own classification -- a row already read this phase, kept at prio 1 only if # it NAMES a mechanism, dropped otherwise. # See the tools/sf3_negpool docstring for why both rules exist and what they cost when absent. negpool: validate @test -f "$(NEGPOOL_TOOL)" @test -f "$(EXTENTS)" @excl=""; for a in $(NAMED_EXCLUSIONS); do excl="$$excl --exclude $$a"; done; \ cls=""; for f in $(NEGPOOL_CLASSIFIED); do cls="$$cls --classified $$f"; done; \ "$(NEGPOOL_TOOL)" --negatives "$(NEGATIVES)" --regions "$(REGIONS)" --extents "$(EXTENTS)" \ --exe "$(EXE)" --out-dir "$(NEGPOOL_DIR)" --prefix negatives --workers 4 \ $$excl $$cls # Verification gates. `test` is synthetic-only and needs no game input; # `check` adds the extents check and the full-binary byte gate (which do). test: @PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tools/tests check: test extents-verify gate clean: @if test -e "$(BUILD)"; then \ test -d "$(BUILD)" && test ! -L "$(BUILD)"; \ rm -rf "$(BUILD)"; \ fi