Reintegrate SRAM save media reader.

This commit is contained in:
Alissa Rao 2022-08-17 00:42:34 -07:00
parent d50413a3cc
commit ec41db2fc9
No known key found for this signature in database
GPG key ID: 9314D8F6745E881E
4 changed files with 302 additions and 195 deletions

View file

@ -224,6 +224,8 @@ pub struct Gba {
pub sound: sound::dmg::Sound, pub sound: sound::dmg::Sound,
/// Manages access to the Game Boy Advance's direct sound mixer for playing raw wav files. /// Manages access to the Game Boy Advance's direct sound mixer for playing raw wav files.
pub mixer: sound::mixer::MixerController, pub mixer: sound::mixer::MixerController,
/// Manages access to the Game Boy Advance cartridge's save chip.
pub save: save::SaveManager,
/// Manages access to the Game Boy Advance's 4 timers. /// Manages access to the Game Boy Advance's 4 timers.
pub timers: timer::TimerController, pub timers: timer::TimerController,
} }
@ -240,6 +242,7 @@ impl Gba {
display: display::Display::new(), display: display::Display::new(),
sound: sound::dmg::Sound::new(), sound: sound::dmg::Sound::new(),
mixer: sound::mixer::MixerController::new(), mixer: sound::mixer::MixerController::new(),
save: save::SaveManager::new(),
timers: timer::TimerController::new(), timers: timer::TimerController::new(),
} }
} }

View file

@ -3,9 +3,9 @@
//! performed via code in WRAM and cannot be accessed by DMA. //! performed via code in WRAM and cannot be accessed by DMA.
extern "C" { extern "C" {
fn WramXferBuf(src: *const u8, dst: *mut u8, count: usize); fn WramXferBuf(src: *const u8, dst: *mut u8, count: usize);
fn WramReadByte(src: *const u8) -> u8; fn WramReadByte(src: *const u8) -> u8;
fn WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; fn WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool;
} }
/// Copies data from a given memory address into a buffer. /// Copies data from a given memory address into a buffer.
@ -17,9 +17,9 @@ extern "C" {
/// This uses raw addresses into the memory space. Use with care. /// This uses raw addresses into the memory space. Use with care.
#[inline(always)] #[inline(always)]
pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) {
if dst.len() != 0 { if dst.len() != 0 {
WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); WramXferBuf(src as _, dst.as_mut_ptr(), dst.len());
} }
} }
/// Copies data from a buffer into a given memory address. /// Copies data from a buffer into a given memory address.
@ -30,9 +30,9 @@ pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) {
/// This uses raw addresses into the memory space. Use with care. /// This uses raw addresses into the memory space. Use with care.
#[inline(always)] #[inline(always)]
pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) {
if src.len() != 0 { if src.len() != 0 {
WramXferBuf(src.as_ptr(), dst as _, src.len()); WramXferBuf(src.as_ptr(), dst as _, src.len());
} }
} }
/// Verifies that the data in a buffer matches that in a given memory address. /// Verifies that the data in a buffer matches that in a given memory address.
@ -44,11 +44,11 @@ pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) {
/// This uses raw addresses into the memory space. Use with care. /// This uses raw addresses into the memory space. Use with care.
#[inline(always)] #[inline(always)]
pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool {
if buf1.len() != 0 { if buf1.len() != 0 {
WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1)
} else { } else {
true true
} }
} }
/// Reads a byte from a given memory address. /// Reads a byte from a given memory address.
@ -59,5 +59,5 @@ pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool {
/// This uses raw addresses into the memory space. Use with care. /// This uses raw addresses into the memory space. Use with care.
#[inline(always)] #[inline(always)]
pub unsafe fn read_raw_byte(src: usize) -> u8 { pub unsafe fn read_raw_byte(src: usize) -> u8 {
WramReadByte(src as _) WramReadByte(src as _)
} }

View file

@ -107,238 +107,286 @@ use crate::sync::{Mutex, RawMutexGuard};
use crate::timer::Timer; use crate::timer::Timer;
mod asm_utils; mod asm_utils;
//mod setup;
mod utils;
//pub use asm_utils::*;
//pub use setup::*;
//pub mod eeprom; //pub mod eeprom;
//pub mod flash; //pub mod flash;
//pub mod sram; mod sram;
mod utils;
/// A list of save media types. /// A list of save media types.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[non_exhaustive]
pub enum MediaType { pub enum MediaType {
/// 32KiB Battery-Backed SRAM or FRAM /// 32KiB Battery-Backed SRAM or FRAM
Sram32K, Sram32K,
/// 8KiB EEPROM /// 8KiB EEPROM
Eeprom8K, Eeprom8K,
/// 512B EEPROM /// 512B EEPROM
Eeprom512B, Eeprom512B,
/// 64KiB flash chip /// 64KiB flash chip
Flash64K, Flash64K,
/// 128KiB flash chip /// 128KiB flash chip
Flash128K, Flash128K,
/// A user-defined save media type
Custom,
} }
/// The type used for errors encountered while reading or writing save media. /// The type used for errors encountered while reading or writing save media.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[non_exhaustive] #[non_exhaustive]
pub enum Error { pub enum Error {
/// There is no save media attached to this game cart. /// There is no save media attached to this game cart.
NoMedia, NoMedia,
/// Failed to write the data to save media. /// Failed to write the data to save media.
WriteError, WriteError,
/// An operation on save media timed out. /// An operation on save media timed out.
OperationTimedOut, OperationTimedOut,
/// An attempt was made to access save media at an invalid offset. /// An attempt was made to access save media at an invalid offset.
OutOfBounds, OutOfBounds,
/// The media is already in use. /// The media is already in use.
/// ///
/// This can generally only happen in an IRQ that happens during an ongoing /// This can generally only happen in an IRQ that happens during an ongoing
/// save media operation. /// save media operation.
MediaInUse, MediaInUse,
/// This command cannot be used with the save media in use. /// This command cannot be used with the save media in use.
IncompatibleCommand, IncompatibleCommand,
} }
/// Information about the save media used. /// Information about the save media used.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[non_exhaustive] #[non_exhaustive]
pub struct MediaInfo { pub struct MediaInfo {
/// The type of save media installed. /// The type of save media installed.
pub media_type: MediaType, pub media_type: MediaType,
/// The power-of-two size of each sector. Zero represents a sector size of /// The power-of-two size of each sector. Zero represents a sector size of
/// 0, implying sectors are not in use. /// 0, implying sectors are not in use.
/// ///
/// (For example, 512 byte sectors would return 9 here.) /// (For example, 512 byte sectors would return 9 here.)
pub sector_shift: usize, pub sector_shift: usize,
/// The size of the save media, in sectors. /// The size of the save media, in sectors.
pub sector_count: usize, pub sector_count: usize,
/// Whether the save media type requires media be prepared before writing. /// Whether the save media type requires media be prepared before writing.
pub uses_prepare_write: bool, pub uses_prepare_write: bool,
} }
/// A trait allowing low-level saving and writing to save media. /// A trait allowing low-level saving and writing to save media.
trait RawSaveAccess: Sync { trait RawSaveAccess: Sync {
fn info(&self) -> Result<&'static MediaInfo, Error>; fn info(&self) -> Result<&'static MediaInfo, Error>;
fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error>; fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error>;
fn verify(&self, offset: usize, buffer: &[u8]) -> Result<bool, Error>; fn verify(&self, offset: usize, buffer: &[u8]) -> Result<bool, Error>;
fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error>; fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error>;
fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error>; fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error>;
} }
static CURRENT_SAVE_ACCESS: Mutex<Option<&'static dyn RawSaveAccess>> = Mutex::new(None); static CURRENT_SAVE_ACCESS: Mutex<Option<&'static dyn RawSaveAccess>> = Mutex::new(None);
fn set_save_implementation(access_impl: &'static dyn RawSaveAccess) { fn set_save_implementation(access_impl: &'static dyn RawSaveAccess) {
let mut access = CURRENT_SAVE_ACCESS.lock(); let mut access = CURRENT_SAVE_ACCESS.lock();
assert!(access.is_none(), "Cannot initialize the savegame engine more than once."); assert!(access.is_none(), "Cannot initialize the savegame engine more than once.");
*access = Some(access_impl); *access = Some(access_impl);
} }
fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> {
*CURRENT_SAVE_ACCESS.lock() *CURRENT_SAVE_ACCESS.lock()
} }
/// Allows reading and writing of save media. /// Allows reading and writing of save media.
#[derive(Copy, Clone)]
pub struct SaveData { pub struct SaveData {
lock: RawMutexGuard<'static>, lock: RawMutexGuard<'static>,
access: &'static dyn RawSaveAccess, access: &'static dyn RawSaveAccess,
info: &'static MediaInfo, info: &'static MediaInfo,
timeout: utils::Timeout, timeout: utils::Timeout,
} }
impl SaveData { impl SaveData {
/// Creates a new save accessor around the current save implementaiton. /// Creates a new save accessor around the current save implementaiton.
fn new(timer: Option<Timer>) -> Result<SaveData, Error> { fn new(timer: Option<Timer>) -> Result<SaveData, Error> {
match get_save_implementation() { match get_save_implementation() {
Some(access) => Ok(SaveData { Some(access) => Ok(SaveData {
lock: utils::lock_media()?, lock: utils::lock_media()?,
access, access,
info: access.info()?, info: access.info()?,
timeout: utils::Timeout::new(timer), timeout: utils::Timeout::new(timer),
}), }),
None => Err(Error::NoMedia), None => Err(Error::NoMedia),
}
} }
}
/// Returns the media info underlying this accessor. /// Returns the media info underlying this accessor.
pub fn media_info(&self) -> &'static MediaInfo { pub fn media_info(&self) -> &'static MediaInfo {
self.info self.info
}
/// Returns the save media type being used.
pub fn media_type(&self) -> MediaType {
self.info.media_type
}
/// Returns the sector size of the save media. It is generally optimal to
/// write data in blocks that are aligned to the sector size.
pub fn sector_size(&self) -> usize {
1 << self.info.sector_shift
}
/// Returns the total length of this save media.
pub fn len(&self) -> usize {
self.info.sector_count << self.info.sector_shift
}
fn check_bounds(&self, range: Range<usize>) -> Result<(), Error> {
if range.start >= self.len() || range.end >= self.len() {
Err(Error::OutOfBounds)
} else {
Ok(())
} }
}
fn check_bounds_len(&self, offset: usize, len: usize) -> Result<(), Error> {
self.check_bounds(offset..(offset + len))
}
/// Copies data from the save media to a buffer. /// Returns the save media type being used.
/// pub fn media_type(&self) -> MediaType {
/// If an error is returned, the contents of the buffer are unpredictable. self.info.media_type
pub fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { }
self.check_bounds_len(offset, buffer.len())?;
self.access.read(offset, buffer) /// Returns the sector size of the save media. It is generally optimal to
} /// write data in blocks that are aligned to the sector size.
pub fn sector_size(&self) -> usize {
/// Verifies that a given block of memory matches the save media. 1 << self.info.sector_shift
pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result<bool, Error> { }
self.check_bounds_len(offset, buffer.len())?;
self.access.verify(offset, buffer) /// Returns the total length of this save media.
} pub fn len(&self) -> usize {
self.info.sector_count << self.info.sector_shift
/// Returns a range that contains all sectors the input range overlaps. }
///
/// This can be used to calculate which blocks would be erased by a call fn check_bounds(&self, range: Range<usize>) -> Result<(), Error> {
/// to [`prepare_write`](`SaveAccess::prepare_write`) if range.start >= self.len() || range.end >= self.len() {
pub fn align_range(&self, range: Range<usize>) -> Range<usize> { Err(Error::OutOfBounds)
let shift = self.info.sector_shift; } else {
let mask = (1 << shift) - 1; Ok(())
(range.start & !mask)..((range.end + mask) & !mask) }
} }
fn check_bounds_len(&self, offset: usize, len: usize) -> Result<(), Error> {
/// Prepares a given span of offsets for writing. self.check_bounds(offset..(offset + len))
/// }
/// This will erase any data in any sector overlapping the input range. To
/// calculate which offset ranges would be affected, use the /// Copies data from the save media to a buffer.
/// [`align_range`](`SaveAccess::align_range`) function. ///
pub fn prepare_write(&mut self, range: Range<usize>) -> Result<SavePreparedBlock, Error> { /// If an error is returned, the contents of the buffer are unpredictable.
self.check_bounds(range.clone())?; pub fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> {
if self.info.uses_prepare_write { self.check_bounds_len(offset, buffer.len())?;
let range = self.align_range(range.clone()); self.access.read(offset, buffer)
let shift = self.info.sector_shift; }
self.access.prepare_write(range.start >> shift, range.len() >> shift)?;
/// Verifies that a given block of memory matches the save media.
pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result<bool, Error> {
self.check_bounds_len(offset, buffer.len())?;
self.access.verify(offset, buffer)
}
/// Returns a range that contains all sectors the input range overlaps.
///
/// This can be used to calculate which blocks would be erased by a call
/// to [`prepare_write`](`SaveAccess::prepare_write`)
pub fn align_range(&self, range: Range<usize>) -> Range<usize> {
let shift = self.info.sector_shift;
let mask = (1 << shift) - 1;
(range.start & !mask)..((range.end + mask) & !mask)
}
/// Prepares a given span of offsets for writing.
///
/// This will erase any data in any sector overlapping the input range. To
/// calculate which offset ranges would be affected, use the
/// [`align_range`](`SaveAccess::align_range`) function.
pub fn prepare_write(&mut self, range: Range<usize>) -> Result<SavePreparedBlock, Error> {
self.check_bounds(range.clone())?;
if self.info.uses_prepare_write {
let range = self.align_range(range.clone());
let shift = self.info.sector_shift;
self.access.prepare_write(range.start >> shift, range.len() >> shift)?;
}
Ok(SavePreparedBlock {
parent: self,
range
})
} }
Ok(SavePreparedBlock {
parent: self,
range
})
}
} }
/// A block of save memory that has been prepared for writing. /// A block of save memory that has been prepared for writing.
pub struct SavePreparedBlock<'a> { pub struct SavePreparedBlock<'a> {
parent: &'a mut SaveData, parent: &'a mut SaveData,
range: Range<usize>, range: Range<usize>,
} }
impl<'a> SavePreparedBlock<'a> { impl<'a> SavePreparedBlock<'a> {
/// Writes a given buffer into the save media. /// Writes a given buffer into the save media.
/// ///
/// Multiple overlapping writes to the same memory range without a separate /// Multiple overlapping writes to the same memory range without a separate
/// call to `prepare_write` will leave the save data in an unpredictable /// call to `prepare_write` will leave the save data in an unpredictable
/// state. If an error is returned, the contents of the save media is /// state. If an error is returned, the contents of the save media is
/// unpredictable. /// unpredictable.
pub fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { pub fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> {
if buffer.len() == 0 { if buffer.len() == 0 {
Ok(()) Ok(())
} else if !self.range.contains(&offset) || } else if !self.range.contains(&offset) ||
!self.range.contains(&(offset + buffer.len() - 1)) { !self.range.contains(&(offset + buffer.len() - 1)) {
Err(Error::OutOfBounds) Err(Error::OutOfBounds)
} else { } else {
self.parent.access.write(offset, buffer) self.parent.access.write(offset, buffer)
}
} }
}
/// Writes and validates a given buffer into the save media. /// Writes and validates a given buffer into the save media.
/// ///
/// This function will verify that the write has completed successfully, and /// This function will verify that the write has completed successfully, and
/// return an error if it has not done so. /// return an error if it has not done so.
/// ///
/// Multiple overlapping writes to the same memory range without a separate /// Multiple overlapping writes to the same memory range without a separate
/// call to `prepare_write` will leave the save data in an unpredictable /// call to `prepare_write` will leave the save data in an unpredictable
/// state. If an error is returned, the contents of the save media is /// state. If an error is returned, the contents of the save media is
/// unpredictable. /// unpredictable.
pub fn write_and_verify(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { pub fn write_and_verify(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> {
self.write(offset, buffer)?; self.write(offset, buffer)?;
if !self.parent.verify(offset, buffer)? { if !self.parent.verify(offset, buffer)? {
Err(Error::WriteError) Err(Error::WriteError)
} else { } else {
Ok(()) Ok(())
}
}
}
mod marker {
#[repr(align(4))]
struct Align<T>(T);
static EEPROM: Align<[u8; 12]> = Align(*b"EEPROM_Vnnn\0");
static SRAM: Align<[u8; 12]> = Align(*b"SRAM_Vnnn\0\0\0");
static FLASH512K: Align<[u8; 16]> = Align(*b"FLASH512_Vnnn\0\0\0");
static FLASH1M: Align<[u8; 16]> = Align(*b"FLASH1M_Vnnn\0\0\0\0");
#[inline(always)]
pub fn emit_eeprom_marker() {
crate::sync::memory_read_hint(&EEPROM);
}
#[inline(always)]
pub fn emit_sram_marker() {
crate::sync::memory_read_hint(&SRAM);
}
#[inline(always)]
pub fn emit_flash_512k_marker() {
crate::sync::memory_read_hint(&FLASH512K);
}
#[inline(always)]
pub fn emit_flash_1m_marker() {
crate::sync::memory_read_hint(&FLASH1M);
} }
}
} }
/// Allows access to the cartridge's save data. /// Allows access to the cartridge's save data.
pub struct SaveManager; #[non_exhaustive]
pub struct SaveManager {}
impl SaveManager { impl SaveManager {
pub fn access() -> Result<SaveData, Error> { pub(crate) const fn new() -> Self {
SaveData::new(None) SaveManager {}
} }
pub fn access_with_timer(timer: Timer) -> Result<SaveData, Error> {
SaveData::new(Some(timer)) /// Declares that the ROM uses battery backed SRAM/FRAM.
} ///
/// Battery Backed SRAM is generally very fast, but limited in size compared
/// to flash chips.
///
/// This creates a marker in the ROM that allows emulators to understand what
/// save type the Game Pak uses, and configures the save manager to use the
/// given save type.
///
/// Only one `init_*` function may be called in the lifetime of the program.
pub fn init_sram() {
marker::emit_sram_marker();
set_save_implementation(&sram::BatteryBackedAccess);
}
/// Creates a new accessor to the save data.
///
/// You must have initialized the save manager beforehand to use a specific
/// type of media before calling this method.
pub fn access() -> Result<SaveData, Error> {
SaveData::new(None)
}
/// Creates a new accessor to the save data that uses the given timer for timeouts.
///
/// You must have initialized the save manager beforehand to use a specific
/// type of media before calling this method.
pub fn access_with_timer(timer: Timer) -> Result<SaveData, Error> {
SaveData::new(Some(timer))
}
} }

56
agb/src/save/sram.rs Normal file
View file

@ -0,0 +1,56 @@
//! Module for battery backed SRAM save media support.
//!
//! SRAM acts as ordinary memory mapped into the memory space, and as such
//! is accessed using normal memory read/write commands.
use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess};
use crate::save::asm_utils::*;
const SRAM_SIZE: usize = 32 * 1024; // 32 KiB
/// Checks whether an offset is contained within the bounds of the SRAM.
fn check_bounds(offset: usize, len: usize) -> Result<(), Error> {
if offset.checked_add(len).is_none() || offset + len > SRAM_SIZE {
return Err(Error::OutOfBounds);
}
Ok(())
}
/// The [`RawSaveAccess`] used for battery backed SRAM.
pub struct BatteryBackedAccess;
impl RawSaveAccess for BatteryBackedAccess {
fn info(&self) -> Result<&'static MediaInfo, Error> {
Ok(&MediaInfo {
media_type: MediaType::Sram32K,
sector_shift: 0,
sector_count: SRAM_SIZE,
uses_prepare_write: false,
})
}
fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> {
check_bounds(offset, buffer.len())?;
unsafe {
read_raw_buf(buffer, 0x0E000000 + offset);
}
Ok(())
}
fn verify(&self, offset: usize, buffer: &[u8]) -> Result<bool, Error> {
check_bounds(offset, buffer.len())?;
let val = unsafe { verify_raw_buf(buffer, 0x0E000000 + offset) };
Ok(val)
}
fn prepare_write(&self, _: usize, _: usize) -> Result<(), Error> {
Ok(())
}
fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> {
check_bounds(offset, buffer.len())?;
unsafe {
write_raw_buf(0x0E000000 + offset, buffer);
}
Ok(())
}
}