Files
psxrecomp/docs/dynamic_handler_install.md
T
Matthew Stanley 5b3c7dd08d phase4: freeze_check diagnostic + accumulated overnight infra
Today's session work
- runtime/src/debug_server.c: freeze_check TCP cmd snapshots all liveness
  counters (PC, exception entries, exception_reentry_blocks, dirty_ram
  blocks/insns, fn_entry total + top-K histogram, SIO IRQ pending +
  countdown, sio_stat/i_stat/i_mask, mc_max_state) for tight-loop
  diagnosis when the runtime hangs
- runtime/src/debug_server.c: wtrace slot 12 covering [0x72F0..0x72F4]
  (data-byte counter); measured peak=1 confirming setup_data_phase resets
  the counter mid-cycle
- runtime/src/interrupts.c: exception_entries_total + exception_reentry_blocks
  counters + psx_get_freeze_diag accessor
- runtime/src/sio.c: sio_get_freeze_diag accessor for IRQ pacing internals
- memory entries: phase4_card_gate_setup_writes_2026_04_28 (refutes the
  prior \"missing [0x7568+slot] clearer\" framing — setup functions write
  LSB=0 directly), phase4_72f0_one_byte_per_vblank_2026_04_28 (peak=1
  measurement)

Overnight session work bundled in
- runtime/src/dirty_ram_interp.{c,h}: small MIPS interpreter for
  install-at-runtime kernel RAM stubs (Rule 18; e.g., SIO data-byte
  handler the BIOS writes to RAM 0xCF0)
- recompiler/src/full_function_emitter.{cpp,h}: 3057 jal/jalr continuation
  return labels added to dispatch table (continuation fix); branch
  fall-through fix for conditional-branch delay slots
- recompiler/src/strict_translator.cpp: matching translator updates
- generated/: regenerated SCPH1001 dispatch + full C output reflecting
  the recompiler changes
- runtime/include/debug_server.h, src/memory.c: card_txn ring + SIO IRQ
  ring + EvCB walk ring + per-PC dirty_ram counters + always-on wtrace
  coverage for [0x755A], [0x7514], [0x7528], [0x7520], [0x74A4],
  [0x75C0..C8], [0x74B8..C0]
- recompiler/tests/, runtime/tests/: SIO/card protocol unit tests
- tools/: 60+ Python diagnostic scripts (chain_trace analysis, SIO trace
  diff, EvCB walker, wtrace dumpers, etc.)
- docs/: psx_bios_disasm reference + dynamic_handler_install rationale
- OVERNIGHT_2026_04_28.md, SUMMARY_20260412_2.md: session logs

Phase 4 status (still blocked, root cause better understood)
- BIOS card chain runs through R-handlers correctly (chain_trace shows
  R1..R11 cycling 26x); shell IS retriggering _card_read
- Install handler at ROM 0xBFC15F1C increments [0x72F0] correctly per
  SIO IRQ; gate at <0x80 / <0x7F is correct
- Bottleneck: SIO IRQ countdown is access-paced, fires ~1× per VBlank
  vs ~128× needed; setup_data_phase (called from outer coord on VBlank)
  zeroes [0x72F0] before counter reaches 128
- Both attempted fixes today caused freezes:
  * VBlank deferral on [0x75C0]==1 — pending VBlank already in i_stat
    delivered anyway; tighter dispatch gate also locked up
  * Cycle-paced sio_tick at rates 50, 200 (and gated on [0x75C0]==1) —
    runtime freezes once data phase activates, root cause unidentified
- delay=5 narrows the threshold (peak=2 vs peak=1, no freeze) but
  doesn't get to 0x80
- Real fix likely requires deeper instrumentation of the freeze (call
  stack / dispatch state at the moment of TCP starvation)

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

146 lines
4.8 KiB
Markdown

