refactor: split into gameboy-rust workspace with core, demos, and desktop crates
Move the emulator into crates/gba-core, the original demo programs into crates/gba-demos, and the minifb frontend into crates/desktop. The binary is now gameboy-rust. Imports change from gba_rust to gba_core/gba_demos; no behavior changes. gba-core has no dependencies. Its integration tests use gba-demos as a development dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a8944d1d5f
commit
f8a0676748
113 changed files with 304 additions and 282 deletions
176
src/machine.rs
176
src/machine.rs
|
|
@ -1,176 +0,0 @@
|
|||
//! 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,
|
||||
}
|
||||
|
||||
#[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),
|
||||
}
|
||||
|
||||
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::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(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns the CPU and memory bus. Use this instead of Cpu::step to advance device
|
||||
/// clocks, execute DMA, honor HALT, and deliver interrupts. No audio or STOP 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 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.
|
||||
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 {
|
||||
self.step().map_err(|error| match error {
|
||||
MachineError::Cpu(error) => FrameRunError::Cpu(error),
|
||||
MachineError::Dma(error) => FrameRunError::Dma(error),
|
||||
})?;
|
||||
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
|
||||
}
|
||||
|
||||
/// Service one ready DMA unit before sampling IRQ or executing 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.
|
||||
/// Wake-up ignores 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 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) {
|
||||
(StepKind::IrqEntry, self.cpu.exception_timing(&self.memory))
|
||||
} else {
|
||||
(
|
||||
StepKind::Instruction,
|
||||
self.cpu
|
||||
.step_timed(&mut self.memory)
|
||||
.map_err(MachineError::Cpu)?,
|
||||
)
|
||||
};
|
||||
self.memory.advance_cycles(timing.total());
|
||||
self.last_timing = timing;
|
||||
Ok(kind)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue