feat(cartridge): implement RTC calendar transfers and clock updates

This commit is contained in:
0xrsydn 2026-10-07 05:55:31 +07:00
commit fce222eea2
22 changed files with 1227 additions and 196 deletions

View file

@ -0,0 +1,122 @@
//! Host-only clock adapter. UTC is sampled once; later changes use monotonic elapsed seconds.
use gba_core::{
cartridge::{RtcDateTime, RtcError},
memory::Memory,
};
use std::{
io,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
const UNIX_2000: u64 = 946_684_800;
pub(crate) struct RtcHostClock {
started: Instant,
supplied: u64,
}
impl RtcHostClock {
pub(crate) fn new(memory: &mut Memory) -> io::Result<Self> {
let unix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(io::Error::other)?
.as_secs();
Self::from_unix_seconds(memory, unix).map_err(io::Error::other)
}
fn from_unix_seconds(memory: &mut Memory, unix: u64) -> Result<Self, RtcError> {
let seconds = unix
.checked_sub(UNIX_2000)
.ok_or(RtcError::InvalidDateTime)?;
memory.set_rtc_datetime(RtcDateTime::from_seconds_since_2000(seconds)?)?;
Ok(Self {
started: Instant::now(),
supplied: 0,
})
}
pub(crate) fn sync(&mut self, memory: &mut Memory) -> Result<(), RtcError> {
self.supply(memory, self.started.elapsed())
}
fn supply(&mut self, memory: &mut Memory, elapsed: Duration) -> Result<(), RtcError> {
let seconds = elapsed.as_secs();
// Ignore a backwards test/source sample without moving the high-water mark.
if seconds > self.supplied {
memory.advance_rtc_seconds(seconds - self.supplied)?;
self.supplied = seconds;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use gba_core::{cartridge::CartridgeHardware, io::HALTCNT};
fn memory() -> Memory {
let mut memory = Memory::new(vec![0; 4]).unwrap();
memory.set_cartridge_hardware(CartridgeHardware::Rtc);
memory
}
#[test]
fn epoch_seed_is_utc_and_range_checked_without_changing_state_on_error() {
let mut memory = memory();
RtcHostClock::from_unix_seconds(&mut memory, UNIX_2000).unwrap();
assert_eq!(
memory.rtc_datetime().unwrap().components(),
[0, 1, 1, 6, 0, 0, 0]
);
for invalid in [UNIX_2000 - 1, UNIX_2000 + 3_155_760_000, u64::MAX] {
assert!(RtcHostClock::from_unix_seconds(&mut memory, invalid).is_err());
assert_eq!(
memory.rtc_datetime().unwrap().components(),
[0, 1, 1, 6, 0, 0, 0]
);
}
}
#[test]
fn elapsed_seconds_are_supplied_once_preserve_guest_time_and_continue_in_stop() {
let mut memory = memory();
let mut clock = RtcHostClock::from_unix_seconds(&mut memory, UNIX_2000).unwrap();
for ms in [999, 1000, 1000, 700, 1999, 2000] {
clock
.supply(&mut memory, Duration::from_millis(ms))
.unwrap();
}
assert_eq!(
memory.rtc_datetime().unwrap().components(),
[0, 1, 1, 6, 0, 0, 2]
);
memory
.set_rtc_datetime(RtcDateTime::new([24, 2, 29, 3, 23, 59, 59]).unwrap())
.unwrap();
memory.write8(HALTCNT, 128).unwrap();
clock.supply(&mut memory, Duration::from_secs(3)).unwrap();
assert_eq!(
memory.rtc_datetime().unwrap().components(),
[24, 3, 1, 4, 0, 0, 0]
);
assert!(memory.stopped());
assert_eq!(memory.cycles(), 0);
}
#[test]
fn failed_tick_does_not_lose_elapsed_seconds() {
let mut memory = memory();
let mut clock = RtcHostClock::from_unix_seconds(&mut memory, UNIX_2000).unwrap();
memory.set_cartridge_hardware(CartridgeHardware::None);
assert_eq!(
clock.supply(&mut memory, Duration::from_secs(5)),
Err(RtcError::NotAttached)
);
memory.set_cartridge_hardware(CartridgeHardware::Rtc);
clock.supply(&mut memory, Duration::from_secs(5)).unwrap();
assert_eq!(
memory.rtc_datetime().unwrap().components(),
[0, 1, 1, 0, 0, 0, 5]
);
}
}

View file

@ -50,6 +50,11 @@ impl Session {
self.machine.memory_mut().set_cartridge_hardware(hardware);
}
/// Host adapters may supply independent cartridge time between execution slices.
pub fn memory_mut(&mut self) -> &mut gba_core::memory::Memory {
self.machine.memory_mut()
}
pub fn frame(&self) -> &Framebuffer {
&self.frame
}