# Dynamic handler install — RAM 0xCF0 (and friends)
This document describes a class of BIOS behavior that requires special
runtime handling: **the BIOS writes MIPS instructions into kernel RAM
at runtime and then transfers control to those addresses.** A pure
static recompiler cannot follow this — only the runtime-installed
instructions do the dispatch — so PSXRecomp v4 maintains a small MIPS
interpreter for "dirty" RAM pages.
## The concrete case: SIO data-byte handler
When a memcard read command is in flight, the BIOS needs to read 128
bytes of sector data byte-by-byte over SIO. It does this not via the
chain dispatcher (which only has 13 handler slots), but via a stub
installed at RAM 0xCF0 that runs once per SIO IRQ.
### Install path
The function at ROM 0xBFC15EBC is the install entry. It:
1. Clears `mem[0x75c0] = 0` (the "data-mode active" flag).
2. Copies four words from RAM 0x6408 (= ROM 0xBFC15F08) to
RAM 0xCF0..0xCFC. Those four words are:
```
lui v0, 0x0
addiu v0, v0, 0x641C
jalr v0
nop
```
At runtime this resolves to `v0 = 0x641C` (the data-byte handler in
RAM, which is ROM 0xBFC15F1C reached via the kernel's part-2 RAM
image), then `jalr v0`. The `nop` is the delay slot.
3. Tail-jumps to a B0 vector trampoline at RAM 0x6A70.
The companion entry at ROM 0xBFC15E80 is the "set [0x75c0] = 1"
function (B0:0x80), which R11 calls on success at the end of the
read-protocol setup phase.
### Dispatch path
When an SIO IRQ fires, the kernel exception handler at RAM 0xC80
(= ROM 0xBFC10780) runs. It saves registers, calls a helper at
RAM 0xEA0, masks the cause register, and (for non-syscall causes)
falls through to:
```
RAM 0xCEC: sw v1, 0x80(k0) ; save scratch
RAM 0xCF0: lui v0, 0x0 ; ←—— installed stub starts here
RAM 0xCF4: addiu v0, v0, 0x641C
RAM 0xCF8: jalr v0
RAM 0xCFC: nop
```
The installed stub `jalr`s to RAM 0x641C (= ROM 0xBFC15F1C), the
data-byte handler. That handler:
1. Reads `[0x75c0]`. If 0, returns immediately.
2. Reads SIO_DATA (the data byte from the card).
3. Stores it at `*mem[0x75c4]` (the running buffer pointer).
4. Increments `mem[0x75c4]`.
5. Acks SIO IRQ.
6. Returns.
After 128 such invocations, the BIOS clears `[0x75c0]` and the chain
dispatcher resumes for R12 (checksum) and R13 (end-byte 'G').
## Why static recompilation can't follow this
`generated/SCPH1001_full.c` was emitted from the ROM image. At ROM
addresses 0xBFC107F0..0xBFC107FC the bytes are NOPs — that's all the
recompiler sees. It emits:
```c
/* 0xBFC107F0: 00000000 nop */
/* nop */
/* 0xBFC107F4: 00000000 nop */
/* nop */
...
```
The recompiler has no way to know that the BIOS overwrites these
runtime addresses with a working dispatch stub. Even if it did, it
couldn't statically know the destination — `addiu v0, v0, 0x641C` is
a runtime computation against `lui v0, 0x0`, which by itself is
underspecified.
## The interpreter (runtime fix)
### Dirty-page tracking
The runtime maintains a bitmap covering all writable RAM pages
(typically 64 KB at 4 KB granularity = 16 bits). Any write into the
kernel code region (RAM 0x000..0xFFFF) sets the bit for that page.
### Dispatch interception
`psx_dispatch(addr)` checks the dirty bit for `addr`. If clean,
calls the static-recompiled function as before. If dirty, hands
control to the interpreter.
### The interpreter
A small MIPS interpreter (~300 LOC) executes the basic block at
`addr` against `cpu->gpr[]`, `cpu->cop0[]`, etc. It supports the
small subset of instructions that BIOS install stubs actually use
(arithmetic, loads/stores, branches, jr/jalr). Unsupported
instructions abort fatally — they should never appear in install
stubs.
After the interpreter executes the block (terminating at jr/j/jalr),
control returns to `psx_dispatch` for the next basic block. The
combined effect is that BIOS-installed code at RAM 0xCF0 *runs*,
correctly, on the same CPU register state as static-recompiled C.
## Discoverability
Other install slots may exist. The tracker should detect them
mechanically by:
- Logging every write into `RAM 0x000..0xFFFF` (kernel code area).
- Reporting any PC dispatch that hits a written page.
Known/suspected slots to verify after the SIO data handler works:
- VBlank pad poll fast-path (?)
- Memcard slot-1 detection
- CD-ROM ready handler
- Anything in `RAM 0xC80..0xFFF` (kernel hook region)
## NOT HLE
This mechanism is explicitly *not* high-level emulation:
- We do not synthesize what the handler "would have produced".
- We do not intercept events, mark them delivered, or short-circuit
any BIOS state machine.
- We do not write C reimplementations of `_card_read`, `OpenEvent`,
or anything else.
We let the BIOS run its own code. The only thing we add is the
ability for that code to live in dirty RAM rather than ROM. That is
a property of the program; we accommodate it. See CLAUDE.md Rule 18.