Adds the cycle-paced sio_tick body, the sio_tick_quantum() wrapper, and
the pace_state TCP telemetry handler. Crucial change vs. failed 1.0c:
the dispatch-loop quantum tick is gated by g_sio_timing_active (volatile
int), so on the psx_check_interrupts hot path the cost is one load + one
branch when no SIO time is pending — not a full function call into the
cycle-paced loop.
In 1.0c-v2 the TX path is still synchronous and nothing arms shift_active
or pending_ack, so g_sio_timing_active stays 0. Phase 1.0d will set it
when arming the shifter and clear it when the shifter+buffer+ack are all
empty.
Files:
- sio.h: SIO_MODEL_CYCLE_PACED moved to header (default 1); declares
g_sio_timing_active (extern volatile int) and sio_tick_quantum()
- sio.c: defines g_sio_timing_active=0; adds cycle-paced helpers
(sio_consume_ack_event, sio_fire_ack_irq, sio_handle_shift_complete);
sio_tick gains a cycle-paced loop that runs only if shift_active or
pending_ack is set (currently never), then falls through to the legacy
IRQ countdown body which remains the live IRQ delivery path; adds
sio_get_pace_state(uint64_t out[16]) accessor
- interrupts.c: in psx_check_interrupts !in_exception block, calls
sio_tick_quantum only if g_sio_timing_active is set
- debug_server.c: new pace_state TCP handler exposing the snapshot
Validation against 64e0194 baseline:
- t=20s/30s TCP responsive; pad arm audit 2840/2840 no_ack=0
- 4x CROSS succeeds
- card txn distribution unchanged (10 txns reach bytes=15 ts=10)
- txn 0 still reads "MC" magic at bytes 10-11 (0x4D 0x43)
- pace_state: timing_active=0, all dynamic fields 0 throughout
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 atj 0xBFC00150.generated/boot_slice.c,generated/boot_slice_manifest.json, and an emptygenerated/unsupported_ops.jsonare 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, withgenerated/instruction_inventory.jsonandgenerated/instruction_coverage.jsonas 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.jsonand 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.hstays 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/rfeare 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.ccompiles cleanly with-c.generated/boot_slice_manifest.jsonis complete.generated/unsupported_ops.jsonis 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.jsonandgenerated/instruction_coverage.jsonexist and show every BIOS-used opcode implemented instrict_translator.generated/function_manifest.json,generated/function_edges.json, andgenerated/discovery_run.log.jsonexist from a normalized Phase 1c re-run, with no duplicate functions under any normalized address.generated/indirect_jumps.json,generated/indirect_jump_classes.json, andgenerated/indirect_jump_ghidra_xref.jsonexist 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.jsonandgenerated/normalization_rule.mdexist and are mechanically derived fromrelocation_proofs/.generated/unsupported_ops.jsonis 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.