rustyn64_core/bus.rs
1//! The Bus owns everything mutable.
2//!
3//! RDRAM (main work RAM), the RSP, the RDP, the AI (audio), the cart (→ PI), the
4//! controllers (→ SI), and the RCP interface register blocks
5//! (SP / DP / VI / AI / PI / SI / RI / MI). The CPU borrows `&mut Bus` during
6//! `tick()`. The RDP and AI see narrower bus traits
7//! ([`rustyn64_rdp::VideoBus`], [`rustyn64_audio::AudioBus`]) which the Bus
8//! implements. See `docs/architecture.md` (the load-bearing facts).
9//!
10//! Per the `TetaNES` postmortem (carried over from `RustyNES`): one owner for
11//! all mutable state avoids the "CPU holds the RSP/RDP, but they also need the
12//! CPU's memory bus"
13//! borrow-checker fight. Each chip sees only the smaller trait it actually needs.
14
15// The MI interrupt block is a row of orthogonal hardware-latch booleans that map
16// 1:1 to real RCP IRQ lines; collapsing them into an enum would obscure the model.
17#![allow(clippy::struct_excessive_bools)]
18// Address math truncates by design when narrowing 32-bit physical addresses.
19#![allow(clippy::cast_possible_truncation)]
20
21use rustyn64_audio::{AiIrq, Audio, AudioBus, StereoSample};
22use rustyn64_cart::{Cart, Cartridge, RdramBus};
23use rustyn64_cpu::Bus as CpuBus;
24use rustyn64_rdp::{Rdp, VideoBus};
25use rustyn64_rsp::Rsp;
26use serde::{Deserialize, Serialize};
27
28use crate::vi::{self, Vi};
29
30/// Expand a 5-bit color channel to 8 bits, replicating the high bits into the
31/// low so 0x1F maps to 0xFF (not 0xF8) — the standard RGBA5551 → RGBA8 widening.
32/// Masks to 5 bits first, so an out-of-range argument cannot overflow the shift.
33const fn expand5(v5: u8) -> u8 {
34 let v = v5 & 0x1F;
35 (v << 3) | (v >> 2)
36}
37
38/// Convert a logical RGBA5551 pixel to the VI's RGB8, using the **truncating**
39/// widening the VI hardware applies (the 5-bit field sits at the top of the byte
40/// with the low bits zero — *not* `expand5`'s high-bit replication). Alpha (bit 0,
41/// coverage) is not carried; the scan-out sets an opaque display alpha. Ledger R-5.
42const fn vi_rgb5551(px: u16) -> [u8; 3] {
43 [
44 ((px >> 8) & 0xF8) as u8,
45 ((px & 0x07C0) >> 3) as u8,
46 ((px & 0x003E) << 2) as u8,
47 ]
48}
49
50/// The VI's 5-bit bilinear lerp, per channel: `a + (((b - a) * frac + 16) >> 5)`
51/// (`frac` a 5-bit weight 0..=31, rounding bias `+16` before the `>> 5`). A faithful
52/// port of Angrylion `vi_vl_lerp` (`vi/lerp.c`). Ledger R-5.
53#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
54fn vi_lerp3(a: [u8; 3], b: [u8; 3], frac: i32) -> [u8; 3] {
55 let mut o = [0u8; 3];
56 for i in 0..3 {
57 let (ai, bi) = (i32::from(a[i]), i32::from(b[i]));
58 o[i] = (ai + (((bi - ai) * frac + 16) >> 5)) as u8;
59 }
60 o
61}
62
63/// The register-derived rules a scan-out filters every source pixel under.
64///
65/// Fixed for the whole of one [`Bus::scanout_scaled`] call, which is what makes a memo
66/// keyed on `(x, y)` alone sound.
67#[derive(Clone, Copy)]
68struct ViCfg {
69 /// `VI_ORIGIN`, the framebuffer base.
70 origin: u32,
71 /// `VI_WIDTH`, source pixels per row.
72 src_stride: i32,
73 /// 2 (RGBA5551) or 4 (RGBA8888).
74 bpp: u32,
75 /// `VI_CTRL` bits 9:8. Only 0 and 1 run the coverage filters.
76 aa_mode: u32,
77 /// `VI_CTRL` bit 4 — the 3-tap median on partial-coverage edges.
78 divot: bool,
79 /// `VI_CTRL` bit 16 — the de-dither restore on fully-covered pixels.
80 dither_filter: bool,
81}
82
83/// One scan-out's source-sampling configuration, plus a two-row memo of source
84/// pixels that have already been through the coverage filters.
85///
86/// The configuration and the memo live in **one** value on purpose. A cache keyed on
87/// `(x, y)` alone is only sound while `origin`, `src_stride`, `bpp`, and the filter
88/// flags are fixed, and pairing a memo with the wrong configuration would return a
89/// pixel filtered under different rules — a corruption no test would localize. Making
90/// them inseparable removes the possibility rather than asserting against it.
91///
92/// **Why memoizing is behavior-identical by construction.** [`Bus::vi_sample`] is a
93/// pure function of RDRAM and the fields here; [`Bus::scanout_scaled`] takes `&self`,
94/// so RDRAM cannot change while one scan-out runs. Two calls with the same `(x, y)`
95/// therefore cannot disagree, and returning the first answer for the second call is
96/// not an approximation.
97///
98/// **On the per-scan-out allocation.** `cells` is one heap allocation per frame — at
99/// most 32 KB, and about 5 KB at the resolutions a real title programs. It is not
100/// hoisted into `Bus` because `scanout_scaled` takes `&self`: caching there would mean
101/// interior mutability in the type whose field layout *is* the save-state format
102/// (ADR 0005), which is a far larger cost than one allocation. Against the ~7.8 ms of
103/// filtering the memo replaces, the allocation does not appear in a profile.
104///
105/// **Why it is worth having.** The scan-out samples each source pixel about three
106/// times: at `x_add = 512` (a 2x upscale, what Super Mario 64 programs) an even output
107/// pixel samples column `sx`, the odd one samples `sx` and `sx + 1`, so every column is
108/// asked for twice as a near sample and once as a far one. Each of those calls runs the
109/// whole filter chain — under `aa_mode` 0 with `divot` and `dither_filter` set, three
110/// divot taps of nine de-dither taps each, 27 [`Bus::vi_read_cov`] calls of three
111/// `rdram_offset` lookups apiece.
112struct ViSampler {
113 /// The register-derived rules every sample is filtered under.
114 cfg: ViCfg,
115 /// First source column the memo covers.
116 x_lo: i32,
117 /// Columns per memo row.
118 span: usize,
119 /// The source row each memo row holds; `None` marks an unused row.
120 ///
121 /// An `Option` rather than a sentinel because a sentinel is a value the domain
122 /// might one day contain, and a collision would return another row's pixels
123 /// without invalidating anything.
124 row_y: [Option<i32>; 2],
125 /// `2 * span` filtered pixels, row-major. `None` is "not computed yet".
126 cells: alloc::vec::Vec<Option<[u8; 3]>>,
127}
128
129impl ViSampler {
130 /// Two rows is exactly what the vertical lerp needs: it samples `sy` and
131 /// `sy + 1`, and the walk over output rows only ever moves `sy` forward.
132 const ROWS: usize = 2;
133
134 /// The widest memo this will allocate, in source columns per row.
135 ///
136 /// The real bound is much smaller — `VI_X_SCALE` fields are 12 bits and the
137 /// output is clamped to a 640-pixel prescale line, so the walk cannot ask for
138 /// more than about 3,000 columns — but that argument lives a hundred lines away
139 /// in `scanout_scaled`'s register decode and would not survive someone relaxing a
140 /// clamp. `x_lo`/`x_hi` are ultimately guest-controlled through VI MMIO, and an
141 /// allocation sized by guest registers deserves a bound stated where the
142 /// allocation happens. Past the cap the memo is simply empty and every sample
143 /// takes the uncached path: slower, never wrong.
144 const MAX_SPAN: usize = 4096;
145
146 /// Build a memo covering source columns `x_lo..=x_hi` inclusive.
147 fn new(cfg: ViCfg, x_lo: i32, x_hi: i32) -> Self {
148 // `i64` throughout: the subtraction is on guest-derived values, and a signed
149 // overflow here would be a debug-build panic in a scan-out path.
150 // An empty or inverted range (`x_hi < x_lo`) and an over-wide one are the same
151 // outcome — no memo — but they are written as one explicit match so neither
152 // reads as an accident of `try_from` failing on a negative.
153 let columns = i64::from(x_hi) - i64::from(x_lo) + 1;
154 let span = match usize::try_from(columns) {
155 Ok(want) if want <= Self::MAX_SPAN => want,
156 _ => 0,
157 };
158 Self {
159 cfg,
160 x_lo,
161 span,
162 row_y: [None; Self::ROWS],
163 cells: alloc::vec![None; span * Self::ROWS],
164 }
165 }
166
167 /// The memo row holding source row `y`, evicting if neither does.
168 ///
169 /// Eviction takes the row with the smaller `y`: the scan-out walks `y` forward
170 /// (`y_add` is unsigned), so the lower row is the one that will not be asked for
171 /// again. A wrong choice here would only cost hit rate, never correctness.
172 ///
173 /// The choice is written as an explicit match rather than as a comparison of two
174 /// `Option`s, because the rule it encodes — unused rows first, then the row
175 /// further behind the walk — should be readable without knowing that `None` sorts
176 /// below every `Some`.
177 fn row_slot(&mut self, y: i32) -> usize {
178 if self.row_y[0] == Some(y) {
179 return 0;
180 }
181 if self.row_y[1] == Some(y) {
182 return 1;
183 }
184 // Written out rather than leaning on `Option`'s derived `Ord`: an unused row
185 // goes first, then the row further behind the forward walk.
186 let victim = match (self.row_y[0], self.row_y[1]) {
187 (None, _) => 0,
188 (_, None) => 1,
189 (Some(y0), Some(y1)) if y1 < y0 => 1,
190 (Some(_), Some(_)) => 0,
191 };
192 self.row_y[victim] = Some(y);
193 let base = victim * self.span;
194 self.cells[base..base + self.span].fill(None);
195 victim
196 }
197}
198
199/// The VI's integer square root (Angrylion `vi_integer_sqrt`), used to build the
200/// gamma curve. A restoring square-root: `res` accumulates the root two bits at a
201/// time from the top. Ledger R-5.
202const fn vi_integer_sqrt(a: u32) -> u32 {
203 let mut op = a;
204 let mut res = 0u32;
205 let mut one = 1u32 << 30;
206 while one > op {
207 one >>= 2;
208 }
209 while one != 0 {
210 if op >= res + one {
211 op -= res + one;
212 res += one << 1;
213 }
214 res >>= 1;
215 one >>= 2;
216 }
217 res
218}
219
220/// The VI AA-edge filter's penultimate min/max over a channel's gathered values
221/// (Angrylion `video_max_optimized`). Returns `(penumin, penumax)` — a specific
222/// single-pass "runner-up" min/max, *not* a plain second-smallest/largest: it tracks
223/// the current min/max position and its predecessor, then refines the runner-up with a
224/// second partial scan. Ported verbatim so the tie-handling matches. Ledger R-5.
225fn vi_video_max(pixels: &[u32]) -> (u32, u32) {
226 debug_assert!(
227 !pixels.is_empty(),
228 "vi_video_max needs at least the center pixel"
229 );
230 let n = pixels.len();
231 let (mut posmax, mut posmin) = (0usize, 0usize);
232 let (mut curpenmax, mut curpenmin) = (pixels[0], pixels[0]);
233 for i in 1..n {
234 if pixels[i] > pixels[posmax] {
235 curpenmax = pixels[posmax];
236 posmax = i;
237 } else if pixels[i] < pixels[posmin] {
238 curpenmin = pixels[posmin];
239 posmin = i;
240 }
241 }
242 if curpenmax != pixels[posmax] {
243 for &p in &pixels[posmax + 1..] {
244 if p > curpenmax {
245 curpenmax = p;
246 }
247 }
248 }
249 if curpenmin != pixels[posmin] {
250 for &p in &pixels[posmin + 1..] {
251 if p < curpenmin {
252 curpenmin = p;
253 }
254 }
255 }
256 (curpenmin, curpenmax)
257}
258
259/// The VI gamma curve for one channel: `sqrt(v << 6) << 1` (Angrylion `gamma_table`,
260/// `vi_gamma_init`). Applied when `gamma_enable` is set and `gamma_dither` is not
261/// (the dithered variants are noise-based and deferred). Ledger R-5.
262#[allow(clippy::cast_possible_truncation)]
263const fn vi_gamma(v: u8) -> u8 {
264 (vi_integer_sqrt((v as u32) << 6) << 1) as u8
265}
266
267/// The 256-entry VI gamma lookup table, built at compile time from [`vi_gamma`]
268/// (Angrylion's `gamma_table`) — a table lookup per channel on the scan-out path
269/// instead of recomputing the integer square root per pixel.
270const GAMMA_TABLE: [u8; 256] = {
271 let mut t = [0u8; 256];
272 let mut i = 0usize;
273 while i < 256 {
274 t[i] = vi_gamma(i as u8);
275 i += 1;
276 }
277 t
278};
279
280/// Base RDRAM size: 4 MiB (8 MiB with the Expansion Pak installed).
281pub const RDRAM_SIZE: usize = 8 * 1024 * 1024;
282
283/// Granularity of the RDRAM dirty-page map, in bytes.
284///
285/// 4 KiB gives 2,048 pages for 8 MiB of RDRAM — a 2 KiB flag array, L1-resident,
286/// so the once-a-frame scan is free. Finer pages would send less redundant data
287/// but grow the scan; coarser would send more. Not tuned: 4 KiB is also the
288/// alignment `VK_EXT_external_memory_host` wants, so the two agree by
289/// construction rather than by coincidence.
290pub const RDRAM_PAGE: usize = 4096;
291
292/// `log2(RDRAM_PAGE)`, so the hot-path mark is a shift rather than a divide.
293const RDRAM_PAGE_SHIFT: usize = 12;
294
295const _: () = assert!(1 << RDRAM_PAGE_SHIFT == RDRAM_PAGE);
296
297/// A dirty map with every page set, for construction **and for `serde`**.
298///
299/// All-dirty is the only correct starting state in both cases: at power-on the
300/// consumer has never seen this RDRAM, and loading a save-state replaces all of
301/// it at once. Coming back clean would present the previous state's framebuffer
302/// until something happened to overwrite it.
303#[cfg(feature = "rdp-tap")]
304fn all_pages_dirty() -> alloc::boxed::Box<[bool]> {
305 alloc::vec![true; RDRAM_SIZE.div_ceil(RDRAM_PAGE)].into_boxed_slice()
306}
307
308/// The RCP MIPS-interface (MI) interrupt lines.
309///
310/// Each bit, when set and unmasked (via [`RcpRegs::mi_mask`]), drives the VR4300
311/// IP2 interrupt. The mask register is implemented (`MI_MASK` set/clear pairs; it
312/// gates the IP2 line).
313#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
314pub struct MiInterrupt {
315 /// SP (RSP) interrupt.
316 pub sp: bool,
317 /// SI (serial / PIF) interrupt.
318 pub si: bool,
319 /// AI (audio-buffer-done) interrupt.
320 pub ai: bool,
321 /// VI (vertical-blank) interrupt.
322 pub vi: bool,
323 /// PI (peripheral DMA-done) interrupt.
324 pub pi: bool,
325 /// DP (RDP-done) interrupt.
326 pub dp: bool,
327}
328
329/// Pack the six interrupt lines into their register bit order.
330///
331/// `MI_INTERRUPT` and `MI_MASK` share it, which is why one packer serves both.
332const fn pack_mi(l: MiInterrupt) -> u32 {
333 (l.sp as u32)
334 | ((l.si as u32) << 1)
335 | ((l.ai as u32) << 2)
336 | ((l.vi as u32) << 3)
337 | ((l.pi as u32) << 4)
338 | ((l.dp as u32) << 5)
339}
340
341impl MiInterrupt {
342 /// `true` if any interrupt line is asserted.
343 #[must_use]
344 pub const fn any(self) -> bool {
345 self.sp || self.si || self.ai || self.vi || self.pi || self.dp
346 }
347}
348
349/// The RCP interface register state.
350///
351/// The SP / DP / VI / AI / PI / SI / RI / MI register blocks the CPU memory-maps
352/// in `$0400_0000..$04FF_FFFF`. Skeleton: each is a placeholder for its real
353/// register set (a roadmap phase).
354#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
355pub struct RcpRegs {
356 /// MI — MIPS interface (interrupt lines + mask + RCP version).
357 pub mi_intr: MiInterrupt,
358 /// MI interrupt mask (a line drives IP2 only when masked-in).
359 pub mi_mask: MiInterrupt,
360 /// `MI_MODE`'s storage bits (the repeat count and flags).
361 pub mi_mode: u32,
362 /// The RI (RDRAM controller) register file, `0x0470_0000..0x0470_0020`:
363 /// `RI_MODE`, `RI_CONFIG`, `RI_CURRENT_LOAD`, `RI_SELECT`, `RI_REFRESH`,
364 /// `RI_LATENCY`, `RI_ERROR`, `RI_BANK_STATUS` (N64brew *RDRAM Interface*
365 /// §Registers).
366 ///
367 /// Plain storage — writes stick and reads return them. That is enough for the
368 /// one thing that actually depends on it today: the cartridge's IPL3 reads
369 /// `RI_SELECT` and branches on whether RDRAM has already been brought up
370 /// (ledger R-18). The documented read *oddities* are deliberately NOT modeled
371 /// (see R-22): `RI_CURRENT_LOAD` is write-only on hardware and its read
372 /// returns a collection of bits from other registers, and `RI_ERROR` /
373 /// `RI_BANK_STATUS` reflect controller state rather than the last write.
374 /// Nothing exercises those yet and there is no oracle for them — the suite has
375 /// no RI group — so they stay honest storage rather than invented behavior.
376 pub ri: [u32; 8],
377 // The SP, DP (DPC), VI, AI, PI, SI, MI, and RI register blocks are all decoded
378 // (see the `is_*_register` methods + the read/write dispatch). Still undecoded:
379 // the RDRAM-config registers (`0x03F0_0000`) — the per-chip Rambus device
380 // registers, distinct from the RI controller block above. n64-systemtest has no
381 // RI/RDRAM-register group, so neither has a suite oracle; they are validated by
382 // commercial-boot progress (ledger R-18) instead.
383}
384
385/// Everything mutable lives here — the single owner.
386#[derive(Serialize, Deserialize)]
387pub struct Bus {
388 /// Main system RDRAM (boxed slice: 8 MiB, heap-allocated without a stack
389 /// temporary).
390 pub rdram: alloc::boxed::Box<[u8]>,
391 /// The RDRAM "hidden" bits: the 9th bit RDRAM carries per byte, used by the
392 /// RDP Z-buffer for the low 2 bits of each pixel's `dz`. Two bits per 16-bit
393 /// halfword, **bit-packed** four halfwords to a byte (`RDRAM_SIZE / 8` = 1 MiB
394 /// for 8 MiB of RDRAM). Lazily allocated — `None` until the first hidden write,
395 /// since only Z-buffered rendering touches it (reads return 0, matching the
396 /// power-on state).
397 pub rdram_hidden: Option<alloc::boxed::Box<[u8]>>,
398 /// Every RDP command word this Bus has fed to the RDP since the last drain
399 /// (ADR 0014's tap), appended whole-command in FIFO order.
400 ///
401 /// Exists so an out-of-core rasterizer can be handed the *same* stream the
402 /// software RDP consumed. It has to be a tap rather than a re-read of
403 /// RDRAM: by the time anything outside the core could look, `DPC_CURRENT`
404 /// has reached `DPC_END` and the game has usually overwritten the buffer.
405 ///
406 /// `#[serde(skip)]` on purpose, and that is not a shortcut. This is
407 /// per-frame scratch that the consumer drains, so it carries no state a
408 /// save-state needs; skipping it also keeps the snapshot layout **identical**
409 /// with the feature on or off, which is what keeps this out of ADR 0005's
410 /// announced-in-advance format-break territory.
411 ///
412 /// **Private.** The only supported access is [`Bus::take_rdp_commands`],
413 /// because the invariant worth protecting is that a consumer takes the whole
414 /// stream: a partial drain would replay this frame's tail on top of the next
415 /// frame's commands, and reordering would submit a command list the machine
416 /// never issued.
417 #[cfg(feature = "rdp-tap")]
418 #[serde(skip)]
419 rdp_tap: alloc::vec::Vec<u32>,
420 /// One flag per [`RDRAM_PAGE`]-sized page, set when anything writes RDRAM.
421 ///
422 /// Lets an out-of-core rasterizer upload only what changed instead of all
423 /// 8 MiB every frame — measured at ~2.1 ms of the ~2.4 ms a GPU frame costs.
424 ///
425 /// **`bool`, not a bitset, and that is the hot-path decision.** Marking is on
426 /// every RDRAM store, so it must be a single store; a bitset would make it a
427 /// read-modify-write. The whole array is 2 KiB for 8 MiB of RDRAM, so it sits
428 /// in L1 either way, and the once-a-frame scan is nothing against the copy it
429 /// replaces.
430 ///
431 /// `#[serde(skip)]` like the tap: it is per-frame scratch, and skipping keeps
432 /// the save-state layout identical with the feature on or off.
433 ///
434 /// **`default` is not optional here.** `#[serde(skip)]` alone fills the field
435 /// from `Default`, and `Box<[bool]>::default()` is **empty** — so the first
436 /// RDRAM store after loading a save-state would index page `off >> 12` into a
437 /// zero-length slice and panic. `rdp_tap` survives the same attribute only
438 /// because `Vec::default()` is empty *and pushable*; an indexed map is not.
439 #[cfg(feature = "rdp-tap")]
440 #[serde(skip, default = "all_pages_dirty")]
441 rdram_dirty: alloc::boxed::Box<[bool]>,
442 /// CPU-facing bus accesses this Bus has serviced (`work-counters`).
443 ///
444 /// Counted at the four leaves of the `CpuBus` surface — `read_u8`,
445 /// `read_u32`, `write_u8`, `write_u32`. `write_sized` is deliberately NOT
446 /// counted itself: it decomposes into those four, so counting it too would
447 /// double-count every store.
448 ///
449 /// **A `sd` costs two dispatches only OUTSIDE the RCP-internal range.**
450 /// `write_sized` splits a 64-bit store into two `write_u32` for RDRAM and
451 /// the external buses, but the RCP-internal path takes the high word and
452 /// **drops the second entirely** (see `write_sized`'s own comment there), so
453 /// a `sd` to an RCP register is one dispatch, not two. The pinned shape in
454 /// `the_cpu_bus_dispatch_shape_is_pinned` uses an RDRAM address and
455 /// therefore measures the two-dispatch case.
456 ///
457 /// Not a cycle counter and nothing schedules against it — the one exception
458 /// the derive-don't-increment rule allows is a retired-work tally, which is
459 /// what this is. `#[serde(skip)]`, so ADR 0005 is untouched.
460 #[cfg(feature = "work-counters")]
461 #[serde(skip)]
462 accesses: u64,
463 /// The PI DMA engine (T-14-001), pulled forward from Phase 5 because
464 /// n64-systemtest loads the rest of its own ELF through it.
465 pub pi: rustyn64_cart::pi::Pi,
466 // DMEM and IMEM are **not** here: the RSP owns them (`Bus::rsp`), and this
467 // Bus reaches them through `Rsp::mem_read`/`mem_write`. They were a separate
468 // `spmem` slice on the Bus while the RSP was a stub, which meant the CPU and
469 // the RSP addressed two different memories that happened to start equal.
470 /// The `ISViewer` buffer, as guest-visible memory.
471 isviewer: alloc::boxed::Box<[u8]>,
472 /// Text the guest has flushed through the `ISViewer` channel.
473 isviewer_out: alloc::vec::Vec<u8>,
474 /// Text the guest has pushed through the **EMUX** `xlog` channel.
475 ///
476 /// Kept separate from [`Bus::isviewer_output`] deliberately: they are two
477 /// independent console paths and n64-systemtest picks whichever the
478 /// emulator advertises, so merging them would hide which one is live.
479 emux_out: alloc::vec::Vec<u8>,
480 /// Set once the guest has issued `EMUX xioctl(EXIT)`.
481 emux_exited: bool,
482 /// Whether this host advertises the EMUX extensions. **Off by default**:
483 /// hardware has none, and offering them changes the guest's control flow.
484 emux_enabled: bool,
485 /// The value a PI direct-I/O write latched, visible to every PI-bus read
486 /// until the write finalizes. See [`Bus::pi_tick`].
487 pi_write_latch: u32,
488 /// RCP cycles remaining before the latched PI write finalizes. Zero is idle.
489 pi_write_countdown: u32,
490 /// `SI_DRAM_ADDR` — the RDRAM side of a PIF-RAM SI DMA.
491 si_dram_addr: u32,
492 /// Set when the PIF's real-PIF boot checksum verify fails: the real PIF
493 /// freezes the CPU via NMI until power-off (`PIF-NUS.md` §Console startup).
494 /// The scheduler stops stepping the CPU once this latches. Only the real-PIF
495 /// path can set it (a genuine ROM matches and never does); always `false`
496 /// under HLE and in normal operation.
497 boot_nmi_halt: bool,
498 /// The RSP coprocessor.
499 pub rsp: Rsp,
500 /// The RDP rasterizer.
501 pub rdp: Rdp,
502 /// The Video Interface register file (`0x0440_0000`). Scan-out and the
503 /// scheduler-driven scan position are follow-up VI tickets.
504 pub vi: Vi,
505 /// The Audio Interface.
506 pub audio: Audio,
507 /// The cartridge (PI/SI + saves).
508 pub cart: Cart,
509 /// The RCP interface register state.
510 pub rcp: RcpRegs,
511 /// Controller button/stick state, 4 ports (latched by the SI joybus).
512 pub controllers: [u32; 4],
513 /// Count of RCP chip-steps taken (diagnostic; used by the scheduler test).
514 rcp_steps: u64,
515}
516
517impl core::fmt::Debug for Bus {
518 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
519 f.debug_struct("Bus")
520 .field("rsp", &self.rsp)
521 .field("rdp", &self.rdp)
522 .field("audio", &self.audio)
523 .field("cart", &self.cart)
524 .field("rcp", &self.rcp)
525 .field("controllers", &self.controllers)
526 .finish_non_exhaustive()
527 }
528}
529
530impl Default for Bus {
531 fn default() -> Self {
532 Self {
533 // `vec![..].into_boxed_slice()` allocates straight on the heap —
534 // no 8 MiB stack temporary (which `Box::new([0; N])` would create).
535 pi: rustyn64_cart::pi::Pi::new(),
536 isviewer: alloc::vec![0u8; 0x20 + Self::ISVIEWER_LEN].into_boxed_slice(),
537 isviewer_out: alloc::vec::Vec::new(),
538 emux_out: alloc::vec::Vec::new(),
539 emux_exited: false,
540 emux_enabled: false,
541 pi_write_latch: 0,
542 pi_write_countdown: 0,
543 si_dram_addr: 0,
544 boot_nmi_halt: false,
545 rdram: alloc::vec![0u8; RDRAM_SIZE].into_boxed_slice(),
546 rdram_hidden: None,
547 #[cfg(feature = "rdp-tap")]
548 rdp_tap: alloc::vec::Vec::new(),
549 // Every page starts dirty: the consumer has never seen this RDRAM, so
550 // the first upload must be complete. Starting clean would hand the GPU
551 // an empty framebuffer and whatever the allocator left behind.
552 #[cfg(feature = "rdp-tap")]
553 rdram_dirty: all_pages_dirty(),
554 #[cfg(feature = "work-counters")]
555 accesses: 0,
556 rsp: Rsp::new(),
557 rdp: Rdp::new(),
558 vi: Vi::new(),
559 audio: Audio::new(),
560 cart: Cart::new(),
561 rcp: RcpRegs::default(),
562 controllers: [0; 4],
563 rcp_steps: 0,
564 }
565 }
566}
567
568impl Bus {
569 /// Construct at power-on.
570 #[must_use]
571 pub fn new() -> Self {
572 Self::default()
573 }
574
575 /// Advance the PI's asynchronous write by one RCP cycle.
576 ///
577 /// # Why a PI write is not immediate
578 ///
579 /// From N64brew *Memory map* (PI external bus):
580 ///
581 /// > All writes are performed **asynchronously** by the PI. Making a write
582 /// > in this area will in fact just cause the PI to latch the value
583 /// > internally, and release the VR4300 immediately. The write will then
584 /// > happen in background. [...] While a write is ongoing, further writes
585 /// > are ignored, and reads (from any address) return the 32-bit value that
586 /// > is being written.
587 ///
588 /// The PI does not know a device is read-only, so a write into ROM follows
589 /// the same path and is simply dropped by the ROM — which is why a value
590 /// written to cart ROM is briefly readable and then gone.
591 ///
592 /// # The duration is bounded by the oracle, not derived from hardware
593 ///
594 /// How long finalization takes depends on the PI domain timing registers
595 /// (`LAT`/`PWD`/`PGS`/`RLS`), which are not modeled. n64-systemtest bounds
596 /// it only *relatively*: the latched value must still be visible after 0
597 /// loop iterations and gone after 110. [`Bus::PI_WRITE_CYCLES`] sits inside
598 /// those bounds; it is **not** a hardware measurement. Accuracy ledger C-9.
599 pub const fn pi_tick(&mut self) {
600 if self.pi_write_countdown > 0 {
601 self.pi_write_countdown -= 1;
602 }
603 }
604
605 /// Is a PI direct-I/O write still in flight?
606 const fn pi_io_busy(&self) -> bool {
607 self.pi_write_countdown > 0
608 }
609
610 /// Step the RSP.
611 ///
612 /// The chip stays **in place**. It used to be moved out with
613 /// `core::mem::take` so that `Rsp::tick` could borrow the Bus, under a
614 /// comment asserting "No allocation" — which was false: `take` needs
615 /// `Default`, and constructing an `Rsp` allocates DMEM and IMEM, so every
616 /// RCP step allocated and freed 8 KiB. `Rsp::tick` now *returns* what it
617 /// wants done instead of borrowing its owner, so there is nothing to move.
618 pub fn rsp_tick(&mut self) {
619 let out = self.rsp.tick();
620 if let Some(raise) = out.interrupt_change {
621 self.rcp.mi_intr.sp = raise;
622 }
623 if let Some(dma) = out.dma {
624 self.sp_dma(dma);
625 }
626 if let Some((off, val)) = out.dp_write {
627 // The RSP's COP0 `c8`–`c15` *are* the RDP command registers; the RSP
628 // crate cannot name `Rdp` (crate-graph rule), so it reports the write
629 // as a DPC word offset and the Bus carries it out — the same seam the
630 // CPU uses at `0x0410_0000`. This is how the rdpq microcode's
631 // `mtc0 DP_END` submits a command list to the RDP.
632 self.rdp.dpc_write(u32::from(off), val);
633 }
634 self.rcp_steps = self.rcp_steps.wrapping_add(1);
635 }
636
637 /// Step the RDP against this bus's narrow [`VideoBus`] view (split-borrow).
638 ///
639 /// The `take` is how the RDP borrows its owner, and it is not free — it reads the
640 /// whole struct out, writes a fresh `Default` into the vacated slot, and the restore
641 /// overwrites that. It used to happen on **every RCP step**; the measurements are in
642 /// `docs/performance.md` §"The Bus split-borrow moves 1.35 GB a frame".
643 ///
644 /// So the step's bus-free half runs first. On most steps the RDP is frozen,
645 /// stalling, or looking at an empty command FIFO, and answers the whole step from
646 /// its own fields — in which case nothing is moved at all. The predicate lives in
647 /// [`rustyn64_rdp::Rdp::tick_without_bus`] beside the early-outs it encodes, not
648 /// here, so it cannot drift away from them, and it hands back a
649 /// [`rustyn64_rdp::NeedsBus`] token that the bus half requires — so the two cannot
650 /// be called out of order.
651 pub fn rdp_tick(&mut self) {
652 let Some(proof) = self.rdp.tick_without_bus() else {
653 return;
654 };
655 let mut rdp = core::mem::take(&mut self.rdp);
656 #[cfg(feature = "rdp-tap")]
657 let before = rdp.cmd_current;
658 rdp.tick_with_bus(proof, self);
659 // Capture what was actually consumed, by DIFFING the FIFO pointer rather
660 // than decoding the command again here. `tick_with_bus` already knows the
661 // opcode's length and already refuses a command that is only partly
662 // written; re-deriving either would be the same rule written twice, free
663 // to drift, and a tap that disagrees with the RDP about where one command
664 // ends is worse than no tap. An unconsumed step leaves the pointer put and
665 // captures nothing.
666 #[cfg(feature = "rdp-tap")]
667 {
668 // Iterate a COUNT, not `while addr < after`. `cmd_current` is masked
669 // to `DPC_ADDR_MASK` (0x00FF_FFF8) so it cannot reach the top of the
670 // address space today, and the comparison form would therefore never
671 // wrap — but "cannot happen because of a mask three files away" is a
672 // reason to write the loop that does not need the argument. A count
673 // terminates whatever the addresses are.
674 let words = rdp.cmd_current.wrapping_sub(before) / 4;
675 for i in 0..words {
676 self.rdp_tap
677 .push(self.rdram_read_u32(before.wrapping_add(i * 4)));
678 }
679 }
680 self.rdp = rdp;
681 }
682
683 /// Drain the RDP command tap, leaving it empty.
684 ///
685 /// The consumer is expected to call this once per frame. Nothing bounds the
686 /// buffer otherwise, and a consumer that stops draining would grow it without
687 /// limit — which is the caller's problem to have loudly rather than this
688 /// Bus's to hide by silently dropping the oldest commands.
689 #[cfg(feature = "rdp-tap")]
690 pub fn take_rdp_commands(&mut self) -> alloc::vec::Vec<u32> {
691 core::mem::take(&mut self.rdp_tap)
692 }
693
694 /// Mark the page containing byte offset `off` as written.
695 ///
696 /// Compiles to nothing without `rdp-tap`, so a default build pays no hot-path
697 /// cost at all — which is the only reason it is acceptable to call this from
698 /// every RDRAM store.
699 ///
700 /// `off` is an **RDRAM byte offset**, not an address: every caller obtains it
701 /// from [`Bus::rdram_offset`], which returns `Option<usize>` and yields
702 /// `Some` only below [`RDRAM_SIZE`]. So the index below cannot go out of
703 /// bounds, and the `debug_assert` says which fact that rests on rather than
704 /// leaving a reader to rediscover it. A clamp here would be worse than the
705 /// panic: it would silently mark the wrong page and stage the wrong bytes.
706 #[allow(
707 clippy::inline_always,
708 reason = "called from every RDRAM store; a call here would cost more than the mark"
709 )]
710 #[cfg_attr(
711 not(feature = "rdp-tap"),
712 allow(
713 clippy::unused_self,
714 clippy::missing_const_for_fn,
715 clippy::needless_pass_by_ref_mut,
716 reason = "the body is empty without the feature; the signature stays uniform so the six \
717 call sites need no cfg of their own"
718 )
719 )]
720 #[inline(always)]
721 fn mark_rdram_dirty(&mut self, off: usize) {
722 #[cfg(feature = "rdp-tap")]
723 {
724 debug_assert!(
725 off < RDRAM_SIZE,
726 "mark_rdram_dirty takes an RDRAM offset from rdram_offset, not an address"
727 );
728 self.rdram_dirty[off >> RDRAM_PAGE_SHIFT] = true;
729 }
730 #[cfg(not(feature = "rdp-tap"))]
731 {
732 let _ = off;
733 }
734 }
735
736 /// As [`Bus::mark_rdram_dirty`], for a write of `len` bytes that may straddle
737 /// a page boundary. The `u32` store does; the byte stores cannot.
738 #[allow(
739 clippy::inline_always,
740 reason = "called from the u32 store fast path; see mark_rdram_dirty"
741 )]
742 #[cfg_attr(
743 not(feature = "rdp-tap"),
744 allow(
745 clippy::unused_self,
746 clippy::missing_const_for_fn,
747 clippy::needless_pass_by_ref_mut,
748 reason = "the body is empty without the feature; the signature stays uniform so the six \
749 call sites need no cfg of their own"
750 )
751 )]
752 #[inline(always)]
753 fn mark_rdram_dirty_range(&mut self, off: usize, len: usize) {
754 #[cfg(feature = "rdp-tap")]
755 {
756 if len == 0 {
757 return;
758 }
759 // Saturating: `off + len` would wrap for an absurd `len`, and a
760 // debug-only panic in a marking helper is the worst of both worlds —
761 // it fires in tests and silently marks page 0 in release. Saturation
762 // clamps to the last page instead, which over-marks (costing one
763 // extra page of staging) rather than under-marking (costing a stale
764 // frame). Every real caller passes 1 or 4.
765 let first = off >> RDRAM_PAGE_SHIFT;
766 let last = off.saturating_add(len - 1) >> RDRAM_PAGE_SHIFT;
767 for p in first..=last.min(self.rdram_dirty.len() - 1) {
768 self.rdram_dirty[p] = true;
769 }
770 }
771 #[cfg(not(feature = "rdp-tap"))]
772 {
773 let _ = (off, len);
774 }
775 }
776
777 /// CPU-facing bus accesses serviced since power-on (`work-counters`).
778 ///
779 /// The unit that answers *did the Bus get slower, or does it run more?* — a
780 /// question a sampled profile share cannot answer, because it cannot
781 /// separate "this code got slower" from "this code ran more often" from
782 /// "the compiler charged it differently".
783 #[cfg(feature = "work-counters")]
784 #[must_use]
785 pub const fn accesses(&self) -> u64 {
786 self.accesses
787 }
788
789 /// Count one CPU-facing bus access.
790 ///
791 /// Compiles to nothing without the feature, which is why it is acceptable at
792 /// the four hottest entry points in the emulator.
793 #[cfg_attr(
794 not(feature = "work-counters"),
795 allow(
796 clippy::unused_self,
797 clippy::needless_pass_by_ref_mut,
798 reason = "the body is empty without the feature; the signature stays uniform so the \
799 call sites need no cfg of their own"
800 )
801 )]
802 #[allow(
803 clippy::inline_always,
804 reason = "called from every CPU bus access; a call here would cost more than the count"
805 )]
806 #[inline(always)]
807 const fn count_access(&mut self) {
808 #[cfg(feature = "work-counters")]
809 {
810 self.accesses = self.accesses.wrapping_add(1);
811 }
812 }
813
814 /// The per-page dirty flags, for a consumer staging RDRAM elsewhere.
815 #[cfg(feature = "rdp-tap")]
816 #[must_use]
817 pub fn rdram_dirty_pages(&self) -> &[bool] {
818 &self.rdram_dirty
819 }
820
821 /// Clear every dirty flag. The consumer calls this once it has staged them.
822 ///
823 /// Separate from [`Bus::rdram_dirty_pages`] on purpose: a consumer that
824 /// failed part-way through must be able to leave the flags set so the next
825 /// attempt re-sends what it missed, rather than losing the pages to a
826 /// read-and-clear it could not complete.
827 #[cfg(feature = "rdp-tap")]
828 pub fn clear_rdram_dirty(&mut self) {
829 self.rdram_dirty.fill(false);
830 }
831
832 /// How many command words are waiting in the tap.
833 ///
834 /// For observation only — a test proving the tap filled before checking that
835 /// something drained it needs to look without consuming, and
836 /// [`Bus::take_rdp_commands`] would make that check its own answer.
837 #[cfg(feature = "rdp-tap")]
838 #[must_use]
839 pub const fn rdp_tap_len(&self) -> usize {
840 self.rdp_tap.len()
841 }
842
843 /// Step the AI against this bus's narrow [`AudioBus`] view (split-borrow),
844 /// advancing the DAC to `master_ticks` so sample emission is derived from
845 /// the one canonical clock (ADR 0006) rather than an independent counter.
846 /// The move is skipped on the steps that emit nothing, which at a typical
847 /// ~32 kHz is about 1,949 of every 1,950: the AI is asked first, and only a
848 /// `NeedsBus` buys the `take`. Same shape as [`Bus::rdp_tick`] and for the
849 /// same reason — the borrow cannot be arranged without moving the chip out,
850 /// so the decision has to happen before it.
851 ///
852 /// The bus-free half still runs every step and still mutates (it stamps
853 /// `last_tick` and anchors the first sample), so this skips the *move*, never
854 /// the step.
855 pub fn audio_tick(&mut self, master_ticks: u64) {
856 let Some(proof) = self.audio.tick_without_bus(master_ticks) else {
857 return;
858 };
859 let mut audio = core::mem::take(&mut self.audio);
860 audio.tick_with_bus(proof, self);
861 self.audio = audio;
862 }
863
864 /// Drain the stereo stream the AI has emitted since the last drain — the
865 /// frontend pushes it into the host ring and resamples (ADR 0004).
866 pub fn drain_audio_samples(&mut self) -> alloc::vec::Vec<StereoSample> {
867 self.audio.drain()
868 }
869
870 /// Diagnostic: count of RCP-chip steps taken (RSP ticks). The scheduler's
871 /// fractional-divisor test reads this to assert the 3:2 ratio.
872 #[must_use]
873 pub const fn rcp_steps_for_test(&self) -> u64 {
874 self.rcp_steps
875 }
876
877 /// Map a CPU physical address into RDRAM (`0..RDRAM_SIZE`), or `None` if it
878 /// targets a memory-mapped register region instead.
879 /// Base of RSP DMEM. IMEM follows at `+0x1000`.
880 pub const SPMEM_BASE: u32 = 0x0400_0000;
881
882 /// RCP cycles a PI direct-I/O write stays latched before finalizing.
883 ///
884 /// **This number is fitted, not measured.** Hardware finalization depends on
885 /// the PI domain timing registers (`LAT`/`PWD`/`PGS`/`RLS`), which are not
886 /// modeled; n64-systemtest bounds the latch only relatively (visible after
887 /// 0 decay-loop iterations, gone after 110). 100 was the best of the values
888 /// tried against the suite.
889 ///
890 /// Treat that provenance as a warning, not a credential. The suite still
891 /// fails `Write32, Read32 (same location)` on its **second** read, where
892 /// hardware has finalized and we have not — a gap no single constant closes,
893 /// because the real duration is not constant. Modeling the domain registers
894 /// is the actual fix. Accuracy ledger C-9.
895 pub const PI_WRITE_CYCLES: u32 = 100;
896
897 /// Base of the **MI** register block (`0x0430_0000`).
898 pub const MI_BASE: u32 = 0x0430_0000;
899
900 /// `MI_VERSION`, the value *"most consoles report"*.
901 ///
902 /// Packed `RSP:RDP:RAC:IO`. Other values exist in the wild — `0x0101_0101`
903 /// and `0x0201_0202` appear in emulators and docs, iQue reports
904 /// `0x0202_b0b0` — so this is a **choice among documented observations**,
905 /// not a derived constant. Retail NTSC hardware is what this emulator
906 /// models, so it reports what retail hardware reports.
907 pub const MI_VERSION_VALUE: u32 = 0x0202_0102;
908
909 /// Is this address in the MI register block?
910 ///
911 /// The block is four registers, and *"accesses beyond `0x0430 0010` are
912 /// mirrored, so only the least significant four bits are taken into account
913 /// for address decoding"*. The window itself runs to `0x0440_0000`, where
914 /// the VI begins.
915 const fn is_mi_register(addr: u32) -> bool {
916 addr >= Self::MI_BASE && addr < 0x0440_0000
917 }
918
919 /// Read an MI register, after the 4-bit mirroring.
920 const fn mi_read(&self, addr: u32) -> u32 {
921 let i = self.rcp.mi_intr;
922 let m = self.rcp.mi_mask;
923 match (addr >> 2) & 3 {
924 0 => self.rcp.mi_mode,
925 1 => Self::MI_VERSION_VALUE,
926 2 => pack_mi(i),
927 _ => pack_mi(m),
928 }
929 }
930
931 /// Write an MI register, after the 4-bit mirroring.
932 fn mi_write(&mut self, addr: u32, val: u32) {
933 match (addr >> 2) & 3 {
934 0 => {
935 // Only the bits that are storage are kept. `ClearDP` (bit 11)
936 // is an action rather than a mode, and the repeat/EBus/Upper
937 // modes are RDRAM-transfer behavior this emulator does not
938 // model -- see the note in `docs/rsp.md`.
939 self.rcp.mi_mode = (self.rcp.mi_mode & !0x7F) | (val & 0x7F);
940 if val & (1 << 11) != 0 {
941 self.rcp.mi_intr.dp = false;
942 }
943 }
944 // MI_VERSION is read-only, and MI_INTERRUPT is driven by the
945 // devices -- a write to either does nothing.
946 1 | 2 => {}
947 _ => {
948 // The mask uses clear/set pairs at `2n` / `2n + 1`, in the same
949 // device order as the read layout. Unlike `SP_STATUS`, the wiki
950 // does not state what both-bits-at-once does here, so this
951 // applies clear before set rather than inventing a rule; if a
952 // test ever pins it, it belongs in the ledger.
953 let mut m = self.rcp.mi_mask;
954 for (bit, line) in [(0u32, 0u32), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)] {
955 let clear = val & (1 << (bit * 2)) != 0;
956 let set = val & (1 << (bit * 2 + 1)) != 0;
957 let slot = match line {
958 0 => &mut m.sp,
959 1 => &mut m.si,
960 2 => &mut m.ai,
961 3 => &mut m.vi,
962 4 => &mut m.pi,
963 _ => &mut m.dp,
964 };
965 if clear {
966 *slot = false;
967 }
968 if set {
969 *slot = true;
970 }
971 }
972 self.rcp.mi_mask = m;
973 }
974 }
975 }
976
977 /// Base of the eight SP interface registers (`0x0404_0000`).
978 pub const SP_REGS_BASE: u32 = 0x0404_0000;
979 /// `SP_STATUS` (`0x0404_0010`), named because tests reach for it directly.
980 pub const SP_STATUS: u32 = 0x0404_0010;
981 /// `SP_PC` (`0x0408_0000`) — in its own window, not with the other eight.
982 pub const SP_PC: u32 = 0x0408_0000;
983
984 /// Is this one of the eight SP interface registers?
985 const fn is_sp_register(addr: u32) -> bool {
986 addr >= Self::SP_REGS_BASE && addr < Self::SP_REGS_BASE + 0x20
987 }
988
989 /// Base of the DP command registers (`0x0410_0000`): START, END, CURRENT,
990 /// STATUS, then the (unmodeled) CLOCK/BUSY/PIPE/TMEM counters.
991 pub const DP_REGS_BASE: u32 = 0x0410_0000;
992
993 /// Is this one of the eight DP command (`DPC_*`) registers?
994 const fn is_dp_register(addr: u32) -> bool {
995 addr >= Self::DP_REGS_BASE && addr < Self::DP_REGS_BASE + 0x20
996 }
997
998 /// Base of the VI register block (`0x0440_0000`); the AI follows at
999 /// `0x0450_0000`.
1000 pub const VI_REGS_BASE: u32 = 0x0440_0000;
1001
1002 /// Is this address in the VI register block? The sixteen registers span
1003 /// `0x0440_0000..0x0440_0040`; the rest of the `0x044x_xxxx` window mirrors
1004 /// them (four-bit decode), which the word-offset mask in [`Vi::read`]
1005 /// handles.
1006 const fn is_vi_register(addr: u32) -> bool {
1007 addr >= Self::VI_REGS_BASE && addr < 0x0450_0000
1008 }
1009
1010 /// Base of the AI register block (`0x0450_0000`); the SI/RI follow above.
1011 pub const AI_REGS_BASE: u32 = 0x0450_0000;
1012
1013 /// Is this address in the AI register block? Six registers span
1014 /// `0x0450_0000..0x0450_0018`; the rest of the `0x045x_xxxx` window mirrors
1015 /// them (the three-bit decode `(addr >> 2) & 7` in [`Audio::read_reg`]).
1016 const fn is_ai_register(addr: u32) -> bool {
1017 addr >= Self::AI_REGS_BASE && addr < 0x0460_0000
1018 }
1019
1020 /// Write an AI register, applying its interrupt effect to the MI: enqueuing
1021 /// the first buffer raises `MI_INTR.ai`; a write to `AI_STATUS` lowers it.
1022 fn ai_write(&mut self, addr: u32, val: u32) {
1023 match self.audio.write_reg((addr >> 2) & 7, val) {
1024 AiIrq::Raise => self.rcp.mi_intr.ai = true,
1025 AiIrq::Lower => self.rcp.mi_intr.ai = false,
1026 AiIrq::None => {}
1027 }
1028 }
1029
1030 /// Base of the RI (RDRAM controller) register block.
1031 ///
1032 /// `0x0470_0000`, holding the eight registers N64brew *RDRAM Interface*
1033 /// §Registers enumerates: `RI_MODE`, `RI_CONFIG`, `RI_CURRENT_LOAD`,
1034 /// `RI_SELECT`, `RI_REFRESH`, `RI_LATENCY`, `RI_ERROR`, `RI_BANK_STATUS`.
1035 pub const RI_BASE: u32 = 0x0470_0000;
1036
1037 /// Is this address in the RI register block? Eight registers span
1038 /// `0x0470_0000..0x0470_0020`; the rest of the `0x047x_xxxx` window mirrors
1039 /// them via the three-bit decode `(addr >> 2) & 7`, as the other RCP blocks do.
1040 const fn is_ri_register(addr: u32) -> bool {
1041 addr >= Self::RI_BASE && addr < Self::SI_BASE
1042 }
1043
1044 /// Base of the SI register block (`0x0480_0000`).
1045 pub const SI_BASE: u32 = 0x0480_0000;
1046
1047 /// Is this address in the SI register block (`0x0480_0000..0x0490_0000`)?
1048 const fn is_si_register(addr: u32) -> bool {
1049 addr >= Self::SI_BASE && addr < 0x0490_0000
1050 }
1051
1052 /// Base of the PIF address space — the PIF **boot ROM** (IPL1/IPL2) window
1053 /// `0x1FC0_0000..0x1FC0_07C0`, mapped only during the real-PIF boot; under HLE
1054 /// no ROM is installed and it reads back 0 (the prior behavior).
1055 const PIF_ROM_BASE: u32 = 0x1FC0_0000;
1056
1057 /// The 64-byte PIF RAM window (`0x1FC0_07C0..0x1FC0_0800`) — the tail of the
1058 /// PIF address space, where the CPU reads/writes the joybus command block.
1059 const PIF_RAM_BASE: u32 = 0x1FC0_07C0;
1060
1061 /// Is this a PIF-block address (`0x1FC0_0000..0x1FC0_0800` — ROM then RAM)?
1062 const fn is_pif(addr: u32) -> bool {
1063 addr >= 0x1FC0_0000 && addr < 0x1FC0_0800
1064 }
1065
1066 /// PIF-RAM command-byte offset (the last byte, `0x3F`).
1067 const PIF_CMD_BYTE: usize = 0x3F;
1068
1069 /// Let the PIF act on a reset-mode command when the CPU write touched the
1070 /// command byte (real-PIF boot only; a no-op under HLE and in run mode).
1071 /// Latches the NMI freeze if IPL2's checksum verify fails.
1072 fn pif_boot_command_if_cmd(&mut self, off: usize) {
1073 if off == Self::PIF_CMD_BYTE && self.cart.pif_boot_command() {
1074 self.boot_nmi_halt = true;
1075 }
1076 }
1077
1078 /// Has the PIF frozen the CPU via NMI after a failed real-PIF boot checksum?
1079 /// Always `false` under HLE, in run mode, and for a genuine ROM.
1080 #[must_use]
1081 pub const fn boot_nmi_halt(&self) -> bool {
1082 self.boot_nmi_halt
1083 }
1084
1085 /// Warm-reset the real-PIF boot latches so a reset restarts IPL1→IPL2: clear
1086 /// the NMI freeze and unlock the PIF ROM (`PIF-NUS.md` §Console Reset). No-op
1087 /// under HLE (nothing is latched). The CPU's reset vector is restored by
1088 /// [`crate::System::reset`], which recreates the CPU at `0xBFC0_0000`.
1089 pub const fn reset_boot_latches(&mut self) {
1090 self.boot_nmi_halt = false;
1091 self.cart.pif_reset_boot();
1092 }
1093
1094 /// Read an SI register (`SI_DRAM_ADDR` / `SI_STATUS`; the PIF-address
1095 /// registers are write-triggers and read back as 0).
1096 const fn si_read(&self, addr: u32) -> u32 {
1097 match (addr - Self::SI_BASE) & 0x1C {
1098 0x00 => self.si_dram_addr,
1099 // SI_STATUS at +0x18: DMA/IO idle (instant DMA); bit 12 = interrupt.
1100 0x18 => (self.rcp.mi_intr.si as u32) << 12,
1101 _ => 0,
1102 }
1103 }
1104
1105 /// Write an SI register. `SI_DRAM_ADDR` latches the RDRAM side; the
1106 /// `PIF_AD_RD64B`/`WR64B` registers trigger the 64-byte PIF DMA; a write to
1107 /// `SI_STATUS` acknowledges (clears) the SI interrupt.
1108 fn si_write(&mut self, addr: u32, val: u32) {
1109 match (addr - Self::SI_BASE) & 0x1C {
1110 0x00 => self.si_dram_addr = val & 0x00FF_FFFF,
1111 // RD64B (+0x04): the PIF executes the joybus frame, then DMAs PIF
1112 // RAM → RDRAM. This is the read that actually runs the handshakes.
1113 0x04 => {
1114 self.cart.pif_execute(&self.controllers);
1115 let ram = *self.cart.pif_ram();
1116 for (i, &b) in ram.iter().enumerate() {
1117 if let Some(off) = Self::rdram_offset(self.si_dram_addr.wrapping_add(i as u32))
1118 {
1119 self.rdram[off] = b;
1120 self.mark_rdram_dirty(off);
1121 }
1122 }
1123 self.rcp.mi_intr.si = true;
1124 }
1125 // WR64B (+0x10): DMA RDRAM → PIF RAM, then the PIF parses (on the
1126 // command-byte write inside `pif_load`/execute).
1127 0x10 => {
1128 let mut ram = [0u8; rustyn64_cart::pif::PIF_RAM_LEN];
1129 for (i, b) in ram.iter_mut().enumerate() {
1130 *b = Self::rdram_offset(self.si_dram_addr.wrapping_add(i as u32))
1131 .map_or(0, |off| self.rdram[off]);
1132 }
1133 self.cart.pif_load(&ram);
1134 self.rcp.mi_intr.si = true;
1135 }
1136 // SI_STATUS write acknowledges the interrupt.
1137 0x18 => self.rcp.mi_intr.si = false,
1138 _ => {}
1139 }
1140 }
1141
1142 /// Write a VI register; a write to `VI_V_CURRENT` acknowledges the VI
1143 /// interrupt (`MI_INTR.vi = false`).
1144 const fn vi_write(&mut self, addr: u32, val: u32) {
1145 if self.vi.write(addr >> 2, val) {
1146 self.rcp.mi_intr.vi = false;
1147 }
1148 }
1149
1150 /// Scan the framebuffer out into `out` as RGBA8, returning the active
1151 /// `(width, height)` — the presentable frame the VI would send to the DAC.
1152 ///
1153 /// Reads `VI_ORIGIN`/`VI_WIDTH`/`VI_CTRL` and derives the height from the
1154 /// active region `VI_V_VIDEO` (`(V_END − V_START)` half-lines → lines).
1155 /// Pixel formats (`VI_CTRL.TYPE`): 2 = 16-bit RGBA5551 (each 5-bit channel
1156 /// expanded to 8, the 1-bit alpha to 0/255), 3 = 32-bit RGBA8888 (a direct
1157 /// copy). `TYPE` 0/1 is blank — returns `(0, 0)` and writes nothing, the
1158 /// caller keeps a black frame.
1159 ///
1160 /// Returns `(0, 0)` and writes nothing when the VI is blanked, the width or
1161 /// height is zero, or `out` is smaller than `width * height * 4` — a caller
1162 /// that gets a non-zero size can trust the whole frame was written.
1163 ///
1164 /// **Scope:** a 1:1 scan (no `VI_X_SCALE`/`VI_Y_SCALE` resampling) and no
1165 /// AA/divot/de-dither post-filter — those are later VI work, recorded as
1166 /// open residual R-5 in `docs/accuracy-ledger.md`. Byte-exact for the direct
1167 /// framebuffer copy, which is what the FILL pipeline produces.
1168 #[must_use]
1169 pub fn scanout(&self, out: &mut [u8]) -> (u32, u32) {
1170 let bpp = match self.vi.read(vi::VI_CTRL) & 0x3 {
1171 2 => 2u32, // 16-bit RGBA5551
1172 3 => 4, // 32-bit RGBA8888
1173 _ => return (0, 0),
1174 };
1175 let origin = self.vi.read(vi::VI_ORIGIN) & 0x00FF_FFFF;
1176 let width = self.vi.read(vi::VI_WIDTH) & 0xFFF;
1177 let v_video = self.vi.read(vi::VI_V_VIDEO);
1178 let height = ((v_video & 0x3FF).saturating_sub((v_video >> 16) & 0x3FF)) / 2;
1179 if width == 0 || height == 0 {
1180 return (0, 0);
1181 }
1182 // Refuse an undersized destination up front rather than write a
1183 // truncated frame and claim full dimensions; this also keeps the
1184 // per-pixel loop bounds-check-free.
1185 if out.len() < (width as usize) * (height as usize) * 4 {
1186 return (0, 0);
1187 }
1188 let stride = width * bpp;
1189 for y in 0..height {
1190 for x in 0..width {
1191 let src = origin.wrapping_add(y * stride).wrapping_add(x * bpp);
1192 let dst = ((y * width + x) * 4) as usize;
1193 if bpp == 2 {
1194 let px = (u16::from(self.rdram_read(src)) << 8)
1195 | u16::from(self.rdram_read(src.wrapping_add(1)));
1196 out[dst] = expand5(((px >> 11) & 0x1F) as u8);
1197 out[dst + 1] = expand5(((px >> 6) & 0x1F) as u8);
1198 out[dst + 2] = expand5(((px >> 1) & 0x1F) as u8);
1199 out[dst + 3] = if px & 1 == 1 { 0xFF } else { 0 };
1200 } else {
1201 // 32-bit RGBA8888 is a direct big-endian copy.
1202 out[dst..dst + 4].copy_from_slice(&self.rdram_read_u32(src).to_be_bytes());
1203 }
1204 }
1205 }
1206 (width, height)
1207 }
1208
1209 /// Hardware-accurate VI scan-out with `VI_X_SCALE`/`VI_Y_SCALE` resampling and
1210 /// the real active-span/overscan geometry (ledger **R-5**, gap-analysis Stage D).
1211 ///
1212 /// This is the accurate replacement for [`Bus::scanout`]'s 1:1 copy, built up a
1213 /// slice at a time and validated RGB byte-for-byte against Angrylion's VI pipeline
1214 /// (`vi_process_full`) through the `.vivec` conformance vectors. Implemented so far:
1215 /// the geometry — the 2.10 fixed-point accumulator (`line_x = x_offs >> 10`, source
1216 /// index `stride*srcY + srcX`), the NTSC/PAL horizontal overscan (`h_start -= 108`
1217 /// / `128`), the 8/7-px `minhpass`/`maxhpass` crop, the `PRESCALE_WIDTH`/`HEIGHT`
1218 /// clamp, and the truncating RGBA5551→8 conversion the VI uses (`(px >> 8) & 0xF8`,
1219 /// not `expand5`'s replicating widening); the 5-bit **bilinear lerp** for **both**
1220 /// 16- and 32-bit sources (`aa_mode != REPLICATE` and a non-zero fraction, nearest
1221 /// otherwise); the **gamma** curve; and, under `aa_mode` 0/1 for **both** source
1222 /// formats, the coverage-gated **de-dither** (`cvg == 7`) and **AA-edge**
1223 /// (`cvg < 7`) filters and the **divot** median (`divot_enable`) — 16-bit reads
1224 /// coverage from the hidden-bits plane, 32-bit from the alpha byte
1225 /// (`vi_read_cov`). Alpha is `0xFF` (opaque) for display; the VI carries
1226 /// coverage in its output alpha, which the harness compares as RGB-only.
1227 ///
1228 /// Still to come (later slices, still substituted here): the gamma-dither
1229 /// variants, the coverage filters under `aa_mode == 2` (`RESAMP_ONLY` forces
1230 /// `cvg = 7`, so de-dither can still apply — currently gated to `aa_mode ≤ 1`),
1231 /// and the remaining R-6 field timing (interlace / serrate and the exact
1232 /// `H_TOTAL`; the PAL 50 Hz field rate itself is handled by `Vi::field_hz`, which
1233 /// drives the same `ispal` region split this geometry uses).
1234 ///
1235 /// **This is the live presented path.** The frontend calls it directly
1236 /// (`rustyn64_frontend::emu::Emu::produce_frame`), so what a user sees is this
1237 /// function's output, not [`Bus::scanout`]'s 1:1 copy. `Bus::scanout` is
1238 /// retained as the simpler unscaled reference the R-5 vectors are compared
1239 /// against and as the geometry contrast in the frontend's own tests.
1240 ///
1241 /// Returns `(0, 0)` (writing nothing) when the VI is blanked (`TYPE` 0/1), the
1242 /// computed width/height is non-positive, or `out` is too small.
1243 #[must_use]
1244 #[allow(
1245 clippy::cast_sign_loss,
1246 clippy::cast_possible_truncation,
1247 clippy::cast_possible_wrap,
1248 clippy::useless_let_if_seq,
1249 clippy::too_many_lines
1250 )]
1251 pub fn scanout_scaled(&self, out: &mut [u8]) -> (u32, u32) {
1252 // The DAC prescale buffer bounds (Angrylion `PRESCALE_WIDTH`/`HEIGHT`).
1253 const PRESCALE_W: i32 = 640;
1254 const PRESCALE_H: i32 = 625;
1255 let ctrl = self.vi.read(vi::VI_CTRL);
1256 let bpp = match ctrl & 0x3 {
1257 2 => 2u32, // 16-bit RGBA5551
1258 3 => 4, // 32-bit RGBA8888
1259 _ => return (0, 0),
1260 };
1261 // aa_mode (VI_CTRL bits 9:8): 3 = REPLICATE (nearest); anything else enables
1262 // the bilinear resample when a fraction is non-zero. aa_mode 0/1 additionally
1263 // reads real coverage and runs the de-dither / AA-edge filters (32-bit path).
1264 let aa_mode = (ctrl >> 8) & 0x3;
1265 // Gamma (VI_CTRL bit 3) applies the sqrt curve to the final RGB; the dithered
1266 // variants (bit 2 set) are noise-based and deferred, so plain gamma is applied
1267 // only when gamma_enable is set and gamma_dither is not (bit 3 set, bit 2 clear).
1268 let gamma = (ctrl & 0x0C) == 0x08;
1269 // The de-dither / AA-edge coverage path (aa_mode 0/1); `dither_filter`
1270 // (VI_CTRL bit 16) enables the de-dither restore on fully-covered pixels, and
1271 // `divot` (VI_CTRL bit 4) the 3-tap median on partial-coverage edges.
1272 let dither_filter = (ctrl >> 16) & 1 != 0;
1273 let divot = (ctrl >> 4) & 1 != 0;
1274 let origin = self.vi.read(vi::VI_ORIGIN) & 0x00FF_FFFF;
1275 let src_stride = (self.vi.read(vi::VI_WIDTH) & 0xFFF) as i32; // source pixels/row
1276 let h_video = self.vi.read(vi::VI_H_VIDEO);
1277 let v_video = self.vi.read(vi::VI_V_VIDEO);
1278 let x_scale = self.vi.read(vi::VI_X_SCALE);
1279 let y_scale = self.vi.read(vi::VI_Y_SCALE);
1280 let v_sync = self.vi.read(vi::VI_V_TOTAL) & 0x3FF;
1281
1282 // Register decode (Angrylion `n64video_update_screen`; 2.10 fixed point).
1283 let h_start_raw = ((h_video >> 16) & 0x3FF) as i32;
1284 let h_end = (h_video & 0x3FF) as i32;
1285 let v_start_raw = ((v_video >> 16) & 0x3FF) as i32;
1286 let v_end = (v_video & 0x3FF) as i32;
1287 let mut hres = h_end - h_start_raw;
1288 let mut vres = (v_end - v_start_raw) >> 1;
1289 let x_add = (x_scale & 0xFFF) as i32;
1290 let mut x_start = ((x_scale >> 16) & 0xFFF) as i32;
1291 let y_add = (y_scale & 0xFFF) as i32;
1292 let mut y_start = ((y_scale >> 16) & 0xFFF) as i32;
1293
1294 // Active-span adjust: NTSC/PAL horizontal overscan, then left/top clamps that
1295 // fold the cropped offset back into the scale accumulator start.
1296 let ispal = v_sync > vi::VI_PAL_V_TOTAL_THRESHOLD;
1297 let mut h_start = h_start_raw - if ispal { 128 } else { 108 };
1298 let mut h_start_clamped = false;
1299 if h_start < 0 {
1300 x_start += x_add * (-h_start);
1301 hres += h_start;
1302 h_start = 0;
1303 h_start_clamped = true;
1304 }
1305 let vstartoffset = if ispal { 44 } else { 34 };
1306 let mut v_start = (v_start_raw - vstartoffset) / 2;
1307 if v_start < 0 {
1308 y_start += y_add * (-v_start);
1309 v_start = 0;
1310 }
1311 let mut hres_clamped = false;
1312 if hres + h_start > PRESCALE_W {
1313 hres = PRESCALE_W - h_start;
1314 hres_clamped = true;
1315 }
1316 if vres + v_start > PRESCALE_H {
1317 vres = PRESCALE_H - v_start;
1318 }
1319 // Horizontal overscan crop; vertical is handled by the `v_start` origin.
1320 let minhpass = if h_start_clamped { 0 } else { 8 };
1321 let maxhpass = if hres_clamped { hres } else { hres - 7 };
1322 // Interlace/serrate (`VI_CTRL` bit 6) is deferred to R-6: this slice models
1323 // only the progressive field, so the height is `vres`. Angrylion doubles it
1324 // (`vres << serrate`) and doubles the source walk per field — modeling only
1325 // the height doubling here would fabricate a half-rate double-height frame,
1326 // which is worse than not modeling interlace at all, so serrate is ignored
1327 // until R-6 lands the field cadence and a vector for it.
1328 let width = (maxhpass - minhpass).max(0);
1329 let height = vres.max(0);
1330 if width == 0 || height == 0 {
1331 return (0, 0);
1332 }
1333 let (w, h) = (width as u32, height as u32);
1334 if out.len() < (w as usize) * (h as usize) * 4 {
1335 return (0, 0);
1336 }
1337
1338 // The source columns the walk below can ask for. `x_add` is unsigned, so `sx`
1339 // is monotonically non-decreasing in `ox`: the first and last output pixels
1340 // bracket it, and the `+ 1` covers the far bilinear column. Derived from the
1341 // loop bounds rather than guessed, because a memo whose range is short by one
1342 // silently falls back to the uncached path and reads as "the optimization did
1343 // not help".
1344 // `i64` because every term is guest-controlled through VI MMIO: `x_start` and
1345 // `x_add` come from `VI_X_SCALE`, and `width` from the `VI_H_VIDEO` span. The
1346 // product is small in practice, but "small in practice" is not a property that
1347 // survives a clamp being relaxed, and the failure mode would be a debug-build
1348 // overflow panic in the scan-out.
1349 let x_span_end =
1350 i64::from(x_start) + (i64::from(minhpass) + i64::from(width) - 1) * i64::from(x_add);
1351 let x_first =
1352 i32::try_from((i64::from(x_start) + i64::from(minhpass) * i64::from(x_add)) >> 10)
1353 .unwrap_or(0);
1354 let x_last = i32::try_from((x_span_end >> 10) + 1).unwrap_or(x_first);
1355 let mut sampler = ViSampler::new(
1356 ViCfg {
1357 origin,
1358 src_stride,
1359 bpp,
1360 aa_mode,
1361 divot,
1362 dither_filter,
1363 },
1364 x_first,
1365 x_last,
1366 );
1367
1368 for oy in 0..height {
1369 let curry = y_start + oy * y_add;
1370 let sy = curry >> 10;
1371 let yfrac = (curry >> 5) & 0x1F;
1372 for ox in 0..width {
1373 let x_offs = x_start + (minhpass + ox) * x_add;
1374 let sx = x_offs >> 10;
1375 let xfrac = (x_offs >> 5) & 0x1F;
1376 let dst = ((oy * width + ox) * 4) as usize;
1377 // Bilinear when aa_mode isn't REPLICATE and a fraction is non-zero
1378 // (Angrylion `lerping`): four texels, vertical lerp per column then
1379 // horizontal between them. Otherwise the exact nearest sample.
1380 //
1381 // Both zero-weight cases are skipped rather than computed and
1382 // multiplied by zero — `xfrac == 0` here and `yfrac == 0` inside
1383 // [`Bus::vi_column`]. Under the configuration Super Mario 64 programs
1384 // (`x_add` 512, so `xfrac` alternates 0 / 16; `y_add` 1024, so `yfrac`
1385 // is always 0) that is the difference between 2.5 filter chains per
1386 // output pixel and 1.5.
1387 let mut rgb = if aa_mode != 3 && (xfrac != 0 || yfrac != 0) {
1388 let col = self.vi_column(&mut sampler, sx, sy, yfrac);
1389 if xfrac == 0 {
1390 col
1391 } else {
1392 let ncol = self.vi_column(&mut sampler, sx + 1, sy, yfrac);
1393 vi_lerp3(col, ncol, xfrac)
1394 }
1395 } else {
1396 self.vi_sample(&mut sampler, sx, sy)
1397 };
1398 // Gamma is the final RGB stage (after scale, before write) — a table
1399 // lookup per channel (the LUT is `vi_gamma` precomputed).
1400 if gamma {
1401 rgb = rgb.map(|c| GAMMA_TABLE[usize::from(c)]);
1402 }
1403 out[dst..dst + 3].copy_from_slice(&rgb);
1404 out[dst + 3] = 0xFF; // opaque display alpha (VI coverage is not shown)
1405 }
1406 }
1407 (w, h)
1408 }
1409
1410 /// One source pixel as the scan-out wants it — filtered under `aa_mode` 0/1,
1411 /// plain under 2/3 — served from the memo when it is there.
1412 ///
1413 /// Only the filtered path is memoized. Under `aa_mode` 2/3 a sample is two RDRAM
1414 /// reads and a format convert, which is cheaper than the row bookkeeping, so
1415 /// caching it would be a pessimization dressed as an optimization. The filters
1416 /// themselves have exactly one implementation either way; this chooses whether to
1417 /// consult a cache before calling it.
1418 fn vi_sample(&self, s: &mut ViSampler, x: i32, y: i32) -> [u8; 3] {
1419 if s.cfg.aa_mode > 1 {
1420 return self.vi_sample_direct(s, x, y);
1421 }
1422 // `checked_sub` rather than `-`: both operands trace back to guest-controlled
1423 // VI registers, and the miss path is a fall-through, not an error — so an
1424 // extreme pair should decline the memo, not panic a debug build.
1425 let Some(idx) = x
1426 .checked_sub(s.x_lo)
1427 .and_then(|offset| usize::try_from(offset).ok())
1428 else {
1429 return self.vi_sample_direct(s, x, y);
1430 };
1431 if idx >= s.span {
1432 return self.vi_sample_direct(s, x, y);
1433 }
1434 let cell = s.row_slot(y) * s.span + idx;
1435 if let Some(hit) = s.cells[cell] {
1436 return hit;
1437 }
1438 let computed = self.vi_sample_direct(s, x, y);
1439 s.cells[cell] = Some(computed);
1440 computed
1441 }
1442
1443 /// [`Bus::vi_sample`] without the memo: the actual filter dispatch.
1444 ///
1445 /// Under `aa_mode` 0/1 the coverage path (de-dither / AA-edge / divot) runs for
1446 /// both formats — 16-bit reads coverage from the hidden-bits plane, 32-bit from the
1447 /// alpha byte ([`Bus::vi_read_cov`]). Under `aa_mode` 2/3 (`RESAMP_ONLY` / REPLICATE)
1448 /// coverage is forced full, so it is a plain format-dispatched fetch.
1449 fn vi_sample_direct(&self, s: &ViSampler, x: i32, y: i32) -> [u8; 3] {
1450 let ViCfg {
1451 origin,
1452 src_stride,
1453 bpp,
1454 aa_mode,
1455 divot,
1456 dither_filter,
1457 } = s.cfg;
1458 if aa_mode <= 1 {
1459 if divot {
1460 self.vi_divot(origin, src_stride, x, y, dither_filter, bpp)
1461 } else {
1462 self.vi_fetch_coverage(origin, src_stride, x, y, dither_filter, bpp)
1463 }
1464 } else if bpp == 2 {
1465 self.vi_fetch16(origin, src_stride, x, y)
1466 } else {
1467 self.vi_fetch32(origin, src_stride, x, y)
1468 }
1469 }
1470
1471 /// One column's vertical lerp, or its single upper sample when `yfrac` weights the
1472 /// lower row at zero.
1473 ///
1474 /// A function rather than two inline copies so the `sx` and `sx + 1` columns cannot
1475 /// drift apart — this is a correctness-critical path pinned by the VI conformance
1476 /// vectors.
1477 ///
1478 /// The `yfrac == 0` case skips a tap whose weight is zero: `vi_lerp3(a, b, 0)` is
1479 /// `a + (((b - a) * 0 + 16) >> 5)` = `a + 0` = `a`, so the far sample is discarded
1480 /// and not fetching it cannot change the result.
1481 fn vi_column(&self, s: &mut ViSampler, x: i32, sy: i32, yfrac: i32) -> [u8; 3] {
1482 if yfrac == 0 {
1483 return self.vi_sample(s, x, sy);
1484 }
1485 let upper = self.vi_sample(s, x, sy);
1486 let lower = self.vi_sample(s, x, sy + 1);
1487 vi_lerp3(upper, lower, yfrac)
1488 }
1489
1490 /// Fetch a 16-bit RGBA5551 source pixel at `(x, y)` (stride `src_stride`, base
1491 /// `origin`) and convert to the VI's truncating RGB8 (`vi_rgb5551`). Reads
1492 /// big-endian through `rdram_read`, which returns 0 for an out-of-range address,
1493 /// so an out-of-bounds sample cannot panic. Ledger R-5 (VI scale resample).
1494 fn vi_fetch16(&self, origin: u32, src_stride: i32, x: i32, y: i32) -> [u8; 3] {
1495 let idx = src_stride.wrapping_mul(y).wrapping_add(x);
1496 // `wrapping_add_signed` adds the (possibly negative) signed byte offset to the
1497 // unsigned base without a sign-losing cast; `rdram_read` bounds-checks.
1498 let byte = origin.wrapping_add_signed(idx.wrapping_mul(2));
1499 let px = (u16::from(self.rdram_read(byte)) << 8)
1500 | u16::from(self.rdram_read(byte.wrapping_add(1)));
1501 vi_rgb5551(px)
1502 }
1503
1504 /// Fetch a 32-bit RGBA8888 source pixel at `(x, y)` as RGB8 (the big-endian
1505 /// R/G/B bytes; the alpha byte carries coverage, not shown). Reads big-endian
1506 /// through `rdram_read_u32`, bounds-safe like `vi_fetch16`. Ledger R-5.
1507 fn vi_fetch32(&self, origin: u32, src_stride: i32, x: i32, y: i32) -> [u8; 3] {
1508 let idx = src_stride.wrapping_mul(y).wrapping_add(x);
1509 let byte = origin.wrapping_add_signed(idx.wrapping_mul(4));
1510 let w = self.rdram_read_u32(byte).to_be_bytes();
1511 [w[0], w[1], w[2]]
1512 }
1513
1514 /// Read the raw 32-bit RGBA8888 source word at `(x, y)` (for coverage + the
1515 /// filter neighbor taps). Big-endian, bounds-safe. Ledger R-5.
1516 fn vi_read32(&self, origin: u32, src_stride: i32, x: i32, y: i32) -> u32 {
1517 let idx = src_stride.wrapping_mul(y).wrapping_add(x);
1518 let byte = origin.wrapping_add_signed(idx.wrapping_mul(4));
1519 self.rdram_read_u32(byte)
1520 }
1521
1522 /// Read the raw 16-bit RGBA5551 source halfword at `(x, y)` (big-endian,
1523 /// bounds-safe), the 16-bit counterpart to [`Bus::vi_read32`]. Ledger R-5.
1524 fn vi_read16(&self, origin: u32, src_stride: i32, x: i32, y: i32) -> u16 {
1525 let idx = src_stride.wrapping_mul(y).wrapping_add(x);
1526 let byte = origin.wrapping_add_signed(idx.wrapping_mul(2));
1527 (u16::from(self.rdram_read(byte)) << 8) | u16::from(self.rdram_read(byte.wrapping_add(1)))
1528 }
1529
1530 /// Read one source pixel as **raw** RGB8 (no filters) plus its 3-bit coverage,
1531 /// dispatching on the framebuffer format (`bpp` = 2 or 4). This is the sole
1532 /// format-specific primitive of the coverage path — every downstream filter
1533 /// (de-dither, AA-edge, divot) then operates on 8-bit channels regardless of
1534 /// source depth (Angrylion `vi_fetch_filter16`/`32`). Ledger R-5.
1535 ///
1536 /// - **32-bit RGBA8888:** channels are the top three big-endian bytes; coverage
1537 /// is alpha bits 7:5 (`(px >> 5) & 7`).
1538 /// - **16-bit RGBA5551:** channels are the truncating [`vi_rgb5551`] expansion;
1539 /// coverage combines the pixel's bit 0 (MSB) with the two **hidden bits** of
1540 /// the 9-bit RDRAM plane (`((px & 1) << 2) | rdram_hidden`), so `cvg == 7`
1541 /// requires bit 0 set **and** hidden bits `0b11`. The hidden read takes the
1542 /// pixel byte address (its own halfword index is derived internally).
1543 fn vi_read_cov(
1544 &self,
1545 origin: u32,
1546 src_stride: i32,
1547 x: i32,
1548 y: i32,
1549 bpp: u32,
1550 ) -> ([u8; 3], u32) {
1551 // Callers derive `bpp` from `VI_CTRL.TYPE` mapped to 2 (RGBA5551) or 4
1552 // (RGBA8888); any other value would silently take the 32-bit branch.
1553 debug_assert!(bpp == 2 || bpp == 4, "vi_read_cov: bpp must be 2 or 4");
1554 if bpp == 2 {
1555 // The hidden-bits halfword shares the color pixel's byte address.
1556 let idx = src_stride.wrapping_mul(y).wrapping_add(x);
1557 let byte = origin.wrapping_add_signed(idx.wrapping_mul(2));
1558 let px = self.vi_read16(origin, src_stride, x, y);
1559 let cvg = ((u32::from(px) & 1) << 2) | u32::from(self.rdram_read_hidden(byte));
1560 (vi_rgb5551(px), cvg)
1561 } else {
1562 let px = self.vi_read32(origin, src_stride, x, y);
1563 (
1564 [(px >> 24) as u8, (px >> 16) as u8, (px >> 8) as u8],
1565 (px >> 5) & 7,
1566 )
1567 }
1568 }
1569
1570 /// A source fetch for the coverage path (`aa_mode` 0/1), format-generic over
1571 /// `bpp` (2 = RGBA5551, 4 = RGBA8888). Reads the pixel's coverage via
1572 /// [`Bus::vi_read_cov`]; a fully-covered pixel (`cvg == 7`) gets the **de-dither**
1573 /// restore filter when `dither_filter` is set, otherwise the raw color. A partial
1574 /// pixel (`cvg < 7`) takes the **AA-edge** filter ([`Bus::vi_video_filter`]).
1575 /// Ledger R-5.
1576 ///
1577 /// De-dither (Angrylion `restore_filter16`/`32`): over the 8 taps of the 3×3
1578 /// neighborhood minus the center, each channel is nudged ±1 toward the neighbor
1579 /// (comparing the top-5-bit values `rgb8 >> 3` — the stored 5-bit channel in both
1580 /// formats), the noise-removing correction; the result is truncated to `u8`
1581 /// (Angrylion stores it into a `u8` field unmasked).
1582 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1583 fn vi_fetch_cov(
1584 &self,
1585 origin: u32,
1586 src_stride: i32,
1587 x: i32,
1588 y: i32,
1589 dither_filter: bool,
1590 bpp: u32,
1591 ) -> ([u8; 3], u32) {
1592 // The 3×3 neighborhood minus the center (restore.c tap layout).
1593 const TAPS: [(i32, i32); 8] = [
1594 (-1, -1),
1595 (0, -1),
1596 (1, -1),
1597 (-1, 1),
1598 (0, 1),
1599 (1, 1),
1600 (-1, 0),
1601 (1, 0),
1602 ];
1603 let (center, cvg) = self.vi_read_cov(origin, src_stride, x, y, bpp);
1604 if cvg < 7 {
1605 // Partial coverage → the AA edge filter (video_filter16/32).
1606 return (
1607 self.vi_video_filter(origin, src_stride, x, y, center, cvg, bpp),
1608 cvg,
1609 );
1610 }
1611 if !dither_filter {
1612 return (center, cvg); // fully covered without dither → raw color
1613 }
1614 let center5 = [center[0] >> 3, center[1] >> 3, center[2] >> 3]; // top 5 bits
1615 let mut acc = [
1616 i32::from(center[0]),
1617 i32::from(center[1]),
1618 i32::from(center[2]),
1619 ];
1620 for (dx, dy) in TAPS {
1621 let (nb, _) = self.vi_read_cov(origin, src_stride, x + dx, y + dy, bpp);
1622 let nb5 = [nb[0] >> 3, nb[1] >> 3, nb[2] >> 3];
1623 for c in 0..3 {
1624 acc[c] += match center5[c].cmp(&nb5[c]) {
1625 core::cmp::Ordering::Less => 1,
1626 core::cmp::Ordering::Greater => -1,
1627 core::cmp::Ordering::Equal => 0,
1628 };
1629 }
1630 }
1631 ([acc[0] as u8, acc[1] as u8, acc[2] as u8], cvg)
1632 }
1633
1634 /// The filtered coverage-path color ([`Bus::vi_fetch_cov`] without the
1635 /// coverage — for the non-divot path, which only needs the RGB).
1636 fn vi_fetch_coverage(
1637 &self,
1638 origin: u32,
1639 src_stride: i32,
1640 x: i32,
1641 y: i32,
1642 dither_filter: bool,
1643 bpp: u32,
1644 ) -> [u8; 3] {
1645 self.vi_fetch_cov(origin, src_stride, x, y, dither_filter, bpp)
1646 .0
1647 }
1648
1649 /// The **divot** filter (Angrylion `divot_filter`), format-generic over `bpp`: the
1650 /// per-channel median of a pixel and its two horizontal neighbors (all
1651 /// post-de-dither/AA-edge, via [`Bus::vi_fetch_cov`]). It is **skipped** (the center
1652 /// passes through) when all three are fully covered
1653 /// (`cen_cvg & left_cvg & right_cvg == 7`), so it only touches partial-coverage
1654 /// edges. Ledger R-5.
1655 fn vi_divot(
1656 &self,
1657 origin: u32,
1658 src_stride: i32,
1659 x: i32,
1660 y: i32,
1661 dither_filter: bool,
1662 bpp: u32,
1663 ) -> [u8; 3] {
1664 let (cen, cen_cvg) = self.vi_fetch_cov(origin, src_stride, x, y, dither_filter, bpp);
1665 let (left, left_cvg) = self.vi_fetch_cov(origin, src_stride, x - 1, y, dither_filter, bpp);
1666 let (right, right_cvg) =
1667 self.vi_fetch_cov(origin, src_stride, x + 1, y, dither_filter, bpp);
1668 if (cen_cvg & left_cvg & right_cvg) == 7 {
1669 return cen; // all fully covered → no divot
1670 }
1671 // Branch-expanded median-of-3 per channel (divot.c), matching its tie-handling.
1672 let median = |lv: u8, cv: u8, rv: u8| {
1673 if (lv >= cv && rv >= lv) || (lv >= rv && cv >= lv) {
1674 lv
1675 } else if (rv >= cv && lv >= rv) || (rv >= lv && cv >= rv) {
1676 rv
1677 } else {
1678 cv
1679 }
1680 };
1681 [
1682 median(left[0], cen[0], right[0]),
1683 median(left[1], cen[1], right[1]),
1684 median(left[2], cen[2], right[2]),
1685 ]
1686 }
1687
1688 /// The AA-edge filter for a partial-coverage pixel (Angrylion
1689 /// `video_filter16`/`32`), format-generic over `bpp`. Gathers the fully-covered
1690 /// pixels (`cvg == 7`) among the 6 taps — the up/down diagonals and the two-away
1691 /// left/right — via [`Bus::vi_read_cov`], plus the center, takes the per-channel
1692 /// penultimate min/max (`vi_video_max`), and pulls the center toward their midpoint
1693 /// weighted by `(7 - cvg)`:
1694 /// `center + (((penmin + penmax - 2*center) * (7 - cvg)) + 4 >> 3)`, masked to 8
1695 /// bits (the intermediate is unsigned two's-complement, so wrapping). Ledger R-5.
1696 #[allow(clippy::cast_possible_truncation, clippy::too_many_arguments)]
1697 fn vi_video_filter(
1698 &self,
1699 origin: u32,
1700 src_stride: i32,
1701 x: i32,
1702 y: i32,
1703 center: [u8; 3],
1704 cvg: u32,
1705 bpp: u32,
1706 ) -> [u8; 3] {
1707 // Up/down diagonals + two-away left/right (video.c `dirs`).
1708 const TAPS: [(i32, i32); 6] = [(-1, -1), (1, -1), (-2, 0), (2, 0), (-1, 1), (1, 1)];
1709 let mut back = [[0u32; 7]; 3]; // per channel, center at index 0
1710 for c in 0..3 {
1711 back[c][0] = u32::from(center[c]);
1712 }
1713 let mut n = 1usize;
1714 for (dx, dy) in TAPS {
1715 let (nb, nb_cvg) = self.vi_read_cov(origin, src_stride, x + dx, y + dy, bpp);
1716 if nb_cvg == 7 {
1717 back[0][n] = u32::from(nb[0]);
1718 back[1][n] = u32::from(nb[1]);
1719 back[2][n] = u32::from(nb[2]);
1720 n += 1;
1721 }
1722 }
1723 let coeff = 7 - cvg;
1724 let mut out = [0u8; 3];
1725 for c in 0..3 {
1726 let (penmin, penmax) = vi_video_max(&back[c][..n]);
1727 let ctr = u32::from(center[c]);
1728 let col = penmin
1729 .wrapping_add(penmax)
1730 .wrapping_sub(ctr << 1)
1731 .wrapping_mul(coeff)
1732 .wrapping_add(4)
1733 >> 3;
1734 out[c] = (col.wrapping_add(ctr) & 0xFF) as u8;
1735 }
1736 out
1737 }
1738
1739 /// Apply a write to the SP register block, performing whatever it starts.
1740 ///
1741 /// Two effects can come from one write and they are collected separately:
1742 /// a length write starts a DMA, and a `SP_STATUS` write can raise or
1743 /// acknowledge the MI's SP line. Folding them into one return value would
1744 /// imply they are alternatives, and `SP_STATUS` is reachable by both.
1745 fn sp_register_write(&mut self, addr: u32, val: u32) {
1746 let index = (addr >> 2) & 7;
1747 if index == rustyn64_rsp::sp::reg::STATUS
1748 && let Some(raise) = rustyn64_rsp::sp::SpRegs::interrupt_change(val)
1749 {
1750 self.rcp.mi_intr.sp = raise;
1751 }
1752 if let Some(dma) = self.rsp.sp.write(index, val) {
1753 self.sp_dma(dma);
1754 }
1755 }
1756 /// DMEM + IMEM, 4 KiB each.
1757 pub const SPMEM_LEN: usize = 0x2000;
1758
1759 /// End of the SP memory window — where the SP *registers* begin.
1760 ///
1761 /// The 8 KiB of real storage repeats for this whole range rather than
1762 /// ending at `0x0400_2000`; see [`rustyn64_rsp::Rsp::mem_read`] and
1763 /// accuracy ledger **C-30**, which records the provenance of the mirroring.
1764 pub const SPMEM_WINDOW_END: u32 = 0x0404_0000;
1765
1766 /// Is this address in the RSP DMEM/IMEM window?
1767 const fn is_spmem(addr: u32) -> bool {
1768 addr >= Self::SPMEM_BASE && addr < Self::SPMEM_WINDOW_END
1769 }
1770
1771 /// Is this address handled by a device on the RCP's **internal** bus?
1772 ///
1773 /// `0x0400_0000-0x04FF_FFFF`, the range N64brew *Memory map* describes as
1774 /// dispatched inside the RCP without going to an external bus. What matters
1775 /// here is the shared consequence: every device in it ignores the access
1776 /// size (see [`CpuBus::write_sized`]).
1777 ///
1778 /// The PI and SI external-bus windows share that size-blindness on hardware
1779 /// and are deliberately **not** included — the PI already models its own
1780 /// bus quirks separately, and folding both into one rule without the cart
1781 /// tests to check it against would be a change made blind. Phase 5.
1782 const fn is_rcp_internal(addr: u32) -> bool {
1783 matches!(addr, 0x0400_0000..=0x04FF_FFFF)
1784 }
1785
1786 /// Base of the **`ISViewer`** debug window, in cart address space.
1787 ///
1788 /// Not real N64 hardware — it is a flashcart/emulator convention that
1789 /// n64-systemtest uses to report results (`ref-proj/n64-systemtest/src/isviewer.rs`).
1790 /// The suite probes for it by writing a magic word to the buffer and reading
1791 /// it back; if the round-trip fails it falls back to a framebuffer console
1792 /// we cannot read. So this window is what turns "the suite runs" into "the
1793 /// suite reports".
1794 pub const ISVIEWER_BASE: u32 = 0x13FF_0000;
1795 /// Writing this register flushes `len` bytes from the buffer.
1796 pub const ISVIEWER_WRITE_LEN: u32 = 0x13FF_0014;
1797 /// The text buffer.
1798 pub const ISVIEWER_BUF: u32 = 0x13FF_0020;
1799 /// Bytes of buffer modeled — the suite writes in `0x200` chunks.
1800 pub const ISVIEWER_LEN: usize = 0x1000;
1801
1802 /// Is this address inside the `ISViewer` window?
1803 const fn is_isviewer(addr: u32) -> bool {
1804 addr >= Self::ISVIEWER_BASE && addr < Self::ISVIEWER_BASE + 0x20 + Self::ISVIEWER_LEN as u32
1805 }
1806
1807 /// The raw `ISViewer` backing memory, for diagnostics.
1808 #[must_use]
1809 pub fn isviewer_raw(&self) -> &[u8] {
1810 &self.isviewer
1811 }
1812
1813 /// Everything the guest has written to the `ISViewer` channel.
1814 #[must_use]
1815 pub fn isviewer_output(&self) -> &[u8] {
1816 &self.isviewer_out
1817 }
1818
1819 /// Text the guest has pushed through the EMUX `xlog` channel.
1820 #[must_use]
1821 pub fn emux_output(&self) -> &[u8] {
1822 &self.emux_out
1823 }
1824
1825 /// Offer the EMUX extensions to the guest.
1826 ///
1827 /// Opt-in, because hardware has none: enabling this changes which console
1828 /// backend n64-systemtest selects and therefore the instructions it
1829 /// executes. Worth it for a test harness (the `xlog` console needs no PI or
1830 /// `ISViewer` emulation and runs ~9x faster); wrong for anything claiming to
1831 /// reproduce a real console.
1832 pub const fn enable_emux(&mut self) {
1833 self.emux_enabled = true;
1834 }
1835
1836 /// Has the guest requested termination via `EMUX xioctl(EXIT)`?
1837 #[must_use]
1838 pub const fn emux_exited(&self) -> bool {
1839 self.emux_exited
1840 }
1841
1842 /// Is this address on the **PI external bus** — the memory-mapped window
1843 /// through which the CPU reaches cart ROM, SRAM and `FlashRAM`?
1844 ///
1845 /// Ranges from N64brew *Memory map*: `0x0500_0000-0x1FBF_FFFF` and
1846 /// `0x1FD0_0000-0x7FFF_FFFF`. Addresses outside them are DMA-only.
1847 const fn is_pi_bus(addr: u32) -> bool {
1848 matches!(addr, 0x0500_0000..=0x1FBF_FFFF | 0x1FD0_0000..=0x7FFF_FFFF)
1849 }
1850
1851 /// Map a PI-bus address through the **16-bit-bus off-by-two**.
1852 ///
1853 /// The PI external bus is 16 bits wide and the RCP ignores access size, so
1854 /// every VR4300 read becomes two 16-bit bus reads: the MSB at the CPU's
1855 /// address with bit 0 ignored, then the LSB at `address + 2`. The RCP thus
1856 /// returns the word starting at `addr & !1`, while the CPU selects its byte
1857 /// lane assuming a word at `addr & !3`. **That two-byte disagreement is the
1858 /// bug**, and it is hardware behavior, not an approximation:
1859 ///
1860 /// > effectively a 16-bit read at `0x1000'0002` returns the 16-bit word at
1861 /// > `0x1000'0004`
1862 /// > — N64brew, *Memory map*, PI external bus
1863 ///
1864 /// Working it through, `byte = (addr & !1) + (addr & 3)`, which collapses to
1865 /// "add two when bit 1 is set". A halfword load needs no special case
1866 /// because it is issued as two byte reads and both land correctly; a **word**
1867 /// load must bypass this entirely, which is why [`Bus::read_u32`] reads the
1868 /// PI window raw.
1869 const fn pi_bus_byte(addr: u32) -> u32 {
1870 if addr & 2 != 0 {
1871 addr.wrapping_add(2)
1872 } else {
1873 addr
1874 }
1875 }
1876
1877 /// Is this address in the PI register block?
1878 const fn is_pi_register(addr: u32) -> bool {
1879 addr >= rustyn64_cart::pi::PI_BASE && addr < rustyn64_cart::pi::PI_BASE + 0x34
1880 }
1881
1882 /// Carry out an SP DMA the register file has programmed.
1883 ///
1884 /// The engine lives in `rustyn64-rsp` and returns a description; the copy
1885 /// happens **here**, because the RSP does not own RDRAM and a chip reaching
1886 /// back into its owner is the dependency cycle `docs/architecture.md` exists
1887 /// to prevent. The PI works the same way.
1888 ///
1889 /// `skip` applies to the RDRAM side only. The SP side is contiguous and
1890 /// **wraps within its own 4 KiB bank** — a single transfer never spans DMEM
1891 /// and IMEM (N64brew *RSP Interface*: *"if the transfer hits the end of
1892 /// either memory area, it wraps around to the beginning of it"*).
1893 pub fn sp_dma(&mut self, dma: rustyn64_rsp::sp::Dma) {
1894 // Bit 12 selects the bank and is held fixed for the whole transfer;
1895 // only the 12-bit offset advances, so it wraps inside that bank.
1896 let bank = dma.sp_addr & 0x1000;
1897 let mut mem = dma.sp_addr & 0xFFF;
1898 let mut dram = dma.ram_addr;
1899
1900 for _ in 0..dma.rows {
1901 for _ in 0..dma.row_len {
1902 let m = bank | (mem & 0xFFF);
1903 if let Some(off) = Self::rdram_offset(dram) {
1904 if dma.to_dram {
1905 self.rdram[off] = self.rsp.mem_read(m);
1906 self.mark_rdram_dirty(off);
1907 } else {
1908 self.rsp.mem_write(m, self.rdram[off]);
1909 }
1910 }
1911 mem = mem.wrapping_add(1);
1912 dram = dram.wrapping_add(1);
1913 }
1914 // The RDRAM pointer steps over the gap between rows; the SP side
1915 // does not.
1916 dram = dram.wrapping_add(dma.skip);
1917 }
1918
1919 // Hardware leaves the pointers past the data, and the length field at
1920 // `0xFF8`. Instantaneous for now: the transfer is a value, so charging
1921 // it real time later is a scheduling change rather than a rewrite.
1922 self.rsp
1923 .sp
1924 .complete_dma(bank | (mem & 0xFFF), dram & 0x00FF_FFFF);
1925 }
1926
1927 /// Write a PI register and perform any transfer it starts.
1928 ///
1929 /// The copy happens **here**, not in the PI engine, because the PI does not
1930 /// own RDRAM — the Bus does. Having the engine reach back into its owner is
1931 /// the cycle this architecture exists to avoid, so the engine returns a
1932 /// description of the transfer and the owner carries it out.
1933 pub fn pi_write_word(&mut self, addr: u32, val: u32) {
1934 let started = self.pi.write(addr, val);
1935 // Mirror the PI's interrupt state into the MI on EVERY write, not only
1936 // on completion. A `PI_STATUS` write that clears the interrupt starts no
1937 // transfer, so an early return here left the MI line asserted -- `IP2`
1938 // stuck high forever, hanging any interrupt-driven loader.
1939 self.rcp.mi_intr.pi = self.pi.interrupt();
1940 let Some(t) = started else {
1941 return;
1942 };
1943 // Instantaneous for now. The transfer is a value, so charging it real
1944 // time later is a scheduling change rather than a rewrite -- which is
1945 // the same reason `SysAD` is a state machine rather than a function.
1946 for i in 0..t.len {
1947 if t.to_dram {
1948 let b = self.cart.pi_read(t.cart.wrapping_add(i));
1949 if let Some(off) = Self::rdram_offset(t.dram.wrapping_add(i)) {
1950 self.rdram[off] = b;
1951 self.mark_rdram_dirty(off);
1952 }
1953 } else {
1954 let b = Self::rdram_offset(t.dram.wrapping_add(i)).map_or(0, |off| self.rdram[off]);
1955 self.cart.pi_write(t.cart.wrapping_add(i), b);
1956 }
1957 }
1958 self.pi.complete();
1959 // Completion raises the PI line into the MI, which the CPU sees as IP2.
1960 self.rcp.mi_intr.pi = self.pi.interrupt();
1961 }
1962
1963 const fn rdram_offset(addr: u32) -> Option<usize> {
1964 // KSEG0/KSEG1 are stripped by the (future) TLB; the physical RDRAM
1965 // window is `$0000_0000..$007F_FFFF`.
1966 let phys = (addr & 0x1FFF_FFFF) as usize;
1967 if phys < RDRAM_SIZE { Some(phys) } else { None }
1968 }
1969}
1970
1971// --- The CPU's view of the whole machine. ---
1972use rustyn64_cart::pi;
1973
1974impl CpuBus for Bus {
1975 fn read_u8(&mut self, addr: u32) -> u8 {
1976 self.count_access();
1977 if let Some(off) = Self::rdram_offset(addr) {
1978 return self.rdram[off];
1979 }
1980 if Self::is_pi_register(addr) {
1981 // PI registers are 32-bit; a byte read selects within the word.
1982 let mut w = self.pi.read(addr);
1983 // `IOBUSY` covers the asynchronous direct-I/O write as well as DMA;
1984 // software polls it to know when a cart write has landed.
1985 if addr & !3 == rustyn64_cart::pi::PI_STATUS && self.pi_io_busy() {
1986 w |= rustyn64_cart::pi::STATUS_IO_BUSY;
1987 }
1988 return (w >> (8 * (3 - (addr & 3)))) as u8;
1989 }
1990 if Self::is_spmem(addr) {
1991 return self.rsp.mem_read(addr - Self::SPMEM_BASE);
1992 }
1993 // The SP interface registers. Word-granular behind a byte read: the
1994 // whole block lives on the RCP's internal bus, which returns the
1995 // aligned word and lets the CPU select within it.
1996 if Self::is_sp_register(addr) {
1997 let w = self.rsp.sp.read((addr >> 2) & 7);
1998 return (w >> (8 * (3 - (addr & 3)))) as u8;
1999 }
2000 if Self::is_mi_register(addr) {
2001 return (self.mi_read(addr) >> (8 * (3 - (addr & 3)))) as u8;
2002 }
2003 if Self::is_dp_register(addr) {
2004 return (self.rdp.dpc_read((addr >> 2) & 7) >> (8 * (3 - (addr & 3)))) as u8;
2005 }
2006 if Self::is_vi_register(addr) {
2007 return (self.vi.read(addr >> 2) >> (8 * (3 - (addr & 3)))) as u8;
2008 }
2009 if Self::is_ai_register(addr) {
2010 return (self.audio.read_reg((addr >> 2) & 7) >> (8 * (3 - (addr & 3)))) as u8;
2011 }
2012 if Self::is_ri_register(addr) {
2013 return (self.rcp.ri[((addr >> 2) & 7) as usize] >> (8 * (3 - (addr & 3)))) as u8;
2014 }
2015 if Self::is_si_register(addr) {
2016 return (self.si_read(addr) >> (8 * (3 - (addr & 3)))) as u8;
2017 }
2018 if Self::is_pif(addr) {
2019 return if addr >= Self::PIF_RAM_BASE {
2020 self.cart.pif_read((addr - Self::PIF_RAM_BASE) as usize)
2021 } else {
2022 // PIF boot ROM (IPL1/IPL2): mapped only on the real-PIF path; 0
2023 // under HLE (no ROM installed).
2024 self.cart
2025 .pif_boot_rom_read((addr - Self::PIF_ROM_BASE) as usize)
2026 };
2027 }
2028 if addr & !3 == Self::SP_PC {
2029 return (self.rsp.sp.pc() >> (8 * (3 - (addr & 3)))) as u8;
2030 }
2031 if Self::is_isviewer(addr) {
2032 // Readable as ordinary memory, which is what makes the suite's
2033 // write-magic-then-read-back probe succeed and select this channel
2034 // instead of the framebuffer console. Bounds-checked for the same
2035 // reason as the write path: the address is guest-controlled.
2036 return self
2037 .isviewer
2038 .get((addr - Self::ISVIEWER_BASE) as usize)
2039 .copied()
2040 .unwrap_or(0);
2041 }
2042 if Self::is_pi_bus(addr) {
2043 // A write in flight shadows the whole bus: reads from ANY address
2044 // return the value being written, not the device's data.
2045 if self.pi_io_busy() {
2046 return self.pi_write_latch.to_be_bytes()[(addr & 3) as usize];
2047 }
2048 return self.cart.pi_read(Self::pi_bus_byte(addr));
2049 }
2050 // TODO(T-CORE-01): decode the remaining RCP register windows.
2051 //
2052 // SP, DP, VI, AI, SI, RI, MI and the PIF ROM/RAM are all decoded above —
2053 // this comment listed every one of them as outstanding long after they
2054 // landed. What is genuinely still undecoded is the **RDRAM device
2055 // register** block (`0x03F0_0000`), the per-chip Rambus registers, which
2056 // is distinct from the RI controller block.
2057 self.cart.pi_read(addr)
2058 }
2059
2060 /// Read an aligned big-endian word.
2061 ///
2062 /// Overridden for the **PI external bus** only. The default composes four
2063 /// [`Bus::read_u8`] calls, which would apply the 16-bit-bus off-by-two to
2064 /// each byte independently and mangle bytes 2 and 3 of every word. A word
2065 /// access puts its own address on the bus, so `addr & !1 == addr` and the
2066 /// word is simply the four bytes there.
2067 fn read_u32(&mut self, addr: u32) -> u32 {
2068 self.count_access();
2069 // ISViewer lives INSIDE the PI bus range and is claimed first, exactly
2070 // as it is on the byte path. Letting the cart branch win here routes the
2071 // debug channel's read-back to ROM and breaks the detection handshake
2072 // the suite uses to select it.
2073 if Self::is_pi_bus(addr) && !Self::is_isviewer(addr) {
2074 if self.pi_io_busy() {
2075 return self.pi_write_latch;
2076 }
2077 return u32::from_be_bytes([
2078 self.cart.pi_read(addr),
2079 self.cart.pi_read(addr.wrapping_add(1)),
2080 self.cart.pi_read(addr.wrapping_add(2)),
2081 self.cart.pi_read(addr.wrapping_add(3)),
2082 ]);
2083 }
2084 // Registers with a **side effect on read** must be read exactly once.
2085 //
2086 // `SP_SEMAPHORE` takes the mutex when read, so composing a word out of
2087 // four byte reads took it four times: the first byte saw 0 and the rest
2088 // saw 1, and the assembled word came back as 1 where hardware returns 0.
2089 // n64-systemtest's `SP Semaphore Register (CPU only)` catches exactly
2090 // that. On hardware the RCP returns the whole aligned word for one
2091 // access regardless of size, so one access is the correct model.
2092 if Self::is_sp_register(addr) {
2093 return self.rsp.sp.read((addr >> 2) & 7);
2094 }
2095 if Self::is_mi_register(addr) {
2096 return self.mi_read(addr);
2097 }
2098 if Self::is_dp_register(addr) {
2099 return self.rdp.dpc_read((addr >> 2) & 7);
2100 }
2101 if Self::is_vi_register(addr) {
2102 return self.vi.read(addr >> 2);
2103 }
2104 if Self::is_ai_register(addr) {
2105 return self.audio.read_reg((addr >> 2) & 7);
2106 }
2107 if Self::is_ri_register(addr) {
2108 return self.rcp.ri[((addr >> 2) & 7) as usize];
2109 }
2110 if Self::is_si_register(addr) {
2111 return self.si_read(addr);
2112 }
2113 if Self::is_pif(addr) {
2114 if addr >= Self::PIF_RAM_BASE {
2115 let off = (addr - Self::PIF_RAM_BASE) as usize;
2116 return u32::from_be_bytes([
2117 self.cart.pif_read(off),
2118 self.cart.pif_read(off + 1),
2119 self.cart.pif_read(off + 2),
2120 self.cart.pif_read(off + 3),
2121 ]);
2122 }
2123 // PIF boot ROM (IPL1/IPL2): mapped only on the real-PIF path (0 under
2124 // HLE). This is the CPU instruction-fetch path from the reset vector.
2125 let off = (addr - Self::PIF_ROM_BASE) as usize;
2126 return u32::from_be_bytes([
2127 self.cart.pif_boot_rom_read(off),
2128 self.cart.pif_boot_rom_read(off + 1),
2129 self.cart.pif_boot_rom_read(off + 2),
2130 self.cart.pif_boot_rom_read(off + 3),
2131 ]);
2132 }
2133 // The RDRAM fast path, mirroring `write_u32`'s.
2134 //
2135 // Safe to skip `read_u8` for this range because its RDRAM arm is a pure
2136 // `self.rdram[off]` with no side effect — unlike its PI arm, which folds
2137 // in `IOBUSY`. A fast path over a side-effecting read is how the
2138 // `SP_SEMAPHORE` bug in this same function happened.
2139 //
2140 // Placed after every register branch rather than at the top. That is
2141 // DEFENSIVE, not load-bearing: `rdram_offset` already returns `None`
2142 // outside the 8 MiB window, so hoisting it would be equally correct
2143 // today. Sitting here means the change cannot come to depend on that
2144 // range staying disjoint from a future register block.
2145 //
2146 // Provenance and the measurement: `docs/performance.md`.
2147 if let Some(word) = Self::rdram_offset(addr)
2148 .and_then(|off| self.rdram.get(off..off + 4))
2149 .and_then(|s| s.try_into().ok())
2150 {
2151 return u32::from_be_bytes(word);
2152 }
2153 u32::from_be_bytes([
2154 self.read_u8(addr),
2155 self.read_u8(addr.wrapping_add(1)),
2156 self.read_u8(addr.wrapping_add(2)),
2157 self.read_u8(addr.wrapping_add(3)),
2158 ])
2159 }
2160
2161 fn write_u8(&mut self, addr: u32, val: u8) {
2162 self.count_access();
2163 if let Some(off) = Self::rdram_offset(addr) {
2164 self.rdram[off] = val;
2165 self.mark_rdram_dirty(off);
2166 return;
2167 }
2168 if Self::is_pi_register(addr) {
2169 // PI registers are **32-bit only**, and a byte write to one is not
2170 // something real code does. Assembling a word by read-modify-write
2171 // is actively wrong for two of them:
2172 //
2173 // * the length registers *trigger* on write, so a byte-wise RMW
2174 // starts a DMA per byte with a partly assembled length;
2175 // * `PI_STATUS`'s read bits (busy, interrupt) do not correspond to
2176 // its write bits (reset, clear-interrupt), so reading it back to
2177 // fill in the other three bytes fabricates command strobes from
2178 // status flags.
2179 //
2180 // Only the address registers can be safely assembled, so only they
2181 // are. A byte write to anything else is dropped rather than guessed
2182 // at -- an explicit nothing beats a plausible wrong action.
2183 if matches!(addr & !3, pi::PI_DRAM_ADDR | pi::PI_CART_ADDR) {
2184 let shift = 8 * (3 - (addr & 3));
2185 let w = (self.pi.read(addr) & !(0xFF << shift)) | (u32::from(val) << shift);
2186 self.pi_write_word(addr, w);
2187 }
2188 return;
2189 }
2190 if Self::is_spmem(addr) {
2191 self.rsp.mem_write(addr - Self::SPMEM_BASE, val);
2192 return;
2193 }
2194 if Self::is_isviewer(addr) {
2195 if let Some(b) = self.isviewer.get_mut((addr - Self::ISVIEWER_BASE) as usize) {
2196 *b = val;
2197 }
2198 return;
2199 }
2200 if Self::is_pif(addr) {
2201 if addr >= Self::PIF_RAM_BASE {
2202 let off = (addr - Self::PIF_RAM_BASE) as usize;
2203 self.cart.pif_write(off, val);
2204 self.pif_boot_command_if_cmd(off);
2205 }
2206 return;
2207 }
2208 // TODO(T-CORE-01): decode + dispatch the remaining RCP register windows —
2209 // specifically the RDRAM device registers (`0x03F0_0000`). Note this is
2210 // the **byte**-write path: RCP register blocks are reached through
2211 // `write_sized`, which funnels to `write_u32`, so their absence here is by
2212 // design rather than an omission.
2213 self.cart.pi_write(addr, val);
2214 }
2215
2216 /// Model the RCP's **size-blind** write path.
2217 ///
2218 /// Everything on the RCP's internal bus latches the whole 32-bit word the
2219 /// VR4300 put on `SysAD`, ignoring both the access size and the low two
2220 /// address bits (N64brew *Memory map* §Physical Memory Map accesses). The
2221 /// VR4300 has already shifted the source register into the byte lane the
2222 /// address selects, so a narrow store writes that shifted register —
2223 /// **including the bits above the stored byte**, which is why the effect
2224 /// looks like zero-fill rather than a partial update.
2225 ///
2226 /// n64-systemtest states the rule outright in its own header comment
2227 /// (`src/tests/sp_memory/mod.rs`): *"SH/SB are broken: they overwrite the
2228 /// whole 32 bit, filling everything that isn't written with zeroes. SD is
2229 /// broken: it only writes the upper 32 bit of the value, touching only 4
2230 /// bytes."* With `$3 = 0x1234_5678`, `SB $3, 5(spmem)` leaves `0x5678_0000`
2231 /// in the word at offset 4 — the register shifted left 16, not the byte
2232 /// `0x78`.
2233 ///
2234 /// RDRAM is excluded because the RI passes the low address bits and the
2235 /// access size on to the RDRAM devices, which build a real byte mask from
2236 /// them; only the RCP's internal path throws that information away.
2237 fn write_sized(&mut self, addr: u32, width: u64, value: u64) {
2238 // Unsupported widths do nothing, matching the default `write_sized`.
2239 // `StoreKind::width` only ever yields 1/2/4/8 so nothing reaches this
2240 // today, but without the guard the internal-bus arm below would accept
2241 // any width and *store* -- so the two paths would disagree about what a
2242 // width of 3 means, which is exactly the kind of divergence that is
2243 // discovered years later through a corrupted byte lane.
2244 if !matches!(width, 1 | 2 | 4 | 8) {
2245 return;
2246 }
2247 if !Self::is_rcp_internal(addr) {
2248 // RDRAM, the PI/SI external buses and the ISViewer keep byte-exact
2249 // semantics -- see `is_rcp_internal` for why the external buses are
2250 // not folded in here yet.
2251 match width {
2252 1 => self.write_u8(addr, value as u8),
2253 2 => {
2254 self.write_u8(addr, (value >> 8) as u8);
2255 self.write_u8(addr.wrapping_add(1), value as u8);
2256 }
2257 4 => self.write_u32(addr, value as u32),
2258 8 => {
2259 self.write_u32(addr, (value >> 32) as u32);
2260 self.write_u32(addr.wrapping_add(4), value as u32);
2261 }
2262 _ => {}
2263 }
2264 return;
2265 }
2266 let word = match width {
2267 // 64-bit: the two words go out MSB-first and the RCP takes the
2268 // first, dropping the second entirely -- so a `SD` touches four
2269 // bytes, not eight.
2270 8 => (value >> 32) as u32,
2271 4 => value as u32,
2272 // Narrow: the register as the VR4300 placed it on the bus.
2273 //
2274 // Saturating, not because the invariant is in doubt but because it
2275 // is enforced somewhere else. MIPS requires natural alignment and
2276 // the CPU raises `AddressError` before a misaligned store ever
2277 // reaches the bus, so `width + (addr & 3) <= 4` holds for every
2278 // access that gets here — but this is a public trait method, and a
2279 // caller that breaks the invariant should get a defined byte lane
2280 // rather than an underflow that panics in debug and silently
2281 // becomes an over-wide shift in release.
2282 w => {
2283 let lane = 4u32.saturating_sub(w as u32).saturating_sub(addr & 3);
2284 (value as u32) << (8 * lane)
2285 }
2286 };
2287 self.write_u32(addr & !3, word);
2288 }
2289
2290 fn write_u32(&mut self, addr: u32, val: u32) {
2291 self.count_access();
2292 // SP DMA registers. Handled here, at word granularity, for the same
2293 // reason as the PI: the default byte-wise path would fire four DMAs for
2294 // one `sw` to a length register.
2295 // A PI direct-I/O write latches and returns immediately; the transfer
2296 // finalizes in the background. Further writes while one is in flight are
2297 // ignored -- not queued.
2298 if Self::is_pi_bus(addr) && !Self::is_isviewer(addr) {
2299 if !self.pi_io_busy() {
2300 self.pi_write_latch = val;
2301 self.pi_write_countdown = Self::PI_WRITE_CYCLES;
2302 // A direct-I/O write must reach a *writable* PI device: SRAM, the
2303 // FlashRAM page buffer, or the FlashRAM Command register. The cart
2304 // ignores writes to the read-only ROM window, so this is safe to
2305 // call for any PI-bus address. (The latch above models the
2306 // read-back-while-busy timing; this performs the actual store.)
2307 self.cart.pi_write_word(addr, val);
2308 }
2309 return;
2310 }
2311 if Self::is_sp_register(addr) {
2312 self.sp_register_write(addr, val);
2313 return;
2314 }
2315 if Self::is_mi_register(addr) {
2316 self.mi_write(addr, val);
2317 return;
2318 }
2319 if Self::is_dp_register(addr) {
2320 self.rdp.dpc_write((addr >> 2) & 7, val);
2321 return;
2322 }
2323 if Self::is_vi_register(addr) {
2324 self.vi_write(addr, val);
2325 return;
2326 }
2327 if Self::is_ai_register(addr) {
2328 self.ai_write(addr, val);
2329 return;
2330 }
2331 if Self::is_ri_register(addr) {
2332 self.rcp.ri[((addr >> 2) & 7) as usize] = val;
2333 return;
2334 }
2335 if Self::is_si_register(addr) {
2336 self.si_write(addr, val);
2337 return;
2338 }
2339 if Self::is_pif(addr) {
2340 if addr >= Self::PIF_RAM_BASE {
2341 let off = (addr - Self::PIF_RAM_BASE) as usize;
2342 for (i, b) in val.to_be_bytes().into_iter().enumerate() {
2343 self.cart.pif_write(off + i, b);
2344 }
2345 // IPL2 writes the command *word* at PIF-RAM 0x3C (`sw` to
2346 // 0xBFC007FC), so a word store is the usual boot-command path.
2347 self.pif_boot_command_if_cmd(off + 3);
2348 }
2349 return;
2350 }
2351 if addr & !3 == Self::SP_PC {
2352 self.rsp.sp.set_pc(val);
2353 return;
2354 }
2355 if addr == Self::ISVIEWER_WRITE_LEN {
2356 // Flushing is triggered by the LENGTH write, not by the buffer
2357 // writes -- so the guest assembles a whole line and then publishes
2358 // it. Capturing on buffer writes instead would interleave partial
2359 // lines and make the output unreadable.
2360 let n = (val as usize).min(Self::ISVIEWER_LEN);
2361 let base = (Self::ISVIEWER_BUF - Self::ISVIEWER_BASE) as usize;
2362 let bytes = &self.isviewer[base..base + n];
2363 self.isviewer_out.extend_from_slice(bytes);
2364 return;
2365 }
2366 if Self::is_isviewer(addr) {
2367 // Bounds-checked, not indexed. `addr` comes from guest code, so a
2368 // word write starting in the last three bytes of the window --
2369 // which `is_isviewer` accepts -- would index past the slice and
2370 // **panic the emulator**. A guest must never be able to do that.
2371 let off = (addr - Self::ISVIEWER_BASE) as usize;
2372 if let Some(dst) = self.isviewer.get_mut(off..off + 4) {
2373 dst.copy_from_slice(&val.to_be_bytes());
2374 }
2375 return;
2376 }
2377 // **A PI register write must be a single WORD write.**
2378 //
2379 // The default `write_u32` composes four `write_u8` calls, and PI
2380 // registers were handled byte-wise -- so a normal guest `sw` to
2381 // `PI_WR_LEN` started **four DMAs**, one per byte, each with a partly
2382 // assembled length. Every PI transfer was wrong, and the failure looks
2383 // like memory corruption rather than a DMA bug.
2384 if Self::is_pi_register(addr) {
2385 self.pi_write_word(addr, val);
2386 return;
2387 }
2388 if let Some(off) = Self::rdram_offset(addr) {
2389 // The fast path, avoiding four bounds checks for the common case.
2390 let b = val.to_be_bytes();
2391 if off + 3 < self.rdram.len() {
2392 self.rdram[off..=off + 3].copy_from_slice(&b);
2393 self.mark_rdram_dirty_range(off, 4);
2394 return;
2395 }
2396 }
2397 let b = val.to_be_bytes();
2398 for (i, byte) in b.iter().enumerate() {
2399 self.write_u8(addr.wrapping_add(i as u32), *byte);
2400 }
2401 }
2402
2403 fn emux_enabled(&self) -> bool {
2404 self.emux_enabled
2405 }
2406
2407 fn emux_log(&mut self, bytes: &[u8]) {
2408 self.emux_out.extend_from_slice(bytes);
2409 }
2410
2411 fn emux_exit(&mut self) {
2412 self.emux_exited = true;
2413 }
2414
2415 fn poll_irq(&mut self) -> bool {
2416 // IP2 asserts when an unmasked MI line is set. The run-cycle gate and the
2417 // DC-stage sampling point live in the CPU pipeline (ADR 0007); this only
2418 // reports the level.
2419 let i = self.rcp.mi_intr;
2420 let m = self.rcp.mi_mask;
2421 (i.sp && m.sp)
2422 || (i.si && m.si)
2423 || (i.ai && m.ai)
2424 || (i.vi && m.vi)
2425 || (i.pi && m.pi)
2426 || (i.dp && m.dp)
2427 }
2428}
2429
2430// --- The shared RDRAM bus (used by the RDP/RSP/AI DMA paths). ---
2431impl RdramBus for Bus {
2432 fn rdram_read(&self, addr: u32) -> u8 {
2433 Self::rdram_offset(addr).map_or(0, |off| self.rdram[off])
2434 }
2435
2436 fn rdram_write(&mut self, addr: u32, val: u8) {
2437 if let Some(off) = Self::rdram_offset(addr) {
2438 self.rdram[off] = val;
2439 self.mark_rdram_dirty(off);
2440 }
2441 }
2442
2443 fn rdram_read_hidden(&self, addr: u32) -> u8 {
2444 // Two bits per 16-bit halfword, packed four halfwords to a byte. `None`
2445 // (never written) reads 0.
2446 match (&self.rdram_hidden, Self::rdram_offset(addr)) {
2447 (Some(hidden), Some(off)) => {
2448 let halfword = off >> 1;
2449 let shift = (halfword & 3) * 2;
2450 (hidden[halfword >> 2] >> shift) & 0x3
2451 }
2452 _ => 0,
2453 }
2454 }
2455
2456 fn rdram_write_hidden(&mut self, addr: u32, val: u8) {
2457 if let Some(off) = Self::rdram_offset(addr) {
2458 let hidden = self
2459 .rdram_hidden
2460 .get_or_insert_with(|| alloc::vec![0u8; RDRAM_SIZE / 8].into_boxed_slice());
2461 let halfword = off >> 1;
2462 let shift = (halfword & 3) * 2;
2463 let byte = &mut hidden[halfword >> 2];
2464 *byte = (*byte & !(0x3 << shift)) | ((val & 0x3) << shift);
2465 }
2466 }
2467}
2468
2469// --- The RDP's narrow view. ---
2470impl VideoBus for Bus {
2471 fn raise_dp_interrupt(&mut self) {
2472 self.rcp.mi_intr.dp = true;
2473 }
2474}
2475
2476// --- The RSP's narrow view. ---
2477// --- The AI's narrow view. ---
2478impl AudioBus for Bus {
2479 fn ai_dma_read_u32(&self, addr: u32) -> u32 {
2480 <Self as RdramBus>::rdram_read_u32(self, addr)
2481 }
2482 fn raise_ai_interrupt(&mut self) {
2483 self.rcp.mi_intr.ai = true;
2484 }
2485}
2486
2487#[cfg(test)]
2488mod tests {
2489 use super::*;
2490
2491 #[test]
2492 fn rdram_round_trips_through_cpu_view() {
2493 let mut bus = Bus::new();
2494 CpuBus::write_u8(&mut bus, 0x0000_1234, 0xAB);
2495 assert_eq!(CpuBus::read_u8(&mut bus, 0x0000_1234), 0xAB);
2496 }
2497
2498 #[test]
2499 fn rdram_hidden_bits_lazy_round_trip_and_masked() {
2500 let mut bus = Bus::new();
2501 // Unallocated at power-on; reads back clear.
2502 assert!(bus.rdram_hidden.is_none());
2503 assert_eq!(bus.rdram_read_hidden(0x1000), 0);
2504 // First write allocates and stores the 2-bit value.
2505 bus.rdram_write_hidden(0x1000, 0x3);
2506 assert!(bus.rdram_hidden.is_some(), "allocated on first write");
2507 assert_eq!(bus.rdram_read_hidden(0x1000), 0x3);
2508 // Bit-packed four halfwords to a byte: the adjacent halfword shares the
2509 // byte but not the bits, so writing it must not clobber the first.
2510 assert_eq!(bus.rdram_read_hidden(0x1002), 0);
2511 bus.rdram_write_hidden(0x1002, 0x2);
2512 assert_eq!(bus.rdram_read_hidden(0x1002), 0x2);
2513 assert_eq!(bus.rdram_read_hidden(0x1000), 0x3, "neighbor unchanged");
2514 // Only the low 2 bits are kept.
2515 bus.rdram_write_hidden(0x1000, 0x5);
2516 assert_eq!(bus.rdram_read_hidden(0x1000), 0x1);
2517 assert_eq!(bus.rdram_read_hidden(0x1002), 0x2, "still independent");
2518 }
2519
2520 #[test]
2521 fn dp_interrupt_sets_mi_line() {
2522 let mut bus = Bus::new();
2523 VideoBus::raise_dp_interrupt(&mut bus);
2524 assert!(bus.rcp.mi_intr.dp);
2525 assert!(bus.rcp.mi_intr.any());
2526 }
2527
2528 #[test]
2529 fn masked_irq_drives_ip2() {
2530 let mut bus = Bus::new();
2531 bus.rcp.mi_intr.ai = true;
2532 bus.rcp.mi_mask.ai = true;
2533 assert!(CpuBus::poll_irq(&mut bus));
2534 }
2535
2536 /// **The AI register block is CPU-addressable and drives audio end to end.**
2537 /// **Stepping the AI one master tick at a time still emits every sample.**
2538 ///
2539 /// [`Bus::audio_tick`] skips the `core::mem::take` on the steps whose bus-free
2540 /// half reports nothing due — the overwhelming majority. This drives the real
2541 /// per-tick cadence rather than one large jump, so every skip is exercised, and
2542 /// asserts the emitted stream is exactly what an unskipped run produces.
2543 ///
2544 /// The oracle is the RDRAM the test wrote, not another run of this path: both
2545 /// sample values are asserted against the words placed at `0x2000`, and the
2546 /// count against the 8-byte `AI_LENGTH`. Mutation-checked — forcing the take to
2547 /// be skipped unconditionally turns this red on the count, which is a clearer
2548 /// signal than the index-out-of-bounds panic that the only other covering test
2549 /// produced.
2550 #[test]
2551 fn skipping_the_take_never_skips_a_sample() {
2552 let mut bus = Bus::new();
2553 CpuBus::write_u32(&mut bus, 0x0000_2000, 0x1111_2222);
2554 CpuBus::write_u32(&mut bus, 0x0000_2004, 0x3333_4444);
2555 CpuBus::write_u32(&mut bus, Bus::AI_REGS_BASE + 0x10, 1103); // AI_DACRATE
2556 CpuBus::write_u32(&mut bus, Bus::AI_REGS_BASE + 0x08, 1); // AI_CONTROL
2557 CpuBus::write_u32(&mut bus, Bus::AI_REGS_BASE, 0x2000); // AI_DRAM_ADDR
2558 CpuBus::write_u32(&mut bus, Bus::AI_REGS_BASE + 0x04, 8); // AI_LENGTH
2559
2560 let period = rustyn64_audio::MASTER_HZ / u64::from(bus.audio.sample_rate());
2561 // Two periods, one master tick at a time: thousands of skipped takes and a
2562 // handful of real ones.
2563 for now in 1..=(period * 2) {
2564 bus.audio_tick(now);
2565 }
2566 let stepped = bus.drain_audio_samples();
2567
2568 assert_eq!(
2569 stepped.len(),
2570 2,
2571 "both buffered samples must come out of the per-tick cadence"
2572 );
2573 assert_eq!(
2574 stepped[0],
2575 StereoSample {
2576 left: 0x1111,
2577 right: 0x2222
2578 }
2579 );
2580 assert_eq!(
2581 stepped[1],
2582 StereoSample {
2583 left: 0x3333,
2584 right: 0x4444
2585 }
2586 );
2587 }
2588
2589 /// Programming `AI_DACRATE`/`AI_CONTROL`/`AI_DRAM_ADDR`/`AI_LENGTH` through
2590 /// the memory-mapped path at `0x0450_0000` starts a transfer that raises
2591 /// `MI_INTR.ai` on enqueue, mirrors `AI_LENGTH` on the write-only registers,
2592 /// acknowledges on an `AI_STATUS` write, and emits the RDRAM samples as the
2593 /// derived-timing DAC advances.
2594 #[test]
2595 fn ai_registers_drive_audio_through_the_cpu_bus() {
2596 let mut bus = Bus::new();
2597 // Two stereo pairs at RDRAM 0x2000.
2598 CpuBus::write_u32(&mut bus, 0x0000_2000, 0x1111_2222);
2599 CpuBus::write_u32(&mut bus, 0x0000_2004, 0x3333_4444);
2600 // Program the AI: ~44 kHz, DMA enabled, buffer at 0x2000, 8 bytes.
2601 CpuBus::write_u32(&mut bus, Bus::AI_REGS_BASE + 0x10, 1103); // AI_DACRATE
2602 CpuBus::write_u32(&mut bus, Bus::AI_REGS_BASE + 0x08, 1); // AI_CONTROL
2603 CpuBus::write_u32(&mut bus, Bus::AI_REGS_BASE, 0x2000); // AI_DRAM_ADDR
2604 CpuBus::write_u32(&mut bus, Bus::AI_REGS_BASE + 0x04, 8); // AI_LENGTH
2605 assert!(
2606 bus.rcp.mi_intr.ai,
2607 "enqueuing the first buffer raises the AI line"
2608 );
2609 // Write-only registers read back the AI_LENGTH mirror (remaining bytes).
2610 assert_eq!(CpuBus::read_u32(&mut bus, Bus::AI_REGS_BASE + 0x10), 8);
2611 // AI_STATUS reports BUSY and ENABLED.
2612 let status = CpuBus::read_u32(&mut bus, Bus::AI_REGS_BASE + 0x0C);
2613 assert_ne!(status & (1 << 30), 0, "BUSY");
2614 assert_ne!(status & (1 << 25), 0, "ENABLED");
2615 // A write to AI_STATUS acknowledges the interrupt.
2616 CpuBus::write_u32(&mut bus, Bus::AI_REGS_BASE + 0x0C, 0);
2617 assert!(!bus.rcp.mi_intr.ai, "an AI_STATUS write acks the interrupt");
2618 // Advance the DAC and drain the emitted samples.
2619 let period = rustyn64_audio::MASTER_HZ / u64::from(bus.audio.sample_rate());
2620 bus.audio_tick(period * 2);
2621 let samples = bus.drain_audio_samples();
2622 assert_eq!(
2623 samples[0],
2624 StereoSample {
2625 left: 0x1111,
2626 right: 0x2222
2627 }
2628 );
2629 assert_eq!(
2630 samples[1],
2631 StereoSample {
2632 left: 0x3333,
2633 right: 0x4444
2634 }
2635 );
2636 }
2637
2638 /// **A `Sync Full` command drives the DP interrupt through to the CPU.** A
2639 /// `0x29` command word placed in RDRAM and consumed by `rdp_tick` raises
2640 /// `MI_INTR.dp`; once the DP line is masked in it asserts IP2, which is how
2641 /// the CPU comes to service the RDP-done interrupt. This is the end-to-end
2642 /// path for Phase 3's `Sync Full` — the RDP dispatcher, the `VideoBus` seam,
2643 /// the MI line, and the mask, together.
2644 #[test]
2645 fn a_sync_full_command_drives_the_dp_interrupt_to_ip2() {
2646 let mut bus = Bus::new();
2647 // A Sync Full command (opcode 0x29 in bits 61:56) at RDRAM 0x100.
2648 bus.rdram[0x100] = 0x29;
2649 // Point the DP FIFO at it: a single 8-byte command.
2650 bus.rdp.dpc_write(0, 0x100); // DPC_START (sets START_VALID)
2651 bus.rdp.dpc_write(1, 0x108); // DPC_END (copies START -> CURRENT)
2652
2653 assert!(!bus.rcp.mi_intr.dp, "DP line clear before the command runs");
2654 bus.rdp_tick();
2655 assert!(bus.rcp.mi_intr.dp, "Sync Full raised the DP line");
2656
2657 bus.rcp.mi_mask.dp = true;
2658 assert!(CpuBus::poll_irq(&mut bus), "the masked DP line asserts IP2");
2659 }
2660
2661 /// **VI registers round-trip through the CPU bus, and a `VI_V_CURRENT` write
2662 /// acknowledges the VI interrupt.** The block is at `0x0440_0000`; a write to
2663 /// `VI_V_CURRENT` (+0x10) clears `MI_INTR.vi`, the interrupt-ack path.
2664 #[test]
2665 fn vi_registers_round_trip_and_v_current_acks_the_interrupt() {
2666 let mut bus = Bus::new();
2667 // VI_ORIGIN (+0x04) is an ordinary latch.
2668 CpuBus::write_u32(&mut bus, Bus::VI_REGS_BASE + 0x04, 0x0010_0000);
2669 assert_eq!(
2670 CpuBus::read_u32(&mut bus, Bus::VI_REGS_BASE + 0x04),
2671 0x0010_0000,
2672 "VI_ORIGIN round-trips"
2673 );
2674 // A pending VI interrupt is cleared by writing VI_V_CURRENT (+0x10).
2675 bus.rcp.mi_intr.vi = true;
2676 CpuBus::write_u32(&mut bus, Bus::VI_REGS_BASE + 0x10, 0x42);
2677 assert!(
2678 !bus.rcp.mi_intr.vi,
2679 "writing VI_V_CURRENT acks the interrupt"
2680 );
2681 // ... and the write did not latch into V_CURRENT.
2682 assert_eq!(CpuBus::read_u32(&mut bus, Bus::VI_REGS_BASE + 0x10), 0);
2683 }
2684
2685 /// **VI registers latch the whole word regardless of store size, and the
2686 /// block mirrors every 16 words.** The VI is on the RCP-internal bus, so
2687 /// `write_sized` routes 8-/16-/64-bit stores through `write_u32`; a byte read
2688 /// recovers the addressed lane; a mirrored address decodes to the same
2689 /// register; and a narrow `VI_V_CURRENT` write still acks without latching.
2690 #[test]
2691 fn vi_accesses_are_size_blind_and_mirrored() {
2692 let mut bus = Bus::new();
2693 // An 8-bit store to VI_WIDTH (+0x08) latches the shifted register across
2694 // the whole word (RCP-internal, size-blind).
2695 CpuBus::write_sized(&mut bus, Bus::VI_REGS_BASE + 0x08, 1, 0x44);
2696 assert_eq!(
2697 CpuBus::read_u32(&mut bus, Bus::VI_REGS_BASE + 0x08),
2698 0x4400_0000,
2699 "SB latches the shifted register across the whole word"
2700 );
2701 // A word store (VI_WIDTH = 320 = 0x0000_0140), then a byte read recovers
2702 // the addressed lane — the low byte at +0x0B is 0x40.
2703 CpuBus::write_sized(&mut bus, Bus::VI_REGS_BASE + 0x08, 4, 320);
2704 assert_eq!(CpuBus::read_u8(&mut bus, Bus::VI_REGS_BASE + 0x0B), 0x40);
2705 // A 64-bit store writes its upper word to VI_ORIGIN (+0x04).
2706 CpuBus::write_sized(&mut bus, Bus::VI_REGS_BASE + 0x04, 8, 0x0010_0000_DEAD_BEEF);
2707 assert_eq!(
2708 CpuBus::read_u32(&mut bus, Bus::VI_REGS_BASE + 0x04),
2709 0x0010_0000
2710 );
2711 // The block mirrors every 16 words: +0x40 decodes to VI_CTRL (offset 0).
2712 CpuBus::write_u32(&mut bus, Bus::VI_REGS_BASE + 0x40, 0x3);
2713 assert_eq!(CpuBus::read_u32(&mut bus, Bus::VI_REGS_BASE), 0x3);
2714 // A narrow (8-bit) VI_V_CURRENT write still acks without latching.
2715 bus.rcp.mi_intr.vi = true;
2716 CpuBus::write_sized(&mut bus, Bus::VI_REGS_BASE + 0x10, 1, 0x99);
2717 assert!(!bus.rcp.mi_intr.vi, "a narrow VI_V_CURRENT write acks");
2718 assert_eq!(CpuBus::read_u32(&mut bus, Bus::VI_REGS_BASE + 0x10), 0);
2719 }
2720
2721 /// **Scan-out converts the framebuffer to RGBA8.** 32-bit RGBA8888 is a
2722 /// direct copy; 16-bit RGBA5551 expands each 5-bit channel to 8 and the
2723 /// 1-bit alpha to 0/255. Height comes from `VI_V_VIDEO`'s active half-lines.
2724 #[test]
2725 fn scanout_converts_32bit_and_16bit_framebuffers() {
2726 let mut bus = Bus::new();
2727 let fb = 0x100usize;
2728 // A 2x2 32-bit framebuffer, row-major.
2729 let px32 = [0xAABB_CCDDu32, 0x1122_3344, 0x5566_7788, 0x99AA_BBCC];
2730 for (i, p) in px32.iter().enumerate() {
2731 bus.rdram[fb + i * 4..fb + i * 4 + 4].copy_from_slice(&p.to_be_bytes());
2732 }
2733 // VI: 32-bit, origin 0x100, width 2, V_VIDEO active = 4 half-lines (h=2).
2734 bus.vi.regs[vi::VI_CTRL as usize] = 3;
2735 bus.vi.regs[vi::VI_ORIGIN as usize] = fb as u32;
2736 bus.vi.regs[vi::VI_WIDTH as usize] = 2;
2737 bus.vi.regs[vi::VI_V_VIDEO as usize] = 4; // start 0, end 4 -> 2 lines
2738 let mut out = alloc::vec![0u8; 2 * 2 * 4];
2739 assert_eq!(bus.scanout(&mut out), (2, 2));
2740 assert_eq!(&out[0..4], &[0xAA, 0xBB, 0xCC, 0xDD], "direct 32-bit copy");
2741 assert_eq!(&out[12..16], &[0x99, 0xAA, 0xBB, 0xCC]);
2742
2743 // 16-bit RGBA5551, non-uniform channels so component order and the
2744 // field shifts are exercised: 0x0887 -> R=1,G=2,B=3,A=1 = [08,10,18,FF];
2745 // 0x0886 is the same color with alpha 0 = [08,10,18,00].
2746 bus.rdram[fb..fb + 2].copy_from_slice(&0x0887u16.to_be_bytes());
2747 bus.rdram[fb + 2..fb + 4].copy_from_slice(&0x0886u16.to_be_bytes());
2748 bus.vi.regs[vi::VI_CTRL as usize] = 2;
2749 bus.vi.regs[vi::VI_V_VIDEO as usize] = 2; // h = 1
2750 let mut out16 = alloc::vec![0u8; 2 * 4];
2751 assert_eq!(bus.scanout(&mut out16), (2, 1));
2752 assert_eq!(
2753 &out16[0..4],
2754 &[0x08, 0x10, 0x18, 0xFF],
2755 "distinct channels, A=1"
2756 );
2757 assert_eq!(&out16[4..8], &[0x08, 0x10, 0x18, 0x00], "same color, A=0");
2758 }
2759
2760 /// **A blanked VI scans out nothing.** With a non-zero sentinel in the
2761 /// destination, both blank types (`TYPE == 0`, the power-on default, and
2762 /// `TYPE == 1`) leave it untouched — a zero-filled buffer would pass even if
2763 /// the blank path erroneously wrote zeroes.
2764 #[test]
2765 fn scanout_is_blank_when_the_vi_is_off() {
2766 for blank_type in [0u32, 1] {
2767 let mut bus = Bus::new();
2768 bus.vi.regs[vi::VI_CTRL as usize] = blank_type;
2769 bus.vi.regs[vi::VI_WIDTH as usize] = 2;
2770 bus.vi.regs[vi::VI_V_VIDEO as usize] = 4;
2771 let mut out = alloc::vec![0xA5u8; 16];
2772 assert_eq!(bus.scanout(&mut out), (0, 0), "TYPE {blank_type}: no frame");
2773 assert!(out.iter().all(|&b| b == 0xA5), "sentinel untouched");
2774 }
2775 }
2776
2777 /// **An undersized destination is refused up front.** Rather than write a
2778 /// truncated frame and claim full dimensions, `scanout` returns `(0, 0)` and
2779 /// writes nothing when `out` cannot hold `width * height * 4` bytes.
2780 #[test]
2781 fn scanout_refuses_an_undersized_buffer() {
2782 let mut bus = Bus::new();
2783 bus.vi.regs[vi::VI_CTRL as usize] = 3; // 32-bit
2784 bus.vi.regs[vi::VI_WIDTH as usize] = 2;
2785 bus.vi.regs[vi::VI_V_VIDEO as usize] = 4; // h = 2 -> needs 2*2*4 = 16 bytes
2786 let mut out = alloc::vec![0xFFu8; 8]; // too small (< 16)
2787 assert_eq!(bus.scanout(&mut out), (0, 0), "undersized: refused");
2788 assert!(out.iter().all(|&b| b == 0xFF), "and left untouched");
2789 }
2790
2791 /// The memo must be invisible: a scan-out that consults it has to produce exactly
2792 /// what one that never does would.
2793 ///
2794 /// This is the test that would catch a wrong key, a stale row, or an eviction that
2795 /// keeps the wrong row — none of which the geometry tests above can see, because
2796 /// they read a single pixel. It recomputes every output pixel from
2797 /// [`Bus::vi_sample_direct`], which bypasses the memo entirely, and compares the
2798 /// whole buffer.
2799 ///
2800 /// The framebuffer is filled with a pattern that varies per pixel in *both* axes
2801 /// and sets coverage bits unevenly, so a sample taken from the wrong column, the
2802 /// wrong row, or the wrong coverage class differs in its bytes. A flat fill would
2803 /// pass with the memo returning any pixel at all.
2804 #[test]
2805 fn memoized_scanout_matches_uncached_recomputation() {
2806 for &(ctrl, x_scale, y_scale) in &[
2807 // The coverage path with divot + de-dither, at the 2x horizontal upscale
2808 // that makes the memo worth having (what Super Mario 64 programs).
2809 (0x0001_3016u32, 0x0000_0200u32, 0x0000_0400u32),
2810 // Same filters, a vertical fraction as well, so both memo rows are live.
2811 (0x0001_3016, 0x0000_0200, 0x0000_0300),
2812 // Coverage path without divot, 1:1.
2813 (0x0001_3006, 0x0000_0400, 0x0000_0400),
2814 // Downscale: consecutive output pixels skip source columns, so the memo
2815 // mostly misses and the eviction path runs hot.
2816 (0x0001_3016, 0x0000_0900, 0x0000_0400),
2817 ] {
2818 let mut bus = Bus::new();
2819 let fb = 0x2000usize;
2820 let stride = 64u32;
2821 for i in 0..(stride as usize * 64) {
2822 // Vary both axes and leave coverage bit 0 set on only some pixels.
2823 let px = ((i * 7919) % 0xFFFF) as u16;
2824 let off = fb + i * 2;
2825 bus.rdram[off..off + 2].copy_from_slice(&px.to_be_bytes());
2826 }
2827 bus.vi.regs[vi::VI_CTRL as usize] = ctrl;
2828 bus.vi.regs[vi::VI_ORIGIN as usize] = fb as u32;
2829 bus.vi.regs[vi::VI_WIDTH as usize] = stride;
2830 bus.vi.regs[vi::VI_V_TOTAL as usize] = 525;
2831 bus.vi.regs[vi::VI_H_VIDEO as usize] = (108 << 16) | 0x94;
2832 bus.vi.regs[vi::VI_V_VIDEO as usize] = (34 << 16) | 0x54;
2833 bus.vi.regs[vi::VI_X_SCALE as usize] = x_scale;
2834 bus.vi.regs[vi::VI_Y_SCALE as usize] = y_scale;
2835
2836 let mut memoized = alloc::vec![0u8; 640 * 64 * 4];
2837 let (w, h) = bus.scanout_scaled(&mut memoized);
2838 assert!(
2839 w > 0 && h > 0,
2840 "ctrl {ctrl:#x}: scan-out produced no pixels"
2841 );
2842
2843 // The same walk with the memo disabled. `span == 0` makes every lookup
2844 // fall through to `vi_sample_direct`, which is the uncached path.
2845 let cfg = ViCfg {
2846 origin: bus.vi.read(vi::VI_ORIGIN) & 0x00FF_FFFF,
2847 src_stride: i32::try_from(bus.vi.read(vi::VI_WIDTH) & 0xFFF).expect("12-bit"),
2848 bpp: 2,
2849 aa_mode: (ctrl >> 8) & 0x3,
2850 divot: (ctrl >> 4) & 1 != 0,
2851 dither_filter: (ctrl >> 16) & 1 != 0,
2852 };
2853 let mut bypass = ViSampler::new(cfg, 0, -1);
2854 assert_eq!(bypass.span, 0, "the bypass sampler must cache nothing");
2855
2856 let x_add = i32::try_from(x_scale & 0xFFF).expect("12-bit field");
2857 let y_add = i32::try_from(y_scale & 0xFFF).expect("12-bit field");
2858 let x_start = i32::try_from((x_scale >> 16) & 0xFFF).expect("12-bit field");
2859 let y_start = i32::try_from((y_scale >> 16) & 0xFFF).expect("12-bit field");
2860 let (wi, hi) = (
2861 i32::try_from(w).expect("width fits i32"),
2862 i32::try_from(h).expect("height fits i32"),
2863 );
2864 for oy in 0..hi {
2865 let curry = y_start + oy * y_add;
2866 let (sy, yfrac) = (curry >> 10, (curry >> 5) & 0x1F);
2867 for ox in 0..wi {
2868 // `8` is `minhpass`: `VI_H_VIDEO`'s start is 108, the NTSC
2869 // overscan adjust takes it to `h_start = 0`, and an unclamped
2870 // `h_start` crops 8 columns. Output column 0 samples source
2871 // column 8.
2872 let x_offs = x_start + (8 + ox) * x_add;
2873 let (sx, xfrac) = (x_offs >> 10, (x_offs >> 5) & 0x1F);
2874 let want = if xfrac != 0 || yfrac != 0 {
2875 let col = bus.vi_column(&mut bypass, sx, sy, yfrac);
2876 if xfrac == 0 {
2877 col
2878 } else {
2879 let ncol = bus.vi_column(&mut bypass, sx + 1, sy, yfrac);
2880 vi_lerp3(col, ncol, xfrac)
2881 }
2882 } else {
2883 bus.vi_sample(&mut bypass, sx, sy)
2884 };
2885 let dst = usize::try_from((oy * wi + ox) * 4).expect("non-negative index");
2886 assert_eq!(
2887 &memoized[dst..dst + 3],
2888 &want,
2889 "ctrl {ctrl:#x} x_scale {x_scale:#x}: output ({ox}, {oy}) \
2890 disagrees with the uncached recomputation"
2891 );
2892 }
2893 }
2894 }
2895 }
2896
2897 /// Eviction keeps the row the walk is still using, and an unused row goes first.
2898 #[test]
2899 fn vi_sampler_evicts_the_lower_row() {
2900 let cfg = ViCfg {
2901 origin: 0,
2902 src_stride: 8,
2903 bpp: 2,
2904 aa_mode: 0,
2905 divot: false,
2906 dither_filter: false,
2907 };
2908 let mut s = ViSampler::new(cfg, 0, 7);
2909 assert_eq!(s.row_y, [None, None], "a fresh memo holds no rows");
2910 assert_eq!(s.row_slot(5), 0, "the first unused row is taken");
2911 assert_eq!(s.row_slot(6), 1, "then the second");
2912 assert_eq!(s.row_slot(5), 0, "an existing row is found, not re-taken");
2913 assert_eq!(s.row_slot(7), 0, "row 5 is evicted, not row 6");
2914 assert_eq!(s.row_y, [Some(7), Some(6)]);
2915 }
2916
2917 /// A column outside the memo's range must fall through rather than index it, and
2918 /// an over-wide range must disable the memo rather than allocate for it.
2919 #[test]
2920 fn vi_sampler_falls_through_outside_its_range() {
2921 let cfg = ViCfg {
2922 origin: 0,
2923 src_stride: 8,
2924 bpp: 2,
2925 aa_mode: 0,
2926 divot: false,
2927 dither_filter: false,
2928 };
2929 let bus = Bus::new();
2930 let mut s = ViSampler::new(cfg, 10, 12);
2931 assert_eq!(s.span, 3);
2932 // Left of, right of, and inside the range all answer; only the last is cached.
2933 let _ = bus.vi_sample(&mut s, 9, 0);
2934 let _ = bus.vi_sample(&mut s, 13, 0);
2935 let _ = bus.vi_sample(&mut s, 11, 0);
2936 assert_eq!(
2937 s.cells.iter().filter(|c| c.is_some()).count(),
2938 1,
2939 "only the in-range column is memoized"
2940 );
2941
2942 let huge = ViSampler::new(cfg, 0, i32::MAX);
2943 assert_eq!(huge.span, 0, "an over-wide range disables the memo");
2944 assert!(huge.cells.is_empty(), "and allocates nothing for it");
2945 }
2946
2947 /// The other side of the skip: when the RDP *does* have a queued command,
2948 /// `rdp_tick` must still take the bus and retire it.
2949 ///
2950 /// The stall test above proves the skip path fires; on its own that is satisfied by
2951 /// a predicate that always skips, which would be a dead RDP. This pins the positive
2952 /// case end to end through `Bus::rdp_tick` — a `Sync Pipe` (0x27) placed in RDRAM,
2953 /// consumed, `cmd_current` advanced past it, and the documented stall applied.
2954 #[test]
2955 fn a_queued_command_is_retired_through_the_bus_half() {
2956 let mut bus = Bus::new();
2957 let fifo = 0x1000u32;
2958 // Sync Pipe: opcode 0x27 in the top byte, one 64-bit word, no operands.
2959 bus.rdram[fifo as usize] = 0x27;
2960 bus.rdp.cmd_current = fifo;
2961 bus.rdp.cmd_end = fifo + 8;
2962
2963 let before = bus.rdp.commands_processed;
2964 bus.rdp_tick();
2965
2966 assert_eq!(
2967 bus.rdp.commands_processed,
2968 before + 1,
2969 "the command must be consumed, not skipped"
2970 );
2971 assert_eq!(
2972 bus.rdp.cmd_current,
2973 fifo + 8,
2974 "and the FIFO pointer advanced past it"
2975 );
2976 assert!(
2977 bus.rdp.stall > 0,
2978 "Sync Pipe applies its documented pipeline stall"
2979 );
2980 }
2981
2982 /// A stalling RDP still burns exactly one GCLK per RCP step through the skip path.
2983 ///
2984 /// The stall is the only thing the skip path **mutates**, so an ordering mistake
2985 /// lands here: checking the FIFO before the stall would leave a stalled RDP with
2986 /// nothing queued counting down forever, which no vector notices because it changes
2987 /// *when* a command retires rather than whether it matches.
2988 #[test]
2989 fn a_stalled_rdp_decrements_exactly_once_per_rcp_step() {
2990 let mut bus = Bus::new();
2991 bus.rdp.stall = 3;
2992 for expected in [2u32, 1, 0] {
2993 bus.rdp_tick();
2994 assert_eq!(
2995 bus.rdp.stall, expected,
2996 "one GCLK burned per step, never two"
2997 );
2998 }
2999 // And the step after the stall expires must reach the FIFO check rather
3000 // than wrapping the counter.
3001 bus.rdp_tick();
3002 assert_eq!(bus.rdp.stall, 0, "an expired stall stays expired");
3003 }
3004
3005 /// **`scanout_scaled` geometry + truncating convert (R-5).** A hand-computed
3006 /// 1:1 case: `VI_H_VIDEO = 108..148` (NTSC overscan `-108` → `h_start = 0`,
3007 /// `minhpass = 8`, so output column 0 samples **source column 8**), one active
3008 /// line. Source column 8 holds `0x1234`; the truncating RGBA5551→8 conversion
3009 /// (`(px>>8)&0xF8`, `(px&0x7C0)>>3`, `(px&0x3E)<<2`) gives `[10,40,D0]` with an
3010 /// opaque display alpha. A `expand5`-style replicating conversion or a wrong
3011 /// overscan offset would change the bytes, so this pins both.
3012 #[test]
3013 fn scanout_scaled_geometry_and_truncating_convert() {
3014 let mut bus = Bus::new();
3015 let fb = 0x2000usize;
3016 // Source column 8 (byte fb + 8*2), row 0, of a 48-wide framebuffer.
3017 bus.rdram[fb + 16..fb + 18].copy_from_slice(&0x1234u16.to_be_bytes());
3018 bus.vi.regs[vi::VI_CTRL as usize] = 0x0302; // 16-bit, aa_mode=REPLICATE
3019 bus.vi.regs[vi::VI_ORIGIN as usize] = fb as u32;
3020 bus.vi.regs[vi::VI_WIDTH as usize] = 48;
3021 bus.vi.regs[vi::VI_V_TOTAL as usize] = 525; // NTSC (< 550)
3022 bus.vi.regs[vi::VI_H_VIDEO as usize] = (108 << 16) | 0x94; // hres 40 -> width 25
3023 bus.vi.regs[vi::VI_V_VIDEO as usize] = (34 << 16) | 0x24; // vres 1 -> height 1
3024 bus.vi.regs[vi::VI_X_SCALE as usize] = 0x0000_0400; // 1:1
3025 bus.vi.regs[vi::VI_Y_SCALE as usize] = 0x0000_0400;
3026 let mut out = alloc::vec![0u8; 25 * 4];
3027 assert_eq!(
3028 bus.scanout_scaled(&mut out),
3029 (25, 1),
3030 "overscan-cropped geometry"
3031 );
3032 assert_eq!(
3033 &out[0..4],
3034 &[0x10, 0x40, 0xD0, 0xFF],
3035 "column 0 samples source column 8, truncating 5551->8, opaque alpha"
3036 );
3037 }
3038
3039 /// **`scanout_scaled` blanks and refuses an undersized buffer.** `TYPE` 0/1
3040 /// returns `(0, 0)` writing nothing; an `out` too small for `width*height*4`
3041 /// is refused rather than truncated.
3042 #[test]
3043 fn scanout_scaled_blanks_and_refuses_undersized() {
3044 let mut bus = Bus::new();
3045 // Blank (TYPE == 0), a valid geometry otherwise.
3046 bus.vi.regs[vi::VI_ORIGIN as usize] = 0x2000;
3047 bus.vi.regs[vi::VI_WIDTH as usize] = 48;
3048 bus.vi.regs[vi::VI_V_TOTAL as usize] = 525;
3049 bus.vi.regs[vi::VI_H_VIDEO as usize] = (108 << 16) | 0x94;
3050 bus.vi.regs[vi::VI_V_VIDEO as usize] = (34 << 16) | 0x24;
3051 bus.vi.regs[vi::VI_X_SCALE as usize] = 0x0000_0400;
3052 bus.vi.regs[vi::VI_Y_SCALE as usize] = 0x0000_0400;
3053 let mut out = alloc::vec![0xA5u8; 25 * 4];
3054 assert_eq!(bus.scanout_scaled(&mut out), (0, 0), "TYPE 0: blank");
3055 assert!(out.iter().all(|&b| b == 0xA5), "sentinel untouched");
3056 // Now enable it but give a too-small buffer.
3057 bus.vi.regs[vi::VI_CTRL as usize] = 0x0302;
3058 let mut small = alloc::vec![0xA5u8; 8]; // < 25*1*4
3059 assert_eq!(
3060 bus.scanout_scaled(&mut small),
3061 (0, 0),
3062 "undersized: refused"
3063 );
3064 assert!(small.iter().all(|&b| b == 0xA5), "and left untouched");
3065 }
3066
3067 /// **`vi_lerp3` — the 5-bit bilinear lerp with `+16 >> 5` rounding (R-5).**
3068 /// `frac = 16` is a 50 % blend; `frac = 0` is an exact passthrough of `a`; and
3069 /// `frac = 2` with a diff of 8 exercises the rounding: `(8*2 + 16) >> 5 = 1`
3070 /// (vs `0` without the `+16`), so the result is `0x29`, not `0x28`.
3071 #[test]
3072 fn vi_lerp3_blends_and_rounds() {
3073 assert_eq!(
3074 vi_lerp3([0x20, 0, 0x20], [0x28, 0, 0x28], 16),
3075 [0x24, 0, 0x24]
3076 );
3077 assert_eq!(
3078 vi_lerp3([0x10, 0x20, 0x30], [0xFF, 0xFF, 0xFF], 0),
3079 [0x10, 0x20, 0x30],
3080 "frac 0 is an exact passthrough of a"
3081 );
3082 assert_eq!(
3083 vi_lerp3([0x28, 0, 0], [0x30, 0, 0], 2),
3084 [0x29, 0, 0],
3085 "the +16 rounding rounds 0x28 up to 0x29"
3086 );
3087 }
3088
3089 /// **`vi_gamma` — the VI sqrt gamma curve (R-5).** `gamma(v) = sqrt(v << 6) << 1`:
3090 /// `gamma(0) = 0`, `gamma(0x40) = sqrt(0x1000) << 1 = 64 << 1 = 0x80`,
3091 /// `gamma(0x48) = sqrt(0x1200) << 1 = 67 << 1 = 0x86`, `gamma(0xFF) = sqrt(0x3FC0)
3092 /// << 1 = 127 << 1 = 0xFE`. The whole curve is then checked exhaustively against an
3093 /// **independent** floor-sqrt (`u32::isqrt`, a different implementation than
3094 /// `vi_integer_sqrt`), which also pins the precomputed `GAMMA_TABLE` to `vi_gamma`.
3095 /// Dropping the `<< 1` fails the anchor cases.
3096 #[test]
3097 fn vi_gamma_curve() {
3098 assert_eq!(vi_gamma(0), 0);
3099 assert_eq!(vi_gamma(0x40), 0x80);
3100 assert_eq!(vi_gamma(0x48), 0x86);
3101 assert_eq!(vi_gamma(0xFF), 0xFE);
3102 for v in 0..=255u8 {
3103 let reference = ((u32::from(v) << 6).isqrt() << 1) as u8;
3104 assert_eq!(vi_gamma(v), reference, "vi_gamma({v}) vs isqrt reference");
3105 assert_eq!(GAMMA_TABLE[usize::from(v)], vi_gamma(v), "LUT entry {v}");
3106 }
3107 }
3108}
3109
3110#[cfg(test)]
3111mod pi_tests {
3112 use super::*;
3113 use rustyn64_cart::pi::{PI_CART_ADDR, PI_DRAM_ADDR, PI_STATUS, PI_WR_LEN};
3114
3115 /// A `PI_WR_LEN` write must copy **cart → RDRAM**, `len + 1` bytes, and
3116 /// raise the PI interrupt line into the MI.
3117 ///
3118 /// This is the path n64-systemtest uses to load the rest of its own ELF, so
3119 /// it is the difference between the suite reporting a number and not
3120 /// starting at all.
3121 #[test]
3122 fn a_pi_wr_len_write_copies_cart_to_rdram_and_raises_the_interrupt() {
3123 let mut bus = Bus::new();
3124 // A cart whose ROM is a recognizable ramp.
3125 let mut rom = alloc::vec![0u8; 0x100];
3126 rom[..4].copy_from_slice(&[0x80, 0x37, 0x12, 0x40]); // .z64 magic
3127 for (i, b) in rom.iter_mut().enumerate().skip(0x40) {
3128 *b = i as u8;
3129 }
3130 bus.cart = rustyn64_cart::Cart::load(&rom).expect("loadable");
3131
3132 bus.pi_write_word(PI_DRAM_ADDR, 0x1000);
3133 bus.pi_write_word(PI_CART_ADDR, 0x1000_0040);
3134 bus.pi_write_word(PI_WR_LEN, 15); // 16 bytes
3135
3136 for i in 0..16u32 {
3137 assert_eq!(
3138 bus.rdram[(0x1000 + i) as usize],
3139 (0x40 + i) as u8,
3140 "byte {i} of the DMA"
3141 );
3142 }
3143 assert_eq!(bus.rdram[0x1000 + 16], 0, "and exactly 16, not 17");
3144 assert!(bus.rcp.mi_intr.pi, "completion raises the PI line");
3145 assert_eq!(
3146 bus.pi.read(PI_STATUS) & rustyn64_cart::pi::STATUS_DMA_BUSY,
3147 0,
3148 "and the DMA is no longer busy"
3149 );
3150 }
3151
3152 /// `len + 1`: a length write of 0 moves **one** byte. Off by one here
3153 /// corrupts the last byte of every block, which presents as memory
3154 /// corruption rather than as a DMA bug.
3155 #[test]
3156 fn a_zero_length_write_transfers_exactly_one_byte() {
3157 let mut bus = Bus::new();
3158 let mut rom = alloc::vec![0u8; 0x80];
3159 rom[..4].copy_from_slice(&[0x80, 0x37, 0x12, 0x40]);
3160 rom[0x40] = 0xAB;
3161 rom[0x41] = 0xCD;
3162 bus.cart = rustyn64_cart::Cart::load(&rom).expect("loadable");
3163
3164 bus.pi_write_word(PI_DRAM_ADDR, 0x2000);
3165 bus.pi_write_word(PI_CART_ADDR, 0x1000_0040);
3166 bus.pi_write_word(PI_WR_LEN, 0);
3167
3168 assert_eq!(bus.rdram[0x2000], 0xAB, "one byte moved");
3169 assert_eq!(bus.rdram[0x2001], 0x00, "and only one");
3170 }
3171
3172 /// The PI registers are reachable through the ordinary CPU bus, which is how
3173 /// guest code drives them.
3174 #[test]
3175 fn the_pi_registers_are_reachable_from_the_cpu_bus() {
3176 let mut bus = Bus::new();
3177 // Word-wise, as real code does. Note the value read back is rounded
3178 // DOWN to a doubleword -- the DRAM side ignores bits 2:0.
3179 bus.pi_write_word(PI_DRAM_ADDR, 0x1234);
3180 assert_eq!(bus.read_u32(PI_DRAM_ADDR), 0x1230, "doubleword-aligned");
3181 // An already-aligned value survives untouched.
3182 bus.pi_write_word(PI_DRAM_ADDR, 0x1238);
3183 assert_eq!(bus.read_u32(PI_DRAM_ADDR), 0x1238);
3184 // And byte reads select within the word.
3185 assert_eq!(bus.read_u8(PI_DRAM_ADDR + 3), 0x38);
3186 assert_eq!(bus.read_u8(PI_DRAM_ADDR + 2), 0x12);
3187 }
3188
3189 /// **A guest `sw` to a length register must start exactly ONE DMA.**
3190 ///
3191 /// The default `write_u32` composes four `write_u8` calls. With PI registers
3192 /// handled byte-wise, a normal word store started **four** transfers, each
3193 /// with a partly assembled length — so every PI transfer was wrong, and the
3194 /// symptom was memory corruption rather than anything that looked like DMA.
3195 #[test]
3196 fn a_word_store_to_a_length_register_starts_exactly_one_dma() {
3197 let mut bus = Bus::new();
3198 let mut rom = alloc::vec![0u8; 0x200];
3199 rom[..4].copy_from_slice(&[0x80, 0x37, 0x12, 0x40]);
3200 for (i, b) in rom.iter_mut().enumerate().skip(0x40) {
3201 *b = i as u8;
3202 }
3203 bus.cart = rustyn64_cart::Cart::load(&rom).expect("loadable");
3204
3205 bus.write_u32(PI_DRAM_ADDR, 0x1000);
3206 bus.write_u32(PI_CART_ADDR, 0x1000_0040);
3207 // The write that matters: through the ordinary CPU word path.
3208 bus.write_u32(PI_WR_LEN, 7); // 8 bytes
3209
3210 for i in 0..8u32 {
3211 assert_eq!(
3212 bus.rdram[(0x1000 + i) as usize],
3213 (0x40 + i) as u8,
3214 "byte {i}"
3215 );
3216 }
3217 assert_eq!(
3218 bus.rdram[0x1000 + 8],
3219 0,
3220 "exactly 8 bytes -- a per-byte trigger would have run four transfers \
3221 with lengths 0x07000000+1, 0x00070000+1, ... and scribbled far past here"
3222 );
3223 }
3224
3225 /// **Clearing the PI interrupt must lower the MI line.** Only a completion
3226 /// used to update it, and a `PI_STATUS` clear starts no transfer — so the
3227 /// line stayed asserted, `IP2` stuck high, and any interrupt-driven loader
3228 /// hung forever.
3229 #[test]
3230 fn clearing_the_pi_interrupt_lowers_the_mi_line() {
3231 let mut bus = Bus::new();
3232 let mut rom = alloc::vec![0u8; 0x80];
3233 rom[..4].copy_from_slice(&[0x80, 0x37, 0x12, 0x40]);
3234 bus.cart = rustyn64_cart::Cart::load(&rom).expect("loadable");
3235
3236 bus.write_u32(PI_WR_LEN, 0);
3237 assert!(bus.rcp.mi_intr.pi, "completion raised it");
3238
3239 bus.write_u32(PI_STATUS, rustyn64_cart::pi::STATUS_W_CLR_INTR);
3240 assert!(!bus.pi.interrupt(), "the PI cleared its own flag");
3241 assert!(
3242 !bus.rcp.mi_intr.pi,
3243 "and the MI line must follow -- otherwise IP2 stays high forever"
3244 );
3245 }
3246
3247 /// **A direct-I/O write to the DOM2 window persists to the SRAM save and
3248 /// reads back.** SRAM lives on the PI bus at `0x0800_0000`; a `SW` there must
3249 /// store into the save backing (the read-only ROM window ignores writes).
3250 /// Reads during the write's busy window return the latch, so the test ticks
3251 /// past `PI_WRITE_CYCLES` before reading the persisted value.
3252 #[test]
3253 fn a_direct_io_write_persists_to_the_sram_save() {
3254 let mut bus = Bus::new();
3255 *bus.cart.save_device_mut() =
3256 rustyn64_cart::save::SaveDevice::new(rustyn64_cart::SaveType::Sram);
3257
3258 CpuBus::write_u32(&mut bus, 0x0800_1000, 0xDEAD_BEEF);
3259 for _ in 0..Bus::PI_WRITE_CYCLES {
3260 bus.pi_tick();
3261 }
3262 assert_eq!(
3263 CpuBus::read_u32(&mut bus, 0x0800_1000),
3264 0xDEAD_BEEF,
3265 "the SRAM store must survive the direct-I/O finalization"
3266 );
3267 // And it is visible in the persistable backing (for the host save file).
3268 assert_eq!(
3269 &bus.cart.save()[0x1000..0x1004],
3270 &0xDEAD_BEEFu32.to_be_bytes()
3271 );
3272 }
3273
3274 /// **A controller read runs end to end through the SI joybus.** The CPU
3275 /// stages a joybus frame in RDRAM, DMAs it to PIF RAM (`SI_PIF_AD_WR64B`),
3276 /// then triggers the read (`SI_PIF_AD_RD64B`) — which makes the PIF execute
3277 /// the handshakes and DMA the replies back. The port-0 controller word must
3278 /// appear at the frame's reply bytes, and the SI interrupt must fire.
3279 #[test]
3280 fn a_controller_read_runs_through_the_si_joybus() {
3281 const SI_DRAM_ADDR: u32 = Bus::SI_BASE;
3282 const SI_RD64B: u32 = Bus::SI_BASE + 0x04;
3283 const SI_WR64B: u32 = Bus::SI_BASE + 0x10;
3284 const SI_STATUS: u32 = Bus::SI_BASE + 0x18;
3285 const FRAME: u32 = 0x2000;
3286
3287 let mut bus = Bus::new();
3288 bus.controllers[0] = 0x8000_1234; // A pressed, stick (0x12, 0x34)
3289
3290 // Joybus frame: channel 0 = { TX=1, RX=4, cmd=0x01, 4 reply bytes };
3291 // the command byte (PIF RAM 0x3F) bit 0 = "run".
3292 bus.rdram[FRAME as usize] = 0x01; // TX len
3293 bus.rdram[FRAME as usize + 1] = 0x04; // RX len
3294 bus.rdram[FRAME as usize + 2] = 0x01; // 0x01 Controller State
3295 bus.rdram[FRAME as usize + 0x3F] = 0x01; // command byte: run
3296
3297 CpuBus::write_u32(&mut bus, SI_DRAM_ADDR, FRAME);
3298 CpuBus::write_u32(&mut bus, SI_WR64B, 0); // DMA RDRAM → PIF RAM
3299 assert!(bus.rcp.mi_intr.si, "the WR64B DMA raises the SI interrupt");
3300 CpuBus::write_u32(&mut bus, SI_STATUS, 0); // ack
3301 CpuBus::write_u32(&mut bus, SI_RD64B, 0); // execute + DMA PIF → RDRAM
3302
3303 // The reply bytes (frame offset 3..7) hold the packed port-0 word.
3304 assert_eq!(
3305 &bus.rdram[FRAME as usize + 3..FRAME as usize + 7],
3306 &[0x80, 0x00, 0x12, 0x34],
3307 "the controller state reached RDRAM through the joybus"
3308 );
3309 assert!(
3310 bus.rcp.mi_intr.si,
3311 "the RD64B execution raises the SI interrupt"
3312 );
3313 }
3314
3315 /// A **byte** write to a trigger or status register is dropped, not
3316 /// assembled. `PI_STATUS`'s read bits (busy, interrupt) do not correspond to
3317 /// its write bits (reset, clear-interrupt), so reading it back to fill in
3318 /// the other three bytes fabricates command strobes out of status flags.
3319 #[test]
3320 fn byte_writes_to_the_trigger_and_status_registers_are_dropped() {
3321 let mut bus = Bus::new();
3322 let mut rom = alloc::vec![0u8; 0x80];
3323 rom[..4].copy_from_slice(&[0x80, 0x37, 0x12, 0x40]);
3324 bus.cart = rustyn64_cart::Cart::load(&rom).expect("loadable");
3325
3326 bus.write_u8(PI_WR_LEN + 3, 0xFF);
3327 assert!(!bus.rcp.mi_intr.pi, "no DMA was started by a byte write");
3328
3329 // Raise the interrupt, then confirm a byte write to STATUS cannot
3330 // fabricate a clear-interrupt strobe out of the busy/interrupt bits.
3331 bus.write_u32(PI_WR_LEN, 0);
3332 assert!(bus.rcp.mi_intr.pi);
3333 bus.write_u8(PI_STATUS + 3, 0x00);
3334 assert!(bus.rcp.mi_intr.pi, "a byte write to STATUS did nothing");
3335
3336 // The address registers CAN be assembled, since they only latch.
3337 bus.write_u8(PI_DRAM_ADDR + 3, 0x18);
3338 assert_eq!(bus.read_u32(PI_DRAM_ADDR) & 0xFF, 0x18);
3339 }
3340
3341 /// **A guest must not be able to panic the emulator.** A word write starting
3342 /// in the last three bytes of the `ISViewer` window is accepted by the range
3343 /// check but would index past the backing slice.
3344 ///
3345 /// The address and value both come from guest code, so this is reachable by
3346 /// any ROM, not just a malformed one.
3347 #[test]
3348 fn a_word_write_at_the_end_of_the_isviewer_window_does_not_panic() {
3349 let mut bus = Bus::new();
3350 let last = Bus::ISVIEWER_BASE + 0x20 + Bus::ISVIEWER_LEN as u32 - 1;
3351 for addr in [last - 3, last - 2, last - 1, last] {
3352 bus.write_u32(addr, 0xDEAD_BEEF);
3353 let _ = bus.read_u32(addr);
3354 }
3355 // ...and reads just past the window are 0 rather than a panic.
3356 assert_eq!(bus.read_u8(last), 0xEF, "the aligned tail write landed");
3357 }
3358
3359 /// The `ISViewer` window must round-trip a written word, because that is
3360 /// exactly the probe n64-systemtest uses to decide whether the channel
3361 /// exists — `isviewer::detect()` writes `0x12345678` and reads it back. If
3362 /// it fails, the suite falls back to a framebuffer console we cannot read.
3363 #[test]
3364 fn the_isviewer_window_round_trips_the_detection_magic() {
3365 let mut bus = Bus::new();
3366 bus.write_u32(Bus::ISVIEWER_BUF, 0x1234_5678);
3367 assert_eq!(
3368 bus.read_u32(Bus::ISVIEWER_BUF),
3369 0x1234_5678,
3370 "detect() must succeed or the suite picks the framebuffer instead"
3371 );
3372 }
3373
3374 /// Text is captured on the **length** write, not on the buffer writes, so a
3375 /// whole line is published at once. Capturing per buffer write would
3376 /// interleave partial lines and make the output unreadable.
3377 #[test]
3378 fn text_is_captured_on_the_length_write_not_the_buffer_writes() {
3379 let mut bus = Bus::new();
3380 // "OK!\n" packed big-endian, as `isviewer::pack` does.
3381 bus.write_u32(Bus::ISVIEWER_BUF, u32::from_be_bytes(*b"OK!\n"));
3382 assert!(
3383 bus.isviewer_output().is_empty(),
3384 "nothing published until the length write"
3385 );
3386 bus.write_u32(Bus::ISVIEWER_WRITE_LEN, 4);
3387 assert_eq!(bus.isviewer_output(), b"OK!\n");
3388
3389 // And a second line appends rather than replacing.
3390 bus.write_u32(Bus::ISVIEWER_BUF, u32::from_be_bytes(*b"two\n"));
3391 bus.write_u32(Bus::ISVIEWER_WRITE_LEN, 4);
3392 assert_eq!(bus.isviewer_output(), b"OK!\ntwo\n");
3393 }
3394
3395 /// A length longer than the buffer is clamped rather than panicking — the
3396 /// value comes from guest code and must not be trusted.
3397 #[test]
3398 fn an_oversized_length_write_is_clamped() {
3399 let mut bus = Bus::new();
3400 bus.write_u32(Bus::ISVIEWER_WRITE_LEN, 0xFFFF_FFFF);
3401 assert_eq!(bus.isviewer_output().len(), Bus::ISVIEWER_LEN);
3402 }
3403
3404 /// **The RI register block round-trips.** Eight registers at
3405 /// `0x0470_0000..0x0470_0020` (N64brew *RDRAM Interface* §Registers). Before
3406 /// this block was decoded every RI address read back `0`, which is not inert:
3407 /// the cartridge's IPL3 opens by reading `RI_SELECT` (`0x0470_000C`) to decide
3408 /// whether RDRAM has already been brought up, so an undecoded block silently
3409 /// forced the cold-init path on every boot (ledger R-18).
3410 ///
3411 /// Each register is given a *distinct* value so a decode that collapses them
3412 /// onto one another — or drops the low address bits — fails rather than
3413 /// passing on a shared zero.
3414 #[test]
3415 fn the_ri_register_block_round_trips() {
3416 let mut bus = Bus::new();
3417 for i in 0..8u32 {
3418 CpuBus::write_u32(&mut bus, Bus::RI_BASE + i * 4, 0x1234_0000 + i);
3419 }
3420 for i in 0..8u32 {
3421 assert_eq!(
3422 CpuBus::read_u32(&mut bus, Bus::RI_BASE + i * 4),
3423 0x1234_0000 + i,
3424 "RI register {i} must read back what was written"
3425 );
3426 }
3427 }
3428
3429 /// **A narrow store to an RI register takes the size-blind RCP path**, like
3430 /// every other RCP block — it is NOT dropped, and it does not need a
3431 /// per-block arm in [`Bus::write_u8`].
3432 ///
3433 /// Pinned because "sub-word writes to RI fall through or are silently
3434 /// dropped" is a reasonable-sounding worry that is wrong here, and only a test
3435 /// settles it. Narrow CPU stores reach the bus through `write_sized`, which
3436 /// funnels them to `write_u32(addr & !3, word)` after shifting the register
3437 /// into its byte lane — the RCP latches the whole word and ignores the access
3438 /// size (N64brew *Memory map* §Physical Memory Map accesses). So a byte store
3439 /// of `0x12` at `RI_SELECT + 3` must leave `0x0000_0012`, not `0x12` merged
3440 /// into a previous value and not nothing at all.
3441 #[test]
3442 fn a_narrow_store_to_ri_latches_the_whole_word() {
3443 let mut bus = Bus::new();
3444 CpuBus::write_u32(&mut bus, 0x0470_000C, 0xFFFF_FFFF);
3445 // Byte lane 3 (the low byte of the word).
3446 bus.write_sized(0x0470_000F, 1, 0x12);
3447 assert_eq!(
3448 CpuBus::read_u32(&mut bus, 0x0470_000C),
3449 0x0000_0012,
3450 "the RCP latches the whole shifted word, zeroing the untouched bytes"
3451 );
3452 // ... and a halfword store into the upper lane behaves the same way.
3453 CpuBus::write_u32(&mut bus, 0x0470_000C, 0xFFFF_FFFF);
3454 bus.write_sized(0x0470_000C, 2, 0xABCD);
3455 assert_eq!(
3456 CpuBus::read_u32(&mut bus, 0x0470_000C),
3457 0xABCD_0000,
3458 "a halfword store shifts into its lane and zero-fills the rest"
3459 );
3460 }
3461
3462 /// **`RI_SELECT` specifically reads back**, since it is the one RI register a
3463 /// real boot depends on: IPL3 branches on it. Asserted separately from the
3464 /// round-trip above so the intent survives if that test is ever narrowed.
3465 #[test]
3466 fn ri_select_reads_back_what_ipl3_writes() {
3467 let mut bus = Bus::new();
3468 // The value IPL3 configures: TSEL = 0b0001, RSEL = 0b0100 (N64brew
3469 // *RDRAM Interface* §RI_SELECT, "Extra Details").
3470 CpuBus::write_u32(&mut bus, 0x0470_000C, 0x14);
3471 assert_eq!(CpuBus::read_u32(&mut bus, 0x0470_000C), 0x14);
3472 }
3473
3474 /// **The RSP powers up halted.** Reading `SP_STATUS` as zero claims a
3475 /// running RSP, which is false; n64-systemtest's `StartupTest` reads `0x1`.
3476 #[test]
3477 fn sp_status_reports_the_rsp_halted_at_power_on() {
3478 let mut bus = Bus::new();
3479 assert_eq!(
3480 bus.read_u32(Bus::SP_STATUS) & rustyn64_rsp::sp::STATUS_HALTED,
3481 rustyn64_rsp::sp::STATUS_HALTED,
3482 "the RSP idles halted until the CPU clears it"
3483 );
3484 }
3485
3486 /// **`SP_RD_LEN` moves RDRAM into SPMEM**, and the length word is not a
3487 /// plain byte count: bits 11:0 are bytes-per-row minus one.
3488 #[test]
3489 fn an_sp_dma_moves_rdram_into_spmem() {
3490 let mut bus = Bus::new();
3491 for (i, b) in bus.rdram[0x100..0x108].iter_mut().enumerate() {
3492 *b = 0xA0 + i as u8;
3493 }
3494 bus.write_u32(Bus::SP_REGS_BASE + 4, 0x100);
3495 bus.write_u32(Bus::SP_REGS_BASE, 0);
3496 bus.write_u32(Bus::SP_REGS_BASE + 8, 7); // 8 bytes, one row
3497 assert_eq!(
3498 core::array::from_fn::<u8, 8, _>(|i| bus.rsp.mem_read(i as u32)),
3499 [0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7]
3500 );
3501 }
3502
3503 /// `SP_WR_LEN` is the other direction — SPMEM into RDRAM.
3504 #[test]
3505 fn an_sp_dma_moves_spmem_into_rdram() {
3506 let mut bus = Bus::new();
3507 for i in 0..8u32 {
3508 bus.rsp.mem_write(i, 0x50 + i as u8);
3509 }
3510 bus.write_u32(Bus::SP_REGS_BASE + 4, 0x200);
3511 bus.write_u32(Bus::SP_REGS_BASE, 0);
3512 bus.write_u32(Bus::SP_REGS_BASE + 12, 7);
3513 assert_eq!(
3514 &bus.rdram[0x200..0x208],
3515 &[0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57]
3516 );
3517 }
3518
3519 /// **`count` and `skip` are real fields, not padding.** A 2D block copy
3520 /// moves `count + 1` rows and steps the RDRAM pointer by `skip` between
3521 /// them, while SPMEM stays contiguous. Reading only bits 11:0 silently
3522 /// drops every row after the first.
3523 #[test]
3524 fn an_sp_dma_honors_the_count_and_skip_fields() {
3525 let mut bus = Bus::new();
3526 // Two rows of 8, separated by an 8-byte gap in RDRAM.
3527 for i in 0..8 {
3528 bus.rdram[0x300 + i] = 0x10 + i as u8;
3529 bus.rdram[0x310 + i] = 0x20 + i as u8;
3530 }
3531 bus.write_u32(Bus::SP_REGS_BASE + 4, 0x300);
3532 bus.write_u32(Bus::SP_REGS_BASE, 0);
3533 // length = 7 (8 bytes), count = 1 (two rows), skip = 8.
3534 bus.write_u32(Bus::SP_REGS_BASE + 8, 7 | (1 << 12) | (8 << 20));
3535 assert_eq!(
3536 core::array::from_fn::<u8, 8, _>(|i| bus.rsp.mem_read(i as u32)),
3537 [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17]
3538 );
3539 assert_eq!(
3540 core::array::from_fn::<u8, 8, _>(|i| bus.rsp.mem_read(8 + i as u32)),
3541 [0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27],
3542 "the second row must land contiguously in SPMEM"
3543 );
3544 }
3545
3546 /// `SP_MEM_ADDR` bit 12 selects **IMEM**, and the 12-bit offset wraps within
3547 /// whichever half was chosen rather than spilling across into the other.
3548 #[test]
3549 fn sp_mem_addr_bit_12_selects_imem() {
3550 let mut bus = Bus::new();
3551 bus.rdram[0x400] = 0x99;
3552 bus.write_u32(Bus::SP_REGS_BASE + 4, 0x400);
3553 bus.write_u32(Bus::SP_REGS_BASE, 0x1000); // IMEM
3554 bus.write_u32(Bus::SP_REGS_BASE + 8, 7);
3555 assert_eq!(bus.rsp.mem_read(0x1000), 0x99, "landed in IMEM");
3556 assert_eq!(bus.rsp.mem_read(0), 0, "and NOT in DMEM");
3557 }
3558
3559 /// Read a word out of SPMEM the way the CPU does, for the tests below.
3560 fn spmem_word(bus: &mut Bus, off: u32) -> u32 {
3561 bus.read_u32(Bus::SPMEM_BASE + off)
3562 }
3563
3564 /// **A byte store to the RCP's internal bus writes 32 bits.**
3565 ///
3566 /// The values are n64-systemtest's, not ours (`sp_memory::SB`): with
3567 /// `$3 = 0x1234_5678`, storing a byte at offsets 0, 5, 10 and 15 leaves the
3568 /// register *shifted into the addressed lane* in each of the four words,
3569 /// wiping the rest. Byte-exact semantics would leave `0x7800_0000`,
3570 /// `0x0078_0000`, `0x0000_7800`, `0x0000_0078` instead -- so this test fails
3571 /// in all four words if the size-blind path is lost.
3572 #[test]
3573 fn a_byte_store_to_spmem_writes_the_whole_shifted_word() {
3574 let mut bus = Bus::new();
3575 for (i, off) in [0u32, 5, 10, 15].iter().enumerate() {
3576 bus.write_sized(Bus::SPMEM_BASE + off, 1, 0x1234_5678);
3577 let _ = i;
3578 }
3579 assert_eq!(spmem_word(&mut bus, 0), 0x7800_0000);
3580 assert_eq!(spmem_word(&mut bus, 4), 0x5678_0000);
3581 assert_eq!(spmem_word(&mut bus, 8), 0x3456_7800);
3582 assert_eq!(spmem_word(&mut bus, 12), 0x1234_5678);
3583 }
3584
3585 /// The same rule for halfwords, and it **destroys the untouched half** --
3586 /// `sp_memory::SH` presets `0xDEAD_BEEF`/`0xBADD_ECAF` and expects both gone.
3587 #[test]
3588 fn a_halfword_store_to_spmem_writes_the_whole_shifted_word() {
3589 let mut bus = Bus::new();
3590 bus.write_u32(Bus::SPMEM_BASE, 0xDEAD_BEEF);
3591 bus.write_u32(Bus::SPMEM_BASE + 4, 0xBADD_ECAF);
3592
3593 bus.write_sized(Bus::SPMEM_BASE, 2, 0x1234_5678);
3594 bus.write_sized(Bus::SPMEM_BASE + 6, 2, 0x1234_5678);
3595
3596 assert_eq!(spmem_word(&mut bus, 0), 0x5678_0000);
3597 assert_eq!(spmem_word(&mut bus, 4), 0x1234_5678);
3598 }
3599
3600 /// **A 64-bit store touches four bytes, not eight.** The RCP takes the first
3601 /// word off the bus and drops the second (`sp_memory::SD`), so the preset
3602 /// second word must survive intact -- which is what distinguishes this from
3603 /// a plain 64-bit write.
3604 #[test]
3605 fn a_doubleword_store_to_spmem_writes_only_the_upper_word() {
3606 let mut bus = Bus::new();
3607 bus.write_u32(Bus::SPMEM_BASE, 0xDEAD_BEEF);
3608 bus.write_u32(Bus::SPMEM_BASE + 4, 0xBADD_ECAF);
3609
3610 bus.write_sized(Bus::SPMEM_BASE, 8, 0xABCD_EF98_7654_3210);
3611
3612 assert_eq!(spmem_word(&mut bus, 0), 0xABCD_EF98);
3613 assert_eq!(
3614 spmem_word(&mut bus, 4),
3615 0xBADD_ECAF,
3616 "the low word is dropped on the floor, not stored"
3617 );
3618 }
3619
3620 /// **RDRAM is not size-blind.** The RI passes the low address bits and the
3621 /// access size to the RDRAM devices, which build a real byte mask. Without
3622 /// this the size-blind rule would corrupt every ordinary narrow store, so
3623 /// the exclusion is load-bearing rather than an optimization.
3624 #[test]
3625 fn a_byte_store_to_rdram_writes_one_byte() {
3626 let mut bus = Bus::new();
3627 bus.write_u32(0x100, 0xDEAD_BEEF);
3628 bus.write_sized(0x101, 1, 0x1234_5678);
3629 assert_eq!(bus.read_u32(0x100), 0xDE78_BEEF);
3630 }
3631
3632 /// The 8 KiB of DMEM+IMEM **repeats** up to `0x0404_0000`, where the SP
3633 /// registers begin. n64-systemtest writes at `0x3E000` and reads the result
3634 /// back at offset 0 (`sp_memory::SW (out of bounds)`).
3635 #[test]
3636 fn the_spmem_window_repeats_every_8_kib() {
3637 let mut bus = Bus::new();
3638 bus.write_u32(Bus::SPMEM_BASE, 0x0123_4567);
3639 bus.write_u32(Bus::SPMEM_BASE + 0x1000, 0x89AB_CDEF);
3640 bus.write_u32(Bus::SPMEM_BASE + 0x3E000, 0x7654_3210);
3641
3642 assert_eq!(
3643 spmem_word(&mut bus, 0),
3644 0x7654_3210,
3645 "0x3E000 is offset 0 seen for the 31st time"
3646 );
3647 assert_eq!(spmem_word(&mut bus, 0x1000), 0x89AB_CDEF, "IMEM untouched");
3648 assert_eq!(spmem_word(&mut bus, 0x3E000), 0x7654_3210);
3649 }
3650
3651 /// **`SP_SEMAPHORE` is taken once per access, not once per byte.**
3652 ///
3653 /// The register has a side effect on read, so composing a word from four
3654 /// byte reads took the mutex four times and returned 1 where hardware
3655 /// returns 0 — n64-systemtest's `SP Semaphore Register (CPU only)` fails on
3656 /// exactly that. The suite also checks that the value written is irrelevant.
3657 #[test]
3658 fn reading_the_semaphore_as_a_word_takes_it_exactly_once() {
3659 const SEMAPHORE: u32 = Bus::SP_REGS_BASE + 0x1C;
3660 for written in [0u32, 1, 0xFFFF_FFFF] {
3661 let mut bus = Bus::new();
3662 bus.write_u32(SEMAPHORE, written);
3663 assert_eq!(bus.read_u32(SEMAPHORE), 0, "the first word read acquires");
3664 assert_eq!(bus.read_u32(SEMAPHORE), 1, "and it stays taken");
3665 }
3666 }
3667
3668 /// A `SP_STATUS` write raises and acknowledges the **MI's SP line**, and the
3669 /// guest can see it in `MI_INTERRUPT`.
3670 #[test]
3671 fn sp_status_drives_the_mi_interrupt_line() {
3672 const SET_INTR: u32 = 1 << 4;
3673 const CLR_INTR: u32 = 1 << 3;
3674 const MI_INTERRUPT: u32 = Bus::MI_BASE + 0x08;
3675 let mut bus = Bus::new();
3676
3677 bus.write_u32(Bus::SP_STATUS, SET_INTR);
3678 assert_eq!(bus.read_u32(MI_INTERRUPT) & 1, 1, "SP line raised");
3679 bus.write_u32(Bus::SP_STATUS, CLR_INTR);
3680 assert_eq!(bus.read_u32(MI_INTERRUPT) & 1, 0, "and acknowledged");
3681
3682 // Set and clear together leaves it alone, as for every other flag.
3683 bus.write_u32(Bus::SP_STATUS, SET_INTR);
3684 bus.write_u32(Bus::SP_STATUS, SET_INTR | CLR_INTR);
3685 assert_eq!(bus.read_u32(MI_INTERRUPT) & 1, 1, "unchanged");
3686 }
3687
3688 /// `MI_MASK` writes as clear/set pairs and reads back as a flag word, and
3689 /// only a masked-in line reaches `IP2`.
3690 #[test]
3691 fn the_mi_mask_gates_the_interrupt_line() {
3692 const MI_MASK: u32 = Bus::MI_BASE + 0x0C;
3693 const SET_SP: u32 = 1 << 1;
3694 const CLR_SP: u32 = 1 << 0;
3695 let mut bus = Bus::new();
3696
3697 bus.write_u32(Bus::SP_STATUS, 1 << 4); // raise SP
3698 assert!(!bus.poll_irq(), "an unmasked line must not reach IP2");
3699
3700 bus.write_u32(MI_MASK, SET_SP);
3701 assert_eq!(bus.read_u32(MI_MASK) & 1, 1, "the mask reads back");
3702 assert!(bus.poll_irq(), "masked in, so IP2 asserts");
3703
3704 bus.write_u32(MI_MASK, CLR_SP);
3705 assert!(!bus.poll_irq(), "masked out again");
3706 }
3707
3708 /// The MI block is four registers **mirrored** on the low four address bits,
3709 /// so `MI_VERSION` is readable at every `+0x10` step.
3710 #[test]
3711 fn the_mi_registers_mirror_every_sixteen_bytes() {
3712 let mut bus = Bus::new();
3713 assert_eq!(bus.read_u32(Bus::MI_BASE + 0x04), Bus::MI_VERSION_VALUE);
3714 assert_eq!(
3715 bus.read_u32(Bus::MI_BASE + 0x14),
3716 Bus::MI_VERSION_VALUE,
3717 "mirrored one block up"
3718 );
3719 assert_eq!(bus.read_u32(Bus::MI_BASE + 0x1004), Bus::MI_VERSION_VALUE);
3720 }
3721
3722 /// **The PI external bus is 16 bits wide and the RCP ignores access size**,
3723 /// so a byte or halfword read returns data two bytes further on than the
3724 /// address asked for, while a word read does not. This is a hardware bug we
3725 /// must reproduce, not an approximation.
3726 ///
3727 /// n64-systemtest pins all three against a ROM beginning
3728 /// `01 23 45 67 89 AB CD EF`.
3729 #[test]
3730 fn a_pi_bus_sub_word_read_lands_two_bytes_late() {
3731 let mut bus = Bus::new();
3732 // A `.z64` header, then a byte-index pattern from 0x40 on.
3733 let mut rom = alloc::vec![0u8; 0x1000];
3734 rom[0..4].copy_from_slice(&[0x80, 0x37, 0x12, 0x40]);
3735 for (i, b) in rom.iter_mut().enumerate().skip(0x40) {
3736 *b = (i & 0xFF) as u8;
3737 }
3738 bus.cart = rustyn64_cart::Cart::load(&rom).expect("valid z64");
3739 let base = 0x1000_0040u32;
3740
3741 // A WORD read is unaffected: the access puts its own address on the bus.
3742 assert_eq!(
3743 bus.read_u32(base),
3744 0x4041_4243,
3745 "a word read is the four bytes at its own address"
3746 );
3747
3748 // A BYTE read at offset 2 returns the byte at offset 4.
3749 assert_eq!(bus.read_u8(base + 2), 0x44, "offset 2 reads byte 4");
3750 assert_eq!(bus.read_u8(base + 3), 0x45, "offset 3 reads byte 5");
3751 // ...but offsets 0 and 1 are unaffected: bit 1 is clear.
3752 assert_eq!(bus.read_u8(base), 0x40);
3753 assert_eq!(bus.read_u8(base + 1), 0x41);
3754
3755 // A HALFWORD needs no special case -- it is two byte reads, and both
3756 // land correctly by the same rule.
3757 let hi = (u16::from(bus.read_u8(base + 2)) << 8) | u16::from(bus.read_u8(base + 3));
3758 assert_eq!(hi, 0x4445, "halfword at offset 2 reads offset 4");
3759 }
3760
3761 /// The quirk is confined to the PI window. RDRAM must be untouched, or every
3762 /// ordinary load in the machine shifts by two bytes.
3763 #[test]
3764 fn the_pi_off_by_two_does_not_leak_into_rdram() {
3765 let mut bus = Bus::new();
3766 for (i, b) in bus.rdram[0..8].iter_mut().enumerate() {
3767 *b = i as u8;
3768 }
3769 assert_eq!(bus.read_u8(0x0000_0002), 2, "RDRAM is NOT shifted");
3770 assert_eq!(bus.read_u32(0x0000_0000), 0x0001_0203);
3771 }
3772
3773 /// **A PI direct-I/O write latches and shadows the whole bus.** While it is
3774 /// in flight, reads from *any* PI address return the value being written --
3775 /// including from ROM, which the PI has no way of knowing is read-only.
3776 #[test]
3777 fn a_pi_write_is_latched_and_shadows_reads_until_it_finalizes() {
3778 let mut bus = Bus::new();
3779 let mut rom = alloc::vec![0u8; 0x1000];
3780 rom[0..4].copy_from_slice(&[0x80, 0x37, 0x12, 0x40]);
3781 for (i, b) in rom.iter_mut().enumerate().skip(0x40) {
3782 *b = (i & 0xFF) as u8;
3783 }
3784 bus.cart = rustyn64_cart::Cart::load(&rom).expect("valid z64");
3785 let base = 0x1000_0040u32;
3786 assert_eq!(bus.read_u32(base), 0x4041_4243, "ROM before the write");
3787
3788 bus.write_u32(base, 0xBADC_0FFE);
3789 assert_eq!(
3790 bus.read_u32(base),
3791 0xBADC_0FFE,
3792 "the latched value is read back"
3793 );
3794 assert_eq!(
3795 bus.read_u32(base + 0x100),
3796 0xBADC_0FFE,
3797 "and shadows a DIFFERENT address too -- it is the bus, not the cell"
3798 );
3799
3800 // ...and it decays: the ROM value returns once the write finalizes.
3801 for _ in 0..Bus::PI_WRITE_CYCLES {
3802 bus.pi_tick();
3803 }
3804 assert_eq!(
3805 bus.read_u32(base),
3806 0x4041_4243,
3807 "ROM is back; ROM ignored the write"
3808 );
3809 }
3810
3811 /// `PI_STATUS.IOBUSY` reports the asynchronous write, which is how software
3812 /// knows when a cart write has landed.
3813 #[test]
3814 fn a_pi_write_sets_io_busy_until_it_finalizes() {
3815 let mut bus = Bus::new();
3816 let st = rustyn64_cart::pi::PI_STATUS;
3817 assert_eq!(bus.read_u32(st) & rustyn64_cart::pi::STATUS_IO_BUSY, 0);
3818 bus.write_u32(0x1000_0000, 0xDEAD_BEEF);
3819 assert_ne!(
3820 bus.read_u32(st) & rustyn64_cart::pi::STATUS_IO_BUSY,
3821 0,
3822 "IOBUSY is set while the write is in flight"
3823 );
3824 for _ in 0..Bus::PI_WRITE_CYCLES {
3825 bus.pi_tick();
3826 }
3827 assert_eq!(bus.read_u32(st) & rustyn64_cart::pi::STATUS_IO_BUSY, 0);
3828 }
3829
3830 /// A second write while one is in flight is **ignored**, not queued.
3831 #[test]
3832 fn a_pi_write_during_another_is_ignored() {
3833 let mut bus = Bus::new();
3834 bus.write_u32(0x1000_0000, 0xAAAA_AAAA);
3835 bus.write_u32(0x1000_0000, 0xBBBB_BBBB);
3836 assert_eq!(
3837 bus.read_u32(0x1000_0000),
3838 0xAAAA_AAAA,
3839 "the FIRST write still owns the bus"
3840 );
3841 }
3842}
3843
3844#[cfg(all(test, feature = "rdp-tap"))]
3845mod rdp_tap_tests {
3846 use super::Bus;
3847
3848 /// Load a command list at `addr` and drain the DP FIFO over it.
3849 fn run(words: &[u32]) -> Bus {
3850 let mut bus = Bus::new();
3851 let addr = 0x0010_0000u32;
3852 for (i, w) in words.iter().enumerate() {
3853 let a = addr as usize + i * 4;
3854 bus.rdram[a..a + 4].copy_from_slice(&w.to_be_bytes());
3855 }
3856 let end = addr + (words.len() * 4) as u32;
3857 bus.rdp.dpc_write(0, addr);
3858 bus.rdp.dpc_write(1, end);
3859 for _ in 0..(words.len() * 8 + 64) {
3860 bus.rdp_tick();
3861 }
3862 bus
3863 }
3864
3865 /// A `Sync Full` (2 words) and a `Set Fill Color` (2 words).
3866 const TWO_COMMANDS: [u32; 4] = [0x2900_0000, 0, 0x3700_0000, 0xFF00_00FF];
3867
3868 /// The tap must reproduce the consumed stream **exactly**, in order.
3869 ///
3870 /// Mutation-checked: dropping the `while addr < after` capture entirely
3871 /// leaves this empty, and advancing `addr` by 8 instead of 4 drops every
3872 /// second word.
3873 #[test]
3874 fn the_tap_reproduces_the_consumed_stream() {
3875 let mut bus = run(&TWO_COMMANDS);
3876 assert_eq!(bus.take_rdp_commands(), TWO_COMMANDS.to_vec());
3877 }
3878
3879 /// Draining must actually empty it, or a per-frame consumer replays every
3880 /// earlier frame's commands on top of its own.
3881 #[test]
3882 fn draining_empties_the_tap() {
3883 let mut bus = run(&TWO_COMMANDS);
3884 assert!(!bus.take_rdp_commands().is_empty(), "nothing was captured");
3885 assert!(
3886 bus.take_rdp_commands().is_empty(),
3887 "a second drain returned commands, so the first did not consume them"
3888 );
3889 }
3890
3891 /// A command the FIFO never consumed must not be captured.
3892 ///
3893 /// The tap diffs the FIFO pointer, so it inherits `tick_with_bus`'s refusal
3894 /// to consume a partly-written command rather than restating it. This is the
3895 /// test that the inheritance is real and not merely intended.
3896 ///
3897 /// **A Fill Triangle, not a 2-word command, and that is the whole point.**
3898 /// The first version of this test used a 2-word command with `DPC_END` set
3899 /// one word in — but `DPC_ADDR_MASK` is `0x00FF_FFF8`, so `addr + 4` masks
3900 /// back down to `addr`, leaving `cmd_end == cmd_current`. The FIFO was
3901 /// *empty*, `tick_without_bus` early-returned, and the test passed without
3902 /// the partial-command path ever running. It survived a mutation that
3903 /// captured unconditionally. A triangle is 4 u64 words (32 bytes), so
3904 /// `addr + 8` is both 8-byte aligned and genuinely short.
3905 #[test]
3906 fn a_partial_command_is_not_captured() {
3907 let mut bus = Bus::new();
3908 let addr = 0x0010_0000u32;
3909 // Fill Triangle (0x08): 32 bytes.
3910 bus.rdram[addr as usize..addr as usize + 4].copy_from_slice(&0x0800_0000u32.to_be_bytes());
3911 bus.rdp.dpc_write(0, addr);
3912 bus.rdp.dpc_write(1, addr + 8);
3913 assert_eq!(
3914 bus.rdp.dpc_read(1),
3915 addr + 8,
3916 "DPC_END was masked away; the FIFO would be empty rather than partial"
3917 );
3918 for _ in 0..64 {
3919 bus.rdp_tick();
3920 }
3921 assert_eq!(
3922 bus.rdp.commands_processed, 0,
3923 "the RDP consumed the partial command, so this tests nothing about the tap"
3924 );
3925 assert!(
3926 bus.take_rdp_commands().is_empty(),
3927 "the tap captured a command the RDP never consumed"
3928 );
3929 }
3930
3931 /// The tap must not perturb what the RDP does.
3932 ///
3933 /// The whole feature is supposed to be observation only, and "it does not
3934 /// change behavior" is a claim like any other. `commands_processed` is
3935 /// retained state the RDP increments itself, so it witnesses the work rather
3936 /// than being derived from the clock.
3937 #[test]
3938 fn the_tap_does_not_change_what_the_rdp_consumes() {
3939 let bus = run(&TWO_COMMANDS);
3940 assert_eq!(
3941 bus.rdp.commands_processed, 2,
3942 "the RDP consumed a different number of commands with the tap compiled in"
3943 );
3944 }
3945}
3946
3947#[cfg(test)]
3948mod read_u32_fast_path_tests {
3949 use super::Bus;
3950 use crate::cpu::Bus as CpuBus;
3951
3952 /// The RDRAM fast path returns exactly what the byte composition returned.
3953 ///
3954 /// A fast path that changes a value is a wrong-memory bug with no test to
3955 /// catch it — every existing test would still pass, because they assert
3956 /// behavior rather than equality between two implementations. So this
3957 /// compares the two directly, byte-composition against fast path, over
3958 /// addresses chosen to exercise the cases that differ:
3959 /// misalignment, page boundaries, and the very end of RDRAM.
3960 #[test]
3961 fn the_fast_path_agrees_with_byte_composition() {
3962 let mut bus = Bus::new();
3963 // A pattern where every byte is distinct mod 251, so a swapped or
3964 // duplicated lane cannot coincide with the right answer.
3965 for (i, b) in bus.rdram.iter_mut().enumerate() {
3966 *b = u8::try_from(i % 251).expect("mod 251 fits a u8");
3967 }
3968 let len = u32::try_from(bus.rdram.len()).expect("RDRAM fits a u32");
3969 for addr in [
3970 0x8000_0000,
3971 0x8000_0001, // misaligned
3972 0x8000_0FFE, // straddles a 4 KiB page
3973 0x8000_1000,
3974 0x8000_1002,
3975 0x8000_0000 + len - 4, // the last whole word
3976 0x8000_0000 + len - 3, // straddles the END of RDRAM
3977 0x8000_0000 + len - 1,
3978 ] {
3979 let composed = u32::from_be_bytes([
3980 CpuBus::read_u8(&mut bus, addr),
3981 CpuBus::read_u8(&mut bus, addr.wrapping_add(1)),
3982 CpuBus::read_u8(&mut bus, addr.wrapping_add(2)),
3983 CpuBus::read_u8(&mut bus, addr.wrapping_add(3)),
3984 ]);
3985 assert_eq!(
3986 CpuBus::read_u32(&mut bus, addr),
3987 composed,
3988 "read_u32 and the byte composition disagree at {addr:#010X}"
3989 );
3990 }
3991 }
3992
3993 /// A word read still reaches the SP register file rather than RDRAM.
3994 ///
3995 /// **MMIO is protected twice over, and either protection alone suffices** —
3996 /// established by mutation rather than assumed:
3997 ///
3998 /// | mutation | result |
3999 /// | --- | --- |
4000 /// | hoist the fast path above every register branch | passes |
4001 /// | widen `rdram_offset` to accept every address | passes |
4002 /// | **both together** | **fails** |
4003 ///
4004 /// So neither single mutation reveals this test, and that is a property of
4005 /// the code rather than a gap in the test: the register branches run first
4006 /// *and* `rdram_offset` returns `None` outside the 8 MiB window. A reader
4007 /// who mutates only one and sees green should not conclude the test is
4008 /// vacuous.
4009 ///
4010 /// The first version of this doc claimed the test pinned the fast path's
4011 /// *placement*. It does not — placement is one of the two protections, not
4012 /// the tested one. What is tested is the conjunction: a word read of a
4013 /// register must return the register, not memory. That failure would be
4014 /// silent and word-width only.
4015 #[test]
4016 fn mmio_word_reads_are_not_captured_by_the_fast_path() {
4017 let mut bus = Bus::new();
4018 // Fill RDRAM with a recognizable pattern, so a read that WAS diverted
4019 // returns something identifiable rather than the zero both paths share.
4020 bus.rdram.fill(0x5A);
4021
4022 // Compared against the register file directly rather than a written-back
4023 // value: SP index 4 is `SP_STATUS`, whose read is computed rather than a
4024 // latch, so a round-trip would be testing the register's semantics
4025 // instead of the routing. The routing is what a fast path can break.
4026 // 0..=6, NOT 0..=7. Index 7 is `SP_SEMAPHORE`, whose read TAKES the
4027 // semaphore — comparing it against a reference read would consume it
4028 // first and then compare 0 against 1. That side effect is the reason
4029 // `read_u32` handles SP registers before composing bytes at all (see its
4030 // comment), so excluding it here is not dodging the case: it is the case
4031 // that made the branch exist.
4032 for idx in 0..=6u32 {
4033 let addr = Bus::SP_REGS_BASE + idx * 4;
4034 // The BUS read first, and the reference second. Either order is fine
4035 // for these seven registers today, but if one ever grows a
4036 // read-side-effect the reference would consume it and the thing
4037 // under test would see the second read. Index 7 already works this
4038 // way, which is why it is excluded rather than reordered.
4039 let via_bus = CpuBus::read_u32(&mut bus, addr);
4040 let direct = bus.rsp.sp.read(idx);
4041 assert_eq!(
4042 via_bus, direct,
4043 "a word read of SP register {idx} was not routed to the register file"
4044 );
4045 assert_ne!(
4046 via_bus, 0x5A5A_5A5A,
4047 "SP register {idx} returned the RDRAM fill pattern; the fast path \
4048 captured an MMIO address"
4049 );
4050 }
4051
4052 // One representative from each of the other register blocks `read_u32`
4053 // routes before reaching the fast path. The SP loop above is the
4054 // thorough case; these are breadth, so a future change to the branch
4055 // ORDER cannot quietly divert a whole block into RDRAM. Asserted
4056 // negatively — against the fill pattern — because each block's read
4057 // semantics differ and this test is about routing, not values.
4058 for (label, addr) in [
4059 ("MI", Bus::MI_BASE),
4060 ("VI", Bus::VI_REGS_BASE),
4061 ("AI", Bus::AI_REGS_BASE),
4062 ("DP", Bus::DP_REGS_BASE),
4063 ("RI", Bus::RI_BASE),
4064 ("SI", Bus::SI_BASE),
4065 ] {
4066 assert_ne!(
4067 CpuBus::read_u32(&mut bus, addr),
4068 0x5A5A_5A5A,
4069 "a word read of the {label} block returned the RDRAM fill pattern"
4070 );
4071 }
4072 }
4073}
4074
4075#[cfg(all(test, feature = "work-counters"))]
4076mod work_counter_tests {
4077 use super::Bus;
4078 use crate::cpu::Bus as CpuBus;
4079
4080 /// What each CPU-facing operation actually costs in bus dispatches.
4081 ///
4082 /// Distinct expected counts rather than "greater than zero": a counter wired
4083 /// to only one of the four leaves, or one that also counted `write_sized`
4084 /// itself and so double-counted every store, would pass a non-zero check
4085 /// comfortably.
4086 ///
4087 /// The counts are **asserted, not described**, because `docs/performance.md`
4088 /// quotes them to explain what a "bus access" is — and a figure quoted in
4089 /// prose while only being true in code is how this project's numbers go
4090 /// stale.
4091 #[test]
4092 fn the_cpu_bus_dispatch_shape_is_pinned() {
4093 // EVERY case below uses an RDRAM address (0x8000_1000), and the
4094 // expectations are only claimed for that route.
4095 //
4096 // `write_sized` at an 8-BYTE width is TWO dispatches here because it
4097 // decomposes into two `write_u32` for RDRAM. That is NOT universal: on
4098 // the RCP-internal route `write_sized` takes the high word and drops the
4099 // second entirely (its own comment, ~line 2247), so a `sd` to an RCP
4100 // register is ONE dispatch. Pinning the RDRAM number as if it were the
4101 // rule is how a scoped measurement becomes a false general claim.
4102 //
4103 // If the 2 ever became 1 *for RDRAM*, the leaf counting has been
4104 // replaced by counting the entry point, which double-counts everything
4105 // else.
4106 /// One operation to time, so the array type stays legible.
4107 type Op = fn(&mut Bus);
4108 let cases: [Op; 6] = [
4109 |b| {
4110 CpuBus::read_u8(b, 0x8000_1000);
4111 },
4112 |b| {
4113 CpuBus::read_u32(b, 0x8000_1000);
4114 },
4115 |b| CpuBus::write_u8(b, 0x8000_1000, 1),
4116 |b| CpuBus::write_u32(b, 0x8000_1000, 1),
4117 |b| CpuBus::write_sized(b, 0x8000_1000, 4, 1),
4118 |b| CpuBus::write_sized(b, 0x8000_1000, 8, 1),
4119 ];
4120 let mut got = [0u64; 6];
4121 for (i, op) in cases.into_iter().enumerate() {
4122 let mut bus = Bus::new();
4123 let before = bus.accesses();
4124 op(&mut bus);
4125 got[i] = bus.accesses() - before;
4126 }
4127 // The measured truth, pinned rather than described — and the shape is
4128 // ASYMMETRIC in a way worth knowing:
4129 //
4130 // read_u8 1
4131 // read_u32 1 <- an RDRAM fast path, as of the change below
4132 // write_u8 1
4133 // write_u32 1 <- an RDRAM fast path; no decomposition
4134 // write_sized w4 1 <- delegates to write_u32
4135 // write_sized w8 2 <- two write_u32, which is what a `sd` costs
4136 //
4137 // The unit this counter reports is **bus dispatch entries**, not CPU
4138 // requests: dispatches are what the `bus.rs` profile bucket spends its
4139 // time in, which is the quantity the count exists to explain.
4140 //
4141 // `read_u32` used to be 5 — itself plus four `read_u8` — while
4142 // `write_u32` was 1, because the write path had an RDRAM fast path and
4143 // the read path did not. The census is what surfaced that, and the fast
4144 // path that closed it is why this row now reads 1. Left recorded because
4145 // the number moving IS the finding.
4146 assert_eq!(
4147 got,
4148 [1, 1, 1, 1, 1, 2],
4149 "the CpuBus dispatch shape changed; docs/performance.md quotes these"
4150 );
4151 }
4152
4153 /// The counter is `#[serde(skip)]`, so a restored save-state starts at zero
4154 /// and keeps counting.
4155 ///
4156 /// Zero rather than preserved is the correct behavior — it is a measurement
4157 /// tally, not machine state, and ADR 0005's layout must not carry it. The
4158 /// second half matters as much: a `skip`ped field that deserialized into
4159 /// something unusable is exactly the bug #245 shipped and had to fix.
4160 #[test]
4161 fn a_deserialized_bus_starts_at_zero_and_still_counts() {
4162 let mut bus = Bus::new();
4163 CpuBus::write_u8(&mut bus, 0x8000_1000, 1);
4164 assert!(bus.accesses() > 0, "the premise: it counted something");
4165
4166 let bytes = bincode::serialize(&bus).expect("serialize");
4167 let mut restored: Bus = bincode::deserialize(&bytes).expect("deserialize");
4168 assert_eq!(
4169 restored.accesses(),
4170 0,
4171 "the tally survived a save-state; it is not machine state"
4172 );
4173 CpuBus::write_u8(&mut restored, 0x8000_1000, 2);
4174 assert_eq!(restored.accesses(), 1, "a restored Bus stopped counting");
4175 }
4176}
4177
4178#[cfg(all(test, feature = "rdp-tap"))]
4179mod rdram_dirty_tests {
4180 use super::{Bus, RDRAM_PAGE, RdramBus};
4181 use crate::cpu::Bus as CpuBus;
4182
4183 /// KSEG0 address of the first byte of RDRAM page `p`.
4184 const fn page_addr(p: usize) -> u32 {
4185 0x8000_0000 + (p * RDRAM_PAGE) as u32
4186 }
4187
4188 fn dirty(bus: &Bus) -> alloc::vec::Vec<usize> {
4189 bus.rdram_dirty_pages()
4190 .iter()
4191 .enumerate()
4192 .filter(|(_, d)| **d)
4193 .map(|(i, _)| i)
4194 .collect()
4195 }
4196
4197 /// A fresh Bus starts **all** dirty.
4198 ///
4199 /// The consumer has never seen this RDRAM, so the first upload must be
4200 /// complete. Starting clean would hand the GPU an empty framebuffer and
4201 /// whatever the allocator left behind — and it would look right, because the
4202 /// second frame would repair it.
4203 #[test]
4204 fn a_fresh_bus_starts_entirely_dirty() {
4205 let bus = Bus::new();
4206 assert_eq!(
4207 dirty(&bus).len(),
4208 bus.rdram_dirty_pages().len(),
4209 "a page started clean, so the first upload would be incomplete"
4210 );
4211 }
4212
4213 /// A byte write marks its page and **only** its page.
4214 #[test]
4215 fn a_write_marks_exactly_one_page() {
4216 let mut bus = Bus::new();
4217 bus.clear_rdram_dirty();
4218 CpuBus::write_u8(&mut bus, page_addr(3) + 17, 0xAB);
4219 assert_eq!(dirty(&bus), alloc::vec![3]);
4220 }
4221
4222 /// The `u32` fast path writes four bytes and can straddle a page boundary.
4223 ///
4224 /// This is the case a point-mark would get wrong, and it is not hypothetical:
4225 /// the assertion that was supposed to guard the write sites caught it,
4226 /// because `write_u32` is `self.rdram[off..=off + 3]` rather than a single
4227 /// indexed store.
4228 #[test]
4229 fn a_straddling_word_write_marks_both_pages() {
4230 let mut bus = Bus::new();
4231 bus.clear_rdram_dirty();
4232 // Two bytes in page 5, two in page 6.
4233 CpuBus::write_u32(&mut bus, page_addr(6) - 2, 0xDEAD_BEEF);
4234 assert_eq!(dirty(&bus), alloc::vec![5, 6]);
4235 }
4236
4237 /// Clearing actually clears, or the consumer re-sends everything forever and
4238 /// the whole feature is a no-op with extra steps.
4239 #[test]
4240 fn clearing_empties_the_map() {
4241 let mut bus = Bus::new();
4242 CpuBus::write_u8(&mut bus, page_addr(1), 1);
4243 bus.clear_rdram_dirty();
4244 assert!(dirty(&bus).is_empty());
4245 // And a later write still marks — clearing must not disable tracking.
4246 CpuBus::write_u8(&mut bus, page_addr(9), 1);
4247 assert_eq!(dirty(&bus), alloc::vec![9]);
4248 }
4249
4250 /// **Every** CPU store width marks, including the ones with no dedicated
4251 /// `Bus` method.
4252 ///
4253 /// A reviewer asked whether `SH`/`SD`/`SWL`/`SDR` bypass the map, since only
4254 /// `write_u8` and `write_u32` carry marks. They do not: `write_sized`
4255 /// decomposes every non-RCP-internal width into those two, and the unaligned
4256 /// family reaches the bus through `Pipeline::write_width` at width 4 or 8, so
4257 /// it decomposes the same way. Nothing *enforces* that delegation, though —
4258 /// a future width-specific override would silently bypass the marks and
4259 /// produce a stale frame with every test still green. Hence this test rather
4260 /// than a reply saying the code is fine today.
4261 #[test]
4262 fn every_store_width_marks() {
4263 // Width, page, and the byte the store must leave at the page's base so
4264 // the marking assertion cannot pass on a store that did nothing.
4265 // Big-endian: the byte at the base is the register's *most* significant
4266 // byte of that width.
4267 for (width, page, value, expect) in [
4268 (1u64, 10usize, 0x00_00_00_00_00_00_00_A1u64, 0xA1u8),
4269 (2, 11, 0x0000_0000_0000_A2B2, 0xA2),
4270 (4, 12, 0x0000_0000_A3B3_C3D3, 0xA3),
4271 (8, 13, 0xA4B4_C4D4_E4F4_0414, 0xA4),
4272 ] {
4273 let mut bus = Bus::new();
4274 bus.clear_rdram_dirty();
4275 CpuBus::write_sized(&mut bus, page_addr(page), width, value);
4276 assert_eq!(
4277 bus.rdram[page * RDRAM_PAGE],
4278 expect,
4279 "a width-{width} store did not land, so its marking assertion is vacuous"
4280 );
4281 assert_eq!(
4282 dirty(&bus),
4283 alloc::vec![page],
4284 "a width-{width} store left its page clean"
4285 );
4286 }
4287 }
4288
4289 /// `RdramBus::rdram_write` marks — the RDP/RSP/AI DMA paths use it.
4290 ///
4291 /// One of three routes the first version of this module left unguarded: the
4292 /// mutation check only removed the SP DMA mark, so deleting this one, SI's or
4293 /// PI's would have gone unnoticed. Caught in review of #245.
4294 #[test]
4295 fn the_shared_rdram_bus_write_marks() {
4296 let mut bus = Bus::new();
4297 bus.clear_rdram_dirty();
4298 RdramBus::rdram_write(&mut bus, page_addr(7) & 0x00FF_FFFF, 0x5A);
4299 assert_eq!(dirty(&bus), alloc::vec![7]);
4300 assert_eq!(
4301 bus.rdram[7 * RDRAM_PAGE],
4302 0x5A,
4303 "the write did not land, so the marking assertion is vacuous"
4304 );
4305 }
4306
4307 /// PI DMA (cart to RDRAM) marks.
4308 #[test]
4309 fn pi_dma_marks_the_pages_it_writes() {
4310 use rustyn64_cart::pi::{PI_CART_ADDR, PI_DRAM_ADDR, PI_WR_LEN};
4311 let mut bus = Bus::new();
4312 // A cart whose first bytes are recognizable. `reattach_rom` is the
4313 // supported way to give a `Cart` an image without parsing a header.
4314 let mut rom = alloc::vec![0u8; 0x1000];
4315 rom[0] = 0xC5;
4316 bus.cart.reattach_rom(rom);
4317 bus.clear_rdram_dirty();
4318
4319 bus.pi_write_word(PI_DRAM_ADDR, page_addr(8) & 0x00FF_FFFF);
4320 bus.pi_write_word(PI_CART_ADDR, 0x1000_0000);
4321 // WR_LEN loads INTO RDRAM; the value is length-1.
4322 bus.pi_write_word(PI_WR_LEN, 7);
4323
4324 assert_eq!(
4325 bus.rdram[8 * RDRAM_PAGE],
4326 0xC5,
4327 "the PI transfer did not land, so the marking assertion is vacuous"
4328 );
4329 assert!(
4330 dirty(&bus).contains(&8),
4331 "a PI DMA to page 8 left it clean; dirty = {:?}",
4332 dirty(&bus)
4333 );
4334 }
4335
4336 /// SI DMA (PIF RAM to RDRAM) marks.
4337 #[test]
4338 fn si_dma_marks_the_pages_it_writes() {
4339 let mut bus = Bus::new();
4340 bus.clear_rdram_dirty();
4341 // SI_DRAM_ADDR, then the RD64B write that runs the joybus frame and DMAs
4342 // PIF RAM into RDRAM.
4343 //
4344 // PHYSICAL addresses: the Bus matches MMIO on raw physical ranges
4345 // (`is_si_register` is `addr >= 0x0480_0000`), so a KSEG1 address never
4346 // matches and the write silently lands nowhere. That is what the first
4347 // version of this test did, and it read as "the mark is missing".
4348 CpuBus::write_u32(&mut bus, Bus::SI_BASE, page_addr(9) & 0x00FF_FFFF);
4349 CpuBus::write_u32(&mut bus, Bus::SI_BASE + 0x04, 0);
4350 assert!(
4351 dirty(&bus).contains(&9),
4352 "an SI DMA to page 9 left it clean; dirty = {:?}",
4353 dirty(&bus)
4354 );
4355 }
4356
4357 /// DMA writes mark too.
4358 ///
4359 /// The CPU store path is the obvious one; a DMA that did not mark would
4360 /// leave the consumer with stale textures, which is the failure that
4361 /// presents as a rendering bug rather than as an error. Driven through
4362 /// `Bus::sp_dma` directly rather than through the SP registers, because what
4363 /// is under test is the marking in the transfer loop, not the register
4364 /// plumbing that reaches it.
4365 #[test]
4366 fn sp_dma_marks_the_pages_it_writes() {
4367 let mut bus = Bus::new();
4368 bus.rsp.mem_write(0, 0x5A);
4369 bus.clear_rdram_dirty();
4370 bus.sp_dma(rustyn64_rsp::sp::Dma {
4371 sp_addr: 0,
4372 ram_addr: page_addr(4) & 0x00FF_FFFF,
4373 row_len: 8,
4374 rows: 1,
4375 skip: 0,
4376 to_dram: true,
4377 });
4378 assert_eq!(
4379 dirty(&bus),
4380 alloc::vec![4],
4381 "an SP DMA to page 4 did not mark it"
4382 );
4383 assert_eq!(
4384 bus.rdram[4 * RDRAM_PAGE],
4385 0x5A,
4386 "the DMA did not actually transfer, so the marking assertion is vacuous"
4387 );
4388 }
4389}
4390
4391#[cfg(all(test, feature = "rdp-tap"))]
4392mod rdram_dirty_savestate_tests {
4393 use super::{Bus, RDRAM_PAGE, RDRAM_SIZE};
4394 use crate::cpu::Bus as CpuBus;
4395
4396 /// A Bus restored from a save-state must still be able to take a store.
4397 ///
4398 /// **This is a crash, not a cosmetic gap.** `#[serde(skip)]` fills the field
4399 /// from `Default`, and `Box<[bool]>::default()` is **empty** — so the first
4400 /// RDRAM store after a load indexes page `off >> 12` into a zero-length slice
4401 /// and panics. `rdp_tap` gets away with the same attribute only because
4402 /// `Vec::default()` is empty *and pushable*; an indexed map is not.
4403 ///
4404 /// Caught in review of #245, not by any gate here, because nothing had
4405 /// round-tripped a Bus with the feature on.
4406 #[test]
4407 fn a_deserialized_bus_can_still_take_a_store() {
4408 let mut bus = Bus::new();
4409 CpuBus::write_u8(&mut bus, 0x8000_1000, 0x11);
4410 let bytes = bincode::serialize(&bus).expect("serialize");
4411 let mut restored: Bus = bincode::deserialize(&bytes).expect("deserialize");
4412
4413 assert_eq!(
4414 restored.rdram_dirty_pages().len(),
4415 bus.rdram_dirty_pages().len(),
4416 "the dirty map came back a different size, so the next store is a panic"
4417 );
4418 // The store itself is the assertion: without a `serde(default)` this
4419 // panics with an out-of-bounds index.
4420 CpuBus::write_u8(&mut restored, 0x8000_2000, 0x22);
4421 assert!(
4422 restored.rdram_dirty_pages()[2],
4423 "the restored Bus did not track the store"
4424 );
4425 }
4426
4427 /// A restored Bus starts **entirely** dirty.
4428 ///
4429 /// Its consumer is a GPU backend that has never seen this machine's RDRAM —
4430 /// loading a save-state is exactly the moment every page is new. Coming back
4431 /// clean would present the previous state's framebuffer until something
4432 /// happened to overwrite it.
4433 #[test]
4434 fn a_deserialized_bus_starts_entirely_dirty() {
4435 let mut bus = Bus::new();
4436 bus.clear_rdram_dirty();
4437 let bytes = bincode::serialize(&bus).expect("serialize");
4438 let restored: Bus = bincode::deserialize(&bytes).expect("deserialize");
4439 // Length FIRST: `.all()` on an empty iterator is vacuously true, and the
4440 // first version of this test passed that way against the very bug it was
4441 // written for.
4442 assert_eq!(
4443 restored.rdram_dirty_pages().len(),
4444 RDRAM_SIZE.div_ceil(RDRAM_PAGE),
4445 "the dirty map came back the wrong size"
4446 );
4447 assert!(
4448 restored.rdram_dirty_pages().iter().all(|d| *d),
4449 "a restored Bus came back with clean pages the GPU has never seen"
4450 );
4451 }
4452}