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}