feat(machine): schedule display DMA IRQs and HALT
Integrate nominal bus timing, device clocks, display events, DMA transfers, keypad state, and interrupt delivery.
This commit is contained in:
parent
156f1eb839
commit
bb6784615a
10 changed files with 3172 additions and 0 deletions
132
src/display.rs
Normal file
132
src/display.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
//! GBA display clock and status, independent of pixel rendering.
|
||||
//! Events use supplied clock cycles. Device updates occur at instruction, DMA,
|
||||
//! or HALT-idle boundaries; display bus contention and IRQ synchronization delays
|
||||
//! are not modeled. Forced blank does not stop this clock.
|
||||
|
||||
pub const CPU_HZ: u32 = 16_777_216;
|
||||
pub const CYCLES_PER_LINE: u32 = 1232;
|
||||
pub const HBLANK_START: u32 = 1006;
|
||||
pub const VISIBLE_LINES: u32 = 160;
|
||||
pub const LINES_PER_FRAME: u32 = 228;
|
||||
pub const CYCLES_PER_FRAME: u32 = CYCLES_PER_LINE * LINES_PER_FRAME;
|
||||
pub const VBLANK_START: u32 = CYCLES_PER_LINE * VISIBLE_LINES;
|
||||
|
||||
/// Display position after the latest clock advance. Event counters wrap at u64::MAX.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DisplayPosition {
|
||||
pub scanline: u16,
|
||||
pub line_cycle: u16,
|
||||
/// Number of completed 228-line frames (increments on entry to line zero).
|
||||
pub frames: u64,
|
||||
/// Number of entries to line 160, independent of local interrupt enables.
|
||||
pub vblanks: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct Display {
|
||||
phase: u32,
|
||||
control: u16,
|
||||
frames: u64,
|
||||
vblanks: u64,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
pub(crate) fn position(&self) -> DisplayPosition {
|
||||
DisplayPosition {
|
||||
scanline: (self.phase / CYCLES_PER_LINE) as u16,
|
||||
line_cycle: (self.phase % CYCLES_PER_LINE) as u16,
|
||||
frames: self.frames,
|
||||
vblanks: self.vblanks,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn status(&self) -> u16 {
|
||||
let position = self.position();
|
||||
// Line 227 is in the non-visible region, but the VBlank flag is clear.
|
||||
let vblank = (160..227).contains(&position.scanline);
|
||||
let hblank = u32::from(position.line_cycle) >= HBLANK_START;
|
||||
let matched = position.scanline == self.control >> 8;
|
||||
self.control | u16::from(vblank) | (u16::from(hblank) << 1) | (u16::from(matched) << 2)
|
||||
}
|
||||
|
||||
/// Write a DISPSTAT byte. Status bits and unused GBA bits are read-only.
|
||||
/// Changing the comparator can generate a rising match event immediately.
|
||||
/// Enabling an IRQ while its status is already set does not synthesize an edge.
|
||||
pub(crate) fn write_status(&mut self, high: bool, value: u8) -> u16 {
|
||||
let previously_matched = self.status() & 4 != 0;
|
||||
let shift = u32::from(high) * 8;
|
||||
self.control = ((self.control & !(0xff << shift)) | (u16::from(value) << shift)) & 0xff38;
|
||||
if !previously_matched && self.status() & 4 != 0 && self.control & 0x20 != 0 {
|
||||
4
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Next HBlank entry or scanline start. Both are strictly in the future.
|
||||
pub(crate) fn next_event_cycles(&self) -> u32 {
|
||||
let cycle = self.phase % CYCLES_PER_LINE;
|
||||
if cycle < HBLANK_START {
|
||||
HBLANK_START - cycle
|
||||
} else {
|
||||
CYCLES_PER_LINE - cycle
|
||||
}
|
||||
}
|
||||
|
||||
/// Display DMA requests are independent of DISPSTAT interrupt enables.
|
||||
/// Unlike HBlank IRQs, HBlank DMA requests occur only on visible lines.
|
||||
pub(crate) fn dma_events(&self, cycles: u32) -> (bool, bool) {
|
||||
let vblank = crossings(self.phase, cycles, CYCLES_PER_FRAME, VBLANK_START) != 0;
|
||||
let visible_hblanks = |time: u64| {
|
||||
let frame = u64::from(CYCLES_PER_FRAME);
|
||||
let line = u64::from(CYCLES_PER_LINE);
|
||||
let visible = u64::from(VISIBLE_LINES);
|
||||
time / frame * visible
|
||||
+ ((time % frame + line - u64::from(HBLANK_START)) / line).min(visible)
|
||||
};
|
||||
let begin = u64::from(self.phase);
|
||||
let hblank = visible_hblanks(begin + u64::from(cycles)) != visible_hblanks(begin);
|
||||
(vblank, hblank)
|
||||
}
|
||||
|
||||
/// Return IRQ request bits crossed during the interval (old time, new time].
|
||||
/// Periodic arithmetic handles large batches without skipping events or
|
||||
/// looping per cycle/scanline. Status derives from the final clock position.
|
||||
pub(crate) fn advance(&mut self, cycles: u32) -> u16 {
|
||||
let vblanks = crossings(self.phase, cycles, CYCLES_PER_FRAME, VBLANK_START);
|
||||
let frames = crossings(self.phase, cycles, CYCLES_PER_FRAME, 0);
|
||||
let mut requests = 0;
|
||||
if self.control & 8 != 0 && vblanks != 0 {
|
||||
requests |= 1;
|
||||
}
|
||||
// HBlank status and IRQ events occur on all 228 lines, including VBlank.
|
||||
if self.control & 0x10 != 0
|
||||
&& crossings(self.phase, cycles, CYCLES_PER_LINE, HBLANK_START) != 0
|
||||
{
|
||||
requests |= 2;
|
||||
}
|
||||
let compare = u32::from(self.control >> 8);
|
||||
if self.control & 0x20 != 0
|
||||
&& compare < LINES_PER_FRAME
|
||||
&& crossings(
|
||||
self.phase,
|
||||
cycles,
|
||||
CYCLES_PER_FRAME,
|
||||
compare * CYCLES_PER_LINE,
|
||||
) != 0
|
||||
{
|
||||
requests |= 4;
|
||||
}
|
||||
self.phase =
|
||||
((u64::from(self.phase) + u64::from(cycles)) % u64::from(CYCLES_PER_FRAME)) as u32;
|
||||
self.vblanks = self.vblanks.wrapping_add(vblanks);
|
||||
self.frames = self.frames.wrapping_add(frames);
|
||||
requests
|
||||
}
|
||||
}
|
||||
|
||||
fn crossings(start: u32, elapsed: u32, period: u32, offset: u32) -> u64 {
|
||||
let shift = u64::from(period - offset);
|
||||
let begin = u64::from(start) + shift;
|
||||
(begin + u64::from(elapsed)) / u64::from(period) - begin / u64::from(period)
|
||||
}
|
||||
259
src/dma.rs
Normal file
259
src/dma.rs
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
//! Four-channel GBA direct memory access (DMA) controller.
|
||||
//! Transfers are scheduled one data unit at a time. Startup/resumption delays,
|
||||
//! open-bus latches, sound FIFO, video capture, and Game Pak DRQ are not modeled.
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use crate::{memory::MemoryError, timing::AccessWidth};
|
||||
|
||||
pub const DMA_BASE: u32 = 0x0400_00b0;
|
||||
pub const DMA_STRIDE: u32 = 12;
|
||||
pub(crate) const DMA_END: u32 = DMA_BASE + 4 * DMA_STRIDE - 1;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DmaError {
|
||||
Memory { channel: usize, error: MemoryError },
|
||||
UnsupportedControl { channel: usize, control: u16 },
|
||||
UnsupportedSource { channel: usize, address: u32 },
|
||||
RegisterDestination { channel: usize, address: u32 },
|
||||
PowerControlDestination { channel: usize, address: u32 },
|
||||
}
|
||||
|
||||
impl fmt::Display for DmaError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Memory { channel, error } => write!(f, "DMA{channel}: {error}"),
|
||||
Self::UnsupportedControl { channel, control } => {
|
||||
write!(f, "DMA{channel}: unsupported control {control:#06x}")
|
||||
}
|
||||
Self::UnsupportedSource { channel, address } => {
|
||||
write!(f, "DMA{channel}: unsupported source {address:#010x}")
|
||||
}
|
||||
Self::PowerControlDestination { channel, address } => {
|
||||
write!(
|
||||
f,
|
||||
"DMA{channel}: power-control writes are not supported ({address:#010x})"
|
||||
)
|
||||
}
|
||||
Self::RegisterDestination { channel, address } => {
|
||||
write!(
|
||||
f,
|
||||
"DMA{channel}: writes to DMA registers are not supported ({address:#010x})"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for DmaError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
Self::Memory { error, .. } => Some(error),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
struct Channel {
|
||||
source: u32,
|
||||
destination: u32,
|
||||
count: u16,
|
||||
control: u16,
|
||||
current_source: u32,
|
||||
current_destination: u32,
|
||||
remaining: u32,
|
||||
active: bool,
|
||||
first: bool,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
fn enabled(&self) -> bool {
|
||||
self.control & 0x8000 != 0
|
||||
}
|
||||
|
||||
fn start_timing(&self) -> u16 {
|
||||
(self.control >> 12) & 3
|
||||
}
|
||||
|
||||
fn unsupported(&self) -> bool {
|
||||
self.start_timing() == 3 || self.control & 0x800 != 0 || self.control & 0x180 == 0x180
|
||||
}
|
||||
|
||||
fn width(&self) -> AccessWidth {
|
||||
if self.control & 0x400 != 0 {
|
||||
AccessWidth::Word
|
||||
} else {
|
||||
AccessWidth::Halfword
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_count(&self, index: usize) -> u32 {
|
||||
if self.count != 0 {
|
||||
u32::from(self.count)
|
||||
} else if index == 3 {
|
||||
0x1_0000
|
||||
} else {
|
||||
0x4000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct Dma {
|
||||
channels: [Channel; 4],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct Transfer {
|
||||
pub channel: usize,
|
||||
pub source: u32,
|
||||
pub destination: u32,
|
||||
pub width: AccessWidth,
|
||||
pub first: bool,
|
||||
}
|
||||
|
||||
impl Dma {
|
||||
/// Address/count registers are write-only. Zero is a diagnostic placeholder,
|
||||
/// not hardware open-bus behavior. Only CNT_H has meaningful readback.
|
||||
pub(crate) fn read8(&self, address: u32) -> u8 {
|
||||
let offset = address - DMA_BASE;
|
||||
let channel = &self.channels[(offset / DMA_STRIDE) as usize];
|
||||
match offset % DMA_STRIDE {
|
||||
10 => channel.control as u8,
|
||||
11 => (channel.control >> 8) as u8,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write8(&mut self, address: u32, value: u8) {
|
||||
let offset = address - DMA_BASE;
|
||||
let index = (offset / DMA_STRIDE) as usize;
|
||||
let channel = &mut self.channels[index];
|
||||
let register_byte = offset % DMA_STRIDE;
|
||||
match register_byte {
|
||||
0..=3 => {
|
||||
channel.source = replace_byte(channel.source, register_byte, value)
|
||||
& if index == 0 { 0x07ff_fffe } else { 0x0fff_fffe };
|
||||
}
|
||||
4..=7 => {
|
||||
channel.destination = replace_byte(channel.destination, register_byte - 4, value)
|
||||
& if index == 3 { 0x0fff_fffe } else { 0x07ff_fffe };
|
||||
}
|
||||
8..=9 => {
|
||||
channel.count = replace_byte(u32::from(channel.count), register_byte - 8, value)
|
||||
as u16
|
||||
& if index == 3 { 0xffff } else { 0x3fff };
|
||||
}
|
||||
10..=11 => {
|
||||
let was_enabled = channel.enabled();
|
||||
channel.control =
|
||||
replace_byte(u32::from(channel.control), register_byte - 10, value) as u16
|
||||
& if index == 3 { 0xffe0 } else { 0xf7e0 };
|
||||
if !channel.enabled() {
|
||||
channel.active = false;
|
||||
} else if !was_enabled {
|
||||
let mask = !(channel.width().bytes() - 1);
|
||||
channel.current_source = channel.source & mask;
|
||||
channel.current_destination = channel.destination & mask;
|
||||
channel.remaining = channel.initial_count(index);
|
||||
channel.first = true;
|
||||
channel.active = channel.start_timing() == 0;
|
||||
}
|
||||
}
|
||||
_ => unreachable!("DMA register byte"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Blanking requests are edges, not queued jobs. An active channel ignores
|
||||
/// further edges until its current block completes.
|
||||
pub(crate) fn trigger(&mut self, vblank: bool, hblank: bool) {
|
||||
for channel in &mut self.channels {
|
||||
if channel.enabled()
|
||||
&& !channel.active
|
||||
&& ((vblank && channel.start_timing() == 1)
|
||||
|| (hblank && channel.start_timing() == 2))
|
||||
{
|
||||
channel.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the highest-priority ready channel without changing its state.
|
||||
/// Unsupported enabled modes report a diagnostic rather than silently waiting.
|
||||
pub(crate) fn next(&self) -> Result<Option<Transfer>, DmaError> {
|
||||
for (index, channel) in self.channels.iter().enumerate() {
|
||||
if !channel.enabled() {
|
||||
continue;
|
||||
}
|
||||
if channel.unsupported() {
|
||||
return Err(DmaError::UnsupportedControl {
|
||||
channel: index,
|
||||
control: channel.control,
|
||||
});
|
||||
}
|
||||
if channel.active {
|
||||
return Ok(Some(Transfer {
|
||||
channel: index,
|
||||
source: channel.current_source,
|
||||
destination: channel.current_destination,
|
||||
width: channel.width(),
|
||||
first: channel.first,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Commit one successful data unit after its bus cycles. Return completion IRQ bits.
|
||||
pub(crate) fn complete_unit(&mut self, index: usize) -> u16 {
|
||||
let channel = &mut self.channels[index];
|
||||
let width = channel.width().bytes();
|
||||
// The Game Pak source counter increments regardless of source control.
|
||||
let source_mode = if (0x08..=0x0d).contains(&(channel.current_source >> 24)) {
|
||||
0
|
||||
} else {
|
||||
(channel.control >> 7) & 3
|
||||
};
|
||||
channel.current_source = advance_address(channel.current_source, width, source_mode);
|
||||
channel.current_destination = advance_address(
|
||||
channel.current_destination,
|
||||
width,
|
||||
(channel.control >> 5) & 3,
|
||||
);
|
||||
channel.remaining -= 1;
|
||||
channel.first = false;
|
||||
if channel.remaining != 0 {
|
||||
return 0;
|
||||
}
|
||||
channel.active = false;
|
||||
if channel.control & 0x200 != 0 && channel.start_timing() != 0 {
|
||||
channel.remaining = channel.initial_count(index);
|
||||
channel.first = true;
|
||||
if channel.control & 0x60 == 0x60 {
|
||||
channel.current_destination = channel.destination & !(width - 1);
|
||||
}
|
||||
} else {
|
||||
channel.control &= !0x8000;
|
||||
}
|
||||
if channel.control & 0x4000 != 0 {
|
||||
1 << (8 + index)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_address(address: u32, width: u32, mode: u16) -> u32 {
|
||||
match mode {
|
||||
0 | 3 => address.wrapping_add(width),
|
||||
1 => address.wrapping_sub(width),
|
||||
2 => address,
|
||||
_ => unreachable!("two-bit address control"),
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_byte(previous: u32, byte: u32, value: u8) -> u32 {
|
||||
let shift = byte * 8;
|
||||
(previous & !(0xff << shift)) | (u32::from(value) << shift)
|
||||
}
|
||||
42
src/input.rs
Normal file
42
src/input.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
//! Platform-independent GBA button state. Bits are pressed-high here;
|
||||
//! the KEYINPUT register exposes their active-low hardware representation.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u16)]
|
||||
pub enum Button {
|
||||
A = 1 << 0,
|
||||
B = 1 << 1,
|
||||
Select = 1 << 2,
|
||||
Start = 1 << 3,
|
||||
Right = 1 << 4,
|
||||
Left = 1 << 5,
|
||||
Up = 1 << 6,
|
||||
Down = 1 << 7,
|
||||
R = 1 << 8,
|
||||
L = 1 << 9,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Buttons(u16);
|
||||
|
||||
impl Buttons {
|
||||
pub fn from_bits(bits: u16) -> Self {
|
||||
Self(bits & 0x03ff)
|
||||
}
|
||||
|
||||
pub fn bits(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn with(self, button: Button, pressed: bool) -> Self {
|
||||
Self(if pressed {
|
||||
self.0 | button as u16
|
||||
} else {
|
||||
self.0 & !(button as u16)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn keyinput(self) -> u16 {
|
||||
!self.0 & 0x03ff
|
||||
}
|
||||
}
|
||||
176
src/machine.rs
Normal file
176
src/machine.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
//! 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)
|
||||
}
|
||||
}
|
||||
126
src/timing.rs
Normal file
126
src/timing.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
//! Nominal GBA bus costs. These calculations do not perform memory accesses.
|
||||
//! Game Pak prefetch, display-bus contention, and configurable EWRAM timing are not modeled.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AccessWidth {
|
||||
Byte,
|
||||
Halfword,
|
||||
Word,
|
||||
}
|
||||
|
||||
impl AccessWidth {
|
||||
pub(crate) fn bytes(self) -> u32 {
|
||||
match self {
|
||||
Self::Byte => 1,
|
||||
Self::Halfword => 2,
|
||||
Self::Word => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AccessKind {
|
||||
NonSequential,
|
||||
#[default]
|
||||
Sequential,
|
||||
}
|
||||
|
||||
/// Timing breakdown for one instruction, exception entry, DMA unit, or HALT batch.
|
||||
/// DMA has zero code cycles and charges two internal cycles on its first unit.
|
||||
/// This is a nominal estimate, not a cycle-accurate bus trace.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct StepTiming {
|
||||
pub code_cycles: u32,
|
||||
pub data_cycles: u32,
|
||||
pub internal_cycles: u32,
|
||||
/// Device-clock cycles while the CPU is halted, without CPU or DMA bus work.
|
||||
pub idle_cycles: u32,
|
||||
}
|
||||
|
||||
impl StepTiming {
|
||||
pub fn total(self) -> u32 {
|
||||
self.code_cycles + self.data_cycles + self.internal_cycles + self.idle_cycles
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute an access cost from a WAITCNT snapshot. This does not validate the
|
||||
/// address against the memory map. Unmapped regions retain a one-cycle fallback
|
||||
/// so a branch can finish before the next instruction fetch reports a diagnostic.
|
||||
/// WAITCNT's prefetch and PHI bits are stored but do not affect this calculation.
|
||||
pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKind) -> u32 {
|
||||
match address >> 24 {
|
||||
0x02 => {
|
||||
if width == AccessWidth::Word {
|
||||
6
|
||||
} else {
|
||||
3
|
||||
}
|
||||
}
|
||||
0x05 | 0x06 => {
|
||||
if width == AccessWidth::Word {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
0x08..=0x0d => {
|
||||
let window = ((address >> 25) - 4) as usize;
|
||||
let first_shift = [2, 5, 8][window];
|
||||
let second_shift = [4, 7, 10][window];
|
||||
let first = 1 + [4, 3, 2, 8][usize::from((waitcnt >> first_shift) & 3)];
|
||||
let second = 1 + if waitcnt & (1 << second_shift) != 0 {
|
||||
1
|
||||
} else {
|
||||
[2, 4, 8][window]
|
||||
};
|
||||
// The Game Pak bus forces N timing at each 128 KiB boundary.
|
||||
let sequential = kind == AccessKind::Sequential && address & 0x1fffe != 0;
|
||||
let cycles = if sequential { second } else { first };
|
||||
// Each word is two halfwords. The second halfword always uses S timing.
|
||||
cycles
|
||||
+ if width == AccessWidth::Word {
|
||||
second
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
// BIOS, IWRAM, I/O, and OAM use a 32-bit bus. Other areas remain unmapped.
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn refill_cycles(waitcnt: u16, target: u32, width: AccessWidth) -> u32 {
|
||||
bus_cycles(waitcnt, target, width, AccessKind::NonSequential)
|
||||
+ bus_cycles(
|
||||
waitcnt,
|
||||
target.wrapping_add(width.bytes()),
|
||||
width,
|
||||
AccessKind::Sequential,
|
||||
)
|
||||
+ bus_cycles(
|
||||
waitcnt,
|
||||
target.wrapping_add(2 * width.bytes()),
|
||||
width,
|
||||
AccessKind::Sequential,
|
||||
)
|
||||
}
|
||||
|
||||
/// A data-only trace. Instruction fetches and branch refills are charged by the
|
||||
/// CPU timing summary, not here. Actual CPU bus calls supply addresses and widths.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub(crate) struct DataTiming {
|
||||
pub cycles: u32,
|
||||
next: Option<u32>,
|
||||
}
|
||||
|
||||
impl DataTiming {
|
||||
pub fn access(&mut self, waitcnt: u16, address: u32, width: AccessWidth) {
|
||||
let kind = if self.next == Some(address) {
|
||||
AccessKind::Sequential
|
||||
} else {
|
||||
AccessKind::NonSequential
|
||||
};
|
||||
self.cycles += bus_cycles(waitcnt, address, width, kind);
|
||||
self.next = Some(address.wrapping_add(width.bytes()));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue