feat(input): implement KEYCNT keypad interrupt control

Add a dependency-free keypad state machine in gba-core, map KEYCNT, latch IF bit 12, and wake HALT through the existing interrupt gates. Sample complete CPU/DMA control writes once so transient byte values cannot create false requests.

Document the mGBA-style polling policy and its unverified hardware retrigger limits. Cover every button-state/selection pair, register masks, repeated requests, byte/word writes, failed-store isolation, DMA priority, ARM/Thumb IRQ return, BIOS IntrWait, and retained BIOS reset state.

Follow the restructured workspace and modular documentation. Validated on Darwin arm64: workspace and core/demo tests pass in debug and release, with Clippy, rustdoc, ten native window smoke tests, and terminal demos passing.
This commit is contained in:
0xrsydn 2026-10-05 18:00:46 +07:00
commit 597b02881b
14 changed files with 710 additions and 21 deletions

View file

@ -1,6 +1,9 @@
//! Platform-independent GBA button state. Bits are pressed-high here;
//! the KEYINPUT register exposes their active-low hardware representation.
mod keypad;
pub(crate) use keypad::Keypad;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum Button {

View file

@ -0,0 +1,88 @@
//! Functional keypad IRQ sampling from input snapshots and control writes.
//! OR requests on every matching sample. AND suppresses identical matching
//! snapshots, following mGBA's polling model. Exact hardware edges are unverified.
use super::Buttons;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct Keypad {
buttons: Buttons,
control: u16,
last_match: Option<u16>,
}
impl Keypad {
pub(crate) fn keyinput(&self) -> u16 {
self.buttons.keyinput()
}
pub(crate) fn control(&self) -> u16 {
self.control
}
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 {
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.
if let Some(previous) = &mut self.last_match {
*previous &= !newly_selected;
}
self.control = value;
self.sample()
}
fn sample(&mut 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 {
pressed & selected == selected
} else {
pressed & selected != 0
};
if !matched {
self.last_match = None;
return false;
}
let previous = self.last_match.replace(pressed);
!all || previous != Some(pressed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_masks_and_button_states_match_independent_per_button_logic() {
for selected in 0..1024 {
for pressed in 0..1024 {
let mut any = false;
let mut all = true;
for bit in 0..10 {
if selected & (1 << bit) != 0 {
any |= pressed & (1 << bit) != 0;
all &= pressed & (1 << bit) != 0;
}
}
for (condition, expected) in [(0, any), (0x8000, all)] {
let mut keypad = Keypad::default();
assert!(!keypad.set_buttons(Buttons::from_bits(pressed)));
assert_eq!(
keypad.write_control(selected | condition | 0x4000),
expected
);
assert_eq!(keypad.keyinput(), !pressed & 0x03ff);
}
}
}
}
}

View file

@ -1,11 +1,11 @@
//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, and polled input.
//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, and keypad input.
//! Timer phase resets on enable or clock-source changes. Hardware startup delays,
//! shared prescaler phase, audio events, and interrupt delivery delays are not modeled.
use crate::{
display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
dma::{Dma, DmaError, Transfer, DMA_BASE, DMA_END},
input::Buttons,
input::{Buttons, Keypad},
};
pub const DISPCNT: u32 = 0x0400_0000;
@ -47,6 +47,7 @@ pub const BLDCNT: u32 = 0x0400_0050;
pub const BLDALPHA: u32 = 0x0400_0052;
pub const BLDY: u32 = 0x0400_0054;
pub const KEYINPUT: u32 = 0x0400_0130;
pub const KEYCNT: u32 = 0x0400_0132;
pub const TIMER_BASE: u32 = 0x0400_0100;
pub const IE: u32 = 0x0400_0200;
pub const IF: u32 = 0x0400_0202;
@ -137,7 +138,7 @@ pub(crate) struct Io {
effects: crate::video::effects::Effects,
window_edges: crate::video::windows::WindowEdges,
horizontal_windows: crate::video::windows::horizontal::HorizontalWindows,
buttons: Buttons,
keypad: Keypad,
display: Display,
dma: Dma,
postflg: bool,
@ -146,7 +147,7 @@ pub(crate) struct Io {
impl Io {
pub(crate) fn mapped(address: u32) -> bool {
matches!(address, 0x0400_0000..=0x0400_0057 | DMA_BASE..=DMA_END | 0x0400_0100..=0x0400_010f | 0x0400_0130..=0x0400_0131 | 0x0400_0200..=0x0400_020b | POSTFLG..=0x0400_0303)
matches!(address, 0x0400_0000..=0x0400_0057 | DMA_BASE..=DMA_END | 0x0400_0100..=0x0400_010f | 0x0400_0130..=0x0400_0133 | 0x0400_0200..=0x0400_020b | POSTFLG..=0x0400_0303)
}
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
@ -165,7 +166,8 @@ impl Io {
WINOUT => self.effects.outside,
BLDCNT => self.effects.control,
BLDALPHA => self.effects.alpha,
KEYINPUT => self.buttons.keyinput(),
KEYINPUT => self.keypad.keyinput(),
KEYCNT => self.keypad.control(),
0x0400_0100..=0x0400_010e => {
let timer = &self.timers[((address - TIMER_BASE) / 4) as usize];
if address & 2 == 0 {
@ -188,8 +190,10 @@ impl Io {
}
/// The memory bus validates every byte before calling this method.
/// Keypad accesses use write_keypad instead, preserving transfer boundaries.
pub(crate) fn write8(&mut self, address: u32, value: u8) {
debug_assert!(Self::mapped(address));
debug_assert!(!(KEYINPUT..=KEYCNT + 1).contains(&address));
if (DMA_BASE..=DMA_END).contains(&address) {
self.dma.write8(address, value);
return;
@ -198,7 +202,7 @@ impl Io {
DISPCNT => self.dispcnt = replace_byte(self.dispcnt, address, value) & 0xfff7,
GREENSWAP if address & 1 == 0 => self.greenswap = value & 1 != 0,
DISPSTAT => self.pending |= self.display.write_status(address & 1 != 0, value),
VCOUNT | KEYINPUT => {} // Read-only; writes are ignored.
VCOUNT => {} // Read-only; writes are ignored.
BG0CNT..=BG3CNT => {
let index = ((address - BG0CNT) / 2) as usize;
let control = &mut self.backgrounds[index].control;
@ -294,7 +298,30 @@ impl Io {
}
pub(crate) fn set_buttons(&mut self, buttons: Buttons) {
self.buttons = buttons;
if self.keypad.set_buttons(buttons) {
self.pending |= 1 << 12;
}
self.wake_if_requested();
}
/// Commit one validated byte/halfword/word access in the keypad block.
/// KEYINPUT writes are ignored. Sample once after both KEYCNT bytes merge,
/// never on a transient low-byte value during a halfword or word write.
pub(crate) fn write_keypad(&mut self, address: u32, bytes: &[u8]) {
debug_assert!(address >= KEYINPUT && address + bytes.len() as u32 <= KEYCNT + 2);
let mut control = self.keypad.control();
let mut touched = false;
for (offset, byte) in bytes.iter().enumerate() {
let address = address + offset as u32;
if (KEYCNT..=KEYCNT + 1).contains(&address) {
control = replace_byte(control, address, *byte);
touched = true;
}
}
if touched && self.keypad.write_control(control) {
self.pending |= 1 << 12;
}
self.wake_if_requested();
}
pub(crate) fn display_control(&self) -> u16 {

View file

@ -4,7 +4,7 @@ use crate::{
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
dma::{DmaError, DMA_BASE, DMA_END},
input::Buttons,
io::{Io, HALTCNT, POSTFLG},
io::{Io, HALTCNT, KEYINPUT, POSTFLG},
timing::{bus_cycles, AccessKind, AccessWidth, DataTiming, StepTiming},
video::{self, sprites::pipeline, Framebuffer, VideoError},
};
@ -325,7 +325,8 @@ impl Memory {
}
/// Replace the current pressed-button state. This does not advance time.
/// KEYCNT and keypad interrupts are not implemented yet.
/// Sample KEYCNT and latch a matching keypad IRQ immediately, without cycles.
/// This can wake HALT; CPU IRQ delivery occurs on a later Machine::step.
pub fn set_buttons(&mut self, buttons: Buttons) {
self.io.set_buttons(buttons);
}
@ -424,6 +425,10 @@ impl Memory {
},
);
if address >> 24 == 0x04 {
if address & !3 == KEYINPUT {
self.io.write_keypad(address, &bytes);
return Ok(());
}
for (offset, byte) in bytes.into_iter().enumerate() {
let byte_address = address + offset as u32;
if matches!(byte_address, POSTFLG | HALTCNT) && !self.can_write_power_control() {