gameboy-rust/crates/gba-core/src/machine.rs
0xrsydn 46af481487 feat(memory): retain sequential Thumb IWRAM bus lanes
Track a separate IWRAM latch with known lanes and transactional updates. Sequential Thumb prefetches and IWRAM byte/halfword/word reads and writes drive only addressed lanes. Other regions and host inspection do not replace the latch. Failed instructions discard staged changes.

Keep cold, refill, discontinuous-entry, IRQ, and DMA-transition history conservative. Share Thumb refill classification with nominal timing, including PC writes to fallthrough. Successful DMA and accepted machine IRQs invalidate history; failed DMA preserves it. Document source evidence, bounded scope, and remaining hardware-validation limits.

Add original lane, alignment, mirror/wrap, signed/unaligned-load, mode, block-transfer, rollback, isolation, control-flow, IRQ/DMA, and nominal-cost regressions. Darwin arm64 validation: 905 workspace tests and 836 core/demo tests per debug/release profile; 39 preparation tests; native ROM windows in both profiles; ten graphics/window smoke modes; fmt, clippy, and rustdoc. Public ARM, Thumb, memory, and BIOS reports remain identical to prior passing reports and between profiles.
2026-10-06 01:44:21 +07:00

195 lines
6.5 KiB
Rust

//! CPU/device integration with nominal ARM7 instruction and GBA bus costs.
//! Device clocks update at instruction, DMA-unit, or HALT-idle boundaries, not per bus access.
use std::{error::Error, fmt};
use crate::{
cpu::{Cpu, CpuError},
dma::DmaError,
memory::Memory,
timing::StepTiming,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepKind {
Instruction,
IrqEntry,
Dma {
channel: usize,
},
/// Advance device clocks to the next event while the CPU stays paused.
HaltIdle,
/// System clock is off; no CPU, DMA, or device progress. Supply host input to wake.
StopIdle,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MachineError {
Cpu(CpuError),
Dma(DmaError),
}
impl fmt::Display for MachineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cpu(error) => write!(f, "{error}"),
Self::Dma(error) => write!(f, "{error}"),
}
}
}
impl Error for MachineError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Cpu(error) => Some(error),
Self::Dma(error) => Some(error),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FrameRunError {
Cpu(CpuError),
Dma(DmaError),
StepLimit(usize),
Stopped,
}
impl fmt::Display for FrameRunError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cpu(error) => write!(f, "{error}"),
Self::Dma(error) => write!(f, "{error}"),
Self::Stopped => write!(f, "machine is in STOP; no VBlank can occur until wake-up"),
Self::StepLimit(limit) => write!(f, "no VBlank entry within {limit} machine steps"),
}
}
}
impl Error for FrameRunError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Cpu(error) => Some(error),
Self::Dma(error) => Some(error),
Self::StepLimit(_) | Self::Stopped => None,
}
}
}
/// Owns the CPU and memory bus. Use this instead of Cpu::step to advance device
/// clocks, execute DMA, honor HALT/STOP, and deliver interrupts. No audio yet.
pub struct Machine {
cpu: Cpu,
memory: Memory,
last_timing: StepTiming,
}
impl Machine {
pub fn new(cpu: Cpu, memory: Memory) -> Self {
Self {
cpu,
memory,
last_timing: StepTiming::default(),
}
}
pub fn cpu(&self) -> &Cpu {
&self.cpu
}
pub fn memory(&self) -> &Memory {
&self.memory
}
/// Host setup and inspection. Bus writes do not automatically advance time.
pub fn memory_mut(&mut self) -> &mut Memory {
&mut self.memory
}
pub fn halted(&self) -> bool {
self.memory.halted()
}
pub fn stopped(&self) -> bool {
self.memory.stopped()
}
pub fn cycles(&self) -> u64 {
self.memory.cycles()
}
/// Execute through the next entry to scanline 160, even when already in VBlank.
/// Stops at the first instruction, DMA-unit, or HALT-idle boundary after the event.
/// DMA requested by that event may still be pending. Prior successful
/// steps remain committed on error or limit exhaustion. Zero steps always fails.
/// StopIdle returns FrameRunError::Stopped immediately; no VBlank can advance.
pub fn run_until_vblank(&mut self, max_steps: usize) -> Result<usize, FrameRunError> {
let previous = self.memory.display_position().vblanks;
for steps in 1..=max_steps {
let kind = self.step().map_err(|error| match error {
MachineError::Cpu(error) => FrameRunError::Cpu(error),
MachineError::Dma(error) => FrameRunError::Dma(error),
})?;
if kind == StepKind::StopIdle {
return Err(FrameRunError::Stopped);
}
if self.memory.display_position().vblanks != previous {
return Ok(steps);
}
}
Err(FrameRunError::StepLimit(max_steps))
}
/// Cost of the last successful step. Failed steps leave this unchanged.
pub fn last_timing(&self) -> StepTiming {
self.last_timing
}
/// STOP returns StopIdle with zero timing before DMA, IRQ, or CPU work.
/// Otherwise, service one ready DMA unit before sampling IRQ or CPU code.
/// Lower channel numbers take priority; higher-priority requests can preempt
/// between units. The CPU and its registers remain paused throughout DMA.
/// If HALT is still waiting, advance to the next display edge or timer overflow.
/// HALT wakes on IE & IF; STOP wakes on the enabled live keypad condition.
/// Both ignore IME and CPSR.I; IRQ delivery still respects both masks.
/// Sample IRQ before executing the next instruction. GBA devices do not
/// generate FIQ. IRQ entry consumes a separate step and does not clear IF.
/// CPU/DMA work uses nominal costs; HALT advances exactly to the next device event.
/// CPU writes take effect before this bulk device update. Within-instruction
/// register timing, prefetch, and IRQ synchronization delays are not modeled.
/// CPU/DMA diagnostics leave CPU, devices, and the clock unchanged for this step.
/// Row-capture diagnostics are deferred to Memory::present_frame instead.
pub fn step(&mut self) -> Result<StepKind, MachineError> {
if self.memory.stopped() {
self.last_timing = StepTiming::default();
return Ok(StepKind::StopIdle);
}
if let Some((channel, timing)) = self.memory.step_dma().map_err(MachineError::Dma)? {
self.last_timing = timing;
return Ok(StepKind::Dma { channel });
}
if self.memory.halted() {
let timing = StepTiming {
idle_cycles: self.memory.next_event_cycles(),
..StepTiming::default()
};
self.memory.advance_cycles(timing.total());
self.last_timing = timing;
return Ok(StepKind::HaltIdle);
}
let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) {
self.memory.invalidate_cpu_bus_history();
(StepKind::IrqEntry, self.cpu.exception_timing(&self.memory))
} else {
(
StepKind::Instruction,
self.cpu
.step_timed(&mut self.memory)
.map_err(MachineError::Cpu)?,
)
};
self.memory.complete_cpu_step(timing.total());
self.last_timing = timing;
Ok(kind)
}
}