pub struct Bus {
pub rdram: Box<[u8]>,
pub rdram_hidden: Option<Box<[u8]>>,
pub pi: Pi,
pub rsp: Rsp,
pub rdp: Rdp,
pub vi: Vi,
pub audio: Audio,
pub cart: Cart,
pub rcp: RcpRegs,
pub controllers: [u32; 4],
/* private fields */
}Expand description
Everything mutable lives here — the single owner.
Fields§
§rdram: Box<[u8]>Main system RDRAM (boxed slice: 8 MiB, heap-allocated without a stack temporary).
The RDRAM “hidden” bits: the 9th bit RDRAM carries per byte, used by the
RDP Z-buffer for the low 2 bits of each pixel’s dz. Two bits per 16-bit
halfword, bit-packed four halfwords to a byte (RDRAM_SIZE / 8 = 1 MiB
for 8 MiB of RDRAM). Lazily allocated — None until the first hidden write,
since only Z-buffered rendering touches it (reads return 0, matching the
power-on state).
pi: PiThe PI DMA engine (T-14-001), pulled forward from Phase 5 because n64-systemtest loads the rest of its own ELF through it.
rsp: RspThe RSP coprocessor.
rdp: RdpThe RDP rasterizer.
vi: ViThe Video Interface register file (0x0440_0000). Scan-out and the
scheduler-driven scan position are follow-up VI tickets.
audio: AudioThe Audio Interface.
cart: CartThe cartridge (PI/SI + saves).
rcp: RcpRegsThe RCP interface register state.
controllers: [u32; 4]Controller button/stick state, 4 ports (latched by the SI joybus).
Implementations§
Source§impl Bus
impl Bus
Sourcepub const SPMEM_BASE: u32 = 0x0400_0000
pub const SPMEM_BASE: u32 = 0x0400_0000
Map a CPU physical address into RDRAM (0..RDRAM_SIZE), or None if it
targets a memory-mapped register region instead.
Base of RSP DMEM. IMEM follows at +0x1000.
Sourcepub const PI_WRITE_CYCLES: u32 = 100
pub const PI_WRITE_CYCLES: u32 = 100
RCP cycles a PI direct-I/O write stays latched before finalizing.
This number is fitted, not measured. Hardware finalization depends on
the PI domain timing registers (LAT/PWD/PGS/RLS), which are not
modeled; n64-systemtest bounds the latch only relatively (visible after
0 decay-loop iterations, gone after 110). 100 was the best of the values
tried against the suite.
Treat that provenance as a warning, not a credential. The suite still
fails Write32, Read32 (same location) on its second read, where
hardware has finalized and we have not — a gap no single constant closes,
because the real duration is not constant. Modeling the domain registers
is the actual fix. Accuracy ledger C-9.
Sourcepub const MI_VERSION_VALUE: u32 = 0x0202_0102
pub const MI_VERSION_VALUE: u32 = 0x0202_0102
MI_VERSION, the value “most consoles report”.
Packed RSP:RDP:RAC:IO. Other values exist in the wild — 0x0101_0101
and 0x0201_0202 appear in emulators and docs, iQue reports
0x0202_b0b0 — so this is a choice among documented observations,
not a derived constant. Retail NTSC hardware is what this emulator
models, so it reports what retail hardware reports.
Sourcepub const SP_REGS_BASE: u32 = 0x0404_0000
pub const SP_REGS_BASE: u32 = 0x0404_0000
Base of the eight SP interface registers (0x0404_0000).
Sourcepub const SP_STATUS: u32 = 0x0404_0010
pub const SP_STATUS: u32 = 0x0404_0010
SP_STATUS (0x0404_0010), named because tests reach for it directly.
Sourcepub const SP_PC: u32 = 0x0408_0000
pub const SP_PC: u32 = 0x0408_0000
SP_PC (0x0408_0000) — in its own window, not with the other eight.
Sourcepub const DP_REGS_BASE: u32 = 0x0410_0000
pub const DP_REGS_BASE: u32 = 0x0410_0000
Base of the DP command registers (0x0410_0000): START, END, CURRENT,
STATUS, then the (unmodeled) CLOCK/BUSY/PIPE/TMEM counters.
Sourcepub const VI_REGS_BASE: u32 = 0x0440_0000
pub const VI_REGS_BASE: u32 = 0x0440_0000
Base of the VI register block (0x0440_0000); the AI follows at
0x0450_0000.
Sourcepub const AI_REGS_BASE: u32 = 0x0450_0000
pub const AI_REGS_BASE: u32 = 0x0450_0000
Base of the AI register block (0x0450_0000); the SI/RI follow above.
Sourcepub const RI_BASE: u32 = 0x0470_0000
pub const RI_BASE: u32 = 0x0470_0000
Base of the RI (RDRAM controller) register block.
0x0470_0000, holding the eight registers N64brew RDRAM Interface
§Registers enumerates: RI_MODE, RI_CONFIG, RI_CURRENT_LOAD,
RI_SELECT, RI_REFRESH, RI_LATENCY, RI_ERROR, RI_BANK_STATUS.
Sourcepub const SPMEM_WINDOW_END: u32 = 0x0404_0000
pub const SPMEM_WINDOW_END: u32 = 0x0404_0000
End of the SP memory window — where the SP registers begin.
The 8 KiB of real storage repeats for this whole range rather than
ending at 0x0400_2000; see rustyn64_rsp::Rsp::mem_read and
accuracy ledger C-30, which records the provenance of the mirroring.
Sourcepub const ISVIEWER_BASE: u32 = 0x13FF_0000
pub const ISVIEWER_BASE: u32 = 0x13FF_0000
Base of the ISViewer debug window, in cart address space.
Not real N64 hardware — it is a flashcart/emulator convention that
n64-systemtest uses to report results (ref-proj/n64-systemtest/src/isviewer.rs).
The suite probes for it by writing a magic word to the buffer and reading
it back; if the round-trip fails it falls back to a framebuffer console
we cannot read. So this window is what turns “the suite runs” into “the
suite reports”.
Sourcepub const ISVIEWER_WRITE_LEN: u32 = 0x13FF_0014
pub const ISVIEWER_WRITE_LEN: u32 = 0x13FF_0014
Writing this register flushes len bytes from the buffer.
Sourcepub const ISVIEWER_BUF: u32 = 0x13FF_0020
pub const ISVIEWER_BUF: u32 = 0x13FF_0020
The text buffer.
Sourcepub const ISVIEWER_LEN: usize = 0x1000
pub const ISVIEWER_LEN: usize = 0x1000
Bytes of buffer modeled — the suite writes in 0x200 chunks.
Sourcepub const fn pi_tick(&mut self)
pub const fn pi_tick(&mut self)
Advance the PI’s asynchronous write by one RCP cycle.
§Why a PI write is not immediate
From N64brew Memory map (PI external bus):
All writes are performed asynchronously by the PI. Making a write in this area will in fact just cause the PI to latch the value internally, and release the VR4300 immediately. The write will then happen in background. […] While a write is ongoing, further writes are ignored, and reads (from any address) return the 32-bit value that is being written.
The PI does not know a device is read-only, so a write into ROM follows the same path and is simply dropped by the ROM — which is why a value written to cart ROM is briefly readable and then gone.
§The duration is bounded by the oracle, not derived from hardware
How long finalization takes depends on the PI domain timing registers
(LAT/PWD/PGS/RLS), which are not modeled. n64-systemtest bounds
it only relatively: the latched value must still be visible after 0
loop iterations and gone after 110. Bus::PI_WRITE_CYCLES sits inside
those bounds; it is not a hardware measurement. Accuracy ledger C-9.
Sourcepub fn rsp_tick(&mut self)
pub fn rsp_tick(&mut self)
Step the RSP.
The chip stays in place. It used to be moved out with
core::mem::take so that Rsp::tick could borrow the Bus, under a
comment asserting “No allocation” — which was false: take needs
Default, and constructing an Rsp allocates DMEM and IMEM, so every
RCP step allocated and freed 8 KiB. Rsp::tick now returns what it
wants done instead of borrowing its owner, so there is nothing to move.
Sourcepub fn rdp_tick(&mut self)
pub fn rdp_tick(&mut self)
Step the RDP against this bus’s narrow VideoBus view (split-borrow).
The take is how the RDP borrows its owner, and it is not free — it reads the
whole struct out, writes a fresh Default into the vacated slot, and the restore
overwrites that. It used to happen on every RCP step; the measurements are in
docs/performance.md §“The Bus split-borrow moves 1.35 GB a frame”.
So the step’s bus-free half runs first. On most steps the RDP is frozen,
stalling, or looking at an empty command FIFO, and answers the whole step from
its own fields — in which case nothing is moved at all. The predicate lives in
rustyn64_rdp::Rdp::tick_without_bus beside the early-outs it encodes, not
here, so it cannot drift away from them, and it hands back a
rustyn64_rdp::NeedsBus token that the bus half requires — so the two cannot
be called out of order.
Sourcepub fn audio_tick(&mut self, master_ticks: u64)
pub fn audio_tick(&mut self, master_ticks: u64)
Step the AI against this bus’s narrow AudioBus view (split-borrow),
advancing the DAC to master_ticks so sample emission is derived from
the one canonical clock (ADR 0006) rather than an independent counter.
The move is skipped on the steps that emit nothing, which at a typical
~32 kHz is about 1,949 of every 1,950: the AI is asked first, and only a
NeedsBus buys the take. Same shape as Bus::rdp_tick and for the
same reason — the borrow cannot be arranged without moving the chip out,
so the decision has to happen before it.
The bus-free half still runs every step and still mutates (it stamps
last_tick and anchors the first sample), so this skips the move, never
the step.
Sourcepub fn drain_audio_samples(&mut self) -> Vec<StereoSample>
pub fn drain_audio_samples(&mut self) -> Vec<StereoSample>
Drain the stereo stream the AI has emitted since the last drain — the frontend pushes it into the host ring and resamples (ADR 0004).
Sourcepub const fn rcp_steps_for_test(&self) -> u64
pub const fn rcp_steps_for_test(&self) -> u64
Diagnostic: count of RCP-chip steps taken (RSP ticks). The scheduler’s fractional-divisor test reads this to assert the 3:2 ratio.
Sourcepub const fn boot_nmi_halt(&self) -> bool
pub const fn boot_nmi_halt(&self) -> bool
Has the PIF frozen the CPU via NMI after a failed real-PIF boot checksum?
Always false under HLE, in run mode, and for a genuine ROM.
Sourcepub const fn reset_boot_latches(&mut self)
pub const fn reset_boot_latches(&mut self)
Warm-reset the real-PIF boot latches so a reset restarts IPL1→IPL2: clear
the NMI freeze and unlock the PIF ROM (PIF-NUS.md §Console Reset). No-op
under HLE (nothing is latched). The CPU’s reset vector is restored by
crate::System::reset, which recreates the CPU at 0xBFC0_0000.
Sourcepub fn scanout(&self, out: &mut [u8]) -> (u32, u32)
pub fn scanout(&self, out: &mut [u8]) -> (u32, u32)
Scan the framebuffer out into out as RGBA8, returning the active
(width, height) — the presentable frame the VI would send to the DAC.
Reads VI_ORIGIN/VI_WIDTH/VI_CTRL and derives the height from the
active region VI_V_VIDEO ((V_END − V_START) half-lines → lines).
Pixel formats (VI_CTRL.TYPE): 2 = 16-bit RGBA5551 (each 5-bit channel
expanded to 8, the 1-bit alpha to 0/255), 3 = 32-bit RGBA8888 (a direct
copy). TYPE 0/1 is blank — returns (0, 0) and writes nothing, the
caller keeps a black frame.
Returns (0, 0) and writes nothing when the VI is blanked, the width or
height is zero, or out is smaller than width * height * 4 — a caller
that gets a non-zero size can trust the whole frame was written.
Scope: a 1:1 scan (no VI_X_SCALE/VI_Y_SCALE resampling) and no
AA/divot/de-dither post-filter — those are later VI work, recorded as
open residual R-5 in docs/accuracy-ledger.md. Byte-exact for the direct
framebuffer copy, which is what the FILL pipeline produces.
Sourcepub fn scanout_scaled(&self, out: &mut [u8]) -> (u32, u32)
pub fn scanout_scaled(&self, out: &mut [u8]) -> (u32, u32)
Hardware-accurate VI scan-out with VI_X_SCALE/VI_Y_SCALE resampling and
the real active-span/overscan geometry (ledger R-5, gap-analysis Stage D).
This is the accurate replacement for Bus::scanout’s 1:1 copy, built up a
slice at a time and validated RGB byte-for-byte against Angrylion’s VI pipeline
(vi_process_full) through the .vivec conformance vectors. Implemented so far:
the geometry — the 2.10 fixed-point accumulator (line_x = x_offs >> 10, source
index stride*srcY + srcX), the NTSC/PAL horizontal overscan (h_start -= 108
/ 128), the 8/7-px minhpass/maxhpass crop, the PRESCALE_WIDTH/HEIGHT
clamp, and the truncating RGBA5551→8 conversion the VI uses ((px >> 8) & 0xF8,
not expand5’s replicating widening); the 5-bit bilinear lerp for both
16- and 32-bit sources (aa_mode != REPLICATE and a non-zero fraction, nearest
otherwise); the gamma curve; and, under aa_mode 0/1 for both source
formats, the coverage-gated de-dither (cvg == 7) and AA-edge
(cvg < 7) filters and the divot median (divot_enable) — 16-bit reads
coverage from the hidden-bits plane, 32-bit from the alpha byte
(vi_read_cov). Alpha is 0xFF (opaque) for display; the VI carries
coverage in its output alpha, which the harness compares as RGB-only.
Still to come (later slices, still substituted here): the gamma-dither
variants, the coverage filters under aa_mode == 2 (RESAMP_ONLY forces
cvg = 7, so de-dither can still apply — currently gated to aa_mode ≤ 1),
and the remaining R-6 field timing (interlace / serrate and the exact
H_TOTAL; the PAL 50 Hz field rate itself is handled by Vi::field_hz, which
drives the same ispal region split this geometry uses).
This is the live presented path. The frontend calls it directly
(rustyn64_frontend::emu::Emu::produce_frame), so what a user sees is this
function’s output, not Bus::scanout’s 1:1 copy. Bus::scanout is
retained as the simpler unscaled reference the R-5 vectors are compared
against and as the geometry contrast in the frontend’s own tests.
Returns (0, 0) (writing nothing) when the VI is blanked (TYPE 0/1), the
computed width/height is non-positive, or out is too small.
Sourcepub fn isviewer_raw(&self) -> &[u8] ⓘ
pub fn isviewer_raw(&self) -> &[u8] ⓘ
The raw ISViewer backing memory, for diagnostics.
Sourcepub fn isviewer_output(&self) -> &[u8] ⓘ
pub fn isviewer_output(&self) -> &[u8] ⓘ
Everything the guest has written to the ISViewer channel.
Sourcepub fn emux_output(&self) -> &[u8] ⓘ
pub fn emux_output(&self) -> &[u8] ⓘ
Text the guest has pushed through the EMUX xlog channel.
Sourcepub const fn enable_emux(&mut self)
pub const fn enable_emux(&mut self)
Offer the EMUX extensions to the guest.
Opt-in, because hardware has none: enabling this changes which console
backend n64-systemtest selects and therefore the instructions it
executes. Worth it for a test harness (the xlog console needs no PI or
ISViewer emulation and runs ~9x faster); wrong for anything claiming to
reproduce a real console.
Sourcepub const fn emux_exited(&self) -> bool
pub const fn emux_exited(&self) -> bool
Has the guest requested termination via EMUX xioctl(EXIT)?
Sourcepub fn sp_dma(&mut self, dma: Dma)
pub fn sp_dma(&mut self, dma: Dma)
Carry out an SP DMA the register file has programmed.
The engine lives in rustyn64-rsp and returns a description; the copy
happens here, because the RSP does not own RDRAM and a chip reaching
back into its owner is the dependency cycle docs/architecture.md exists
to prevent. The PI works the same way.
skip applies to the RDRAM side only. The SP side is contiguous and
wraps within its own 4 KiB bank — a single transfer never spans DMEM
and IMEM (N64brew RSP Interface: “if the transfer hits the end of
either memory area, it wraps around to the beginning of it”).
Sourcepub fn pi_write_word(&mut self, addr: u32, val: u32)
pub fn pi_write_word(&mut self, addr: u32, val: u32)
Write a PI register and perform any transfer it starts.
The copy happens here, not in the PI engine, because the PI does not own RDRAM — the Bus does. Having the engine reach back into its owner is the cycle this architecture exists to avoid, so the engine returns a description of the transfer and the owner carries it out.
Trait Implementations§
Source§impl AudioBus for Bus
impl AudioBus for Bus
Source§fn ai_dma_read_u32(&self, addr: u32) -> u32
fn ai_dma_read_u32(&self, addr: u32) -> u32
addr.Source§fn raise_ai_interrupt(&mut self)
fn raise_ai_interrupt(&mut self)
Source§impl Bus for Bus
impl Bus for Bus
Source§fn read_u32(&mut self, addr: u32) -> u32
fn read_u32(&mut self, addr: u32) -> u32
Read an aligned big-endian word.
Overridden for the PI external bus only. The default composes four
Bus::read_u8 calls, which would apply the 16-bit-bus off-by-two to
each byte independently and mangle bytes 2 and 3 of every word. A word
access puts its own address on the bus, so addr & !1 == addr and the
word is simply the four bytes there.
Source§fn write_sized(&mut self, addr: u32, width: u64, value: u64)
fn write_sized(&mut self, addr: u32, width: u64, value: u64)
Model the RCP’s size-blind write path.
Everything on the RCP’s internal bus latches the whole 32-bit word the
VR4300 put on SysAD, ignoring both the access size and the low two
address bits (N64brew Memory map §Physical Memory Map accesses). The
VR4300 has already shifted the source register into the byte lane the
address selects, so a narrow store writes that shifted register —
including the bits above the stored byte, which is why the effect
looks like zero-fill rather than a partial update.
n64-systemtest states the rule outright in its own header comment
(src/tests/sp_memory/mod.rs): “SH/SB are broken: they overwrite the
whole 32 bit, filling everything that isn’t written with zeroes. SD is
broken: it only writes the upper 32 bit of the value, touching only 4
bytes.” With $3 = 0x1234_5678, SB $3, 5(spmem) leaves 0x5678_0000
in the word at offset 4 — the register shifted left 16, not the byte
0x78.
RDRAM is excluded because the RI passes the low address bits and the access size on to the RDRAM devices, which build a real byte mask from them; only the RCP’s internal path throws that information away.
Source§fn write_u8(&mut self, addr: u32, val: u8)
fn write_u8(&mut self, addr: u32, val: u8)
Source§fn emux_enabled(&self) -> bool
fn emux_enabled(&self) -> bool
Source§impl<'de> Deserialize<'de> for Bus
impl<'de> Deserialize<'de> for Bus
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Source§impl RdramBus for Bus
impl RdramBus for Bus
Source§fn rdram_read(&self, addr: u32) -> u8
fn rdram_read(&self, addr: u32) -> u8
Source§fn rdram_write(&mut self, addr: u32, val: u8)
fn rdram_write(&mut self, addr: u32, val: u8)
addr — the 9th
bit RDRAM carries per byte, which the RDP Z-buffer uses for the low 2 bits
of the per-pixel dz. Returns the 2-bit value (0..=3). Read more0..=3) for the halfword at addr. Default
no-op for impls that do not model them.Source§fn rdram_read_u32(&self, addr: u32) -> u32
fn rdram_read_u32(&self, addr: u32) -> u32
rustyn64-core overrides with a fast slice path.