Skip to main content

Bus

Struct Bus 

Source
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).

§rdram_hidden: Option<Box<[u8]>>

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: Pi

The PI DMA engine (T-14-001), pulled forward from Phase 5 because n64-systemtest loads the rest of its own ELF through it.

§rsp: Rsp

The RSP coprocessor.

§rdp: Rdp

The RDP rasterizer.

§vi: Vi

The Video Interface register file (0x0440_0000). Scan-out and the scheduler-driven scan position are follow-up VI tickets.

§audio: Audio

The Audio Interface.

§cart: Cart

The cartridge (PI/SI + saves).

§rcp: RcpRegs

The RCP interface register state.

§controllers: [u32; 4]

Controller button/stick state, 4 ports (latched by the SI joybus).

Implementations§

Source§

impl Bus

Source

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.

Source

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.

Source

pub const MI_BASE: u32 = 0x0430_0000

Base of the MI register block (0x0430_0000).

Source

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.

Source

pub const SP_REGS_BASE: u32 = 0x0404_0000

Base of the eight SP interface registers (0x0404_0000).

Source

pub const SP_STATUS: u32 = 0x0404_0010

SP_STATUS (0x0404_0010), named because tests reach for it directly.

Source

pub const SP_PC: u32 = 0x0408_0000

SP_PC (0x0408_0000) — in its own window, not with the other eight.

Source

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.

Source

pub const VI_REGS_BASE: u32 = 0x0440_0000

Base of the VI register block (0x0440_0000); the AI follows at 0x0450_0000.

Source

pub const AI_REGS_BASE: u32 = 0x0450_0000

Base of the AI register block (0x0450_0000); the SI/RI follow above.

Source

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.

Source

pub const SI_BASE: u32 = 0x0480_0000

Base of the SI register block (0x0480_0000).

Source

pub const SPMEM_LEN: usize = 0x2000

DMEM + IMEM, 4 KiB each.

Source

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.

Source

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”.

Source

pub const ISVIEWER_WRITE_LEN: u32 = 0x13FF_0014

Writing this register flushes len bytes from the buffer.

Source

pub const ISVIEWER_BUF: u32 = 0x13FF_0020

The text buffer.

Source

pub const ISVIEWER_LEN: usize = 0x1000

Bytes of buffer modeled — the suite writes in 0x200 chunks.

Source

pub fn new() -> Self

Construct at power-on.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn isviewer_raw(&self) -> &[u8]

The raw ISViewer backing memory, for diagnostics.

Source

pub fn isviewer_output(&self) -> &[u8]

Everything the guest has written to the ISViewer channel.

Source

pub fn emux_output(&self) -> &[u8]

Text the guest has pushed through the EMUX xlog channel.

Source

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.

Source

pub const fn emux_exited(&self) -> bool

Has the guest requested termination via EMUX xioctl(EXIT)?

Source

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”).

Source

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

Source§

fn ai_dma_read_u32(&self, addr: u32) -> u32

Fetch a big-endian 32-bit sample word (two 16-bit L/R samples) from the AI DMA buffer in RDRAM at addr.
Source§

fn raise_ai_interrupt(&mut self)

Raise the AI interrupt on the MI (a queued buffer became active). Default no-op so a non-interrupt bus can still drive the DAC.
Source§

impl Bus for Bus

Source§

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)

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 read_u8(&mut self, addr: u32) -> u8

Read a byte at a 32-bit physical address (post-TLB).
Source§

fn write_u8(&mut self, addr: u32, val: u8)

Write a byte at a 32-bit physical address (post-TLB).
Source§

fn write_u32(&mut self, addr: u32, val: u32)

Write an aligned big-endian 32-bit word.
Source§

fn emux_enabled(&self) -> bool

Does this host offer the EMUX emulator extensions? Read more
Source§

fn emux_log(&mut self, bytes: &[u8])

EMUX xlog: the guest has asked the emulator to print bytes. Read more
Source§

fn emux_exit(&mut self)

EMUX xioctl(EXIT): the guest has asked the emulator to terminate. Read more
Source§

fn poll_irq(&mut self) -> bool

Sample the pending-interrupt level: the MI lines masked by MI_MASK. Read more
Source§

impl Debug for Bus

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Bus

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for Bus

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl RdramBus for Bus

Source§

fn rdram_read(&self, addr: u32) -> u8

Read a byte from RDRAM at a physical address.
Source§

fn rdram_write(&mut self, addr: u32, val: u8)

Write a byte to RDRAM at a physical address.
Source§

fn rdram_read_hidden(&self, addr: u32) -> u8

Read the RDRAM “hidden” bits for the 16-bit halfword at 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 more
Source§

fn rdram_write_hidden(&mut self, addr: u32, val: u8)

Write the RDRAM hidden bits (0..=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

Read a big-endian 32-bit word from RDRAM. Default composes four byte reads; rustyn64-core overrides with a fast slice path.
Source§

impl Serialize for Bus

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl VideoBus for Bus

Source§

fn raise_dp_interrupt(&mut self)

Raise the DP (RDP-done) interrupt on the MI. Default no-op for ad-hoc test buses; rustyn64-core sets the live MI_INTR.dp line.

Auto Trait Implementations§

§

impl Freeze for Bus

§

impl RefUnwindSafe for Bus

§

impl Send for Bus

§

impl Sync for Bus

§

impl Unpin for Bus

§

impl UnsafeUnpin for Bus

§

impl UnwindSafe for Bus

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,