Files
BFM-decomp/Makefile
T
Drew T 6fb8d323bd perf(phase-26): parallelize R22 clean-fleet — ~9min -> ~50s (10.5x), 136/136 byte-identical
- profiled the clean-fleet R22: extract-all ~6m11s (136 serial `splat split`) + check-all
  ~2m58s (136 serial builds) = ~9 min, all serial on a 32-core box
- the only shared WRITE is the 4 generated include/*.inc macros at extract time (identical
  content per binary); everything else is per-binary-disjoint and include/ is read-only during
  a build -> concurrent builds/extracts don't race
- Makefile: JOBS ?= 16 + `make extract-all` (seed main serial for the macros, then parallel-
  extract the rest via xargs -P) + parallel `make check-all` (xargs -P), correctness-gated
- MEASURED: `make clean && make extract-all && make check-all` = ~50s, check-all 136/136
  BYTE-IDENTICAL (== the serial result) -> ~10.5x. Compounds across the endgame (R22 per commit)
- new R22 recipe: `make clean && make extract-all && make check-all` (was the serial for-loop)
2026-07-12 19:12:33 -06:00

605 lines
35 KiB
Makefile
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Makefile — Brave Fencer Musashi decompilation (SLUS-00726, USA)
# =============================================================================
# Phase 4 deliverable. The ONLY live target is `check-env` (the Phase-4
# milestone: toolchain preflight). The split/build/check/expected/clean targets
# have their NAMES fixed here per docs/SETUP.md §6.3, but are loud-failing stubs
# until Phase 5 implements them. Run builds natively from the ext4 clone (H2/R2).
# =============================================================================
SHELL := /bin/bash
.ONESHELL:
.DEFAULT_GOAL := help
# --- paths & tooling ---------------------------------------------------------
PYTHON := python3
VENV := .venv
VENV_PY := $(VENV)/bin/python
MIPS_PREFIX := mipsel-linux-gnu-
AS := $(MIPS_PREFIX)as
LD := $(MIPS_PREFIX)ld
OBJCOPY := $(MIPS_PREFIX)objcopy
CC1_PSX := tools/bin/gcc-2.7.2-psx/cc1
CC1_CDK := tools/bin/gcc-2.7.2-cdk/cc1
MASPSX := tools/maspsx/maspsx.py
# =============================================================================
# Binaries — data-driven (Phase 9). Each binary is an alias key in BINARIES with a
# namespaced <alias>_* variable set. `main` is the retail EXE SLUS_007.26 (the FIRST
# instance); its artifact paths are PRESERVED VERBATIM (build/us/, *.us.* config) so
# its rebuild stays a byte-exact no-op. The clean <bin> path convention (config/
# splat.<bin>.yaml, build/<bin>/, config/check.<bin>.sha, config/symbols.<bin>.txt,
# .run/sig.<bin>.jsonl) is documented now but first INSTANTIATED by Phase 10's second
# binary. Select with `make build BINARY=<alias>`; defaults to the EXE.
# -----------------------------------------------------------------------------
# Overlay binaries (Phase 13): each location overlay is registered as an alias in the
# GENERATED config/overlays.mk (it defines OVERLAY_BINARIES + the per-<ov> var blocks),
# kept out of this hand-maintained file so tools/new_overlay.sh never edits the Makefile
# body. The `-include` is silent when absent (fresh clone / no overlays onboarded yet) ->
# OVERLAY_BINARIES expands empty -> BINARIES stays `main resident` and every byte-locked
# build is unchanged. Must precede the `:=` BINARIES line (simply-expanded -> read now).
-include config/overlays.mk
BINARIES := main resident $(OVERLAY_BINARIES)
BINARY ?= main
$(if $(filter $(BINARY),$(BINARIES)),,$(error BINARY='$(BINARY)' not in BINARIES='$(BINARIES)'))
# --- main (retail EXE SLUS_007.26) — values preserved from Phases 4-8 ---------
main_EXE := extracted/retail/SLUS_007.26
main_NAME := SLUS_007.26
main_OUT_DIR := build/us
main_OUT := $(main_OUT_DIR)/$(main_NAME)
main_ELF := $(main_OUT).elf
main_MAPFILE := $(main_OUT).map
main_LD_SCRIPT := $(main_OUT).ld
main_SPLAT_YAML := config/splat.us.exe.yaml
main_CHECK_SHA := config/check.us.sha
main_SYMBOLS := config/symbols.us.txt
main_SIG := .run/sig.SLUS_007.26.jsonl
main_GHIDRA_PROG := SLUS_007.26
# The fileoff->vram relation: text loads at file 0x800 / vram 0x80010000, so
# base = 0x80010000 - 0x800 = 0x8000F800. NOT a universal PS1 constant — overlays
# differ. Threaded into the tools as a REQUIRED param starting T5; defined here now.
main_VRAM_BASE := 0x8000F800
main_TEXT_LO := 0x80010000
main_TEXT_HI := 0x800629DC
# Source roots + undefined-sym outputs: main lives at the repo root (verbatim).
main_ASM_DIR := asm
main_SRC_DIR := src
main_UNDEF_SYMS := undefined_syms_auto.txt
main_UNDEF_FUNCS := undefined_funcs_auto.txt
# --- resident (engine blob MAIN.CD/FILE_010/1.1, vram 0x800CEDF8 — Phase 10) ----
# The always-resident engine blob: extracted type-1 (uncompressed) payload, load
# address RAM-proven in Phase 3 (T6b). Flat image (no PS-X EXE header); fileoff 0 ->
# vram 0x800CEDF8 so VRAM_BASE = 0x800CEDF8 (NOT main's 0x8000F800). 365,404 B (0x5935C)
# -> end vram 0x80128154. Its splat output NESTS under asm/resident + src/resident +
# build/resident, kept disjoint from main's asm/ + src/ by the per-binary OBJS glob below.
resident_EXE := extracted/retail/MAIN.CD.dir/FILE_010.dir/1.1
resident_NAME := resident
resident_OUT_DIR := build/resident
resident_OUT := $(resident_OUT_DIR)/$(resident_NAME)
resident_ELF := $(resident_OUT).elf
resident_MAPFILE := $(resident_OUT).map
resident_LD_SCRIPT := $(resident_OUT).ld
resident_SPLAT_YAML := config/splat.resident.yaml
resident_CHECK_SHA := config/check.resident.sha
resident_SYMBOLS := config/symbols.resident.txt
resident_SIG := .run/sig.resident.jsonl
resident_GHIDRA_PROG := resident
resident_VRAM_BASE := 0x800CEDF8
resident_TEXT_LO := 0x800CEDF8
resident_TEXT_HI := 0x80128154
resident_ASM_DIR := asm/resident
resident_SRC_DIR := src/resident
resident_UNDEF_SYMS := build/resident/undefined_syms_auto.txt
resident_UNDEF_FUNCS := build/resident/undefined_funcs_auto.txt
# --- selected-binary aliases (resolve $(BINARY) -> the active instance) -------
EXE := $($(BINARY)_EXE)
NAME := $($(BINARY)_NAME)
OUT_DIR := $($(BINARY)_OUT_DIR)
OUT := $($(BINARY)_OUT)
ELF := $($(BINARY)_ELF)
MAPFILE := $($(BINARY)_MAPFILE)
LD_SCRIPT := $($(BINARY)_LD_SCRIPT)
# Phase-26 §8: overlay jtbl-rodata carve args (empty = no carve). $(strip) so a per-binary var
# that is unset stays EMPTY (a trailing comment on the := line would leave whitespace -> non-empty
# -> the extract branch would misfire on every binary; caught on resident).
JTBL_INTERLEAVE := $(strip $($(BINARY)_JTBL_INTERLEAVE))
SPLAT_YAML := $($(BINARY)_SPLAT_YAML)
CHECK_SHA := $($(BINARY)_CHECK_SHA)
SYMBOLS := $($(BINARY)_SYMBOLS)
VRAM_BASE := $($(BINARY)_VRAM_BASE)
TEXT_LO := $($(BINARY)_TEXT_LO)
TEXT_HI := $($(BINARY)_TEXT_HI)
ASM_DIR := $($(BINARY)_ASM_DIR)
SRC_DIR := $($(BINARY)_SRC_DIR)
UNDEF_SYMS := $($(BINARY)_UNDEF_SYMS)
UNDEF_FUNCS := $($(BINARY)_UNDEF_FUNCS)
GHIDRA_PROG := $($(BINARY)_GHIDRA_PROG)
# cc1 smoke flags — the §5.4 first-candidate set; the real triple is pinned only
# after Phase-6 fingerprinting. Used here purely to prove cc1 executes.
CC1_SMOKE_FLAGS := -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
# binutils regression line (docs/SETUP.md §4.5): >= 2.38 is a WARN (PS1-matching
# regression suspect; 2.35 known-good). The verdict is revisited in Phase 5.
BINUTILS_WARN_MAJOR := 2
BINUTILS_WARN_MINOR := 38
.PHONY: help check-env extract build check expected clean report sig-refresh sig-overlays build-all check-all
# -----------------------------------------------------------------------------
help:
@echo "BFM-decomp — make targets:"
echo " make check-env Phase-4 toolchain preflight (the only live target)"
echo " make extract [Phase 5] splat split -> asm/ + linker scripts"
echo " make build [Phase 5] full pipeline -> build/us/SLUS_007.26 (+ SHA1 check)"
echo " make check [Phase 5] standalone SHA1 verification"
echo " make expected [Phase 5] snapshot build/us -> expected/ (asm-differ baseline)"
echo " make clean [Phase 5] remove build output"
echo " make report [Phase 7] regenerate docs/ progress+difficulty+duplicate digests"
echo " make sig-refresh [Phase 7] regenerate .run/sig.*.jsonl from Ghidra (MCP must be stopped)"
# -----------------------------------------------------------------------------
# Phase 7 reports: deterministic, committable docs/ digests. progress/difficulty/dup_report
# are Ghidra-free; sig-refresh regenerates dup_report's input from the saved Ghidra DB.
GHIDRA := $(or $(GHIDRA_INSTALL_DIR),$(HOME)/ghidra_12.1_PUBLIC)
GHIDRA_PROJ := $(HOME)/bfm-decomp/ghidra
report:
$(VENV_PY) tools/progress.py --binary $(BINARY) --audit
$(VENV_PY) tools/difficulty.py --binary $(BINARY)
$(VENV_PY) tools/dup_report.py --binary $(BINARY)
# Cross-binary dedup report (Phase 11): binary-spanning, run ONCE (not per-binary), so it
# only fires for the default binary — avoids `make report BINARY=resident` rewriting the
# identical file. --cross ignores --binary and scans every sig in BINARIES.
ifeq ($(BINARY),main)
$(VENV_PY) tools/dup_report.py --cross
# Fleet roll-up (Phase 15): deterministic per-binary table + fleet totals -> docs/progress.fleet.md.
$(VENV_PY) tools/progress.py --fleet
# Byte-honesty gate (Phase 11): fail-closed if any registered code-share drifted from its
# recorded signature hash. Last in the recipe, so a stale share fails `make report` (P9).
$(VENV_PY) tools/dedup_integrate.py --check
endif
sig-refresh:
@if ss -tln 2>/dev/null | grep -qE ':8080([^0-9]|$$)'; then
echo "sig-refresh: ERROR — Ghidra MCP serving on :8080; run tools/ghidra_mcp_stop.sh first."; exit 2
fi
"$(GHIDRA)/support/analyzeHeadless" "$(GHIDRA_PROJ)" bfm -process $(GHIDRA_PROG) -noanalysis -readOnly \
-scriptPath tools/ghidra_scripts -postScript DumpFunctionSignatures.java
# sig-overlays (Phase 11): Ghidra-FREE — sign every location-overlay payload (the 134 SCxx 0.4.dec)
# at the shared overlay vram with tools/sig_image.py, so `make report` (--cross) can find cross-overlay
# duplicates. Each -> .run/sig.ov_<SCxx>_<nnn>.jsonl (gitignored; regenerable). Re-run when overlays
# change; not part of `make report` (it scans whatever ov_* sigs exist, like sig-refresh).
OVERLAY_VRAM := 0x80128158
sig-overlays:
@n=0
for f in $$(find extracted/retail -path '*SC*.CD.dir/FILE_*.dir/0.4.dec' | sort); do
nm=$$(echo "$$f" | sed -E 's|.*/(SC[0-9]+)\.CD\.dir/FILE_([0-9]+)\.dir.*|ov_\1_\2|')
$(VENV_PY) tools/sig_image.py --image "$$f" --vram-base $(OVERLAY_VRAM) --bootstrap --name "$$nm" >/dev/null
n=$$((n+1))
done
echo "sig-overlays: signed $$n overlays -> .run/sig.ov_*.jsonl"
# -----------------------------------------------------------------------------
# check-env: assert every Phase-4 toolchain component. Runs ALL checks (does not
# stop at the first failure) so the report is complete, then exits nonzero if any
# hard check failed. binutils >= 2.38 is a WARN, never a FAIL (§4.5).
check-env:
@fail=0
echo "== BFM-decomp environment preflight (Phase 4 check-env) =="
echo
# 1) Python >= 3.12 (system python3 drives tooling + the EXE-hash import)
pyver=$$($(PYTHON) -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null)
if $(PYTHON) -c 'import sys; raise SystemExit(0 if sys.version_info[:2] >= (3,12) else 1)' 2>/dev/null; then
echo "[PASS] python3 $$pyver (>= 3.12)"
else
echo "[FAIL] python3 $${pyver:-not-found} (need >= 3.12)"; fail=1
fi
# 2) venv present + splat importable
if [ -x "$(VENV_PY)" ]; then
if $(VENV_PY) -c 'import splat' 2>/dev/null; then
sv=$$($(VENV_PY) -c 'import importlib.metadata as m; print(m.version("splat64"))' 2>/dev/null)
echo "[PASS] venv 'import splat' OK (splat64 $${sv:-?})"
else
echo "[FAIL] venv present but 'import splat' failed (run: $(VENV_PY) -m pip install 'splat64[mips]>=0.41.0,<1.0.0')"; fail=1
fi
else
echo "[FAIL] $(VENV_PY) missing (run: $(PYTHON) -m venv $(VENV) && $(VENV)/bin/pip install 'splat64[mips]>=0.41.0,<1.0.0')"; fail=1
fi
# 3) cc1 candidates executable — R12-clean smoke (stdin -> /dev/null, no temp file)
for cc1 in "$(CC1_PSX)" "$(CC1_CDK)"; do
if [ -x "$$cc1" ] && echo 'int _ce(){return 0;}' | "$$cc1" $(CC1_SMOKE_FLAGS) -o /dev/null 2>/dev/null; then
echo "[PASS] cc1 runs: $$cc1"
else
echo "[FAIL] cc1 not runnable: $$cc1 (see docs/SETUP.md §4.7)"; fail=1
fi
done
# 4) maspsx submodule populated
if [ -f "$(MASPSX)" ]; then
echo "[PASS] maspsx present: $(MASPSX)"
else
echo "[FAIL] $(MASPSX) missing (run: git submodule update --init)"; fail=1
fi
# 5) mipsel binutils on PATH (as / ld / objcopy)
for t in $(AS) $(LD) $(OBJCOPY); do
if command -v $$t >/dev/null 2>&1; then
echo "[PASS] $$t: $$($$t --version | head -1)"
else
echo "[FAIL] $$t not on PATH (apt install binutils-mipsel-linux-gnu)"; fail=1
fi
done
# 5b) binutils regression line: >= 2.38 -> WARN (not FAIL); §4.5
asver=$$($(AS) --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+' | head -1)
if [ -n "$$asver" ]; then
amaj=$${asver%%.*}; amin=$${asver##*.}
if [ "$$amaj" -gt $(BINUTILS_WARN_MAJOR) ] || { [ "$$amaj" -eq $(BINUTILS_WARN_MAJOR) ] && [ "$$amin" -ge $(BINUTILS_WARN_MINOR) ]; }; then
echo "[WARN] mipsel binutils $$asver >= 2.38 — PS1-matching regression suspect (2.35 known-good); revisit in Phase 5 (docs/SETUP.md §4.5)"
else
echo "[PASS] mipsel binutils $$asver (< 2.38)"
fi
fi
# 6) committed EXE hash == EXPECTED_EXE_SHA1 (reused constant; fresh-clone-safe)
if [ -f "$(EXE)" ]; then
want=$$($(PYTHON) -c 'from tools.bfm_extract.extract_exe import EXPECTED_EXE_SHA1 as h; print(h)' 2>/dev/null)
got=$$(sha1sum "$(EXE)" | cut -d' ' -f1)
if [ -n "$$want" ] && [ "$$got" = "$$want" ]; then
echo "[PASS] $(EXE) sha1 $$got == EXPECTED_EXE_SHA1"
else
echo "[FAIL] $(EXE) sha1 $${got:-none} != expected $${want:-unknown}"; fail=1
fi
else
echo "[FAIL] $(EXE) missing (committed retail EXE)"; fail=1
fi
echo
if [ "$$fail" -ne 0 ]; then
echo "check-env: FAIL — see the [FAIL] lines above."
exit 1
fi
echo "check-env: OK — Phase-4 toolchain ready."
# -----------------------------------------------------------------------------
# Phase-5 build: splat split -> assemble -> link -> objcopy -> SHA1 check.
# The code is 100% assembly (the "all-asm byte-match" milestone). The cpp->cc1->
# maspsx->as path is documented below but dormant until Phase 6 adds `c` segments.
SPLAT := $(VENV_PY) -m splat
CPP := $(MIPS_PREFIX)cpp
# (SPLAT_YAML / OUT_DIR / OUT / ELF / MAPFILE / LD_SCRIPT / CHECK_SHA are per-binary
# aliases in the "Binaries" data block near the top of this file — Phase 9. UNDEF_SYMS /
# UNDEF_FUNCS / ASM_DIR / SRC_DIR joined them per-binary in Phase 10: a second binary
# writes its undefined_*_auto under build/<bin>/ and nests its sources under <bin>/.)
# Phase 7 (Task 2'): link the real PsyQ libcd SDK objects in place of the libcd-region asm stubs.
# tools/psyq_integrate.py rewrites the splat .ld (swap stub objects -> build/psyq/libcd/*.o + NOLOAD
# data placement, no carving) and emits the externals defsym fragment. Conditional on the SDK ELF
# objects being present (gitignored, SDK-derived, via tools/psyq_build_libs.sh LIBCD); a fresh clone
# without them builds byte-identically via the stubs.
LIBCD_ELF := .run/obj40/libcd
LIBCD_OBJDIR := build/psyq/libcd
LIBCD_SYMS := build/psyq/libcd_externals.ld
# libgs (Phase 7 Task #9, FULL integration): 31 libgs objects in 6 contiguous blocks linked in place
# of the libgs1..libgs6 block stubs (the 5 non-libgs gaps stay gsgapN asm stubs). Same conditional/
# idempotent model as libcd. The curated object dir is SDK-derived (gitignored), regenerated by
# tools/make_libgs.sh (needs the LIBGS ELF from psyq_build_libs.sh LIBGS). GS_106 (block 4) anchors
# uniquely only within the libgs window, so the integrate call passes 0x80051804 0x80057928.
LIBGS_ELF := .run/obj40/libgs_used
LIBGS_OBJDIR := build/psyq/libgs
LIBGS_SYMS := build/psyq/libgs_externals.ld
# libetc (Phase 8): 5 objects (VSYNC/INTR/INTR_VB/INTR_DMA/VMODE) in ONE contiguous block at the tail
# of the old 800 subseg (ends at libcd1). Single stub "libetc"; no placement window needed (all 5
# anchor uniquely over the full text window). Same conditional/idempotent model as libcd/libgs.
LIBETC_ELF := .run/obj40/libetc
LIBETC_OBJDIR := build/psyq/libetc
LIBETC_SYMS := build/psyq/libetc_externals.ld
# libgpu (Phase 8): EXT+PRIM only — SYS.o EXCLUDED (scattered-.bss, cookbook §9.1, GS_001 class; stays a
# stub in 800c). Curated dir libgpu_used = {EXT,PRIM}; regenerate: tools/psyq_build_libs.sh LIBGPU then
# `mkdir -p .run/obj40/libgpu_used && cp .run/obj40/libgpu/{EXT,PRIM}.o .run/obj40/libgpu_used/`.
LIBGPU_ELF := .run/obj40/libgpu_used
LIBGPU_OBJDIR := build/psyq/libgpu
LIBGPU_SYMS := build/psyq/libgpu_externals.ld
# libmcrd (Phase 8): 2 objects (LIBMCRD.o = the 55 LIBMCRD_OBJ_* + _card_* memcard I/O; USERFUNC.o), 2
# non-adjacent blocks. Clean (.bss commons all recovered). NB: these are the libmcrd SDK objects; the
# GAME's SaveLoadRoutine/Q#5 save logic is a separate Phase-12 item.
LIBMCRD_ELF := .run/obj40/libmcrd
LIBMCRD_OBJDIR := build/psyq/libmcrd
LIBMCRD_SYMS := build/psyq/libmcrd_externals.ld
# libc2 (Phase 8): C stdlib, 17 objects, 2 blocks (16-obj main run libc2_1 + STRCAT.o libc2_2). Clean
# (PRNT.o's printf-format jtbl resolves via NOLOAD .rodata).
LIBC2_ELF := .run/obj40/libc2
LIBC2_OBJDIR := build/psyq/libc2
LIBC2_SYMS := build/psyq/libc2_externals.ld
# libgte (Phase 8): GTE math, 53 objects in 22 blocks across the 800b region (subseg lines generated by
# tools/gen_lib_subsegs.py). The integrate window 0x4787C..0x51804 restricts placement to 800b so it
# sees 22 blocks (excludes the 5 deferred libgs-gap objects MTX_05/07/11/REG03/REG11; gsgap1/2/4/5 stay
# stubs). Clean (no scattered .bss). stub list = libgte1..libgte22.
LIBGTE_ELF := .run/obj40/libgte
LIBGTE_OBJDIR := build/psyq/libgte
LIBGTE_SYMS := build/psyq/libgte_externals.ld
LIBGTE_STUBS := libgte1,libgte2,libgte3,libgte4,libgte5,libgte6,libgte7,libgte8,libgte9,libgte10,libgte11,libgte12,libgte13,libgte14,libgte15,libgte16,libgte17,libgte18,libgte19,libgte20,libgte21,libgte22
# Combined libspu+libsnd sound region (Phase 8): the two SDK sound libs interleave in 0x3A444..0x4239C
# so they link as one 60-object region (snd1..snd9). Curated dir .run/obj40/snd_used built by
# tools/make_snd_used.py (4 addresses excluded as scattered-.bss/false-positive stubs). Window arg below.
SND_ELF := .run/obj40/snd_used
SND_OBJDIR := build/psyq/snd
SND_SYMS := build/psyq/snd_externals.ld
SND_STUBS := snd1,snd2,snd3,snd4,snd5,snd6,snd7,snd8,snd9
# Combined libapi+libcard 800c2 region (Phase 8): 22 objects in 4 blocks (apicard1..4). Curated dir
# .run/obj40/apicard_used (tools/make_apicard_used.py). Window 0x61F38..0x62888. (libapi's ~22 objects
# in the 800c3 region are DEFERRED — lowest value.)
APICARD_ELF := .run/obj40/apicard_used
APICARD_OBJDIR := build/psyq/apicard
APICARD_SYMS := build/psyq/apicard_externals.ld
APICARD_STUBS := apicard1,apicard2,apicard3,apicard4
# Assembler flags (docs/SETUP.md §6.2). -G0 is confirmed by the disassembly
# (ledger #8: zero $gp-relative addressing). -no-pad-sections keeps section ends
# un-padded so the link reproduces the original layout.
ASFLAGS := -Iinclude -march=r3000 -mtune=r3000 -no-pad-sections -O1 -G0
# maspsx ASPSX version — ALWAYS explicit (G8). Inert for the all-asm build; the
# real pin is Phase 6. (Only used on the future cpp->cc1->maspsx `c` path.)
ASPSX_VERSION := 2.56
# Extra maspsx flags. --expand-div is PINNED (Phase-6 fingerprint): the original
# emits the full aspsx div sequence (divu + bnez + break 0x7 zero-check); without it
# maspsx leaves a bare divu and div/rem functions never match. Only affects div/rem,
# so the all-INCLUDE_ASM build and div-free functions are unchanged.
MASPSX_FLAGS := --expand-div
# Object set must match the splat linker script's references. After the Phase-6 asm->c
# flip the text subseg is src/800.c -> build/src/800.o; the per-function
# asm/nonmatchings/<seg>/*.s are TEXTUALLY .include'd by the .c (via INCLUDE_ASM) at
# assembly time, so they are NOT separate objects and must be excluded from the glob.
# header.s and the data subseg stay asm. Globbed at parse time -> run the canonical
# `make extract && make build`.
# Per-binary object scoping (Phase 10): main's sources live at the repo-level asm/ + src/;
# a second binary (resident) nests at asm/<bin>/ + src/<bin>/. The active binary's roots are
# $(ASM_DIR)/$(SRC_DIR). main's roots CONTAIN the nested siblings, so they must be pruned from
# main's glob (else resident's .s/.c contaminate main's OBJS and the link). The prune list is
# DERIVED FROM $(BINARIES) — the $(filter $(ASM_DIR)/%,...) guard prunes only a sibling whose
# root is genuinely nested under the active root, so it self-balances as binaries are added.
OTHER_BINS := $(filter-out $(BINARY),$(BINARIES))
ASM_PRUNE := $(foreach b,$(OTHER_BINS),$(if $(filter $(ASM_DIR)/%,$($(b)_ASM_DIR)),-not -path '$($(b)_ASM_DIR)/*'))
SRC_PRUNE := $(foreach b,$(OTHER_BINS),$(if $(filter $(SRC_DIR)/%,$($(b)_SRC_DIR)),-not -path '$($(b)_SRC_DIR)/*'))
ASM_SRCS := $(shell find $(ASM_DIR) -name '*.s' -not -path '$(ASM_DIR)/nonmatchings/*' $(ASM_PRUNE) 2>/dev/null)
C_SRCS := $(shell find $(SRC_DIR) -name '*.c' $(SRC_PRUNE) 2>/dev/null)
OBJS := $(ASM_SRCS:%.s=build/%.o) $(C_SRCS:%.c=build/%.o)
# Header-dependency tracking (Phase 15): now that shared headers (src/shared/*.h, common.h)
# are build inputs, the cpp stage emits a .d per C object (-MMD, below) so editing a #included
# header triggers a recompile — incremental `make check` stays trustworthy (R22). .d files live
# under build/ (gitignored); -include ignores them on the first build. No effect on output bytes.
C_DEPS := $(C_SRCS:%.c=build/%.d)
-include $(C_DEPS)
# splat `bin` subsegs (raw byte regions — e.g. an overlay's trailing non-word-aligned bytes that
# spimdisasm's data path drops, since it won't emit a <4-byte partial word). splat extracts them to
# assets/<alias>/*.bin and references build/assets/<alias>/*.o in the .ld; wrap each raw .bin into a
# linkable object (bytes verbatim in .data). Per-binary: asset_path is scoped to assets/<alias> so
# overlays' same-named `trailing.bin` never collide; main/resident have no assets -> empty.
ASSET_BINS := $(shell find assets/$(BINARY) -name '*.bin' 2>/dev/null)
ASSET_OBJS := $(ASSET_BINS:assets/%.bin=build/assets/%.o)
# extract: splat split -> asm/, the linker script, include/ macros, undefined_*_auto.txt.
extract:
@mkdir -p $(OUT_DIR)
$(SPLAT) split $(SPLAT_YAML)
ifeq ($(BINARY),main)
# Phase 7 (LZSS): reorder splat's section-major .main into the real
# .data(front) -> .rodata -> .data(tail) sandwich, so the migrated LZSS
# jtbl_80072A38 (800.o .rodata) lands at 0x80072A38 between 531DC.data and
# 6324C.data. Idempotent; keyed off splat's exact output (re-run = no-op).
# EXE-only (overlays have no rodata island) — gated to BINARY=main; --front/--tail
# name the sandwich .data objects (cookbook §8).
$(PYTHON) tools/ld_interleave.py --front 53198.data.o --tail 6324C.data.o $(LD_SCRIPT)
endif
# Phase-26 §8: overlays that carve a jr-function's jtbl into a dotted .rodata subseg run
# ld_interleave to place the migrated .rodata between the pre/post data-tail chunks (the
# data->rodata->data sandwich; cookbook §8). <bin>_JTBL_INTERLEAVE holds the --front/--tail
# object basenames (set per overlay in config/overlays.mk). Empty for overlays with no carve.
ifneq ($(JTBL_INTERLEAVE),)
$(PYTHON) tools/ld_interleave.py --section .$(BINARY) $(JTBL_INTERLEAVE) $(LD_SCRIPT)
endif
# The linker script is an `extract` output, not produced by `build` — guard with a
# friendly message instead of make's raw "No rule to make target".
$(LD_SCRIPT):
@echo "make: $(LD_SCRIPT) missing — run 'make extract' first."; exit 1
# Assemble one splat .s (all-asm path).
build/asm/%.o: asm/%.s
@mkdir -p $(dir $@)
@echo " AS $@"
@$(AS) $(ASFLAGS) -o $@ $<
# Wrap a splat `bin` asset (raw bytes) into a linkable object: assemble a one-line stub that
# .incbin's the raw file into .data (format-safe — same mipsel-as as everything else, no objcopy
# -I binary arch guessing). The .ld pulls it by path. The --set-section-alignment forces .data to
# 1-byte align (as defaults it to 16) so ld places the 1-3 trailing bytes at the exact word-floor
# offset and does NOT pad the image up to a 16/8-byte boundary (that added a stray byte otherwise).
build/assets/%.o: assets/%.bin
@mkdir -p $(dir $@)
@echo " INCBIN $@"
@printf '.section .data\n.incbin "%s"\n' "$<" | $(AS) $(ASFLAGS) -o $@
@$(OBJCOPY) --set-section-alignment .data=1 $@
# C path (Phase 6): modern cpp -> vintage cc1 -> maspsx -> modern as. Each src/*.c is
# splat-generated INCLUDE_ASM stubs (file-scope __asm__ .include of the per-function
# asm/nonmatchings/<seg>/<fn>.s); as we match, stubs are replaced by real C. The flags
# below are the docs/SETUP.md §5.4 FIRST-CANDIDATE set — provisional until the Phase-6
# fingerprint ladder PINS the triple (then this block + ASPSX_VERSION are updated, G8).
CPPFLAGS := -lang-c -Iinclude -undef -Wall -fno-builtin -Dmips -D__GNUC__=2 -D__OPTIMIZE__ -Dpsx -D_PSYQ -D_MIPSEL -D_LANGUAGE_C
CC1FLAGS := -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
build/src/%.o: src/%.c
@mkdir -p $(dir $@)
@echo " CC $@"
@set -o pipefail; $(CPP) $(CPPFLAGS) -MMD -MP -MT $@ -MF $(@:.o=.d) $< | $(CC1_PSX) $(CC1FLAGS) | $(VENV_PY) $(MASPSX) --aspsx-version=$(ASPSX_VERSION) $(MASPSX_FLAGS) | $(AS) $(ASFLAGS) -o $@
# Per-module optimization override (SETUP §5.5 — per-module compiler mixing). The boot/
# main/game-mode-dispatch module (src/boot.c, vram 0x80010000-0x800123F0) was compiled at
# -O0, NOT the -O2 game-code default: frame-pointer setup + unfolded large-offset loads
# are the evidence (GameModeDispatch byte-matches only at -O0). gcc 2.7.2 has no
# per-function optimize pragma, so opt level is per-file. Target-specific CC1FLAGS (the
# pattern recipe reads $(CC1FLAGS), so this overrides it for just build/src/boot.o):
build/src/boot.o: CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
# Phase-19 T1: same per-file -O0 mechanism for the ov_SC01_077 -O0 cluster (16 contiguous fns
# vram 0x8013B568..0x8013C98C, prologue sig 21F0A003). Split into its own .c by the splat config
# (config/splat.ov_SC01_077.yaml) so this override reaches just that .o.
build/src/ov_SC01_077/ov_SC01_077_o0.o: CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
# 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.
WHALE_O0B_OBJS := $(patsubst src/%.c,build/src/%.o,$(wildcard src/ov_*/ov_*_o0b.c))
$(WHALE_O0B_OBJS): CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
# link (the .ld pulls in the .o by path) + objcopy to the raw PS-X EXE image.
$(OUT): $(OBJS) $(ASSET_OBJS) $(LD_SCRIPT)
@set -e
mkdir -p $(dir $@)
# PsyQ SDK library integrations are EXE-only (libgs/libgte/sound/apicard are
# SLUS_007.26's layout). A second binary (BINARY != main) skips this block and links
# its own stubs. NB: ifeq/endif are make directives (column 0, no tab), resolved at
# parse time; with .ONESHELL the included recipe lines still run as one shell.
ifeq ($(BINARY),main)
# Wire in the real libcd objects (after the build objects exist — the externals discovery
# trial-links the whole image). Idempotent: re-running re-derives the externals only.
if [ -d "$(LIBCD_ELF)" ]; then
$(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) $(LIBCD_ELF) $(LD_SCRIPT) $(LIBCD_OBJDIR) $(LIBCD_SYMS) libcd1,libcd2
else
echo " (no $(LIBCD_ELF) — libcd region stays asm stubs; run tools/psyq_build_libs.sh LIBCD)"
fi
if [ -d "$(LIBGS_ELF)" ]; then
$(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) $(LIBGS_ELF) $(LD_SCRIPT) $(LIBGS_OBJDIR) $(LIBGS_SYMS) libgs1,libgs2,libgs3,libgs4,libgs5,libgs6 0x80051804 0x80057928
else
echo " (no $(LIBGS_ELF) — libgs region stays asm stubs; run tools/make_libgs.sh)"
fi
if [ -d "$(LIBETC_ELF)" ]; then
$(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) $(LIBETC_ELF) $(LD_SCRIPT) $(LIBETC_OBJDIR) $(LIBETC_SYMS) libetc
else
echo " (no $(LIBETC_ELF) — libetc region stays asm stubs; run tools/psyq_build_libs.sh LIBETC)"
fi
if [ -d "$(LIBGPU_ELF)" ]; then
$(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) $(LIBGPU_ELF) $(LD_SCRIPT) $(LIBGPU_OBJDIR) $(LIBGPU_SYMS) libgpu
else
echo " (no $(LIBGPU_ELF) — libgpu region stays asm stubs; run tools/psyq_build_libs.sh LIBGPU + curate libgpu_used)"
fi
if [ -d "$(LIBMCRD_ELF)" ]; then
$(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) $(LIBMCRD_ELF) $(LD_SCRIPT) $(LIBMCRD_OBJDIR) $(LIBMCRD_SYMS) libmcrd1,libmcrd2
else
echo " (no $(LIBMCRD_ELF) — libmcrd region stays asm stubs; run tools/psyq_build_libs.sh LIBMCRD)"
fi
if [ -d "$(LIBC2_ELF)" ]; then
$(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) $(LIBC2_ELF) $(LD_SCRIPT) $(LIBC2_OBJDIR) $(LIBC2_SYMS) libc2_1,libc2_2
else
echo " (no $(LIBC2_ELF) — libc2 region stays asm stubs; run tools/psyq_build_libs.sh LIBC2)"
fi
if [ -d "$(LIBGTE_ELF)" ]; then
$(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) $(LIBGTE_ELF) $(LD_SCRIPT) $(LIBGTE_OBJDIR) $(LIBGTE_SYMS) $(LIBGTE_STUBS) 0x8004787C 0x80051804
else
echo " (no $(LIBGTE_ELF) — libgte region stays asm stubs; run tools/psyq_build_libs.sh LIBGTE)"
fi
if [ -d "$(SND_ELF)" ]; then
$(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) $(SND_ELF) $(LD_SCRIPT) $(SND_OBJDIR) $(SND_SYMS) $(SND_STUBS) 0x8003A444 0x8004239C
else
echo " (no $(SND_ELF) — sound region stays asm stubs; run tools/psyq_build_libs.sh LIBSPU LIBSND + tools/make_snd_used.py)"
fi
if [ -d "$(APICARD_ELF)" ]; then
$(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) $(APICARD_ELF) $(LD_SCRIPT) $(APICARD_OBJDIR) $(APICARD_SYMS) $(APICARD_STUBS) 0x80061F38 0x80062888
else
echo " (no $(APICARD_ELF) — apicard region stays asm stubs; run tools/psyq_build_libs.sh LIBAPI LIBCARD + tools/make_apicard_used.py)"
fi
endif
SYMS=""; [ -f "$(LIBCD_SYMS)" ] && SYMS="-T $(LIBCD_SYMS)"; [ -f "$(LIBGS_SYMS)" ] && SYMS="$$SYMS -T $(LIBGS_SYMS)"; [ -f "$(LIBETC_SYMS)" ] && SYMS="$$SYMS -T $(LIBETC_SYMS)"; [ -f "$(LIBGPU_SYMS)" ] && SYMS="$$SYMS -T $(LIBGPU_SYMS)"; [ -f "$(LIBMCRD_SYMS)" ] && SYMS="$$SYMS -T $(LIBMCRD_SYMS)"; [ -f "$(LIBC2_SYMS)" ] && SYMS="$$SYMS -T $(LIBC2_SYMS)"; [ -f "$(LIBGTE_SYMS)" ] && SYMS="$$SYMS -T $(LIBGTE_SYMS)"; [ -f "$(SND_SYMS)" ] && SYMS="$$SYMS -T $(SND_SYMS)"; [ -f "$(APICARD_SYMS)" ] && SYMS="$$SYMS -T $(APICARD_SYMS)"
echo " LD $(ELF)"
$(LD) -T $(LD_SCRIPT) -T $(UNDEF_SYMS) -T $(UNDEF_FUNCS) $$SYMS --no-check-sections -Map $(MAPFILE) -o $(ELF)
echo " OBJCOPY $@"
$(OBJCOPY) -O binary $(ELF) $@
# Trim the linker's end-of-segment 4-align pad: splat's .ld does `. = ALIGN(., 4)` after the
# data section, over-emitting up to 3 zero bytes when the payload size isn't 4-aligned (most
# overlays; the EXE + resident are 4-aligned so this never fires for them). Shrink-ONLY and
# capped at 3 bytes -> it can never hide a real shortfall (build < target fails the SHA) nor
# extend the image. The target's true byte length is the matched payload $(EXE).
tsz=$$(stat -c%s "$(EXE)"); osz=$$(stat -c%s "$@"); d=$$((osz - tsz))
if [ "$$d" -gt 0 ] && [ "$$d" -le 3 ]; then truncate -s "$$tsz" "$@"; echo " TRIM $@ (-$$d B linker end-align pad)"; fi
# build = produce $(OUT) and verify its SHA1 (check pulls in $(OUT)).
build: check
# extract-all / build-all / check-all (Phase 13; PARALLELIZED Phase 26): build + SHA1-check EVERY
# binary in $(BINARIES) -> a single fleet PASS/FAIL. Recursion ($(MAKE) BINARY={}) RE-PARSES the
# Makefile per binary so each gets its correctly-pruned OBJS (a `foreach` can't — the OBJS glob is
# parse-time, keyed on $(BINARY)). PARALLEL across binaries via `xargs -P$(JOBS)`: every binary's
# outputs are per-binary-disjoint (asm/<bin>, build/<bin>, build/{src,asm}/<bin>) and include/ is
# READ-ONLY during a build, so concurrent builds never race. The one shared WRITE is the 4 generated
# include/*.inc macros at EXTRACT time (identical content per binary) -> `extract-all` seeds them once
# via main (serial) before fanning out. Proven 136/136 byte-identical, ~10x faster (Phase 26: the
# serial R22 ~9m -> the parallel R22 ~1m). CLEAN fleet proof (R22 — clean rebuild):
# make clean && make extract-all && make check-all
JOBS ?= 16 # parallel binary builds/extracts (override: `make check-all JOBS=32`)
# extract-all: splat-split every binary. Seed `main` FIRST (serial) so the shared include/*.inc macros
# (+ build/psyq) exist before the parallel fan-out; then extract the rest in parallel.
extract-all:
@mkdir -p .run; : > .run/extract-all.txt
$(MAKE) --no-print-directory extract BINARY=main
echo "$(filter-out main,$(BINARIES))" | tr ' ' '\n' | xargs -P$(JOBS) -I{} sh -c \
'$(MAKE) --no-print-directory extract BINARY={} >.run/extract.{}.log 2>&1 || echo "[EXTRACT FAIL] {}"' \
| tee .run/extract-all.txt
! grep -q "EXTRACT FAIL" .run/extract-all.txt
check-all:
@mkdir -p .run; : > .run/check-all.txt
echo "$(BINARIES)" | tr ' ' '\n' | xargs -P$(JOBS) -I{} sh -c \
'$(MAKE) --no-print-directory check BINARY={} >.run/check.{}.log 2>&1 && echo "[ OK ] {}" || { echo "[FAIL] {}"; tail -3 .run/check.{}.log >&2; }' \
| tee .run/check-all.txt
pass=$$(grep -c "^\[ OK \]" .run/check-all.txt); fail=$$(grep -c "^\[FAIL\]" .run/check-all.txt)
echo "check-all: $$pass passed, $$fail failed of $(words $(BINARIES))"
[ "$$fail" -eq 0 ]
build-all: check-all
# check: SHA1 of the build vs the committed original hash. The definition of "build OK".
check: $(OUT)
@got=$$(sha1sum $(OUT) | cut -d' ' -f1)
want=$$(cut -d' ' -f1 $(CHECK_SHA) 2>/dev/null)
if [ -z "$$want" ]; then echo "[FAIL] $(CHECK_SHA) missing or empty"; exit 1; fi
if [ "$$got" = "$$want" ]; then
echo "[ OK ] $(OUT)"
echo " sha1 $$got == $(CHECK_SHA) (BYTE-IDENTICAL)"
else
echo "[FAIL] $(OUT)"
echo " got $$got"
echo " want $$want"
exit 1
fi
# expected: snapshot a SHA1-verified build into expected/build/ as the asm-differ
# baseline for Phase 6 (asm-differ diffs build/<obj> vs expected/build/<obj>).
expected: build
@set -e
mkdir -p expected/build
# Per-binary-safe (Phase 10): refresh ONLY the active binary's image dir, then merge-copy
# build/ into the shared expected/ mirror. cp MERGES (never deletes), so `make expected
# BINARY=X` preserves binary Y's baseline even when Y isn't currently in build/ (the old
# `rm -rf expected/build` wiped every sibling). asm-differ reads expected/$(OUT) (image mode)
# + expected/build/<obj> (object mode); both resolve under the merged mirror.
rm -rf expected/$(OUT_DIR)
cp -r build/. expected/build/
echo "expected: baseline refreshed for binary=$(BINARY) -> expected/build/ (siblings preserved)"
# clean: remove ALL regenerable outputs (build/ + the splat tree) so a config change
# is followed by a stale-free `make clean && make extract && make build` (H3).
clean:
@rm -rf build expected asm assets undefined_syms_auto.txt undefined_funcs_auto.txt
@rm -f include/include_asm.h include/macro.inc include/labels.inc include/gte_macros.inc
@echo "clean: removed build/, expected/, and the regenerated splat tree (asm/, assets/, include macros, undefined_*_auto.txt)."