fix(timers): apply machine accesses at bus completion
This commit is contained in:
parent
8930ea93b4
commit
bfecc87e84
20 changed files with 765 additions and 137 deletions
|
|
@ -233,12 +233,13 @@ fn failed_alias_reads_preserve_register_banks_device_state_and_clock() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn alias_io_load_reads_before_device_progress_and_irq_delivery() {
|
fn alias_io_load_reads_at_data_completion_before_irq_delivery() {
|
||||||
let mut bus = memory(0xe5b0_0004); // LDR r0,[r0,#4]!
|
let mut bus = memory(0xe5b0_0004); // LDR r0,[r0,#4]!
|
||||||
bus.write32(TIMER_BASE, 0x00c0_fffa).unwrap();
|
bus.write32(TIMER_BASE, 0x00c0_fffa).unwrap();
|
||||||
bus.write16(IE, 8).unwrap();
|
bus.write16(IE, 8).unwrap();
|
||||||
bus.write16(IME, 1).unwrap();
|
bus.write16(IME, 1).unwrap();
|
||||||
let expected = bus.read32(TIMER_BASE).unwrap();
|
// Six source cycles overflow/reload the timer; the data cycle adds one tick.
|
||||||
|
let expected = 0x00c0_fffb;
|
||||||
let mut cpu = Cpu::new(ROM_START);
|
let mut cpu = Cpu::new(ROM_START);
|
||||||
cpu.registers[0] = TIMER_BASE - 4;
|
cpu.registers[0] = TIMER_BASE - 4;
|
||||||
let mut machine = Machine::new(cpu, bus);
|
let mut machine = Machine::new(cpu, bus);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
//! Timer phase resets on enable or clock-source changes. Hardware startup delays,
|
//! Timer phase resets on enable or clock-source changes. Hardware startup delays,
|
||||||
//! shared prescaler phase, audio events, and interrupt delivery delays are not modeled.
|
//! shared prescaler phase, audio events, and interrupt delivery delays are not modeled.
|
||||||
|
|
||||||
|
mod timer_step;
|
||||||
|
pub(crate) use timer_step::TimerStep;
|
||||||
|
use timer_step::TIMER_IRQ_MASK;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||||
dma::{Dma, DmaError, Transfer, DMA_BASE, DMA_END},
|
dma::{Dma, DmaError, Transfer, DMA_BASE, DMA_END},
|
||||||
|
|
@ -58,7 +62,7 @@ pub const HALTCNT: u32 = 0x0400_0301;
|
||||||
|
|
||||||
const IRQ_MASK: u16 = 0x3fff;
|
const IRQ_MASK: u16 = 0x3fff;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
struct Timer {
|
struct Timer {
|
||||||
reload: u16,
|
reload: u16,
|
||||||
counter: u16,
|
counter: u16,
|
||||||
|
|
@ -67,6 +71,23 @@ struct Timer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Timer {
|
impl Timer {
|
||||||
|
fn read8(&self, address: u32) -> u8 {
|
||||||
|
let value = if address & 2 == 0 {
|
||||||
|
self.counter
|
||||||
|
} else {
|
||||||
|
u16::from(self.control)
|
||||||
|
};
|
||||||
|
value.to_le_bytes()[(address & 1) as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write8(&mut self, address: u32, value: u8, index: usize) {
|
||||||
|
if address & 2 == 0 {
|
||||||
|
self.reload = replace_byte(self.reload, address, value);
|
||||||
|
} else if address & 1 == 0 {
|
||||||
|
self.set_control(value, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn enabled(&self) -> bool {
|
fn enabled(&self) -> bool {
|
||||||
self.control & 0x80 != 0
|
self.control & 0x80 != 0
|
||||||
}
|
}
|
||||||
|
|
@ -162,6 +183,9 @@ impl Io {
|
||||||
if (DMA_BASE..=DMA_END).contains(&address) {
|
if (DMA_BASE..=DMA_END).contains(&address) {
|
||||||
return Some(self.dma.read8(address));
|
return Some(self.dma.read8(address));
|
||||||
}
|
}
|
||||||
|
if (TIMER_BASE..=TIMER_BASE + 15).contains(&address) {
|
||||||
|
return Some(self.timers[((address - TIMER_BASE) / 4) as usize].read8(address));
|
||||||
|
}
|
||||||
let value = match address & !1 {
|
let value = match address & !1 {
|
||||||
DISPCNT => self.dispcnt,
|
DISPCNT => self.dispcnt,
|
||||||
GREENSWAP => u16::from(self.greenswap),
|
GREENSWAP => u16::from(self.greenswap),
|
||||||
|
|
@ -176,14 +200,6 @@ impl Io {
|
||||||
BLDALPHA => self.effects.alpha,
|
BLDALPHA => self.effects.alpha,
|
||||||
KEYINPUT => self.keypad.keyinput(),
|
KEYINPUT => self.keypad.keyinput(),
|
||||||
KEYCNT => self.keypad.control(),
|
KEYCNT => self.keypad.control(),
|
||||||
0x0400_0100..=0x0400_010e => {
|
|
||||||
let timer = &self.timers[((address - TIMER_BASE) / 4) as usize];
|
|
||||||
if address & 2 == 0 {
|
|
||||||
timer.counter
|
|
||||||
} else {
|
|
||||||
u16::from(timer.control)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IE => self.enable,
|
IE => self.enable,
|
||||||
IF => self.pending,
|
IF => self.pending,
|
||||||
WAITCNT => self.waitcnt,
|
WAITCNT => self.waitcnt,
|
||||||
|
|
@ -258,13 +274,7 @@ impl Io {
|
||||||
}
|
}
|
||||||
0x0400_0100..=0x0400_010e => {
|
0x0400_0100..=0x0400_010e => {
|
||||||
let index = ((address - TIMER_BASE) / 4) as usize;
|
let index = ((address - TIMER_BASE) / 4) as usize;
|
||||||
let timer = &mut self.timers[index];
|
self.timers[index].write8(address, value, index);
|
||||||
if address & 2 == 0 {
|
|
||||||
// Merge into the reload latch, never the visible counter.
|
|
||||||
timer.reload = replace_byte(timer.reload, address, value);
|
|
||||||
} else if address & 1 == 0 {
|
|
||||||
timer.set_control(value, index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
IE => self.enable = replace_byte(self.enable, address, value) & IRQ_MASK,
|
IE => self.enable = replace_byte(self.enable, address, value) & IRQ_MASK,
|
||||||
WAITCNT => self.waitcnt = replace_byte(self.waitcnt, address, value) & 0x5fff,
|
WAITCNT => self.waitcnt = replace_byte(self.waitcnt, address, value) & 0x5fff,
|
||||||
|
|
@ -388,7 +398,29 @@ impl Io {
|
||||||
self.wake_if_requested();
|
self.wake_if_requested();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn advance(&mut self, cycles: u32) {
|
pub(crate) fn timer_step(&self) -> TimerStep {
|
||||||
|
TimerStep::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn commit_timer_step(&mut self, step: TimerStep) {
|
||||||
|
self.timers = step.timers;
|
||||||
|
self.pending = (self.pending & !TIMER_IRQ_MASK) | step.pending;
|
||||||
|
self.wake_if_requested();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_timer_bank(timers: &mut [Timer; 4], cycles: u32) -> u16 {
|
||||||
|
let mut pending = 0;
|
||||||
|
let mut overflows = 0;
|
||||||
|
for (index, timer) in timers.iter_mut().enumerate() {
|
||||||
|
overflows = timer.advance(cycles, overflows);
|
||||||
|
if overflows != 0 && timer.control & 0x40 != 0 {
|
||||||
|
pending |= 1 << (3 + index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pending
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn advance(&mut self, cycles: u32, advance_timers: bool) {
|
||||||
// Hardware state tracks time even when host-side frame capture is disabled.
|
// Hardware state tracks time even when host-side frame capture is disabled.
|
||||||
let position = self.display.position();
|
let position = self.display.position();
|
||||||
let phase = u32::from(position.scanline) * CYCLES_PER_LINE + u32::from(position.line_cycle);
|
let phase = u32::from(position.scanline) * CYCLES_PER_LINE + u32::from(position.line_cycle);
|
||||||
|
|
@ -420,13 +452,8 @@ impl Io {
|
||||||
let (vblank, hblank) = self.display.dma_events(cycles);
|
let (vblank, hblank) = self.display.dma_events(cycles);
|
||||||
self.dma.trigger(vblank, hblank);
|
self.dma.trigger(vblank, hblank);
|
||||||
self.pending |= self.display.advance(cycles);
|
self.pending |= self.display.advance(cycles);
|
||||||
let mut overflows = 0;
|
if advance_timers {
|
||||||
for (index, timer) in self.timers.iter_mut().enumerate() {
|
self.pending |= Self::advance_timer_bank(&mut self.timers, cycles);
|
||||||
overflows = timer.advance(cycles, overflows);
|
|
||||||
if overflows != 0 && timer.control & 0x40 != 0 {
|
|
||||||
// Pending flags latch even if IE, IME, or CPSR.I blocks delivery.
|
|
||||||
self.pending |= 1 << (3 + index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.wake_if_requested();
|
self.wake_if_requested();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
crates/gba-core/src/io/timer_step.rs
Normal file
55
crates/gba-core/src/io/timer_step.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
//! Staged timer clocks for one machine instruction, IRQ entry, or DMA unit.
|
||||||
|
//! Other devices remain on the instruction-boundary scheduler.
|
||||||
|
use super::{Io, Timer, IF, TIMER_BASE};
|
||||||
|
|
||||||
|
pub(super) const TIMER_IRQ_MASK: u16 = 0x78;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) struct TimerStep {
|
||||||
|
pub(super) timers: [Timer; 4],
|
||||||
|
pub(super) pending: u16,
|
||||||
|
elapsed: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimerStep {
|
||||||
|
pub(super) fn new(io: &Io) -> Self {
|
||||||
|
Self {
|
||||||
|
timers: io.timers,
|
||||||
|
pending: io.pending & TIMER_IRQ_MASK,
|
||||||
|
elapsed: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn advance_to(&mut self, elapsed: u32) {
|
||||||
|
assert!(elapsed >= self.elapsed, "timer step time must be monotonic");
|
||||||
|
self.pending |= Io::advance_timer_bank(&mut self.timers, elapsed - self.elapsed);
|
||||||
|
self.elapsed = elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All lanes of a bus access observe the same staged time.
|
||||||
|
pub(crate) fn read8(&self, address: u32, committed: u8) -> Option<u8> {
|
||||||
|
match address {
|
||||||
|
TIMER_BASE..=0x0400_010f => {
|
||||||
|
Some(self.timers[((address - TIMER_BASE) / 4) as usize].read8(address))
|
||||||
|
}
|
||||||
|
IF => Some((committed & !(TIMER_IRQ_MASK as u8)) | self.pending as u8),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return true only when the staged state owns the entire register byte.
|
||||||
|
pub(crate) fn write8(&mut self, address: u32, value: u8) -> bool {
|
||||||
|
match address {
|
||||||
|
TIMER_BASE..=0x0400_010f => {
|
||||||
|
let index = ((address - TIMER_BASE) / 4) as usize;
|
||||||
|
self.timers[index].write8(address, value, index);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
IF => {
|
||||||
|
self.pending &= !u16::from(value);
|
||||||
|
false // The normal I/O path must also acknowledge non-timer sources.
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -155,8 +155,8 @@ impl Machine {
|
||||||
/// Sample IRQ before executing the next instruction. GBA devices do not
|
/// Sample IRQ before executing the next instruction. GBA devices do not
|
||||||
/// generate FIQ. IRQ entry consumes a separate step and does not clear IF.
|
/// 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/DMA work uses nominal costs; HALT advances exactly to the next device event.
|
||||||
/// CPU writes take effect before this bulk device update. Within-instruction
|
/// Timer data accesses use staged bus-completion phases. Other device writes
|
||||||
/// register timing and IRQ synchronization delays are not modeled. Opcode
|
/// precede their bulk clock update. IRQ synchronization delays are not modeled. Opcode
|
||||||
/// prefetch follows ordered nominal CPU/DMA costs, not per-cycle arbitration.
|
/// prefetch follows ordered nominal CPU/DMA costs, not per-cycle arbitration.
|
||||||
/// CPU/DMA diagnostics leave CPU, devices, and the clock unchanged for this step.
|
/// CPU/DMA diagnostics leave CPU, devices, and the clock unchanged for this step.
|
||||||
/// Row-capture diagnostics are deferred to Memory::present_frame instead.
|
/// Row-capture diagnostics are deferred to Memory::present_frame instead.
|
||||||
|
|
@ -178,15 +178,21 @@ impl Machine {
|
||||||
self.last_timing = timing;
|
self.last_timing = timing;
|
||||||
return Ok(StepKind::HaltIdle);
|
return Ok(StepKind::HaltIdle);
|
||||||
}
|
}
|
||||||
let (kind, timing) = if let Some(timing) = self.cpu.take_irq_timed(&mut self.memory) {
|
self.memory.begin_timer_step();
|
||||||
(StepKind::IrqEntry, timing)
|
let result = if let Some(timing) = self.cpu.take_irq_timed(&mut self.memory) {
|
||||||
|
Ok((StepKind::IrqEntry, timing))
|
||||||
} else {
|
} else {
|
||||||
(
|
|
||||||
StepKind::Instruction,
|
|
||||||
self.cpu
|
self.cpu
|
||||||
.step_timed(&mut self.memory)
|
.step_timed(&mut self.memory)
|
||||||
.map_err(MachineError::Cpu)?,
|
.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.memory.complete_cpu_step(timing.total());
|
||||||
self.last_timing = timing;
|
self.last_timing = timing;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ mod iwram_history_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod prefetch_tests;
|
mod prefetch_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod timer_step_tests;
|
||||||
|
#[cfg(test)]
|
||||||
mod timing_event_tests;
|
mod timing_event_tests;
|
||||||
pub(crate) use fetch::InstructionFetch;
|
pub(crate) use fetch::InstructionFetch;
|
||||||
use iwram_bus::IwramBus;
|
use iwram_bus::IwramBus;
|
||||||
|
|
@ -20,7 +22,7 @@ use crate::{
|
||||||
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||||
dma::{DmaError, DMA_BASE, DMA_END},
|
dma::{DmaError, DMA_BASE, DMA_END},
|
||||||
input::Buttons,
|
input::Buttons,
|
||||||
io::{Io, HALTCNT, KEYINPUT, POSTFLG},
|
io::{Io, TimerStep, HALTCNT, KEYINPUT, POSTFLG},
|
||||||
timing::{AccessKind, AccessWidth, CpuTiming, Prefetch, StepTiming},
|
timing::{AccessKind, AccessWidth, CpuTiming, Prefetch, StepTiming},
|
||||||
video::{self, sprites::pipeline, Framebuffer, VideoError},
|
video::{self, sprites::pipeline, Framebuffer, VideoError},
|
||||||
};
|
};
|
||||||
|
|
@ -82,7 +84,7 @@ struct CpuAccess {
|
||||||
/// Successful DMA accesses drive local lanes even before the first CPU instruction.
|
/// Successful DMA accesses drive local lanes even before the first CPU instruction.
|
||||||
/// Channel data remains separate; general and sub-instruction CPU bus handoff is unmodeled.
|
/// Channel data remains separate; general and sub-instruction CPU bus handoff is unmodeled.
|
||||||
/// Instruction buffering and supported bus history consume shared fetch samples.
|
/// Instruction buffering and supported bus history consume shared fetch samples.
|
||||||
/// Complete bus ownership and per-access timing remain unmodeled.
|
/// Timer data accesses use staged machine-step time; other per-access device timing remains unmodeled.
|
||||||
pub struct Memory {
|
pub struct Memory {
|
||||||
external_ram: Vec<u8>,
|
external_ram: Vec<u8>,
|
||||||
internal_ram: Vec<u8>,
|
internal_ram: Vec<u8>,
|
||||||
|
|
@ -94,6 +96,7 @@ pub struct Memory {
|
||||||
io: Io,
|
io: Io,
|
||||||
cycles: u64,
|
cycles: u64,
|
||||||
cpu_timing: Cell<Option<CpuTiming>>,
|
cpu_timing: Cell<Option<CpuTiming>>,
|
||||||
|
timer_step: Cell<Option<TimerStep>>,
|
||||||
gamepak_prefetch: Prefetch,
|
gamepak_prefetch: Prefetch,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
last_cpu_timing: Option<CpuTiming>,
|
last_cpu_timing: Option<CpuTiming>,
|
||||||
|
|
@ -122,6 +125,7 @@ impl Memory {
|
||||||
io: Io::default(),
|
io: Io::default(),
|
||||||
cycles: 0,
|
cycles: 0,
|
||||||
cpu_timing: Cell::new(None),
|
cpu_timing: Cell::new(None),
|
||||||
|
timer_step: Cell::new(None),
|
||||||
gamepak_prefetch: Prefetch::default(),
|
gamepak_prefetch: Prefetch::default(),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
last_cpu_timing: None,
|
last_cpu_timing: None,
|
||||||
|
|
@ -167,10 +171,41 @@ impl Memory {
|
||||||
pub(crate) fn complete_cpu_step(&mut self, cycles: u32) {
|
pub(crate) fn complete_cpu_step(&mut self, cycles: u32) {
|
||||||
// IRQ entry consumes the resume too, without executing an instruction.
|
// IRQ entry consumes the resume too, without executing an instruction.
|
||||||
self.cpu_resume_nonsequential = false;
|
self.cpu_resume_nonsequential = false;
|
||||||
self.advance_running_cycles(cycles);
|
self.complete_timer_step(cycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn begin_timer_step(&self) {
|
||||||
|
debug_assert!(self.timer_step.get().is_none());
|
||||||
|
self.timer_step.set(Some(self.io.timer_step()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn discard_timer_step(&self) {
|
||||||
|
self.timer_step.set(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_timer_step(&self, cycles: u32) {
|
||||||
|
if let Some(mut step) = self.timer_step.get() {
|
||||||
|
step.advance_to(cycles);
|
||||||
|
self.timer_step.set(Some(step));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_timer_step(&mut self, cycles: u32) {
|
||||||
|
self.advance_timer_step(cycles);
|
||||||
|
let step = self
|
||||||
|
.timer_step
|
||||||
|
.take()
|
||||||
|
.expect("machine timer transaction is active");
|
||||||
|
self.io.commit_timer_step(step);
|
||||||
|
// Timers already reached this boundary. Advance other devices exactly once.
|
||||||
|
self.advance_running_cycles_with_timers(cycles, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn advance_running_cycles(&mut self, cycles: u32) {
|
fn advance_running_cycles(&mut self, cycles: u32) {
|
||||||
|
self.advance_running_cycles_with_timers(cycles, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_running_cycles_with_timers(&mut self, cycles: u32, advance_timers: bool) {
|
||||||
if cycles != 0 && !self.sprite_pipeline.started {
|
if cycles != 0 && !self.sprite_pipeline.started {
|
||||||
// Synthetic reset begins on row0, with no preceding row227 to prepare it.
|
// Synthetic reset begins on row0, with no preceding row227 to prepare it.
|
||||||
self.prepare_sprite_row(0);
|
self.prepare_sprite_row(0);
|
||||||
|
|
@ -182,14 +217,14 @@ impl Memory {
|
||||||
u32::from(position.scanline) * CYCLES_PER_LINE + u32::from(position.line_cycle);
|
u32::from(position.scanline) * CYCLES_PER_LINE + u32::from(position.line_cycle);
|
||||||
let mut advanced = 0;
|
let mut advanced = 0;
|
||||||
for (offset, row) in pipeline::latest_events(phase, cycles).into_iter().flatten() {
|
for (offset, row) in pipeline::latest_events(phase, cycles).into_iter().flatten() {
|
||||||
self.io.advance(offset - advanced);
|
self.io.advance(offset - advanced, advance_timers);
|
||||||
self.prepare_sprite_row(row);
|
self.prepare_sprite_row(row);
|
||||||
advanced = offset;
|
advanced = offset;
|
||||||
}
|
}
|
||||||
self.io.advance(cycles - advanced);
|
self.io.advance(cycles - advanced, advance_timers);
|
||||||
} else {
|
} else {
|
||||||
// Split at display and sprite-preparation edges. CPU/DMA writes have
|
// Split at display and sprite-preparation edges. Video writes have
|
||||||
// already committed before these nominal clocks; no per-access timing.
|
// already committed. Machine timer transactions skip this bulk timer clock.
|
||||||
let mut remaining = cycles;
|
let mut remaining = cycles;
|
||||||
while remaining != 0 {
|
while remaining != 0 {
|
||||||
let position = self.io.display_position();
|
let position = self.io.display_position();
|
||||||
|
|
@ -202,7 +237,7 @@ impl Memory {
|
||||||
let until =
|
let until =
|
||||||
display_edge.min(pipeline::next_event(position.scanline, position.line_cycle));
|
display_edge.min(pipeline::next_event(position.scanline, position.line_cycle));
|
||||||
let step = remaining.min(until);
|
let step = remaining.min(until);
|
||||||
self.io.advance(step);
|
self.io.advance(step, advance_timers);
|
||||||
remaining -= step;
|
remaining -= step;
|
||||||
if step == until {
|
if step == until {
|
||||||
let position = self.io.display_position();
|
let position = self.io.display_position();
|
||||||
|
|
@ -316,28 +351,6 @@ impl Memory {
|
||||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||||
}
|
}
|
||||||
.map_err(map_error)?;
|
.map_err(map_error)?;
|
||||||
let data_latch = if transfer.source < 0x0200_0000 {
|
|
||||||
// DMA cannot read BIOS or the lower unused region. Retain this channel's
|
|
||||||
// full word; never read firmware bytes or borrow CPU/another channel's data.
|
|
||||||
transfer
|
|
||||||
.data_latch
|
|
||||||
.expect("unknown blocked source rejected above")
|
|
||||||
} else {
|
|
||||||
match transfer.width {
|
|
||||||
AccessWidth::Halfword => {
|
|
||||||
u32::from(self.read16(transfer.source).map_err(map_error)?) * 0x0001_0001
|
|
||||||
}
|
|
||||||
AccessWidth::Word => self.read32(transfer.source).map_err(map_error)?,
|
|
||||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// A blocked halfword read keeps both old lanes. The destination selects
|
|
||||||
// one lane; mapped halfword reads have already duplicated their source data.
|
|
||||||
let value = match transfer.width {
|
|
||||||
AccessWidth::Halfword => data_latch >> ((transfer.destination & 2) * 8),
|
|
||||||
AccessWidth::Word => data_latch,
|
|
||||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
|
||||||
};
|
|
||||||
let kind = if transfer.first {
|
let kind = if transfer.first {
|
||||||
AccessKind::NonSequential
|
AccessKind::NonSequential
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -348,19 +361,50 @@ impl Memory {
|
||||||
let internal_cycles = if transfer.first { 2 } else { 0 };
|
let internal_cycles = if transfer.first { 2 } else { 0 };
|
||||||
// Keep the existing whole-unit DMA startup model; no sub-instruction arbitration.
|
// Keep the existing whole-unit DMA startup model; no sub-instruction arbitration.
|
||||||
prefetch.advance(internal_cycles);
|
prefetch.advance(internal_cycles);
|
||||||
|
let source_cycles = prefetch.data(transfer.source, transfer.width, kind);
|
||||||
|
let destination_cycles = prefetch.data(transfer.destination, transfer.width, kind);
|
||||||
let timing = StepTiming {
|
let timing = StepTiming {
|
||||||
code_cycles: 0,
|
code_cycles: 0,
|
||||||
data_cycles: prefetch.data(transfer.source, transfer.width, kind)
|
data_cycles: source_cycles + destination_cycles,
|
||||||
+ prefetch.data(transfer.destination, transfer.width, kind),
|
|
||||||
internal_cycles,
|
internal_cycles,
|
||||||
idle_cycles: 0,
|
idle_cycles: 0,
|
||||||
};
|
};
|
||||||
|
self.begin_timer_step();
|
||||||
|
let result = (|| {
|
||||||
|
self.advance_timer_step(internal_cycles + source_cycles);
|
||||||
|
let data_latch = if transfer.source < 0x0200_0000 {
|
||||||
|
// Blocked reads retain channel data, not firmware or another channel's value.
|
||||||
|
transfer
|
||||||
|
.data_latch
|
||||||
|
.expect("unknown blocked source rejected above")
|
||||||
|
} else {
|
||||||
|
match transfer.width {
|
||||||
|
AccessWidth::Halfword => u32::from(self.read16(transfer.source)?) * 0x0001_0001,
|
||||||
|
AccessWidth::Word => self.read32(transfer.source)?,
|
||||||
|
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Blocked halfword reads retain both lanes; the destination selects a lane.
|
||||||
|
let value = match transfer.width {
|
||||||
|
AccessWidth::Halfword => data_latch >> ((transfer.destination & 2) * 8),
|
||||||
|
AccessWidth::Word => data_latch,
|
||||||
|
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||||
|
};
|
||||||
|
self.advance_timer_step(timing.total());
|
||||||
match transfer.width {
|
match transfer.width {
|
||||||
AccessWidth::Halfword => self.write16(transfer.destination, value as u16),
|
AccessWidth::Halfword => self.write16(transfer.destination, value as u16),
|
||||||
AccessWidth::Word => self.write32(transfer.destination, value),
|
AccessWidth::Word => self.write32(transfer.destination, value),
|
||||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||||
|
}?;
|
||||||
|
Ok::<_, MemoryError>((data_latch, value))
|
||||||
|
})();
|
||||||
|
let (data_latch, value) = match result {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(error) => {
|
||||||
|
self.discard_timer_step();
|
||||||
|
return Err(map_error(error));
|
||||||
}
|
}
|
||||||
.map_err(map_error)?;
|
};
|
||||||
// Only a successful unit commits queue progress. A WAITCNT destination can reset it.
|
// Only a successful unit commits queue progress. A WAITCNT destination can reset it.
|
||||||
prefetch.configure(self.waitcnt());
|
prefetch.configure(self.waitcnt());
|
||||||
self.gamepak_prefetch = prefetch;
|
self.gamepak_prefetch = prefetch;
|
||||||
|
|
@ -373,7 +417,7 @@ impl Memory {
|
||||||
.dma_access(transfer.destination, transfer.width, value);
|
.dma_access(transfer.destination, transfer.width, value);
|
||||||
// Keep this channel active during its cycles: another edge must not
|
// Keep this channel active during its cycles: another edge must not
|
||||||
// queue a second block while the current block is still transferring.
|
// queue a second block while the current block is still transferring.
|
||||||
self.advance_cycles(timing.total());
|
self.complete_timer_step(timing.total());
|
||||||
self.io.complete_dma_unit(channel, data_latch);
|
self.io.complete_dma_unit(channel, data_latch);
|
||||||
self.cpu_resume_nonsequential = true;
|
self.cpu_resume_nonsequential = true;
|
||||||
Ok(Some((channel, timing)))
|
Ok(Some((channel, timing)))
|
||||||
|
|
@ -566,6 +610,7 @@ impl Memory {
|
||||||
if let Some(mut trace) = self.cpu_timing.get() {
|
if let Some(mut trace) = self.cpu_timing.get() {
|
||||||
trace.code(address, width, kind);
|
trace.code(address, width, kind);
|
||||||
self.cpu_timing.set(Some(trace));
|
self.cpu_timing.set(Some(trace));
|
||||||
|
self.advance_timer_step(trace.total.total());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -576,6 +621,7 @@ impl Memory {
|
||||||
if let Some(mut trace) = self.cpu_timing.get() {
|
if let Some(mut trace) = self.cpu_timing.get() {
|
||||||
trace.internal(cycles);
|
trace.internal(cycles);
|
||||||
self.cpu_timing.set(Some(trace));
|
self.cpu_timing.set(Some(trace));
|
||||||
|
self.advance_timer_step(trace.total.total());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -583,12 +629,13 @@ impl Memory {
|
||||||
if let Some(mut trace) = self.cpu_timing.get() {
|
if let Some(mut trace) = self.cpu_timing.get() {
|
||||||
trace.data(self.waitcnt(), address, width);
|
trace.data(self.waitcnt(), address, width);
|
||||||
self.cpu_timing.set(Some(trace));
|
self.cpu_timing.set(Some(trace));
|
||||||
|
self.advance_timer_step(trace.total.total());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read8(&self, address: u32) -> Result<u8, MemoryError> {
|
pub fn read8(&self, address: u32) -> Result<u8, MemoryError> {
|
||||||
let value = self.read_byte(address)?;
|
|
||||||
self.record_access(address, AccessWidth::Byte);
|
self.record_access(address, AccessWidth::Byte);
|
||||||
|
let value = self.read_byte(address)?;
|
||||||
self.iwram_bus
|
self.iwram_bus
|
||||||
.access(address, AccessWidth::Byte, u32::from(value));
|
.access(address, AccessWidth::Byte, u32::from(value));
|
||||||
Ok(value)
|
Ok(value)
|
||||||
|
|
@ -596,6 +643,13 @@ impl Memory {
|
||||||
|
|
||||||
// Multi-byte reads use this helper to avoid charging each byte as a bus access.
|
// Multi-byte reads use this helper to avoid charging each byte as a bus access.
|
||||||
fn read_byte(&self, address: u32) -> Result<u8, MemoryError> {
|
fn read_byte(&self, address: u32) -> Result<u8, MemoryError> {
|
||||||
|
if address >> 24 == 0x04 {
|
||||||
|
if let Some(step) = self.timer_step.get() {
|
||||||
|
if let Some(value) = step.read8(address, self.io.read8(address).unwrap_or(0)) {
|
||||||
|
return Ok(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if address < BIOS_SIZE as u32
|
if address < BIOS_SIZE as u32
|
||||||
&& self.bios.is_some()
|
&& self.bios.is_some()
|
||||||
&& self
|
&& self
|
||||||
|
|
@ -694,6 +748,13 @@ impl Memory {
|
||||||
if matches!(byte_address, POSTFLG | HALTCNT) && !self.can_write_power_control() {
|
if matches!(byte_address, POSTFLG | HALTCNT) && !self.can_write_power_control() {
|
||||||
continue; // BIOS-only CPU writes; ignored elsewhere, including STOP requests.
|
continue; // BIOS-only CPU writes; ignored elsewhere, including STOP requests.
|
||||||
}
|
}
|
||||||
|
if let Some(mut step) = self.timer_step.get() {
|
||||||
|
let handled = step.write8(byte_address, byte);
|
||||||
|
self.timer_step.set(Some(step));
|
||||||
|
if handled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
self.io.write8(byte_address, byte);
|
self.io.write8(byte_address, byte);
|
||||||
}
|
}
|
||||||
if address & !3 == crate::io::WAITCNT {
|
if address & !3 == crate::io::WAITCNT {
|
||||||
|
|
@ -786,8 +847,8 @@ impl Memory {
|
||||||
if address & 1 != 0 {
|
if address & 1 != 0 {
|
||||||
return Err(MemoryError::Unaligned(address));
|
return Err(MemoryError::Unaligned(address));
|
||||||
}
|
}
|
||||||
let value = u16::from_le_bytes([self.read_byte(address)?, self.read_byte(address + 1)?]);
|
|
||||||
self.record_access(address, AccessWidth::Halfword);
|
self.record_access(address, AccessWidth::Halfword);
|
||||||
|
let value = u16::from_le_bytes([self.read_byte(address)?, self.read_byte(address + 1)?]);
|
||||||
self.iwram_bus
|
self.iwram_bus
|
||||||
.access(address, AccessWidth::Halfword, u32::from(value));
|
.access(address, AccessWidth::Halfword, u32::from(value));
|
||||||
Ok(value)
|
Ok(value)
|
||||||
|
|
@ -798,13 +859,13 @@ impl Memory {
|
||||||
if address & 3 != 0 {
|
if address & 3 != 0 {
|
||||||
return Err(MemoryError::Unaligned(address));
|
return Err(MemoryError::Unaligned(address));
|
||||||
}
|
}
|
||||||
|
self.record_access(address, AccessWidth::Word);
|
||||||
let value = u32::from_le_bytes([
|
let value = u32::from_le_bytes([
|
||||||
self.read_byte(address)?,
|
self.read_byte(address)?,
|
||||||
self.read_byte(address + 1)?,
|
self.read_byte(address + 1)?,
|
||||||
self.read_byte(address + 2)?,
|
self.read_byte(address + 2)?,
|
||||||
self.read_byte(address + 3)?,
|
self.read_byte(address + 3)?,
|
||||||
]);
|
]);
|
||||||
self.record_access(address, AccessWidth::Word);
|
|
||||||
self.iwram_bus.access(address, AccessWidth::Word, value);
|
self.iwram_bus.access(address, AccessWidth::Word, value);
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
346
crates/gba-core/src/memory/timer_step_tests.rs
Normal file
346
crates/gba-core/src/memory/timer_step_tests.rs
Normal file
|
|
@ -0,0 +1,346 @@
|
||||||
|
//! Original checks of timer transaction ownership and bus-completion sampling.
|
||||||
|
use super::{timing_event_tests::prepared, *};
|
||||||
|
use crate::{
|
||||||
|
cpu::Cpu,
|
||||||
|
display::HBLANK_START,
|
||||||
|
dma::DMA_STRIDE,
|
||||||
|
io::{DISPSTAT, HALTCNT, IE, IF, IME, TIMER_BASE, WAITCNT},
|
||||||
|
machine::{Machine, StepKind},
|
||||||
|
};
|
||||||
|
|
||||||
|
const NOP: u32 = 0xe1a0_0000;
|
||||||
|
const RAM: u32 = 0x0300_1000;
|
||||||
|
|
||||||
|
fn start(memory: &mut Memory, index: u32, reload: u16, control: u16) {
|
||||||
|
memory
|
||||||
|
.write32(
|
||||||
|
TIMER_BASE + index * 4,
|
||||||
|
u32::from(reload) | u32::from(control) << 16,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dma(memory: &mut Memory, source: u32, destination: u32, count: u16) {
|
||||||
|
let base = DMA_BASE + 3 * DMA_STRIDE;
|
||||||
|
memory.write32(base, source).unwrap();
|
||||||
|
memory.write32(base + 4, destination).unwrap();
|
||||||
|
memory
|
||||||
|
.write32(base + 8, 0x8400_0000 | u32::from(count))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arm_and_thumb_timer_loads_observe_source_and_data_but_not_trailing_internal_cycles() {
|
||||||
|
for (thumb, instruction, sample, final_count) in
|
||||||
|
[(false, 0xe591_0000, 7, 8), (true, 0x6808, 4, 5)]
|
||||||
|
{
|
||||||
|
let (cpu, mut memory) = prepared(thumb, &[instruction], &[(1, TIMER_BASE)]);
|
||||||
|
start(&mut memory, 0, 0, 0x80);
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.cpu().registers()[0], 0x0080_0000 | sample);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), final_count);
|
||||||
|
assert_eq!(machine.cycles(), u64::from(final_count));
|
||||||
|
assert!(machine.memory().timer_step.get().is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn byte_halfword_and_word_lanes_share_one_completed_access() {
|
||||||
|
for (instruction, address, expected) in [
|
||||||
|
(0xe5d1_0000, TIMER_BASE, 6),
|
||||||
|
(0xe5d1_0000, TIMER_BASE + 1, 1),
|
||||||
|
(0xe1d1_00b0, TIMER_BASE, 0x106),
|
||||||
|
(0xe591_0000, TIMER_BASE, 0x0080_0106),
|
||||||
|
] {
|
||||||
|
let (cpu, mut memory) = prepared(false, &[instruction], &[(1, address)]);
|
||||||
|
start(&mut memory, 0, 0xff, 0x80);
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.cpu().registers()[0], expected);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0x107);
|
||||||
|
assert_eq!(machine.cycles(), 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn starting_and_stopping_timers_take_effect_after_the_store_bus_cycle() {
|
||||||
|
for running in [false, true] {
|
||||||
|
let control = if running { 0 } else { 0x0080_0000 };
|
||||||
|
let (cpu, mut memory) =
|
||||||
|
prepared(false, &[0xe581_0000, NOP], &[(0, control), (1, TIMER_BASE)]);
|
||||||
|
if running {
|
||||||
|
start(&mut memory, 0, 0, 0x80);
|
||||||
|
}
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.cycles(), 7);
|
||||||
|
assert_eq!(
|
||||||
|
machine.memory().read16(TIMER_BASE).unwrap(),
|
||||||
|
if running { 7 } else { 0 }
|
||||||
|
);
|
||||||
|
machine.step().unwrap(); // Store left N: this ROM fetch costs eight cycles.
|
||||||
|
assert_eq!(
|
||||||
|
machine.memory().read16(TIMER_BASE).unwrap(),
|
||||||
|
if running { 7 } else { 8 }
|
||||||
|
);
|
||||||
|
assert_eq!(machine.cycles(), 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_timer_accesses_keep_distinct_word_phases() {
|
||||||
|
let (cpu, mut memory) = prepared(false, &[0xe891_0005], &[(1, TIMER_BASE)]); // LDM r1,{r0,r2}
|
||||||
|
start(&mut memory, 0, 0, 0x80);
|
||||||
|
start(&mut memory, 1, 0, 0x80);
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.cpu().registers()[0], 0x0080_0007);
|
||||||
|
assert_eq!(machine.cpu().registers()[2], 0x0080_0008);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 9);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE + 4).unwrap(), 9);
|
||||||
|
|
||||||
|
let (cpu, memory) = prepared(
|
||||||
|
false,
|
||||||
|
&[0xe880_0006],
|
||||||
|
&[(0, TIMER_BASE), (1, 0x0080_0000), (2, 0x0080_0000)],
|
||||||
|
);
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 1);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE + 4).unwrap(), 0);
|
||||||
|
assert_eq!(machine.cycles(), 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reload_changes_preserve_pre_write_overflows_and_prescaler_phase() {
|
||||||
|
// Overflow during source fetch uses the old reload, not the value stored afterward.
|
||||||
|
let (cpu, mut memory) = prepared(false, &[0xe1c1_00b0], &[(0, 0x1234), (1, TIMER_BASE)]);
|
||||||
|
start(&mut memory, 0, 0xfffa, 0xc0);
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfffb);
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
|
||||||
|
machine.memory_mut().advance_cycles(5);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0x1234);
|
||||||
|
|
||||||
|
// A read crossing a prescaler edge must retain the remainder for the next step.
|
||||||
|
let (cpu, mut memory) = prepared(false, &[0xe591_0000], &[(1, TIMER_BASE)]);
|
||||||
|
start(&mut memory, 0, 0, 0x81);
|
||||||
|
memory.advance_cycles(60);
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.cpu().registers()[0], 0x0081_0001);
|
||||||
|
machine.memory_mut().advance_cycles(59);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 1);
|
||||||
|
machine.memory_mut().advance_cycles(1);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cascades_and_timer_irq_bits_are_visible_at_the_read_phase() {
|
||||||
|
let (cpu, mut memory) = prepared(false, &[0xe591_0000], &[(1, TIMER_BASE + 4)]);
|
||||||
|
start(&mut memory, 0, 0xfffe, 0xc0);
|
||||||
|
start(&mut memory, 1, 0xfffe, 0xc4);
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap(); // Source+data=7 gives three timer0 pulses and one timer1 overflow.
|
||||||
|
assert_eq!(machine.cpu().registers()[0], 0x00c4_ffff);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE + 4).unwrap(), 0xfffe);
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0x18);
|
||||||
|
|
||||||
|
let (cpu, mut memory) = prepared(false, &[0xe591_0000], &[(1, IE)]);
|
||||||
|
start(&mut memory, 0, 0xfffa, 0xc0);
|
||||||
|
memory.write16(IE, 8).unwrap();
|
||||||
|
memory.write16(IME, 1).unwrap();
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||||
|
assert_eq!(machine.cpu().registers()[0], 0x0008_0008); // Word IE+IF, coherent timer flag.
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn if_acknowledgement_clears_earlier_timer_events_but_not_later_ones() {
|
||||||
|
for (instruction, reload, expected) in [(0xe1c1_00b0, 0xfffa, 0), (0xe141_0092, 0xffff, 8)] {
|
||||||
|
let (cpu, mut memory) = prepared(false, &[instruction], &[(0, 8), (1, IF), (2, 8)]);
|
||||||
|
start(&mut memory, 0, reload, 0xc0);
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), expected);
|
||||||
|
if instruction == 0xe141_0092 {
|
||||||
|
// SWPB acknowledges at the second data access; its final I cycle raises IF again.
|
||||||
|
assert_eq!(machine.cpu().registers()[0], 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failed_instructions_discard_partial_timer_progress_and_pending_irqs() {
|
||||||
|
for (instruction, base) in [
|
||||||
|
(0xe591_0000, 0x0e00_0000),
|
||||||
|
(0xe891_0005, TIMER_BASE + 12),
|
||||||
|
(0xe881_0005, TIMER_BASE + 12),
|
||||||
|
(0xffff_ffff, 0),
|
||||||
|
] {
|
||||||
|
let (cpu, mut memory) = prepared(false, &[instruction], &[(1, base)]);
|
||||||
|
start(&mut memory, 0, 0xfffc, 0xc0);
|
||||||
|
let before = memory.io.timer_step();
|
||||||
|
let cpu_before = cpu.clone();
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
for _ in 0..2 {
|
||||||
|
assert!(machine.step().is_err());
|
||||||
|
assert_eq!(machine.memory().io.timer_step(), before);
|
||||||
|
assert_eq!(machine.cpu(), &cpu_before);
|
||||||
|
assert_eq!(machine.cycles(), 0);
|
||||||
|
assert!(machine.memory().timer_step.get().is_none());
|
||||||
|
assert!(machine.memory().cpu_timing.get().is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A missing current instruction fails before CpuTiming starts, but must still discard TimerStep.
|
||||||
|
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||||
|
start(&mut memory, 0, 0xffff, 0xc0);
|
||||||
|
let mut machine = Machine::new(Cpu::new(ROM_START), memory);
|
||||||
|
assert!(machine.step().is_err());
|
||||||
|
assert!(machine.memory().timer_step.get().is_none());
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cpu_only_apis_and_host_reads_do_not_advance_timer_devices() {
|
||||||
|
for prefetch in [0, 0x4000] {
|
||||||
|
for timed in [false, true] {
|
||||||
|
let (mut cpu, mut memory) = prepared(false, &[0xe591_0000], &[(1, TIMER_BASE)]);
|
||||||
|
memory.write16(WAITCNT, prefetch).unwrap();
|
||||||
|
start(&mut memory, 0, 123, 0x80);
|
||||||
|
let before = memory.io.timer_step();
|
||||||
|
if timed {
|
||||||
|
cpu.step_timed(&mut memory).unwrap();
|
||||||
|
} else {
|
||||||
|
cpu.step(&mut memory).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(cpu.registers()[0], 0x0080_007b);
|
||||||
|
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 123);
|
||||||
|
assert_eq!(memory.io.timer_step(), before);
|
||||||
|
assert_eq!(memory.cycles(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dma_samples_timers_after_startup_and_source_and_writes_after_destination() {
|
||||||
|
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||||
|
start(&mut memory, 0, 0, 0x80);
|
||||||
|
dma(&mut memory, TIMER_BASE, RAM, 1);
|
||||||
|
memory.step_dma().unwrap().unwrap();
|
||||||
|
assert_eq!(memory.read32(RAM).unwrap(), 0x0080_0003);
|
||||||
|
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 4);
|
||||||
|
assert_eq!(memory.cycles(), 4);
|
||||||
|
|
||||||
|
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||||
|
memory.write32(RAM, 0x0080_0000).unwrap();
|
||||||
|
memory.write32(RAM + 4, 0x0080_0000).unwrap();
|
||||||
|
dma(&mut memory, RAM, TIMER_BASE, 2);
|
||||||
|
memory.step_dma().unwrap().unwrap();
|
||||||
|
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 0);
|
||||||
|
memory.step_dma().unwrap().unwrap();
|
||||||
|
assert_eq!(memory.read16(TIMER_BASE).unwrap(), 2);
|
||||||
|
assert_eq!(memory.read16(TIMER_BASE + 4).unwrap(), 0);
|
||||||
|
assert_eq!(memory.cycles(), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dma_failures_preserve_timers_and_if_and_clear_the_transaction() {
|
||||||
|
for (source, destination) in [(0x0e00_0000, RAM), (TIMER_BASE, ROM_START)] {
|
||||||
|
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||||
|
start(&mut memory, 0, 0xffff, 0xc0);
|
||||||
|
let before = memory.io.timer_step();
|
||||||
|
dma(&mut memory, source, destination, 1);
|
||||||
|
assert!(memory.step_dma().is_err());
|
||||||
|
assert_eq!(memory.io.timer_step(), before);
|
||||||
|
assert!(memory.timer_step.get().is_none());
|
||||||
|
assert_eq!(memory.cycles(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_capture_splits_do_not_advance_committed_timers_twice() {
|
||||||
|
for capture in [false, true] {
|
||||||
|
let (cpu, mut memory) = prepared(false, &[0xe891_007d], &[(1, 0x0200_1000)]);
|
||||||
|
memory.set_scanline_rendering(capture);
|
||||||
|
memory.advance_cycles(HBLANK_START - 2);
|
||||||
|
start(&mut memory, 0, 0, 0x80);
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap(); // Source 6, six EWRAM words 36, internal 1.
|
||||||
|
assert_eq!(machine.last_timing().total(), 43);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 43);
|
||||||
|
assert_eq!(machine.cycles(), u64::from(HBLANK_START - 2 + 43));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn timer_flag_commit_preserves_other_irq_sources() {
|
||||||
|
for (instruction, expected) in [(0xe1d1_00b0, 0xa), (0xe1c1_00b0, 2)] {
|
||||||
|
let (cpu, mut memory) = prepared(false, &[instruction], &[(0, 8), (1, IF)]);
|
||||||
|
memory.write16(DISPSTAT, 0x10).unwrap();
|
||||||
|
memory.advance_cycles(HBLANK_START);
|
||||||
|
assert_eq!(memory.read16(IF).unwrap(), 2);
|
||||||
|
start(&mut memory, 0, 0xfffa, 0xc0);
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), expected);
|
||||||
|
if instruction == 0xe1d1_00b0 {
|
||||||
|
assert_eq!(machine.cpu().registers()[0], 0xa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bios_power_store_pays_its_cycles_and_new_timer_irq_wakes_only_halt() {
|
||||||
|
for stop in [false, true] {
|
||||||
|
let (mut cpu, mut memory) = prepared(
|
||||||
|
false,
|
||||||
|
&[0xe12f_ff1c],
|
||||||
|
&[(0, if stop { 0x80 } else { 0 }), (1, HALTCNT), (12, 0x100)],
|
||||||
|
);
|
||||||
|
memory.bios.as_mut().unwrap()[0x100..0x104].copy_from_slice(&0xe5c1_0000_u32.to_le_bytes());
|
||||||
|
cpu.step(&mut memory).unwrap(); // BX into original BIOS STRB r0,[r1].
|
||||||
|
start(&mut memory, 0, 0xffff, 0xc0);
|
||||||
|
memory.write16(IE, 8).unwrap();
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.cycles(), 2);
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
|
||||||
|
assert!(!machine.halted());
|
||||||
|
assert_eq!(machine.stopped(), stop);
|
||||||
|
if stop {
|
||||||
|
let before = machine.memory().io.timer_step();
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::StopIdle);
|
||||||
|
assert_eq!(machine.memory().io.timer_step(), before);
|
||||||
|
assert_eq!(machine.cycles(), 2);
|
||||||
|
} else {
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn halt_and_stop_keep_their_clock_ownership() {
|
||||||
|
for stop in [false, true] {
|
||||||
|
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||||
|
start(&mut memory, 0, 0, 0x80);
|
||||||
|
memory.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap();
|
||||||
|
let mut machine = Machine::new(Cpu::new(ROM_START), memory);
|
||||||
|
assert_eq!(
|
||||||
|
machine.step().unwrap(),
|
||||||
|
if stop {
|
||||||
|
StepKind::StopIdle
|
||||||
|
} else {
|
||||||
|
StepKind::HaltIdle
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
u64::from(machine.memory().read16(TIMER_BASE).unwrap()),
|
||||||
|
machine.cycles()
|
||||||
|
);
|
||||||
|
assert!(machine.memory().timer_step.get().is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -93,7 +93,8 @@ pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKi
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One speculative instruction/IRQ timing transaction. Events arrive in source,
|
/// One speculative instruction/IRQ timing transaction. Events arrive in source,
|
||||||
/// data, internal, then target-pair order. Device clocks still advance in bulk.
|
/// data, internal, then target-pair order. Machine timers follow these events;
|
||||||
|
/// other device clocks still advance in bulk.
|
||||||
/// Code uses the entry WAITCNT snapshot; data uses settings at each bus access.
|
/// Code uses the entry WAITCNT snapshot; data uses settings at each bus access.
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub(crate) struct CpuTiming {
|
pub(crate) struct CpuTiming {
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,8 @@ fn timer_demo_configures_hardware_in_cpu_code_and_handles_exactly_one_irq() {
|
||||||
assert_eq!(machine.cpu().registers()[10], 1);
|
assert_eq!(machine.cpu().registers()[10], 1);
|
||||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||||
assert_eq!(machine.memory().read16(TIMER_BASE + 2).unwrap(), 0);
|
assert_eq!(machine.memory().read16(TIMER_BASE + 2).unwrap(), 0);
|
||||||
// Persistent fetch kinds move costs between setup instructions and the running timer.
|
// Start no longer pays its preceding nine cycles; stop now pays its two bus cycles.
|
||||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfffb);
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfff4);
|
||||||
for _ in 0..100 {
|
for _ in 0..100 {
|
||||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,20 +42,17 @@ fn observed_extra_cycle_is_in_rom_data_cancellation_not_internal_work() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unadjusted_timer_samples_expose_the_instruction_boundary_sampling_limit() {
|
fn unadjusted_timer_samples_match_published_observations_at_the_bus_access() {
|
||||||
// This is a known-gap regression, not an assertion that hardware behaves this way.
|
// Keep the original program and published values unchanged. No result correction.
|
||||||
// Replace it when timer I/O moves to ordered bus phases; do not correct probe output.
|
|
||||||
for (index, expected) in PUBLISHED_READ_DELTAS.into_iter().enumerate() {
|
for (index, expected) in PUBLISHED_READ_DELTAS.into_iter().enumerate() {
|
||||||
for (waitcnt, expected) in WAITCNT_SETTINGS.into_iter().zip(expected) {
|
for (waitcnt, expected) in WAITCNT_SETTINGS.into_iter().zip(expected) {
|
||||||
let control = measure(waitcnt, index as u8 + 1, false).unwrap();
|
let control = measure(waitcnt, index as u8 + 1, false).unwrap();
|
||||||
let read = measure(waitcnt, index as u8 + 1, true).unwrap();
|
let read = measure(waitcnt, index as u8 + 1, true).unwrap();
|
||||||
let sampled = i64::from(read.timer_sample) - i64::from(control.timer_sample);
|
let sampled = i64::from(read.timer_sample) - i64::from(control.timer_sample);
|
||||||
let boundary = read.boundary_cycles as i64 - control.boundary_cycles as i64;
|
let boundary = read.boundary_cycles as i64 - control.boundary_cycles as i64;
|
||||||
let endpoint_fetch_difference = i64::from(read.sample_timing.code_cycles)
|
assert_eq!(sampled, i64::from(expected));
|
||||||
- i64::from(control.sample_timing.code_cycles);
|
assert_eq!(boundary, sampled);
|
||||||
assert_ne!(sampled, i64::from(expected));
|
assert!(read.sample_timing.code_cycles > control.sample_timing.code_cycles);
|
||||||
assert!(endpoint_fetch_difference > 0);
|
|
||||||
assert_eq!(boundary - sampled, endpoint_fetch_difference);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ The repository is a Cargo workspace with three crates:
|
||||||
| `crates/gba-core/src/cpu/fetch_history_tests.rs` | Shared sample consumption, no execution-entry rereads, and failed-step retry |
|
| `crates/gba-core/src/cpu/fetch_history_tests.rs` | Shared sample consumption, no execution-entry rereads, and failed-step retry |
|
||||||
| `crates/gba-core/src/cpu/fetch_boundary_tests.rs` | Actual fetch-region snapshots, split refills, IWRAM lane requirements, ROM-window boundaries, and nominal timing |
|
| `crates/gba-core/src/cpu/fetch_boundary_tests.rs` | Actual fetch-region snapshots, split refills, IWRAM lane requirements, ROM-window boundaries, and nominal timing |
|
||||||
| `crates/gba-core/src/io.rs` | I/O registers, timers, HALT/STOP wake-up, next-event bounds, and IRQ latches |
|
| `crates/gba-core/src/io.rs` | I/O registers, timers, HALT/STOP wake-up, next-event bounds, and IRQ latches |
|
||||||
|
| `crates/gba-core/src/io/timer_step.rs` | Staged timer bank and timer IF bits at ordered CPU/DMA bus phases |
|
||||||
|
| `crates/gba-core/src/memory/timer_step_tests.rs` | Timer access phases, coherent lanes, cascades, IF ordering, rollback, DMA, idle, and clock ownership |
|
||||||
| `crates/gba-core/src/display.rs` | Display clock, scanline status, comparison edges, and display IRQ events |
|
| `crates/gba-core/src/display.rs` | Display clock, scanline status, comparison edges, and display IRQ events |
|
||||||
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
|
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
|
||||||
| `crates/gba-core/src/machine.rs` | Timed CPU/DMA/device stepping and IRQ delivery |
|
| `crates/gba-core/src/machine.rs` | Timed CPU/DMA/device stepping and IRQ delivery |
|
||||||
|
|
@ -69,7 +71,7 @@ The repository is a Cargo workspace with three crates:
|
||||||
| `crates/gba-demos/src/timer_demo.rs` | Original timer-configuration program and IRQ handler |
|
| `crates/gba-demos/src/timer_demo.rs` | Original timer-configuration program and IRQ handler |
|
||||||
| `crates/gba-demos/src/prefetch_probe.rs` | Original bounded ARM read/control probes and pinned published cancellation observations |
|
| `crates/gba-demos/src/prefetch_probe.rs` | Original bounded ARM read/control probes and pinned published cancellation observations |
|
||||||
| `crates/gba-demos/examples/prefetch_cancellation.rs` | CSV comparison of published, instruction-boundary, and unadjusted timer intervals; fails on mismatches |
|
| `crates/gba-demos/examples/prefetch_cancellation.rs` | CSV comparison of published, instruction-boundary, and unadjusted timer intervals; fails on mismatches |
|
||||||
| `crates/gba-core/tests/prefetch_cancellation.rs` | Published read comparisons, cancellation phase, disabled-prefetch controls, and the known timer sampling gap |
|
| `crates/gba-core/tests/prefetch_cancellation.rs` | Published read comparisons, cancellation phase, disabled-prefetch controls, and unadjusted timer samples |
|
||||||
| `Cargo.toml` | Workspace members and shared package settings |
|
| `Cargo.toml` | Workspace members and shared package settings |
|
||||||
| `crates/gba-core/src/lib.rs` | Core modules |
|
| `crates/gba-core/src/lib.rs` | Core modules |
|
||||||
| `crates/gba-demos/src/lib.rs` | Demo modules, original instruction/exception bytes, and raw input-test ROM |
|
| `crates/gba-demos/src/lib.rs` | Demo modules, original instruction/exception bytes, and raw input-test ROM |
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ direnv exec . cargo run --locked -p gba-demos --example prefetch_cancellation >
|
||||||
```
|
```
|
||||||
|
|
||||||
This headless original ARM probe compares published read-cancellation observations with two separate emulator measurements.
|
This headless original ARM probe compares published read-cancellation observations with two separate emulator measurements.
|
||||||
Instruction-boundary totals match, but actual timer samples currently fail. The example therefore exits with status 1.
|
Both instruction-boundary totals and actual timer samples now match; the example exits with status 0.
|
||||||
Do not treat the matching totals as a hardware-timing pass.
|
This is a bounded comparison with published observations, not a complete hardware-timing pass.
|
||||||
See [the protocol, source hashes, and timer sampling gap](research/prefetch-cancellation.md).
|
See [the protocol, source hashes, and timer sampling correction](research/prefetch-cancellation.md).
|
||||||
The probe needs no downloaded ROM, assets, window, or external assembler.
|
The probe needs no downloaded ROM, assets, window, or external assembler.
|
||||||
|
|
||||||
## Open the affine raster demo
|
## Open the affine raster demo
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ The loaded value takes precedence over the updated base. Addresses and register
|
||||||
Pre-indexed loads access the adjusted address; post-indexed loads access the original address.
|
Pre-indexed loads access the adjusted address; post-indexed loads access the original address.
|
||||||
The discarded post-indexed writeback address is not a second memory access.
|
The discarded post-indexed writeback address is not a second memory access.
|
||||||
Normal alignment, word rotation, odd-halfword behavior, and sign extension still apply.
|
Normal alignment, word rotation, odd-halfword behavior, and sign extension still apply.
|
||||||
Loads preserve CPSR and use normal load timing. Device clocks advance after the read, with IRQ sampling on the next step.
|
Loads preserve CPSR and use normal load timing. Machine timer reads sample at data-access completion; IRQ delivery waits until the next step.
|
||||||
Read diagnostics leave CPU state and device clocks unchanged under the existing atomic-error policy.
|
Read diagnostics leave CPU state and device clocks unchanged under the existing atomic-error policy.
|
||||||
Stores with matching base/source still store the original base value before writeback.
|
Stores with matching base/source still store the original base value before writeback.
|
||||||
PC writeback, byte/halfword PC destinations, and unimplemented user-transfer encodings remain diagnostics.
|
PC writeback, byte/halfword PC destinations, and unimplemented user-transfer encodings remain diagnostics.
|
||||||
|
|
@ -359,7 +359,8 @@ The new settings apply to subsequent code accesses.
|
||||||
Timed CPU execution uses an ordered `CpuTiming` transaction: source fetch, actual data accesses, internal work, then target-pair fetches.
|
Timed CPU execution uses an ordered `CpuTiming` transaction: source fetch, actual data accesses, internal work, then target-pair fetches.
|
||||||
A failed instruction discards the entire transaction. Target samples do not add duplicate fetch or data costs.
|
A failed instruction discards the entire transaction. Target samples do not add duplicate fetch or data costs.
|
||||||
IRQ entry records its source fetch and vector pair in the same path.
|
IRQ entry records its source fetch and vector pair in the same path.
|
||||||
Device clocks still advance once after the successful step; this is not per-cycle scheduling.
|
Machine timer state advances through these events and commits only after success. Other device clocks still advance in bulk afterward.
|
||||||
|
This is not a complete per-cycle scheduler.
|
||||||
The transaction also stages the nominal Game Pak prefetch queue described below.
|
The transaction also stages the nominal Game Pak prefetch queue described below.
|
||||||
|
|
||||||
### Game Pak opcode prefetch
|
### Game Pak opcode prefetch
|
||||||
|
|
@ -387,7 +388,7 @@ Host reads and `Memory::advance_cycles` do not advance the queue; machine steps
|
||||||
These are source-backed nominal rules, not a hardware conformance claim.
|
These are source-backed nominal rules, not a hardware conformance claim.
|
||||||
See [queue evidence, regression coverage, and limits](../research/gamepak-prefetch.md).
|
See [queue evidence, regression coverage, and limits](../research/gamepak-prefetch.md).
|
||||||
The [original cancellation probe](../research/prefetch-cancellation.md) matches published read interval totals at instruction boundaries.
|
The [original cancellation probe](../research/prefetch-cancellation.md) matches published read interval totals at instruction boundaries.
|
||||||
Its unadjusted timer samples still fail because timer reads precede the instruction's device-clock update.
|
Its unadjusted timer samples now also match after [ordered timer accesses](../research/ordered-timer-access.md).
|
||||||
|
|
||||||
### Nominal CPU resume after DMA
|
### Nominal CPU resume after DMA
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ CPU delivery waits until no DMA is ready. IRQ entry does not acknowledge IF.
|
||||||
Timing uses independent source and destination bus costs, including WAITCNT and 128 KiB ROM boundaries.
|
Timing uses independent source and destination bus costs, including WAITCNT and 128 KiB ROM boundaries.
|
||||||
The first unit uses non-sequential accesses plus two internal cycles. Later units use sequential accesses.
|
The first unit uses non-sequential accesses plus two internal cycles. Later units use sequential accesses.
|
||||||
Each step reports zero code cycles. Overlapping transfers read each unit after the preceding write; they are not bulk host copies.
|
Each step reports zero code cycles. Overlapping transfers read each unit after the preceding write; they are not bulk host copies.
|
||||||
|
Timers stage progress through startup, source, and destination phases. Timer reads sample after source completion; writes apply after destination completion.
|
||||||
|
Timer IF acknowledgements use that same ordering. Success commits the staged bank; failure discards it.
|
||||||
|
Other devices then advance in bulk without ticking timers twice. Exact physical DMA startup timing remains unverified.
|
||||||
A successful unit breaks the next nominal CPU code-access sequence, even if DMA accessed only RAM.
|
A successful unit breaks the next nominal CPU code-access sequence, even if DMA accessed only RAM.
|
||||||
The resumed instruction requests N once; enabled opcode prefetch can still satisfy a matching request.
|
The resumed instruction requests N once; enabled opcode prefetch can still satisfy a matching request.
|
||||||
RAM accesses and the nominal startup cycles advance an active prefetch stream; cartridge accesses cancel it.
|
RAM accesses and the nominal startup cycles advance an active prefetch stream; cartridge accesses cancel it.
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Entry charges the discarded incoming-state PC+8/PC+4 fetch and the ARM vector pa
|
||||||
DMA resume can make the discarded fetch non-sequential. Missing discarded bytes do not prevent IRQ entry.
|
DMA resume can make the discarded fetch non-sequential. Missing discarded bytes do not prevent IRQ entry.
|
||||||
See [refill timing](../research/refill-fetch-timing.md) for history effects and remaining limits.
|
See [refill timing](../research/refill-fetch-timing.md) for history effects and remaining limits.
|
||||||
A device event during a step can trigger IRQ entry once no DMA is ready and CPU masks allow delivery.
|
A device event during a step can trigger IRQ entry once no DMA is ready and CPU masks allow delivery.
|
||||||
Bus writes take effect before the step's device-clock update.
|
Timer accesses use ordered bus-completion phases. Other device writes still take effect before their bulk clock update.
|
||||||
`StepKind::Dma { channel }` identifies a DMA unit. `StepKind::HaltIdle` identifies one bounded HALT clock advance.
|
`StepKind::Dma { channel }` identifies a DMA unit. `StepKind::HaltIdle` identifies one bounded HALT clock advance.
|
||||||
`StepKind::StopIdle` identifies a stopped system with no clock progress.
|
`StepKind::StopIdle` identifies a stopped system with no clock progress.
|
||||||
The other step kinds remain `Instruction` and `IrqEntry`.
|
The other step kinds remain `Instruction` and `IrqEntry`.
|
||||||
|
|
@ -23,14 +23,24 @@ The other step kinds remain `Instruction` and `IrqEntry`.
|
||||||
Failed steps do not advance the clock or partially change CPU or device state. Earlier successful steps remain committed.
|
Failed steps do not advance the clock or partially change CPU or device state. Earlier successful steps remain committed.
|
||||||
|
|
||||||
**The clock now uses instruction-specific costs, but it is not cycle-accurate.**
|
**The clock now uses instruction-specific costs, but it is not cycle-accurate.**
|
||||||
Device clocks update in batches after instructions, exception entries, DMA units, or HALT idle intervals.
|
Timers now use a staged clock during machine instructions, IRQ entry, and DMA units.
|
||||||
Reads observe register state before that batch.
|
Source-fetch cycles advance that clock before data work. Each data access advances it before reading or writing timer registers.
|
||||||
Writes, including timer start/stop and IF acknowledgement, take effect before the entire batch.
|
All byte lanes of one access share the same completion time. Internal and target-fetch cycles follow in order.
|
||||||
This does not reproduce bus-access timing within an instruction or IRQ synchronization delays.
|
A timer load therefore includes its source and data cycles, but not its trailing internal cycle.
|
||||||
The [prefetch cancellation probe](../research/prefetch-cancellation.md) makes this limitation measurable.
|
Timer-start writes do not count preceding cycles. Timer-stop writes count through their own bus completion.
|
||||||
Its timer load omits its own source-fetch time, which differs between the read and control runs.
|
Block transfers preserve distinct word phases; combined reload/control writes merge at one phase, with reload bytes first.
|
||||||
Nominal instruction-boundary totals match published observations; the actual timer samples do not.
|
|
||||||
The diagnostic reports both values and returns failure rather than correcting the timer sample.
|
The transaction also owns timer IF bits. Reads see overflows through the current access; acknowledgements clear earlier timer requests.
|
||||||
|
A later internal cycle can raise an acknowledged timer request again. Non-timer IF bits retain their existing behavior.
|
||||||
|
IRQ delivery still waits until the next machine step. A committed timer request can wake HALT, but not STOP.
|
||||||
|
|
||||||
|
Successful steps commit timers and timer IF bits. Failed steps discard staged counters, prescaler phases, reload/control writes, and requests.
|
||||||
|
Display and other device clocks then advance once, without advancing timers again.
|
||||||
|
Those devices still use the bulk scheduler. Mid-instruction display events, general bus arbitration, and IRQ synchronization delays remain unmodeled.
|
||||||
|
Host clock advances and HALT idle batches retain ordinary bulk timer advancement; STOP idle does not advance timers.
|
||||||
|
|
||||||
|
The unchanged [prefetch cancellation probe](../research/prefetch-cancellation.md) now matches both published interval totals and actual timer samples.
|
||||||
|
No offset is added to its timer values. See [clock ownership and evidence](../research/ordered-timer-access.md) for the bounded scope.
|
||||||
|
|
||||||
`Cpu::step` remains a CPU-only API. It does not honor HALT/STOP, execute DMA, advance device clocks, or sample device IRQs.
|
`Cpu::step` remains a CPU-only API. It does not honor HALT/STOP, execute DMA, advance device clocks, or sample device IRQs.
|
||||||
`Cpu::step_timed` executes the same operation and returns `StepTiming`, without advancing devices.
|
`Cpu::step_timed` executes the same operation and returns `StepTiming`, without advancing devices.
|
||||||
|
|
@ -78,9 +88,11 @@ Audio events and interrupt sources other than timers, display events, DMA comple
|
||||||
`--timer-demo` executes original ARM code that configures Timer 0, IE, and IME.
|
`--timer-demo` executes original ARM code that configures Timer 0, IE, and IME.
|
||||||
The timer overflows after 16 supplied cycles and enters the original handler through vector `0x18`.
|
The timer overflows after 16 supplied cycles and enters the original handler through vector `0x18`.
|
||||||
The handler stops Timer 0, acknowledges IF, increments r10, and returns with `SUBS pc, lr, #4`.
|
The handler stops Timer 0, acknowledges IF, increments r10, and returns with `SUBS pc, lr, #4`.
|
||||||
The demo ends after 21 steps and 151 nominal cycles with one handler call, using default WAITCNT settings.
|
The demo completes with one handler call, using default WAITCNT settings.
|
||||||
Its initial ARM ROM fetch uses PC+8 sequential timing; cold pipeline filling has no separate startup charge.
|
Its initial ARM ROM fetch uses PC+8 sequential timing; cold pipeline filling has no separate startup charge.
|
||||||
These counts test the timing model; they are not hardware timing measurements.
|
The stopped counter is now `0xfff4`, rather than the earlier bulk-write result `0xfffb`.
|
||||||
|
Timer start excludes its preceding nine cycles; timer stop includes its two bus cycles.
|
||||||
|
These are nominal regression results, not hardware startup-delay measurements.
|
||||||
Its test handler deliberately changes r2 and r10; it does not implement the Nintendo BIOS calling convention.
|
Its test handler deliberately changes r2 and r10; it does not implement the Nintendo BIOS calling convention.
|
||||||
|
|
||||||
## Keypad interrupt control
|
## Keypad interrupt control
|
||||||
|
|
|
||||||
|
|
@ -209,12 +209,20 @@ Game Pak prefetch research:
|
||||||
- [Jgenesis prefetch at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/prefetch.rs) and its [bus integration](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs), for independent halfword capacity, full-buffer pause/restart, CPU delivery, and cancellation rules.
|
- [Jgenesis prefetch at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/prefetch.rs) and its [bus integration](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs), for independent halfword capacity, full-buffer pause/restart, CPU delivery, and cancellation rules.
|
||||||
- [NanoBoyAdvance boundary test at cc3f4a28](https://codeberg.org/nba-emu/hw-test/src/commit/cc3f4a286cdef823980d9b353bd70befc9927d28/bus/128kb-boundary/source/main.c), reviewed for data LDM/DMA boundaries, not active opcode-queue conformance.
|
- [NanoBoyAdvance boundary test at cc3f4a28](https://codeberg.org/nba-emu/hw-test/src/commit/cc3f4a286cdef823980d9b353bd70befc9927d28/bus/128kb-boundary/source/main.c), reviewed for data LDM/DMA boundaries, not active opcode-queue conformance.
|
||||||
- [PrefetchAbuse source at 9ca57c13](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/src/main.c), its [README](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/README.md), and [Makefile](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/Makefile), for published read observations, actual multiply delays, and build assumptions. No upstream program or ROM was imported.
|
- [PrefetchAbuse source at 9ca57c13](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/src/main.c), its [README](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/README.md), and [Makefile](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/Makefile), for published read observations, actual multiply delays, and build assumptions. No upstream program or ROM was imported.
|
||||||
See [the original cancellation comparison](research/prefetch-cancellation.md) for hashes, correspondence limits, matching nominal intervals, and failing timer samples.
|
See [the original cancellation comparison](research/prefetch-cancellation.md) for hashes, correspondence limits, and the initial timer sampling failure.
|
||||||
|
Both nominal intervals and unadjusted timer samples now match after ordered timer accesses.
|
||||||
|
|
||||||
See [prefetch behavior and evidence](research/gamepak-prefetch.md).
|
See [prefetch behavior and evidence](research/gamepak-prefetch.md).
|
||||||
The nominal queue is implemented. Source differences and live WAITCNT transitions remain validation questions.
|
The nominal queue is implemented. Source differences and live WAITCNT transitions remain validation questions.
|
||||||
No hardware conformance result is claimed.
|
No hardware conformance result is claimed.
|
||||||
|
|
||||||
|
Ordered timer data accesses:
|
||||||
|
|
||||||
|
- The pinned [Jgenesis bus at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs) charges one I/O cycle before register reads/writes and passes the resulting clock to timers.
|
||||||
|
- The pinned PrefetchAbuse read observations above isolate the missing source-fetch interval in the previous bulk timer model.
|
||||||
|
|
||||||
|
See [staged timer clock ownership](research/ordered-timer-access.md). Relative probe agreement does not establish all absolute timer edges.
|
||||||
|
|
||||||
Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work.
|
Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work.
|
||||||
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
|
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
|
||||||
The ignore file excludes `roms/`, common game ROM extensions, and save files.
|
The ignore file excludes `roms/`, common game ROM extensions, and save files.
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ Background progress cannot cause an early ROM-file lookup or change instruction
|
||||||
- [NanoBoyAdvance 128 KiB boundary test at cc3f4a28](https://codeberg.org/nba-emu/hw-test/src/commit/cc3f4a286cdef823980d9b353bd70befc9927d28/bus/128kb-boundary/source/main.c) tests data LDM and DMA boundaries.
|
- [NanoBoyAdvance 128 KiB boundary test at cc3f4a28](https://codeberg.org/nba-emu/hw-test/src/commit/cc3f4a286cdef823980d9b353bd70befc9927d28/bus/128kb-boundary/source/main.c) tests data LDM and DMA boundaries.
|
||||||
Reading this test does not validate active opcode-queue crossings. It was not run here.
|
Reading this test does not validate active opcode-queue crossings. It was not run here.
|
||||||
- [PrefetchAbuse at 9ca57c13](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/src/main.c) supplies published hardware read-cancellation observations.
|
- [PrefetchAbuse at 9ca57c13](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/src/main.c) supplies published hardware read-cancellation observations.
|
||||||
A later [original probe](prefetch-cancellation.md) matches the instruction-boundary differences, but exposes incorrect bulk timer sampling.
|
The [original probe](prefetch-cancellation.md) matches instruction-boundary differences and, after ordered timer accesses, actual timer samples.
|
||||||
The source, README, and build assumptions were reviewed. No upstream implementation or ROM was imported.
|
The source, README, and build assumptions were reviewed. No upstream implementation or ROM was imported.
|
||||||
|
|
||||||
No external implementation or ROM was added to the repository.
|
No external implementation or ROM was added to the repository.
|
||||||
|
|
@ -76,7 +76,7 @@ A later ROM fetch still needs an exact head match to use queued timing.
|
||||||
|
|
||||||
Success commits queue progress. A diagnostic discards all staged progress, including earlier successful data accesses.
|
Success commits queue progress. A diagnostic discards all staged progress, including earlier successful data accesses.
|
||||||
Missing lookahead or target bytes retain deferred diagnostics and nominal attempted-fetch costs.
|
Missing lookahead or target bytes retain deferred diagnostics and nominal attempted-fetch costs.
|
||||||
Device clocks still advance once after a successful machine step, not after each timing event.
|
Machine timer transactions now follow these events and commit on success. Other devices still advance in bulk after the step.
|
||||||
|
|
||||||
`Cpu::step_timed` updates the queue and returns costs without advancing devices.
|
`Cpu::step_timed` updates the queue and returns costs without advancing devices.
|
||||||
`Cpu::step` uses the same path when prefetch is enabled, but discards returned costs.
|
`Cpu::step` uses the same path when prefetch is enabled, but discards returned costs.
|
||||||
|
|
@ -135,8 +135,8 @@ Public ARM, Thumb, memory, and BIOS reports match the preceding ordered-timing c
|
||||||
Debug and release reports also match. Original CPU and timer demo traces are unchanged.
|
Debug and release reports also match. Original CPU and timer demo traces are unchanged.
|
||||||
These public checkpoints do not independently validate active prefetch timing.
|
These public checkpoints do not independently validate active prefetch timing.
|
||||||
|
|
||||||
The later [cancellation comparison](prefetch-cancellation.md) matches published read observations only at instruction boundaries.
|
The [cancellation comparison](prefetch-cancellation.md) originally matched published read observations only at instruction boundaries.
|
||||||
Actual timer readings still fail because device updates occur after the timer-load instruction.
|
[Ordered timer accesses](ordered-timer-access.md) now make the unchanged timer readings match as well.
|
||||||
Next, implement ordered timer observations and validate full-buffer restart and page boundaries independently.
|
Next, validate absolute timer edges, full-buffer restart, and page boundaries independently.
|
||||||
Cold-start timing, live WAITCNT reconfiguration, per-access device scheduling, and sub-instruction DMA arbitration remain incomplete.
|
Cold-start timing, live WAITCNT reconfiguration, per-access device scheduling, and sub-instruction DMA arbitration remain incomplete.
|
||||||
Exact bus history, display contention, configurable EWRAM timing, and timer/IRQ delays remain separate work.
|
Exact bus history, display contention, configurable EWRAM timing, and timer/IRQ delays remain separate work.
|
||||||
|
|
|
||||||
100
docs/research/ordered-timer-access.md
Normal file
100
docs/research/ordered-timer-access.md
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Ordered timer data accesses
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
Machine timer reads and writes now use bus-access completion times instead of the instruction's initial clock.
|
||||||
|
The unchanged [prefetch cancellation probe](prefetch-cancellation.md) matches both published interval totals and unadjusted timer readings.
|
||||||
|
Its program, published observations, and measurement code were not changed to obtain this result.
|
||||||
|
|
||||||
|
This is a timer-specific extension to nominal timing, not a complete cycle-accurate device scheduler.
|
||||||
|
Display events, DMA arbitration, timer startup delays, and IRQ synchronization remain separate limits.
|
||||||
|
No physical-hardware measurement or external-emulator differential run was performed locally.
|
||||||
|
|
||||||
|
## Evidence and phase choice
|
||||||
|
|
||||||
|
The preceding probe isolated the error: its missing interval equaled the difference between the final source-fetch costs.
|
||||||
|
The opcode queue computed those costs correctly, but the timer read occurred before they advanced device time.
|
||||||
|
Moving only the probe's reported result would conceal that error rather than correct device behavior.
|
||||||
|
|
||||||
|
The reviewed [Jgenesis bus at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs) charges the I/O cycle before register access.
|
||||||
|
Its timer reads and writes receive the resulting clock value.
|
||||||
|
This supports the implemented completion-phase rule for a one-cycle I/O access.
|
||||||
|
No source code was imported from that implementation.
|
||||||
|
|
||||||
|
The published subtraction experiment validates relative read intervals.
|
||||||
|
It does not independently establish every absolute timer edge, startup delay, or control-write phase.
|
||||||
|
Those remain explicit validation limits.
|
||||||
|
|
||||||
|
## Staged clock ownership
|
||||||
|
|
||||||
|
`Memory` owns an optional `TimerStep` during each machine instruction, IRQ entry, or DMA unit.
|
||||||
|
The transaction copies only the four timers and timer IF bits, not RAM, display state, or the entire I/O object.
|
||||||
|
Its elapsed timestamp starts at zero and increases monotonically.
|
||||||
|
It has no heap allocation or event log.
|
||||||
|
|
||||||
|
For a CPU instruction:
|
||||||
|
|
||||||
|
1. Stage timer progress through the source fetch.
|
||||||
|
2. For each data access, stage its elapsed cycles before reading or writing timer registers.
|
||||||
|
3. Stage internal cycles and any target-pair fetches in their existing order.
|
||||||
|
4. On success, commit timers and timer IF bits at the final timestamp.
|
||||||
|
5. Advance the other devices and the machine clock once, without ticking timers again.
|
||||||
|
|
||||||
|
One word access has one sampling time for all four byte lanes.
|
||||||
|
Reload bytes merge before control bytes at that same time.
|
||||||
|
Block transfers have separate completion times for successive words.
|
||||||
|
A start write cannot count earlier source cycles. A stop write includes clocks through its completion.
|
||||||
|
Prescaler remainders, cascaded overflow pulses, and reload behavior use the existing timer implementation.
|
||||||
|
|
||||||
|
Timer IF reads include requests raised through the access phase.
|
||||||
|
Acknowledgement clears timer requests already present at that phase; later work can raise them again.
|
||||||
|
Committing timer flags preserves non-timer IF bits.
|
||||||
|
Those other sources still follow the existing bulk event model, so IF is not a fully cycle-accurate cross-device snapshot.
|
||||||
|
IRQ delivery remains at machine-step boundaries, never halfway through an instruction.
|
||||||
|
A committed enabled timer request wakes HALT. STOP does not use timer requests as a wake source.
|
||||||
|
|
||||||
|
### DMA
|
||||||
|
|
||||||
|
Each unit uses the existing nominal two-cycle startup on its first transfer.
|
||||||
|
Timer sampling follows startup plus source cost; destination writes follow the destination cost.
|
||||||
|
A failed source or destination discards the timer transaction.
|
||||||
|
A successful unit commits timer state before other device clocks and DMA completion IRQ handling.
|
||||||
|
This extends the existing whole-unit scheduler; it does not establish physical DMA startup or preemption timing.
|
||||||
|
|
||||||
|
### Errors and API isolation
|
||||||
|
|
||||||
|
CPU diagnostics discard staged time, counter values, prescaler remainders, and pending timer requests.
|
||||||
|
This includes failures before instruction fetch and failures after earlier block-load accesses.
|
||||||
|
Existing store validation still prevents partial writes when a block transfer has an invalid destination.
|
||||||
|
Previously completed machine steps remain committed.
|
||||||
|
|
||||||
|
`Cpu::step` and `Cpu::step_timed` remain CPU-only APIs.
|
||||||
|
They update CPU state, ordinary register writes, instruction retention, and enabled opcode-prefetch timing as before.
|
||||||
|
They do not create a timer transaction or advance device clocks.
|
||||||
|
Host reads and writes likewise remain clock-free.
|
||||||
|
|
||||||
|
`Memory::advance_cycles` and HALT idle intervals use the existing bulk timer advancement.
|
||||||
|
STOP idle freezes all clocks. The instruction entering STOP still pays its complete nominal duration.
|
||||||
|
Instruction fetches from I/O are not given the data-access timer sampling rule.
|
||||||
|
Other device reads, writes, rendering, and event scheduling retain their documented limits.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Original tests cover ARM/Thumb loads, coherent byte lanes, start/stop stores, and ordered block transfers.
|
||||||
|
They check old-reload overflow behavior, prescaler retention, cascades, timer IF reads, acknowledgement, and later reassertion.
|
||||||
|
Other tests cover non-timer IF preservation, deferred IRQ delivery, DMA phases, rollback, and CPU-only isolation.
|
||||||
|
Capture-enabled and capture-disabled steps check that display clock splits do not tick timers twice.
|
||||||
|
BIOS HALT/STOP stores check final-cycle ownership and timer wake behavior.
|
||||||
|
|
||||||
|
The cancellation example exits successfully in debug and release on Darwin arm64.
|
||||||
|
Its CSV reports match between build profiles. No correction is applied to loaded timer values.
|
||||||
|
Workspace/core/demo tests, formatting, clippy, rustdoc, Python tests, native ROM windows, and graphics smoke modes pass.
|
||||||
|
Pinned public ARM, Thumb, memory, and BIOS reports remain byte-identical to the preceding change.
|
||||||
|
|
||||||
|
The original timer demo retains its cycle count and IRQ-entry step.
|
||||||
|
Its stopped counter changes from `0xfffb` to `0xfff4`: start excludes nine preceding cycles, while stop includes two bus cycles.
|
||||||
|
The alias-load regression now samples `0x00c0fffb` after source and data cycles, instead of the previous `0x00c0fffa`.
|
||||||
|
These expectation changes follow the access phases; the independent cancellation program and observations remain unchanged.
|
||||||
|
|
||||||
|
Next, validate absolute timer startup/control edges and independently test full-buffer prefetch restart and page boundaries.
|
||||||
|
Extend other device observations only with explicit clock ownership and rollback coverage.
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
An original ARM probe reproduces the measured structure of the read cases in `zaydlang/PrefetchAbuse`.
|
An original ARM probe reproduces the measured structure of the read cases in `zaydlang/PrefetchAbuse`.
|
||||||
Its instruction-boundary cycle differences match every published read observation.
|
Its instruction-boundary cycle differences match every published read observation.
|
||||||
Its actual emulated timer readings do **not** match those observations.
|
Its actual emulated timer readings now also match, after [ordered timer accesses](ordered-timer-access.md).
|
||||||
The difference isolates an existing timer-sampling limitation, not a new queue discrepancy.
|
The initial comparison isolated a bulk timer-sampling error, not a queue discrepancy.
|
||||||
No core timing behavior was changed for this comparison.
|
The original program, published observations, and measurement code remain unchanged.
|
||||||
|
|
||||||
This is not a run or a port of the upstream ROM.
|
This is not a run or a port of the upstream ROM.
|
||||||
The expected values are published measurement facts. The program builder and runner are original.
|
The expected values are published measurement facts. The program builder and runner are original.
|
||||||
|
|
@ -70,10 +70,10 @@ direnv exec . cargo test --locked -p gba-core --test prefetch_cancellation
|
||||||
direnv exec . cargo run --locked -p gba-demos --example prefetch_cancellation > /tmp/prefetch-cancellation.csv
|
direnv exec . cargo run --locked -p gba-demos --example prefetch_cancellation > /tmp/prefetch-cancellation.csv
|
||||||
```
|
```
|
||||||
|
|
||||||
**The example currently exits with status 1 because the timer comparisons fail.**
|
**The example now exits with status 0 because both comparisons match.**
|
||||||
That status is expected evidence of a known limit, not a passing hardware-timing test.
|
It previously exited with status 1, exposing the bulk timer-sampling error described below.
|
||||||
A matching instruction-boundary total cannot override a failed timer comparison.
|
A matching instruction-boundary total still cannot override a failed timer comparison.
|
||||||
Status 0 requires both comparisons to match for every case. Status 2 reports invalid arguments or execution errors.
|
Status 0 requires both comparisons to match for every case. Status 1 reports mismatches; status 2 reports invalid arguments or execution errors.
|
||||||
The example takes no arguments and needs no downloaded files, window, or external assembler.
|
The example takes no arguments and needs no downloaded files, window, or external assembler.
|
||||||
|
|
||||||
Use `--release` to repeat the same checks in a release build.
|
Use `--release` to repeat the same checks in a release build.
|
||||||
|
|
@ -90,11 +90,11 @@ The CSV columns are:
|
||||||
No timing correction is applied to `timer_delta`.
|
No timing correction is applied to `timer_delta`.
|
||||||
The runner does not hide, patch, or reinterpret failing timer values.
|
The runner does not hide, patch, or reinterpret failing timer values.
|
||||||
|
|
||||||
## Root cause of the timer mismatch
|
## Historical timer mismatch and correction
|
||||||
|
|
||||||
The opcode queue sees ordered source, data, and internal costs.
|
Before the correction, the opcode queue saw ordered source, data, and internal costs.
|
||||||
The timer device still updates only after the entire instruction completes.
|
The timer device updated only after the entire instruction completed.
|
||||||
Consequently, the timer load reads the old counter before its own source-fetch cost advances device time.
|
Consequently, the timer load read the old counter before its own source-fetch cost advanced device time.
|
||||||
|
|
||||||
The read run cancels prefetch with ROM data traffic.
|
The read run cancels prefetch with ROM data traffic.
|
||||||
Its subsequent timer load must fetch code from ROM again.
|
Its subsequent timer load must fetch code from ROM again.
|
||||||
|
|
@ -107,22 +107,24 @@ For a one-cycle multiply and WAITCNT `0x4000`:
|
||||||
| --- | ---: |
|
| --- | ---: |
|
||||||
| Published interval difference | 17 |
|
| Published interval difference | 17 |
|
||||||
| Machine-clock boundary difference | 17 |
|
| Machine-clock boundary difference | 17 |
|
||||||
| Actual timer-sample difference | 14 |
|
| Previous timer-sample difference | 14 |
|
||||||
|
| Current timer-sample difference | 17 |
|
||||||
| Timer-load source fetch in the read run | 8 |
|
| Timer-load source fetch in the read run | 8 |
|
||||||
| Timer-load source fetch in the control run | 5 |
|
| Timer-load source fetch in the control run | 5 |
|
||||||
|
|
||||||
The missing three cycles equal `8 - 5`.
|
The missing three cycles equal `8 - 5`.
|
||||||
For every compared case, the boundary-minus-timer discrepancy equals the final source-fetch cost difference.
|
Before the correction, each boundary-minus-timer discrepancy equaled the final source-fetch cost difference.
|
||||||
The sample instructions have identical data and internal costs, so those costs cancel in the subtraction.
|
The sample instructions have identical data and internal costs, so those costs cancel in the subtraction.
|
||||||
|
|
||||||
With prefetch disabled, the two final source-fetch costs match.
|
With prefetch disabled, the two final source-fetch costs match.
|
||||||
The timer and boundary differences then match each other, and the cancellation-specific extra cycle disappears.
|
The timer and boundary differences then match each other, and the cancellation-specific extra cycle disappears.
|
||||||
This negative control helps separate the sampling problem from timer subtraction or multiply decoding errors.
|
This negative control helps separate the sampling problem from timer subtraction or multiply decoding errors.
|
||||||
|
|
||||||
The regression tests explicitly mark the bulk-sampling expectation as a known gap.
|
Timer data accesses now sample staged device state at bus completion, including the source and data cycles.
|
||||||
Replace that expectation when timer I/O moves to ordered bus phases.
|
Timer writes use the same phase. Successful steps commit that state; failed steps discard it.
|
||||||
Do not make the probe pass by adding an offset to its loaded timer value.
|
The regression now requires actual timer values to match the unchanged published observations.
|
||||||
Timer startup/stop placement, other device events, and atomic failure behavior also need coverage before changing clock ownership.
|
No offset was added to the loaded value or diagnostic report.
|
||||||
|
See [ordered timer accesses](ordered-timer-access.md) for start/stop, IF, DMA, API isolation, and rollback coverage.
|
||||||
|
|
||||||
## Coverage and limits
|
## Coverage and limits
|
||||||
|
|
||||||
|
|
@ -139,5 +141,6 @@ The largest multiply delay here does not establish eight-halfword capacity behav
|
||||||
Live WAITCNT changes, DMA, HALT/STOP edges, and exact hardware timer startup delays remain outside this probe.
|
Live WAITCNT changes, DMA, HALT/STOP edges, and exact hardware timer startup delays remain outside this probe.
|
||||||
See [the queue model](gamepak-prefetch.md) for those separate limits.
|
See [the queue model](gamepak-prefetch.md) for those separate limits.
|
||||||
|
|
||||||
Next, move timer observations toward ordered bus-access phases with explicit rollback and no duplicate device advancement.
|
Ordered timer observations now retain rollback and avoid duplicate device advancement.
|
||||||
|
Absolute timer startup and control-edge validation remain necessary.
|
||||||
Independent full-buffer and page-boundary measurements remain necessary before claiming complete prefetch timing accuracy.
|
Independent full-buffer and page-boundary measurements remain necessary before claiming complete prefetch timing accuracy.
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,8 @@ Update this file when a feature lands or a limit is removed.
|
||||||
- Persistent fetch access kinds follow CPU data/internal work, compose with DMA resume, and reset after target refills.
|
- Persistent fetch access kinds follow CPU data/internal work, compose with DMA resume, and reset after target refills.
|
||||||
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
|
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
|
||||||
- Bit 14 enables a nominal eight-halfword opcode prefetch queue, with partial fetches, cancellation, and DMA/idle integration.
|
- Bit 14 enables a nominal eight-halfword opcode prefetch queue, with partial fetches, cancellation, and DMA/idle integration.
|
||||||
- An original ARM cancellation probe matches published read interval totals, while separately reporting incorrect bulk timer samples.
|
- Staged timer clocks follow CPU and DMA bus-completion phases, including timer IF reads/acknowledgements and failed-step rollback.
|
||||||
|
- The unchanged original ARM cancellation probe matches both published read interval totals and its unadjusted timer samples.
|
||||||
- Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing.
|
- Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing.
|
||||||
- Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words.
|
- Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words.
|
||||||
- Optional original BIOS replacement: minimal boot, SoftReset, selective RegisterRamReset, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, affine matrices, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters.
|
- Optional original BIOS replacement: minimal boot, SoftReset, selective RegisterRamReset, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, affine matrices, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters.
|
||||||
|
|
@ -121,7 +122,7 @@ CPU/bus costs are nominal estimates, not a cycle-accurate implementation.
|
||||||
The instruction buffer retains the next fetch kind; cold startup costs and per-access device scheduling remain unmodeled.
|
The instruction buffer retains the next fetch kind; cold startup costs and per-access device scheduling remain unmodeled.
|
||||||
Game Pak prefetch uses source-backed queue rules, not hardware-verified timing.
|
Game Pak prefetch uses source-backed queue rules, not hardware-verified timing.
|
||||||
Live WAITCNT transitions, page-boundary behavior, and exact DMA/idle ordering still need independent timing validation.
|
Live WAITCNT transitions, page-boundary behavior, and exact DMA/idle ordering still need independent timing validation.
|
||||||
Timer reads occur before the current instruction's device update. Prefetch cancellation probes expose the resulting sample error.
|
Timer data accesses follow ordered nominal bus phases. Other devices remain instruction-batched; exact timer startup and IRQ delays remain unverified.
|
||||||
Graphics support includes Mode 0–5 debug snapshots and HBlank row capture with sprites, mosaic, windows, and color effects.
|
Graphics support includes Mode 0–5 debug snapshots and HBlank row capture with sprites, mosaic, windows, and color effects.
|
||||||
Internal affine origins, vertical window flags, and vertical mosaic counters track line boundaries.
|
Internal affine origins, vertical window flags, and vertical mosaic counters track line boundaries.
|
||||||
Horizontal window comparators retain four-cycle event history.
|
Horizontal window comparators retain four-cycle event history.
|
||||||
|
|
@ -173,8 +174,8 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
|
||||||
Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.
|
Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.
|
||||||
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
|
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
|
||||||
2. Validate the active nominal Game Pak prefetch queue against independent timing tests.
|
2. Validate the active nominal Game Pak prefetch queue against independent timing tests.
|
||||||
The [cancellation probe](research/prefetch-cancellation.md) isolates a timer sampling gap despite matching nominal interval totals.
|
The [cancellation probe](research/prefetch-cancellation.md) now matches after [ordered timer accesses](research/ordered-timer-access.md).
|
||||||
Move timer observations to ordered bus phases without losing rollback or advancing devices twice.
|
Validate absolute timer startup/control edges and extend ordered observations to other devices without duplicate clocks or lost rollback.
|
||||||
Validate full-buffer restart, page boundaries, live WAITCNT transitions, and cold-start costs.
|
Validate full-buffer restart, page boundaries, live WAITCNT transitions, and cold-start costs.
|
||||||
See [prefetch behavior and evidence](research/gamepak-prefetch.md); exact bus arbitration and timer/IRQ delays remain separate work.
|
See [prefetch behavior and evidence](research/gamepak-prefetch.md); exact bus arbitration and timer/IRQ delays remain separate work.
|
||||||
3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.
|
3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.
|
||||||
|
|
|
||||||
|
|
@ -276,13 +276,17 @@ Integration tests check RAM/internal progress, ROM data stalls, branch and IRQ f
|
||||||
WAITCNT CPU/host/DMA writes, RAM/cart DMA, HALT/STOP, failed steps, and missing ROM bytes have separate checks.
|
WAITCNT CPU/host/DMA writes, RAM/cart DMA, HALT/STOP, failed steps, and missing ROM bytes have separate checks.
|
||||||
See [prefetch evidence and limits](research/gamepak-prefetch.md). These tests validate the nominal model, not physical-hardware accuracy.
|
See [prefetch evidence and limits](research/gamepak-prefetch.md). These tests validate the nominal model, not physical-hardware accuracy.
|
||||||
An [original cancellation probe](research/prefetch-cancellation.md) compares read/control intervals with pinned, published hardware observations.
|
An [original cancellation probe](research/prefetch-cancellation.md) compares read/control intervals with pinned, published hardware observations.
|
||||||
Its instruction-boundary totals match; its actual timer samples fail. The example returns nonzero for those failures.
|
Both its instruction-boundary totals and actual timer samples now match. Any mismatch still returns a nonzero status.
|
||||||
Regressions identify the missing source-fetch interval without applying a correction to the timer values.
|
Regressions retain the original program and published observations without correcting the loaded timer values.
|
||||||
Disabled-prefetch controls, delay bounds, completion PCs, and fresh-run determinism have separate checks.
|
Disabled-prefetch controls, delay bounds, completion PCs, and fresh-run determinism have separate checks.
|
||||||
Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns.
|
Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns.
|
||||||
Timed and untimed execution are compared across all 65,536 Thumb encodings with fixed initial registers.
|
Timed and untimed execution are compared across all 65,536 Thumb encodings with fixed initial registers.
|
||||||
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.
|
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.
|
||||||
Timer tests cover all prescalers, reload changes, start/stop behavior, byte writes, large clock advances, and cascades.
|
Timer tests cover all prescalers, reload changes, start/stop behavior, byte writes, large clock advances, and cascades.
|
||||||
|
Ordered timer tests cover ARM/Thumb sampling, coherent byte lanes, block-transfer phases, reload/control ordering, and prescaler retention.
|
||||||
|
They verify timer IF reads/acknowledgements, preservation of other IF bits, deferred IRQ delivery, DMA phases, and HALT/STOP ownership.
|
||||||
|
Failures before fetch and after partial data work must discard staged time and requests. CPU-only APIs must not advance timer devices.
|
||||||
|
Display-capture splits must not tick committed timers a second time. See [ordered timer accesses](research/ordered-timer-access.md).
|
||||||
Bulk timer advancement is compared against an independent cycle-by-cycle reference for each prescaler and cascade configuration.
|
Bulk timer advancement is compared against an independent cycle-by-cycle reference for each prescaler and cascade configuration.
|
||||||
Machine tests cover ARM/Thumb IRQ return, pending-request gating, acknowledgement, and the complete timer demo.
|
Machine tests cover ARM/Thumb IRQ return, pending-request gating, acknowledgement, and the complete timer demo.
|
||||||
Failed I/O block transfers are checked for partial register writes, hidden reload changes, and unintended IF acknowledgement.
|
Failed I/O block transfers are checked for partial register writes, hidden reload changes, and unintended IF acknowledgement.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue