Matthew Stanley 77e66f9da3 phase1.0e-d: per-block guest cycle accounting in codegen
Codegen-side foundation for cycle-driven peripheral timing per
docs/CYCLE_TIMING_ARCH.md. Each emitted basic block now calls
psx_advance_cycles(N) where N is the conservative 1-cycle-per-MIPS-
instruction count for the block (sum from leader to next leader,
including delay slot).

Generic per-instruction emit, no card-specific paths. Macro-gated by
PSX_ENABLE_BLOCK_CYCLES so generated output is comparable on/off.

This slice does NOT wire psx_advance_cycles to peripheral advance.
Reason: sio_tick has a legacy IRQ-countdown body that decrements
unconditionally per call; calling it from psx_advance_cycles (which
fires per block, ~100M+ times/sec) drains that countdown in
microseconds and breaks pad timing. A future slice with a peripheral-
only sio_advance (no legacy side effect) will wire SIO. Card byte-15
abort persists for now — expected; the fix needs that peripheral wiring.

Files:
- recompiler/src/full_function_emitter.cpp:
  - precompute block_cycles map (instr count between leaders)
  - emit psx_advance_cycles(<N>u) under #ifdef PSX_ENABLE_BLOCK_CYCLES
    at every block leader, before psx_check_interrupts
  - emit extern decl in generated header
- generated/SCPH1001_full.c: regenerated; 12794 PSX_ENABLE_BLOCK_CYCLES
  guards, one per block leader
- runtime/CMakeLists.txt: PSX_ENABLE_BLOCK_CYCLES=1 added to both
  psx-runtime and psx-beetleoracle target_compile_definitions
- runtime/src/psx_cycles.c: psx_advance_cycles accumulates counter only;
  peripheral dispatch deferred to next slice (rationale in comment)

Validation against 5d4d4bd baseline:
- Build clean both targets
- t=20s tx_writes=928, t=30s tx_writes=5000 (matches baseline)
- pad arm audit 5056/5056 armed, no_ack=0
- 4x CROSS press all succeed
- card txn distribution unchanged: txns reach READ_DATA, "MC" bytes
  flow at txn 0 bytes 10-11 (0x4D 0x43)
- psx_cycle_count grows: 1.72B (t=20s), 2.33B (t=30s), 10.04B
  post-CROSS — proves codegen emit live and per-block accounting works
- Macro=0 build (verified before macro flip): output identical to
  baseline

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 20:03:07 -07:00

Architecture note: install-at-runtime code

PSXRecomp v4 is a static recompiler with a small interpreter for self-modifying / install-at-runtime RAM. The PS1 BIOS writes 4-instruction dispatch stubs into kernel RAM at runtime (e.g. RAM 0xCF0 for the SIO data-byte handler) and then transfers control to those addresses. A pure static recompiler can't see those instructions because they don't exist at compile time — only the program's intent to install them does.

The runtime detects writes into kernel-RAM code regions, marks the affected pages "dirty", and routes any dispatch into a dirty page through a tiny MIPS interpreter that runs the actual installed instructions on the CPU register state. Static-recompiled C continues to handle the rest of the BIOS and all game code.

This is not HLE. We do not synthesize what the handler "would have done". We execute the BIOS author's own instructions, exactly as written, just from a different code source (RAM-at-runtime instead of ROM-at-compile-time). Mature static recompilers (N64Recomp, mednafen-PSX's dynarec) handle this the same way. See docs/dynamic_handler_install.md for the detailed analysis (kernel exception handler at RAM 0xC80 falls through to the installed stub at RAM 0xCF0 → handler at 0x641C).

The interpreter is not a fallback for code the recompiler failed to translate. Code that the recompiler can statically see must be statically recompiled. The interpreter exists solely for code that did not exist at recompile time.


Current Milestone

The authoritative milestone document is FIRST_MILESTONE.md. The authoritative phase plan is PLAN.md, Phase 1 section.

  • Phase 1a — Boot Slice: COMPLETE (2026-04-06). Reset-vector slice 0xBFC00000..0xBFC00074, 30 instructions, terminating at j 0xBFC00150. generated/boot_slice.c, generated/boot_slice_manifest.json, and an empty generated/unsupported_ops.json are the proof artifacts. Do not redo. Do not extend.
  • Phase 1b — Instruction Coverage: NEXT. Every R3000A opcode the BIOS uses must have a real implementation in strict_translator, with generated/instruction_inventory.json and generated/instruction_coverage.json as proof.
  • Phase 1c — Function Discovery Pipeline: after 1b. Bounded walk from explicit seed file, producing function_manifest.json, function_edges.json, discovery_run.log.json.
  • Phase 1d — Indirect Control Flow Recording: after 1c. Records, does not resolve. Produces indirect_jumps.json and the Ghidra cross-reference.
  • Phase 1e — Relocation Planning (HARD GATE): after 1d. Produces relocation_proofs/, address_aliases.json, normalization_rule.md. Full BIOS recompilation is forbidden until 1e is complete.

See PLAN.md → "Phase 1 — Controlled Recompiler Bring-up" for required artifacts, failure conditions, and acceptance criteria per sub-phase.


Non-Goals (Current Phase)

The following are explicitly out of scope for the entire Phase 1 window. Touching any of them is architectural drift and is not permitted regardless of how convenient it would be:

  • No runtime. There is no runtime/ directory in v4 yet, and Phase 1 does not create one.
  • No MMIO. No psx_mmio_read_* / psx_mmio_write_* entry points. generated/cpu_state.h stays compile-only.
  • No GPU. No GP0, no GP1, no software rasterizer, no VRAM.
  • No interrupts. No I_STAT, no I_MASK, no COP0 IRQ delivery hooks. (mtc0/mfc0/rfe are translated as instructions in Phase 1b — they are not wired to any runtime delivery path.)
  • No DMA, no timers, no SIO, no memcard, no CD-ROM, no SPU.
  • No full BIOS recompilation. Bounded walks only until Phase 1e closes.
  • No game loading. No PS-X EXE ingestion. No ISO. No Tomba. The game side does not exist in v4 until Phase 5.
  • No BIOS interpreter. Not as a fallback, not as a "temporary" bridge, not for "code we couldn't recompile yet".
  • No HLE BIOS functions. No bios.c, no A0/B0/C0 reimplementation in C. The recompiled BIOS is the BIOS.

Definition of Done (Current Phase)

Phase 1a (already met):

  • generated/boot_slice.c compiles cleanly with -c.
  • generated/boot_slice_manifest.json is complete.
  • generated/unsupported_ops.json is empty.
  • Every instruction in the slice is accounted for in the manifest.
  • No TODO / FIXME / XXX in any generated file.

Phase 1 as a whole (the gate that unlocks Phase 2):

  • All Phase 1a artifacts above continue to regenerate identically.
  • generated/instruction_inventory.json and generated/instruction_coverage.json exist and show every BIOS-used opcode implemented in strict_translator.
  • generated/function_manifest.json, generated/function_edges.json, and generated/discovery_run.log.json exist from a normalized Phase 1c re-run, with no duplicate functions under any normalized address.
  • generated/indirect_jumps.json, generated/indirect_jump_classes.json, and generated/indirect_jump_ghidra_xref.json exist and cover every indirect jump site in every discovered function.
  • generated/relocation_proofs/ exists with one subdirectory per proven ROM→RAM copy operation, each containing both Ghidra and DuckStation evidence.
  • generated/address_aliases.json and generated/normalization_rule.md exist and are mechanically derived from relocation_proofs/.
  • generated/unsupported_ops.json is still empty.
  • Nothing under generated/ was hand-edited.

PSXRecomp v4

A static MIPS-to-C recompiler for the PlayStation BIOS, with PS1 games as a follow-on layer. The BIOS runs as native compiled C, not as interpreted MIPS bytes.

This is a clean-slate restart after three failed prior attempts (v1, v2, v3) all collapsed into emulator-with-HLE shape instead of true static recompilation. See PLAN.md for the full background, the architectural decision, the salvage list, the phase plan, and the open questions we want outside review on.

Layout

psxrecomp-v4/
├── PLAN.md              the document, hand to outside reviewers
├── CLAUDE.md            rules for in-session work (Architecture A locked in)
├── README.md            this file
├── bios/
│   └── SCPH1001.BIN     the recompilation target, 524288 bytes
├── duckstation/         modified DuckStation, used as runtime oracle
└── recompiler/          MIPS->C translator, salvaged from v3
    ├── src/
    ├── include/
    ├── lib/             rabbitizer, fmt, toml11
    └── CMakeLists.txt

The runtime is intentionally absent at this stage — it will be built up fresh in Phase 2 once the recompiler can ingest the BIOS ROM and emit SCPH1001_full.c.

S
Description
No description provided
Readme 165 MiB
Languages
C 59.7%
C++ 24.2%
Python 12.6%
CMake 1.5%
Shell 0.6%
Other 1.3%