- apt: build-essential, binutils-mipsel-linux-gnu 2.42 (+gcc-mipsel 12.x), ninja
1.11.1, python3-pip/venv, libelf-dev, clang-format, p7zip, bchunk, bsdmainutils
- .venv + splat64 0.41.0 ([mips]: spimdisasm 1.41.0, rabbitizer 1.16.2, PyYAML 6.0.3)
- 4 pinned submodules under tools/: maspsx@874855c5, decomp-permuter@b44b0622,
asm-differ@2ad4a4a4, m2c@4266cc28 (.gitmodules)
- old-gcc 0.17 cc1 candidates -> tools/bin/gcc-2.7.2-{psx,cdk}/ (32-bit i386 static;
tarballs are flat + same-named -> per-subdir extraction); CHECKSUMS.sha256 committed
(gitignore exception !/tools/bin/*.sha256); psx cc1 self-id "GNU C 2.7.2 [AL 1.1,
MM 40] Sony Playstation"
- Makefile: check-env (.ONESHELL bash) asserts python>=3.12, venv import splat, both
cc1 (stdin smoke), maspsx, mipsel as/ld/objcopy, sha1(EXE)==EXPECTED_EXE_SHA1;
binutils>=2.38 -> WARN (verdict deferred to Phase 5, §4.5); extract/build/check/
expected/clean are loud-failing Phase-5 stubs (names per §6.3)
- docs/SETUP.md: as-built versions; §4.7 corrected (i386, flat-tarball fix); §4.8
deferred to Phase 6; ledger #5 confirmed (Ubuntu 24.04.4), #6 (2.42), #7 (sha256)
- MILESTONE: make check-env exits 0; zero ROM-derived bytes staged
- bumps project version 1.3.1 -> 1.4.0
45 KiB
SETUP.md — Environment Setup & Daily Operations Reference
This file is the EVOLVABLE reference layer. Unlike
PROJECT_CONTEXT.md(permanent, never edited), this document holds volatile facts — pinned versions, URLs, commands, ports — and may be updated freely as tools move. Note each change in the active phase log (CURRENT_PHASE.md). Items marked TBD / UNVERIFIED / JP-only — re-derive for US are honest gaps: confirm before relying on them, then update this file.
Last full revision: 2026-06-10 (initial authoring, pre-Phase-1 — nothing below is installed yet except the repo itself; same-day conversion to the all-in-WSL / Linux-first architecture — everything now runs inside a single WSL2 Ubuntu 24.04 clone, no Windows/WSL split).
Version pin summary
All components run inside WSL2 Ubuntu 24.04 (single ext4 clone). The "Side" column below is retained for historical continuity but every entry is now WSL2 (Linux).
| Component | Pinned version | Side | Phase |
|---|---|---|---|
| JDK | 21 (Temurin) | WSL2 (Linux) | 1 |
| Ghidra | 12.1 PUBLIC exactly (ghidra_12.1_PUBLIC_20260513.zip) — NOT 12.1.2 |
WSL2 (Linux) | 1 |
| GhidrAssistMCP | v2.8.0 (ghidra_12.1_PUBLIC_20260530_GhidrAssistMCP.zip) |
WSL2 (Linux) | 1 |
| ghidra_psx_ldr | release 2026.06.04 (ghidra_12.1_PUBLIC_20260604_ghidra_psx_ldr.zip) |
WSL2 (Linux) | 1 |
| PCSX-Redux | Linux build (AppImage/flatpak; no version pin; record build on install) | WSL2 (Linux) | 3 |
| WSL2 distro | Ubuntu-24.04 (Python 3.12 ships with it) | WSL2 (Linux) | 4 |
| splat | pip splat64[mips] >=0.41.0,<1.0.0 — as-built 0.41.0 (Phase 4); freeze exact once Phase 5 green |
WSL2 (Linux) | 4 |
| Vintage compiler | decompals/old-gcc release 0.17: gcc-2.7.2-psx + gcc-2.7.2-cdk |
WSL2 (Linux) | 4 |
| maspsx | git submodule, mkst/maspsx (decomp.me pins commit 874855c53f65f8fa57447e1da6bde6236dbef9d5 — reasonable default pin) |
WSL2 (Linux) | 4 |
| asm-differ / m2c / decomp-permuter | git submodules (URLs in §4.6) | WSL2 (Linux) | 4 |
| binutils (mipsel) | apt binutils-mipsel-linux-gnu — as-built 2.42 (Phase 4; ≥2.38 → check-env WARN, verdict deferred to Phase 5), 2.35 known-good |
WSL2 (Linux) | 4 |
§1 The all-in-WSL environment (single ext4 clone)
┌─ WSL2 Ubuntu 24.04 (ext4) — everything lives here ──────────────────────────┐
│ │
│ ~/bfm-decomp ← THE clone (one git working tree, one remote) │
│ ├─ Ghidra 12.1 PUBLIC (JDK 21 Temurin, GUI via WSLg) │
│ │ ├─ ghidra_psx_ldr 2026.06.04 (PSX loader, PsyQ signatures, │
│ │ │ .gdt type archives) │
│ │ └─ GhidrAssistMCP v2.8.0 ── SSE 127.0.0.1:8080 (local to WSL) │
│ │ │
│ ├─ Ghidra project .gpr/.rep (under ~/bfm-decomp or ~/.ghidra) │
│ ├─ PCSX-Redux (Linux build: runtime oracle — debugger, Lua, │
│ │ web API RAM dumps, GDB server :3333) │
│ ├─ Claude Code ← .mcp.json → 127.0.0.1:8080/sse (same box, no boundary) │
│ │ │
│ ├─ .venv/ splat64[mips] etc. │
│ ├─ tools/maspsx, tools/asm-differ, tools/m2c, tools/decomp-permuter (subm.)│
│ ├─ tools/bin/gcc-2.7.2-psx, tools/bin/gcc-2.7.2-cdk │
│ │ (old-gcc 0.17, Linux x86-64 binaries) │
│ ├─ disks/ BIN/CUE dump, copied in ONCE (gitignored) │
│ └─ asm/ build/ expected/ (generated, never committed) │
│ │
│ apt: binutils/gcc-mipsel-linux-gnu, make, ninja, python3.12, JDK 21, ... │
└───────────────────────────────────┬─────────────────────────────────────────┘
│
└── git remote (push/pull off-box only) ──►
One repository clone on ext4 at ~/bfm-decomp holds everything — RE stack, build toolchain, runtime oracle, docs, and Claude Code's working directory. There is no Windows/WSL split: no second clone, no cross-OS working-tree edits, no /mnt build path. The git remote is used only to push/pull off the machine (backup, collaboration), never to synchronize two local trees. Rationale for keeping the clone on ext4 (not on a /mnt/* drvfs mount):
- 9P filesystem penalty. File access on
/mnt/*drvfs mounts goes through the Plan 9 protocol and is the slowest path WSL2 offers — Microsoft's own numbers put ext4 at 2–20× faster, and community benchmarks measured builds up to 375% faster after moving off/mnt. Everymake,git status, and splat extract pays the tax if the build tree sits on a mounted Windows drive. Keeping the clone on ext4 sidesteps this entirely. - inotify works on ext4. Linux file-watchers get no events for changes on
/mnt/*drvfs mounts (microsoft/WSL #4739, #5424). asm-differ watch mode fires reliably only when source and build outputs live on ext4 — which, all-in-WSL, they always do. - No ghost-file hazard. anthropics/claude-code #28015: Write/Edit on WSL2 drvfs mounts can hit a statx/9P cache-poisoning kernel bug producing "ghost files" (ENOENT for files that exist). Editing only on ext4 avoids the bug; Claude Code runs inside WSL and never touches a drvfs path.
One-shot file copies onto ext4 (e.g. the disc dump into disks/) from any source are fine — it is sustained/random IO and watchers on drvfs that break, not correctness. Ghidra project files (.gpr/.rep) live on ext4 alongside the clone (or under ~/.ghidra), consistent with Ghidra's local-drive-storage recommendation; their ~lock files behave correctly on a native Linux filesystem.
Git hygiene: the committed .gitattributes (* text=auto eol=lf + binary exclusions) keeps line endings consistent. On the Linux clone core.filemode true is fine (ext4) and core.autocrlf is false by default (LF-native), so no line-ending dance is needed.
§2 RE stack install (Phase 1, all in WSL)
Order matters: JDK → Ghidra → both extensions → import → MCP wiring.
§2.1 JDK 21 (Temurin)
sudo apt-get update && sudo apt-get install -y openjdk-21-jdk
Required by Ghidra 12.x. (The Temurin apt repo — https://packages.adoptium.net — is an equivalent alternative if you prefer Temurin specifically.) Verify: java -version reports 21.x, and ensure it is the JDK on PATH for the shell that launches Ghidra (ghidraRun needs JDK 21 visible).
§2.2 Ghidra 12.1 PUBLIC — pin EXACTLY
Download ghidra_12.1_PUBLIC_20260513.zip from
https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1_build — plain zip, no installer; the same cross-platform Java distribution on every OS. Unzip and launch the GUI under WSLg:
cd ~ && unzip ghidra_12.1_PUBLIC_20260513.zip
~/ghidra_12.1_PUBLIC/ghidraRun # WSLg supplies the X11/Wayland display
./ghidraRun needs JDK 21 on PATH (§2.1); the Ghidra GUI renders through WSLg with no extra X-server setup.
⚠️ Extension version-lock warning. Ghidra extensions are point-version-locked via
extension.properties. Both extension zips below are built against12.1_PUBLIC; installing them on 12.1.2 (current latest, 2026-06-05) may trip the version check (red "incompatible" flag — same failure mode as LaurieWired issue #83 on 11.4.1). Whether the 12.1 zips happen to load on 12.1.2 is UNVERIFIED — pin 12.1, and only move to a point release when both extensions ship matching assets or you rebuild them from source (gradle installExtensionwithGHIDRA_INSTALL_DIRset). The extension assets are platform-independent Java (ghidra_12.1_PUBLIC_..._GhidrAssistMCP.zip/..._ghidra_psx_ldr.zip— the "12.1" is the Ghidra version, not the OS), so the same asset filenames install on Linux Ghidra 12.1.
§2.3 GhidrAssistMCP v2.8.0
- Asset for our Ghidra:
ghidra_12.1_PUBLIC_20260530_GhidrAssistMCP.zipfromhttps://github.com/symgraph/GhidrAssistMCP/releases/tag/2.8.0(repo moved fromjtang613/GhidrAssistMCPtosymgraph/GhidrAssistMCP; GitHub redirects. The release carries TWO assets — take the12.1one, not12.0.) - Install: Ghidra → File → Install Extensions… →
+→ select zip → restart Ghidra. - Enable plugin: File → Configure → Configure Plugins → check
GhidrAssistMCP. - Control panel: Window → GhidrAssistMCP → set Host = localhost, Port = 8080. Server exposes
/sse(SSE),/message, and/mcp(streamable HTTP). No Python bridge process — Claude Code connects directly. - This is the server the psxrecomp proof-of-concept actually ran with Claude Code (their port was 7777, a user setting; we standardize on 8080 to match the committed
.mcp.json).
Fallbacks (documented, not installed):
| Server | Status | Why fallback only |
|---|---|---|
| bethington/ghidra-mcp v5.13.x | active, claims Ghidra 12.1, 249 tools | Unaudited, single-maintainer with day-apart releases, needs a Python bridge (plugin HTTP :8089), 249 tools swamp Claude's tool context. Consider only if a missing niche tool (mass create_enum, batch_decompile) becomes a bottleneck. |
| clearbluejar/pyghidra-mcp | active, pip/uvx, headless multi-binary | No struct-creation or apply-type-at-address tools — dead end as primary server for matching-decomp data work; fine for scripted headless batch passes. |
| LaurieWired/GhidraMCP 1.4 | unmaintained — DO NOT INSTALL | Caps at Ghidra 11.3.2 (issues #83/#131 open, no fix since 2025-06-23); ghidra_psx_ldr dropped 11.x. Older write-ups (including the 1379.tech psxrecomp blog post) credit it incorrectly. |
| ismaelcaraballo-afk/GhidraMCP-12 | stale one-off recompile | 12.0.1 only, no struct tools, not viable. |
§2.4 ghidra_psx_ldr release 2026.06.04
- Asset:
ghidra_12.1_PUBLIC_20260604_ghidra_psx_ldr.zip(~9.15 MB, PsyQ signatures bundled — no separate OBJ download needed) fromhttps://github.com/lab313ru/ghidra_psx_ldr/releases/tag/2026.06.04 - Install via the same File → Install Extensions… path → restart.
- Coexists with GhidrAssistMCP without conflicts (PSX loader/analyzer/SLEIGH vs HTTP-server plugin).
§2.5 PSX-EXE import flow (SLUS_007.26)
- Extract
SLUS_007.26from the disc image (LBA 24, 0x65000 bytes, track 1 is MODE2/2352) and import into Ghidra. The loader auto-selects "PSX Executables Loader", languagePSX:LE:32:default, and builds the full PS1 memory map: RAM around the image at 0x80000000, scratchpad 0x1F800000, all IO/DMA/timer/CD/GPU/SPU register blocks, and a synthetic GTEMAC segment at 0x20000000 automatically — do NOT run theCreateGteMacSegmentscript (that is only for migrating legacy non-PSX projects). - Run auto-analysis with the "PsyQ Signatures" analyzer enabled (auto-enabled for PSX-language programs). Analyzer options: "Only first match", "Minimal signature entropy" (default 3.0 — can skip tiny low-entropy library stubs), "PsyQ Version if not found" (manual override).
- Read the detected PsyQ version: Edit → Options for Program → Program Information → "PsyQ Version". Expected: 4.0 (our own EXE scan found 12 genuine
Psstamps: 9× 4.0, one 4.0.1x on libnum 16, one 4.2 on libnum 0, one 4.2.1x on libnum 12 — i.e. PsyQ 4.0 libs + 4.2 library updates; see §5.1). If detection errors with'psyq/xx' cannot be found, append.0to the version field. Record the detected value in the phase log. CONFIRMED 2026-06-13 (Phase 1): DetectPsyQ at headless import recordedPsyQ Version = 4.0.0on the extracted US EXE (resolves ledger #12). Import also reported: loaderPSX Executables Loader, languagePSX:LE:32:default, ImageBase80000000, address range1f800000–801fffff, 1726 functions, ~177 s analysis. (Note: a raw track-1 scan during research reported slightly different per-libnum details — raw 2352-byte-sector scans produce false positives; the extracted-EXE scan is the ground truth, and DetectPsyQ at import is the final word.) - One-time manual
.gdtattach (GUI only — no MCP tool opens archives): in the CodeBrowser Data Type Manager, attach the bundled PsyQ type archive for the detected version —psyq400.gdt(psyq420.gdtalso exists). This gives PsyQ struct/typedef types for retyping work. - Early MCP type-resolution test (run before any bulk typing): via MCP, run the
typestool withaction=setapplying a PsyQ type (e.g. apply a known PsyQ struct at some address) and confirm it resolves. RESOLVED 2026-06-13 (Phase 1) — answer: import the types into the program. The.gdtattach is doable headlessly (no GUI needed) viatools/ghidra_scripts/ImportPsyqGdt.java, which openspsyq400.gdtas aFileDataTypeManagerandresolve()s all its types into the program's DataTypeManager — this both copies the types in and registerspsyq400as a SourceArchive. After that, the MCPtypestool fully resolves them:types action=list filter=DRAWENVandtypes action=get name=DRAWENVreturn the complete struct (DRAWENV[/LIBGPU.H], 92 bytes, all fields incl.RECT/DR_ENV), andSVECTOR[/LIBGTE.H]. psyq400.gdt carries 2599 types (program DTM went 205 → 2609). Takeaway for the typing workflow: don't rely on a bare archive attachment — import (resolve) the PsyQ types into the program once, and all MCP type/struct tools see them. The bundled archives live at<ghidra>/Ghidra/Extensions/ghidra_psx_ldr/data/psyq*.gdt(psyq400.gdt for our SDK).
§2.6 .mcp.json wiring + verification
The committed repo-root .mcp.json (already present):
{
"mcpServers": {
"ghidra": {
"type": "sse",
"url": "http://127.0.0.1:8080/sse"
}
}
}
Single ghidra entry only — do NOT copy psxrecomp's duplicated ghidra + ghidra_psx pair (same URL twice = every tool duplicated in context).
Verify in Claude Code with /mcp: ~38–41 mcp__ghidra__* tools should appear. (CONFIRMED 2026-06-13: GhidrAssistMCP v2.8.0 reports 41 tools headless. Operational notes from Phase 1: tool responses come back as SSE frames (event: message / data: {json}) on the streamable /mcp endpoint; heavy tools like get_code run asynchronously — the tools/call returns a task_id and you must poll get_task_status for the result. get_code takes {function: "0x80018730", format: "decompiler|disassembly|pcode"}.) These are the v2.8.0 names — e.g. get_binary_info, get_code (format: disassembly|decompiler), disassemble_at, analyze_function, xrefs, get_functions (paginated), struct (actions: create/modify/merge/set_field/name_gap/auto_create/rename_field/field_xrefs), types (list/get_info/set/delete), variables (list/rename/set_type/set_prototype), rename_symbol, batch_rename, create_data_var, create_function, search_bytes, patch_bytes, assemble_code, export_program. NOT the pre-2.4.0 names (get_function_info, list_data) that appear in psxrecomp's PLAN.md and older write-ups — those were renamed in v2.4.0 (upstream commit aa3ffc7d, 2026-03-14).
Operational cautions:
- Tools operate on the program currently open in CodeBrowser and fail (sometimes silently) if none is open.
- Do not run Ghidra auto-analysis concurrently with MCP-driven writes (renames/comments) — both mutate the program DB and can stall each other.
§2.7 Headless mode (batch passes)
For unattended batch passes, GhidrAssistMCP runs headless (supported since v2.3.0):
<ghidra>/support/analyzeHeadless <projects-dir> McpHeadless \
-import <binary> \
-scriptPath "<extension-dir>/ghidra_scripts" \
-preScript GAMCPStartServerScript.java "host=127.0.0.1" "port=8080" "wait=true"
wait=trueis mandatory — without it the analyzeHeadless process exits right after the prescript instead of serving MCP clients.- On subsequent runs use
-process SLUS_007.26(not-import) to reuse the existing project. Pair with-noanalysisso it doesn't re-run analysis on every server start. - CONFIRMED 2026-06-13: this headless flow works end-to-end (server "started on port 8080 … with 41 tools";
get_binary_infoandget_codeoperate on the-processprogram). The headless server holds the project.replock while serving — stop it (cancel the analyzeHeadless process) before opening the same project in the GUI. - Extension install (headless-compatible): extract each extension zip into
<GHIDRA_INSTALL_DIR>/Ghidra/Extensions/(e.g.unzip ext.zip -d ~/ghidra_12.1_PUBLIC/Ghidra/Extensions/). Both GUI andanalyzeHeadlessthen load the extracted module dirs (no GUI "Install Extensions" step needed). Verified for GhidrAssistMCP + ghidra_psx_ldr on Ghidra 12.1. - Loader selection in headless:
-loader "PSX Executables Loader"is rejected (InvalidInputException: Invalid loader name specified) even though that is the loader's display name. Omit-loaderand let auto-detection pick — for a realPS-X EXEit correctly selects "PSX Executables Loader" over Raw Binary (log line:Using Loader: PSX Executables Loader).
§3 PCSX-Redux (Linux) — the runtime oracle
Role: Ghidra is the static oracle; PCSX-Redux is the runtime oracle. It proves overlay load addresses by live RAM comparison, validates decompressed blobs byte-for-byte against what the game actually loads, and drives the debug-menu/loader RE.
- Install: Linux AppImage from
https://distrib.app/pub/org/pcsx-redux/project/dev-linux-x64(no stable pin — record the build date). As-built (2026-06-13):tools/pcsx-redux/PCSX-Redux-HEAD-x86_64.AppImage(87.5 MB, ELF static-pie, BuildIDdb3b55ac…). distrib.app is a JS SPA — not curl-able headlessly (thedev-linux-x64URL returns only an HTML shell; the real file is numeric-ID-gated); download via a browser. Runs directly under WSLg (FUSE 3.14.0 present; no--appimage-extractneeded). Docs:https://pcsx-redux.consoledev.net. Ships OpenBIOS (boots without a retail BIOS dump). Launch with disc + auto-run:./tools/pcsx-redux/PCSX-Redux-HEAD-x86_64.AppImage -iso "<cue>" -run. - RAM dumps (primary use): enable the built-in web server, then
GET http://127.0.0.1:8081/api/v1/cpu/ram/rawreturns the full 2 MB RAM image — diff slices of it against our extractor's output to prove byte-identity. (Port 8081, resolved at Phase 3 install — ledger #4 — moved off 8080 to avoid the GhidrAssistMCP collision. Verified 2026-06-13: returns exactly 2,097,152 B; RAM[0x10000:] == extractedSLUS_007.26[0x800:] byte-for-byte.)✅ Port collision (RESOLVED 2026-06-13): PCSX-Redux's web server defaults to
localhost:8080— same as GhidrAssistMCP. GhidrAssistMCP stays on 8080 (.mcp.json); the Redux web server moves to 8081 via~/.config/pcsx-redux/pcsx.json→emulator.Debug.WebServer=true+emulator.Debug.WebServerPort=8081(GUI: Configuration → Emulation → Web Server). Editpcsx.jsononly while Redux is stopped (it rewrites the file on exit). - Lua scripting: LuaJIT 2.1.0-beta3 (Lua 5.2 compat), FFI direct memory access; console + editor under the Debug menu. Gotcha: the editor autosaves to
pcsx.luaand reloads it at startup — a crashing script wedges the emulator across restarts (deletepcsx.luato recover); long-running scripts must yield via coroutines (~15 ms/frame budget). - Debugger: fully featured MIPS debugger, VRAM/SPU viewers. GDB server on port 3333 (Configuration → Emulation → Enable GDB server); for Ghidra-attached debugging launch Redux with
-interpreter -debugger -gdb(dynarec breaks debugging) and connectgdb-multiarch -i mi2→target remote localhost:3333. - PSX RAM at 0x80000000 mirrors physical 0x0 (0x800425D0 ≡ 0x000425D0) — relevant when reading dump offsets.
§4 Build environment (Phase 4)
Everything below installs inside the same WSL2 Ubuntu 24.04 that already hosts Ghidra and Claude Code (§1). Where Phase 1 (§2) already set up the distro and JDK, this phase adds the build toolchain on top.
§4.1 WSL2 + Ubuntu 24.04 (already present from Phase 1)
The all-in-WSL architecture means WSL2 Ubuntu 24.04 is the single host for the whole project, so it exists before Phase 4 begins (it is the same environment §2 installed Ghidra into). Confirm it is the expected distro and version:
cat /etc/os-release # Ubuntu 24.04
uname -a # Linux kernel (WSL2)
whoami # the Linux username; ~ resolves to /home/<user>
All project paths are plain Linux paths under ~/bfm-decomp — there is no Windows distro name or wsl.exe --cd target to track.
§4.2 Networking: MCP is local
Under the all-in-WSL architecture there is no cross-OS networking. Ghidra/GhidrAssistMCP, PCSX-Redux, and Claude Code all run inside the same WSL2 instance, so the MCP endpoint is plain loopback: MCP is local to WSL at http://127.0.0.1:8080; no mirrored-mode .wslconfig, firewall rule, or host-IP discovery is needed. Smoke-test with Ghidra running: curl http://127.0.0.1:8080/ from any shell in the same WSL instance.
§4.3 The clone on ext4
cd ~ && git clone <remote-url> bfm-decomp
The single clone lives at ~/bfm-decomp (ext4). Builds, splat, asm-differ, Ghidra, and Claude Code all run here. In this clone: git config core.filemode true. TBD: the canonical remote URL (GitHub private repo planned; not created as of this writing).
§4.4 Copy the disc dump into the clone
One-shot copy onto ext4 is fine (and required once):
mkdir -p ~/bfm-decomp/disks
cp '<dump-source>/Brave Fencer Musashi (USA)/'*.bin \
'<dump-source>/Brave Fencer Musashi (USA)/'*.cue ~/bfm-decomp/disks/
<dump-source> is wherever the disc dump currently lives (e.g. a one-time download into ~/Downloads, or a one-shot copy from external media). disks/ is gitignored — no ROM-derived bytes ever reach the remote (rule H1).
Status (Phase 2, 2026-06-13): the disc was staged early — extraction needs it before Phase 4. Track 1 alone (it holds all 27 root files) was copied once from the /mnt/z dump to ext4 at disks/Brave Fencer Musashi (USA) (Track 1).bin (364,846,944 bytes). WSL extract_exe.py --bin "disks/…(Track 1).bin" --verify-disc PASSED — SHA1 b44f0f0a19936f23b26188b658e13201a6a9c211, CRC32 c238191b, both == redump — which closes the Phase-1 deferral (verify-disc had previously only run on Windows; PhaseEnd_Phase1 Deviations).
§4.5 apt packages
Adapted from sotn-decomp's tools/requirements-debian.txt (dropped Saturn/PSP-only items binutils-sh-elf, xfonts-utils; Rust/Go deferred until a duplicate-detector or asset tool needs them):
sudo apt-get update && sudo apt-get install -y \
bchunk binutils-mipsel-linux-gnu bsdmainutils clang-format coreutils curl \
gcc-mipsel-linux-gnu git libelf-dev make ninja-build p7zip-full \
python3-pip python3-venv unzip wget
⚠️ binutils regression check (mandatory before trusting builds): open-ribbon documents that
binutils-mipsel-linux-gnu >= 2.38generated broken binaries; 2.35 is the known-good reference. Ubuntu 24.04 ships newer binutils — VERIFY on 24.04: after Phase 5's first full build, if the SHA1 check mysteriously fails with correct-looking asm, suspect the assembler first (mipsel-linux-gnu-as --version), and pin/downgrade or build binutils 2.35 if confirmed. Record the verdict here.As-built (Phase 4, 2026-06-14, ledger #6): apt installed binutils-mipsel-linux-gnu 2.42 (as/ld/objcopy all 2.42; mipsel-gcc 12.4.0). 2.42 ≥ 2.38, so
make check-envemits a [WARN] (not FAIL) and the regression verdict is deferred to Phase 5's first full build exactly as above — no preemptive downgrade.
§4.6 Python venv + splat + submodules
cd ~/bfm-decomp
python3 -m venv .venv # Python >= 3.12 required (24.04 ships 3.12; older = f-string SyntaxError mid-build)
.venv/bin/pip install -U 'splat64[mips]>=0.41.0,<1.0.0'
The PyPI package is splat64 (not splat), and the [mips] extra is required for PSX (pulls spimdisasm/rabbitizer). Always invoke as .venv/bin/splat or .venv/bin/python3 -m splat — splat: command not found means you're outside the venv. Once Phase 5 builds green, freeze the exact working version in a committed tools/requirements-python.txt.
Submodules (add under tools/):
| Submodule | URL | Pin |
|---|---|---|
tools/maspsx |
https://github.com/mkst/maspsx.git |
commit 874855c53f65f8fa57447e1da6bde6236dbef9d5 (decomp.me's pin — keeps local results comparable to decomp.me scratches) |
tools/asm-differ |
https://github.com/simonlindholm/asm-differ.git |
pin current HEAD at adoption |
tools/m2c |
https://github.com/matt-kempster/m2c.git |
pin current HEAD at adoption |
tools/decomp-permuter |
https://github.com/simonlindholm/decomp-permuter |
sotn pins b44b0622269fb4bff29e79fbbad26b9f47beda79 — sane default |
Pin all four (sotn precedent: blindly updating submodules breaks tooling). Note: sotn's asm-differ --overlay flag is sotn-fork-specific, not upstream — for BFM overlay diffing use upstream's -o object mode or port their fork later.
As-built (Phase 4, 2026-06-14): .venv created (Python 3.12.3); installed splat64 0.41.0 (splat64[mips]) — deps spimdisasm 1.41.0, rabbitizer 1.16.2, PyYAML 6.0.3, colorama 0.4.6, intervaltree 3.1.0, tqdm 4.67.1; import splat OK. Submodule pins as adopted: maspsx 874855c5, decomp-permuter b44b0622 (both per the table); asm-differ 2ad4a4a4 and m2c 4266cc28 (each HEAD-at-adoption). Their pip deps are not installed yet (Phase 6, when first invoked); tools/requirements-python.txt is frozen only after Phase 5 is green.
§4.7 Vintage compilers (old-gcc 0.17)
Linux prebuilts from decompals/old-gcc, release 0.17 (32-bit i386 static — see the correction below):
mkdir -p ~/bfm-decomp/tools/bin && cd ~/bfm-decomp/tools/bin
wget https://github.com/decompals/old-gcc/releases/download/0.17/gcc-2.7.2-psx.tar.gz
wget https://github.com/decompals/old-gcc/releases/download/0.17/gcc-2.7.2-cdk.tar.gz
sha256sum gcc-2.7.2-*.tar.gz # record hashes in a committed tools/bin/*.sha256 on first download,
# then verify with `sha256sum --check` on every fresh setup (sotn pattern)
# The 0.17 tarballs are FLAT (no top-level dir) and SHARE filenames (cc1, cpp, gcc, ...)
# -> extract each into its OWN subdir, or the second clobbers the first (Phase-4 finding):
mkdir -p gcc-2.7.2-psx gcc-2.7.2-cdk
tar xzf gcc-2.7.2-psx.tar.gz -C gcc-2.7.2-psx
tar xzf gcc-2.7.2-cdk.tar.gz -C gcc-2.7.2-cdk
gcc-2.7.2-psx= community GCC 2.7.2 PSX build (primary candidate).gcc-2.7.2-cdk= cygnus-2.7.2-970404, the exact base of PsyQ 4.0/4.1's CC1PSX (added in old-gcc 0.14).- sha256 (RECORDED Phase 4, old-gcc 0.17, ledger #7):
gcc-2.7.2-psx.tar.gz=500a459b3485e885a8d302cac23c2a4632f3900e03a09153f6190699fd723571;gcc-2.7.2-cdk.tar.gz=42bb0df96db11a9b5d2e23d78bdc962791f40046280d3d360da93fe5eef6f0bb. Committed totools/bin/CHECKSUMS.sha256(gitignore exception!/tools/bin/*.sha256); re-verify withsha256sum --check tools/bin/CHECKSUMS.sha256. - CORRECTION (Phase 4): these are 32-bit i386 statically-linked ELF binaries (NOT x86-64 as previously written) — they run on x86-64 WSL2 via the kernel's IA-32 emulation (verified:
cc1smoke-compiles to MIPS asm and self-identifies asGNU C 2.7.2 [AL 1.1, MM 40] Sony Playstation). Still Linux-only — why the build side must be Linux/WSL2. As-built layout:tools/bin/gcc-2.7.2-psx/cc1+tools/bin/gcc-2.7.2-cdk/cc1(matches the §6.2 path).
§4.8 Optional: PsyQ 4.0/4.1 binaries for arbitration (via Wine)
For byte-exact arbitration when maspsx output is in doubt, the real PsyQ Win32 tools can be driven from WSL under Wine (sudo apt-get install -y wine):
https://github.com/mkst/esa/releases/download/psyq-binaries/psyq4.0.tar.gzhttps://github.com/mkst/esa/releases/download/psyq-binaries/psyq4.1.tar.gz(containCC1PSX.EXE,ASPSX.EXE,CCPSX.EXE,PSYLINK.EXE,PSYLIB.EXE; 1–2.3 MB each)- Their
.OBJoutput converts to ELF with psyq-obj-parser (part of pcsx-redux; prebuilt Linux binary:https://github.com/decompme/compilers/releases/download/compilers/psyq-obj-parser.tar.gz).
Keep these under tools/ on ext4 (not committed); they are a tie-breaker, not the daily pipeline. (decomp.me runs these same Win32 tools under Wine for its psyq presets — the precedent that this works headless.)
DEFERRED to Phase 6 (Drew decision, Phase 4): not staged in Phase 4 — fetched only if/when maspsx output is disputed during fingerprinting. Wine is not installed. The §4.8 "optional native PsyQ binaries" checkbox is consciously skipped for Phase 4.
§4.9 make check-env (Phase 4 exit milestone)
Phase 4's observable milestone: a check-env make target that asserts every §4 component (venv + splat import, cc1 binaries executable, maspsx present, mipsel-as/ld/objcopy on PATH, python >= 3.12) and exits 0 when invoked directly in the WSL clone (see §6.1).
As-built (Phase 4, 2026-06-14): the root Makefile implements check-env (.ONESHELL bash; default goal help). Beyond the components above it also asserts sha1(committed extracted/retail/SLUS_007.26) == EXPECTED_EXE_SHA1 (imported from tools/bfm_extract/extract_exe.py — fresh-clone-safe; the disc-walk --verify-disc needs the gitignored disks/ and is intentionally NOT in check-env) and WARNs on binutils ≥ 2.38. make check-env exits 0 (milestone met). extract/build/check/expected/clean exist as loud-failing Phase-5 stubs (names fixed per §6.3).
§5 Compiler candidate ladder (Phase 6 fingerprinting)
§5.1 The evidence
Locally verified on the extracted US EXE (DetectPsyQ-style masked-pattern scan): 13 pattern hits, of which 12 are genuine Ps library stamps — 9× PsyQ 4.0 (libnums 2, 3, 4, 6, 7, 8, 9, 17, 24), 1× 4.0.1x (libnum 16), 1× 4.2 (libnum 0), 1× 4.2.1x (libnum 12); the remaining hit (ver 0x0000 at vaddr 0x8005CD20) is a code false positive. A raw-track scan during research reported 16 hits with extra spurious 4.0 stamps — the extracted-EXE scan is ground truth, and ghidra_psx_ldr's detection at import is the final word (ledger #12). Library copyright string (c) 1993-1997 Sony corroborates the era. Conclusion: BFM links PsyQ 4.0 libraries with 4.2 library updates ⇒ the GCC 2.7.2/SN32-era toolchain — NOT sotn's GCC 2.6.3 (the starting-point doc's claim is corrected). PsyQ 4.2 was a library-only refresh: no 4.2 toolchain disc survives (absent from redump/arthus sets), so 4.2 stamps still mean the 4.0/4.1 toolchain.
Caveat: Ps stamps date the linked libraries, not the compiler that built game code — Square mixed cc1 builds within one EXE (see §5.5). The final triple is pinned only by Phase-6 fingerprinting.
§5.2 SDK → GCC → ASPSX mapping (verified from the actual binaries in mkst/esa psyq-binaries tarballs)
| PsyQ SDK | CC1PSX identifies as | ASPSX | old-gcc 0.17 artifact | maspsx flag |
|---|---|---|---|---|
| 3.3 | GNU C 2.6.0 [AL 1.1, MM 40] | 2.21 | gcc-2.6.0-psx |
--aspsx-version=2.21 |
| 3.5 | GNU C 2.6.0 (same binary) | 2.34 | gcc-2.6.0-psx |
--aspsx-version=2.34 |
| 3.6 | GNU C 2.7.2.SN.1 | 2.34 | gcc-2.7.2 (vanilla-ish) |
--aspsx-version=2.34 |
| 4.0 | GNU C 2.7.2.SN32.3.7.0002 | 2.56 | gcc-2.7.2-psx / gcc-2.7.2-cdk |
--aspsx-version=2.56 |
| 4.1 | cygnus-2.7.2-970404 SN32.3.7.0004 (SonyPSX) | 2.67 | gcc-2.7.2-cdk (exact base) |
--aspsx-version=2.67 |
| 4.2 | library-only release — no toolchain exists | n/a | use 4.0/4.1 row | use 4.0/4.1 row |
| 4.3 | GNU C 2.8.0 SN32 Build 4.0.0007 (community shorthand "2.8.1" is wrong — that's 4.4) | 2.77 | gcc-2.8.0-psx |
--aspsx-version=2.77 |
| 4.4 | GNU C 2.8.1 SN32 BUILD 4.0.0010 | 2.79 | gcc-2.8.1-psx |
--aspsx-version=2.79 |
| 4.5 | egcs-2.91.66 (egcs-1.1.2) | 2.81 | gcc-2.91.66-psx |
--aspsx-version=2.81 |
| 4.6 | GNU C 2.95.2 BUILD 4.0.0030 | 2.86 | gcc-2.95.2-psx |
--aspsx-version=2.86 |
⚠️
--aspsx-versionMUST always be passed explicitly. maspsx with no flag is not "latest behavior" — the dataclass defaults approximate ASPSX ~2.3x (expand_li=True,sltu_at=True,nop_mflo_mfhi=True). Behavior thresholds:expand_lioff ≥2.50;sltu_atoff ≥2.60;$gpsymbol+offset ≥2.70;$gpforla≥2.80. The observable 2.56 vs 2.67 tell: "$at forsltu < 0" present at 2.56, gone at 2.67, and %hi/%lo support arrives at 2.67 — decide on functions with unsigned comparisons / li/sltu idioms.
§5.3 -G0 vs -G8
Read gp_value from the SLUS_007.26 EXE header and check for $gp-relative loads in Ghidra before fixing the flag (TBD — not yet read). Precedent: FF7 used -G 0; Xenogears used -G8 for game code. maspsx forces -G0 to GNU as by default — non-zero $gp needs -G8 passed to maspsx and a look at --dont-force-G0.
§5.4 Candidate ladder (try in this order)
gcc-2.7.2-psxcc1 +--aspsx-version=2.56, flags-O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker(FF7 style; swap to -G8 if §5.3 says so).- Same cc1 +
--aspsx-version=2.67(PsyQ 4.1 assembler era). gcc-2.7.2-cdk(cygnus-2.7.2-970404, the exact CC1PSX 4.0/4.1 base) × 2.56, then × 2.67.- Real PsyQ 4.0/4.1
CC1PSX.EXE+ASPSX.EXE(under Wine, §4.8) + psyq-obj-parser — byte-exact arbitration when maspsx emulation is in question. - Only if diffs show GCC 2.8-style codegen:
gcc-2.8.0-psx+ 2.77 (unlikely — the JP master predates PsyQ 4.3-era adoption).
Library-code preset (for PsyQ SDK functions linked into the EXE): --aspsx-version=2.56 --expand-div, -O3 -G0 — the Xenogears precedent, consistent with our 4.0 lib stamps.
§5.5 Per-module mixing warning
Xenogears (the closest comparable: Square US, Oct 1998) mixes three cc1 builds in one EXE (gcc-2.7.2-psx, gcc-2.6.0-psx, gcc-2.7.2-cdk) with different maspsx flags per module. Expect per-module variation in BFM: fingerprint several modules independently (3–5 medium leaf functions each), and design the build config for per-file compiler/flag overrides from day one (sotn does this with //! comments in line 1–2 of a .c file; Xenogears with gears.toml presets).
Reference repos for build-config patterns: https://github.com/ladysilverberg/xenogears-decomp (gears.toml presets), https://github.com/Drahsid/ffvii (Makefile).
§6 Daily command crib
§6.1 Running builds (native, in the WSL clone)
Claude Code runs inside WSL, so builds are plain native commands — no wsl.exe wrapper, no cross-shell quoting:
cd ~/bfm-decomp
make -j$(nproc) build # exit status is the build result; nonzero = failed
- The shell exit code (
$?) carries the build result directly — no launcher layer to distinguish from a real failure. - Output is native UTF-8; logs capture/pipe cleanly with no encoding workaround.
- The clone always lives on ext4 under
~/bfm-decomp, so builds never accidentally touch a/mntdrvfs path.
§6.2 Canonical compile pipeline (one object)
mipsel-linux-gnu-cpp -lang-c -Iinclude -undef -Wall -fno-builtin \
-Dmips -D__GNUC__=2 -D__OPTIMIZE__ -Dpsx -D_PSYQ -D_MIPSEL -D_LANGUAGE_C src/foo.c \
| bin/gcc-2.7.2-psx/cc1 -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker \
| python3 tools/maspsx/maspsx.py --aspsx-version=2.56 \
| mipsel-linux-gnu-as -Iinclude -march=r3000 -mtune=r3000 -no-pad-sections -O1 -G0 -o build/foo.o
Modern cpp preprocesses → vintage cc1 compiles to asm → maspsx emulates ASPSX quirks → modern GNU as assembles. Then mipsel-linux-gnu-ld with the splat-generated linker script, objcopy -O binary to the PS-EXE, SHA1-compare. cc1 path/flags above reflect the §5.4 first candidate — the exact flag set is pinned only after Phase-6 fingerprinting (-funsigned-char, -fpeephole, etc. are decided then; the cpp defines list is the sotn convention, adjust as evidence dictates).
§6.3 Planned make targets (Phase 5 builds these; names fixed now)
| Target | Does |
|---|---|
make extract |
splat split per config/splat.us.*.yaml → asm/, linker scripts |
make build |
full pipeline → build/us/SLUS_007.26, auto-runs the SHA1 check |
make check |
standalone SHA1 manifest verification (byte-for-byte = the only "OK") |
make expected |
snapshot build/us → expected/build/us (asm-differ baseline) |
make check-env |
toolchain preflight, exit 0 = environment sane (§4.9) |
make clean |
mandatory after ANY config/ change, before re-extract |
§6.4 asm-differ + baseline discipline
.venv/bin/python3 tools/asm-differ/diff.py -mwo3 <function> # -m rebuild, -w watch, -o vs object, -3 three-way
- Watch mode works only with source and build outputs on ext4, modified from inside Linux (§1).
- Re-snapshot
expected/only on green: runmake expectedexclusively after a build whose check passed. A staleexpected/makes asm-differ silently diff against the wrong baseline — the classic "phantom regression/phantom match". - Diff score 0 = matched; anything else is not matched, no matter how close.
§6.5 decomp.me settings for BFM
- Platform: PlayStation; Compiler:
gcc2.7.2-psx(orgcc2.7.2-cdk) + maspsx family — these images bundle old-gcc cc1 + maspsx at the same pinned commit we use. - Starting flags:
-O2 -G0(adjust per §5). - Do NOT use the SOTN preset (
Castlevania: Symphony of the Night/gcc 2.6.3-psx/psyq_263_221) — wrong era, guaranteed near-miss diffs. - decomp.me's API is Cloudflare-challenged (403 to scripts) — scratch searches/uploads needing the API must be done manually in a browser.
§7 Session-start ritual
Order is load-bearing — MCP tools fail (sometimes silently) without an open program.
- Start Ghidra (in WSL, under WSLg):
~/ghidra_12.1_PUBLIC/ghidraRun. - Open the BFM project → open
SLUS_007.26in CodeBrowser. Confirm the GhidrAssistMCP control panel (Window → GhidrAssistMCP) shows the server up onlocalhost:8080. - Start/resume Claude Code in
~/bfm-decomp. Run/mcp— theghidraserver must be connected with ~38 tools. Then make one cheap verification call (e.g.get_binary_info) before any heavier work. No MCP round-trip = no RE work this session (rule G2). - Build preflight (when the session involves building, Phase 4+):
One tree to check — clean or intentionally dirty before starting.
cd ~/bfm-decomp git status --short && make check-env - If PCSX-Redux work is planned: launch it (under WSLg), confirm the web server port (§3) and that a RAM dump fetch returns 2 MB.
Shutdown note: Ghidra writes the program DB on save — save (or deliberately discard) before closing, and never kill Ghidra mid-MCP-write.
§8 Model strategy per phase
Principle: the oracles (SHA1 check, asm-differ, RAM-dump byte-compares) make correctness model-independent — a weaker model can't fake a match. Model tier therefore buys fewer dead ends in ambiguous work, not safer results. Spend the strongest available model where ambiguity is highest; let the oracle-protected grind run on cheaper tiers. (Precedent: psxrecomp's post-mortem — model capability was load-bearing exactly once, on the most ambiguous subsystem.)
| Phase | Reasoning demand | Recommended tier |
|---|---|---|
| 1 — Installs, EXE import | Mechanical | Standard (Opus-class) |
| 2 — Extraction pipeline | Well-specified coding vs byte-exact oracle | Standard |
| 3 — File-loader & overlay-map RE | Highest in project — raw MIPS reading, US address derivation, RAM-dump experiment design | Strongest available |
| 4 — WSL setup | Mechanical; order-flexible (nothing in 1–3 depends on it — schedule it when the strong-model window is closed or limits are exhausted) | Any |
| 5 — splat config + build skeleton | Iterative debugging, loud error signals | Standard; strongest if available |
| 6 — Compiler fingerprint + first matches | Second highest — ASPSX 2.56-vs-2.67 idiom discrimination is subtle. The fingerprint analysis is pure RE and can be front-run before Phase 4/5 exist if a strong-model window is closing | Strongest available |
| 7 — Matching at scale | Pattern grind against hard oracle | Standard; smaller tiers acceptable for bulk iteration (cost = wasted iterations, never wrong matches) |
Budget notes (Max 20x plan): long autonomous RE sessions are token-hungry; prefer single-agent flow with oracle checks for in-phase grind, reserving multi-agent fan-outs for verification moments. Window note (2026-06-10): Fable 5 access expires ~2026-06-22 — priority order for that window: Phases 1→2 fast, then maximum depth on Phase 3, then Phase 6 fingerprint analysis if time remains; defer Phase 4 past the window.
Disc provenance (redump — canonical integrity reference)
The SLUS_007.26 executable's own hash is not published anywhere (redump hashes disc tracks, not inner files), so dump authenticity is proven at the track level. tools/bfm_extract/extract_exe.py --verify-disc checks the Track-1 BIN against these:
| Track 1 (data) | Value |
|---|---|
| SHA1 | b44f0f0a19936f23b26188b658e13201a6a9c211 |
| CRC32 | c238191b |
| MD5 | 838e9ecf8b4e4810f3f82228b40e77d8 |
| EXE build date | 1998-08-25 |
A Track-1 match proves the dump is the canonical redump dump, which transitively validates the extracted EXE (our own SHA1 143dbb89f34491258bbc27810d0a12ec8b43a8dd, 413,696 bytes — stable, independently reproduced from raw sectors). Audio tracks 2–4 SHA1s are in the research archive if ever needed.
Known-unverified ledger (recheck and update in place)
| # | Item | Status |
|---|---|---|
| 1 | GhidrAssistMCP/psx_ldr 12.1 zips on Ghidra 12.1.2 | MOOT for us — pinned Ghidra 12.1 exactly; both extensions load fine at version=12.1 (confirmed 2026-06-13). The 12.1.2 question stays untested by design. |
| 2 | MCP types/struct resolution of attached-archive (.gdt) types |
RESOLVED 2026-06-13 — import (resolve()) the .gdt types into the program headlessly (tools/ghidra_scripts/ImportPsyqGdt.java); MCP types then resolves them fully (§2.5 step 5) |
| 3 | GhidrAssistMCP struct-tool ergonomics under matching-decomp load | UNPROVEN — psxrecomp never exercised heavy struct creation |
| 4 | PCSX-Redux web-server port config field (8080 collision) | RESOLVED 2026-06-13 — pcsx.json → emulator.Debug.WebServer=true + emulator.Debug.WebServerPort=8081; dump at GET http://127.0.0.1:8081/api/v1/cpu/ram/raw (verified 2 MB; EXE-in-RAM byte-match) |
| 5 | WSL distro is Ubuntu 24.04 (the single all-in-WSL host) | CONFIRMED 2026-06-14 (Phase 4) — /etc/os-release = Ubuntu 24.04.4 LTS (VERSION_ID 24.04) |
| 6 | binutils ≥2.38 regression on Ubuntu 24.04's shipped binutils | AS-BUILT 2.42 recorded (Phase 4); make check-env WARNs (not FAIL); empirical verdict still deferred to Phase 5's first full build (§4.5) |
| 7 | sha256 hashes of old-gcc 0.17 tarballs | RECORDED 2026-06-14 (Phase 4) — psx 500a459b…, cdk 42bb0df9… in tools/bin/CHECKSUMS.sha256 (§4.7) |
| 8 | gp_value in SLUS_007.26 header → -G0 vs -G8 |
TBD (§5.3) |
| 9 | ASPSX tier for game code: 2.56 vs 2.67 | OPEN — Phase 6 empirical (§5.2 tell) |
| 10 | Cross-OS networking | N/A under all-in-WSL — MCP is local loopback (§4.2); no mirrored mode, firewall rule, or host-IP discovery |
| 11 | Canonical git remote URL (off-box push/pull backup) | TBD (§4.3) |
| 12 | Per-libnum stamp detail (raw-track scan reported 16 hits vs 12 genuine in extracted EXE — extracted-EXE scan is ground truth, see §5.1) | RESOLVED 2026-06-13 — DetectPsyQ at headless import recorded PsyQ Version = 4.0.0 (§2.5 step 3) |
| 13 | Overlay load addresses (resident 0x800CDF58 / location 0x80128508, EXE ptr table ~0x62620) | JP-only — re-derive for US (owned by docs/memory-map.md) |
| 14 | Greenfield claim: decomp.me scratch search is script-blocked (Cloudflare) | TBD — one-time manual browser check for BFM scratches |