Files
psxrecomp/CLAUDE.md
T

457 lines
19 KiB
Markdown
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.
# PSXRecomp v4 — Rules
This file is the constitution for v4. Read it at the start of every
session before doing any work.
---
## ⛔ RULE -1: BUILD THE FAITHFUL CORE — NO HACKS, NO PRESERVATION
This ecosystem is being BUILT, not maintained. There is nothing to preserve.
- The correct fix is ALWAYS the faithful, hardware-accurate, class-level core.
NEVER a surgical per-game patch, a symptom workaround, a `game.toml` hack
(e.g. `overlay_native_block`), or "make native agree with interp even if both
are fake."
- When a narrow patch and a general faithfulness fix both exist, the
faithfulness fix WINS — even if larger, even if it breaks other titles.
Other games were built on a faulty ecosystem and will be **regenerated**.
Backward-compatibility is NOT a constraint.
- The recurring failure mode (weeks lost) is veering into a quick hack that
doesn't even work and burns the session. Do not do this. If you catch
yourself proposing a "surgical"/"temporary"/"just for this title" fix — STOP
and build the real thing.
- PSX timing specifically: the faithful core = ONE shared per-instruction
cycle-cost function for BOTH backends (compiled + dirty-RAM interp), exact
block cycle totals (delay-slot ownership), timers derived on-demand from a
global guest-cycle counter, devices on scheduled event deadlines, every
basic-block leader re-enterable. Confer with ChatGPT via the Chrome MCP
browser (chatgpt.com, "PSX Static Recompiler Debug" chat), not the codex CLI.
This supersedes any pressure to ship fast. Completeness is absolute here.
**Scope (refined 2026-06-29 — faithfulness is the FOUNDATION, not the
destination).** The rule above governs FOUNDATION work — building the faithful
core — and forbids per-game patches / symptom-workarounds / `game.toml` hacks
*as a way to fake faithfulness*. It does NOT forbid them forever: once the
faithful core is proven, per-game shims/hacks are the legitimate tool of the
*enhancement* phase — accelerated load times (toward 0), widescreen, etc. (see
`docs/ENHANCEMENTS.md`). Likewise **LLE is the BASELINE, not an absolute**: a
faithful, Beetle-validated HLE *subsystem replacement* is permitted at a genuine
LLE landmine (non-determinism with no hardware analog, or profound performance
loss), never as the starting point — see §0's amendment below and
recomp-template `PRINCIPLES.md` → "LLE Is the Baseline; HLE Is a Subsystem
Replacement, Not a Starting Point".
**The authoritative game plan is `docs/internal/FAITHFUL_TIMING_PLAN.md` — READ IT EACH
SESSION** (north star, phased plan P1–P6, current status/log). Update its
Status/Log section every session. The full-coverage accuracy burndown across ALL
axes (semantics, cycle, IRQ, MMIO, peripherals, static-vs-dynamic, determinism)
lives in `docs/internal/ACCURACY_BURNDOWN.md` — every item must be cross-referenced against an
external comparative (psx-spx / in-tree Beetle source / DuckStation / HW test
ROMs), not asserted. Axis 5 (peripherals — esp. SIO/controller, the hybrid-pad
bug) is the suspected-weakest second front after the cycle axis.
---
## 0. The architecture is locked
v4 implements **Architecture A**: static MIPS-to-C recompilation of
`bios/SCPH1001.BIN`, producing native C that links into the runtime as
real compiled functions.
There is **no MIPS interpreter** in v4 for the BIOS path. Not as a
fallback. Not as a "temporary" measure. Not for "code we couldn't
recompile yet". If a BIOS function cannot be recompiled, the recompiler
is wrong and must be fixed. The interpreter does not exist. Do not
write one.
For the faithful reference, the BIOS remains recompiled code. The historical
no-HLE restriction below is superseded for deliberate replacements by the owner
HLE policy in this section. Historically there was **no HLE BIOS layer** in v4. No `bios.c` with case branches
intercepting A0/B0/C0 vectors. No C reimplementations of `OpenEvent` or
`StartCard` or `alloc_kernel_memory`. The BIOS IS the recompiled C
output of `SCPH1001.BIN`. If a BIOS routine misbehaves, the answer is
to fix the recompiler or fix the hardware simulation it touches via
MMIO — never to write a C "shim" that produces the answer the BIOS
would have produced.
There are **no stubs**. A function is either fully implemented or it
aborts with a fatal error. `return 0;`, `return 1;`, `cpu->v0 = 1;
return;` are all stubs. `// TODO`, `// FIXME`, `// for now` are all
stubs. Hand-delivering an event because the chain handler isn't
installed is a stub wearing a costume and is the worst kind because it
hides the missing integration.
**Owner HLE policy, 2026-10-06.** The older HLE amendments are superseded by
`F:/Projects/recomp-template/HLE.md` and [the build integration](docs/HLE_EXECUTION.md).
Keep LLE buildable and runnable as the correctness reference. Qualified HLE is
the default performance implementation, selected at build time. Preserve actual
caller inputs, outputs, required memory/register effects, continuation and
observable completion ordering. Batching, native/GPU algorithms, private host
state, caching and approximate internal timing are allowed. Small practically
unnoticeable differences are already authorized. HLE need not support every game,
dual-execute, retain a live LLE state or provide a runtime switch. Compare exact
outputs exactly, intentional approximations within their declared contract, and
measure useful performance. Missing effects, false success and material gameplay
changes remain defects. Faithful-core accuracy requirements apply to the LLE
reference, not to unobserved HLE internals.
---
## 1. The BIOS is recompilation target #1, the game is target #2
Phase 1-3 of docs/internal/PLAN.md exist to get the BIOS recompiled and booting on
its own. The BIOS must reach the Sony logo and the BIOS shell, running
entirely as native C, before any game work begins. There is no path
that loads a game EXE before the BIOS is fully working in v4. **Do not
load a game ISO. Do not load a game EXE.** Tomba does not exist in v4
until Phase 5.
If you find yourself needing to load a game to "test something",
whatever you're testing belongs to a phase that hasn't started yet.
---
## 2. Three sources of truth, in priority order
Truth comes from three sources, in this order:
1. **BIOS disassembly** at `docs/psx_bios_disasm.txt` for what the BIOS
code is supposed to do. Human-annotated pseudocode with named functions,
kernel data structures (IntRP, ExCB, TCB, TCBH), A0/B0/C0 tables, boot
sequence, and exception handler logic. Check this FIRST.
2. **Ghidra MCP** for what the raw bytes are at a given address (static
analysis of `SCPH1001.BIN` loaded at `0xBFC00000`). Use when the disasm
doesn't cover a function or you need exact instruction-level detail.
3. **Beetle PSX oracle** (embedded in `psx-beetleoracle.exe`) for what real
PS1 hardware does at runtime. Beetle PSX (mednafen-psx libretro core)
runs in-process with the native runtime, sharing the same debug server.
Oracle commands: `find_first_divergence`, `emu_read_ram`, `emu_sio_trace`,
`emu_trace_addr`, `emu_step`. Where Beetle has gaps, **build new tooling
and visibility** — don't guess, don't route around.
Use all three, never just one. Don't guess. Don't say "probably". If you
cannot answer a question from the disasm, Ghidra, or the Beetle oracle,
the answer is "I don't know yet" — not a confident guess.
---
## 3. No printf debugging. No log files. Ever.
If you need to inspect runtime state, **build a TCP debug server
command** for it. The v3 build accumulated 555 GB of `boot_trace*.log`
and `card_test*.log` files because previous sessions used `fprintf` for
"just this one thing". The rule is absolute: **no `fprintf(stderr, ...)`
in source code, ever, for any reason.**
When the v4 runtime is built (Phase 2+), it will have a TCP debug
server on a fresh port. All inspection goes through that.
---
## 4. Never modify generated code
The output of the recompiler — files in `recompiler/output/` or
`generated/SCPH1001_full.c` etc. — is a build artifact. If the
generated code is wrong, the fix is in the recompiler source
(`recompiler/src/code_generator.cpp` and friends), not in the
generated file.
This is the same rule as v3 had, and it stays.
---
## 5. Don't accept partial milestones
Phase completion requires the user-visible end state, not "I think it
should work now". Phase 3 is "Sony logo displays on screen". Not "the
recompiler emitted code that probably draws the logo". Not "the GPU
command stream looks right in the debug server". **The pixels appear
on screen, or the phase is not done.**
This was the v3 failure mode: declaring "memory card screen freeze
RESOLVED" when in fact the screen had been unlocked by hand-delivering
a fake event. The fake delivery was not progress, it was theater.
---
## 6. Session start checklist
At the start of every session, before any code change:
1. Read this file (CLAUDE.md).
2. Read docs/internal/PLAN.md to confirm what phase we are in and what the next
concrete milestone is.
3. Verify `docs/psx_bios_disasm.txt` exists (primary reference).
4. Verify Ghidra MCP is reachable. If not, stop and ask.
5. State out loud: "Architecture A is locked. No interpreter. No stubs.
LLE default + oracle; HLE only per the §0 amendments (opt-in tier,
LLE fallback, no fakes). BIOS first. Game never until Phase 5."
If any of these fail, do not proceed with the user's task — surface
the failure first.
---
## 7. Salvage from v3 — what's allowed and what's not
The recompiler in `recompiler/` was salvaged from v3 because the
core MIPS-to-C translator pieces (`basic_block.cpp`, `control_flow.cpp`,
`function_analysis.cpp`, `mips_decoder.cpp`, `code_generator.cpp`)
operate on raw MIPS bytes and have nothing wrong with them. They just
need a new entry point that ingests a flat ROM at `0xBFC00000` instead
of a `PS-X EXE`-headered file, plus extensions to `code_generator.cpp`
to handle COP0 kernel-mode instructions the BIOS uses.
**The runner from v3 was not salvaged.** Specifically:
- `bios.c` (1808 LOC HLE shims) — discarded
- `interpreter.c` (919 LOC MIPS interpreter) — discarded
- `events.c`, `threads.c` — discarded (recompiled BIOS manages its own EvCB/TCB)
- `bios_trace.c`, `func_logger.c` — discarded (interpreter-era helpers)
- `main_runner.cpp` — discarded (drove the interpreter)
The hardware simulation files from v3 (`memory.c`, `gpu.c`,
`gpu_sw_renderer.c`, `dma.c`, `interrupts.c`, `timers.c`, `sio.c`,
`memcard.c`, `cdrom.c`, `iso_reader.cpp`, `gte.cpp`, `spu.c`,
`debug_server.c`) are **eligible for salvage in Phase 2** when v4
needs them, but they will be copied in **one at a time**, audited for
HLE-state-leakage and stub patterns first, and only the parts that are
hardware simulation (not BIOS state simulation) are kept.
**Do not bulk-copy `psxrecomp/runner/src/` from v3.** Doing so will
re-import the disease.
---
## 8. Reference the right project for examples
PSXRecomp v4 is a sibling project to:
- **N64Recomp** (RT64 team) — proven static recompilation model for N64
- **SuperMarioWorldRecomp** (`F:/Projects/SuperMarioWorldRecomp/`) — sibling SNES recomp
- **SuperMarioWorldRecomp-oracle** (`F:/Projects/SuperMarioWorldRecomp-oracle/`)
- **NESRecomp** — referenced in v3's debug_server.c comments
When you need to know "how does a recomp project handle X?", read those
projects. **Do not** look at v1 (`F:/Projects/psxrecomp/`) or v2
(`F:/Projects/psxrecomp-v2/`) or v3 (`F:/Projects/psxrecomp-projects-v3/`)
for architectural guidance. They are reference for what failed, not
what worked.
---
## 9. Memory and prior session context
Auto-memory continues to work across sessions. Existing v3-era memories
about printf rules, no-stubs, BIOS-first, DuckStation oracle, etc. all
still apply. New v4-specific memories should be tagged so future
sessions can tell them apart from v3 memories. The most important new
memory is: **"v3 failed because it was an interpreter+HLE emulator
masquerading as a recompiler. v4 fixes this by ACTUALLY recompiling
the BIOS."**
---
## 10. No speculative progress
If a step involves:
- indirect jumps
- relocation
- hardware interaction
You MUST produce:
- manifest
- proof artifact
Code without proof is invalid.
---
## 11. First milestone is absolute
Before any Phase 2 work:
- docs/internal/FIRST_MILESTONE.md must be complete
- boot_slice must compile
- all instructions must be supported
No exceptions.
---
## 12. Relocation is mandatory before full BIOS
Do NOT attempt full BIOS recompilation until:
- BOOT_RELOCATION_PLAN.md is implemented
- address_aliases.json exists
- duplicate code is impossible
---
## 13. No large-step execution
You may NOT:
- "recompile the full BIOS"
- "walk the entire ROM"
Until:
- function discovery pipeline exists
- manifest output is verified
---
## 14. Unknown is acceptable. Guessing is not.
If something is unknown:
→ STOP
→ produce artifact showing unknown
Do NOT guess behavior.
---
## 15. Broken tooling is never acceptable. Fix it when identified.
If a tool, command, or verification mechanism fails or returns
unexpected results:
→ **Fix the tool, immediately, the moment you identify the breakage.**
Diagnose why it failed and repair it before continuing the
investigation that surfaced it.
→ Do NOT route around it with indirect evidence.
→ Do NOT infer correctness from two broken implementations agreeing.
→ Do NOT log the breakage as a "caveat to live with" or carry it forward
in handoffs as a known limitation. A known-broken tool is a debt that
compounds: every later session pays interest in the form of
reconstructed-from-fragments evidence and shaky conclusions.
"The screenshot command returns black" is not a reason to skip visual
verification. It is a reason to fix the screenshot command.
"Both the native runtime and interpreter show the same wrong value"
does not make the value correct. It means both have the same bug.
"Beetle's fntrace caller_pc field is always 0, so we'll lean on ra
chains" is not an acceptable workaround. It is a reason to fix the
fntrace hook before the next investigation pass.
If you cannot fix the tool, **ask the user** what they observe.
Never declare a result "correct" without direct verification against
the oracle.
---
## 16. Two independent processes, identical debug harness
v4 runs two processes for cross-checking, NEVER one process with both
backends in it:
- **`psx-runtime.exe`** — recompiled BIOS only. SDL window, keyboard
input, TCP debug server on port **4370**.
- **`psx-beetle.exe`** — Beetle PSX (mednafen-psx libretro core) only.
SDL window, keyboard input, TCP debug server on port **4380**.
Both binaries expose the **same JSON wire protocol** for debug
commands — `read_ram`, `press`, `set_input`, `clear_input`,
`pad_status`, `wtrace_*`, `fntrace_*`, `screenshot`, `ping`, etc.
This means a tool written against psx-runtime works unchanged against
psx-beetle just by switching ports. Implementations differ (psx-beetle
uses libretro hooks; psx-runtime uses recomp-emitted instrumentation),
but the protocol is identical.
**Why two processes, not one:** the embedded-oracle approach
(`psx-beetleoracle.exe`, retired 2026-05-05) shared input across both
backends in lockstep. They desynced constantly — once their internal
state diverged, the same keypress drove them to different screens, and
"compare on the same press" became unreliable. Two independent
processes is the only setup where each backend can be navigated to its
own state and queried on its own timeline. Cross-process comparison
is done by querying both ports from a tool, NOT by sharing memory.
```bash
# Build beetle-psx static lib (one-time, or after modifying beetle-psx/ source)
cd beetle-psx && make platform=mingw_x86_64 STATIC_LINKING=1 HAVE_LIGHTREC=0 -j8
cp mednafen_psx_libretro.dll libmednafen_psx.a && cd ..
# Build both binaries
PATH=/c/msys64/mingw64/bin:$PATH
cd runtime/build && cmake --build . --target psx-runtime psx-beetle && cd ../..
# Run independently. Either can run alone; both can run together.
taskkill //F //IM psx-runtime.exe 2>/dev/null
taskkill //F //IM psx-beetle.exe 2>/dev/null
start "" "./runtime/build/psx-runtime.exe" "./bios/SCPH1001.BIN"
start "" "./runtime/build/psx-beetle.exe" "./bios/SCPH1001.BIN"
```
**Key files:**
- `beetle-psx/` — cloned beetle-psx-libretro repo
- `beetle-psx/libmednafen_psx.a` — static library used by psx-beetle
- `runtime/src/main.cpp` + `runtime/src/debug_server.c` — psx-runtime
- `runtime/src/beetle_main.cpp` + `runtime/src/beetle_debug_server.c`
+ `runtime/src/beetle_libretro.cpp` — psx-beetle
**Where Beetle has gaps, build new tooling.** Do not fall back to
DuckStation. Do not guess. Build the diagnostic tool that gives you
the visibility you need.
---
## 17. Phase 5 gate — fix hardware stubs before loading a game
`docs/internal/STUBS_TO_FIX.md` lists every known stub in the runtime. Before any
Phase 5 work (loading Tomba or any game EXE), every stub marked
"Phase 5+" in that file **must be implemented and oracle-verified**:
- **S3 — MDEC decoder** (FMV playback)
- **S4 — SPU audio synthesis** (sound output)
- **S5 — DMA channels 0/1/3/4** (MDEC, CDROM, SPU data pipes)
These cannot be tested until disc data flows, but they cannot be
skipped either. The first task of Phase 5 is to implement them, not
to load the game and see what breaks. Loading the game with known
stubs is how v3 ended up with 1808 lines of shims.
---
## 18. Self-modifying / install-at-runtime RAM is interpreted, not HLE'd
The PSX BIOS dynamically writes 4-instruction dispatch stubs into kernel
RAM (e.g. RAM 0xCF0 for the SIO data-byte handler) and then transfers
control to those addresses. The static recompiler CANNOT see those
instructions, because they don't exist at compile time — only the
program's intent to install them does.
**The correct answer is to interpret, not to HLE.** A small MIPS
interpreter in the runtime tracks writes into the kernel-RAM code region,
marks affected pages "dirty", and runs any `psx_dispatch` whose target
falls in a dirty page through the interpreter. The interpreter executes
the program's own instructions on the CPU register state. After the basic
block, control returns to static-recompiled C.
**This is not HLE.** HLE means "the program would have produced result X,
so we synthesize X ourselves and skip the program's code." This rule is
the opposite: we run the program's code, exactly as the BIOS author wrote
it. The only difference from a pure static recompile is the *source* of
the instructions (RAM-written-at-runtime vs ROM-at-compile-time).
**This rule does NOT relax Rule 0.** The interpreter is not a fallback for
code the recompiler failed to translate. If a function exists in ROM at
recompile time, it MUST be statically recompiled. The interpreter only
runs against PCs in pages that have been written to since boot — i.e.,
code that was put there at runtime by the program.
Mature static-recompilation projects (N64Recomp, mednafen-PSX's dynarec)
all handle install-at-runtime code this way. PSXRecomp v4 follows suit.
Implementation lives in `runtime/src/dirty_ram_interp.c` (or similar). It
is intentionally small (~300 LOC), modular, and isolated. It does NOT
expand into a general-purpose CPU emulator.