Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_rt/
interrupts.rs

1//! Minimal interrupt support used by engine-level clocks.
2//!
3//! The first consumer is a monotonic VBlank counter. We install a
4//! tiny exception-vector trampoline that handles VBlank IRQs itself,
5//! increments a volatile counter, acknowledges the IRQ, then returns
6//! with `rfe`. The handler deliberately uses only the MIPS kernel
7//! registers `$k0/$k1`, so it does not need a stack frame.
8
9#[cfg(target_arch = "mips")]
10use psx_hw::{cop0, dma, gpu, gte, memory};
11#[cfg(target_arch = "mips")]
12use psx_io::irq;
13
14#[cfg(target_arch = "mips")]
15core::arch::global_asm!(
16    r#"
17    .set noreorder
18    .section .text.psx_rt_exception
19    .globl __psx_rt_exception_handler
20__psx_rt_exception_handler:
21    lui   $26, {io_hi}
22    lw    $27, {i_stat}($26)
23    lw    $26, {i_mask}($26)
24    nop
25    and   $27, $27, $26
26    andi  $27, $27, {vblank_mask}
27    beqz  $27, 9f
28    nop
29
30    lui   $26, %hi(__psx_rt_vblank_count)
31    lw    $27, %lo(__psx_rt_vblank_count)($26)
32    nop
33    addiu $27, $27, 1
34    sw    $27, %lo(__psx_rt_vblank_count)($26)
35
36    # Present queue (`crate::present`, `present-queue` feature only). One
37    # published chain waits in __psx_rt_present_head (0 = empty). Kick it on
38    # the first edge where the previous chain's closing GP0(1Fh) has run
39    # (GPUSTAT bit 24) and DMA channel 2 is idle: flip to the finished frame,
40    # acknowledge the flag with GP1(02h) (the new chain raises it again at its
41    # own end), point GP0 DMA at the CPU with GP1(04h)=2, enable channel 2 in
42    # DPCR and start the linked-list walk. Ported from quake-psx 3ca915c.
43    # With the slot empty this costs one load.
44    .if {present}
45    lui   $26, %hi(__psx_rt_present_head)
46    lw    $27, %lo(__psx_rt_present_head)($26)
47    nop
48    beqz  $27, 8f
49    nop
50    lui   $26, {io_hi}
51    lw    $27, {gp1}($26)
52    nop
53    srl   $27, $27, {gpustat_irq1_shift}
54    andi  $27, $27, 1
55    beqz  $27, 7f
56    nop
57    lw    $27, {gpu_chcr}($26)
58    nop
59    srl   $27, $27, {chcr_start_shift}
60    andi  $27, $27, 1
61    bnez  $27, 7f
62    nop
63    lui   $27, %hi(__psx_rt_present_display)
64    lw    $27, %lo(__psx_rt_present_display)($27)
65    nop
66    beqz  $27, 6f
67    nop
68    sw    $27, {gp1}($26)
696:
70    lui   $27, {ack_irq_hi}
71    sw    $27, {gp1}($26)
72    lui   $27, {dma_dir_hi}
73    ori   $27, $27, {dma_dir_lo}
74    sw    $27, {gp1}($26)
75    lw    $27, {dpcr}($26)
76    nop
77    ori   $27, $27, {gpu_dpcr_enable}
78    sw    $27, {dpcr}($26)
79    lui   $27, %hi(__psx_rt_present_head)
80    lw    $27, %lo(__psx_rt_present_head)($27)
81    nop
82    sw    $27, {gpu_madr}($26)
83    sw    $zero, {gpu_bcr}($26)
84    lui   $27, {chcr_hi}
85    ori   $27, $27, {chcr_lo}
86    sw    $27, {gpu_chcr}($26)
87    lui   $26, %hi(__psx_rt_present_head)
88    sw    $zero, %lo(__psx_rt_present_head)($26)
89    lui   $26, %hi(__psx_rt_present_kick_count)
90    lw    $27, %lo(__psx_rt_present_kick_count)($26)
91    nop
92    addiu $27, $27, 1
93    b     8f
94    sw    $27, %lo(__psx_rt_present_kick_count)($26)
957:
96    lui   $26, %hi(__psx_rt_present_skip_count)
97    lw    $27, %lo(__psx_rt_present_skip_count)($26)
98    nop
99    addiu $27, $27, 1
100    sw    $27, %lo(__psx_rt_present_skip_count)($26)
1018:
102    .endif
103
104    # Apply one queued GP1 word exactly at the blank edge (deferred display
105    # flip). Zero means no request; a display-start word is never zero.
106    # Only apply once GPUSTAT bit 24 is set: the frame's work ends with
107    # GP0(1Fh), which sets it when the GPU reaches it, i.e. when everything
108    # before it has been drawn, and the game acknowledged the flag with
109    # GP1(02h) when it kicked that work. Until then the word stays queued for
110    # a later edge, so a flip never exposes a partial buffer and the CPU never
111    # blocks on the GPU. Bit 28 is not a drawing-complete test: on silicon it
112    # rises when the DMA has pushed the last packet, about one large
113    # primitive before the drawing ends (hardware-tests v1.24 cases 219-226);
114    # the v1.24 present probe flipped on bit 24 with 120 of 120 frames right.
115    lui   $26, %hi(__psx_rt_pending_gp1)
116    lw    $27, %lo(__psx_rt_pending_gp1)($26)
117    nop
118    beqz  $27, 2f
119    nop
120    lui   $26, {io_hi}
121    lw    $26, {gp1}($26)
122    nop
123    srl   $26, $26, {gpustat_irq1_shift}
124    andi  $26, $26, 1
125    beqz  $26, 2f
126    nop
127    lui   $26, %hi(__psx_rt_pending_gp1)
128    sw    $zero, %lo(__psx_rt_pending_gp1)($26)
129    lui   $26, {io_hi}
130    sw    $27, {gp1}($26)
131
1322:
133    lui   $26, {io_hi}
134    addiu $27, $zero, {ack_vblank}
135    sw    $27, {i_stat}($26)
136
1371:
138    mfc0  $27, $13
139    nop
140    andi  $27, $27, 0x007c
141    beqz  $27, 2f
142    nop
143
144    # Preserve a compact unexpected-exception signature before deciding
145    # whether this fault is recoverable. Immediate-abort panics compile to
146    # BREAK, so Cause + EPC are the only reliable shipping diagnostics.
147    lui   $26, %hi(__psx_rt_fault_cause)
148    mfc0  $27, $13
149    nop
150    sw    $27, %lo(__psx_rt_fault_cause)($26)
151    lui   $26, %hi(__psx_rt_fault_epc)
152    mfc0  $27, $14
153    nop
154    sw    $27, %lo(__psx_rt_fault_epc)($26)
155    lui   $26, %hi(__psx_rt_fault_badvaddr)
156    mfc0  $27, $8
157    nop
158    sw    $27, %lo(__psx_rt_fault_badvaddr)($26)
159
160    lui   $26, %hi(__psx_rt_fault_count)
161    lw    $27, %lo(__psx_rt_fault_count)($26)
162    nop
163    addiu $27, $27, 1
164    sw    $27, %lo(__psx_rt_fault_count)($26)
165
166    # Never advance EPC for fatal instruction-side faults. BREAK (ExcCode 9,
167    # 0x24) marks panic=immediate-abort; IBE (ExcCode 6, 0x18) cannot become
168    # executable by skipping one word. For AdEL (ExcCode 4, 0x10), halt only
169    # when BadVAddr == EPC, which identifies a misaligned instruction fetch;
170    # retain the historical skip-and-count policy for data-side AdEL.
171    mfc0  $27, $13
172    nop
173    andi  $27, $27, 0x007c
174    addiu $26, $zero, 0x0024
175    beq   $27, $26, 3f
176    nop
177    addiu $26, $zero, 0x0018
178    beq   $27, $26, 3f
179    nop
180    addiu $26, $zero, 0x0010
181    bne   $27, $26, 4f
182    nop
183    mfc0  $26, $8
184    mfc0  $27, $14
185    nop
186    beq   $26, $27, 3f
187    nop
188
189    # Step over the faulting instruction: resume at EPC + 4. Not with
190    # Cause.BD set: then it sat in the delay slot of the branch at EPC, and
191    # EPC + 4 would re-run it as if the branch had fallen through. Resuming
192    # correctly means evaluating that branch, which costs every game code
193    # for a path only a bug reaches, so halt on it like a fatal fault, with
194    # Cause, EPC and BadVAddr recorded. With the `strict-faults` feature
195    # nothing is stepped over: every fault halts here. `fault_resume_pc`
196    # below is the same decision in Rust, unit-tested on host.
1974:
198    .if {strict}
199    b     3f
200    nop
201    .endif
202    mfc0  $27, $13
203    mfc0  $26, $14
204    nop
205    bltz  $27, 3f
206    nop
207    addiu $26, $26, 4
208    jr    $26
209    .word {rfe}
210
211    # Interrupt return: EPC, or EPC + 4 when the word at EPC is a GTE
212    # command (top seven bits 0100101). An interrupt taken on a GTE command
213    # lets the command run and still leaves EPC on it, so returning to EPC
214    # runs it a second time (hardware-tests v1.24 on silicon: case 0xC9,
215    # 38 of 38 such interrupts ran an RTPS twice; with this step, case 0xCB,
216    # 61 skips, none doubled or lost). This is psx-spx's fix and the Sony
217    # kernel's. With Cause.BD set EPC holds the branch, never a GTE command,
218    # so a GTE command in a delay slot is not stepped over and does run twice:
219    # psx-spx's answer is to keep GTE commands out of delay slots, and
220    # hazard-scan warns about any it finds. `interrupt_resume_pc`
221    # below is the same decision in Rust, unit-tested on host.
2222:
223    mfc0  $26, $14
224    nop
225    lw    $27, 0($26)
226    nop
227    srl   $27, $27, {gte_prefix_shift}
228    xori  $27, $27, {gte_prefix}
229    bnez  $27, 5f
230    nop
231    lui   $27, %hi(__psx_rt_gte_skip_count)
232    lw    $26, %lo(__psx_rt_gte_skip_count)($27)
233    nop
234    addiu $26, $26, 1
235    sw    $26, %lo(__psx_rt_gte_skip_count)($27)
236    mfc0  $26, $14
237    nop
238    addiu $26, $26, 4
2395:
240    jr    $26
241    .word {rfe}
2423:
243    b     3b
244    nop
245
246    # No VBlank pending. An interrupt (ExcCode 0) with an enabled source
247    # pending here is one nothing in this handler owns, and returning without
248    # acknowledging it re-enters at once, forever. Record it, acknowledge it
249    # (writing a 0 to an I_STAT bit clears it, a 1 leaves it) and count it,
250    # then go on to the common return. Not done when VBlank is pending too:
251    # a game's own handler chains here for VBlank, and a source it services
252    # itself must stay in I_STAT until it does. `stray_interrupt_sources`
253    # below is the same decision in Rust, unit-tested on host.
2549:
255    mfc0  $27, $13
256    nop
257    andi  $27, $27, 0x007c
258    bnez  $27, 1b
259    nop
260    lui   $26, {io_hi}
261    lw    $27, {i_stat}($26)
262    lw    $26, {i_mask}($26)
263    nop
264    and   $27, $27, $26
265    beqz  $27, 1b
266    nop
267    lui   $26, %hi(__psx_rt_stray_irq_last)
268    sw    $27, %lo(__psx_rt_stray_irq_last)($26)
269    nor   $27, $27, $zero
270    lui   $26, {io_hi}
271    sw    $27, {i_stat}($26)
272    lui   $26, %hi(__psx_rt_stray_irq_count)
273    lw    $27, %lo(__psx_rt_stray_irq_count)($26)
274    nop
275    addiu $27, $27, 1
276    b     1b
277    sw    $27, %lo(__psx_rt_stray_irq_count)($26)
278    .set reorder
279    "#,
280    strict = const STRICT_FAULTS as u32,
281    present = const PRESENT_QUEUE as u32,
282    // Every register the handler touches is in the I/O window, so one `lui`
283    // of the window's high half reaches them all and the offsets are the low
284    // halves (checked by the assertions below the block).
285    io_hi = const memory::io::BASE >> 16,
286    i_stat = const psx_hw::irq::I_STAT & 0xFFFF,
287    i_mask = const psx_hw::irq::I_MASK & 0xFFFF,
288    vblank_mask = const 1u32 << psx_hw::irq::source::VBLANK,
289    ack_vblank = const !(1u32 << psx_hw::irq::source::VBLANK) as i32,
290    gp1 = const gpu::GP1 & 0xFFFF,
291    gpustat_irq1_shift = const gpu::GpuStat::IRQ1.bits().trailing_zeros(),
292    ack_irq_hi = const gpu::gp1::ACK_IRQ >> 16,
293    dma_dir_hi = const gpu::gp1::dma_direction(gpu::DmaDirection::CpuToGp0 as u32) >> 16,
294    dma_dir_lo = const gpu::gp1::dma_direction(gpu::DmaDirection::CpuToGp0 as u32) & 0xFFFF,
295    gpu_madr = const (dma::CHANNEL_BASE + dma::CHANNEL_STRIDE * dma::channel::GPU + dma::MADR) & 0xFFFF,
296    gpu_bcr = const (dma::CHANNEL_BASE + dma::CHANNEL_STRIDE * dma::channel::GPU + dma::BCR) & 0xFFFF,
297    gpu_chcr = const (dma::CHANNEL_BASE + dma::CHANNEL_STRIDE * dma::channel::GPU + dma::CHCR) & 0xFFFF,
298    chcr_start_shift = const dma::CHCR_START.trailing_zeros(),
299    chcr_hi = const dma::CHCR_LINKED_LIST_TO_DEVICE >> 16,
300    chcr_lo = const dma::CHCR_LINKED_LIST_TO_DEVICE & 0xFFFF,
301    dpcr = const dma::DPCR & 0xFFFF,
302    gpu_dpcr_enable = const dma::dpcr_enable(dma::channel::GPU),
303    rfe = const cop0::RFE,
304    gte_prefix_shift = const gte::PREFIX_SHIFT,
305    gte_prefix = const gte::COMMAND >> gte::PREFIX_SHIFT,
306);
307// The handler reaches every register through one `lui` of the I/O window's
308// high half plus a signed 16-bit offset, and builds its GP1 and CHCR words
309// from immediates; check each the way the template spells them.
310#[cfg(target_arch = "mips")]
311const _: () = {
312    let window = memory::io::BASE >> 16;
313    assert!(psx_hw::irq::I_STAT >> 16 == window && psx_hw::irq::I_MASK >> 16 == window);
314    assert!(gpu::GP1 >> 16 == window && dma::DPCR >> 16 == window);
315    let gpu_block = dma::CHANNEL_BASE + dma::CHANNEL_STRIDE * dma::channel::GPU;
316    assert!(gpu_block >> 16 == window && (gpu_block + dma::CHCR) & 0xFFFF < 0x8000);
317    // `lui` of a GP1 word with an empty low half.
318    assert!(gpu::gp1::ACK_IRQ & 0xFFFF == 0);
319    // `ori` takes an unsigned 16-bit immediate.
320    assert!(dma::dpcr_enable(dma::channel::GPU) <= 0xFFFF);
321};
322
323/// True when psx-rt was built with the `present-queue` feature: the VBlank
324/// handler kicks frames published through [`crate::present`].
325pub const PRESENT_QUEUE: bool = cfg!(feature = "present-queue");
326
327/// Present-queue slot: the chain head the VBlank handler kicks at the next
328/// ready edge, 0 when empty. Owned by [`crate::present`].
329#[cfg(feature = "present-queue")]
330#[no_mangle]
331pub(crate) static mut __psx_rt_present_head: u32 = 0;
332
333/// GP1(05h) word the handler writes just before kicking the slot's chain;
334/// 0 = no flip.
335#[cfg(feature = "present-queue")]
336#[no_mangle]
337pub(crate) static mut __psx_rt_present_display: u32 = 0;
338
339/// Chains the handler has kicked.
340#[cfg(feature = "present-queue")]
341#[no_mangle]
342pub(crate) static mut __psx_rt_present_kick_count: u32 = 0;
343
344/// Edges on which a published chain found the previous one undrawn or DMA
345/// channel 2 still busy.
346#[cfg(feature = "present-queue")]
347#[no_mangle]
348pub(crate) static mut __psx_rt_present_skip_count: u32 = 0;
349
350/// True when psx-rt was built with the `strict-faults` feature: the
351/// exception handler halts on every fault instead of stepping over the
352/// faulting instruction (see [`fault_resume_pc`]).
353pub const STRICT_FAULTS: bool = cfg!(feature = "strict-faults");
354
355/// Monotonic VBlank IRQ count.
356#[no_mangle]
357static mut __psx_rt_vblank_count: u32 = 0;
358
359/// Count of exceptions that were NOT interrupts: bus errors, address
360/// errors, reserved instructions.
361///
362/// The handler used to return straight to EPC for these, which
363/// re-executes the faulting instruction and loops forever: a silent
364/// freeze with no diagnostic, indistinguishable from a hung spin. It now
365/// steps over the faulting instruction and counts it here, so a bad
366/// access costs one wrong value instead of the whole program, and the
367/// count says it happened. Fatal instruction-side faults are deliberately
368/// different: BREAK is the shipping representation of
369/// `panic=immediate-abort`, while IBE and instruction-side AdEL cannot be
370/// repaired by skipping one word. The handler records Cause/EPC and halts for
371/// all three instead of entering unreachable or non-executable code. It also
372/// halts on any fault in a branch delay slot, where skipping one word would
373/// drop the branch (see [`fault_resume_pc`]).
374///
375/// Stepping over hides bugs: a misaligned store that is skipped leaves the
376/// program running on a wrong value, and only this count says so (one such
377/// store in WipEout drew wrong geometry in one build and, landing in a delay
378/// slot, hung another). Build development and test discs with the
379/// `strict-faults` feature to halt on the first fault instead.
380#[no_mangle]
381static mut __psx_rt_fault_count: u32 = 0;
382
383/// Interrupts the handler returned from at EPC + 4 because the word at EPC
384/// was a GTE command (see [`interrupt_resume_pc`]).
385#[no_mangle]
386static mut __psx_rt_gte_skip_count: u32 = 0;
387
388/// Raw COP0 Cause captured for the latest unexpected exception.
389#[no_mangle]
390static mut __psx_rt_fault_cause: u32 = 0;
391
392/// COP0 EPC captured for the latest unexpected exception.
393#[no_mangle]
394static mut __psx_rt_fault_epc: u32 = 0;
395
396/// COP0 BadVAddr captured for the latest unexpected exception. Only
397/// meaningful for an address error; other faults leave a stale value in the
398/// register.
399#[no_mangle]
400static mut __psx_rt_fault_badvaddr: u32 = 0;
401
402/// One queued GP1 word the VBlank handler writes to the GPU at the first
403/// blank edge on which GPUSTAT bit 24 is set, then clears. Zero = empty.
404/// Written by [`queue_display_control_at_vblank`].
405#[no_mangle]
406static mut __psx_rt_pending_gp1: u32 = 0;
407
408/// Interrupts other than VBlank the handler acknowledged because nothing
409/// owned them (see [`stray_interrupt_sources`]).
410#[no_mangle]
411static mut __psx_rt_stray_irq_count: u32 = 0;
412
413/// The `I_STAT & I_MASK` sources of the latest stray interrupt.
414#[no_mangle]
415static mut __psx_rt_stray_irq_last: u32 = 0;
416
417/// Stray interrupts acknowledged so far. psx-rt's handler owns VBlank only;
418/// any other source enabled in `I_MASK` that fires while no VBlank is
419/// pending, and that no handler in front of this one claimed, is
420/// acknowledged and counted here instead of re-entering forever. Non-zero
421/// means something enabled a source nothing services.
422#[inline]
423pub fn stray_interrupt_count() -> u32 {
424    // SAFETY: a volatile aligned u32 read through a raw pointer, so no reference is formed. Only
425    // the asm exception handler writes it, and an aligned word load cannot tear.
426    unsafe { core::ptr::read_volatile(&raw const __psx_rt_stray_irq_count) }
427}
428
429/// The `I_STAT & I_MASK` bits of the latest stray interrupt
430/// ([`stray_interrupt_count`]); zero before the first.
431#[inline]
432pub fn last_stray_interrupt_sources() -> u32 {
433    // SAFETY: a volatile aligned u32 read through a raw pointer, so no reference is formed. Only
434    // the asm exception handler writes it, and an aligned word load cannot tear.
435    unsafe { core::ptr::read_volatile(&raw const __psx_rt_stray_irq_last) }
436}
437
438/// The sources psx-rt's handler acknowledges as strays for one entry, given
439/// `I_STAT`, `I_MASK` and COP0 Cause; zero when it leaves them alone.
440///
441/// It acts only on an interrupt (Cause.ExcCode 0) with no VBlank pending: a
442/// pending VBlank means a game's own handler may have chained here with a
443/// source of its own still waiting for it, which must stay in `I_STAT`. The
444/// handler's assembly makes exactly this decision.
445pub const fn stray_interrupt_sources(status: u32, mask: u32, cause: u32) -> u32 {
446    let pending = status & mask;
447    let is_interrupt = (cause >> 2) & 0x1F == 0;
448    if is_interrupt && pending & (1 << psx_hw::irq::source::VBLANK) == 0 {
449        pending
450    } else {
451        0
452    }
453}
454
455/// Set once [`install_vblank_counter`] has run, so [`wait_vblank`] can
456/// install lazily without resetting a counter the game is already using.
457#[cfg(target_arch = "mips")]
458static mut INSTALLED: bool = false;
459
460/// The first word of the exception vector as `_start` found it, before any
461/// game code ran: the BIOS's own, or whatever a previous program left.
462#[cfg(target_arch = "mips")]
463static mut BOOT_VECTOR_WORD: u32 = 0;
464
465/// Set once `_start` has stored [`BOOT_VECTOR_WORD`].
466#[cfg(target_arch = "mips")]
467static mut BOOT_VECTOR_RECORDED: bool = false;
468
469/// Remember the vector word as the program found it. Called once from
470/// `_start`, after `.bss` is zeroed and before `main`.
471#[cfg(target_arch = "mips")]
472pub(crate) fn record_boot_vector() {
473    // SAFETY: two private statics written once at boot, before `main` and before any code that
474    // reads them runs; volatile aligned stores through raw pointers.
475    unsafe {
476        core::ptr::write_volatile(&raw mut BOOT_VECTOR_WORD, vector_word());
477        core::ptr::write_volatile(&raw mut BOOT_VECTOR_RECORDED, true);
478    }
479}
480
481/// Whether [`wait_vblank`] may install psx-rt's handler on first use.
482///
483/// It may when the vector still holds what the program booted with (nobody
484/// has taken it) or already jumps to psx-rt's handler. A different word
485/// means the game put its own handler there (hk-psx and cs-psx wrap psx-rt's
486/// to service CD interrupts), and a safe wait must not overwrite it. An
487/// entry that never recorded the boot word keeps the old rule: install.
488#[cfg(any(target_arch = "mips", test))]
489const fn may_install_lazily(boot_word: Option<u32>, vector: u32, handler_jump: u32) -> bool {
490    match boot_word {
491        None => true,
492        Some(boot) => vector == boot || vector == handler_jump,
493    }
494}
495
496#[cfg(target_arch = "mips")]
497extern "C" {
498    fn __psx_rt_exception_handler();
499}
500
501/// Install and enable the VBlank counter interrupt path.
502///
503/// This writes a branch into the MIPS general exception vector,
504/// enables the VBlank source in `I_MASK`, and sets the COP0 interrupt
505/// enable bits used by the R3000A. The operation is idempotent for
506/// the current runtime: reinstalling simply resets the software
507/// counter and refreshes the vector.
508///
509/// The handler owns VBlank only. Another source enabled in `I_MASK` that
510/// fires while no VBlank is pending is acknowledged and counted
511/// ([`stray_interrupt_count`]) rather than re-entering forever, so a source a
512/// game needs must be serviced by the game's own handler ahead of this one
513/// (which then chains here), not left to this handler.
514#[cfg(target_arch = "mips")]
515pub fn install_vblank_counter() {
516    const EXCEPTION_VECTOR: *mut u32 = 0x8000_0080 as *mut u32;
517    const J_OPCODE: u32 = 0x0800_0000;
518
519    // SAFETY: 0x8000_0080 is the R3000 general exception vector in KSEG0 kernel RAM, two aligned
520    // words written here as `j handler` plus a nop delay slot, then the I-cache is flushed so the
521    // CPU fetches them. The counter and INSTALLED are psx-rt-private statics written through raw
522    // pointers, so no reference aliases them. `enable_cpu_interrupts` runs only after the vector
523    // points at psx-rt's handler. Assumes nothing still depends on the vector this replaces (a game
524    // wrapper installed earlier is overwritten).
525    unsafe {
526        let handler = __psx_rt_exception_handler as *const () as usize as u32;
527        core::ptr::write_volatile(EXCEPTION_VECTOR, J_OPCODE | ((handler >> 2) & 0x03ff_ffff));
528        core::ptr::write_volatile(EXCEPTION_VECTOR.add(1), 0);
529        crate::cache::flush_instruction_cache();
530
531        core::ptr::write_volatile(&raw mut __psx_rt_vblank_count, 0);
532        irq::acknowledge(1 << psx_hw::irq::source::VBLANK);
533        // This handler services VBlank only. After a BIOS disc boot, do not
534        // preserve CD-ROM/DMA/etc. bits the BIOS may have left enabled.
535        irq::set_mask(1 << psx_hw::irq::source::VBLANK);
536        enable_cpu_interrupts();
537        core::ptr::write_volatile(&raw mut INSTALLED, true);
538    }
539}
540
541/// Install and enable the VBlank counter interrupt path.
542#[cfg(not(target_arch = "mips"))]
543pub fn install_vblank_counter() {}
544
545/// Unexpected exceptions survived so far. Non-zero means some access
546/// faulted and was stepped over; the value it read or wrote is garbage.
547#[inline]
548pub fn fault_count() -> u32 {
549    // SAFETY: a volatile aligned u32 read through a raw pointer, so no reference is formed. Only
550    // the asm exception handler writes it, and an aligned word load cannot tear.
551    unsafe { core::ptr::read_volatile(&raw const __psx_rt_fault_count) }
552}
553
554/// Interrupts that landed on a GTE command, whose return the handler moved
555/// to EPC + 4 so the command did not run twice.
556#[inline]
557pub fn gte_skip_count() -> u32 {
558    // SAFETY: a volatile aligned u32 read through a raw pointer, so no reference is formed. Only
559    // the asm exception handler writes it, and an aligned word load cannot tear.
560    unsafe { core::ptr::read_volatile(&raw const __psx_rt_gte_skip_count) }
561}
562
563/// True when `word` is a GTE command (a COP2 `cofun`: opcode 0x12 with bit
564/// 25 set, so the top seven bits are `0100101`). psx-spx's test is
565/// `(word & 0xFE00_0000) == 0x4A00_0000`. GTE register moves (`mfc2`,
566/// `mtc2`, `cfc2`, `ctc2`), `lwc2`/`swc2` and the `bc2` branches are not
567/// commands: an interrupt on them is taken before they run.
568#[inline]
569pub const fn is_gte_command(word: u32) -> bool {
570    psx_hw::gte::is_command(word)
571}
572
573/// Where psx-rt's exception handler resumes after an interrupt: `epc`, or
574/// `epc + 4` when `word_at_epc` is a GTE command.
575///
576/// On silicon an interrupt taken on a GTE command lets the command run and
577/// still reports EPC at it, so resuming at EPC runs it twice
578/// (hardware-tests v1.24, case 0xC9: 38 of 38; with this rule, case 0xCB:
579/// 61 skips, none doubled or lost). The handler's assembly makes exactly
580/// this decision.
581///
582/// With Cause.BD set, EPC is the branch whose delay slot was interrupted,
583/// so `word_at_epc` is a branch and the result is `epc`: the branch and a
584/// GTE command in its delay slot both run again. psx-spx documents that
585/// the fix cannot cover delay slots; keep GTE commands out of them
586/// (`hazard-scan` warns about any in an image).
587#[inline]
588pub const fn interrupt_resume_pc(epc: u32, word_at_epc: u32) -> u32 {
589    if is_gte_command(word_at_epc) {
590        epc.wrapping_add(4)
591    } else {
592        epc
593    }
594}
595
596/// Where psx-rt's exception handler resumes after a fault (any exception
597/// but an interrupt), or `None` where it halts. `badvaddr` is COP0
598/// BadVAddr.
599///
600/// BREAK (the shipping form of `panic=immediate-abort`), an instruction bus
601/// error and a misaligned instruction fetch (AdEL with BadVAddr == EPC)
602/// halt. So does any fault with [`psx_hw::cop0::CAUSE_BD`] set: the faulting instruction
603/// sat in the delay slot of the branch at EPC, which has already run, so
604/// `epc + 4` would run the faulting instruction again as if the branch had
605/// fallen through, and resuming where the branch goes would take decoding
606/// it, handler code every game carries for a path only a bug reaches.
607/// Every other fault is stepped over (`epc + 4`) and counted, unless
608/// [`STRICT_FAULTS`] is set, in which case every fault halts. The
609/// handler's assembly makes exactly this decision; before halting it
610/// records Cause, EPC and BadVAddr ([`fault_cause`], [`fault_pc`],
611/// [`fault_bad_address`]).
612pub const fn fault_resume_pc(cause: u32, epc: u32, badvaddr: u32) -> Option<u32> {
613    resume_after_fault(STRICT_FAULTS, cause, epc, badvaddr)
614}
615
616/// [`fault_resume_pc`] for either policy, so host tests cover both.
617const fn resume_after_fault(strict: bool, cause: u32, epc: u32, badvaddr: u32) -> Option<u32> {
618    if strict {
619        return None;
620    }
621    match (cause >> 2) & 0x1F {
622        // Break, IBE
623        9 | 6 => return None,
624        // AdEL on the fetch itself
625        4 if badvaddr == epc => return None,
626        _ => {}
627    }
628    if cause & psx_hw::cop0::CAUSE_BD != 0 {
629        None
630    } else {
631        Some(epc.wrapping_add(4))
632    }
633}
634
635/// COP0 BadVAddr captured for the latest unexpected exception.
636#[inline]
637#[doc(alias = "BadVAddr")]
638pub fn fault_bad_address() -> u32 {
639    // SAFETY: a volatile aligned u32 read through a raw pointer, so no reference is formed. Only
640    // the asm exception handler writes it, and an aligned word load cannot tear.
641    unsafe { core::ptr::read_volatile(&raw const __psx_rt_fault_badvaddr) }
642}
643
644/// Raw COP0 Cause captured for the latest unexpected exception.
645#[inline]
646pub fn fault_cause() -> u32 {
647    // SAFETY: a volatile aligned u32 read through a raw pointer, so no reference is formed. Only
648    // the asm exception handler writes it, and an aligned word load cannot tear.
649    unsafe { core::ptr::read_volatile(&raw const __psx_rt_fault_cause) }
650}
651
652/// COP0 EPC captured for the latest unexpected exception.
653#[inline]
654#[doc(alias = "EPC")]
655pub fn fault_pc() -> u32 {
656    // SAFETY: a volatile aligned u32 read through a raw pointer, so no reference is formed. Only
657    // the asm exception handler writes it, and an aligned word load cannot tear.
658    unsafe { core::ptr::read_volatile(&raw const __psx_rt_fault_epc) }
659}
660
661/// Current monotonic VBlank count.
662#[inline]
663#[doc(alias = "VSync")]
664pub fn vblank_count() -> u32 {
665    // SAFETY: a volatile aligned u32 read through a raw pointer, so no reference is formed. The
666    // asm handler and `install_vblank_counter` are the only writers, and an aligned word load
667    // cannot tear.
668    unsafe { core::ptr::read_volatile(&raw const __psx_rt_vblank_count) }
669}
670
671/// Queue one GP1 word for the VBlank handler to apply at a blank edge
672/// (deferred tear-free display flip). Overwrites any unapplied word.
673///
674/// The handler applies the word at the first VBlank edge on which GPUSTAT
675/// bit 24 (the GPU's IRQ1 flag) is set, so the frame must signal its own
676/// end:
677///
678/// 1. acknowledge the flag with GP1(02h) before kicking the frame's work
679///    (`psx_gpu::arm_draw_done`), after the previous flip has landed;
680/// 2. end that work with GP0(1Fh): as the last node of the DMA chain
681///    (`psx_gpu::OrderingTable::end_with_draw_done`, or a one-word packet
682///    in an ordered stream) or, after drawing through the ports,
683///    `psx_gpu::signal_draw_done`;
684/// 3. queue the display-start word here.
685///
686/// The flag stays set until the next acknowledge, so a word queued while
687/// the GPU is idle after such a frame (a display enable, say) applies at
688/// the next edge. A game that never sends GP0(1Fh) never flips: before this
689/// handler tested bit 24 it tested GPUSTAT bit 28, which on silicon rises
690/// about one large primitive before the drawing ends (hardware-tests v1.24
691/// cases 219-226), so a flip could expose a frame one primitive short.
692/// Keep interrupt source 1 (GPU) masked in `I_MASK`: this handler owns VBlank
693/// only, so a GPU interrupt it finds is acknowledged as a stray and counted
694/// ([`stray_interrupt_count`]), which costs a handler entry per frame.
695#[cfg(target_arch = "mips")]
696#[inline]
697#[doc(alias = "GP1")]
698pub fn queue_display_control_at_vblank(word: u32) {
699    // SAFETY: a volatile aligned u32 store through a raw pointer, so no reference is formed. The
700    // VBlank handler is the only other accessor, and an aligned word store cannot tear.
701    unsafe { core::ptr::write_volatile(&raw mut __psx_rt_pending_gp1, word) }
702}
703
704/// Host no-op: no IRQ exists to consume the queue off-target.
705#[cfg(not(target_arch = "mips"))]
706#[inline]
707#[doc(alias = "GP1")]
708pub fn queue_display_control_at_vblank(_word: u32) {}
709
710/// Renamed to [`queue_display_control_at_vblank`].
711#[deprecated(note = "renamed to `queue_display_control_at_vblank`")]
712#[inline(always)]
713pub fn queue_gp1_at_vblank(word: u32) {
714    queue_display_control_at_vblank(word)
715}
716
717/// True while a word queued by [`queue_display_control_at_vblank`] has not
718/// yet been applied by the VBlank handler.
719#[cfg(target_arch = "mips")]
720#[inline]
721#[doc(alias = "GP1")]
722pub fn is_display_control_queued() -> bool {
723    // SAFETY: a volatile aligned u32 read through a raw pointer, so no reference is formed. The
724    // VBlank handler is the only other accessor, and an aligned word load cannot tear.
725    unsafe { core::ptr::read_volatile(&raw const __psx_rt_pending_gp1) != 0 }
726}
727
728/// Empty the queue and hand back the word the handler has not applied, so
729/// the caller can write it to GP1 itself. Returns 0 when nothing was queued.
730///
731/// The handler only applies its word at a blank edge on which GPUSTAT
732/// bit 24 is set (see [`queue_display_control_at_vblank`]), so a long enough frame, or
733/// one that never sends GP0(1Fh), leaves it queued indefinitely. A caller that has given up waiting must take the word
734/// rather than leave it: the next [`queue_display_control_at_vblank`] overwrites the
735/// slot, and a display start that never reaches the GPU desynchronises the
736/// display side from the draw side for the rest of the session.
737///
738/// Racing the handler is harmless. If the IRQ lands between the read and
739/// the clear, the handler applies the word and the caller writes the same
740/// value again; GP1(05h) is idempotent.
741#[cfg(target_arch = "mips")]
742#[inline]
743#[doc(alias = "GP1")]
744pub fn take_queued_display_control() -> u32 {
745    // SAFETY: volatile aligned u32 accesses through a raw pointer, so no reference is formed. A
746    // handler run between the read and the clear is harmless (see above).
747    unsafe {
748        let word = core::ptr::read_volatile(&raw const __psx_rt_pending_gp1);
749        if word != 0 {
750            core::ptr::write_volatile(&raw mut __psx_rt_pending_gp1, 0);
751        }
752        word
753    }
754}
755
756/// Host: nothing is ever queued, so nothing can be taken.
757#[cfg(not(target_arch = "mips"))]
758#[inline]
759#[doc(alias = "GP1")]
760pub fn take_queued_display_control() -> u32 {
761    0
762}
763
764/// Renamed to [`take_queued_display_control`].
765#[deprecated(note = "renamed to `take_queued_display_control`")]
766#[inline(always)]
767pub fn take_pending_gp1() -> u32 {
768    take_queued_display_control()
769}
770
771/// Host no-op: nothing is ever pending off-target.
772#[cfg(not(target_arch = "mips"))]
773#[inline]
774#[doc(alias = "GP1")]
775pub fn is_display_control_queued() -> bool {
776    false
777}
778
779/// Renamed to [`is_display_control_queued`].
780#[deprecated(note = "renamed to `is_display_control_queued`")]
781#[inline(always)]
782pub fn gp1_queue_pending() -> bool {
783    is_display_control_queued()
784}
785
786/// Block until the next VBlank IRQ.
787///
788/// This is the display-sync primitive: a frame that finishes early sleeps
789/// until the blank, and a slow frame snaps to the next one, so presentation
790/// quantizes to whole display periods. (`psx_gpu::vsync()` cannot do this:
791/// it reconfigures Timer 1 on every call, and a mode write resets the
792/// counter, so it busy-waits a fixed 242 HBlanks from the call site
793/// instead of syncing to the display.)
794///
795/// Installs the VBlank counter on first use if the game has not already
796/// called [`install_vblank_counter`], but only while the exception vector is
797/// still the one the program booted with. A game that has put its own
798/// handler there keeps it: this function never overwrites it. Such a game
799/// calls [`install_vblank_counter`] first and then wraps psx-rt's handler,
800/// or its handler chains to psx-rt's, which is what advances the count this
801/// waits on. The wait is bounded: after about four million reads
802/// without the count moving it returns (and asserts in a debug build) instead
803/// of hanging, as it would inside a critical section or without the handler.
804/// [`try_wait_vblank`] takes the bound.
805#[cfg(target_arch = "mips")]
806#[doc(alias = "VSync")]
807pub fn wait_vblank() {
808    // SAFETY: volatile reads of private flags through raw pointers; only `install_vblank_counter`
809    // and `record_boot_vector` write them, on this same single thread.
810    unsafe {
811        if !core::ptr::read_volatile(&raw const INSTALLED) {
812            let boot = core::ptr::read_volatile(&raw const BOOT_VECTOR_RECORDED)
813                .then(|| core::ptr::read_volatile(&raw const BOOT_VECTOR_WORD));
814            let handler = __psx_rt_exception_handler as *const () as usize as u32;
815            if may_install_lazily(boot, vector_word(), jump_word(handler)) {
816                install_vblank_counter();
817            }
818        }
819    }
820    if !try_wait_vblank(crate::wait::SPIN_LIMIT) {
821        debug_assert!(
822            false,
823            "wait_vblank: the VBlank count never advanced; psx-rt's handler is not running"
824        );
825    }
826}
827
828/// [`wait_vblank`] with its own bound: `true` once the count has advanced,
829/// `false` after `spins` reads of it without a change. A `false` means no
830/// VBlank interrupt reached psx-rt's handler: interrupts are masked (inside a
831/// [`critical_section`](crate::critical_section)), the counter was never
832/// installed, or a game's handler does not chain to psx-rt's. Does not
833/// install the counter; [`wait_vblank`] does that first.
834#[cfg(target_arch = "mips")]
835pub fn try_wait_vblank(spins: u32) -> bool {
836    let start = vblank_count();
837    crate::wait::wait_while(|| vblank_count() == start, || false, || {}, spins)
838}
839
840/// Block until the next VBlank IRQ. Host no-op: the counter never
841/// advances off-target, so waiting would hang.
842#[cfg(not(target_arch = "mips"))]
843#[doc(alias = "VSync")]
844pub fn wait_vblank() {}
845
846/// True when the general exception vector jumps to psx-rt's handler, the
847/// one that never touches `$sp` (so interrupts are safe on any stack).
848#[cfg(target_arch = "mips")]
849pub fn is_handler_installed() -> bool {
850    let handler = __psx_rt_exception_handler as *const () as usize as u32;
851    vector_word() == jump_word(handler)
852}
853
854/// The vector word of a game's exception handler declared with
855/// [`declare_stack_safe_handler`]; zero when none is.
856#[cfg(target_arch = "mips")]
857static mut STACK_SAFE_HANDLER: u32 = 0;
858
859/// Declare that the game's own exception handler at `handler` is safe to
860/// take an interrupt on any stack, including a
861/// [`ScratchpadStack`](crate::scratchpad::ScratchpadStack).
862///
863/// With the `scratchpad-stack-check` feature, a stack switch traps when
864/// interrupts are on and the general exception vector jumps anywhere but
865/// psx-rt's handler, because a handler that pushes onto the interrupted
866/// stack would write below the scratchpad stack's frames. A game that
867/// installs its own vector (hk-psx wraps psx-rt's handler to service CD
868/// interrupts, then jumps to it) calls this once with the address it puts
869/// in the vector; the check then accepts exactly that vector word, and
870/// still traps any other handler, such as a BIOS vector restored behind the
871/// game's back. Declaring does not install anything.
872///
873/// ```no_run
874/// unsafe extern "C" fn game_exception_wrapper() {}
875/// // SAFETY: game_exception_wrapper switches to its own stack before it
876/// // stores anything and restores $sp before it hands back.
877/// unsafe { psx_rt::interrupts::declare_stack_safe_handler(game_exception_wrapper) };
878/// ```
879///
880/// # Safety
881///
882/// `handler`, and everything it calls or jumps to, must leave `$sp` and the
883/// memory below it alone: it uses only `$k0`/`$k1` or saves state to memory
884/// of its own and runs on a stack of its own, and it returns (or chains to
885/// psx-rt's handler) with `$sp` unchanged.
886#[cfg(target_arch = "mips")]
887pub unsafe fn declare_stack_safe_handler(handler: unsafe extern "C" fn()) {
888    // SAFETY: a plain store; nothing reads it from an interrupt.
889    unsafe {
890        core::ptr::write_volatile(
891            &raw mut STACK_SAFE_HANDLER,
892            jump_word(handler as *const () as usize as u32),
893        )
894    }
895}
896
897/// Declare a stack-safe exception handler. Host no-op: there are no
898/// exception vectors off-target.
899///
900/// # Safety
901///
902/// As on the target.
903#[cfg(not(target_arch = "mips"))]
904pub unsafe fn declare_stack_safe_handler(_handler: unsafe extern "C" fn()) {}
905
906/// True when the general exception vector jumps to psx-rt's handler or to
907/// the one declared with [`declare_stack_safe_handler`].
908#[cfg(target_arch = "mips")]
909pub fn is_stack_safe_handler_installed() -> bool {
910    // SAFETY: a plain read of a word only this module writes.
911    let declared = unsafe { core::ptr::read_volatile(&raw const STACK_SAFE_HANDLER) };
912    is_handler_installed() || (declared != 0 && vector_word() == declared)
913}
914
915/// The `j handler` word psx-rt writes into the vector.
916#[cfg(target_arch = "mips")]
917fn jump_word(handler: u32) -> u32 {
918    0x0800_0000 | ((handler >> 2) & 0x03ff_ffff)
919}
920
921/// The first word of the general exception vector.
922#[cfg(target_arch = "mips")]
923fn vector_word() -> u32 {
924    const EXCEPTION_VECTOR: *const u32 = 0x8000_0080 as *const u32;
925    // SAFETY: a read of the kernel's vector word.
926    unsafe { core::ptr::read_volatile(EXCEPTION_VECTOR) }
927}
928
929/// True when COP0 SR has interrupts enabled (IEc).
930#[cfg(target_arch = "mips")]
931pub fn are_interrupts_enabled() -> bool {
932    let sr: u32;
933    // The nop covers MFC0's load delay.
934    // SAFETY: reads COP0 SR into $8, declared as the output, and the nop covers MFC0's load delay.
935    // No memory or stack is touched, as the options state.
936    unsafe { core::arch::asm!("mfc0 $8, $12", "nop", lateout("$8") sr, options(nomem, nostack)) };
937    sr & 1 != 0
938}
939
940#[cfg(target_arch = "mips")]
941unsafe fn enable_cpu_interrupts() {
942    const STATUS_IE: u32 = 1 << 0;
943    const STATUS_IM2: u32 = 1 << 10;
944    const STATUS_CU2: u32 = 1 << 30;
945
946    let mut sr: u32;
947    // MFC0 has a one-instruction load-delay hazard on the R3000: without the
948    // nop the asm block hands back the STALE $8, and whatever garbage it held
949    // gets OR'd into SR (seen in the wild as BEV set -> exceptions vectoring
950    // into ROM -> pc walking off the end of the BIOS).
951    // SAFETY: reads COP0 SR into $8, declared as the output, and the nop covers MFC0's load delay.
952    // No other register or memory is touched.
953    unsafe { core::arch::asm!("mfc0 $8, $12", "nop", lateout("$8") sr) };
954    sr |= STATUS_IE | STATUS_IM2 | STATUS_CU2;
955    // SAFETY: writes SR back with IEc, IM2 and CU2 set and every other bit as read above. The only
956    // caller, `install_vblank_counter`, has already pointed the exception vector at psx-rt's
957    // handler, so the interrupts this enables land there.
958    unsafe { core::arch::asm!("mtc0 $8, $12", in("$8") sr) };
959}
960
961#[cfg(test)]
962mod tests {
963    use super::*;
964
965    const EPC: u32 = 0x8001_2340;
966
967    /// GTE commands the SDK issues, as their encoded words.
968    const RTPS: u32 = 0x4A18_0001;
969    const RTPT: u32 = 0x4A28_0030;
970    const NCLIP: u32 = 0x4B40_0006;
971    const MVMVA: u32 = 0x4A48_6012;
972    const AVSZ3: u32 = 0x4B58_002D;
973    const GPF: u32 = 0x4B90_003D;
974
975    #[test]
976    fn an_interrupt_on_a_gte_command_resumes_after_it() {
977        for word in [RTPS, RTPT, NCLIP, MVMVA, AVSZ3, GPF] {
978            assert!(is_gte_command(word), "{word:#010x}");
979            assert_eq!(interrupt_resume_pc(EPC, word), EPC + 4, "{word:#010x}");
980        }
981    }
982
983    #[test]
984    fn psx_spx_mask_and_seven_bit_test_agree() {
985        // Every top byte, with the rest of the word set both ways.
986        for top in 0u32..=0xFF {
987            for low in [0, 0x00FF_FFFF] {
988                let word = (top << 24) | low;
989                assert_eq!(
990                    is_gte_command(word),
991                    word & 0xFE00_0000 == 0x4A00_0000,
992                    "{word:#010x}"
993                );
994            }
995        }
996    }
997
998    #[test]
999    fn gte_register_moves_and_loads_resume_at_epc() {
1000        let not_commands = [
1001            0x480C_6000, // mfc2 $12, SXY0
1002            0x4889_6000, // mtc2 $9, SXY0
1003            0x4842_F800, // cfc2 $2, FLAG
1004            0x48C2_C000, // ctc2 $2, OFX
1005            0xC885_0000, // lwc2 $5, 0($4)
1006            0xE8AC_0000, // swc2 $12, 0($5)
1007            0x4900_0003, // bc2f
1008            0x4901_0003, // bc2t
1009            0x4080_6000, // mtc0 $0, SR
1010            0x0000_0000, // nop
1011            0x8C82_0000, // lw $2, 0($4)
1012            0x03E0_0008, // jr $ra
1013        ];
1014        for word in not_commands {
1015            assert!(!is_gte_command(word), "{word:#010x}");
1016            assert_eq!(interrupt_resume_pc(EPC, word), EPC, "{word:#010x}");
1017        }
1018    }
1019
1020    #[test]
1021    fn a_gte_command_in_a_delay_slot_is_not_stepped_over() {
1022        // Cause.BD set: EPC names the branch, the RTPS sits at EPC + 4. The
1023        // handler reads the branch, so it resumes at EPC and the branch and
1024        // its RTPS both run again (psx-spx: the fix does not cover delay
1025        // slots). Stepping to EPC + 4 would drop the branch instead, and
1026        // EPC + 8 would drop the branch target; neither is safe, which is
1027        // why the answer is to keep GTE commands out of delay slots.
1028        let branches = [
1029            0x1509_FFF0, // bne $8, $9, back
1030            0x1000_0004, // b forward
1031            0x0C00_4000, // jal
1032            0x0800_4000, // j
1033            0x0100_F809, // jalr $8
1034            0x03E0_0008, // jr $ra
1035        ];
1036        for branch in branches {
1037            assert_eq!(interrupt_resume_pc(EPC, branch), EPC, "{branch:#010x}");
1038        }
1039    }
1040
1041    #[test]
1042    fn the_resume_address_wraps_like_the_hardware_add() {
1043        assert_eq!(interrupt_resume_pc(0xFFFF_FFFC, RTPS), 0);
1044        assert_eq!(resume_after_fault(false, DBE, 0xFFFF_FFFC, 0), Some(0));
1045    }
1046
1047    // Fault path. Cause values are ExcCode << 2, plus CAUSE_BD.
1048    const ADEL: u32 = 4 << 2;
1049    const ADES: u32 = 5 << 2;
1050    const IBE: u32 = 6 << 2;
1051    const DBE: u32 = 7 << 2;
1052    const BREAK: u32 = 9 << 2;
1053    const RI: u32 = 10 << 2;
1054    const CPU: u32 = 11 << 2;
1055    const OV: u32 = 12 << 2;
1056
1057    /// A misaligned data address: never equal to an (aligned) EPC.
1058    const BAD_DATA: u32 = 0x8001_0001;
1059
1060    #[test]
1061    fn a_fault_outside_a_delay_slot_resumes_after_it() {
1062        for cause in [ADEL, ADES, DBE, RI, CPU, OV] {
1063            assert_eq!(
1064                resume_after_fault(false, cause, EPC, BAD_DATA),
1065                Some(EPC + 4),
1066                "{cause:#x}"
1067            );
1068        }
1069    }
1070
1071    #[test]
1072    fn a_fault_in_a_delay_slot_halts() {
1073        // Cause.BD: the faulting word is at EPC + 4, behind the branch at
1074        // EPC. EPC + 4 would re-run it with the branch dropped (a taken
1075        // branch would fall through), so the handler halts instead.
1076        for cause in [ADEL, ADES, DBE, RI, CPU, OV] {
1077            assert_eq!(
1078                resume_after_fault(false, cause | psx_hw::cop0::CAUSE_BD, EPC, BAD_DATA),
1079                None,
1080                "{cause:#x}"
1081            );
1082        }
1083    }
1084
1085    #[test]
1086    fn fatal_faults_halt_in_or_out_of_a_delay_slot() {
1087        for bd in [0, psx_hw::cop0::CAUSE_BD] {
1088            assert_eq!(resume_after_fault(false, BREAK | bd, EPC, 0), None);
1089            assert_eq!(resume_after_fault(false, IBE | bd, EPC, 0), None);
1090            assert_eq!(resume_after_fault(false, ADEL | bd, EPC, EPC), None);
1091        }
1092    }
1093
1094    #[test]
1095    fn a_gte_command_in_a_delay_slot_interrupt_versus_fault() {
1096        // bne $8, $9, -4 at EPC with an RTPS in its slot. An interrupt there
1097        // resumes at the branch, so both run again (documented above); a
1098        // fault there (Coprocessor Unusable with SR.CU2 clear, say) halts.
1099        let bne = 0x1509_FFFC;
1100        assert_eq!(interrupt_resume_pc(EPC, bne), EPC);
1101        assert_eq!(
1102            resume_after_fault(false, CPU | psx_hw::cop0::CAUSE_BD, EPC, 0),
1103            None
1104        );
1105        // Outside a delay slot the same fault steps over the RTPS.
1106        assert_eq!(resume_after_fault(false, CPU, EPC, 0), Some(EPC + 4));
1107    }
1108
1109    #[test]
1110    fn strict_faults_halt_on_every_fault() {
1111        for bd in [0, psx_hw::cop0::CAUSE_BD] {
1112            for cause in [ADEL, ADES, IBE, DBE, BREAK, RI, CPU, OV] {
1113                assert_eq!(
1114                    resume_after_fault(true, cause | bd, EPC, BAD_DATA),
1115                    None,
1116                    "{cause:#x} {bd:#x}"
1117                );
1118            }
1119        }
1120    }
1121
1122    // Vector words: a BIOS stub, psx-rt's jump, and a game wrapper's jump.
1123    const BIOS_STUB: u32 = 0x3C1A_0000;
1124    const RT_JUMP: u32 = 0x0800_4000;
1125    const GAME_JUMP: u32 = 0x0800_8000;
1126
1127    #[test]
1128    fn a_safe_wait_installs_only_over_the_boot_vector_or_its_own() {
1129        // Untouched since boot, or already psx-rt's: install.
1130        assert!(may_install_lazily(Some(BIOS_STUB), BIOS_STUB, RT_JUMP));
1131        assert!(may_install_lazily(Some(BIOS_STUB), RT_JUMP, RT_JUMP));
1132        // A game's own handler is never overwritten.
1133        assert!(!may_install_lazily(Some(BIOS_STUB), GAME_JUMP, RT_JUMP));
1134        // The boot word was itself another program's handler: still "untouched".
1135        assert!(may_install_lazily(Some(GAME_JUMP), GAME_JUMP, RT_JUMP));
1136        // No recorded boot word: the entry predates the rule, keep installing.
1137        assert!(may_install_lazily(None, GAME_JUMP, RT_JUMP));
1138    }
1139
1140    // I_STAT / I_MASK bits (psx-spx): VBlank 0, GPU 1, CDROM 2, DMA 3.
1141    const VBLANK_BIT: u32 = 1 << 0;
1142    const GPU_BIT: u32 = 1 << 1;
1143    const CDROM_BIT: u32 = 1 << 2;
1144    const INTERRUPT: u32 = 0;
1145
1146    #[test]
1147    fn an_enabled_source_nothing_owns_is_acknowledged() {
1148        // CD enabled and firing with no VBlank: the case that re-entered forever.
1149        assert_eq!(
1150            stray_interrupt_sources(CDROM_BIT, VBLANK_BIT | CDROM_BIT, INTERRUPT),
1151            CDROM_BIT
1152        );
1153        // Only enabled sources count.
1154        assert_eq!(
1155            stray_interrupt_sources(CDROM_BIT | GPU_BIT, VBLANK_BIT | GPU_BIT, INTERRUPT),
1156            GPU_BIT
1157        );
1158        assert_eq!(stray_interrupt_sources(CDROM_BIT, VBLANK_BIT, INTERRUPT), 0);
1159    }
1160
1161    #[test]
1162    fn a_chained_vblank_leaves_other_sources_for_their_owner() {
1163        // A game's handler services CD itself and chains here for VBlank; a CD
1164        // flag that latched in between must stay in I_STAT for it.
1165        let mask = VBLANK_BIT | CDROM_BIT;
1166        assert_eq!(
1167            stray_interrupt_sources(VBLANK_BIT | CDROM_BIT, mask, INTERRUPT),
1168            0
1169        );
1170    }
1171
1172    #[test]
1173    fn only_interrupts_are_acknowledged() {
1174        for cause in [ADEL, ADES, IBE, DBE, BREAK, RI, CPU, OV] {
1175            assert_eq!(
1176                stray_interrupt_sources(CDROM_BIT, CDROM_BIT, cause),
1177                0,
1178                "{cause:#x}"
1179            );
1180        }
1181        // Cause bits above ExcCode (BD, pending lines) do not hide an interrupt.
1182        let busy = psx_hw::cop0::CAUSE_BD | 0x0400;
1183        assert_eq!(
1184            stray_interrupt_sources(CDROM_BIT, CDROM_BIT, busy),
1185            CDROM_BIT
1186        );
1187    }
1188
1189    #[test]
1190    fn the_public_decision_follows_the_feature() {
1191        for cause in [ADEL, ADES, DBE, RI, CPU, OV] {
1192            assert_eq!(
1193                fault_resume_pc(cause, EPC, BAD_DATA),
1194                resume_after_fault(STRICT_FAULTS, cause, EPC, BAD_DATA),
1195                "{cause:#x}"
1196            );
1197        }
1198    }
1199}