feat(power): implement keypad-wake STOP and BIOS Stop service

Separate Running, Halt, and Stop states. Complete the entering instruction before freezing CPU, DMA, timers, display, and externally supplied clock advances. Wake STOP through the live enabled keypad condition without creating a clock-off IF request.

Implement original ARM SWI 0x03, preserve caller state, and expose zero-cycle StopIdle plus stopped frame-runner results. Retain DMA state, timer phase, and captured frames across sleep. Document keypad-only wake and unverified oscillator/edge timing.

Add core, BIOS, and demo regression tests; preserve BIOS-only access and failed-store isolation. Validated on Darwin arm64 with debug/release workspace and core/demo suites, Clippy, rustdoc, ten native window smoke tests, and terminal demos.
This commit is contained in:
0xrsydn 2026-10-05 18:27:35 +07:00
commit c836944c2e
20 changed files with 748 additions and 105 deletions

View file

@ -126,6 +126,7 @@ pub fn image() -> Vec<u8> {
(0, "soft_reset"),
(1, "register_ram_reset"),
(2, "halt"),
(3, "stop"),
(4, "intr_wait"),
(5, "vblank_wait"),
(6, "div"),
@ -152,6 +153,12 @@ pub fn image() -> Vec<u8> {
}
a.branch(14, "unsupported");
a.label("stop");
a.literal(1, HALTCNT);
a.emit(0xe3a0_0080); // MOV r0,#STOP
a.emit(0xe5c1_0000); // STRB r0,[r1]; enters STOP after this instruction
a.branch(14, "return");
a.label("halt");
a.literal(1, HALTCNT);
a.emit(0xe3a0_0000); // MOV r0,#0

View file

@ -20,12 +20,21 @@ impl Keypad {
self.control
}
pub(crate) fn update_stopped_buttons(&mut self, buttons: Buttons) {
self.buttons = buttons;
}
pub(crate) fn set_buttons(&mut self, buttons: Buttons) -> bool {
self.buttons = buttons;
self.sample()
}
pub(crate) fn write_control(&mut self, value: u16) -> bool {
self.update_control(value);
self.sample()
}
pub(crate) fn update_control(&mut self, value: u16) {
let value = value & 0xc3ff;
let newly_selected = (value & !self.control) & 0x03ff;
// A newly selected held key must not be hidden by an earlier AND sample.
@ -33,27 +42,34 @@ impl Keypad {
*previous &= !newly_selected;
}
self.control = value;
self.sample()
}
fn sample(&mut self) -> bool {
/// Live enabled condition for asynchronous STOP wake; no IF/history update.
pub(crate) fn wake_condition(&self) -> bool {
if self.control & 0x4000 == 0 {
return false;
}
let selected = self.control & 0x03ff;
let pressed = self.buttons.bits();
let all = self.control & 0x8000 != 0;
let matched = if all {
if all {
pressed & selected == selected
} else {
pressed & selected != 0
};
if !matched {
}
}
fn sample(&mut self) -> bool {
if self.control & 0x4000 == 0 {
return false;
}
if !self.wake_condition() {
self.last_match = None;
return false;
}
let pressed = self.buttons.bits();
let previous = self.last_match.replace(pressed);
!all || previous != Some(pressed)
self.control & 0x8000 == 0 || previous != Some(pressed)
}
}

View file

@ -123,6 +123,14 @@ impl Timer {
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum PowerState {
#[default]
Running,
Halt,
Stop,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct Io {
timers: [Timer; 4],
@ -142,7 +150,7 @@ pub(crate) struct Io {
display: Display,
dma: Dma,
postflg: bool,
halted: bool,
power: PowerState,
}
impl Io {
@ -264,9 +272,12 @@ impl Io {
IME if address & 1 == 0 => self.master_enable = value & 1 != 0,
POSTFLG if address == POSTFLG => self.postflg = value & 1 != 0,
POSTFLG => {
// The bus has already rejected STOP and enforced CPU write access.
debug_assert_eq!(value & 0x80, 0);
self.halted = true;
// The bus has enforced BIOS-only CPU access. Low seven bits are ignored.
self.power = if value & 0x80 == 0 {
PowerState::Halt
} else {
PowerState::Stop
};
}
_ => {} // Unused I/O bytes ignore writes.
}
@ -274,13 +285,23 @@ impl Io {
}
pub(crate) fn halted(&self) -> bool {
self.halted
self.power == PowerState::Halt
}
pub(crate) fn stopped(&self) -> bool {
self.power == PowerState::Stop
}
fn wake_if_requested(&mut self) {
// Wake-up ignores IME and CPSR.I. It does not acknowledge IF.
if self.enable & self.pending != 0 {
self.halted = false;
// Both wake paths ignore IME and CPSR.I and leave IF unchanged.
// STOP uses the live keypad condition, not a stale IF latch.
let wake = match self.power {
PowerState::Running => false,
PowerState::Halt => self.enable & self.pending != 0,
PowerState::Stop => self.enable & (1 << 12) != 0 && self.keypad.wake_condition(),
};
if wake {
self.power = PowerState::Running;
}
}
@ -298,7 +319,10 @@ impl Io {
}
pub(crate) fn set_buttons(&mut self, buttons: Buttons) {
if self.keypad.set_buttons(buttons) {
if self.stopped() {
// System clock is off: update wake inputs without latching IF/history.
self.keypad.update_stopped_buttons(buttons);
} else if self.keypad.set_buttons(buttons) {
self.pending |= 1 << 12;
}
self.wake_if_requested();
@ -318,8 +342,12 @@ impl Io {
touched = true;
}
}
if touched && self.keypad.write_control(control) {
self.pending |= 1 << 12;
if touched {
if self.stopped() {
self.keypad.update_control(control);
} else if self.keypad.write_control(control) {
self.pending |= 1 << 12;
}
}
self.wake_if_requested();
}

View file

@ -19,6 +19,8 @@ pub enum StepKind {
},
/// Advance device clocks to the next event while the CPU stays paused.
HaltIdle,
/// System clock is off; no CPU, DMA, or device progress. Supply host input to wake.
StopIdle,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -50,6 +52,7 @@ pub enum FrameRunError {
Cpu(CpuError),
Dma(DmaError),
StepLimit(usize),
Stopped,
}
impl fmt::Display for FrameRunError {
@ -57,6 +60,7 @@ impl fmt::Display for FrameRunError {
match self {
Self::Cpu(error) => write!(f, "{error}"),
Self::Dma(error) => write!(f, "{error}"),
Self::Stopped => write!(f, "machine is in STOP; no VBlank can occur until wake-up"),
Self::StepLimit(limit) => write!(f, "no VBlank entry within {limit} machine steps"),
}
}
@ -67,13 +71,13 @@ impl Error for FrameRunError {
match self {
Self::Cpu(error) => Some(error),
Self::Dma(error) => Some(error),
Self::StepLimit(_) => None,
Self::StepLimit(_) | Self::Stopped => None,
}
}
}
/// Owns the CPU and memory bus. Use this instead of Cpu::step to advance device
/// clocks, execute DMA, honor HALT, and deliver interrupts. No audio or STOP yet.
/// clocks, execute DMA, honor HALT/STOP, and deliver interrupts. No audio yet.
pub struct Machine {
cpu: Cpu,
memory: Memory,
@ -106,6 +110,10 @@ impl Machine {
self.memory.halted()
}
pub fn stopped(&self) -> bool {
self.memory.stopped()
}
pub fn cycles(&self) -> u64 {
self.memory.cycles()
}
@ -114,13 +122,17 @@ impl Machine {
/// Stops at the first instruction, DMA-unit, or HALT-idle boundary after the event.
/// DMA requested by that event may still be pending. Prior successful
/// steps remain committed on error or limit exhaustion. Zero steps always fails.
/// StopIdle returns FrameRunError::Stopped immediately; no VBlank can advance.
pub fn run_until_vblank(&mut self, max_steps: usize) -> Result<usize, FrameRunError> {
let previous = self.memory.display_position().vblanks;
for steps in 1..=max_steps {
self.step().map_err(|error| match error {
let kind = self.step().map_err(|error| match error {
MachineError::Cpu(error) => FrameRunError::Cpu(error),
MachineError::Dma(error) => FrameRunError::Dma(error),
})?;
if kind == StepKind::StopIdle {
return Err(FrameRunError::Stopped);
}
if self.memory.display_position().vblanks != previous {
return Ok(steps);
}
@ -133,11 +145,13 @@ impl Machine {
self.last_timing
}
/// Service one ready DMA unit before sampling IRQ or executing CPU code.
/// STOP returns StopIdle with zero timing before DMA, IRQ, or CPU work.
/// Otherwise, service one ready DMA unit before sampling IRQ or CPU code.
/// Lower channel numbers take priority; higher-priority requests can preempt
/// between units. The CPU and its registers remain paused throughout DMA.
/// If HALT is still waiting, advance to the next display edge or timer overflow.
/// Wake-up ignores IME and CPSR.I; IRQ delivery still respects both masks.
/// HALT wakes on IE & IF; STOP wakes on the enabled live keypad condition.
/// Both ignore IME and CPSR.I; IRQ delivery still respects both masks.
/// Sample IRQ before executing the next instruction. GBA devices do not
/// generate FIQ. IRQ entry consumes a separate step and does not clear IF.
/// CPU/DMA work uses nominal costs; HALT advances exactly to the next device event.
@ -146,6 +160,10 @@ impl Machine {
/// CPU/DMA diagnostics leave CPU, devices, and the clock unchanged for this step.
/// Row-capture diagnostics are deferred to Memory::present_frame instead.
pub fn step(&mut self) -> Result<StepKind, MachineError> {
if self.memory.stopped() {
self.last_timing = StepTiming::default();
return Ok(StepKind::StopIdle);
}
if let Some((channel, timing)) = self.memory.step_dma().map_err(MachineError::Dma)? {
self.last_timing = timing;
return Ok(StepKind::Dma { channel });
@ -169,7 +187,7 @@ impl Machine {
.map_err(MachineError::Cpu)?,
)
};
self.memory.advance_cycles(timing.total());
self.memory.complete_cpu_step(timing.total());
self.last_timing = timing;
Ok(kind)
}

View file

@ -24,13 +24,11 @@ pub enum MemoryError {
Unaligned(u32),
RomTooLarge(usize),
InvalidBiosSize(usize),
UnsupportedStop,
}
impl fmt::Display for MemoryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedStop => write!(f, "HALTCNT STOP mode is not implemented"),
Self::Unmapped(address) => write!(f, "unmapped memory at {address:#010x}"),
Self::ReadOnly(address) => write!(f, "read-only memory at {address:#010x}"),
Self::Unaligned(address) => write!(f, "unaligned memory access at {address:#010x}"),
@ -104,15 +102,28 @@ impl Memory {
Ok(memory)
}
/// Total supplied clock cycles, wrapping at u64::MAX. Reads/writes alone
/// consume no cycles. Machine stepping or the caller must advance time.
/// Total elapsed emulated clock cycles, wrapping at u64::MAX. Reads/writes
/// alone consume no cycles. STOP discards externally supplied clock advances.
pub fn cycles(&self) -> u64 {
self.cycles
}
/// Advance devices and optional row capture. Multiple unserviced DMA requests
/// coalesce. No CPU or DMA work is executed by this method.
/// coalesce. No CPU or DMA work is executed by this method. STOP freezes all
/// progress: supplied cycles are ignored until an external wake condition.
pub fn advance_cycles(&mut self, cycles: u32) {
if !self.stopped() {
self.advance_running_cycles(cycles);
}
}
/// Finish the instruction/IRQ entry that started while running. A STOP store
/// still pays its full nominal cost before later steps freeze the system.
pub(crate) fn complete_cpu_step(&mut self, cycles: u32) {
self.advance_running_cycles(cycles);
}
fn advance_running_cycles(&mut self, cycles: u32) {
if cycles != 0 && !self.sprite_pipeline.started {
// Synthetic reset begins on row0, with no preceding row227 to prepare it.
self.prepare_sprite_row(0);
@ -296,6 +307,11 @@ impl Memory {
self.io.halted()
}
/// Whether STOP is waiting for an enabled live keypad wake condition.
pub fn stopped(&self) -> bool {
self.io.stopped()
}
pub(crate) fn next_event_cycles(&self) -> u32 {
self.io.next_event_cycles()
}
@ -325,8 +341,8 @@ impl Memory {
}
/// Replace the current pressed-button state. This does not advance time.
/// Sample KEYCNT and latch a matching keypad IRQ immediately, without cycles.
/// This can wake HALT; CPU IRQ delivery occurs on a later Machine::step.
/// While running, sample KEYCNT and latch a matching IRQ without cycles.
/// This can wake HALT. In STOP, input can wake the system without setting IF.
pub fn set_buttons(&mut self, buttons: Buttons) {
self.io.set_buttons(buttons);
}
@ -414,7 +430,7 @@ impl Memory {
address: u32,
bytes: [u8; N],
) -> Result<(), MemoryError> {
let index = self.validate_write(address, &bytes)?;
let index = self.write_index::<N>(address)?;
self.record_access(
address,
match N {
@ -473,22 +489,6 @@ impl Memory {
Ok(())
}
fn validate_write<const N: usize>(
&self,
address: u32,
bytes: &[u8; N],
) -> Result<usize, MemoryError> {
let index = self.write_index::<N>(address)?;
if self.can_write_power_control() {
for (offset, byte) in bytes.iter().enumerate() {
if address + offset as u32 == HALTCNT && byte & 0x80 != 0 {
return Err(MemoryError::UnsupportedStop);
}
}
}
Ok(index)
}
fn write_index<const N: usize>(&self, address: u32) -> Result<usize, MemoryError> {
if address & (N as u32 - 1) != 0 {
return Err(MemoryError::Unaligned(address));
@ -519,8 +519,8 @@ impl Memory {
/// leave a partial block store. This is not a model of hardware data aborts.
/// Mapped I/O reads currently have no side effects; writes cannot fail after validation.
pub(crate) fn write_words(&mut self, writes: &[(u32, u32)]) -> Result<(), MemoryError> {
for &(address, value) in writes {
self.validate_write(address, &value.to_le_bytes())?;
for &(address, _) in writes {
self.write_index::<4>(address)?;
}
for &(address, value) in writes {
// The map cannot change between validation and these writes.

View file

@ -32,6 +32,8 @@ mod ram_reset;
mod reset;
#[path = "bios/run_length.rs"]
mod run_length;
#[path = "bios/stop.rs"]
mod stop;
fn words(code: &[u32]) -> Vec<u8> {
code.iter().flat_map(|word| word.to_le_bytes()).collect()
@ -231,7 +233,7 @@ fn vblank_wait_halts_dispatches_irq_and_returns_through_original_code() {
#[test]
fn unsupported_services_fail_at_an_explicit_instruction_trap() {
for service in [3, 0x0d, 0x19, 0xff] {
for service in [0x0d, 0x19, 0xff] {
let (mut machine, _) = call(service, false, [0, 0, 0]);
let error = (0..100)
.find_map(|_| machine.step().err())

View file

@ -0,0 +1,80 @@
use super::*;
use gba_core::{input::Buttons, io::KEYCNT};
const KEY_IRQ: u16 = 1 << 12;
#[test]
fn arm_thumb_stop_waits_for_keypad_and_restores_user_system_callers() {
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
for ime in [0, 1] {
let (mut m, pc) = call_status(3, thumb, [0x1234, 0x5678, 0x9abc], status);
m.memory_mut().write16(KEYCNT, 0x4001).unwrap();
m.memory_mut().write16(IE, KEY_IRQ).unwrap();
m.memory_mut().write16(IME, ime).unwrap();
m.memory_mut()
.write32(bios::SVC_STACK - 32, 0x1234_5678)
.unwrap();
let before = m.cpu().clone();
for _ in 0..200 {
if m.stopped() {
break;
}
m.step().unwrap();
}
assert!(m.stopped());
assert!(!m.halted());
let cycles = m.cycles();
let asleep = m.cpu().clone();
for _ in 0..3 {
assert_eq!(m.step().unwrap(), StepKind::StopIdle);
}
assert_eq!(m.cycles(), cycles);
assert_eq!(m.cpu(), &asleep);
m.memory_mut().set_buttons(Buttons::from_bits(1));
assert!(!m.stopped());
assert_eq!(m.memory().read16(IF).unwrap(), 0);
reach(&mut m, pc, 100);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(m.memory().read16(IE).unwrap(), KEY_IRQ);
assert_eq!(m.memory().read16(IME).unwrap(), ime);
assert_eq!(m.memory().read16(KEYCNT).unwrap(), 0x4001);
assert_eq!(
m.memory().read32(bios::SVC_STACK - 32).unwrap(),
0x1234_5678
);
}
}
}
}
#[test]
fn stop_service_returns_immediately_for_live_matching_input() {
for thumb in [false, true] {
let (mut m, pc) = call(3, thumb, [0, 0, 0]);
m.memory_mut().write16(KEYCNT, 0xc003).unwrap();
m.memory_mut().write16(IE, KEY_IRQ).unwrap();
m.memory_mut().set_buttons(Buttons::from_bits(3));
m.memory_mut().write16(IF, KEY_IRQ).unwrap();
finish(&mut m, pc);
assert!(!m.stopped());
assert_eq!(m.memory().read16(IF).unwrap(), 0);
}
}
#[test]
fn stop_service_with_no_wake_source_reports_stopped_instead_of_advancing_frames() {
let (mut m, _) = call(3, false, [0, 0, 0]);
assert_eq!(
m.run_until_vblank(1000),
Err(gba_core::machine::FrameRunError::Stopped)
);
assert!(m.stopped());
let cycles = m.cycles();
assert_eq!(
m.run_until_vblank(1000),
Err(gba_core::machine::FrameRunError::Stopped)
);
assert_eq!(m.cycles(), cycles);
}

View file

@ -554,40 +554,35 @@ fn halt_store_finishes_and_can_wake_during_its_own_cycles() {
}
#[test]
fn stop_diagnostics_validate_entire_write_before_postflg_or_halt_changes() {
let mut bus = Memory::new(vec![]).unwrap();
for value in [0x80, 0xff] {
assert_eq!(
bus.write8(HALTCNT, value),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(
bus.write16(POSTFLG, u16::from(value) << 8 | 1),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(
bus.write32(POSTFLG, u32::from(value) << 8 | 1),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(bus.read32(POSTFLG).unwrap(), 0);
assert!(!bus.halted());
assert_eq!(bus.cycles(), 0);
fn stop_writes_support_all_widths_without_becoming_halt() {
for value in 0x80..=0xff {
for width in [1, 2, 4] {
let mut bus = Memory::new(vec![]).unwrap();
match width {
1 => bus.write8(HALTCNT, value).unwrap(),
2 => bus.write16(POSTFLG, u16::from(value) << 8 | 1).unwrap(),
_ => bus.write32(POSTFLG, u32::from(value) << 8 | 1).unwrap(),
}
assert_eq!(bus.read32(POSTFLG).unwrap(), u32::from(width != 1));
assert!(bus.stopped());
assert!(!bus.halted());
assert_eq!(bus.cycles(), 0);
}
}
}
#[test]
fn bios_stop_error_preserves_cpu_timing_and_clears_access_context() {
fn bios_stop_store_finishes_before_freezing_and_access_context_is_cleared() {
let mut machine = store_machine(0x100, 0xe581_0000, POSTFLG, 0x8001, true);
let before = machine.cpu().clone();
assert_eq!(
machine.step(),
Err(MachineError::Cpu(MemoryError::UnsupportedStop.into()))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.last_timing(), StepTiming::default());
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().pc(), 0x10c);
assert_eq!(machine.cycles(), u64::from(machine.last_timing().total()));
assert!(machine.cycles() > 0);
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 1);
assert!(machine.stopped());
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::StopIdle);
assert_eq!(machine.last_timing(), StepTiming::default());
// A failed non-BIOS instruction must also clear its access context.
let mut machine = store_machine(ROM_START, 0xe581_0000, 0x0400_0058, 0, true);
assert!(machine.step().is_err());
@ -604,6 +599,7 @@ fn non_bios_stop_write_is_ignored_not_a_diagnostic() {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert!(!machine.halted());
assert_eq!(machine.memory().read32(POSTFLG).unwrap(), 0);
assert!(!machine.stopped());
}
}

View file

@ -0,0 +1,402 @@
use gba_core::{
cpu::{Cpu, CpuError, InstructionSet},
display::{CYCLES_PER_FRAME, VBLANK_START},
dma::{DmaError, DMA_BASE},
input::Buttons,
io::{DISPCNT, HALTCNT, IE, IF, IME, KEYCNT, POSTFLG, TIMER_BASE},
machine::{FrameRunError, Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
timing::StepTiming,
video::Framebuffer,
};
const KEY_IRQ: u16 = 1 << 12;
fn words(code: &[u32]) -> Vec<u8> {
code.iter().flat_map(|word| word.to_le_bytes()).collect()
}
fn machine(masked: bool) -> Machine {
let mut bios = vec![0; BIOS_SIZE];
bios[..12].copy_from_slice(&words(&[
if masked { 0xe321_f09f } else { 0xe321_f01f },
0xe280_0001,
0xeaff_fffe,
]));
let mut m = Machine::new(Cpu::new(0), Memory::with_bios(vec![], bios).unwrap());
m.step().unwrap();
m
}
fn enable_keypad(m: &mut Memory) {
m.write16(KEYCNT, 0x4001).unwrap();
m.write16(IE, KEY_IRQ).unwrap();
}
fn stop(m: &mut Memory) {
m.write8(HALTCNT, 0x80).unwrap();
}
fn press(m: &mut Memory, buttons: u16) {
m.set_buttons(Buttons::from_bits(buttons));
}
fn pending_timer(m: &mut Memory) {
m.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
m.advance_cycles(1);
m.write16(TIMER_BASE + 2, 0).unwrap();
assert_ne!(m.read16(IF).unwrap() & 8, 0);
}
#[test]
fn stop_idle_does_not_fetch_or_advance_any_clock() {
let mut m = Machine::new(Cpu::new(0xdead_beec), Memory::new(vec![]).unwrap());
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
enable_keypad(m.memory_mut());
stop(m.memory_mut());
let cpu = m.cpu().clone();
let position = m.memory().display_position();
for _ in 0..100 {
assert_eq!(m.step().unwrap(), StepKind::StopIdle);
assert_eq!(m.cpu(), &cpu);
assert_eq!(m.last_timing(), StepTiming::default());
assert_eq!(m.cycles(), 0);
assert_eq!(m.memory().display_position(), position);
assert_eq!(m.memory().read16(TIMER_BASE).unwrap(), 0xfff0);
assert_eq!(m.memory().read16(IF).unwrap(), 0);
}
for cycles in [0, 1, VBLANK_START, CYCLES_PER_FRAME, u32::MAX] {
m.memory_mut().advance_cycles(cycles);
assert_eq!(m.cycles(), 0);
assert_eq!(m.memory().display_position(), position);
assert_eq!(m.memory().read16(TIMER_BASE).unwrap(), 0xfff0);
}
assert!(m.stopped());
assert!(!m.halted());
}
#[test]
fn keypad_wake_ignores_ime_and_cpu_mask_and_does_not_latch_if() {
for masked in [false, true] {
for ie in [0, KEY_IRQ] {
for ime in [0, 1] {
let mut m = machine(masked);
enable_keypad(m.memory_mut());
m.memory_mut().write16(IE, ie).unwrap();
m.memory_mut().write16(IME, ime).unwrap();
stop(m.memory_mut());
let before = m.cpu().clone();
let cycles = m.cycles();
press(m.memory_mut(), 1);
assert_eq!(m.cpu(), &before);
assert_eq!(m.cycles(), cycles);
assert_eq!(m.stopped(), ie == 0);
assert_eq!(m.memory().read16(IF).unwrap(), 0);
assert_eq!(
m.step().unwrap(),
if ie == 0 {
StepKind::StopIdle
} else {
StepKind::Instruction
}
);
}
}
}
}
#[test]
fn live_keypad_wake_uses_enable_selection_and_or_and_conditions() {
for (control, inputs, wakes) in [
(0x0001, 1, false),
(0x8001, 1, false),
(0x4001, 2, false),
(0x4003, 2, true),
(0xc003, 1, false),
(0xc003, 7, true),
(0x4000, 0x3ff, false),
(0xc000, 0, true),
(0x4100, 0x100, true),
(0xc200, 0x200, true),
] {
let mut m = machine(false);
enable_keypad(m.memory_mut());
stop(m.memory_mut());
// These are debug/host writes; a stopped CPU cannot execute stores.
m.memory_mut().write16(KEYCNT, control).unwrap();
press(m.memory_mut(), inputs);
assert_eq!(
m.stopped(),
!wakes,
"control={control:#x}, inputs={inputs:#x}"
);
// Empty AND can wake on the control write before the later input sample.
if control != 0xc000 {
assert_eq!(m.memory().read16(IF).unwrap(), 0);
}
}
}
#[test]
fn stale_keypad_and_timer_if_flags_cannot_wake_stop() {
let mut m = machine(false);
pending_timer(m.memory_mut());
enable_keypad(m.memory_mut());
press(m.memory_mut(), 1);
press(m.memory_mut(), 0);
m.memory_mut().write16(IE, KEY_IRQ | 8).unwrap();
m.memory_mut().write16(IME, 1).unwrap();
assert_eq!(m.memory().read16(IF).unwrap(), KEY_IRQ | 8);
stop(m.memory_mut());
assert!(m.stopped());
assert_eq!(m.step().unwrap(), StepKind::StopIdle);
press(m.memory_mut(), 1);
assert!(!m.stopped());
assert_eq!(m.memory().read16(IF).unwrap(), KEY_IRQ | 8);
assert_eq!(m.step().unwrap(), StepKind::IrqEntry); // Old pending request, not a STOP-generated IF.
}
#[test]
fn already_held_matching_keys_prevent_stop_even_after_and_request_was_acknowledged() {
let mut m = machine(false);
m.memory_mut().write16(KEYCNT, 0xc003).unwrap();
m.memory_mut().write16(IE, KEY_IRQ).unwrap();
press(m.memory_mut(), 3);
m.memory_mut().write16(IF, KEY_IRQ).unwrap();
press(m.memory_mut(), 3); // AND history suppresses another IF request.
assert_eq!(m.memory().read16(IF).unwrap(), 0);
stop(m.memory_mut());
assert!(!m.stopped()); // Wake depends on live keys, not AND polling history.
assert_eq!(m.memory().read16(IF).unwrap(), 0);
}
#[test]
fn enabling_ie_can_wake_held_keys_without_if_and_release_does_not_reenter_stop() {
let mut m = machine(false);
m.memory_mut().write16(KEYCNT, 0x4001).unwrap();
stop(m.memory_mut());
press(m.memory_mut(), 1);
assert!(m.stopped());
m.memory_mut().write16(IE, KEY_IRQ).unwrap();
assert!(!m.stopped());
assert_eq!(m.memory().read16(IF).unwrap(), 0);
press(m.memory_mut(), 0);
assert!(!m.stopped());
assert_eq!(m.step().unwrap(), StepKind::Instruction);
}
#[test]
fn stopped_input_does_not_consume_the_first_running_and_sample() {
let mut m = machine(false);
m.memory_mut().write16(KEYCNT, 0xc001).unwrap();
m.memory_mut().write16(IE, KEY_IRQ).unwrap();
stop(m.memory_mut());
press(m.memory_mut(), 1);
assert!(!m.stopped());
assert_eq!(m.memory().read16(IF).unwrap(), 0);
press(m.memory_mut(), 1);
assert_eq!(m.memory().read16(IF).unwrap(), KEY_IRQ);
}
#[test]
fn dma_is_frozen_before_work_or_diagnostics_and_resumes_before_cpu_after_wake() {
for invalid in [false, true] {
let mut m = machine(false);
enable_keypad(m.memory_mut());
m.memory_mut().write32(0x0200_0000, 0x1234_5678).unwrap();
m.memory_mut().write32(DMA_BASE, 0x0200_0000).unwrap();
m.memory_mut().write32(DMA_BASE + 4, 0x0300_0000).unwrap();
m.memory_mut()
.write32(
DMA_BASE + 8,
if invalid { 0xb400_0001 } else { 0x8400_0001 },
)
.unwrap();
stop(m.memory_mut());
let cpu = m.cpu().clone();
let cycles = m.cycles();
for _ in 0..4 {
assert_eq!(m.step().unwrap(), StepKind::StopIdle);
}
assert_eq!(m.cpu(), &cpu);
assert_eq!(m.cycles(), cycles);
assert_eq!(m.memory().read32(0x0300_0000).unwrap(), 0);
press(m.memory_mut(), 1);
if invalid {
assert_eq!(
m.step(),
Err(MachineError::Dma(DmaError::UnsupportedControl {
channel: 0,
control: 0xb400
}))
);
} else {
assert_eq!(m.step().unwrap(), StepKind::Dma { channel: 0 });
assert_eq!(m.cpu(), &cpu);
assert_eq!(m.memory().read32(0x0300_0000).unwrap(), 0x1234_5678);
assert_eq!(m.step().unwrap(), StepKind::Instruction);
}
}
}
#[test]
fn stopped_clock_keeps_timer_phase_display_phase_and_video_memory() {
let mut frozen = Memory::new(vec![]).unwrap();
let mut reference = Memory::new(vec![]).unwrap();
for m in [&mut frozen, &mut reference] {
enable_keypad(m);
m.write32(TIMER_BASE, 0x0081_1000).unwrap(); // /64, with a partial prescaler period.
m.write16(DISPCNT, 0x80).unwrap();
m.write32(0x0600_0000, 0x1234_5678).unwrap();
m.advance_cycles(63);
}
stop(&mut frozen);
frozen.advance_cycles(u32::MAX);
press(&mut frozen, 1);
assert!(!frozen.stopped());
assert_eq!(frozen.read16(IF).unwrap(), 0);
for cycles in [1, 63, 1, 1000, CYCLES_PER_FRAME] {
frozen.advance_cycles(cycles);
reference.advance_cycles(cycles);
assert_eq!(frozen.cycles(), reference.cycles());
assert_eq!(frozen.display_position(), reference.display_position());
assert_eq!(
frozen.read16(TIMER_BASE).unwrap(),
reference.read16(TIMER_BASE).unwrap()
);
}
assert_eq!(frozen.read32(0x0600_0000).unwrap(), 0x1234_5678);
}
#[test]
fn frame_runner_reports_stop_without_spinning_and_can_resume_after_input() {
let mut m = machine(false);
enable_keypad(m.memory_mut());
stop(m.memory_mut());
let cycles = m.cycles();
assert_eq!(m.run_until_vblank(0), Err(FrameRunError::StepLimit(0)));
assert_eq!(m.run_until_vblank(usize::MAX), Err(FrameRunError::Stopped));
assert_eq!(m.cycles(), cycles);
press(m.memory_mut(), 1);
assert!(m.run_until_vblank(100_000).is_ok());
assert_eq!(m.memory().display_position().vblanks, 1);
}
#[test]
fn stop_preserves_a_completed_captured_frame() {
let mut m = machine(false);
enable_keypad(m.memory_mut());
m.memory_mut().write16(DISPCNT, 0x80).unwrap();
m.memory_mut().set_scanline_rendering(true);
m.memory_mut().advance_cycles(CYCLES_PER_FRAME * 2);
let mut before = Framebuffer::default();
assert!(m.memory().present_frame(&mut before).unwrap());
let position = m.memory().display_position();
stop(m.memory_mut());
for _ in 0..10 {
assert_eq!(m.step().unwrap(), StepKind::StopIdle);
}
m.memory_mut().advance_cycles(CYCLES_PER_FRAME);
let mut after = Framebuffer::default();
assert!(m.memory().present_frame(&mut after).unwrap());
assert_eq!(m.memory().display_position(), position);
assert_eq!(before.pixels(), after.pixels());
}
#[test]
fn arm_and_thumb_stop_stores_finish_with_normal_cycles_before_freezing() {
for thumb in [false, true] {
let entry = 0x100;
let (program, setup_steps, next_pc) = if thumb {
let mut bytes = words(&[0xe28f_0001, 0xe12f_ff10]);
for op in [0x2080_u16, 0x4901, 0x7008, 0x3201] {
bytes.extend(op.to_le_bytes());
}
bytes.extend(HALTCNT.to_le_bytes());
(bytes, 4, entry + 14)
} else {
(
words(&[0xe59f_1008, 0xe3a0_0080, 0xe5c1_0000, 0xe282_2001, HALTCNT]),
2,
entry + 12,
)
};
let mut bios = vec![0; BIOS_SIZE];
bios[entry as usize..entry as usize + program.len()].copy_from_slice(&program);
let mut mem = Memory::with_bios(vec![], bios).unwrap();
let mut cpu = Cpu::new(entry);
for _ in 0..setup_steps {
cpu.step(&mut mem).unwrap();
}
enable_keypad(&mut mem);
mem.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
let mut m = Machine::new(cpu, mem);
assert_eq!(m.step().unwrap(), StepKind::Instruction);
assert_eq!(m.cpu().pc(), next_pc);
assert_eq!(
m.cpu().instruction_set(),
if thumb {
InstructionSet::Thumb
} else {
InstructionSet::Arm
}
);
assert!(m.stopped());
assert_eq!(m.cycles(), u64::from(m.last_timing().total()));
assert!(m.cycles() > 0);
assert_eq!(m.memory().read16(IF).unwrap(), 8); // Last instruction's clocks completed.
let cpu = m.cpu().clone();
assert_eq!(m.step().unwrap(), StepKind::StopIdle);
assert_eq!(m.cpu(), &cpu);
press(m.memory_mut(), 1);
assert_eq!(m.step().unwrap(), StepKind::Instruction);
assert_eq!(m.cpu().registers()[2], 1);
}
}
#[test]
fn failed_block_store_cannot_partially_enter_stop_or_set_postflg() {
let mut bios = vec![0; BIOS_SIZE];
bios[..24].copy_from_slice(&words(&[
0xe59f_0008,
0xe59f_1008,
0xe8a0_0006,
0xeaff_fffe,
POSTFLG,
0x8001,
]));
let mut mem = Memory::with_bios(vec![], bios).unwrap();
let mut cpu = Cpu::new(0);
cpu.step(&mut mem).unwrap();
cpu.step(&mut mem).unwrap();
let mut m = Machine::new(cpu, mem);
let before = m.cpu().clone();
assert_eq!(
m.step(),
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(
POSTFLG + 4
))))
);
assert_eq!(m.cpu(), &before);
assert!(!m.stopped());
assert!(!m.halted());
assert_eq!(m.memory().read8(POSTFLG).unwrap(), 0);
assert_eq!(m.cycles(), 0);
assert_eq!(m.last_timing(), StepTiming::default());
}
#[test]
fn non_bios_thumb_stop_writes_are_ignored() {
let mut rom = words(&[0xe28f_0001, 0xe12f_ff10]);
for op in [0x2080_u16, 0x4901, 0x7008, 0x3201] {
rom.extend(op.to_le_bytes());
}
rom.extend(HALTCNT.to_le_bytes());
let mut m = Machine::new(Cpu::new(ROM_START), Memory::new(rom).unwrap());
for _ in 0..6 {
assert_eq!(m.step().unwrap(), StepKind::Instruction);
}
assert!(!m.stopped());
assert!(!m.halted());
assert_eq!(m.cpu().registers()[2], 1);
}

View file

@ -29,6 +29,7 @@ pub enum GraphicsError {
Video(VideoError),
FrameTimeout,
FrameUnavailable,
Stopped,
}
impl fmt::Display for GraphicsError {
@ -38,6 +39,7 @@ impl fmt::Display for GraphicsError {
Self::Dma(error) => write!(f, "{error}"),
Self::Memory(error) => write!(f, "{error}"),
Self::Video(error) => write!(f, "{error}"),
Self::Stopped => write!(f, "graphics program is in STOP; waiting for host input"),
Self::FrameUnavailable => write!(f, "no complete scanline frame is available"),
Self::FrameTimeout => write!(
f,
@ -54,7 +56,7 @@ impl Error for GraphicsError {
Self::Dma(error) => Some(error),
Self::Memory(error) => Some(error),
Self::Video(error) => Some(error),
Self::FrameTimeout | Self::FrameUnavailable => None,
Self::FrameTimeout | Self::FrameUnavailable | Self::Stopped => None,
}
}
}
@ -93,6 +95,7 @@ impl GraphicsDemo {
/// The first call also runs startup under forced blank and primes the VBlank wait.
/// Emulated CPU code and DMA write display registers, VRAM, and demo state.
/// Errors retain completed machine steps; the output buffer is not updated.
/// STOP returns GraphicsError::Stopped promptly so the caller can supply input.
pub fn frame(
&mut self,
buttons: Buttons,
@ -123,7 +126,11 @@ impl GraphicsDemo {
if steps == FRAME_STEP_LIMIT {
return Err(GraphicsError::FrameTimeout);
}
self.machine.step().map_err(GraphicsError::from)?;
if self.machine.step().map_err(GraphicsError::from)?
== gba_core::machine::StepKind::StopIdle
{
return Err(GraphicsError::Stopped);
}
steps += 1;
}
steps += self.wait_vblank(FRAME_STEP_LIMIT - steps)?;
@ -150,6 +157,7 @@ impl GraphicsDemo {
FrameRunError::Cpu(error) => GraphicsError::Cpu(error),
FrameRunError::Dma(error) => GraphicsError::Dma(error),
FrameRunError::StepLimit(_) => GraphicsError::FrameTimeout,
FrameRunError::Stopped => GraphicsError::Stopped,
})
}
}
@ -308,6 +316,28 @@ mod tests {
use super::*;
use gba_core::{cpu::Cpu, io::DISPSTAT, memory::Memory};
#[test]
fn stopped_program_reports_promptly_during_startup_and_frame_wait() {
for primed in [false, true] {
let mut demo =
GraphicsDemo::with_program(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap();
demo.primed = primed;
demo.machine
.memory_mut()
.write8(gba_core::io::HALTCNT, 0x80)
.unwrap();
let mut output = Framebuffer::default();
output.clear(31);
let before = output.pixels().to_vec();
assert!(matches!(
demo.frame(Buttons::default(), &mut output),
Err(GraphicsError::Stopped)
));
assert_eq!(demo.machine.cycles(), 0);
assert_eq!(output.pixels(), before);
}
}
#[test]
fn frame_runner_does_not_require_a_ram_counter_update() {
let mut memory = Memory::new(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap();