pub struct Cpu {
pub a: u8,
pub x: u8,
pub y: u8,
pub pc: u16,
pub s: u8,
pub p: Status,
pub cycles: u64,
pub jammed: bool,
/* private fields */
}Expand description
6502 CPU core.
Fields§
§a: u8Accumulator.
x: u8X index register.
y: u8Y index register.
pc: u16Program counter.
s: u8Stack pointer (low byte; effective address 0x0100 | s).
p: StatusProcessor status.
cycles: u64Cumulative CPU cycle count.
jammed: booltrue when the CPU has executed a JAM/KIL/STP and is waiting for reset.
Implementations§
Source§impl Cpu
impl Cpu
Sourcepub const fn new() -> Self
pub const fn new() -> Self
New CPU in “post-reset” state. Caller must invoke Cpu::reset with a
real bus before stepping (PC is undefined until reset reads $FFFC/D).
This constructor is the convenience entry-point used by unit tests and
nestest fixtures that drive the CPU without going through a full
power-on path: S=$FD, P=$24 (UNUSED + INTERRUPT_DISABLE). If the
caller subsequently invokes Cpu::reset the stack pointer will be
decremented by 3 (per the reset sequence), landing on $FA — that is
the input shape several tests/opcodes.rs fixtures expect.
For the real cold-boot path (Nes::from_rom, Nes::power_cycle),
use Cpu::power_on instead, which seeds S=$00. After the 3-decrement
reset sequence that lands S=$FD, matching Mesen2’s power-up state.
See docs/audit/session-13-cpu-boot-fix-2026-05-21.md for the reference
behaviour from Core/NES/NesCpu.cpp::NesCpu::Reset(softReset=false).
Sourcepub const fn power_on() -> Self
pub const fn power_on() -> Self
New CPU in real-hardware cold-boot state (S=$00).
Real silicon comes up with the stack pointer in an undefined state;
the convention used by Mesen2 (and adopted here for trace parity) is
to treat power-up as S=$00 and rely on the reset sequence’s three
“phantom” decrements to wrap into S=$FD. See Mesen2
Core/NES/NesCpu.cpp::Reset(softReset=false):
if(softReset) {
_state.SP -= 0x03; // soft reset path
} else {
_state.SP = 0xFD; // power-up: direct assignment
}RustyNES models that two-path behaviour by gating the SP delta through
the constructor: Cpu::power_on() + reset() ⇒ $00 - 3 = $FD (cold);
cpu.reset() again ⇒ $FD - 3 = $FA (subsequent soft reset).
P is left at $24 (INTERRUPT_DISABLE | UNUSED). Mesen2’s trace
surface shows P = $04 because it masks UNUSED out of the displayed
byte, but the bit is conventionally always set on a 6502 internally
(nesdev: “Bit 5: Always 1, the so-called ‘unused’ bit”); the trace
divergence on P is cosmetic.
Sourcepub const fn master_clock(&self) -> u64
pub const fn master_clock(&self) -> u64
Read-only accessor for the CPU’s authoritative master clock (v2.0.0-beta.1 one-clock instrumentation).
master_clock counts master-clock units (NTSC: 12 per CPU cycle,
PAL: 16, Dendy: 15) and is advanced only by start_cycle /
end_cycle (the asymmetric read 5/7 vs write 7/5 φ1/φ2 split on
NTSC) plus the bus-side DMA coherence fold
(Bus::take_dma_mc_consumed). It is the counter the v2.0.0
“Timebase” rewrite (ADR 0002) promotes to the ONE canonical
timebase; the test harness asserts the affine relation
master_clock == seed + cpu_divider * cycles against the other
cycle counters (one_clock_invariants.rs) as the gate for the
beta.1 counter collapse.
Sourcepub fn reset<B: Bus>(&mut self, bus: &mut B)
pub fn reset<B: Bus>(&mut self, bus: &mut B)
Reset (warm boot).
Real hardware: 8-cycle sequence with suppressed pushes, then PC loads
from the reset vector and the I flag is set. We model the cycle count
(advances cycles by 8 and fires on_cpu_cycle 8 times) without
mutating registers other than P (set I), S (decrement by 3), and PC.
Matches Mesen2’s NesCpu::Reset() 8-cycle post-power-up loop (“CPU
takes 8 cycles before it starts executing the ROM’s code”). Combined
with the PPU power-up at (scanline=-1, dot=340) (see Ppu::new),
this closes the +344-dot PPU offset identified empirically in
Session-13 (docs/audit/session-13-cpu-boot-fix-2026-05-21.md).
Sourcepub const fn set_pc(&mut self, addr: u16)
pub const fn set_pc(&mut self, addr: u16)
Force PC to addr. Used by the nestest harness which enters at
$C000 rather than the reset vector.
Sourcepub fn step<B: Bus>(&mut self, bus: &mut B) -> u8
pub fn step<B: Bus>(&mut self, bus: &mut B) -> u8
Step one instruction (or service an interrupt). Returns the number of CPU cycles consumed.
On a JAM-state CPU this is a no-op returning 0.
§Interrupt timing model
Real 6502 hardware samples the NMI / IRQ lines at the second-to-last
cycle of every instruction and, if asserted there, queues the
interrupt to be serviced after the current instruction completes.
Our model dispatches all bus operations atomically before ticking
cycles, so a write that itself raises NMI (e.g. STA $2000 enabling
NMI while VBL is set) appears to the bus’s edge detector during the
FIRST cycle of the tally loop — earlier than hardware would observe
it. Hardware places the actual write at the last cycle of the
instruction, so the second-to-last sample point would NOT see the new
line state; only the NEXT instruction’s sample sees it. We model
that by introducing a one-instruction promotion delay: edges captured
at end-of-step land in pending_* and, after the following step,
promote to armed_* which is the gate that actually triggers
service. This passes 04-nmi_control test 11 (“Immediate occurence
should be after NEXT instruction”) without regressing the
instruction-count-insensitive tests like 02-vbl_set_time,
09-even_odd_frames, or any instr_test_v5 ROM (which never raise
NMI from within a single instruction).
Source§impl Cpu
impl Cpu
Sourcepub fn snapshot(&self) -> Vec<u8> ⓘ
pub fn snapshot(&self) -> Vec<u8> ⓘ
Encode the CPU’s mutable state into a versioned binary blob.
Format is little-endian, version-tagged at offset 0. See
CPU_SNAPSHOT_VERSION for the current schema number.
Sourcepub fn restore(&mut self, data: &[u8]) -> Result<(), CpuSnapshotError>
pub fn restore(&mut self, data: &[u8]) -> Result<(), CpuSnapshotError>
Decode a previously Cpu::snapshoted blob back into self.
§Errors
Returns CpuSnapshotError if the blob is the wrong length or
carries an unrecognized version.