Skip to main content

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}