Skip to main content

LockstepBus

Struct LockstepBus 

Source
pub struct LockstepBus { /* private fields */ }
Expand description

Lockstep bus.

Owns the entire emulator’s mutable state. The CPU borrows &mut LockstepBus during Cpu::step. The PPU and APU are ticked from the bus’s cpu_read/cpu_write implementations (3 dots per CPU cycle, NTSC; APU every CPU cycle).

Implementations§

Source§

impl LockstepBus

Source

pub fn new(rom_bytes: &[u8]) -> Result<Self, RomError>

Construct from a parsed ROM with a default 44.1 kHz audio sample rate.

§Errors

Returns the underlying [RomError] if the bytes don’t parse.

Source

pub fn with_sample_rate( rom_bytes: &[u8], sample_rate: u32, ) -> Result<Self, RomError>

Construct with an explicit audio sample rate.

§Errors

Returns the underlying [RomError] if the bytes don’t parse.

Source

pub fn with_disk( disk_bytes: &[u8], bios_bytes: &[u8], sample_rate: u32, ) -> Result<Self, RomError>

Construct a Famicom Disk System bus from a .fds disk image and a user-supplied 8 KiB BIOS (disksys.rom).

Parses the disk container ([rustynes_mappers::parse_fds]), constructs the FDS device ([rustynes_mappers::Fds]) as the bus’s Box<dyn Mapper>, and wires the bus exactly like a cartridge build (shared internal from_cart_and_mapper). The FDS is NTSC/Famicom hardware, so the synthetic [Cartridge] metadata reports [rustynes_mappers::Region::Ntsc].

§Errors

Returns [RomError] if the disk image is unparseable, or the BIOS is not exactly 8 KiB.

Source

pub fn with_nsf(nsf_bytes: &[u8], sample_rate: u32) -> Result<Self, RomError>

Build a bus that plays an NSF music file. Parses the .nsf, builds an [rustynes_mappers::NsfMapper] (a synthetic driver + the program image) as the bus’s Box<dyn Mapper>, and reports synthetic NTSC cartridge metadata (the file carries no CHR / PPU program).

§Errors

Returns [RomError::InvalidConfig] when the NSF header is malformed.

Source

pub fn reset(&mut self)

Reset (warm). Defers to Ppu::reset and clears DMA state. CPU is reset by the caller.

Source

pub fn power_cycle(&mut self)

Power-cycle. Zeroes RAM and resets all state. Caller resets the CPU.

Source

pub fn randomize_power_on_ram(&mut self, seed: u64)

Developer-mode power-on randomization (Phase 7 / T-72-005).

Fills the 2 KiB CPU work RAM and the external open-bus latch from a deterministic xorshift64 PRNG. Real hardware powers up with unreliable RAM (see nesdev “CPU power up state”); games that depend on a particular post-power-on RAM pattern are buggy, and this option surfaces such bugs the way Mesen2’s “randomize RAM on power-on” does.

The fill is seeded and deterministic — the same seed always yields the same power-on state, so the same seed + ROM + input ⇒ bit-identical determinism contract (and therefore save-state round-trip and the regression oracle) is preserved. CI and tests use the default (zeroed) path; this is opt-in via crate::Nes::from_rom_with_power_on_seed.

CPU/PPU phase alignment and DMA get/put phase are intentionally not randomized here: the lockstep scheduler’s phase is deterministic by design and randomizing it is entangled with the v2.0 master-clock scheduling refactor (see docs/audit/phase-7-assessment-2026-05-24.md).

Source

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

Borrow the framebuffer (RGBA8, 256x240).

Source

pub fn debug_set_framebuffer(&mut self, rgba: &[u8])

v1.7.0 “Forge” Workstream B (B3) — overwrite the RGBA8 output framebuffer (the Lua emu:setScreenBuffer). Output-only; see [rustynes_ppu::Ppu::debug_set_framebuffer]. debug-hooks-gated.

Source

pub fn index_framebuffer(&self) -> &[u16]

Borrow the parallel palette-index framebuffer (256x240 u16s) for the NES_NTSC composite filter. See [rustynes_ppu::Ppu::index_framebuffer].

Source

pub fn hd_tile_source(&self) -> &[HdTileSource]

v1.2.0 C3 (hd-pack) — borrow the per-pixel HD-pack tile-source buffer. See [rustynes_ppu::Ppu::hd_tile_source]. Output-only telemetry.

Source

pub const fn ntsc_phase(&self) -> u8

The per-frame NTSC composite colour phase for the NES_NTSC filter (0..=2 on NTSC; frame parity 0..=1 on PAL/Dendy). See [rustynes_ppu::Ppu::ntsc_phase].

Source

pub const fn set_custom_palette(&mut self, base: Option<[[u8; 3]; 64]>)

v1.1.0 beta.1 — install (Some) or clear (None) a custom 64-entry base palette from a loaded .pal file. A presentation override; None (default) is byte-identical to the built-in palette.

Source

pub const fn set_extra_scanlines(&mut self, lines: u16)

v1.7.0 “Forge” F3 — set the PPU extra-scanlines overclock (extra idle vblank lines per frame). 0 (default) is byte-identical to stock timing.

Source

pub const fn extra_scanlines(&self) -> u16

v1.7.0 F3 — the configured extra-scanline count (0 = stock).

Source

pub const fn set_fast_dotloop(&mut self, enabled: bool)

v2.1.8 A1 — enable/disable the specialized visible-scanline fast dot path. false (default) is byte-identical to a build without it. See [rustynes_ppu::Ppu::set_fast_dotloop].

Source

pub const fn fast_dotloop(&self) -> bool

v2.1.8 A1 — whether the visible-scanline fast dot path is enabled.

Source

pub const fn set_oam_decay(&mut self, enabled: bool)

v2.1.4 F2.3 — enable/disable the optional OAM-decay accuracy model. false (default) is byte-identical to a decay-free PPU. See [rustynes_ppu::Ppu::set_oam_decay].

Source

pub const fn set_ppu_revision(&mut self, revision: PpuRevision)

v2.1.7 P5 — select the emulated 2C02 die revision, storing it so a power-cycle re-applies it, and applying it to the live PPU now. The default revision is byte-identical. See PpuRevision.

Source

pub const fn ppu_revision(&self) -> PpuRevision

v2.1.7 P5 — the currently-selected 2C02 die revision.

Source

pub const fn set_power_up_palette(&mut self, init: PaletteInit)

v2.1.7 P5 — apply a power-up palette-RAM pattern, storing it so a power-cycle re-applies it and writing it to the live PPU’s palette RAM now. The default (PaletteInit::Zeroed) is byte-identical. See PaletteInit.

Source

pub const fn power_up_palette(&self) -> PaletteInit

v2.1.7 P5 — the currently-selected power-up palette pattern.

Source

pub fn set_power_on_ram(&mut self, ram: PowerOnRam)

v2.1.7 P5 — select the power-on work-RAM fill, storing it so a power-cycle re-applies it, and applying it to the current RAM now. The default (crate::nes::PowerOnRam::Zeroed) is byte-identical. See crate::nes::PowerOnRam.

Source

pub const fn power_on_ram(&self) -> PowerOnRam

v2.1.7 P5 — the currently-selected power-on work-RAM fill.

Source

pub const fn oam_decay_enabled(&self) -> bool

v2.1.4 F2.3 — whether the optional OAM-decay model is enabled.

Source

pub const fn set_cpu_2a03_revision(&mut self, revision: Cpu2A03Revision)

v2.1.7 — set the emulated 2A03 die revision (DMA “unexpected read” axis). Cpu2A03Revision::Rp2A03G (default) is byte-identical to the pre-v2.1.7 core; Cpu2A03Revision::Rp2A03H is the opt-in later-die model. See the Cpu2A03Revision docs + ADR 0033.

Source

pub const fn cpu_2a03_revision(&self) -> Cpu2A03Revision

v2.1.7 — the configured 2A03 die revision (default Cpu2A03Revision::Rp2A03G).

Source

pub const fn region(&self) -> Region

Cartridge region (NTSC / PAL / Dendy / Multi). Drives wall-clock frame pacing in the frontend and clock-divider selection inside the PPU + APU.

Source

pub const fn prg_rom_len(&self) -> usize

Length in bytes of the loaded cartridge’s PRG-ROM (read-only metadata).

Source

pub const fn chr_rom_len(&self) -> usize

Length in bytes of the loaded cartridge’s CHR-ROM (0 when the board uses CHR-RAM). Read-only metadata.

Source

pub fn peek_cpu(&mut self, addr: u16) -> u8

Direct CPU-bus probe (does not advance time). Intended for blargg-style status polls at $6000-$7FFF and the test harness’s mapper-resident WRAM peek. Note that this still has side effects on PPU registers ($2002 clears VBL and toggle, $2007 reads advance the buffer); callers should avoid touching $2000-$3FFF via peek.

Source

pub fn add_genie_code(&mut self, code: &str) -> Result<(), GenieError>

Add a Game Genie code (6 or 8 characters, case-insensitive). The code patches a PRG address ($8000-$FFFF) on the CPU read path; adding a code at an address that already has one replaces it.

§Errors

Returns GenieError if the code string cannot be decoded.

Source

pub fn remove_genie_code(&mut self, code: &str)

Remove the active Game Genie code whose canonical (upper-case) string matches code. No-op if no such code is active.

Source

pub fn clear_genie_codes(&mut self)

Remove all active Game Genie codes.

Source

pub fn genie_codes(&self) -> impl Iterator<Item = &GenieCode>

Iterate the active Game Genie codes (address-sorted).

Source

pub fn debug_peek_cpu(&mut self, addr: u16) -> u8

Side-effect-free CPU bus sample for the debugger hex viewer.

Returns the bus’s view of the byte at addr without the side effects peek_cpu / raw_cpu_read carry on PPU register space (no VBL clear, no PPUDATA buffer advance, no open-bus update). For PPU registers we read back the cached snapshot; for mappers we go through cpu_read — the overwhelming majority of mappers are idempotent on $8000-$FFFF reads, and the few that latch on read (MMC2 in particular) document that behavior as inherent.

Takes &mut self because mapper cpu_read is &mut — but no emulator-visible state advances. The CPU cycle counter, PPU scheduler, and APU all stay put.

Source

pub fn debug_peek_ppu(&mut self, addr: u16) -> u8

Side-effect-free PPU bus sample ($0000-$3FFF).

$0000-$1FFF -> mapper CHR, $2000-$3EFF -> nametable (via mapper’s mirroring), $3F00-$3FFF -> palette RAM.

Source

pub fn debug_poke_ppu(&mut self, addr: u16, value: u8)

v1.7.0 “Forge” Workstream A1 — debugger writeback. The structural mirror of Self::debug_peek_ppu: $0000-$1FFF → mapper CHR (ppu_write, a no-op on CHR-ROM), $2000-$3EFF → nametable (mapper-absorbed, else CIRAM via the active mirroring), $3F00-$3FFF → palette RAM.

Side-effect-free w.r.t. the run loop: it is reached only through the gated post-frame poke path (the same caller-side, after-run_frame stage the raw RAM cheats use), so the deterministic core run loop is unchanged and the no-edit path is byte-identical. debug-hooks-gated.

Source

pub const fn debug_poke_oam(&mut self, idx: u8, value: u8)

v1.7.0 “Forge” Workstream A1 — debugger writeback for one OAM byte (idx = 0..256). debug-hooks-gated; reached only through the gated post-frame poke path, so the default build is byte-identical.

Source

pub const fn ppu(&self) -> &Ppu

Borrow the PPU (debugger / tests).

Source

pub const fn ppu_mut(&mut self) -> &mut Ppu

Mutably borrow the PPU (debugger / tests).

Source

pub const fn apu(&self) -> &Apu

Borrow the APU (debugger / tests).

Source

pub const fn apu_mut(&mut self) -> &mut Apu

Mutably borrow the APU (debugger / tests).

Source

pub const fn set_buttons(&mut self, port: usize, buttons: Buttons)

Set the buttons currently held on player port. Ports 0/1 are the standard $4016/$4017 controllers; ports 2/3 are players 3/4 on the Four Score adapter (only polled when Self::set_four_score is on). The change takes effect on the next strobe edge.

§Panics

Panics if port is not in 0..=3.

Source

pub fn set_expansion_device(&mut self, port: usize, device: Option<InputDevice>)

Attach (or replace) a non-standard overlay device on port (0 = $4016, 1 = $4017). Pass None to unplug the device and return the port to the standard controller / Four Score path (byte-identical).

§Panics

Panics if port is not in 0..=1.

Source

pub const fn expansion_device(&self, port: usize) -> &Option<InputDevice>

Borrow the overlay device attached to port, if any.

§Panics

Panics if port is not in 0..=1.

Source

pub const fn set_paddle(&mut self, port: usize, position: u8, fire: bool)

Update an attached Vaus paddle’s position + fire state on port. No-op if the attached device is not a Vaus (or no device is attached).

§Panics

Panics if port is not in 0..=1.

Source

pub const fn set_zapper(&mut self, port: usize, x: u16, y: u16, trigger: bool)

Update an attached Zapper’s aim point + trigger on port. No-op if the attached device is not a Zapper (or no device is attached).

§Panics

Panics if port is not in 0..=1.

Source

pub const fn set_power_pad(&mut self, port: usize, buttons: u16)

Update an attached Power Pad’s live button mask (bit i = mat button i+1) on port. No-op if the attached device is not a Power Pad.

§Panics

Panics if port is not in 0..=1.

Source

pub const fn set_snes_mouse( &mut self, port: usize, dx: i16, dy: i16, left: bool, right: bool, sensitivity: u8, )

Update an attached SNES mouse’s movement + buttons + sensitivity on port. No-op if the attached device is not a mouse.

§Panics

Panics if port is not in 0..=1.

Source

pub const fn set_family_keyboard(&mut self, port: usize, keys: [u8; 9])

Update an attached Family BASIC keyboard’s pressed-key bitmap on port (one byte per matrix row). No-op if the attached device is not a keyboard.

§Panics

Panics if port is not in 0..=1.

Source

pub const fn set_family_trainer(&mut self, port: usize, buttons: u16)

v1.3.0 Workstream F1 — update an attached Family Trainer mat’s 12-button mask on port. No-op if the attached device is not a Family Trainer.

§Panics

Panics if port is not in 0..=1.

Source

pub const fn set_subor_keyboard(&mut self, port: usize, keys: [u8; 9])

v1.3.0 Workstream F1 — update an attached Subor keyboard’s pressed-key bitmap on port. No-op if the attached device is not a Subor keyboard.

§Panics

Panics if port is not in 0..=1.

Source

pub const fn set_konami_hyper_shot(&mut self, port: usize, buttons: u8)

v1.3.0 Workstream F1 — update an attached Konami Hyper Shot’s 4-button mask on port. No-op if the attached device is not a Konami Hyper Shot.

§Panics

Panics if port is not in 0..=1.

Source

pub const fn set_bandai_hyper_shot(&mut self, port: usize, sensors: u8)

v1.3.0 Workstream F1 — update an attached Bandai Hyper Shot’s 8-sensor mask on port. No-op if the attached device is not a Bandai Hyper Shot.

§Panics

Panics if port is not in 0..=1.

Source

pub const fn set_mirroring_override(&mut self, m: Option<Mirroring>)

v1.1.0 beta.1 (T-110-B4) — set (Some) or clear (None) the per-game nametable mirroring override. A frontend load-time correction; None (default) defers to the mapper (byte-identical).

Source

pub const fn mirroring_override(&self) -> Option<Mirroring>

The current per-game mirroring override (for the save-state).

Source

pub fn mapper_has_hardwired_mirroring(&self) -> bool

Whether the loaded mapper hardwires its nametable mirroring (so an external mirroring correction is safe to honor). See [rustynes_mappers::Mapper::has_hardwired_mirroring].

Source

pub const fn set_event_logging(&mut self, enabled: bool)

v1.1.0 beta.2 (T-110-C3) — start/stop event-viewer recording.

Source

pub const fn event_logging(&self) -> bool

Whether event-viewer recording is on.

Source

pub fn events(&self) -> &[EventRec]

The events captured so far this frame.

Source

pub const fn set_access_logging(&mut self, enabled: bool)

v1.1.0 beta.3 (T-110-E2) — start/stop the Lua bus-access log.

Source

pub const fn access_logging(&self) -> bool

Whether the bus-access log is recording.

Source

pub fn accesses(&self) -> &[AccessRec]

The CPU bus accesses captured so far this frame.

Source

pub fn clear_accesses(&mut self)

Clear the bus-access log (called per frame by the run loop).

Source

pub const fn set_interrupt_logging(&mut self, enabled: bool)

v1.2.0 (T-110-E1) — start/stop the Lua interrupt-service log.

Source

pub const fn interrupt_logging(&self) -> bool

Whether the interrupt-service log is recording.

Source

pub fn interrupts(&self) -> &[InterruptRec]

The interrupt-service entries captured so far this frame.

Source

pub fn clear_interrupts(&mut self)

Clear the interrupt-service log (called per frame by the run loop).

Source

pub const fn set_event_breakpoints(&mut self, mask: u16)

v1.4.0 Workstream D (D2) — set the armed event-breakpoint category mask (a bit-OR of EventBpKind::bit). 0 disarms all (the default + the per-cycle-cheap path).

Source

pub const fn event_breakpoints(&self) -> u16

The armed event-breakpoint category mask.

Source

pub const fn take_event_break_hit(&mut self) -> Option<EventBreakHit>

Take the first event-breakpoint hit of the current frame (cleared on read). The frontend polls this after run_frame.

Source

pub const fn clear_event_break_hit(&mut self)

Clear any recorded event-breakpoint hit (called per frame by the run loop so each frame starts fresh).

Source

pub fn clear_events(&mut self)

Clear the event log (called at each frame start while recording).

Source

pub fn sample_zapper_light(&mut self)

Sample the framebuffer luminance at each attached Zapper’s aim point. Called once per frame (only does work when a Zapper is attached, so the no-device path is byte-identical).

Source

pub const fn controller(&self, port: usize) -> &Controller

Borrow controller port (0/1 = $4016/$4017; 2/3 = Four Score players 3/4).

§Panics

Panics if port is not in 0..=3.

Source

pub const fn take_frame_complete(&mut self) -> bool

Has the PPU completed a frame? Drains the latch.

Source

pub fn drain_audio(&mut self) -> Vec<f32>

Drain finalized audio samples (host sample rate, normalized [0, ~1]).

Source

pub fn drain_audio_into(&mut self, out: &mut [f32]) -> usize

Drain into a slice.

Source

pub const fn cycle(&self) -> u64

Cumulative CPU cycle count.

Source

pub const fn current_m2_phase(&self) -> M2Phase

Returns the M2 phase the lockstep scheduler is currently in.

The scheduler ticks the PPU 3 dots per CPU cycle. Convention: M2Phase::Low is the FIRST half of the cycle (the cycle’s pre-sub-dot-1 portion, corresponding to silicon’s φ1); M2Phase::High is the SECOND half (post-sub-dot-1, silicon’s φ2). The boundary is the M2-rising edge between sub-dot 1 and sub-dot 2.

At the start of tick_one_cpu_cycle the bus is in M2Phase::Low. After sub-dot 1 of the 3-PPU-dot tick loop, it transitions to M2Phase::High. After the cycle’s last sub-dot the bus advances the cycle counter and returns to M2Phase::Low for the next cycle.

This accessor is informational. As of Phase B2 the bus stores per-phase IRQ snapshots (read by [Bus::poll_irq_at_phase]) independently of this accessor — current_m2_phase() itself is not consulted by the CPU’s IRQ sample path.

Source

pub const fn set_vs_dip(&mut self, dip: u8)

Set the Vs. System 8-bit DIP switch bank (switch 1 = bit 0 .. switch 8 = bit 7). No effect on non-Vs. carts. Default 0.

Source

pub const fn vs_dip(&self) -> u8

Current Vs. System DIP switch bank.

Source

pub const fn set_vs_ppu_type(&mut self, t: VsPpuType)

Override the Vs. System PPU type and re-apply the output palette / 2C05 quirks immediately.

iNES-1.0 dumps carry no NES 2.0 byte-13, so the parser defaults a Vs. cart to [rustynes_mappers::VsPpuType::Rp2C03]; a per-game database (keyed on the ROM SHA-256) supplies the correct 2C04-000x / 2C05 type, which the frontend applies through this setter. No effect on the running game’s logic — only the colour LUT the PPU emits through. No-op shape on non-Vs. carts (the default path never calls this).

Source

pub const fn insert_coin(&mut self, acceptor: u8)

Latch a Vs. System coin insertion. acceptor 0 = acceptor #1 ($4016 bit 5), 1 = acceptor #2 ($4016 bit 6); any other value is ignored. The frontend should clear the latch (see Self::clear_coin) after the real-hardware ~40-70 ms window (a few frames). No effect on non-Vs. carts.

Source

pub const fn clear_coin(&mut self)

Clear all latched Vs. System coin-insert signals.

Source

pub const fn set_vs_service(&mut self, pressed: bool)

Set / clear the Vs. System service button ($4016 bit 2).

Source

pub const fn set_vs_sub(&mut self, is_sub: bool)

v2.0.0 beta.5 (Vs. DualSystem): mark this console as the SUB half of a DualSystem pair ($4016 reads return bit 7 = 0x80). Wrapper-only.

Source

pub const fn set_vs_external_irq(&mut self, asserted: bool)

v2.0.0 beta.5 (Vs. DualSystem): drive this console’s external /IRQ line (the partner console’s $4016 bit-1 signal, Mesen2 IRQSource::External). Wrapper-only; OR’d into the IRQ level.

Source

pub const fn take_vs_mainsub_edge(&mut self) -> Option<bool>

v2.0.0 beta.5 (Vs. DualSystem): poll-and-clear the latched $4016 bit-1 (main/sub comms signal) LEVEL. Returns Some(level) whenever this console wrote $4016 since the last poll — deliberately level-driven, not edge-filtered (see the vs_4016_bit1_dirty field doc); the wrapper turns the level into the partner’s external-IRQ assert (LOW asserts, HIGH clears). The shared-WRAM convergence (pump_comms’s separate drain_vs_dual_wram_writes step) runs on BOTH consoles every poll, independent of this bit-1 signal.

Source

pub fn enable_vs_dual_wram(&mut self)

v2.0.0 beta.5 (Vs. DualSystem): provision the mapper-99 shared 2 KiB WRAM window ($6000-$7FFF). Wrapper-only; no-op on other boards.

Source

pub fn set_vs_dual_sub(&mut self)

v2.0.0 beta.5 (Vs. DualSystem): mark the mapper as the SUB console’s instance (banks the second PRG half + upper CHR pages — the two CPUs run different programs). Wrapper-only cabinet wiring.

Source

pub fn take_vs_dual_wram_writes(&mut self) -> Vec<(u16, u8)>

v2.0.0 beta.5 (Vs. DualSystem): drain this console’s shared-WRAM write log for the wrapper to replay into the partner console (the fully-shared MAME model). Empty off-board. Allocates a fresh Vec each call — fine for diagnostics/tests, NOT used by the hot pump_comms path (see Self::drain_vs_dual_wram_writes).

Source

pub fn drain_vs_dual_wram_writes(&mut self, dst: &mut Vec<(u16, u8)>)

v2.0.0 beta.5 (Vs. DualSystem): drain this console’s shared-WRAM write log into a caller-owned, reusable dst buffer — the hot-path counterpart of Self::take_vs_dual_wram_writes, used by VsDualSystem::pump_comms (called after every stepped instruction) to avoid allocating a fresh Vec on every call.

Source

pub fn apply_vs_dual_wram_write(&mut self, offset: u16, value: u8)

v2.0.0 beta.5 (Vs. DualSystem): replay one partner-console write into this console’s shared-WRAM copy (no re-log).

Source

pub fn take_vs_dual_wram(&mut self) -> Option<Box<[u8]>>

v2.0.0 beta.5 (Vs. DualSystem): take the shared-WRAM copy (wrapper snapshot-restore normalization). None off-board.

Source

pub fn set_vs_dual_wram(&mut self, wram: Box<[u8]>)

v2.0.0 beta.5 (Vs. DualSystem): install a shared-WRAM copy (the other half of the restore normalization).

Source

pub fn is_vs_system(&self) -> bool

True when the running cart is Vs. System hardware (NES 2.0 console type).

Source

pub const fn is_vs_dual_system(&self) -> bool

True when the cart’s header marks a Vs. DualSystem board (two CPUs / two PPUs). Detection only — the dual-console emulation is a documented v2.0 deferral (docs/audit/vs-dualsystem-design-2026-06-11.md); this lets the frontend surface a clear note instead of a black screen.

Source

pub fn mapper_debug_info(&self) -> MapperDebugInfo

Mapper debug info for the debugger UI: the mapper’s own bank/IRQ state ENRICHED (v1.5.0 “Lens” Workstream I8) with the cartridge-level metadata the bus owns — submapper, accuracy tier, ROM/RAM sizes, battery, the IRQ mechanism, and the expansion-audio chip. Output-only; these enrichment fields are filled here rather than in each of the 100+ mappers, and they default to empty (so a mapper’s own debug_info() is unchanged).

Source

pub const fn mapper_caps(&self) -> MapperCaps

The cached per-cycle mapper capability flags (see [rustynes_mappers::MapperCaps]). caps.audio reflects whether the loaded mapper has on-cart expansion audio with the mapper-audio feature compiled in — used by the frontend to surface expansion-channel mixing controls only for boards that actually have them.

Source

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

Borrow CPU RAM (2 KiB).

Source

pub const fn controllers_ref(&self) -> &[Controller; 2]

Borrow both controllers as a slice.

Source

pub const fn controllers34_ref(&self) -> &[Controller; 2]

Borrow the Four Score players 3 & 4 (save-state).

Source

pub const fn set_four_score(&mut self, enabled: bool)

Enable/disable the Four Score 4-player adapter. Off by default; while off, $4016/$4017 behave exactly as the standard two controllers (byte-identical reads — determinism + save-states unaffected).

Source

pub const fn four_score(&self) -> bool

Whether the Four Score adapter is currently enabled.

Source

pub fn disk_side_count(&self) -> usize

Number of disk sides in the inserted FDS image (0 for cartridge builds).

Source

pub fn inserted_disk_side(&self) -> Option<usize>

The currently inserted FDS disk side, or None when ejected (or for a cartridge build).

Source

pub fn set_disk_side(&mut self, side: Option<usize>)

Insert FDS side i (Some) or eject (None). No-op on cartridge builds.

Source

pub fn nsf_song_count(&self) -> u8

Number of selectable NSF songs (0 for cartridge / disk builds).

Source

pub fn nsf_current_song(&self) -> u8

The currently-selected 0-based NSF song (0 for cartridge / disk builds).

Source

pub fn nsf_set_song(&mut self, song: u8) -> bool

Select a 0-based NSF song. Returns true if this is an NSF build (so the caller re-runs the reset that re-enters the driver’s init).

Source

pub fn enable_fds_trace(&mut self)

Start recording the diagnostic FDS read-stream trace (off by default; observation-only). No-op on cartridge builds.

Source

pub fn take_fds_trace(&mut self) -> Vec<FdsTraceRec>

Drain the accumulated FDS read-stream trace records (empty for cartridge builds / when tracing was never enabled).

Source

pub fn disk_image_bytes(&self) -> Vec<u8>

Re-serialize the (possibly-modified) FDS disk image to its byte layout for host persistence. Empty for cartridge builds.

Source

pub fn disk_is_dirty(&self) -> bool

Whether the FDS disk image has unsaved writes.

Source

pub fn clear_disk_dirty(&mut self)

Clear the FDS disk dirty flag (after the host persists the image).

Source

pub fn set_disk_write_protected(&mut self, protected: bool)

Mark the inserted FDS disk read-only (true) or writable (false).

Source

pub const fn bus_misc_state(&self) -> BusMiscState

Bus-side bookkeeping snapshot used by bus_snapshot::encode_bus.

Source

pub const fn set_bus_misc_state(&mut self, s: BusMiscState)

Apply a previously-snapshotted bus bookkeeping state.

Source

pub const fn set_cycle(&mut self, cycle: u64)

Set the cumulative CPU cycle counter (used by save-state restore).

Source

pub fn set_ram_bytes(&mut self, bytes: &[u8]) -> Result<(), SnapshotError>

Overwrite the 2 KiB CPU RAM.

§Errors

Returns SnapshotError::SectionInvalid if bytes.len() != 2048.

Source

pub const fn set_controllers(&mut self, controllers: [Controller; 2])

Overwrite both controllers’ state.

Source

pub const fn set_controllers34(&mut self, controllers: [Controller; 2])

Overwrite the Four Score players 3 & 4 (save-state restore).

Source

pub fn poke_ram(&mut self, addr: u16, value: u8)

Write a byte directly into CPU work RAM ($0000-$1FFF, mirrored every $800). Used by the frontend’s raw RAM cheats (GameShark-style), applied caller-side after crate::Nes::run_frame so the core run loop stays pure (the determinism contract is unperturbed for the no-cheat path). No-op for addresses outside system RAM.

Source

pub fn snapshot(&self, rom_hash_tag: [u8; 6]) -> Vec<u8>

Encode the entire bus + chip state into a .rns snapshot.

Returns the bytes the caller should persist via frontend::save_state (or feed into the rewind ring).

The output is bit-deterministic: same (seed, ROM, input sequence) produces identical bytes.

Source

pub fn snapshot_into(&self, out: &mut Vec<u8>, rom_hash_tag: [u8; 6])

v2.8.0 Phase 3 — Self::snapshot into a caller-owned buffer (cleared first; capacity reused across calls). The per-call allocation of the ~250 KiB blob matters to per-frame consumers (run-ahead, the netplay save-state ring, rewind).

Source

pub fn restore(&mut self, data: &[u8]) -> Result<(), SnapshotError>

Apply a previously snapshotted blob to the bus and chips. The CPU is restored separately by crate::Nes::restore.

§Errors

Returns SnapshotError for unknown sections, version mismatches, or malformed bodies.

Trait Implementations§

Source§

impl Bus for LockstepBus

Source§

fn read(&mut self, addr: u16) -> u8

Pure address-space read under R1 (the DMA drain happens in [Bus::cpu_clock]; Phase 3 will split the drain out of cpu_read). Phase 1 delegates to the legacy path so the contract compiles.

Source§

fn cpu_divider(&self) -> u64

R1 master clocks per CPU cycle for the cartridge region (NTSC 12 / PAL 16 / Dendy 15) — the cpu_divider half of region_dividers. Drives the CPU loop’s master_clock advance + read/write split so the CPU<->PPU phase is 3:1 NTSC, 3.2:1 PAL, 3:1 Dendy.

Source§

fn run_ppu_to(&mut self, target: u64, is_post_access: bool)

R1 double catch-up: tick whole PPU dots while ppu_clock + ppu_divider <= target.

R1c-3 (mmc3-m2-phase-irq, default-off): when the feature is enabled, sub_dot is seeded from the REAL M2-phase of this catch-up call (0 = pre-access / M2-low, called from Cpu::start_cycle before the bus access; 2 = post-access / M2-high, called from Cpu::end_cycle after it) instead of always restarting at 0. Prior to this experiment sub_dot was a call-LOCAL counter that reset to zero on every invocation of this function — since run_ppu_to is called twice per CPU cycle (once per half) and each half typically ticks at most one PPU dot, the value threaded to Mapper::notify_a12_at_sub_dot was almost always 0 regardless of which half of the cycle actually produced the A12 transition. That meant the M2-phase plumbing ADR-0002 describes (“sub-dot 0/1 is M2-low, 2 is M2-high”) was never actually true on the live R1 (non-DMA) scheduler path — only on the legacy tick_one_cpu_cycle DMA-burst path, which genuinely walks all 3 dots of a cycle in one call with a persistent counter. This experiment closes that gap so MMC3’s (default-off) M2-phase-aware IRQ-visibility pipeline can be evaluated against real phase data on the promoted core. See docs/adr/0002-irq-timing-coordination.md and docs/audit/r1r2-per-dot-scheduler-attempt-2026-07-02.md.

When the feature is OFF this compiles to the exact prior call-local-counter behavior (sub_dot always starts at 0) — byte-identical default build, per the project’s additive/off-by- default convention.

Source§

fn cpu_clock(&mut self)

R1: one CPU cycle of bus-side work (NO PPU advance — that lives in [Bus::run_ppu_to]). Controller strobe + bus-side DMA drain + cycle counter + per-cycle PPU/mapper hooks + APU tick. DMA stays bus-side (the pivot’s working service_dmc_dma); Phase 3 wires the dma_mc_consumed coherence accounting.

Source§

fn cpu_read(&mut self, addr: u16) -> u8

Read a byte at addr.
Source§

fn cpu_write(&mut self, addr: u16, value: u8)

Write value to addr.
Source§

fn poll_nmi(&mut self) -> bool

Edge-triggered NMI poll. Returns true exactly once per high-to-low transition of the NMI line; subsequent calls return false until the next transition.
Source§

fn poll_irq(&mut self) -> bool

Level-sensitive IRQ. Sampled by the CPU on every instruction’s second-to-last cycle; only honored when the CPU’s I flag is clear.
Source§

fn poll_irq_at_phase(&mut self, phase: M2Phase) -> bool

Phase-aware level-sensitive IRQ sample. Read more
Source§

fn on_cpu_cycle(&mut self)

Called once per CPU cycle consumed. Used by the scheduler to advance the PPU/APU in lockstep (Phase 2+) and by the test harness to count cycles for golden-log compare.
Source§

fn internal_data_bus(&self) -> u8

Most recent value driven onto the internal CPU data bus. Read more
Source§

fn cycle_count(&self) -> u64

Cumulative bus-side cycle counter. Read more
Source§

fn notify_irq_service(&mut self, vector: u16, is_nmi: bool)

Notify the bus that the CPU is about to perform an interrupt vector fetch from vector ($FFFE for IRQ/BRK, $FFFA for NMI, or $FFFA if an IRQ/BRK service sequence was hijacked by an NMI edge during cycles 1..=5 of the service sequence). is_nmi is true for an NMI service entry and false for an IRQ or BRK service entry (so the bus can distinguish hijack from a clean NMI even when the vector is the same). Read more
Source§

fn write(&mut self, addr: u16, value: u8)

Pure address-space write. Default delegates to [Bus::cpu_write].
Source§

fn cpu_clock_apu_dmc(&mut self)

F-2: tick ONLY the DMC byte-timer + DMA arm, at END of cycle (called from Cpu::end_cycle after the access + PPU catch-up). This places the DMC fire-phase at main’s end-of-cycle position (so DMASync’s $4000 open-bus conflict lands), while the rest of the APU (incl. the IRQ line) stays on the cycle-start cpu_clock tick (so the C1 φ2 IRQ sample is unchanged). Default no-op. Pairs with Apu::set_dmc_driven_externally.
Source§

fn take_dma_mc_consumed(&mut self) -> u64

Master clocks consumed by bus-side DMA cycles since the last call, then reset to 0. The R1 CPU loop folds this into master_clock in end_cycle so the CPU<->PPU phase stays coherent across a bus-side DMA span. Default 0 (no bus-side DMA accounting on test stubs).
Source§

fn irq_level(&self) -> bool

Live IRQ line level (mapper IRQ OR APU frame-counter/DMC IRQ). The CPU does the I-flag mask + one-cycle prev_run_irq delay itself. Default false; the production bus overrides this.
Source§

fn nmi_level(&self) -> bool

Live /NMI line level (PPU-driven). The CPU does its own edge detect + one-cycle prev_need_nmi delay. Default false (test stubs).
Source§

fn dmc_dma_pending(&self) -> bool

Phase B (interleaved DMC DMA): is a DMC DMA pending and needing cycles? The CPU loops on this in read1, running one dmc_dma_step per R1 cycle BEFORE its own read (DMA halts only on read cycles). Default false.
Source§

fn dmc_dma_defer_load_entry(&self) -> bool

mc-r1-dmc-load-get-entry: defer a LOAD whose first-service would be a PUT cycle by 1 CPU cycle so it enters on a GET (span-3 hardware load). Gates BOTH the read1 loop AND the idle_tick loop (DMASync’s load fires during NOPs=idle).
Source§

fn dmc_dma_step(&mut self, halted_addr: u16)

Phase B: perform ONE cycle’s worth of interleaved DMC DMA bus access (halt re-read / sample get), advancing the halt/get state. halted_addr is the CPU read the DMA is preempting. Default no-op.
Source§

fn dmc_dma_step_idle(&mut self)

mc-r1-dmc-idle-halt: perform one interleaved DMC-DMA cycle during a CPU INTERNAL cycle (no instruction read). The bus supplies the held address (its last-read bus address) since idle_tick has none. Default no-op.
Source§

fn oam_dma_pending(&self) -> bool

Stage-D (mc-r1-full-cpu): is an OAM DMA pending or in flight? The CPU loops on this in read1 (after the DMC loop, DMC-get-before-OAM-get), so each OAM cycle runs CPU-driven (wrapped start_cycle/end_cycle) and samples IRQ/NMI via the φ2 pipeline — the surface the bus-burst bypassed. Default false.
Source§

fn oam_dma_step(&mut self, halted_addr: u16)

Stage-D: perform ONE cycle of the OAM DMA (set-up on first call from a pending $4014, then halt/align/read/write per cycle). Does NOT advance time — the surrounding start_cycle/end_cycle do. Default no-op.
Source§

fn unified_dma_pending(&self) -> bool

W3-Stage-1 (mc-r1-dma-unified): is ANY DMA work pending for the unified DMC/OAM engine — a serviceable DMC DMA (pending and not a load deferred to its get-cycle entry, the mc-r1-dmc-load-get-entry rule), a $4014 OAM DMA awaiting its first cycle, or an OAM transfer still in flight? The ONE Cpu::read1/idle_tick DMA loop spins on this, running one [Bus::unified_dma_cycle] per CPU cycle (each a full R1 cycle: start_cycle -> dispatch -> end_cycle, so every DMA cycle keeps the φ2 IRQ sample — the C1-safe shape). Default false.
Source§

fn unified_dma_cycle(&mut self, halted_addr: u16)

W3-Stage-1 (mc-r1-dma-unified): ONE cycle of the unified DMC/OAM DMA engine — a direct port of the TriCNES _6502 per-cycle DMA dispatch table (the SINGLE driver standalone DMC, standalone OAM, and the overlap all ride), at FLOOR parity for this stage. halted_addr is the CPU read the DMA is preempting (the parked 6502 address bus). Does NOT advance time — the surrounding start_cycle/end_cycle do. Default no-op.
Source§

fn unified_dma_cycle_idle(&mut self)

W3-Stage-1 (mc-r1-dma-unified): one unified-engine DMA cycle during a CPU INTERNAL cycle (no instruction read; the bus supplies its held last-read address). The unified replacement for [Bus::dmc_dma_step_idle]. Default no-op.
Source§

fn oam_dma_in_flight(&self) -> bool

Program M (M-2, mc-r1-dmc-oam-overlap): is an OAM DMA actually IN FLIGHT (started, cycles still owed) — distinct from oam_dma_pending, which is true for a not-yet-started $4014 write too. The overlap loop uses this to decide whether a DMC halt cycle can SHARE an OAM cycle. Default false.
Source§

fn oam_dma_overlap_ready(&self) -> bool

W3-Stage-0 (mc-r1-counter-collapse boundary realign): may a pending DMC DMA join an OAM DMA as an OVERLAP event? Default delegates to [Bus::oam_dma_in_flight]. Under the counter-collapse flag the bus also answers true for a $4014 write that is PENDING but not yet started: the end-of-cycle byte-timer shift can surface the DMC arm in the gap between the $4014 write and OAM’s first cycle, and routing that arm to the standalone dmc_dma_step (full unshared span) instead of the overlap event is exactly the DMC+OAM idx[7] regime-transition error (lockstep latches OAM in drain_dma BEFORE its DMC-pending check, so the same arm overlaps OAM’s halt/alignment cycles there).
Source§

fn dmc_dma_last_was_get(&self) -> bool

Program M (M-2): did the most recent [Bus::dmc_dma_step] perform the DMC GET (the sample fetch) rather than a halt/dummy/align cycle? The overlap loop advances OAM on non-GET (halt) cycles only — the GET steals an OAM slot. Default false.
Source§

fn oam_dma_overlap_cycle(&mut self)

Program M (M-2): advance ONE OAM DMA cycle that is SHARED with a DMC halt cycle (the 6502 is RDY-halted by the DMC, but the OAM engine keeps consuming its read/write slot on the external bus). Does NOT advance time — the surrounding start_cycle/end_cycle do. Default no-op.
Source§

fn dmc_overlap_begin(&mut self, halted_addr: u16) -> u32

Program M (M-2, exact): begin ONE DMC-DMA-during-OAM event, mirroring the lockstep service_dmc_dma_during_oam prologue. Latches the DMA span + the open-bus replay and returns the UNCONDITIONAL halt/dummy/align noop count (2 for a short/load DMA, 3 for a reload) — NOT parity-gated. The CPU then runs exactly that many [Bus::dmc_overlap_noop_cycle]s, one [Bus::dmc_overlap_get_cycle], and (if OAM still owes) one [Bus::dmc_overlap_realign_cycle]. halted_addr is the CPU read the DMA pair is preempting — used as the OAM halt address when the event starts a PENDING (not-yet-latched) $4014 OAM DMA (the counter-collapse boundary case; an already-in-flight OAM keeps its own latched halt address). Default 0 (no DMC event).
Source§

fn dmc_overlap_noop_cycle(&mut self)

Program M (M-2, exact): one DMC halt/dummy/align cycle that OVERLAPS OAM. Replays the held CPU read’s side-effect, then (if OAM still owes) advances one OAM slot. Mirrors lockstep’s noop-loop body (replay_dma_noop_read + clock_oam_dma_cycle) minus the time tick. Default no-op.
Source§

fn dmc_overlap_get_cycle(&mut self)

Program M (M-2, exact): the DMC GET cycle — owns the memory read; OAM is STALLED (does NOT advance). Fetches + delivers the sample and clears the DMC-DMA pending state. Mirrors lockstep’s get block + the R1 dmc_dma_step GET. Default no-op.
Source§

fn dmc_overlap_realign_cycle(&mut self)

Program M (M-2, exact): the post-GET realign stall — ONE extra OAM-stalled cycle (OAM does NOT advance) so the next OAM read resumes on a later get, mirroring lockstep’s if dma_cycles_owed > 0 { tick }. The cycle the prior per-cycle scaffold was MISSING. Default no-op.
Source§

fn dmc_abort_pending(&self) -> bool

accuracycoin-100 Phase 2 (mc-r1-dmc-abort-cancel): is a 1-byte non-looping implicit DMC-DMA abort matured and awaiting service? The CPU consults this at the top of read1/write1. Default false.
Source§

fn dmc_abort_is_get_cycle(&self) -> bool

accuracycoin-100 Phase 2: is the upcoming cycle a GET (read) cycle for the DMC DMA (!put_cycle)? On a get cycle the matured abort runs as a 1-cycle DMA (Y=1); on a put cycle (or any CPU write) it does NOT occur (Y=0, “the abort will not land on a write cycle”). Default false.
Source§

fn dmc_abort_halt_step(&mut self, halted_addr: u16)

accuracycoin-100 Phase 2: service the matured abort as a 1-cycle DMA (Y=1) — one halt re-read of halted_addr, then clear the abort + the pending reload. Called by read1 only on a get cycle. Default no-op.
Source§

fn dmc_abort_cancel(&mut self)

accuracycoin-100 Phase 2: cancel the matured abort with NO halt cycle (Y=0) — the abort lands on a write/put cycle so the DMA does not occur. Clears the abort + the pending reload. Default no-op.
Source§

fn trace_end_cycle(&mut self)

Diagnostic-only hook fired once per R1 CPU cycle from Cpu::end_cycle (after handle_interrupts), so the irq-timing-trace tooling can record a CycleRecord for the R1 access path (which bypasses the LockstepBus tick_one_cpu_cycle push). Default no-op; the production bus overrides it only under the irq-timing-trace feature, so non-trace R1 builds compile this to an empty call.
§

fn cpu_cycle_phi1(&mut self)

φ1 (pre-access) half of one CPU cycle, for the C1 access-reorder axis attempt 17. Called BEFORE the bus access in Cpu::read1 / Cpu::write1 when the cpu-c1-attempt-17-access-reorder feature is enabled. Read more
§

fn cpu_cycle_phi2(&mut self)

φ2 (post-access) half of one CPU cycle. Called AFTER the bus access in Cpu::read1 / Cpu::write1 when the cpu-c1-attempt-17-access-reorder feature is enabled. Read more

Auto Trait Implementations§

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> Same for T

Source§

type Output = T

Should always be Self
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.