201 lines
6.8 KiB
Rust
201 lines
6.8 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.
|
|
/// Timer data accesses use staged bus-completion phases. Other device writes
|
|
/// precede their bulk clock update. IRQ synchronization delays are not modeled. Opcode
|
|
/// prefetch follows ordered nominal CPU/DMA costs, not per-cycle arbitration.
|
|
/// 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_halt_cycles(timing.total());
|
|
self.last_timing = timing;
|
|
return Ok(StepKind::HaltIdle);
|
|
}
|
|
self.memory.begin_timer_step();
|
|
let result = if let Some(timing) = self.cpu.take_irq_timed(&mut self.memory) {
|
|
Ok((StepKind::IrqEntry, timing))
|
|
} else {
|
|
self.cpu
|
|
.step_timed(&mut self.memory)
|
|
.map(|timing| (StepKind::Instruction, timing))
|
|
.map_err(MachineError::Cpu)
|
|
};
|
|
let (kind, timing) = match result {
|
|
Ok(result) => result,
|
|
Err(error) => {
|
|
self.memory.discard_timer_step();
|
|
return Err(error);
|
|
}
|
|
};
|
|
self.memory.complete_cpu_step(timing.total());
|
|
self.last_timing = timing;
|
|
Ok(kind)
|
|
}
|
|
}
|