From 217f42a635dfd3b0099cdfac12daeacfdc81ba08 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Tue, 16 Aug 2022 17:16:34 -0700 Subject: [PATCH 01/49] Initial work on porting gba save code to agb codebase. --- agb/build.rs | 1 + agb/src/lib.rs | 2 + agb/src/save/asm_routines.s | 89 ++++++++++ agb/src/save/asm_utils.rs | 63 +++++++ agb/src/save/mod.rs | 345 ++++++++++++++++++++++++++++++++++++ 5 files changed, 500 insertions(+) create mode 100644 agb/src/save/asm_routines.s create mode 100644 agb/src/save/asm_utils.rs create mode 100644 agb/src/save/mod.rs diff --git a/agb/build.rs b/agb/build.rs index 2bb2f4bb..d4245b4a 100644 --- a/agb/build.rs +++ b/agb/build.rs @@ -7,6 +7,7 @@ fn main() { "src/sound/mixer/mixer.s", "src/agbabi/memset.s", "src/agbabi/memcpy.s", + "src/save/asm_routines.s", ]; println!("cargo:rerun-if-changed=gba.ld"); diff --git a/agb/src/lib.rs b/agb/src/lib.rs index fa3dd6f9..08ba848f 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -168,6 +168,8 @@ pub use agb_fixnum as fixnum; pub mod hash_map; /// Simple random number generator pub mod rng; +/// Implements save games. +pub mod save; mod single; /// Implements sound output. pub mod sound; diff --git a/agb/src/save/asm_routines.s b/agb/src/save/asm_routines.s new file mode 100644 index 00000000..41e8df74 --- /dev/null +++ b/agb/src/save/asm_routines.s @@ -0,0 +1,89 @@ +@ +@ char WramReadByte(const char* offset); +@ +@ A routine that reads a byte from a given memory offset. +@ + .thumb + .global WramReadByte + .thumb_func + .align 2 +WramReadByte: + ldr r1, =WramReadByteInner + bx r1 + + .section .data + + .thumb + .thumb_func + .align 2 +WramReadByteInner: + ldrb r0, [r0] + mov pc, lr + + .section .text + +@ +@ bool WramVerifyBuf(const char* buf1, const char* buf2, int count); +@ +@ A routine that compares two memory offsets. +@ + .thumb + .global WramVerifyBuf + .thumb_func + .align 2 +WramVerifyBuf: + push {r4-r5, lr} + movs r5, r0 @ set up r5 to be r0, so we can use it immediately for the return result + movs r0, #0 @ set up r0 so the default return result is false + ldr r4, =WramVerifyBufInner + bx r4 @ jump to the part in WRAM + + .section .data + + .thumb + .thumb_func + .align 2 +WramVerifyBufInner: + @ At this point, buf1 is actually in r5, so r0 can be used as a status return + ldrb r3, [r5,r2] + ldrb r4, [r1,r2] + cmp r3, r4 + bne 0f + sub r2, #1 + bpl WramVerifyBufInner + + @ Returns from the function successfully + movs r0, #1 +0: @ Jumps to here return the function unsuccessfully, because r0 contains 0 at this point + pop {r4-r5, pc} + + .section .text + +@ +@ void WramXferBuf(const char* source, char* dest, int count); +@ +@ A routine that copies one buffer into another. +@ + .thumb + .global WramXferBuf + .thumb_func + .align 2 +WramXferBuf: + ldr r3, =WramXferBufInner + bx r3 + + .pool + .section .data + + .thumb + .thumb_func + .align 2 +WramXferBufInner: + sub r2, #1 + ldrb r3, [r0,r2] + strb r3, [r1,r2] + bne WramXferBufInner + mov pc, lr + + .pool + .section .text diff --git a/agb/src/save/asm_utils.rs b/agb/src/save/asm_utils.rs new file mode 100644 index 00000000..f9ca1db0 --- /dev/null +++ b/agb/src/save/asm_utils.rs @@ -0,0 +1,63 @@ +//! A module containing low-level assembly functions that can be loaded into +//! WRAM. Both flash media and battery-backed SRAM require reads to be +//! performed via code in WRAM and cannot be accessed by DMA. + +extern "C" { + fn WramXferBuf(src: *const u8, dst: *mut u8, count: usize); + fn WramReadByte(src: *const u8) -> u8; + fn WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; +} + +/// Copies data from a given memory address into a buffer. +/// +/// This should be used to access any data found in flash or battery-backed +/// SRAM, as you must read those one byte at a time and from code stored +/// in WRAM. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { + if dst.len() != 0 { + WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); + } +} + +/// Copies data from a buffer into a given memory address. +/// +/// This is not strictly needed to write into save media, but reuses the +/// optimized loop used in `read_raw_buf`, and will often be faster. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { + if src.len() != 0 { + WramXferBuf(src.as_ptr(), dst as _, src.len()); + } +} + +/// Verifies that the data in a buffer matches that in a given memory address. +/// +/// This should be used to access any data found in flash or battery-backed +/// SRAM, as you must read those one byte at a time and from code stored +/// in WRAM. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { + if buf1.len() != 0 { + WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) + } else { + true + } +} + +/// Reads a byte from a given memory address. +/// +/// This should be used to access any data found in flash or battery-backed +/// SRAM, as you must read those from code found in WRAM. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn read_raw_byte(src: usize) -> u8 { + WramReadByte(src as _) +} diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs new file mode 100644 index 00000000..c4fad1a2 --- /dev/null +++ b/agb/src/save/mod.rs @@ -0,0 +1,345 @@ +//! Module for reading and writing to save media. +//! +//! This module provides both specific interfaces that directly access particular +//! types of save media, and an abstraction layer that allows access to all kinds +//! of save media using a shared interface. +//! +//! ## Save media types +//! +//! There are, broadly speaking, three different kinds of save media that can be +//! found in official Game Carts: +//! +//! * Battery-Backed SRAM: The simplest kind of save media, which can be accessed +//! like normal memory. You can have SRAM up to 32KiB, and while there exist a +//! few variants this does not matter much for a game developer. +//! * EEPROM: A kind of save media based on very cheap chips and slow chips. +//! These are accessed using a serial interface based on reading/writing bit +//! streams into IO registers. This memory comes in 8KiB and 512 byte versions, +//! which unfortunately cannot be distinguished at runtime. +//! * Flash: A kind of save media based on flash memory. Flash memory can be read +//! like ordinary memory, but writing requires sending commands using multiple +//! IO register spread across the address space. This memory comes in 64KiB +//! and 128KiB variants, which can thankfully be distinguished using a chip ID. +//! +//! As these various types of save media cannot be easily distinguished at +//! runtime, the kind of media in use should be set manually. +//! +//! ## Setting save media type +//! +//! To use save media in your game, you must set which type to use. This is done +//! by calling one of the following functions at startup: +//! +//! * For 32 KiB battery-backed SRAM, call [`use_sram`]. +//! * For 64 KiB flash memory, call [`use_flash_64k`]. +//! * For 128 KiB flash memory, call [`use_flash_128k`]. +//! * For 512 byte EEPROM, call [`use_eeprom_512b`]. +//! * For 8 KiB EEPROM, call [`use_eeprom_8k`]. +//! +//! Then, call [`set_timer_for_timeout`] to set the timer you intend to use to +//! track the timeout that prevents errors with the save media from hanging your +//! game. For more information on GBA timers, see the [`timer`](`crate::timer`) +//! module's documentation. +//! +//! TODO Update example +//! ```rust,norun +//! # use gba::save; +//! save::use_flash_128k(); +//! save::set_timer_for_timeout(3); // Uses timer 3 for save media timeouts. +//! ``` +//! +//! ## Using save media +//! +//! To access save media, use the [`SaveAccess::new`] method to create a new +//! [`SaveAccess`] object. Its methods are used to read or write save media. +//! +//! Reading data from the savegame is simple. Use [`read`](`SaveAccess::read`) +//! to copy data from an offset in the savegame into a buffer in memory. +//! +//! TODO Update example +//! ```rust,norun +//! # use gba::{info, save::SaveAccess}; +//! let mut buf = [0; 1000]; +//! SaveAccess::new()?.read(1000, &mut buf)?; +//! info!("Memory result: {:?}", buf); +//! ``` +//! +//! Writing to save media requires you to prepare the area for writing by calling +//! the [`prepare_write`](`SaveAccess::prepare_write`) method before doing the +//! actual write commands with the [`write`](`SaveAccess::write`) method. +//! +//! TODO Update example +//! ```rust,norun +//! # use gba::{info, save::SaveAccess}; +//! let access = SaveAccess::new()?; +//! access.prepare_write(500..600)?; +//! access.write(500, &[10; 25])?; +//! access.write(525, &[20; 25])?; +//! access.write(550, &[30; 25])?; +//! access.write(575, &[40; 25])?; +//! ``` +//! +//! The `prepare_write` method leaves everything in a sector that overlaps the +//! range passed to it in an implementation defined state. On some devices it may +//! do nothing, and on others, it may clear the entire range to `0xFF`. +//! +//! Because writes can only be prepared on a per-sector basis, a clear on a range +//! of `4000..5000` on a device with 4096 byte sectors will actually clear a range +//! of `0..8192`. Use [`sector_size`](`SaveAccess::sector_size`) to find the +//! sector size, or [`align_range`](`SaveAccess::align_range`) to directly +//! calculate the range of memory that will be affected by the clear. +//! +//! ## Performance and Other Details +//! +//! Because `prepare_write` does nothing on non-flash chips, it would not cause +//! correctness issues to ignore it. Even so, it is recommend to write code to +//! use the `prepare_write` function regardless of the save media, as it has +//! minimal runtime cost on other save media types. If needed, you can check if +//! `prepare_write` is required by calling the +//! (`requires_prepare_write`)(`SaveAccess::requires_prepare_write`) method. +//! +//! Some memory types have a `sector_size` above `1`, but do not use +//! `prepare_write`. This indicates that the media type has sectors that must +//! be rewritten all at once, instead of supporting the separate erase/write +//! cycles that flash media does. Writing non-sector aligned memory will be +//! slower on such save media, as the implementation needs to read the old +//! contents into a buffer before writing to avoid data loss. +//! +//! To summarize, for all supported media types: +//! +//! * SRAM does not require `prepare_write` and has no sectors to align to. Reads +//! and writes at any alignment are efficient. Furthermore, it does not require +//! a timer to be set with [`set_timer_for_timeout`]. +//! * Non-Atmel flash chips requires `prepare_write`, and have sectors of 4096 +//! bytes. Atmel flash chips instead do not require `prepare_write`, and instead +//! have sectors of 128 bytes. You should generally try to use `prepare_write` +//! regardless, and write in blocks of 128 bytes if at all possible. +//! * EEPROM does not require `prepare_write` and has sectors of 8 bytes. + +use core::cell::Cell; +use core::ops::Range; +use bare_metal::Mutex; + +mod asm_utils; +//mod setup; +//mod utils; + +//pub use asm_utils::*; +//pub use setup::*; +//pub use utils::*; + +//pub mod eeprom; +//pub mod flash; +//pub mod sram; + +/// A list of save media types. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub enum MediaType { + /// 32KiB Battery-Backed SRAM or FRAM + Sram32K, + /// 8KiB EEPROM + Eeprom8K, + /// 512B EEPROM + Eeprom512B, + /// 64KiB flash chip + Flash64K, + /// 128KiB flash chip + Flash128K, + /// A user-defined save media type + Custom, +} + +/// The type used for errors encountered while reading or writing save media. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Error { + /// There is no save media attached to this game cart. + NoMedia, + /// Failed to write the data to save media. + WriteError, + /// An operation on save media timed out. + OperationTimedOut, + /// An attempt was made to access save media at an invalid offset. + OutOfBounds, + /// The media is already in use. + /// + /// This can generally only happen in an IRQ that happens during an ongoing + /// save media operation. + MediaInUse, + /// This command cannot be used with the save media in use. + IncompatibleCommand, +} + +/// Information about the save media used. +#[derive(Clone, Debug)] +pub struct MediaInfo { + /// The type of save media installed. + pub media_type: MediaType, + /// The power-of-two size of each sector. Zero represents a sector size of + /// 0, implying sectors are not in use. + /// + /// (For example, 512 byte sectors would return 9 here.) + pub sector_shift: usize, + /// The size of the save media, in sectors. + pub sector_count: usize, + /// Whether the save media type requires the use of the + /// [`prepare_write`](`SaveAccess::prepare_write`) function before a block of + /// memory can be overwritten. + pub requires_prepare_write: bool, +} + +/// A trait allowing low-level saving and writing to save media. +/// +/// It exposes an interface mostly based around the requirements of reading and +/// writing flash memory, as those are the most restrictive. +/// +/// This interface treats memory as a continuous block of bytes for purposes of +/// reading, and as an array of sectors . +pub trait RawSaveAccess: Sync { + /// Returns information about the save media used. + fn info(&self) -> Result<&'static MediaInfo, Error>; + + /// Reads a slice of memory from save media. + /// + /// This will attempt to fill `buffer` entirely, and will error if this is + /// not possible. The contents of `buffer` are unpredictable if an error is + /// returned. + fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error>; + + /// Verifies that the save media has been successfully written, comparing + /// it against the given buffer. + fn verify(&self, offset: usize, buffer: &[u8]) -> Result; + + /// Prepares a given span of sectors for writing. This may permanently erase + /// the current contents of the sector on some save media. + fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error>; + + /// Writes a buffer to the save media. + /// + /// The sectors you are writing to must be prepared with a call to the + /// `prepare_write` function beforehand, or else the contents of the save + /// media may be unpredictable after writing. + fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error>; +} + +/// Contains the current save media implementation. +static CURRENT_SAVE_ACCESS: Mutex>> = + Mutex::new(Cell::new(None)); + +/// Sets the save media implementation in use. +pub fn set_save_implementation(access: Option<&'static dyn RawSaveAccess>) { + crate::interrupt::free(|c| { + CURRENT_SAVE_ACCESS.borrow(c).set(access) + }) +} + +/// Gets the save media implementation in use. +pub fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { + crate::interrupt::free(|c| { + CURRENT_SAVE_ACCESS.borrow(c).get() + }) +} + +/// Allows reading and writing of save media. +#[derive(Copy, Clone)] +pub struct SaveAccess { + access: &'static dyn RawSaveAccess, + info: &'static MediaInfo, +} +impl SaveAccess { + /// Creates a new save accessor around the current save implementaiton. + pub fn new() -> Result { + match get_save_implementation() { + Some(access) => Ok(SaveAccess { access, info: access.info()? }), + None => Err(Error::NoMedia), + } + } + + /// Returns the media info underlying this accessor. + pub fn media_info(&self) -> &'static MediaInfo { + 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 + } + + /// Copies data from the save media to a buffer. + pub fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + self.access.read(offset, buffer) + } + + /// Verifies that a given block of memory matches the save media. + pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + self.access.verify(offset, buffer) + } + + /// Returns whether this save media requires the use of [`SaveAccess::prepare_write`]. + pub fn requires_prepare_write(&self) -> bool { + self.info.requires_prepare_write + } + + /// 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) -> Range { + 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(&self, range: Range) -> Result<(), Error> { + if self.info.requires_prepare_write { + let range = self.align_range(range); + let shift = self.info.sector_shift; + self.access.prepare_write(range.start >> shift, range.len() >> shift) + } else { + Ok(()) + } + } + + /// Writes a given buffer into the save media. + /// + /// If [`requires_prepare_write`](`SaveAccess::requires_prepare_write`) returns + /// `true`, you must call [`prepare_write`](`SaveAccess::prepare_write`) on the + /// range you intend to write for this to function correctly. The contents of + /// the save media are unpredictable if you do not. + pub fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + self.access.write(offset, buffer) + } + + /// Writes and validates a given buffer into the save media. + /// + /// If [`requires_prepare_write`](`SaveAccess::requires_prepare_write`) returns + /// `true`, you must call [`prepare_write`](`SaveAccess::prepare_write`) on the + /// range you intend to write for this to function correctly. The contents of + /// the save media will be unpredictable if you do not. + /// + /// This function will verify that the write has completed successfully, and + /// return an error if it has not done so. + pub fn write_and_verify(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + self.write(offset, buffer)?; + if !self.verify(offset, buffer)? { + Err(Error::WriteError) + } else { + Ok(()) + } + } +} From d50413a3cccffb716845e696ff2076ab0d15dcbe Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Thu, 15 Sep 2022 23:20:35 -0700 Subject: [PATCH 02/49] Update the code style of the save module to better match agb's philosophy. --- agb/src/lib.rs | 1 - agb/src/save/mod.rs | 227 +++++++++++++++++++++--------------------- agb/src/save/utils.rs | 54 ++++++++++ 3 files changed, 167 insertions(+), 115 deletions(-) create mode 100644 agb/src/save/utils.rs diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 08ba848f..59174b71 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -168,7 +168,6 @@ pub use agb_fixnum as fixnum; pub mod hash_map; /// Simple random number generator pub mod rng; -/// Implements save games. pub mod save; mod single; /// Implements sound output. diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index c4fad1a2..bcc0e6c0 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -1,9 +1,5 @@ //! Module for reading and writing to save media. //! -//! This module provides both specific interfaces that directly access particular -//! types of save media, and an abstraction layer that allows access to all kinds -//! of save media using a shared interface. -//! //! ## Save media types //! //! There are, broadly speaking, three different kinds of save media that can be @@ -35,11 +31,6 @@ //! * For 512 byte EEPROM, call [`use_eeprom_512b`]. //! * For 8 KiB EEPROM, call [`use_eeprom_8k`]. //! -//! Then, call [`set_timer_for_timeout`] to set the timer you intend to use to -//! track the timeout that prevents errors with the save media from hanging your -//! game. For more information on GBA timers, see the [`timer`](`crate::timer`) -//! module's documentation. -//! //! TODO Update example //! ```rust,norun //! # use gba::save; @@ -49,11 +40,11 @@ //! //! ## Using save media //! -//! To access save media, use the [`SaveAccess::new`] method to create a new -//! [`SaveAccess`] object. Its methods are used to read or write save media. +//! To access save media, use the [`SaveData::new`] method to create a new +//! [`SaveData`] object. Its methods are used to read or write save media. //! -//! Reading data from the savegame is simple. Use [`read`](`SaveAccess::read`) -//! to copy data from an offset in the savegame into a buffer in memory. +//! Reading data from the savegame is simple. Use [`read`] to copy data from an +//! offset in the savegame into a buffer in memory. //! //! TODO Update example //! ```rust,norun @@ -64,8 +55,8 @@ //! ``` //! //! Writing to save media requires you to prepare the area for writing by calling -//! the [`prepare_write`](`SaveAccess::prepare_write`) method before doing the -//! actual write commands with the [`write`](`SaveAccess::write`) method. +//! the [`prepare_write`] method to return a [`SavePreparedBlock`], which contains +//! the actual [`write`] method. //! //! TODO Update example //! ```rust,norun @@ -84,48 +75,43 @@ //! //! Because writes can only be prepared on a per-sector basis, a clear on a range //! of `4000..5000` on a device with 4096 byte sectors will actually clear a range -//! of `0..8192`. Use [`sector_size`](`SaveAccess::sector_size`) to find the -//! sector size, or [`align_range`](`SaveAccess::align_range`) to directly -//! calculate the range of memory that will be affected by the clear. +//! of `0..8192`. Use [`sector_size`] to find the sector size, or [`align_range`] +//! to directly calculate the range of memory that will be affected by the clear. +//! +//! [`read`]: SaveData::read +//! [`prepare_write`]: SaveData::prepare_write +//! [`write`]: SavePreparedBlock::write +//! [`sector_size`]: SaveAccess::sector_size +//! [`align_range`]: SaveAccess::align_range //! //! ## Performance and Other Details //! -//! Because `prepare_write` does nothing on non-flash chips, it would not cause -//! correctness issues to ignore it. Even so, it is recommend to write code to -//! use the `prepare_write` function regardless of the save media, as it has -//! minimal runtime cost on other save media types. If needed, you can check if -//! `prepare_write` is required by calling the -//! (`requires_prepare_write`)(`SaveAccess::requires_prepare_write`) method. +//! The performance characteristics of the media types are as follows: //! -//! Some memory types have a `sector_size` above `1`, but do not use -//! `prepare_write`. This indicates that the media type has sectors that must -//! be rewritten all at once, instead of supporting the separate erase/write -//! cycles that flash media does. Writing non-sector aligned memory will be -//! slower on such save media, as the implementation needs to read the old -//! contents into a buffer before writing to avoid data loss. -//! -//! To summarize, for all supported media types: -//! -//! * SRAM does not require `prepare_write` and has no sectors to align to. Reads -//! and writes at any alignment are efficient. Furthermore, it does not require -//! a timer to be set with [`set_timer_for_timeout`]. -//! * Non-Atmel flash chips requires `prepare_write`, and have sectors of 4096 -//! bytes. Atmel flash chips instead do not require `prepare_write`, and instead -//! have sectors of 128 bytes. You should generally try to use `prepare_write` -//! regardless, and write in blocks of 128 bytes if at all possible. -//! * EEPROM does not require `prepare_write` and has sectors of 8 bytes. +//! * SRAM is simply a form of battery backed memory, and has no particular +//! performance characteristics. Reads and writes at any alignment are +//! efficient. Furthermore, no timer is needed for accesses to this type of +//! media. `prepare_write` does not immediately erase any data. +//! * Non-Atmel flash chips have a sector size of 4096 bytes. Reads and writes +//! to any alignment are efficient, however, `prepare_write` will erase all +//! data in an entire sector before writing. +//! * Atmel flash chips have a sector size of 128 bytes. Reads to any alignment +//! are efficient, however, unaligned writes are extremely slow. +//! `prepare_write` does not immediately erase any data. +//! * EEPROM has a sector size of 8 bytes. Unaligned reads and writes are +//! slower than aligned writes, however, this is easily mitigated by the +//! small sector size. -use core::cell::Cell; use core::ops::Range; -use bare_metal::Mutex; +use crate::sync::{Mutex, RawMutexGuard}; +use crate::timer::Timer; mod asm_utils; //mod setup; -//mod utils; +mod utils; //pub use asm_utils::*; //pub use setup::*; -//pub use utils::*; //pub mod eeprom; //pub mod flash; @@ -171,6 +157,7 @@ pub enum Error { /// Information about the save media used. #[derive(Clone, Debug)] +#[non_exhaustive] pub struct MediaInfo { /// The type of save media installed. pub media_type: MediaType, @@ -181,75 +168,49 @@ pub struct MediaInfo { pub sector_shift: usize, /// The size of the save media, in sectors. pub sector_count: usize, - /// Whether the save media type requires the use of the - /// [`prepare_write`](`SaveAccess::prepare_write`) function before a block of - /// memory can be overwritten. - pub requires_prepare_write: bool, + /// Whether the save media type requires media be prepared before writing. + pub uses_prepare_write: bool, } /// A trait allowing low-level saving and writing to save media. -/// -/// It exposes an interface mostly based around the requirements of reading and -/// writing flash memory, as those are the most restrictive. -/// -/// This interface treats memory as a continuous block of bytes for purposes of -/// reading, and as an array of sectors . -pub trait RawSaveAccess: Sync { - /// Returns information about the save media used. +trait RawSaveAccess: Sync { fn info(&self) -> Result<&'static MediaInfo, Error>; - - /// Reads a slice of memory from save media. - /// - /// This will attempt to fill `buffer` entirely, and will error if this is - /// not possible. The contents of `buffer` are unpredictable if an error is - /// returned. fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error>; - - /// Verifies that the save media has been successfully written, comparing - /// it against the given buffer. fn verify(&self, offset: usize, buffer: &[u8]) -> Result; - - /// Prepares a given span of sectors for writing. This may permanently erase - /// the current contents of the sector on some save media. fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error>; - - /// Writes a buffer to the save media. - /// - /// The sectors you are writing to must be prepared with a call to the - /// `prepare_write` function beforehand, or else the contents of the save - /// media may be unpredictable after writing. fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error>; } -/// Contains the current save media implementation. -static CURRENT_SAVE_ACCESS: Mutex>> = - Mutex::new(Cell::new(None)); +static CURRENT_SAVE_ACCESS: Mutex> = Mutex::new(None); -/// Sets the save media implementation in use. -pub fn set_save_implementation(access: Option<&'static dyn RawSaveAccess>) { - crate::interrupt::free(|c| { - CURRENT_SAVE_ACCESS.borrow(c).set(access) - }) +fn set_save_implementation(access_impl: &'static dyn RawSaveAccess) { + let mut access = CURRENT_SAVE_ACCESS.lock(); + assert!(access.is_none(), "Cannot initialize the savegame engine more than once."); + *access = Some(access_impl); } -/// Gets the save media implementation in use. -pub fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { - crate::interrupt::free(|c| { - CURRENT_SAVE_ACCESS.borrow(c).get() - }) +fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { + *CURRENT_SAVE_ACCESS.lock() } /// Allows reading and writing of save media. #[derive(Copy, Clone)] -pub struct SaveAccess { +pub struct SaveData { + lock: RawMutexGuard<'static>, access: &'static dyn RawSaveAccess, info: &'static MediaInfo, + timeout: utils::Timeout, } -impl SaveAccess { +impl SaveData { /// Creates a new save accessor around the current save implementaiton. - pub fn new() -> Result { + fn new(timer: Option) -> Result { match get_save_implementation() { - Some(access) => Ok(SaveAccess { access, info: access.info()? }), + Some(access) => Ok(SaveData { + lock: utils::lock_media()?, + access, + info: access.info()?, + timeout: utils::Timeout::new(timer), + }), None => Err(Error::NoMedia), } } @@ -275,21 +236,31 @@ impl SaveAccess { self.info.sector_count << self.info.sector_shift } + fn check_bounds(&self, range: Range) -> 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. + /// + /// If an error is returned, the contents of the buffer are unpredictable. pub fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + self.check_bounds_len(offset, buffer.len())?; self.access.read(offset, buffer) } /// Verifies that a given block of memory matches the save media. pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + self.check_bounds_len(offset, buffer.len())?; self.access.verify(offset, buffer) } - /// Returns whether this save media requires the use of [`SaveAccess::prepare_write`]. - pub fn requires_prepare_write(&self) -> bool { - self.info.requires_prepare_write - } - /// 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 @@ -305,41 +276,69 @@ impl SaveAccess { /// 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(&self, range: Range) -> Result<(), Error> { - if self.info.requires_prepare_write { - let range = self.align_range(range); + pub fn prepare_write(&mut self, range: Range) -> Result { + 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) - } else { - Ok(()) + self.access.prepare_write(range.start >> shift, range.len() >> shift)?; } + Ok(SavePreparedBlock { + parent: self, + range + }) } +} +/// A block of save memory that has been prepared for writing. +pub struct SavePreparedBlock<'a> { + parent: &'a mut SaveData, + range: Range, +} +impl<'a> SavePreparedBlock<'a> { /// Writes a given buffer into the save media. /// - /// If [`requires_prepare_write`](`SaveAccess::requires_prepare_write`) returns - /// `true`, you must call [`prepare_write`](`SaveAccess::prepare_write`) on the - /// range you intend to write for this to function correctly. The contents of - /// the save media are unpredictable if you do not. + /// Multiple overlapping writes to the same memory range without a separate + /// 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 + /// unpredictable. pub fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { - self.access.write(offset, buffer) + if buffer.len() == 0 { + Ok(()) + } else if !self.range.contains(&offset) || + !self.range.contains(&(offset + buffer.len() - 1)) { + Err(Error::OutOfBounds) + } else { + self.parent.access.write(offset, buffer) + } } /// Writes and validates a given buffer into the save media. /// - /// If [`requires_prepare_write`](`SaveAccess::requires_prepare_write`) returns - /// `true`, you must call [`prepare_write`](`SaveAccess::prepare_write`) on the - /// range you intend to write for this to function correctly. The contents of - /// the save media will be unpredictable if you do not. - /// /// This function will verify that the write has completed successfully, and /// return an error if it has not done so. + /// + /// Multiple overlapping writes to the same memory range without a separate + /// 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 + /// unpredictable. pub fn write_and_verify(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { self.write(offset, buffer)?; - if !self.verify(offset, buffer)? { + if !self.parent.verify(offset, buffer)? { Err(Error::WriteError) } else { Ok(()) } } } + +/// Allows access to the cartridge's save data. +pub struct SaveManager; +impl SaveManager { + pub fn access() -> Result { + SaveData::new(None) + } + pub fn access_with_timer(timer: Timer) -> Result { + SaveData::new(Some(timer)) + } +} \ No newline at end of file diff --git a/agb/src/save/utils.rs b/agb/src/save/utils.rs new file mode 100644 index 00000000..2ada1eeb --- /dev/null +++ b/agb/src/save/utils.rs @@ -0,0 +1,54 @@ +//! A package containing useful utilities for writing save accessors. + +use super::Error; +use crate::sync::{RawMutex, RawMutexGuard}; +use crate::timer::{Timer, Divider}; + +/// A timeout type used to prevent hardware errors in save media from hanging +/// the game. +pub struct Timeout { + timer: Option, +} +impl Timeout { + /// Creates a new timeout from the timer passed to [`set_timer_for_timeout`]. + /// + /// ## Errors + /// + /// If another timeout has already been created. + #[inline(never)] + pub fn new(timer: Option) -> Self { + Timeout { timer } + } + + /// Starts this timeout. + pub fn start(&mut self) { + if let Some(timer) = &mut self.timer { + timer.set_divider(Divider::Divider1024); + timer.set_interrupt(false); + timer.set_overflow_amount(0xFFFF); + timer.set_cascade(false); + timer.set_enabled(true); + } + } + + /// Returns whether a number of milliseconds has passed since the last call + /// to [`Timeout::start()`]. + pub fn check_timeout_met(&self, check_ms: u16) -> bool { + if let Some(timer) = &self.timer { + check_ms * 17 < timer.value() + } else { + false + } + } +} + +/// Tries to obtain a lock on the global lock for save operations. +/// +/// This is used to prevent problems with stateful save media. +pub fn lock_media() -> Result, Error> { + static LOCK: RawMutex = RawMutex::new(); + match LOCK.try_lock() { + Some(x) => Ok(x), + None => Err(Error::MediaInUse), + } +} From ec41db2fc925719ebe4abd1ffd3ed5463cf33737 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Wed, 17 Aug 2022 00:42:34 -0700 Subject: [PATCH 03/49] Reintegrate SRAM save media reader. --- agb/src/lib.rs | 3 + agb/src/save/asm_utils.rs | 30 +-- agb/src/save/mod.rs | 408 +++++++++++++++++++++----------------- agb/src/save/sram.rs | 56 ++++++ 4 files changed, 302 insertions(+), 195 deletions(-) create mode 100644 agb/src/save/sram.rs diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 59174b71..764cdbfa 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -224,6 +224,8 @@ pub struct Gba { pub sound: sound::dmg::Sound, /// Manages access to the Game Boy Advance's direct sound mixer for playing raw wav files. 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. pub timers: timer::TimerController, } @@ -240,6 +242,7 @@ impl Gba { display: display::Display::new(), sound: sound::dmg::Sound::new(), mixer: sound::mixer::MixerController::new(), + save: save::SaveManager::new(), timers: timer::TimerController::new(), } } diff --git a/agb/src/save/asm_utils.rs b/agb/src/save/asm_utils.rs index f9ca1db0..030fe0df 100644 --- a/agb/src/save/asm_utils.rs +++ b/agb/src/save/asm_utils.rs @@ -3,9 +3,9 @@ //! performed via code in WRAM and cannot be accessed by DMA. extern "C" { - fn WramXferBuf(src: *const u8, dst: *mut u8, count: usize); - fn WramReadByte(src: *const u8) -> u8; - fn WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; + fn WramXferBuf(src: *const u8, dst: *mut u8, count: usize); + fn WramReadByte(src: *const u8) -> u8; + fn WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; } /// 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. #[inline(always)] pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { - if dst.len() != 0 { - WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); - } + if dst.len() != 0 { + WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); + } } /// 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. #[inline(always)] pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { - if src.len() != 0 { - WramXferBuf(src.as_ptr(), dst as _, src.len()); - } + if src.len() != 0 { + WramXferBuf(src.as_ptr(), dst as _, src.len()); + } } /// 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. #[inline(always)] pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { - if buf1.len() != 0 { - WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) - } else { - true - } + if buf1.len() != 0 { + WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) + } else { + true + } } /// 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. #[inline(always)] pub unsafe fn read_raw_byte(src: usize) -> u8 { - WramReadByte(src as _) + WramReadByte(src as _) } diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index bcc0e6c0..eef39947 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -107,238 +107,286 @@ use crate::sync::{Mutex, RawMutexGuard}; use crate::timer::Timer; mod asm_utils; -//mod setup; -mod utils; - -//pub use asm_utils::*; -//pub use setup::*; - //pub mod eeprom; //pub mod flash; -//pub mod sram; +mod sram; +mod utils; /// A list of save media types. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +#[non_exhaustive] pub enum MediaType { - /// 32KiB Battery-Backed SRAM or FRAM - Sram32K, - /// 8KiB EEPROM - Eeprom8K, - /// 512B EEPROM - Eeprom512B, - /// 64KiB flash chip - Flash64K, - /// 128KiB flash chip - Flash128K, - /// A user-defined save media type - Custom, + /// 32KiB Battery-Backed SRAM or FRAM + Sram32K, + /// 8KiB EEPROM + Eeprom8K, + /// 512B EEPROM + Eeprom512B, + /// 64KiB flash chip + Flash64K, + /// 128KiB flash chip + Flash128K, } /// The type used for errors encountered while reading or writing save media. #[derive(Clone, Debug)] #[non_exhaustive] pub enum Error { - /// There is no save media attached to this game cart. - NoMedia, - /// Failed to write the data to save media. - WriteError, - /// An operation on save media timed out. - OperationTimedOut, - /// An attempt was made to access save media at an invalid offset. - OutOfBounds, - /// The media is already in use. - /// - /// This can generally only happen in an IRQ that happens during an ongoing - /// save media operation. - MediaInUse, - /// This command cannot be used with the save media in use. - IncompatibleCommand, + /// There is no save media attached to this game cart. + NoMedia, + /// Failed to write the data to save media. + WriteError, + /// An operation on save media timed out. + OperationTimedOut, + /// An attempt was made to access save media at an invalid offset. + OutOfBounds, + /// The media is already in use. + /// + /// This can generally only happen in an IRQ that happens during an ongoing + /// save media operation. + MediaInUse, + /// This command cannot be used with the save media in use. + IncompatibleCommand, } /// Information about the save media used. #[derive(Clone, Debug)] #[non_exhaustive] pub struct MediaInfo { - /// The type of save media installed. - pub media_type: MediaType, - /// The power-of-two size of each sector. Zero represents a sector size of - /// 0, implying sectors are not in use. - /// - /// (For example, 512 byte sectors would return 9 here.) - pub sector_shift: usize, - /// The size of the save media, in sectors. - pub sector_count: usize, - /// Whether the save media type requires media be prepared before writing. - pub uses_prepare_write: bool, + /// The type of save media installed. + pub media_type: MediaType, + /// The power-of-two size of each sector. Zero represents a sector size of + /// 0, implying sectors are not in use. + /// + /// (For example, 512 byte sectors would return 9 here.) + pub sector_shift: usize, + /// The size of the save media, in sectors. + pub sector_count: usize, + /// Whether the save media type requires media be prepared before writing. + pub uses_prepare_write: bool, } /// A trait allowing low-level saving and writing to save media. trait RawSaveAccess: Sync { - fn info(&self) -> Result<&'static MediaInfo, Error>; - fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error>; - fn verify(&self, offset: usize, buffer: &[u8]) -> Result; - fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error>; - fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error>; + fn info(&self) -> Result<&'static MediaInfo, Error>; + fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error>; + fn verify(&self, offset: usize, buffer: &[u8]) -> Result; + fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error>; + fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error>; } static CURRENT_SAVE_ACCESS: Mutex> = Mutex::new(None); fn set_save_implementation(access_impl: &'static dyn RawSaveAccess) { - let mut access = CURRENT_SAVE_ACCESS.lock(); - assert!(access.is_none(), "Cannot initialize the savegame engine more than once."); - *access = Some(access_impl); + let mut access = CURRENT_SAVE_ACCESS.lock(); + assert!(access.is_none(), "Cannot initialize the savegame engine more than once."); + *access = Some(access_impl); } fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { - *CURRENT_SAVE_ACCESS.lock() + *CURRENT_SAVE_ACCESS.lock() } /// Allows reading and writing of save media. -#[derive(Copy, Clone)] pub struct SaveData { - lock: RawMutexGuard<'static>, - access: &'static dyn RawSaveAccess, - info: &'static MediaInfo, - timeout: utils::Timeout, + lock: RawMutexGuard<'static>, + access: &'static dyn RawSaveAccess, + info: &'static MediaInfo, + timeout: utils::Timeout, } impl SaveData { - /// Creates a new save accessor around the current save implementaiton. - fn new(timer: Option) -> Result { - match get_save_implementation() { - Some(access) => Ok(SaveData { - lock: utils::lock_media()?, - access, - info: access.info()?, - timeout: utils::Timeout::new(timer), - }), - None => Err(Error::NoMedia), + /// Creates a new save accessor around the current save implementaiton. + fn new(timer: Option) -> Result { + match get_save_implementation() { + Some(access) => Ok(SaveData { + lock: utils::lock_media()?, + access, + info: access.info()?, + timeout: utils::Timeout::new(timer), + }), + None => Err(Error::NoMedia), + } } - } - /// Returns the media info underlying this accessor. - pub fn media_info(&self) -> &'static MediaInfo { - 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) -> Result<(), Error> { - if range.start >= self.len() || range.end >= self.len() { - Err(Error::OutOfBounds) - } else { - Ok(()) + /// Returns the media info underlying this accessor. + pub fn media_info(&self) -> &'static MediaInfo { + self.info } - } - 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. - /// - /// If an error is returned, the contents of the buffer are unpredictable. - pub fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { - self.check_bounds_len(offset, buffer.len())?; - self.access.read(offset, buffer) - } - - /// Verifies that a given block of memory matches the save media. - pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result { - 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) -> Range { - 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) -> Result { - 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)?; + /// 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) -> 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. + /// + /// If an error is returned, the contents of the buffer are unpredictable. + pub fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + self.check_bounds_len(offset, buffer.len())?; + self.access.read(offset, buffer) + } + + /// Verifies that a given block of memory matches the save media. + pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + 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) -> Range { + 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) -> Result { + 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. pub struct SavePreparedBlock<'a> { - parent: &'a mut SaveData, - range: Range, + parent: &'a mut SaveData, + range: Range, } impl<'a> SavePreparedBlock<'a> { - /// Writes a given buffer into the save media. - /// - /// Multiple overlapping writes to the same memory range without a separate - /// 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 - /// unpredictable. - pub fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { - if buffer.len() == 0 { - Ok(()) - } else if !self.range.contains(&offset) || - !self.range.contains(&(offset + buffer.len() - 1)) { - Err(Error::OutOfBounds) - } else { - self.parent.access.write(offset, buffer) + /// Writes a given buffer into the save media. + /// + /// Multiple overlapping writes to the same memory range without a separate + /// 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 + /// unpredictable. + pub fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + if buffer.len() == 0 { + Ok(()) + } else if !self.range.contains(&offset) || + !self.range.contains(&(offset + buffer.len() - 1)) { + Err(Error::OutOfBounds) + } else { + self.parent.access.write(offset, buffer) + } } - } - /// Writes and validates a given buffer into the save media. - /// - /// This function will verify that the write has completed successfully, and - /// return an error if it has not done so. - /// - /// Multiple overlapping writes to the same memory range without a separate - /// 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 - /// unpredictable. - pub fn write_and_verify(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { - self.write(offset, buffer)?; - if !self.parent.verify(offset, buffer)? { - Err(Error::WriteError) - } else { - Ok(()) + /// Writes and validates a given buffer into the save media. + /// + /// This function will verify that the write has completed successfully, and + /// return an error if it has not done so. + /// + /// Multiple overlapping writes to the same memory range without a separate + /// 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 + /// unpredictable. + pub fn write_and_verify(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + self.write(offset, buffer)?; + if !self.parent.verify(offset, buffer)? { + Err(Error::WriteError) + } else { + Ok(()) + } + } +} + +mod marker { + #[repr(align(4))] + struct Align(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. -pub struct SaveManager; +#[non_exhaustive] +pub struct SaveManager {} impl SaveManager { - pub fn access() -> Result { - SaveData::new(None) - } - pub fn access_with_timer(timer: Timer) -> Result { - SaveData::new(Some(timer)) - } + pub(crate) const fn new() -> Self { + SaveManager {} + } + + /// 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::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::new(Some(timer)) + } } \ No newline at end of file diff --git a/agb/src/save/sram.rs b/agb/src/save/sram.rs new file mode 100644 index 00000000..b4e21eca --- /dev/null +++ b/agb/src/save/sram.rs @@ -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 { + 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(()) + } +} From 2be44c12e5ec5f560d32e29bf035483c0a4fb149 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Wed, 17 Aug 2022 01:04:07 -0700 Subject: [PATCH 04/49] Improve codestyle in asm_routines.s, remove unsafe save implementation in hyperspace-roll. --- agb/src/asm_include.s | 16 +++++++ agb/src/save/asm_routines.s | 64 ++++++---------------------- agb/src/save/asm_utils.rs | 14 +++--- agb/src/save/mod.rs | 7 +-- examples/hyperspace-roll/src/main.rs | 7 +-- examples/hyperspace-roll/src/save.rs | 56 ++++++++++++------------ 6 files changed, 70 insertions(+), 94 deletions(-) diff --git a/agb/src/asm_include.s b/agb/src/asm_include.s index 5508e4fe..1682792c 100644 --- a/agb/src/asm_include.s +++ b/agb/src/asm_include.s @@ -13,3 +13,19 @@ .size \functionName,.-\functionName .endfunc .endm + +.macro agb_thumb_func functionName:req +.section .iwram.\functionName, "ax", %progbits +.thumb +.align 2 +.global \functionName +.type \functionName, %function +.func \functionName +\functionName: +.endm + +.macro agb_thumb_end functionName:req +.pool +.size \functionName,.-\functionName +.endfunc +.endm diff --git a/agb/src/save/asm_routines.s b/agb/src/save/asm_routines.s index 41e8df74..b04d407b 100644 --- a/agb/src/save/asm_routines.s +++ b/agb/src/save/asm_routines.s @@ -1,89 +1,49 @@ +.include "src/asm_include.s" + @ @ char WramReadByte(const char* offset); @ @ A routine that reads a byte from a given memory offset. @ - .thumb - .global WramReadByte - .thumb_func - .align 2 -WramReadByte: - ldr r1, =WramReadByteInner - bx r1 - - .section .data - - .thumb - .thumb_func - .align 2 -WramReadByteInner: +agb_thumb_func agb_rs__WramReadByte ldrb r0, [r0] mov pc, lr - - .section .text +agb_thumb_end agb_rs__WramReadByte @ @ bool WramVerifyBuf(const char* buf1, const char* buf2, int count); @ @ A routine that compares two memory offsets. @ - .thumb - .global WramVerifyBuf - .thumb_func - .align 2 -WramVerifyBuf: +agb_thumb_func agb_rs__WramVerifyBuf push {r4-r5, lr} movs r5, r0 @ set up r5 to be r0, so we can use it immediately for the return result movs r0, #0 @ set up r0 so the default return result is false - ldr r4, =WramVerifyBufInner - bx r4 @ jump to the part in WRAM - .section .data - - .thumb - .thumb_func - .align 2 -WramVerifyBufInner: @ At this point, buf1 is actually in r5, so r0 can be used as a status return - ldrb r3, [r5,r2] +1: ldrb r3, [r5,r2] ldrb r4, [r1,r2] cmp r3, r4 bne 0f sub r2, #1 - bpl WramVerifyBufInner + bpl 1b @ Returns from the function successfully movs r0, #1 0: @ Jumps to here return the function unsuccessfully, because r0 contains 0 at this point pop {r4-r5, pc} +agb_thumb_end agb_rs__WramVerifyBuf - .section .text @ @ void WramXferBuf(const char* source, char* dest, int count); @ @ A routine that copies one buffer into another. @ - .thumb - .global WramXferBuf - .thumb_func - .align 2 -WramXferBuf: - ldr r3, =WramXferBufInner - bx r3 - - .pool - .section .data - - .thumb - .thumb_func - .align 2 -WramXferBufInner: - sub r2, #1 +agb_thumb_func agb_rs__WramXferBuf +0: sub r2, #1 ldrb r3, [r0,r2] strb r3, [r1,r2] - bne WramXferBufInner + bne 0b mov pc, lr - - .pool - .section .text +agb_thumb_end agb_rs__WramXferBuf diff --git a/agb/src/save/asm_utils.rs b/agb/src/save/asm_utils.rs index 030fe0df..930995ad 100644 --- a/agb/src/save/asm_utils.rs +++ b/agb/src/save/asm_utils.rs @@ -3,9 +3,9 @@ //! performed via code in WRAM and cannot be accessed by DMA. extern "C" { - fn WramXferBuf(src: *const u8, dst: *mut u8, count: usize); - fn WramReadByte(src: *const u8) -> u8; - fn WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; + fn agb_rs__WramXferBuf(src: *const u8, dst: *mut u8, count: usize); + fn agb_rs__WramReadByte(src: *const u8) -> u8; + fn agb_rs__WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; } /// Copies data from a given memory address into a buffer. @@ -18,7 +18,7 @@ extern "C" { #[inline(always)] pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { if dst.len() != 0 { - WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); + agb_rs__WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); } } @@ -31,7 +31,7 @@ pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { #[inline(always)] pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { if src.len() != 0 { - WramXferBuf(src.as_ptr(), dst as _, src.len()); + agb_rs__WramXferBuf(src.as_ptr(), dst as _, src.len()); } } @@ -45,7 +45,7 @@ pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { #[inline(always)] pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { if buf1.len() != 0 { - WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) + agb_rs__WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) } else { true } @@ -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. #[inline(always)] pub unsafe fn read_raw_byte(src: usize) -> u8 { - WramReadByte(src as _) + agb_rs__WramReadByte(src as _) } diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index eef39947..af9d7927 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -370,7 +370,7 @@ impl SaveManager { /// given save type. /// /// Only one `init_*` function may be called in the lifetime of the program. - pub fn init_sram() { + pub fn init_sram(&mut self) { marker::emit_sram_marker(); set_save_implementation(&sram::BatteryBackedAccess); } @@ -379,14 +379,15 @@ impl SaveManager { /// /// You must have initialized the save manager beforehand to use a specific /// type of media before calling this method. - pub fn access() -> Result { + pub fn access(&mut self) -> Result { 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 { + pub fn access_with_timer(&mut self, timer: Timer) -> Result { SaveData::new(Some(timer)) } } \ No newline at end of file diff --git a/examples/hyperspace-roll/src/main.rs b/examples/hyperspace-roll/src/main.rs index 9acf3c04..9ec4cc1a 100644 --- a/examples/hyperspace-roll/src/main.rs +++ b/examples/hyperspace-roll/src/main.rs @@ -96,10 +96,10 @@ struct Agb<'a> { } fn main(mut gba: agb::Gba) -> ! { - save::init_save(); + save::init_save(&mut gba).expect("Could not initialize save game"); if save::load_high_score() > 1000 { - save::save_high_score(0); + save::save_high_score(&mut gba, 0).expect("Could not reset high score"); } let gfx = gba.display.object.get(); @@ -207,7 +207,8 @@ fn main(mut gba: agb::Gba) -> ! { agb.obj.commit(); agb.sfx.customise(); if save::load_high_score() < current_level { - save::save_high_score(current_level); + save::save_high_score(&mut gba, current_level) + .expect("Could not save high score"); } break; } diff --git a/examples/hyperspace-roll/src/save.rs b/examples/hyperspace-roll/src/save.rs index e5df03e8..0db839de 100644 --- a/examples/hyperspace-roll/src/save.rs +++ b/examples/hyperspace-roll/src/save.rs @@ -1,44 +1,42 @@ -use agb::interrupt::free; -use bare_metal::Mutex; -use core::cell::RefCell; +use agb::Gba; +use agb::save::Error; +use agb::sync::Static; -const RAM_ADDRESS: *mut u8 = 0x0E00_0000 as *mut u8; -const HIGH_SCORE_ADDRESS_START: *mut u8 = RAM_ADDRESS.wrapping_offset(1); +static HIGHSCORE: Static = Static::new(0); -static HIGHSCORE: Mutex> = Mutex::new(RefCell::new(0)); +pub fn init_save(gba: &mut Gba) -> Result<(), Error> { + gba.save.init_sram(); -pub fn init_save() { - if (unsafe { RAM_ADDRESS.read_volatile() } == !0) { - save_high_score(0); - unsafe { RAM_ADDRESS.write_volatile(0) }; - } + let mut access = gba.save.access()?; - let mut a = [0; 4]; - for (idx, a) in a.iter_mut().enumerate() { - *a = unsafe { HIGH_SCORE_ADDRESS_START.add(idx).read_volatile() }; - } + let mut buffer = [0; 1]; + access.read(0, &mut buffer)?; - let high_score = u32::from_le_bytes(a); + if buffer[0] != 0 { + access.prepare_write(0..1)?.write(0, &[0])?; + core::mem::drop(access); + save_high_score(gba, 0)?; + } else { + let mut buffer = [0; 4]; + access.read(1, &mut buffer)?; + let high_score = u32::from_le_bytes(buffer); - free(|cs| { if high_score > 100 { - HIGHSCORE.borrow(cs).replace(0); + HIGHSCORE.write(0) } else { - HIGHSCORE.borrow(cs).replace(high_score); + HIGHSCORE.write(high_score) } - }); + } + + Ok(()) } pub fn load_high_score() -> u32 { - free(|cs| *HIGHSCORE.borrow(cs).borrow()) + HIGHSCORE.read() } -pub fn save_high_score(score: u32) { - let a = score.to_le_bytes(); - - for (idx, &a) in a.iter().enumerate() { - unsafe { HIGH_SCORE_ADDRESS_START.add(idx).write_volatile(a) }; - } - - free(|cs| HIGHSCORE.borrow(cs).replace(score)); +pub fn save_high_score(gba: &mut Gba, score: u32) -> Result<(), Error> { + gba.save.access()?.prepare_write(1..5)?.write(1, &score.to_le_bytes())?; + HIGHSCORE.write(score); + Ok(()) } From 4397bb0d6649c662ab2435cf4241dfd872b218b7 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Thu, 15 Sep 2022 23:21:10 -0700 Subject: [PATCH 05/49] Add tests for cartridge save access. --- agb-tests/.cargo/config.toml | 14 ++++ agb-tests/Cargo.toml | 19 +++++ agb-tests/rust-toolchain.toml | 3 + agb-tests/tests/save_test_common/mod.rs | 105 ++++++++++++++++++++++++ agb-tests/tests/test_save_sram.rs | 16 ++++ agb/src/save/mod.rs | 18 +++- agb/src/sync/statics.rs | 2 +- justfile | 3 + 8 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 agb-tests/.cargo/config.toml create mode 100644 agb-tests/Cargo.toml create mode 100644 agb-tests/rust-toolchain.toml create mode 100644 agb-tests/tests/save_test_common/mod.rs create mode 100644 agb-tests/tests/test_save_sram.rs diff --git a/agb-tests/.cargo/config.toml b/agb-tests/.cargo/config.toml new file mode 100644 index 00000000..d5f7f86c --- /dev/null +++ b/agb-tests/.cargo/config.toml @@ -0,0 +1,14 @@ +[unstable] +build-std = ["core", "alloc"] +build-std-features = ["compiler-builtins-mem"] + +[build] +target = "thumbv4t-none-eabi" + +[target.thumbv4t-none-eabi] +rustflags = ["-Clink-arg=-T../agb/gba.ld", "-Ctarget-cpu=arm7tdmi"] +runner = "mgba-test-runner" + +[target.armv4t-none-eabi] +rustflags = ["-Clink-arg=-T../agb/gba.ld", "-Ctarget-cpu=arm7tdmi"] +runner = "mgba-test-runner" diff --git a/agb-tests/Cargo.toml b/agb-tests/Cargo.toml new file mode 100644 index 00000000..9917f81b --- /dev/null +++ b/agb-tests/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "agb-tests" +version = "0.1.0" +edition = "2018" + +[profile.dev] +opt-level = 3 +debug = true + +[profile.release] +lto = true +debug = true + +[dependencies] +agb = { version = "*", path = "../agb", features = ["testing"] } + +[package.metadata.docs.rs] +default-target = "thumbv6m-none-eabi" +targets = [] diff --git a/agb-tests/rust-toolchain.toml b/agb-tests/rust-toolchain.toml new file mode 100644 index 00000000..06842486 --- /dev/null +++ b/agb-tests/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src", "clippy"] \ No newline at end of file diff --git a/agb-tests/tests/save_test_common/mod.rs b/agb-tests/tests/save_test_common/mod.rs new file mode 100644 index 00000000..9e494424 --- /dev/null +++ b/agb-tests/tests/save_test_common/mod.rs @@ -0,0 +1,105 @@ +use core::cmp; +use agb::save::{Error, MediaInfo}; +use agb::sync::InitOnce; + +fn init_sram(gba: &mut agb::Gba) -> &'static MediaInfo { + static ONCE: InitOnce = InitOnce::new(); + ONCE.get(|| { + crate::save_setup(gba); + gba.save.access().unwrap().media_info().clone() + }) +} + +#[derive(Clone)] +struct Rng(u32); +impl Rng { + fn iter(&mut self) { + self.0 = self.0.wrapping_mul(2891336453).wrapping_add(100001); + } + fn next_u8(&mut self) -> u8 { + self.iter(); + (self.0 >> 22) as u8 ^ self.0 as u8 + } + fn next_under(&mut self, under: u32) -> u32 { + self.iter(); + let scale = 31 - under.leading_zeros(); + ((self.0 >> scale) ^ self.0) % under + } +} + +const MAX_BLOCK_SIZE: usize = 4 * 1024; + +#[allow(clippy::needless_range_loop)] +fn do_test( + gba: &mut agb::Gba, seed: Rng, offset: usize, len: usize, block_size: usize, +) -> Result<(), Error> { + let mut buffer = [0; MAX_BLOCK_SIZE]; + + let timers = gba.timers.timers(); + let mut access = gba.save.access_with_timer(timers.timer2)?; + + // writes data to the save media + let prepared = access.prepare_write(offset..offset + len)?; + let mut rng = seed.clone(); + let mut current = offset; + let end = offset + len; + while current != end { + let cur_len = cmp::min(end - current, block_size); + for i in 0..cur_len { + buffer[i] = rng.next_u8(); + } + prepared.write(current, &buffer[..cur_len])?; + current += cur_len; + } + + // validates the save media + rng = seed; + current = offset; + while current != end { + let cur_len = cmp::min(end - current, block_size); + access.read(current, &mut buffer[..cur_len])?; + for i in 0..cur_len { + let cur_byte = rng.next_u8(); + assert_eq!( + buffer[i], cur_byte, + "Read does not match earlier write: {} != {} @ 0x{:05x}", + buffer[i], cur_byte, current + i, + ); + } + current += cur_len; + } + + Ok(()) +} + +#[test_case] +fn test_4k_blocks(gba: &mut agb::Gba) { + let info = init_sram(gba); + + if info.len() >= (1 << 12) { + do_test(gba, Rng(2000), 0, info.len(), 4 * 1024).expect("Test encountered error"); + } +} + +#[test_case] +fn test_512b_blocks(gba: &mut agb::Gba) { + let info = init_sram(gba); + do_test(gba, Rng(1000), 0, info.len(), 512).expect("Test encountered error"); +} + +#[test_case] +fn test_partial_writes(gba: &mut agb::Gba) { + let info = init_sram(gba); + + // test with random segments now. + let mut rng = Rng(12345); + for i in 0..8 { + let rand_length = rng.next_under((info.len() >> 1) as u32) as usize + 50; + let rand_offset = rng.next_under(info.len() as u32 - rand_length as u32) as usize; + let block_size = cmp::min(rand_length >> 2, MAX_BLOCK_SIZE - 100); + let block_size = rng.next_under(block_size as u32) as usize + 50; + + do_test(gba, Rng(i * 10000), rand_offset, rand_length, block_size) + .expect("Test encountered error"); + } +} \ No newline at end of file diff --git a/agb-tests/tests/test_save_sram.rs b/agb-tests/tests/test_save_sram.rs new file mode 100644 index 00000000..f3348eda --- /dev/null +++ b/agb-tests/tests/test_save_sram.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_sram(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index af9d7927..7de71242 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -165,6 +165,18 @@ pub struct MediaInfo { /// Whether the save media type requires media be prepared before writing. pub uses_prepare_write: bool, } +impl MediaInfo { + /// 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.sector_shift + } + + /// Returns the total length of this save media. + pub fn len(&self) -> usize { + self.sector_count << self.sector_shift + } +} /// A trait allowing low-level saving and writing to save media. trait RawSaveAccess: Sync { @@ -221,16 +233,16 @@ impl SaveData { /// 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 + self.info.sector_size() } /// Returns the total length of this save media. pub fn len(&self) -> usize { - self.info.sector_count << self.info.sector_shift + self.info.len() } fn check_bounds(&self, range: Range) -> Result<(), Error> { - if range.start >= self.len() || range.end >= self.len() { + if range.start >= self.len() || range.end > self.len() { Err(Error::OutOfBounds) } else { Ok(()) diff --git a/agb/src/sync/statics.rs b/agb/src/sync/statics.rs index 3970589f..62ec9d1d 100644 --- a/agb/src/sync/statics.rs +++ b/agb/src/sync/statics.rs @@ -284,7 +284,7 @@ mod test { // the actual main test loop let mut interrupt_seen = false; let mut no_interrupt_seen = false; - for i in 0..100000 { + for i in 0..250000 { // write to the static let new_value = [i; COUNT]; value.write(new_value); diff --git a/justfile b/justfile index 23837d70..ba8f243c 100644 --- a/justfile +++ b/justfile @@ -13,6 +13,7 @@ clippy: test: just _test-debug agb + just _test-debug agb-tests just _test-debug agb-fixnum just _test-debug-arm agb just _test-debug tools @@ -20,6 +21,8 @@ test: test-release: just _test-release agb just _test-release-arm agb + just _test-release agb-tests + just _test-release-arm agb-tests doctest-agb: (cd agb && cargo test --doc -Z doctest-xcompile) From 8dd0f4768ab379b88debb6c2b18f19255e6b1078 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Wed, 17 Aug 2022 04:27:42 -0700 Subject: [PATCH 06/49] Add support for flash save media. --- agb-tests/tests/save_test_common/mod.rs | 2 +- agb-tests/tests/test_save_flash_128k.rs | 16 + agb-tests/tests/test_save_flash_64k.rs | 16 + agb/src/save/flash.rs | 470 ++++++++++++++++++++++++ agb/src/save/mod.rs | 63 +++- agb/src/save/sram.rs | 9 +- agb/src/save/utils.rs | 72 ++-- 7 files changed, 591 insertions(+), 57 deletions(-) create mode 100644 agb-tests/tests/test_save_flash_128k.rs create mode 100644 agb-tests/tests/test_save_flash_64k.rs create mode 100644 agb/src/save/flash.rs diff --git a/agb-tests/tests/save_test_common/mod.rs b/agb-tests/tests/save_test_common/mod.rs index 9e494424..d3f823c2 100644 --- a/agb-tests/tests/save_test_common/mod.rs +++ b/agb-tests/tests/save_test_common/mod.rs @@ -39,7 +39,7 @@ fn do_test( let mut access = gba.save.access_with_timer(timers.timer2)?; // writes data to the save media - let prepared = access.prepare_write(offset..offset + len)?; + let mut prepared = access.prepare_write(offset..offset + len)?; let mut rng = seed.clone(); let mut current = offset; let end = offset + len; diff --git a/agb-tests/tests/test_save_flash_128k.rs b/agb-tests/tests/test_save_flash_128k.rs new file mode 100644 index 00000000..7256ddfb --- /dev/null +++ b/agb-tests/tests/test_save_flash_128k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_flash_128k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb-tests/tests/test_save_flash_64k.rs b/agb-tests/tests/test_save_flash_64k.rs new file mode 100644 index 00000000..6c179ae3 --- /dev/null +++ b/agb-tests/tests/test_save_flash_64k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_flash_64k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/src/save/flash.rs b/agb/src/save/flash.rs new file mode 100644 index 00000000..a3b470e5 --- /dev/null +++ b/agb/src/save/flash.rs @@ -0,0 +1,470 @@ +//! Module for flash save media support. +//! +//! Flash may be read with ordinary read commands, but writing requires +//! sending structured commands to the flash chip. + +// TODO: Setup cartridge read timings for faster Flash access. + +use crate::memory_mapped::{MemoryMapped, MemoryMapped1DArray}; +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::asm_utils::*; +use crate::sync::{InitOnce, Static}; +use core::cmp; +use crate::save::utils::Timeout; + +// Volatile address ports for flash +const FLASH_PORT_BANK: MemoryMapped = unsafe { MemoryMapped::new(0x0E000000) }; +const FLASH_PORT_A: MemoryMapped = unsafe { MemoryMapped::new(0x0E005555) }; +const FLASH_PORT_B: MemoryMapped = unsafe { MemoryMapped::new(0x0E002AAA) }; +const FLASH_DATA: MemoryMapped1DArray = unsafe { MemoryMapped1DArray::new(0x0E000000) }; + +// Various constants related to sector sizes +const BANK_SHIFT: usize = 16; // 64 KiB +const BANK_LEN: usize = 1 << BANK_SHIFT; +const BANK_MASK: usize = BANK_LEN - 1; + +// Constants relating to flash commands. +const CMD_SET_BANK: u8 = 0xB0; +const CMD_READ_CHIP_ID: u8 = 0x90; +const CMD_READ_CONTENTS: u8 = 0xF0; +const CMD_WRITE: u8 = 0xA0; +const CMD_ERASE_SECTOR_BEGIN: u8 = 0x80; +const CMD_ERASE_SECTOR_CONFIRM: u8 = 0x30; +const CMD_ERASE_SECTOR_ALL: u8 = 0x10; + +/// Starts a command to the flash chip. +fn start_flash_command() { + FLASH_PORT_A.set(0xAA); + FLASH_PORT_B.set(0x55); +} + +/// Helper function for issuing commands to the flash chip. +fn issue_flash_command(c2: u8) { + start_flash_command(); + FLASH_PORT_A.set(c2); +} + +/// A simple thing to avoid excessive bank switches +static CURRENT_BANK: Static = Static::new(!0); +fn set_bank(bank: u8) -> Result<(), Error> { + if bank == 0xFF { + Err(Error::OutOfBounds) + } else if bank != CURRENT_BANK.read() { + issue_flash_command(CMD_SET_BANK); + FLASH_PORT_BANK.set(bank as u8); + CURRENT_BANK.write(bank); + Ok(()) + } else { + Ok(()) + } +} + +/// Identifies a particular f +/// lash chip in use by a Game Pak. +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] +#[repr(u8)] +pub enum FlashChipType { + /// 64KiB SST chip + Sst64K, + /// 64KiB Macronix chip + Macronix64K, + /// 64KiB Panasonic chip + Panasonic64K, + /// 64KiB Atmel chip + Atmel64K, + /// 128KiB Sanyo chip + Sanyo128K, + /// 128KiB Macronix chip + Macronix128K, + /// An unidentified chip + Unknown, +} +impl FlashChipType { + /// Returns the type of the flash chip currently in use. + pub fn detect() -> Result { + Ok(Self::from_id(detect_chip_id()?)) + } + + /// Determines the flash chip type from an ID. + pub fn from_id(id: u16) -> Self { + match id { + 0xD4BF => FlashChipType::Sst64K, + 0x1CC2 => FlashChipType::Macronix64K, + 0x1B32 => FlashChipType::Panasonic64K, + 0x3D1F => FlashChipType::Atmel64K, + 0x1362 => FlashChipType::Sanyo128K, + 0x09C2 => FlashChipType::Macronix128K, + _ => FlashChipType::Unknown, + } + } +} + +/// Determines the raw ID of the flash chip currently in use. +pub fn detect_chip_id() -> Result { + issue_flash_command(CMD_READ_CHIP_ID); + let high = unsafe { read_raw_byte(0x0E000001) }; + let low = unsafe { read_raw_byte(0x0E000000) }; + let id = (high as u16) << 8 | low as u16; + issue_flash_command(CMD_READ_CONTENTS); + Ok(id) +} + +/// Information relating to a particular flash chip that could be found in a +/// Game Pak. +#[allow(dead_code)] +struct ChipInfo { + /// The wait state required to read from the chip. + read_wait: u8, + /// The wait state required to write to the chip. + write_wait: u8, + + /// The timeout in milliseconds for writes to this chip. + write_timeout: u16, + /// The timeout in milliseconds for erasing a sector in this chip. + erase_sector_timeout: u16, + /// The timeout in milliseconds for erasing the entire chip. + erase_chip_timeout: u16, + + /// The number of 64KiB banks in this chip. + bank_count: u8, + /// Whether this is an Atmel chip, which has 128 byte sectors instead of 4K. + uses_atmel_api: bool, + /// Whether this is an Macronix chip, which requires an additional command + /// to cancel the current action after a timeout. + requires_cancel_command: bool, + + /// The [`MediaInfo`] to return for this chip type. + info: &'static MediaInfo, +} + +// Media info for the various chipsets. +static INFO_64K: MediaInfo = MediaInfo { + media_type: MediaType::Flash64K, + sector_shift: 12, // 4 KiB + sector_count: 16, // 4 KiB * 16 = 64 KiB + uses_prepare_write: true, +}; +static INFO_64K_ATMEL: MediaInfo = MediaInfo { + media_type: MediaType::Flash64K, + sector_shift: 7, // 128 bytes + sector_count: 512, // 128 bytes * 512 = 64 KiB + uses_prepare_write: false, +}; +static INFO_128K: MediaInfo = MediaInfo { + media_type: MediaType::Flash128K, + sector_shift: 12, + sector_count: 32, // 4 KiB * 32 = 128 KiB + uses_prepare_write: true, +}; + +// Chip info for the various chipsets. +static CHIP_INFO_SST_64K: ChipInfo = ChipInfo { + read_wait: 2, // 2 cycles + write_wait: 1, // 3 cycles + write_timeout: 10, + erase_sector_timeout: 40, + erase_chip_timeout: 200, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_64K, +}; +static CHIP_INFO_MACRONIX_64K: ChipInfo = ChipInfo { + read_wait: 1, // 3 cycles + write_wait: 3, // 8 cycles + write_timeout: 10, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: true, + info: &INFO_64K, +}; +static CHIP_INFO_PANASONIC_64K: ChipInfo = ChipInfo { + read_wait: 2, // 2 cycles + write_wait: 0, // 4 cycles + write_timeout: 10, + erase_sector_timeout: 500, + erase_chip_timeout: 500, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_64K, +}; +static CHIP_INFO_ATMEL_64K: ChipInfo = ChipInfo { + read_wait: 3, // 8 cycles + write_wait: 3, // 8 cycles + write_timeout: 40, + erase_sector_timeout: 40, + erase_chip_timeout: 40, + bank_count: 1, + uses_atmel_api: true, + requires_cancel_command: false, + info: &INFO_64K_ATMEL, +}; +static CHIP_INFO_GENERIC_64K: ChipInfo = ChipInfo { + read_wait: 3, // 8 cycles + write_wait: 3, // 8 cycles + write_timeout: 40, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: true, + info: &INFO_128K, +}; +static CHIP_INFO_GENERIC_128K: ChipInfo = ChipInfo { + read_wait: 1, // 3 cycles + write_wait: 3, // 8 cycles + write_timeout: 10, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 2, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_128K, +}; + +impl FlashChipType { + /// Returns the internal info for this chip. + fn chip_info(&self) -> &'static ChipInfo { + match *self { + FlashChipType::Sst64K => &CHIP_INFO_SST_64K, + FlashChipType::Macronix64K => &CHIP_INFO_MACRONIX_64K, + FlashChipType::Panasonic64K => &CHIP_INFO_PANASONIC_64K, + FlashChipType::Atmel64K => &CHIP_INFO_ATMEL_64K, + FlashChipType::Sanyo128K => &CHIP_INFO_GENERIC_128K, + FlashChipType::Macronix128K => &CHIP_INFO_GENERIC_128K, + FlashChipType::Unknown => &CHIP_INFO_GENERIC_64K, + } + } +} +static CHIP_INFO: InitOnce<&'static ChipInfo> = InitOnce::new(); +fn cached_chip_info() -> Result<&'static ChipInfo, Error> { + CHIP_INFO + .try_get(|| -> Result<_, Error> { Ok(FlashChipType::detect()?.chip_info()) }) + .map(Clone::clone) +} + +/// Actual implementation of the ChipInfo functions. +impl ChipInfo { + /// Returns the total length of this chip. + fn total_len(&self) -> usize { + self.info.sector_count << self.info.sector_shift + } + + // Checks whether a byte offset is in bounds. + fn check_len(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_some() && offset + len <= self.total_len() { + Ok(()) + } else { + Err(Error::OutOfBounds) + } + } + + // Checks whether a sector offset is in bounds. + fn check_sector_len(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_some() && offset + len <= self.info.sector_count { + Ok(()) + } else { + Err(Error::OutOfBounds) + } + } + + /// Sets the currently active bank. + fn set_bank(&self, bank: usize) -> Result<(), Error> { + if bank >= self.bank_count as usize { + Err(Error::OutOfBounds) + } else if self.bank_count > 1 { + set_bank(bank as u8) + } else { + Ok(()) + } + } + + /// Reads a buffer from save media into memory. + fn read_buffer(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { + while buf.len() != 0 { + self.set_bank(offset >> BANK_SHIFT)?; + let start = offset & BANK_MASK; + let end_len = cmp::min(BANK_LEN - start, buf.len()); + unsafe { + read_raw_buf(&mut buf[..end_len], 0x0E000000 + start); + } + buf = &mut buf[end_len..]; + offset += end_len; + } + Ok(()) + } + + /// Verifies that a buffer was properly stored into save media. + fn verify_buffer(&self, mut offset: usize, mut buf: &[u8]) -> Result { + while buf.len() != 0 { + self.set_bank(offset >> BANK_SHIFT)?; + let start = offset & BANK_MASK; + let end_len = cmp::min(BANK_LEN - start, buf.len()); + if !unsafe { verify_raw_buf(&buf[..end_len], 0x0E000000 + start) } { + return Ok(false); + } + buf = &buf[end_len..]; + offset += end_len; + } + Ok(true) + } + + /// Waits for a timeout, or an operation to complete. + fn wait_for_timeout( + &self, offset: usize, val: u8, ms: u16, timeout: &mut Timeout, + ) -> Result<(), Error> { + timeout.start(); + let offset = 0x0E000000 + offset; + + while unsafe { read_raw_byte(offset) != val } { + if timeout.check_timeout_met(ms) { + if self.requires_cancel_command { + FLASH_PORT_A.set(0xF0); + } + return Err(Error::OperationTimedOut); + } + } + Ok(()) + } + + /// Erases a sector to flash. + fn erase_sector(&self, sector: usize, timeout: &mut Timeout) -> Result<(), Error> { + let offset = sector << self.info.sector_shift; + self.set_bank(offset >> BANK_SHIFT)?; + issue_flash_command(CMD_ERASE_SECTOR_BEGIN); + start_flash_command(); + FLASH_DATA.set(offset & BANK_MASK, CMD_ERASE_SECTOR_CONFIRM); + self.wait_for_timeout(offset & BANK_MASK, 0xFF, self.erase_sector_timeout, timeout) + } + + /// Erases the entire chip. + fn erase_chip(&self, timeout: &mut Timeout) -> Result<(), Error> { + issue_flash_command(CMD_ERASE_SECTOR_BEGIN); + issue_flash_command(CMD_ERASE_SECTOR_ALL); + self.wait_for_timeout(0, 0xFF, 3000, timeout) + } + + /// Writes a byte to the save media. + fn write_byte(&self, offset: usize, byte: u8, timeout: &mut Timeout) -> Result<(), Error> { + issue_flash_command(CMD_WRITE); + FLASH_DATA.set(offset, byte); + self.wait_for_timeout(offset, byte, self.write_timeout, timeout) + } + + /// Writes an entire buffer to the save media. + fn write_buffer(&self, offset: usize, buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + self.set_bank(offset >> BANK_SHIFT)?; + for i in 0..buf.len() { + let byte_off = offset + i; + if (byte_off & BANK_MASK) == 0 { + self.set_bank(byte_off >> BANK_SHIFT)?; + } + self.write_byte(byte_off & BANK_MASK, buf[i], timeout)?; + } + Ok(()) + } + + /// Erases and writes an entire 128b sector on Atmel devices. + fn write_atmel_sector_raw( + &self, offset: usize, buf: &[u8], timeout: &mut Timeout, + ) -> Result<(), Error> { + crate::interrupt::free(|_| { + issue_flash_command(CMD_WRITE); + for i in 0..128 { + FLASH_DATA.set(offset + i, buf[i]); + } + self.wait_for_timeout(offset + 127, buf[127], self.erase_sector_timeout, timeout) + })?; + Ok(()) + } + + /// Writes an entire 128b sector on Atmel devices, copying existing data in + /// case of non-sector aligned writes. + #[inline(never)] // avoid allocating the 128 byte buffer for no reason. + fn write_atmel_sector_safe( + &self, offset: usize, buf: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let mut sector = [0u8; 128]; + self.read_buffer(offset, &mut sector[0..start])?; + sector[start..start + buf.len()].copy_from_slice(buf); + self.read_buffer(offset + start + buf.len(), &mut sector[start + buf.len()..128])?; + self.write_atmel_sector_raw(offset, §or, timeout) + } + + /// Writes an entire 128b sector on Atmel devices, copying existing data in + /// case of non-sector aligned writes. + /// + /// This avoids allocating stack if there is no need to. + fn write_atmel_sector( + &self, offset: usize, buf: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + if start == 0 && buf.len() == 128 { + self.write_atmel_sector_raw(offset, buf, timeout) + } else { + self.write_atmel_sector_safe(offset, buf, start, timeout) + } + } +} + +/// The [`RawSaveAccess`] used for flash save media. +pub struct FlashAccess; +impl RawSaveAccess for FlashAccess { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(cached_chip_info()?.info) + } + + fn read(&self, offset: usize, buf: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + chip.read_buffer(offset, buf) + } + + fn verify(&self, offset: usize, buf: &[u8], _: &mut Timeout) -> Result { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + chip.verify_buffer(offset, buf) + } + + fn prepare_write( + &self, sector: usize, count: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_sector_len(sector, count)?; + + if chip.uses_atmel_api { + Ok(()) + } else if count == chip.info.sector_count { + chip.erase_chip(timeout) + } else { + for i in sector..sector + count { + chip.erase_sector(i, timeout)?; + } + Ok(()) + } + } + + fn write(&self, mut offset: usize, mut buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + if chip.uses_atmel_api { + while buf.len() != 0 { + let start = offset & 127; + let end_len = cmp::min(128 - start, buf.len()); + chip.write_atmel_sector(offset & !127, &buf[..end_len], start, timeout)?; + buf = &buf[end_len..]; + offset += end_len; + } + Ok(()) + } else { + // Write the bytes one by one. + chip.write_buffer(offset, buf, timeout)?; + Ok(()) + } + } +} diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index 7de71242..34195676 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -103,12 +103,13 @@ //! small sector size. use core::ops::Range; +use crate::save::utils::Timeout; use crate::sync::{Mutex, RawMutexGuard}; use crate::timer::Timer; mod asm_utils; //pub mod eeprom; -//pub mod flash; +mod flash; mod sram; mod utils; @@ -181,10 +182,10 @@ impl MediaInfo { /// A trait allowing low-level saving and writing to save media. trait RawSaveAccess: Sync { fn info(&self) -> Result<&'static MediaInfo, Error>; - fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error>; - fn verify(&self, offset: usize, buffer: &[u8]) -> Result; - fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error>; - fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error>; + fn read(&self, offset: usize, buffer: &mut [u8], timeout: &mut Timeout) -> Result<(), Error>; + fn verify(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result; + fn prepare_write(&self, sector: usize, count: usize, timeout: &mut Timeout) -> Result<(), Error>; + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error>; } static CURRENT_SAVE_ACCESS: Mutex> = Mutex::new(None); @@ -201,7 +202,7 @@ fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { /// Allows reading and writing of save media. pub struct SaveData { - lock: RawMutexGuard<'static>, + _lock: RawMutexGuard<'static>, access: &'static dyn RawSaveAccess, info: &'static MediaInfo, timeout: utils::Timeout, @@ -211,7 +212,7 @@ impl SaveData { fn new(timer: Option) -> Result { match get_save_implementation() { Some(access) => Ok(SaveData { - lock: utils::lock_media()?, + _lock: utils::lock_media_access()?, access, info: access.info()?, timeout: utils::Timeout::new(timer), @@ -255,15 +256,15 @@ impl SaveData { /// Copies data from the save media to a buffer. /// /// If an error is returned, the contents of the buffer are unpredictable. - pub fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + pub fn read(&mut self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { self.check_bounds_len(offset, buffer.len())?; - self.access.read(offset, buffer) + self.access.read(offset, buffer, &mut self.timeout) } /// Verifies that a given block of memory matches the save media. - pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + pub fn verify(&mut self, offset: usize, buffer: &[u8]) -> Result { self.check_bounds_len(offset, buffer.len())?; - self.access.verify(offset, buffer) + self.access.verify(offset, buffer, &mut self.timeout) } /// Returns a range that contains all sectors the input range overlaps. @@ -286,7 +287,9 @@ impl SaveData { 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)?; + self.access.prepare_write( + range.start >> shift, range.len() >> shift, &mut self.timeout, + )?; } Ok(SavePreparedBlock { parent: self, @@ -307,14 +310,14 @@ impl<'a> SavePreparedBlock<'a> { /// 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 /// unpredictable. - pub fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + pub fn write(&mut self, offset: usize, buffer: &[u8]) -> Result<(), Error> { if buffer.len() == 0 { Ok(()) } else if !self.range.contains(&offset) || !self.range.contains(&(offset + buffer.len() - 1)) { Err(Error::OutOfBounds) } else { - self.parent.access.write(offset, buffer) + self.parent.access.write(offset, buffer, &mut self.parent.timeout) } } @@ -327,7 +330,7 @@ impl<'a> SavePreparedBlock<'a> { /// 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 /// unpredictable. - pub fn write_and_verify(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + pub fn write_and_verify(&mut self, offset: usize, buffer: &[u8]) -> Result<(), Error> { self.write(offset, buffer)?; if !self.parent.verify(offset, buffer)? { Err(Error::WriteError) @@ -387,6 +390,36 @@ impl SaveManager { set_save_implementation(&sram::BatteryBackedAccess); } + /// Declares that the ROM uses 64KiB flash memory. + /// + /// Flash save media is generally very slow to write to and relatively fast + /// to read from. It is the only real option if you need larger save data. + /// + /// 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_flash_64k(&mut self) { + marker::emit_flash_512k_marker(); + set_save_implementation(&flash::FlashAccess); + } + + /// Declares that the ROM uses 128KiB flash memory. + /// + /// Flash save media is generally very slow to write to and relatively fast + /// to read from. It is the only real option if you need larger save data. + /// + /// 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_flash_128k(&mut self) { + marker::emit_flash_1m_marker(); + set_save_implementation(&flash::FlashAccess); + } + /// Creates a new accessor to the save data. /// /// You must have initialized the save manager beforehand to use a specific diff --git a/agb/src/save/sram.rs b/agb/src/save/sram.rs index b4e21eca..614b751e 100644 --- a/agb/src/save/sram.rs +++ b/agb/src/save/sram.rs @@ -5,6 +5,7 @@ use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; use crate::save::asm_utils::*; +use crate::save::utils::Timeout; const SRAM_SIZE: usize = 32 * 1024; // 32 KiB @@ -28,7 +29,7 @@ impl RawSaveAccess for BatteryBackedAccess { }) } - fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { check_bounds(offset, buffer.len())?; unsafe { read_raw_buf(buffer, 0x0E000000 + offset); @@ -36,17 +37,17 @@ impl RawSaveAccess for BatteryBackedAccess { Ok(()) } - fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { check_bounds(offset, buffer.len())?; let val = unsafe { verify_raw_buf(buffer, 0x0E000000 + offset) }; Ok(val) } - fn prepare_write(&self, _: usize, _: usize) -> Result<(), Error> { + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { Ok(()) } - fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + fn write(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result<(), Error> { check_bounds(offset, buffer.len())?; unsafe { write_raw_buf(0x0E000000 + offset, buffer); diff --git a/agb/src/save/utils.rs b/agb/src/save/utils.rs index 2ada1eeb..a19879c2 100644 --- a/agb/src/save/utils.rs +++ b/agb/src/save/utils.rs @@ -7,48 +7,46 @@ use crate::timer::{Timer, Divider}; /// A timeout type used to prevent hardware errors in save media from hanging /// the game. pub struct Timeout { - timer: Option, + timer: Option, } impl Timeout { - /// Creates a new timeout from the timer passed to [`set_timer_for_timeout`]. - /// - /// ## Errors - /// - /// If another timeout has already been created. - #[inline(never)] - pub fn new(timer: Option) -> Self { - Timeout { timer } - } - - /// Starts this timeout. - pub fn start(&mut self) { - if let Some(timer) = &mut self.timer { - timer.set_divider(Divider::Divider1024); - timer.set_interrupt(false); - timer.set_overflow_amount(0xFFFF); - timer.set_cascade(false); - timer.set_enabled(true); + /// Creates a new timeout from the timer passed to [`set_timer_for_timeout`]. + /// + /// ## Errors + /// + /// If another timeout has already been created. + #[inline(never)] + pub fn new(timer: Option) -> Self { + Timeout { timer } } - } - /// Returns whether a number of milliseconds has passed since the last call - /// to [`Timeout::start()`]. - pub fn check_timeout_met(&self, check_ms: u16) -> bool { - if let Some(timer) = &self.timer { - check_ms * 17 < timer.value() - } else { - false + /// Starts this timeout. + pub fn start(&mut self) { + if let Some(timer) = &mut self.timer { + timer.set_enabled(false); + timer.set_divider(Divider::Divider1024); + timer.set_interrupt(false); + timer.set_overflow_amount(0xFFFF); + timer.set_cascade(false); + timer.set_enabled(true); + } + } + + /// Returns whether a number of milliseconds has passed since the last call + /// to [`Timeout::start()`]. + pub fn check_timeout_met(&self, check_ms: u16) -> bool { + if let Some(timer) = &self.timer { + check_ms * 17 < timer.value() + } else { + false + } } - } } -/// Tries to obtain a lock on the global lock for save operations. -/// -/// This is used to prevent problems with stateful save media. -pub fn lock_media() -> Result, Error> { - static LOCK: RawMutex = RawMutex::new(); - match LOCK.try_lock() { - Some(x) => Ok(x), - None => Err(Error::MediaInUse), - } +pub fn lock_media_access() -> Result, Error> { + static LOCK: RawMutex = RawMutex::new(); + match LOCK.try_lock() { + Some(x) => Ok(x), + None => Err(Error::MediaInUse), + } } From 821098dd268cb40ebbb7cc174baf0bdef909c5f6 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Wed, 17 Aug 2022 04:52:41 -0700 Subject: [PATCH 07/49] Implement EEPROM save media. Fix EEPROM implementation. --- agb-tests/tests/test_save_eeprom_512b.rs | 16 ++ agb-tests/tests/test_save_eeprom_8k.rs | 16 ++ agb/src/dma.rs | 26 +++ agb/src/save/eeprom.rs | 271 +++++++++++++++++++++++ agb/src/save/mod.rs | 32 ++- agb/src/save/utils.rs | 7 + 6 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 agb-tests/tests/test_save_eeprom_512b.rs create mode 100644 agb-tests/tests/test_save_eeprom_8k.rs create mode 100644 agb/src/save/eeprom.rs diff --git a/agb-tests/tests/test_save_eeprom_512b.rs b/agb-tests/tests/test_save_eeprom_512b.rs new file mode 100644 index 00000000..19c3f6c9 --- /dev/null +++ b/agb-tests/tests/test_save_eeprom_512b.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_eeprom_512b(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb-tests/tests/test_save_eeprom_8k.rs b/agb-tests/tests/test_save_eeprom_8k.rs new file mode 100644 index 00000000..95677321 --- /dev/null +++ b/agb-tests/tests/test_save_eeprom_8k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_eeprom_8k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/src/dma.rs b/agb/src/dma.rs index 71651cfc..1bf1b0bd 100644 --- a/agb/src/dma.rs +++ b/agb/src/dma.rs @@ -24,3 +24,29 @@ pub(crate) unsafe fn dma_copy16(src: *const u16, dest: *mut u16, count: usize) { DMA3_CONTROL.set(count as u32 | (1 << 31)); } + +pub(crate) fn dma3_exclusive(f: impl FnOnce() -> R) -> R { + const DMA0_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(0) + 2) }; + const DMA1_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(1) + 2) }; + const DMA2_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(2) + 2) }; + + crate::interrupt::free(|_| { + let dma0_ctl = DMA0_CTRL_HI.get(); + let dma1_ctl = DMA1_CTRL_HI.get(); + let dma2_ctl = DMA2_CTRL_HI.get(); + DMA0_CTRL_HI.set(dma0_ctl & !(1 << 15)); + DMA1_CTRL_HI.set(dma1_ctl & !(1 << 15)); + DMA2_CTRL_HI.set(dma2_ctl & !(1 << 15)); + + // Executes the body of the function with DMAs and IRQs disabled. + let ret = f(); + + // Continues higher priority DMAs if they were enabled before. + DMA0_CTRL_HI.set(dma0_ctl); + DMA1_CTRL_HI.set(dma1_ctl); + DMA2_CTRL_HI.set(dma2_ctl); + + // returns the return value + ret + }) +} \ No newline at end of file diff --git a/agb/src/save/eeprom.rs b/agb/src/save/eeprom.rs new file mode 100644 index 00000000..51753d50 --- /dev/null +++ b/agb/src/save/eeprom.rs @@ -0,0 +1,271 @@ +//! A module containing support for EEPROM. +//! +//! EEPROM requires using DMA to issue commands for both reading and writing. + +use crate::memory_mapped::MemoryMapped; +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::utils::Timeout; +use core::cmp; + +const PORT: MemoryMapped = unsafe { MemoryMapped::new(0x0DFFFF00) }; +const SECTOR_SHIFT: usize = 3; +const SECTOR_LEN: usize = 1 << SECTOR_SHIFT; +const SECTOR_MASK: usize = SECTOR_LEN - 1; + +/// Sends a DMA command to EEPROM. +fn dma_send(source: &[u32], ct: usize) { + crate::dma::dma3_exclusive(|| unsafe { + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + crate::dma::dma_copy16(source.as_ptr() as *mut u16, 0x0DFFFF00 as *mut u16, ct); + }); +} + +/// Receives a DMA packet from EEPROM. +fn dma_receive(source: &mut [u32], ct: usize) { + crate::dma::dma3_exclusive(|| unsafe { + crate::dma::dma_copy16(0x0DFFFF00 as *mut u16, source.as_ptr() as *mut u16, ct); + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + }); +} + +/// Union type to help build/receive commands. +struct BufferData { + idx: usize, + data: BufferContents, +} +#[repr(align(4))] +union BufferContents { + uninit: (), + bits: [u16; 82], + words: [u32; 41], +} +impl BufferData { + fn new() -> Self { + BufferData { idx: 0, data: BufferContents { uninit: () } } + } + + /// Writes a bit to the output buffer. + fn write_bit(&mut self, val: u8) { + unsafe { + self.data.bits[self.idx] = val as u16; + self.idx += 1; + } + } + + /// Writes a number to the output buffer + fn write_num(&mut self, count: usize, num: u32) { + for i in 0..count { + self.write_bit(((num >> (count - 1 - i)) & 1) as u8); + } + } + + /// Reads a number from the input buffer. + fn read_num(&mut self, off: usize, count: usize) -> u32 { + let mut accum = 0; + unsafe { + for i in 0..count { + accum <<= 1; + accum |= self.data.bits[off + i] as u32; + } + } + accum + } + + /// Receives a number of words into the input buffer. + fn receive(&mut self, count: usize) { + unsafe { + dma_receive(&mut self.data.words, count); + } + } + + /// Submits the current buffer via DMA. + fn submit(&self) { + unsafe { + dma_send(&self.data.words, self.idx); + } + } +} + +/// The properties of a given EEPROM type. +struct EepromProperties { + addr_bits: usize, + byte_len: usize, +} +impl EepromProperties { + /// Reads a block from the save media. + fn read_sector(&self, word: usize) -> [u8; 8] { + // Set address command. The command is two one bits, followed by the + // address, followed by a zero bit. + // + // 512B Command: [1 1|n n n n n n|0] + // 8KiB Command: [1 1|n n n n n n n n n n n n n n|0] + let mut buf = BufferData::new(); + buf.write_bit(1); + buf.write_bit(1); + buf.write_num(self.addr_bits, word as u32); + buf.write_bit(0); + buf.submit(); + + // Receive the buffer data. The EEPROM sends 3 irrelevant bits followed + // by 64 data bits. + buf.receive(68); + let mut out = [0; 8]; + for i in 0..8 { + out[i] = buf.read_num(4 + i * 8, 8) as u8; + } + out + } + + /// Writes a sector directly. + fn write_sector_raw( + &self, word: usize, block: &[u8], timeout: &mut Timeout, + ) -> Result<(), Error> { + // Write sector command. The command is a one bit, followed by a + // zero bit, followed by the address, followed by 64 bits of data. + // + // 512B Command: [1 0|n n n n n n|v v v v ...] + // 8KiB Command: [1 0|n n n n n n n n n n n n n n|v v v v ...] + let mut buf = BufferData::new(); + buf.write_bit(1); + buf.write_bit(0); + buf.write_num(self.addr_bits, word as u32); + for i in 0..8 { + buf.write_num(8, block[i] as u32); + } + buf.write_bit(0); + buf.submit(); + + // Wait for the sector to be written for 10 milliseconds. + timeout.start(); + while PORT.get() & 1 != 1 { + if timeout.check_timeout_met(10) { + return Err(Error::OperationTimedOut); + } + } + Ok(()) + } + + /// Writes a sector to the EEPROM, keeping any current contents outside the + /// buffer's range. + fn write_sector_safe( + &self, word: usize, data: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let mut buf = self.read_sector(word); + buf[start..start + data.len()].copy_from_slice(data); + self.write_sector_raw(word, &buf, timeout) + } + + /// Writes a sector to the EEPROM. + fn write_sector( + &self, word: usize, data: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + if data.len() == 8 && start == 0 { + self.write_sector_raw(word, data, timeout) + } else { + self.write_sector_safe(word, data, start, timeout) + } + } + + /// Checks whether an offset is in range. + fn check_offset(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_none() && (offset + len) > self.byte_len { + Err(Error::OutOfBounds) + } else { + Ok(()) + } + } + + /// Implements EEPROM reads. + fn read(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { + self.check_offset(offset, buf.len())?; + while buf.len() != 0 { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + let sector = self.read_sector(offset >> SECTOR_SHIFT); + buf[..end_len].copy_from_slice(§or[start..start + end_len]); + buf = &mut buf[end_len..]; + offset += end_len; + } + Ok(()) + } + + /// Implements EEPROM verifies. + fn verify(&self, mut offset: usize, mut buf: &[u8]) -> Result { + self.check_offset(offset, buf.len())?; + while buf.len() != 0 { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + if &buf[..end_len] != &self.read_sector(offset >> SECTOR_SHIFT) { + return Ok(false); + } + buf = &buf[end_len..]; + offset += end_len; + } + Ok(true) + } + + /// Implements EEPROM writes. + fn write(&self, mut offset: usize, mut buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + self.check_offset(offset, buf.len())?; + while buf.len() != 0 { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + self.write_sector(offset >> SECTOR_SHIFT, &buf[..end_len], start, timeout)?; + buf = &buf[end_len..]; + offset += end_len; + } + Ok(()) + } +} +const PROPS_512B: EepromProperties = EepromProperties { addr_bits: 6, byte_len: 512 }; +const PROPS_8K: EepromProperties = EepromProperties { addr_bits: 14, byte_len: 8 * 1024 }; + +/// The [`RawSaveAccess`] used for 512 byte EEPROM. +pub struct Eeprom512B; +impl RawSaveAccess for Eeprom512B { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Eeprom512B, + sector_shift: 3, + sector_count: 64, + uses_prepare_write: false, + }) + } + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + PROPS_512B.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + PROPS_512B.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + PROPS_512B.write(offset, buffer, timeout) + } +} + +/// The [`RawSaveAccess`] used for 8 KiB EEPROM. +pub struct Eeprom8K; +impl RawSaveAccess for Eeprom8K { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Eeprom8K, + sector_shift: 3, + sector_count: 1024, + uses_prepare_write: false, + }) + } + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + PROPS_8K.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + PROPS_8K.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + PROPS_8K.write(offset, buffer, timeout) + } +} diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index 34195676..8639666d 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -108,7 +108,7 @@ use crate::sync::{Mutex, RawMutexGuard}; use crate::timer::Timer; mod asm_utils; -//pub mod eeprom; +mod eeprom; mod flash; mod sram; mod utils; @@ -420,6 +420,36 @@ impl SaveManager { set_save_implementation(&flash::FlashAccess); } + /// Declares that the ROM uses 512 bytes EEPROM memory. + /// + /// EEPROM is generally pretty slow and also very small. It's mainly used in + /// Game Paks because it's cheap. + /// + /// 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_eeprom_512b(&mut self) { + marker::emit_eeprom_marker(); + set_save_implementation(&eeprom::Eeprom512B); + } + + /// Declares that the ROM uses 8 KiB EEPROM memory. + /// + /// EEPROM is generally pretty slow and also very small. It's mainly used in + /// Game Paks because it's cheap. + /// + /// 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_eeprom_8k(&mut self) { + marker::emit_eeprom_marker(); + set_save_implementation(&eeprom::Eeprom8K); + } + /// Creates a new accessor to the save data. /// /// You must have initialized the save manager beforehand to use a specific diff --git a/agb/src/save/utils.rs b/agb/src/save/utils.rs index a19879c2..b94cb3fe 100644 --- a/agb/src/save/utils.rs +++ b/agb/src/save/utils.rs @@ -42,6 +42,13 @@ impl Timeout { } } } +impl Drop for Timeout { + fn drop(&mut self) { + if let Some(timer) = &mut self.timer { + timer.set_enabled(false); + } + } +} pub fn lock_media_access() -> Result, Error> { static LOCK: RawMutex = RawMutex::new(); From 8f3e438070fd981787c41e9ee0c70cb69f758332 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Thu, 15 Sep 2022 23:21:31 -0700 Subject: [PATCH 08/49] Final round of bugfixes, documentation fixes and clippy warning fixes. --- agb/src/save/asm_routines.s | 4 +-- agb/src/save/asm_utils.rs | 6 ++--- agb/src/save/eeprom.rs | 10 +++++--- agb/src/save/flash.rs | 12 +++++---- agb/src/save/mod.rs | 51 +++++++++++++++---------------------- 5 files changed, 38 insertions(+), 45 deletions(-) diff --git a/agb/src/save/asm_routines.s b/agb/src/save/asm_routines.s index b04d407b..f257a524 100644 --- a/agb/src/save/asm_routines.s +++ b/agb/src/save/asm_routines.s @@ -7,7 +7,7 @@ @ agb_thumb_func agb_rs__WramReadByte ldrb r0, [r0] - mov pc, lr + bx lr agb_thumb_end agb_rs__WramReadByte @ @@ -45,5 +45,5 @@ agb_thumb_func agb_rs__WramXferBuf ldrb r3, [r0,r2] strb r3, [r1,r2] bne 0b - mov pc, lr + bx lr agb_thumb_end agb_rs__WramXferBuf diff --git a/agb/src/save/asm_utils.rs b/agb/src/save/asm_utils.rs index 930995ad..577d99f0 100644 --- a/agb/src/save/asm_utils.rs +++ b/agb/src/save/asm_utils.rs @@ -17,7 +17,7 @@ extern "C" { /// This uses raw addresses into the memory space. Use with care. #[inline(always)] pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { - if dst.len() != 0 { + if !dst.is_empty() { agb_rs__WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); } } @@ -30,7 +30,7 @@ pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { /// This uses raw addresses into the memory space. Use with care. #[inline(always)] pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { - if src.len() != 0 { + if !src.is_empty() { agb_rs__WramXferBuf(src.as_ptr(), dst as _, src.len()); } } @@ -44,7 +44,7 @@ pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { /// This uses raw addresses into the memory space. Use with care. #[inline(always)] pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { - if buf1.len() != 0 { + if !buf1.is_empty() { agb_rs__WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) } else { true diff --git a/agb/src/save/eeprom.rs b/agb/src/save/eeprom.rs index 51753d50..d2f7258a 100644 --- a/agb/src/save/eeprom.rs +++ b/agb/src/save/eeprom.rs @@ -93,6 +93,7 @@ struct EepromProperties { } impl EepromProperties { /// Reads a block from the save media. + #[allow(clippy::needless_range_loop)] fn read_sector(&self, word: usize) -> [u8; 8] { // Set address command. The command is two one bits, followed by the // address, followed by a zero bit. @@ -117,6 +118,7 @@ impl EepromProperties { } /// Writes a sector directly. + #[allow(clippy::needless_range_loop)] fn write_sector_raw( &self, word: usize, block: &[u8], timeout: &mut Timeout, ) -> Result<(), Error> { @@ -178,7 +180,7 @@ impl EepromProperties { /// Implements EEPROM reads. fn read(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { self.check_offset(offset, buf.len())?; - while buf.len() != 0 { + while !buf.is_empty() { let start = offset & SECTOR_MASK; let end_len = cmp::min(SECTOR_LEN - start, buf.len()); let sector = self.read_sector(offset >> SECTOR_SHIFT); @@ -192,10 +194,10 @@ impl EepromProperties { /// Implements EEPROM verifies. fn verify(&self, mut offset: usize, mut buf: &[u8]) -> Result { self.check_offset(offset, buf.len())?; - while buf.len() != 0 { + while !buf.is_empty() { let start = offset & SECTOR_MASK; let end_len = cmp::min(SECTOR_LEN - start, buf.len()); - if &buf[..end_len] != &self.read_sector(offset >> SECTOR_SHIFT) { + if buf[..end_len] != self.read_sector(offset >> SECTOR_SHIFT) { return Ok(false); } buf = &buf[end_len..]; @@ -207,7 +209,7 @@ impl EepromProperties { /// Implements EEPROM writes. fn write(&self, mut offset: usize, mut buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { self.check_offset(offset, buf.len())?; - while buf.len() != 0 { + while !buf.is_empty() { let start = offset & SECTOR_MASK; let end_len = cmp::min(SECTOR_LEN - start, buf.len()); self.write_sector(offset >> SECTOR_SHIFT, &buf[..end_len], start, timeout)?; diff --git a/agb/src/save/flash.rs b/agb/src/save/flash.rs index a3b470e5..0384926b 100644 --- a/agb/src/save/flash.rs +++ b/agb/src/save/flash.rs @@ -227,8 +227,8 @@ static CHIP_INFO_GENERIC_128K: ChipInfo = ChipInfo { impl FlashChipType { /// Returns the internal info for this chip. - fn chip_info(&self) -> &'static ChipInfo { - match *self { + fn chip_info(self) -> &'static ChipInfo { + match self { FlashChipType::Sst64K => &CHIP_INFO_SST_64K, FlashChipType::Macronix64K => &CHIP_INFO_MACRONIX_64K, FlashChipType::Panasonic64K => &CHIP_INFO_PANASONIC_64K, @@ -284,7 +284,7 @@ impl ChipInfo { /// Reads a buffer from save media into memory. fn read_buffer(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { - while buf.len() != 0 { + while !buf.is_empty() { self.set_bank(offset >> BANK_SHIFT)?; let start = offset & BANK_MASK; let end_len = cmp::min(BANK_LEN - start, buf.len()); @@ -299,7 +299,7 @@ impl ChipInfo { /// Verifies that a buffer was properly stored into save media. fn verify_buffer(&self, mut offset: usize, mut buf: &[u8]) -> Result { - while buf.len() != 0 { + while !buf.is_empty() { self.set_bank(offset >> BANK_SHIFT)?; let start = offset & BANK_MASK; let end_len = cmp::min(BANK_LEN - start, buf.len()); @@ -355,6 +355,7 @@ impl ChipInfo { } /// Writes an entire buffer to the save media. + #[allow(clippy::needless_range_loop)] fn write_buffer(&self, offset: usize, buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { self.set_bank(offset >> BANK_SHIFT)?; for i in 0..buf.len() { @@ -368,6 +369,7 @@ impl ChipInfo { } /// Erases and writes an entire 128b sector on Atmel devices. + #[allow(clippy::needless_range_loop)] fn write_atmel_sector_raw( &self, offset: usize, buf: &[u8], timeout: &mut Timeout, ) -> Result<(), Error> { @@ -453,7 +455,7 @@ impl RawSaveAccess for FlashAccess { chip.check_len(offset, buf.len())?; if chip.uses_atmel_api { - while buf.len() != 0 { + while !buf.is_empty() { let start = offset & 127; let end_len = cmp::min(128 - start, buf.len()); chip.write_atmel_sector(offset & !127, &buf[..end_len], start, timeout)?; diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index 8639666d..485cc832 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -25,18 +25,17 @@ //! To use save media in your game, you must set which type to use. This is done //! by calling one of the following functions at startup: //! -//! * For 32 KiB battery-backed SRAM, call [`use_sram`]. -//! * For 64 KiB flash memory, call [`use_flash_64k`]. -//! * For 128 KiB flash memory, call [`use_flash_128k`]. -//! * For 512 byte EEPROM, call [`use_eeprom_512b`]. -//! * For 8 KiB EEPROM, call [`use_eeprom_8k`]. +//! * For 32 KiB battery-backed SRAM, call [`init_sram`]. +//! * For 64 KiB flash memory, call [`init_flash_64k`]. +//! * For 128 KiB flash memory, call [`init_flash_128k`]. +//! * For 512 byte EEPROM, call [`init_eeprom_512b`]. +//! * For 8 KiB EEPROM, call [`init_eeprom_8k`]. //! -//! TODO Update example -//! ```rust,norun -//! # use gba::save; -//! save::use_flash_128k(); -//! save::set_timer_for_timeout(3); // Uses timer 3 for save media timeouts. -//! ``` +//! [`init_sram`]: SaveManager::init_sram +//! [`init_flash_64k`]: SaveManager::init_flash_64k +//! [`init_flash_128k`]: SaveManager::init_flash_128k +//! [`init_eeprom_512b`]: SaveManager::init_eeprom_512b +//! [`init_eeprom_8k`]: SaveManager::init_eeprom_8k //! //! ## Using save media //! @@ -46,29 +45,10 @@ //! Reading data from the savegame is simple. Use [`read`] to copy data from an //! offset in the savegame into a buffer in memory. //! -//! TODO Update example -//! ```rust,norun -//! # use gba::{info, save::SaveAccess}; -//! let mut buf = [0; 1000]; -//! SaveAccess::new()?.read(1000, &mut buf)?; -//! info!("Memory result: {:?}", buf); -//! ``` -//! //! Writing to save media requires you to prepare the area for writing by calling //! the [`prepare_write`] method to return a [`SavePreparedBlock`], which contains //! the actual [`write`] method. //! -//! TODO Update example -//! ```rust,norun -//! # use gba::{info, save::SaveAccess}; -//! let access = SaveAccess::new()?; -//! access.prepare_write(500..600)?; -//! access.write(500, &[10; 25])?; -//! access.write(525, &[20; 25])?; -//! access.write(550, &[30; 25])?; -//! access.write(575, &[40; 25])?; -//! ``` -//! //! The `prepare_write` method leaves everything in a sector that overlaps the //! range passed to it in an implementation defined state. On some devices it may //! do nothing, and on others, it may clear the entire range to `0xFF`. @@ -169,11 +149,14 @@ pub struct MediaInfo { impl MediaInfo { /// Returns the sector size of the save media. It is generally optimal to /// write data in blocks that are aligned to the sector size. + #[must_use] pub fn sector_size(&self) -> usize { 1 << self.sector_shift } /// Returns the total length of this save media. + #[must_use] + #[allow(clippy::len_without_is_empty)] // is_empty() would always be false pub fn len(&self) -> usize { self.sector_count << self.sector_shift } @@ -222,22 +205,27 @@ impl SaveData { } /// Returns the media info underlying this accessor. + #[must_use] pub fn media_info(&self) -> &'static MediaInfo { self.info } /// Returns the save media type being used. + #[must_use] 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. + #[must_use] pub fn sector_size(&self) -> usize { self.info.sector_size() } /// Returns the total length of this save media. + #[must_use] + #[allow(clippy::len_without_is_empty)] // is_empty() would always be false pub fn len(&self) -> usize { self.info.len() } @@ -271,6 +259,7 @@ impl SaveData { /// /// This can be used to calculate which blocks would be erased by a call /// to [`prepare_write`](`SaveAccess::prepare_write`) + #[must_use] pub fn align_range(&self, range: Range) -> Range { let shift = self.info.sector_shift; let mask = (1 << shift) - 1; @@ -311,7 +300,7 @@ impl<'a> SavePreparedBlock<'a> { /// state. If an error is returned, the contents of the save media is /// unpredictable. pub fn write(&mut self, offset: usize, buffer: &[u8]) -> Result<(), Error> { - if buffer.len() == 0 { + if buffer.is_empty() { Ok(()) } else if !self.range.contains(&offset) || !self.range.contains(&(offset + buffer.len() - 1)) { From 223ef150abfa3a7d65aad56b671840d4dc8ec287 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Thu, 15 Sep 2022 23:21:45 -0700 Subject: [PATCH 09/49] Update changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c9d4bb..a2f75f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for using windows on the GBA. Windows are used to selectively enable rendering of certain layers or effects. - Support for the blend mode of the GBA. Blending allows for alpha blending between layers and fading to black and white. - Added a new agb::sync module that contains GBA-specific synchronization primitives. +- Added support for save files. ### Changes - Many of the places that originally disabled IRQs now use the `sync` module, reducing the chance of missed interrupts. From b920d94f8352b4216e6561a3bd7adb6cb9389b96 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Thu, 15 Sep 2022 23:28:38 -0700 Subject: [PATCH 10/49] Remove agb-tests in favor of putting the tests directly in agb. --- agb-tests/.cargo/config.toml | 14 -------------- agb-tests/Cargo.toml | 19 ------------------- agb-tests/rust-toolchain.toml | 3 --- .../tests/save_test_common/mod.rs | 0 .../tests/test_save_eeprom_512b.rs | 0 .../tests/test_save_eeprom_8k.rs | 0 .../tests/test_save_flash_128k.rs | 0 .../tests/test_save_flash_64k.rs | 0 {agb-tests => agb}/tests/test_save_sram.rs | 0 justfile | 3 --- 10 files changed, 39 deletions(-) delete mode 100644 agb-tests/.cargo/config.toml delete mode 100644 agb-tests/Cargo.toml delete mode 100644 agb-tests/rust-toolchain.toml rename {agb-tests => agb}/tests/save_test_common/mod.rs (100%) rename {agb-tests => agb}/tests/test_save_eeprom_512b.rs (100%) rename {agb-tests => agb}/tests/test_save_eeprom_8k.rs (100%) rename {agb-tests => agb}/tests/test_save_flash_128k.rs (100%) rename {agb-tests => agb}/tests/test_save_flash_64k.rs (100%) rename {agb-tests => agb}/tests/test_save_sram.rs (100%) diff --git a/agb-tests/.cargo/config.toml b/agb-tests/.cargo/config.toml deleted file mode 100644 index d5f7f86c..00000000 --- a/agb-tests/.cargo/config.toml +++ /dev/null @@ -1,14 +0,0 @@ -[unstable] -build-std = ["core", "alloc"] -build-std-features = ["compiler-builtins-mem"] - -[build] -target = "thumbv4t-none-eabi" - -[target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-T../agb/gba.ld", "-Ctarget-cpu=arm7tdmi"] -runner = "mgba-test-runner" - -[target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-T../agb/gba.ld", "-Ctarget-cpu=arm7tdmi"] -runner = "mgba-test-runner" diff --git a/agb-tests/Cargo.toml b/agb-tests/Cargo.toml deleted file mode 100644 index 9917f81b..00000000 --- a/agb-tests/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "agb-tests" -version = "0.1.0" -edition = "2018" - -[profile.dev] -opt-level = 3 -debug = true - -[profile.release] -lto = true -debug = true - -[dependencies] -agb = { version = "*", path = "../agb", features = ["testing"] } - -[package.metadata.docs.rs] -default-target = "thumbv6m-none-eabi" -targets = [] diff --git a/agb-tests/rust-toolchain.toml b/agb-tests/rust-toolchain.toml deleted file mode 100644 index 06842486..00000000 --- a/agb-tests/rust-toolchain.toml +++ /dev/null @@ -1,3 +0,0 @@ -[toolchain] -channel = "nightly" -components = ["rust-src", "clippy"] \ No newline at end of file diff --git a/agb-tests/tests/save_test_common/mod.rs b/agb/tests/save_test_common/mod.rs similarity index 100% rename from agb-tests/tests/save_test_common/mod.rs rename to agb/tests/save_test_common/mod.rs diff --git a/agb-tests/tests/test_save_eeprom_512b.rs b/agb/tests/test_save_eeprom_512b.rs similarity index 100% rename from agb-tests/tests/test_save_eeprom_512b.rs rename to agb/tests/test_save_eeprom_512b.rs diff --git a/agb-tests/tests/test_save_eeprom_8k.rs b/agb/tests/test_save_eeprom_8k.rs similarity index 100% rename from agb-tests/tests/test_save_eeprom_8k.rs rename to agb/tests/test_save_eeprom_8k.rs diff --git a/agb-tests/tests/test_save_flash_128k.rs b/agb/tests/test_save_flash_128k.rs similarity index 100% rename from agb-tests/tests/test_save_flash_128k.rs rename to agb/tests/test_save_flash_128k.rs diff --git a/agb-tests/tests/test_save_flash_64k.rs b/agb/tests/test_save_flash_64k.rs similarity index 100% rename from agb-tests/tests/test_save_flash_64k.rs rename to agb/tests/test_save_flash_64k.rs diff --git a/agb-tests/tests/test_save_sram.rs b/agb/tests/test_save_sram.rs similarity index 100% rename from agb-tests/tests/test_save_sram.rs rename to agb/tests/test_save_sram.rs diff --git a/justfile b/justfile index ba8f243c..23837d70 100644 --- a/justfile +++ b/justfile @@ -13,7 +13,6 @@ clippy: test: just _test-debug agb - just _test-debug agb-tests just _test-debug agb-fixnum just _test-debug-arm agb just _test-debug tools @@ -21,8 +20,6 @@ test: test-release: just _test-release agb just _test-release-arm agb - just _test-release agb-tests - just _test-release-arm agb-tests doctest-agb: (cd agb && cargo test --doc -Z doctest-xcompile) From 102df934d0eea28940e3ec312a7b1305e74e6178 Mon Sep 17 00:00:00 2001 From: Corwin Date: Thu, 22 Sep 2022 20:15:11 +0100 Subject: [PATCH 11/49] add all relevant directories to the code workspace --- .vscode/agb.code-workspace | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.vscode/agb.code-workspace b/.vscode/agb.code-workspace index 376761be..0d33085f 100644 --- a/.vscode/agb.code-workspace +++ b/.vscode/agb.code-workspace @@ -29,6 +29,18 @@ }, { "path": "../template" + }, + { + "path": "../agb-tests" + }, + { + "path": "../book" + }, + { + "path": "../mgba-test-runner" + }, + { + "path": "../tools" } ] } \ No newline at end of file From 9ae2f7390be33ce58add41c90f7d8bdc21de89ae Mon Sep 17 00:00:00 2001 From: Corwin Date: Fri, 23 Sep 2022 13:23:35 +0100 Subject: [PATCH 12/49] satisfy linter --- examples/hyperspace-roll/src/level_generation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hyperspace-roll/src/level_generation.rs b/examples/hyperspace-roll/src/level_generation.rs index be1a87af..04c45b5f 100644 --- a/examples/hyperspace-roll/src/level_generation.rs +++ b/examples/hyperspace-roll/src/level_generation.rs @@ -80,7 +80,7 @@ pub fn generate_upgrades(level: u32) -> Vec { .chain(core::iter::once(&next)) .filter(|&x| *x == Face::Malfunction) .count(); - let maximum_number_of_malfunctions = if level < 5 { 0 } else { 1 }; + let maximum_number_of_malfunctions = (level >= 5).into(); if upgrade_value(&upgrades, next) <= max_upgrade_value && number_of_malfunctions <= maximum_number_of_malfunctions { From 999f3707e2e0313ba2f7c68af6acea10799ad5b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Sep 2022 20:14:52 +0000 Subject: [PATCH 13/49] Update Rust crate clap to v4 --- tools/Cargo.lock | 16 ++++------------ tools/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/tools/Cargo.lock b/tools/Cargo.lock index b175a490..4f0e3f57 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -33,24 +33,22 @@ checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "clap" -version = "3.2.21" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7" +checksum = "dd03107d0f87139c1774a15f3db2165b0652b5460c58c27e561f89c20c599eaf" dependencies = [ "atty", "bitflags", "clap_lex", - "indexmap", "strsim", "termcolor", - "textwrap", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" dependencies = [ "os_str_bytes", ] @@ -138,12 +136,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - [[package]] name = "toml_edit" version = "0.14.4" diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 25c3cb11..81b24569 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = "3.2" +clap = "4.0" toml_edit = "0.14" From 81d02db5fcf1758bef3470730702f238858670d7 Mon Sep 17 00:00:00 2001 From: GBA bot Date: Wed, 28 Sep 2022 20:16:01 +0000 Subject: [PATCH 14/49] Update lockfiles --- book/games/pong/Cargo.lock | 24 ++++++------- examples/hyperspace-roll/Cargo.lock | 24 ++++++------- .../the-hat-chooses-the-wizard/Cargo.lock | 24 ++++++------- examples/the-purple-night/Cargo.lock | 24 ++++++------- mgba-test-runner/Cargo.lock | 36 +++++++++---------- tools/Cargo.lock | 8 ++--- 6 files changed, 70 insertions(+), 70 deletions(-) diff --git a/book/games/pong/Cargo.lock b/book/games/pong/Cargo.lock index 2f28485a..57603a26 100644 --- a/book/games/pong/Cargo.lock +++ b/book/games/pong/Cargo.lock @@ -217,9 +217,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.132" +version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "log" @@ -318,9 +318,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "png" @@ -343,9 +343,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" dependencies = [ "unicode-ident", ] @@ -367,18 +367,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "serde" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -393,9 +393,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", diff --git a/examples/hyperspace-roll/Cargo.lock b/examples/hyperspace-roll/Cargo.lock index 99c59380..cb715599 100644 --- a/examples/hyperspace-roll/Cargo.lock +++ b/examples/hyperspace-roll/Cargo.lock @@ -225,9 +225,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.132" +version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "log" @@ -326,9 +326,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "png" @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" dependencies = [ "unicode-ident", ] @@ -368,18 +368,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "serde" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -394,9 +394,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", diff --git a/examples/the-hat-chooses-the-wizard/Cargo.lock b/examples/the-hat-chooses-the-wizard/Cargo.lock index 19c728cf..4a212765 100644 --- a/examples/the-hat-chooses-the-wizard/Cargo.lock +++ b/examples/the-hat-chooses-the-wizard/Cargo.lock @@ -223,9 +223,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "libc" -version = "0.2.132" +version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "log" @@ -324,9 +324,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "png" @@ -342,9 +342,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" dependencies = [ "unicode-ident", ] @@ -372,18 +372,18 @@ checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "serde" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -409,9 +409,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", diff --git a/examples/the-purple-night/Cargo.lock b/examples/the-purple-night/Cargo.lock index d42b4a43..c59cb1dd 100644 --- a/examples/the-purple-night/Cargo.lock +++ b/examples/the-purple-night/Cargo.lock @@ -241,9 +241,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.132" +version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "libflate" @@ -354,9 +354,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "png" @@ -372,9 +372,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" dependencies = [ "unicode-ident", ] @@ -402,18 +402,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "serde" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -428,9 +428,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", diff --git a/mgba-test-runner/Cargo.lock b/mgba-test-runner/Cargo.lock index cdd404be..9b69d6d6 100644 --- a/mgba-test-runner/Cargo.lock +++ b/mgba-test-runner/Cargo.lock @@ -107,9 +107,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clang-sys" -version = "1.3.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" dependencies = [ "glob", "libc", @@ -118,9 +118,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.21" +version = "3.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" dependencies = [ "atty", "bitflags", @@ -163,9 +163,9 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "env_logger" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" dependencies = [ "atty", "humantime", @@ -213,9 +213,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "image" -version = "0.24.3" +version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" +checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c" dependencies = [ "bytemuck", "byteorder", @@ -237,9 +237,9 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" dependencies = [ "libc", ] @@ -258,9 +258,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.132" +version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "libloading" @@ -355,9 +355,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "os_str_bytes" @@ -385,9 +385,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" dependencies = [ "unicode-ident", ] @@ -447,9 +447,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" [[package]] name = "unicode-ident" diff --git a/tools/Cargo.lock b/tools/Cargo.lock index 4f0e3f57..c80295e6 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -96,18 +96,18 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.4" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bf247779e67a9082a4790b45e71ac7cfd1321331a5c856a74a9faebdab78d0" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "libc" -version = "0.2.132" +version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "memchr" From ae5d8818b67788880dc8315be3c5b91788258534 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 17:09:48 +0100 Subject: [PATCH 15/49] Fix test in release mode (don't know why this works :/ but is more correct) --- agb/src/display/example_logo.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agb/src/display/example_logo.rs b/agb/src/display/example_logo.rs index bec4df92..9b6dbe25 100644 --- a/agb/src/display/example_logo.rs +++ b/agb/src/display/example_logo.rs @@ -36,5 +36,8 @@ mod tests { display_logo(&mut map, &mut vram); crate::test_runner::assert_image_output("gfx/test_logo.png"); + + map.clear(&mut vram); + vram.gc(); } } From aecd4077dadef1c7727b9d946894631340ac0c36 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 19:42:39 +0100 Subject: [PATCH 16/49] Add verify test --- tools/src/publish.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/src/publish.rs b/tools/src/publish.rs index 9542f7e6..73a34d57 100644 --- a/tools/src/publish.rs +++ b/tools/src/publish.rs @@ -17,7 +17,7 @@ pub enum Error { CargoToml, } -pub fn command() -> clap::Command<'static> { +pub fn command() -> clap::Command { clap::Command::new("publish") .about("Publishes agb and all subcrates") .arg( @@ -196,6 +196,11 @@ fn read_cargo_toml(folder: &Path) -> Result { mod test { use super::*; + #[test] + fn verify_cli() { + command().debug_assert(); + } + #[test] fn url_to_poll_should_return_correct_url() { let test_cases = [ From 5733d038779f41d64f9a7633b7daefd44699d1c7 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 19:44:21 +0100 Subject: [PATCH 17/49] Add verify_cli for main too --- tools/src/main.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/src/main.rs b/tools/src/main.rs index 09fce409..676c7388 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -3,12 +3,15 @@ use clap::Command; mod publish; -fn main() { - let matches = Command::new("Agb tools") +fn cli() -> Command { + Command::new("Agb tools") .subcommand_required(true) .arg_required_else_help(true) .subcommand(publish::command()) - .get_matches(); +} + +fn main() { + let matches = cli().get_matches(); let result = match matches.subcommand() { Some(("publish", arg_matches)) => publish::publish(arg_matches), @@ -19,3 +22,13 @@ fn main() { eprintln!("Error: {:?}", e); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_cli() { + cli().debug_assert(); + } +} From b9e9bada9f5a2a65eafc1f579a79fb394059e322 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 19:45:28 +0100 Subject: [PATCH 18/49] Be less specific about the clap version --- tools/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 81b24569..7c53cbf7 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = "4.0" +clap = "4" toml_edit = "0.14" From b24b4faeae81b962bfe5b3cd12044b53a681fa46 Mon Sep 17 00:00:00 2001 From: GBA bot Date: Sat, 1 Oct 2022 18:46:59 +0000 Subject: [PATCH 19/49] Update lockfiles --- book/games/pong/Cargo.lock | 8 ++++---- examples/hyperspace-roll/Cargo.lock | 8 ++++---- examples/the-hat-chooses-the-wizard/Cargo.lock | 8 ++++---- examples/the-purple-night/Cargo.lock | 8 ++++---- mgba-test-runner/Cargo.lock | 8 ++++---- tools/Cargo.lock | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/book/games/pong/Cargo.lock b/book/games/pong/Cargo.lock index 57603a26..9d847e4d 100644 --- a/book/games/pong/Cargo.lock +++ b/book/games/pong/Cargo.lock @@ -217,9 +217,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.133" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "log" @@ -343,9 +343,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.44" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] diff --git a/examples/hyperspace-roll/Cargo.lock b/examples/hyperspace-roll/Cargo.lock index cb715599..e9a74349 100644 --- a/examples/hyperspace-roll/Cargo.lock +++ b/examples/hyperspace-roll/Cargo.lock @@ -225,9 +225,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.133" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "log" @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.44" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] diff --git a/examples/the-hat-chooses-the-wizard/Cargo.lock b/examples/the-hat-chooses-the-wizard/Cargo.lock index 4a212765..3ffac11c 100644 --- a/examples/the-hat-chooses-the-wizard/Cargo.lock +++ b/examples/the-hat-chooses-the-wizard/Cargo.lock @@ -223,9 +223,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "libc" -version = "0.2.133" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "log" @@ -342,9 +342,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.44" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] diff --git a/examples/the-purple-night/Cargo.lock b/examples/the-purple-night/Cargo.lock index c59cb1dd..8ade9367 100644 --- a/examples/the-purple-night/Cargo.lock +++ b/examples/the-purple-night/Cargo.lock @@ -241,9 +241,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.133" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "libflate" @@ -372,9 +372,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.44" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] diff --git a/mgba-test-runner/Cargo.lock b/mgba-test-runner/Cargo.lock index 9b69d6d6..a9dcbf58 100644 --- a/mgba-test-runner/Cargo.lock +++ b/mgba-test-runner/Cargo.lock @@ -258,9 +258,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.133" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "libloading" @@ -385,9 +385,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.44" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] diff --git a/tools/Cargo.lock b/tools/Cargo.lock index c80295e6..9c1065ce 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -33,9 +33,9 @@ checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "clap" -version = "4.0.1" +version = "4.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd03107d0f87139c1774a15f3db2165b0652b5460c58c27e561f89c20c599eaf" +checksum = "0a1af219c3e254a8b4649d6ddaef886b2015089f35f2ac5e1db31410c0566ab8" dependencies = [ "atty", "bitflags", @@ -105,9 +105,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.133" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "memchr" From 57772af4167f8868136c103635ea890b5a221681 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 20:21:15 +0100 Subject: [PATCH 20/49] Start the release script rewrite --- tools/src/main.rs | 3 ++ tools/src/release.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 tools/src/release.rs diff --git a/tools/src/main.rs b/tools/src/main.rs index 676c7388..c0b13de7 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -2,12 +2,14 @@ use clap::Command; mod publish; +mod release; fn cli() -> Command { Command::new("Agb tools") .subcommand_required(true) .arg_required_else_help(true) .subcommand(publish::command()) + .subcommand(release::command()) } fn main() { @@ -15,6 +17,7 @@ fn main() { let result = match matches.subcommand() { Some(("publish", arg_matches)) => publish::publish(arg_matches), + Some(("release", arg_matches)) => todo!(), _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), }; diff --git a/tools/src/release.rs b/tools/src/release.rs new file mode 100644 index 00000000..0cd04340 --- /dev/null +++ b/tools/src/release.rs @@ -0,0 +1,82 @@ +pub fn command() -> clap::Command { + clap::Command::new("release") + .about("Prepares and commits the changes required to release agb") + .arg( + clap::Arg::new("version") + .help("New version to release") + .value_parser(version_parser), + ) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Version { + major: u32, + minor: u32, + patch: u32, +} + +impl Version { + #[cfg(test)] + pub fn new(major: u32, minor: u32, patch: u32) -> Self { + Self { + major, + minor, + patch, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +struct ParseVersionError; + +impl std::str::FromStr for Version { + type Err = ParseVersionError; + + fn from_str(s: &str) -> Result { + let version_array: Vec<_> = s + .split('.') + .map(|v| v.parse()) + .collect::, _>>() + .map_err(|_| ParseVersionError)?; + + if version_array.len() > 3 || version_array.is_empty() { + return Err(ParseVersionError); + } + + Ok(Version { + major: version_array[0], + minor: *version_array.get(1).unwrap_or(&0), + patch: *version_array.get(2).unwrap_or(&0), + }) + } +} + +fn version_parser(maybe_version: &str) -> Result { + maybe_version.parse().map_err(|_| "Failed to parse version") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn verify_cli() { + command().debug_assert(); + } + + #[test] + fn can_parse_versions() { + assert_eq!(Version::from_str("0.1.2").unwrap(), Version::new(0, 1, 2)); + assert_eq!(Version::from_str("0.1").unwrap(), Version::new(0, 1, 0)); + assert_eq!( + Version::from_str("33.23.4000").unwrap(), + Version::new(33, 23, 4000) + ); + + assert_eq!(Version::from_str("abc").unwrap_err(), ParseVersionError); + assert_eq!(Version::from_str("").unwrap_err(), ParseVersionError); + assert_eq!(Version::from_str("0.2.4.5").unwrap_err(), ParseVersionError); + assert_eq!(Version::from_str("0.2.4a").unwrap_err(), ParseVersionError); + } +} From d8ca41ec3d7e346d0e818918a89ecdda40c29ae3 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 20:30:45 +0100 Subject: [PATCH 21/49] Finish the argument parsing --- tools/src/main.rs | 16 ++++++++++++++-- tools/src/release.rs | 24 +++++++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/tools/src/main.rs b/tools/src/main.rs index c0b13de7..4df25fdd 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -4,6 +4,12 @@ use clap::Command; mod publish; mod release; +#[derive(Debug)] +pub enum Error { + PublishError(publish::Error), + ReleaseError(release::Error), +} + fn cli() -> Command { Command::new("Agb tools") .subcommand_required(true) @@ -16,8 +22,14 @@ fn main() { let matches = cli().get_matches(); let result = match matches.subcommand() { - Some(("publish", arg_matches)) => publish::publish(arg_matches), - Some(("release", arg_matches)) => todo!(), + Some(("publish", arg_matches)) => { + publish::publish(arg_matches).map_err(Error::PublishError) + } + + Some(("release", arg_matches)) => { + release::release(arg_matches).map_err(Error::ReleaseError) + } + _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), }; diff --git a/tools/src/release.rs b/tools/src/release.rs index 0cd04340..a0c52df2 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -3,11 +3,31 @@ pub fn command() -> clap::Command { .about("Prepares and commits the changes required to release agb") .arg( clap::Arg::new("version") + .required(true) .help("New version to release") .value_parser(version_parser), ) + .arg( + clap::Arg::new("Dry run") + .long("dry-run") + .help("Don't do anything with git (but does everything else)") + .action(clap::ArgAction::SetTrue), + ) } +pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { + let dry_run = matches.get_one::("Dry run").expect("defined by clap"); + let version = matches + .get_one::("version") + .expect("defined by clap"); + + println!("dry run: {}, version: {:?}", dry_run, version); + todo!() +} + +#[derive(Debug)] +pub enum Error {} + #[derive(Debug, Clone, PartialEq, Eq)] struct Version { major: u32, @@ -52,7 +72,9 @@ impl std::str::FromStr for Version { } fn version_parser(maybe_version: &str) -> Result { - maybe_version.parse().map_err(|_| "Failed to parse version") + maybe_version + .parse() + .map_err(|_| "Failed to parse version, must be of the format x.y.z") } #[cfg(test)] From f2f0f771fb074e3d67d0767344a35c03d9d5d5a6 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 20:36:42 +0100 Subject: [PATCH 22/49] Extract utils --- tools/src/main.rs | 1 + tools/src/publish.rs | 32 +++++++------------------------- tools/src/utils.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 25 deletions(-) create mode 100644 tools/src/utils.rs diff --git a/tools/src/main.rs b/tools/src/main.rs index 4df25fdd..79118229 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -3,6 +3,7 @@ use clap::Command; mod publish; mod release; +mod utils; #[derive(Debug)] pub enum Error { diff --git a/tools/src/publish.rs b/tools/src/publish.rs index 73a34d57..0f8935f7 100644 --- a/tools/src/publish.rs +++ b/tools/src/publish.rs @@ -1,12 +1,14 @@ use clap::{Arg, ArgAction, ArgMatches}; use std::collections::{HashMap, HashSet}; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::Command; +use std::thread; use std::time::Duration; -use std::{env, thread}; use toml_edit::Document; +use crate::utils::*; + #[derive(Debug)] pub enum Error { FindRootDirectory, @@ -31,7 +33,7 @@ pub fn command() -> clap::Command { pub fn publish(matches: &ArgMatches) -> Result<(), Error> { let dry_run = matches.get_one::("Dry run").expect("defined by clap"); - let root_directory = find_agb_root_directory()?; + let root_directory = find_agb_root_directory().map_err(|_| Error::FindRootDirectory)?; let mut fully_published_crates: HashSet = HashSet::new(); let mut published_crates: HashSet = HashSet::new(); @@ -86,19 +88,6 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> { Ok(()) } -fn find_agb_root_directory() -> Result { - let mut current_path = env::current_dir().map_err(|_| Error::FindRootDirectory)?; - - while !current_path.clone().join("justfile").exists() { - current_path = current_path - .parent() - .ok_or(Error::FindRootDirectory)? - .to_owned(); - } - - Ok(current_path) -} - fn check_if_released(crate_to_publish: &str, expected_version: &str) -> Result { let url_to_poll = &get_url_to_poll(crate_to_publish); @@ -220,16 +209,9 @@ mod test { } } - #[test] - fn should_find_root_directory() -> Result<(), Error> { - assert_ne!(find_agb_root_directory()?.to_string_lossy(), ""); - - Ok(()) - } - #[test] fn should_read_version() -> Result<(), Error> { - let root_directory = find_agb_root_directory()?; + let root_directory = crate::utils::find_agb_root_directory().unwrap(); let my_version = read_cargo_toml_version(&root_directory.join("tools"))?; assert_eq!(my_version, "0.1.0"); @@ -238,7 +220,7 @@ mod test { #[test] fn should_detect_dependencies() -> Result<(), Error> { - let root_directory = find_agb_root_directory()?; + let root_directory = crate::utils::find_agb_root_directory().unwrap(); let deps = get_agb_dependencies(&root_directory.join("agb"))?; assert_eq!( diff --git a/tools/src/utils.rs b/tools/src/utils.rs new file mode 100644 index 00000000..a9759165 --- /dev/null +++ b/tools/src/utils.rs @@ -0,0 +1,27 @@ +use std::{env, path::PathBuf}; + +#[derive(Debug)] +pub struct FindRootDirectoryError; + +pub fn find_agb_root_directory() -> Result { + let mut current_path = env::current_dir().map_err(|_| FindRootDirectoryError)?; + + while !current_path.clone().join("justfile").exists() { + current_path = current_path + .parent() + .ok_or(FindRootDirectoryError)? + .to_owned(); + } + + Ok(current_path) +} + +#[cfg(test)] +mod tests { + use super::find_agb_root_directory; + + #[test] + fn find_agb_root_directory_works() { + find_agb_root_directory().unwrap(); + } +} From 75cd2cd0e1b5cf7d8ce481da076a3702fa24fd4e Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 20:57:41 +0100 Subject: [PATCH 23/49] More functionality in release.rs --- tools/src/release.rs | 48 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/tools/src/release.rs b/tools/src/release.rs index a0c52df2..f10820e2 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -1,3 +1,7 @@ +use std::{io::Read, path::Path, process::Command}; + +use crate::utils::find_agb_root_directory; + pub fn command() -> clap::Command { clap::Command::new("release") .about("Prepares and commits the changes required to release agb") @@ -21,12 +25,46 @@ pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { .get_one::("version") .expect("defined by clap"); - println!("dry run: {}, version: {:?}", dry_run, version); + let root_directory = find_agb_root_directory().map_err(|_| Error::FindRootDirectory)?; + + // if not dry run, check that there are no out-standing changes in git + if !dry_run && !execute_git_command(&root_directory, &["status", "--porcelain"])?.is_empty() { + println!("Uncommitted changes, please commit first"); + return Ok(()); + } + + // Check that we are in the master branch + if !dry_run + && execute_git_command(&root_directory, &["symbolic-ref", "--short", "HEAD"])? != "master" + { + println!("You must be on the master branch before releasing"); + return Ok(()); + } + todo!() } +fn execute_git_command(root_directory: &Path, args: &[&str]) -> Result { + let git_cmd = Command::new("git") + .args(args) + .current_dir(root_directory) + .spawn() + .map_err(|_| Error::GitError)?; + let mut buf = Vec::new(); + git_cmd + .stdout + .ok_or(Error::GitError)? + .read_to_end(&mut buf) + .map_err(|_| Error::GitError)?; + + String::from_utf8(buf).map_err(|_| Error::GitError) +} + #[derive(Debug)] -pub enum Error {} +pub enum Error { + FindRootDirectory, + GitError, +} #[derive(Debug, Clone, PartialEq, Eq)] struct Version { @@ -46,6 +84,12 @@ impl Version { } } +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + #[derive(Debug, PartialEq, Eq)] struct ParseVersionError; From 9af8b19859858d6719a55247a45ec0b90d00d0b2 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 21:00:29 +0100 Subject: [PATCH 24/49] Use .output() instead --- tools/src/release.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tools/src/release.rs b/tools/src/release.rs index f10820e2..c2113fc9 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -1,4 +1,4 @@ -use std::{io::Read, path::Path, process::Command}; +use std::{path::Path, process::Command}; use crate::utils::find_agb_root_directory; @@ -48,22 +48,16 @@ fn execute_git_command(root_directory: &Path, args: &[&str]) -> Result Date: Sat, 1 Oct 2022 21:39:34 +0100 Subject: [PATCH 25/49] Attempt at modifying the cargo tomls with the correct version --- tools/Cargo.lock | 7 ++++ tools/Cargo.toml | 1 + tools/src/release.rs | 89 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/tools/Cargo.lock b/tools/Cargo.lock index 9c1065ce..6b38b187 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -69,6 +69,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "hashbrown" version = "0.12.3" @@ -152,6 +158,7 @@ name = "tools" version = "0.1.0" dependencies = [ "clap", + "glob", "toml_edit", ] diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 7c53cbf7..f735cd0e 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [dependencies] clap = "4" toml_edit = "0.14" +glob = "0.3" \ No newline at end of file diff --git a/tools/src/release.rs b/tools/src/release.rs index c2113fc9..3c1d5ebb 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -41,23 +41,106 @@ pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { return Ok(()); } + let project_toml_files = glob_many(&root_directory, &["agb-*/Cargo.toml"])?; + + update_to_version( + &root_directory, + &root_directory.join("agb/Cargo.toml"), + version, + )?; + + for toml_file in project_toml_files { + update_to_version(&root_directory, &toml_file, version)?; + } + + Command::new("just") + .arg("ci") + .spawn() + .map_err(|_| Error::JustCiFailed)?; + todo!() } +fn update_to_version( + root_directory: &Path, + toml_file: &Path, + new_version: &Version, +) -> Result<(), Error> { + let directory_name = toml_file.parent().unwrap(); + let project_name = directory_name.to_string_lossy().replace('-', "_"); + + let toml_file_content = std::fs::read_to_string(toml_file).map_err(|_| Error::ReadTomlFile)?; + let mut cargo_toml = toml_file_content + .parse::() + .map_err(|_| Error::InvalidToml)?; + + let new_version = format!("{new_version}"); + cargo_toml["package"]["version"] = toml_edit::value(&new_version); + + std::fs::write(toml_file, cargo_toml.to_string()).map_err(|_| Error::WriteTomlFile)?; + + for cargo_toml_file in glob_many( + root_directory, + &[ + "agb-*/Cargo.toml", + "agb/Cargo.toml", + "examples/*/Cargo.toml", + "book/games/*/Cargo.toml", + "template/Cargo.toml", + ], + )? { + let toml_file_content = + std::fs::read_to_string(&cargo_toml_file).map_err(|_| Error::ReadTomlFile)?; + let mut cargo_toml = toml_file_content + .parse::() + .map_err(|_| Error::InvalidToml)?; + + if let Some(this_dep) = cargo_toml["dependencies"].get_mut(&project_name) { + match this_dep { + toml_edit::Item::Value(value) => *value = new_version.clone().into(), + toml_edit::Item::Table(t) => t["version"] = toml_edit::value(&new_version), + _ => return Err(Error::InvalidToml), + } + } + + std::fs::write(cargo_toml_file, cargo_toml.to_string()) + .map_err(|_| Error::WriteTomlFile)?; + } + + Ok(()) +} + fn execute_git_command(root_directory: &Path, args: &[&str]) -> Result { let git_cmd = Command::new("git") .args(args) .current_dir(root_directory) .output() - .map_err(|_| Error::GitError("Failed to run command"))?; + .map_err(|_| Error::Git("Failed to run command"))?; - String::from_utf8(git_cmd.stdout).map_err(|_| Error::GitError("Output not utf-8")) + String::from_utf8(git_cmd.stdout).map_err(|_| Error::Git("Output not utf-8")) +} + +fn glob_many(root_directory: &Path, globs: &[&str]) -> Result, Error> { + let mut result = vec![]; + + for g in globs.iter() { + for path in glob::glob(&root_directory.join(g).to_string_lossy()).expect("Invalid glob") { + result.push(path.map_err(|_| Error::Glob)?); + } + } + + Ok(result) } #[derive(Debug)] pub enum Error { FindRootDirectory, - GitError(&'static str), + Git(&'static str), + Glob, + ReadTomlFile, + InvalidToml, + WriteTomlFile, + JustCiFailed, } #[derive(Debug, Clone, PartialEq, Eq)] From 62964428d88faba2ed971847c5b7bda0ca24dbb0 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 21:55:33 +0100 Subject: [PATCH 26/49] Update the version --- tools/src/release.rs | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/tools/src/release.rs b/tools/src/release.rs index 3c1d5ebb..67b672d3 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -58,7 +58,7 @@ pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { .spawn() .map_err(|_| Error::JustCiFailed)?; - todo!() + Ok(()) } fn update_to_version( @@ -66,13 +66,13 @@ fn update_to_version( toml_file: &Path, new_version: &Version, ) -> Result<(), Error> { - let directory_name = toml_file.parent().unwrap(); + let directory_name = toml_file.parent().unwrap().file_name().unwrap(); let project_name = directory_name.to_string_lossy().replace('-', "_"); let toml_file_content = std::fs::read_to_string(toml_file).map_err(|_| Error::ReadTomlFile)?; let mut cargo_toml = toml_file_content .parse::() - .map_err(|_| Error::InvalidToml)?; + .map_err(|_| Error::InvalidToml(toml_file.to_string_lossy().into_owned()))?; let new_version = format!("{new_version}"); cargo_toml["package"]["version"] = toml_edit::value(&new_version); @@ -93,13 +93,30 @@ fn update_to_version( std::fs::read_to_string(&cargo_toml_file).map_err(|_| Error::ReadTomlFile)?; let mut cargo_toml = toml_file_content .parse::() - .map_err(|_| Error::InvalidToml)?; + .map_err(|_| Error::InvalidToml(cargo_toml_file.to_string_lossy().into_owned()))?; + println!( + "{}: {} {:?}", + cargo_toml_file.to_string_lossy(), + project_name, + cargo_toml["dependencies"].get(&project_name) + ); if let Some(this_dep) = cargo_toml["dependencies"].get_mut(&project_name) { match this_dep { - toml_edit::Item::Value(value) => *value = new_version.clone().into(), - toml_edit::Item::Table(t) => t["version"] = toml_edit::value(&new_version), - _ => return Err(Error::InvalidToml), + toml_edit::Item::Value(s @ toml_edit::Value::String(_)) => { + *s = new_version.clone().into() + } + toml_edit::Item::Value(toml_edit::Value::InlineTable(t)) => { + t["version"] = new_version.clone().into() + } + toml_edit::Item::None => continue, + _ => { + return Err(Error::InvalidToml(format!( + "{:?} while seaching dependencies in {}", + this_dep, + cargo_toml_file.to_string_lossy() + ))) + } } } @@ -138,7 +155,7 @@ pub enum Error { Git(&'static str), Glob, ReadTomlFile, - InvalidToml, + InvalidToml(String), WriteTomlFile, JustCiFailed, } From 0678752be0d57643202dcdddcd869d47acd4a3ad Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 21:58:57 +0100 Subject: [PATCH 27/49] Finish off the last bit --- tools/src/release.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tools/src/release.rs b/tools/src/release.rs index 67b672d3..34e1225d 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -58,6 +58,26 @@ pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { .spawn() .map_err(|_| Error::JustCiFailed)?; + if !dry_run { + execute_git_command( + &root_directory, + &["commit", "-am", &format!("Release v{version}")], + )?; + execute_git_command( + &root_directory, + &[ + "tag", + "-a", + &version.to_string(), + "-m", + &format!("v{version}"), + ], + )?; + } + + println!("Done! Push with"); + println!("git push --atomic origin master v{version}"); + Ok(()) } @@ -95,12 +115,6 @@ fn update_to_version( .parse::() .map_err(|_| Error::InvalidToml(cargo_toml_file.to_string_lossy().into_owned()))?; - println!( - "{}: {} {:?}", - cargo_toml_file.to_string_lossy(), - project_name, - cargo_toml["dependencies"].get(&project_name) - ); if let Some(this_dep) = cargo_toml["dependencies"].get_mut(&project_name) { match this_dep { toml_edit::Item::Value(s @ toml_edit::Value::String(_)) => { From 53aa9b5c103b1333a7b4a382ba02ae6887940710 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 22:01:36 +0100 Subject: [PATCH 28/49] Spawn doesn't wait for the command to finish --- tools/src/publish.rs | 2 ++ tools/src/release.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tools/src/publish.rs b/tools/src/publish.rs index 0f8935f7..b6002293 100644 --- a/tools/src/publish.rs +++ b/tools/src/publish.rs @@ -66,6 +66,8 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> { .arg("publish") .current_dir(&root_directory.join(publishable_crate)) .spawn() + .map_err(|_| Error::PublishCrate)? + .wait() .map_err(|_| Error::PublishCrate)?; } diff --git a/tools/src/release.rs b/tools/src/release.rs index 34e1225d..1b35697e 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -56,6 +56,8 @@ pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { Command::new("just") .arg("ci") .spawn() + .map_err(|_| Error::JustCiFailed)? + .wait() .map_err(|_| Error::JustCiFailed)?; if !dry_run { From 52adc63581c81e36e21b8c7336a1b41afeca4a0f Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 17:40:23 +0100 Subject: [PATCH 29/49] Run cargo update on all crates after updating the version number --- tools/src/release.rs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tools/src/release.rs b/tools/src/release.rs index 1b35697e..b7ccd17c 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -42,19 +42,35 @@ pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { } let project_toml_files = glob_many(&root_directory, &["agb-*/Cargo.toml"])?; + let agb_cargo_toml = root_directory.join("agb/Cargo.toml"); - update_to_version( - &root_directory, - &root_directory.join("agb/Cargo.toml"), - version, - )?; + update_to_version(&root_directory, &agb_cargo_toml, version)?; - for toml_file in project_toml_files { - update_to_version(&root_directory, &toml_file, version)?; + for toml_file in &project_toml_files { + update_to_version(&root_directory, toml_file, version)?; + } + + for toml_file in project_toml_files + .iter() + .chain(std::iter::once(&agb_cargo_toml)) + { + let directory_name = toml_file.parent().unwrap(); + + println!( + "Running cargo update in {}", + directory_name.to_string_lossy() + ); + + Command::new("cargo") + .arg("update") + .current_dir(directory_name) + .output() + .map_err(|_| Error::CargoUpdateFailed)?; } Command::new("just") .arg("ci") + .current_dir(&root_directory) .spawn() .map_err(|_| Error::JustCiFailed)? .wait() @@ -174,6 +190,7 @@ pub enum Error { InvalidToml(String), WriteTomlFile, JustCiFailed, + CargoUpdateFailed, } #[derive(Debug, Clone, PartialEq, Eq)] From 0c0e9e7165c1246b43160a530d12950f27d5dcf1 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 17:51:30 +0100 Subject: [PATCH 30/49] Actually assert that things return successfully --- tools/src/release.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tools/src/release.rs b/tools/src/release.rs index b7ccd17c..ea94b080 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -61,20 +61,20 @@ pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { directory_name.to_string_lossy() ); - Command::new("cargo") + assert!(Command::new("cargo") .arg("update") .current_dir(directory_name) - .output() - .map_err(|_| Error::CargoUpdateFailed)?; + .status() + .map_err(|_| Error::CargoUpdateFailed)? + .success()); } - Command::new("just") + assert!(Command::new("just") .arg("ci") .current_dir(&root_directory) - .spawn() + .status() .map_err(|_| Error::JustCiFailed)? - .wait() - .map_err(|_| Error::JustCiFailed)?; + .success()); if !dry_run { execute_git_command( @@ -166,6 +166,8 @@ fn execute_git_command(root_directory: &Path, args: &[&str]) -> Result Date: Sun, 2 Oct 2022 17:52:15 +0100 Subject: [PATCH 31/49] Fix cargo publish while I'm at it --- tools/src/publish.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tools/src/publish.rs b/tools/src/publish.rs index b6002293..da637e1e 100644 --- a/tools/src/publish.rs +++ b/tools/src/publish.rs @@ -62,13 +62,12 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> { if *dry_run { println!("Would execute cargo publish for {publishable_crate}"); } else { - Command::new("cargo") + assert!(Command::new("cargo") .arg("publish") .current_dir(&root_directory.join(publishable_crate)) - .spawn() + .status() .map_err(|_| Error::PublishCrate)? - .wait() - .map_err(|_| Error::PublishCrate)?; + .success()); } published_crates.insert(publishable_crate.to_string()); From a52b1e16cce15621b38375e70e4242e5c6cc9abf Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 17:53:42 +0100 Subject: [PATCH 32/49] cargo run adds junk to the environment --- justfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index 23837d70..542861fd 100644 --- a/justfile +++ b/justfile @@ -69,7 +69,8 @@ update-linker-scripts: publish: (_run-tool "publish") _run-tool +tool: - cargo run --manifest-path "{{justfile_directory() + "/tools/Cargo.toml"}}" -- {{tool}} + (cd tools && cargo build) + tools/target/debug/tools {{tool}} _build-rom folder name: #!/usr/bin/env bash From 866cc8f5a00d4ec5d080723c02a019bf6ae238d9 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 17:55:05 +0100 Subject: [PATCH 33/49] Add release just script --- justfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/justfile b/justfile index 542861fd..575f9c3a 100644 --- a/justfile +++ b/justfile @@ -68,6 +68,8 @@ update-linker-scripts: publish: (_run-tool "publish") +release +args: (_run-tool "release" args) + _run-tool +tool: (cd tools && cargo build) tools/target/debug/tools {{tool}} From 0bc3ec7481912f26a7771d557809d80e3818579b Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 17:55:17 +0100 Subject: [PATCH 34/49] Delete the old release script --- release.sh | 96 ------------------------------------------------------ 1 file changed, 96 deletions(-) delete mode 100755 release.sh diff --git a/release.sh b/release.sh deleted file mode 100755 index a454e429..00000000 --- a/release.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash - -# Fail if any command fails -set -e -set -x - -VERSION=$1 -NO_COMMIT=$2 - -# Sanity check that we actually have a version -if [ "$VERSION" = "" ]; then - echo "Usage $0 [--no-commit]" - exit 1 -fi - -# Check the format of version -if echo "$VERSION" | grep -q -Ev "^[0-9]+\.[0-9]+\.[0-9]+$"; then - echo "Version must be of the form x.y.z, got $VERSION" - exit 1 -fi - -# Check if no commit option is valid -if [ ! "$NO_COMMIT" = "" ] && [ ! "$NO_COMMIT" = "--no-commit" ]; then - echo "Must pass either no last argument or --no-commit" - exit 1 -fi - -function maybe_git() { - if [ "$NO_COMMIT" = "--no-commit" ]; then - echo "Would run: git $*" - else - git "$@" - fi -} - -# Check that no out-standing changes in git -if [ "$NO_COMMIT" = "" ] && [ -n "$(git status --porcelain)" ]; then - echo "Uncommitted changes, please commit first" - exit 1 -fi - -# Check that we are in the master branch, but only if actually committing -if [ ! "$NO_COMMIT" = "--no-commit" ] && [ "$(git symbolic-ref --short HEAD)" != "master" ]; then - echo "You must be in the master branch before releasing" - exit 1 -fi - -TAGNAME="v$VERSION" - -for PROJECT_TOML_FILE in agb/Cargo.toml agb-*/Cargo.toml; do - DIRECTORY=$(dirname "$PROJECT_TOML_FILE") - - # Update the version in Cargo.toml - sed -i -e "s/^version = \".*\"/version = \"$VERSION\"/" "$DIRECTORY/Cargo.toml" - - # Also update the lock file - (cd "$DIRECTORY" && cargo update) - - if [ "$DIRECTORY" = "agb" ]; then - # also update the agb version in the template and the examples - sed -i -e "s/^agb = \".*\"/agb = \"$VERSION\"/" template/Cargo.toml - - for EXAMPLE_TOML_FILE in examples/*/Cargo.toml book/games/*/Cargo.toml template/Cargo.toml; do - EXAMPLE_DIR=$(dirname "$EXAMPLE_TOML_FILE") - sed -E -i -e "/agb =/ s/version = \"[^\"]+\"/version = \"$VERSION\"/" "$EXAMPLE_DIR/Cargo.toml" - done - for EXAMPLE_TOML_FILE in examples/*/Cargo.toml book/games/*/Cargo.toml; do - EXAMPLE_DIR=$(dirname "$EXAMPLE_TOML_FILE") - (cd "$EXAMPLE_DIR" && cargo update) - done - else - PROJECT_NAME_WITH_UNDERSCORES=$(echo -n "$DIRECTORY" | tr - _) - - for CARGO_TOML_FILE in agb-*/Cargo.toml agb/Cargo.toml examples/*/Cargo.toml book/games/*/Cargo.toml; do - sed -i -E -e "s/($PROJECT_NAME_WITH_UNDERSCORES = .*version = \")[^\"]+(\".*)/\1$VERSION\2/" "$CARGO_TOML_FILE" - (cd "$(dirname "$CARGO_TOML_FILE")" && cargo generate-lockfile) - done - fi -done - -# Sanity check to make sure the build works -just ci - -for EXAMPLE_TOML_FILE in examples/*/Cargo.toml book/games/*/Cargo.toml; do - EXAMPLE_DIR=$(dirname "$EXAMPLE_TOML_FILE") - (cd "$EXAMPLE_DIR" && cargo check --release) -done - -# Commit the Cargo.toml changes -maybe_git commit -am "Release v$VERSION" - -# Tag the version -maybe_git tag -a "$TAGNAME" -m "v$VERSION" - -echo "Done! Push with" -echo "git push --atomic origin master $TAGNAME" From 030d43d68cf34afa975690bcf5e3ce7b5176ba2d Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 18:13:31 +0100 Subject: [PATCH 35/49] Attempt to write something which updates the changelog for me --- CHANGELOG.md | 2 +- tools/Cargo.lock | 205 +++++++++++++++++++++++++++++++++++++++++++ tools/Cargo.toml | 3 +- tools/src/release.rs | 58 ++++++++++-- 4 files changed, 260 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2f75f63..92c2f064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the fast magnitude function in agb_fixnum. This is also used in fast_normalise. Previously only worked for positive (x, y). - Fixed formatting of fixed point numbers in the range (-1, 0), which previously appeared positive. -## Changed +### Changed - `testing` is now a default feature, so you no longer need to add a separate `dev-dependencies` line for `agb` in order to enable unit tests for your project. ## [0.11.1] - 2022/08/02 diff --git a/tools/Cargo.lock b/tools/Cargo.lock index 6b38b187..871dc847 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "atty" version = "0.2.14" @@ -25,12 +34,39 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + [[package]] name = "bytes" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + [[package]] name = "clap" version = "4.0.7" @@ -63,6 +99,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "either" version = "1.8.0" @@ -90,6 +132,19 @@ dependencies = [ "libc", ] +[[package]] +name = "iana-time-zone" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + [[package]] name = "indexmap" version = "1.9.1" @@ -109,30 +164,102 @@ dependencies = [ "either", ] +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" + [[package]] name = "os_str_bytes" version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +[[package]] +name = "proc-macro2" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "syn" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -142,6 +269,17 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "toml_edit" version = "0.14.4" @@ -157,11 +295,78 @@ dependencies = [ name = "tools" version = "0.1.0" dependencies = [ + "chrono", "clap", "glob", "toml_edit", ] +[[package]] +name = "unicode-ident" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + [[package]] name = "winapi" version = "0.3.9" diff --git a/tools/Cargo.toml b/tools/Cargo.toml index f735cd0e..b0777d6c 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -8,4 +8,5 @@ edition = "2021" [dependencies] clap = "4" toml_edit = "0.14" -glob = "0.3" \ No newline at end of file +glob = "0.3" +chrono = "0.4" diff --git a/tools/src/release.rs b/tools/src/release.rs index ea94b080..a6f82c6b 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -69,12 +69,16 @@ pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { .success()); } - assert!(Command::new("just") - .arg("ci") - .current_dir(&root_directory) - .status() - .map_err(|_| Error::JustCiFailed)? - .success()); + // assert!(Command::new("just") + // .arg("ci") + // .current_dir(&root_directory) + // .status() + // .map_err(|_| Error::JustCiFailed)? + // .success()); + + let changelog_text = update_changelog(&root_directory, version)?; + + println!("Content of changelog:\n\n{changelog_text}"); if !dry_run { execute_git_command( @@ -159,6 +163,45 @@ fn update_to_version( Ok(()) } +fn update_changelog(root_directory: &Path, new_version: &Version) -> Result { + use chrono::Datelike; + + let changelog_file = root_directory.join("CHANGELOG.md"); + let changelog_content = + std::fs::read_to_string(&changelog_file).map_err(|_| Error::FailedToReadChangelog)?; + + let today = chrono::Local::today(); + let formatted_date = format!( + "{:04}/{:02}/{:02}", + today.year(), + today.month(), + today.day() + ); + + const UNRELEASED_HEADER: &str = "## [Unreleased]"; + + let unreleased_bit_start = changelog_content + .find(UNRELEASED_HEADER) + .ok_or(Error::FailedToParseChangelog)? + + UNRELEASED_HEADER.len(); + let unreleased_bit_end = changelog_content + .find("\n## [") // the start of the next entry + .ok_or(Error::FailedToParseChangelog)?; + + let change_content = changelog_content[unreleased_bit_start..unreleased_bit_end].to_owned(); + + let changelog_content = changelog_content.replacen( + UNRELEASED_HEADER, + &format!("{UNRELEASED_HEADER}\n\n## [{new_version}] - {formatted_date}"), + 1, + ); + + std::fs::write(&changelog_file, &changelog_content) + .map_err(|_| Error::FailedToWriteChangelog)?; + + Ok(change_content) +} + fn execute_git_command(root_directory: &Path, args: &[&str]) -> Result { let git_cmd = Command::new("git") .args(args) @@ -193,6 +236,9 @@ pub enum Error { WriteTomlFile, JustCiFailed, CargoUpdateFailed, + FailedToReadChangelog, + FailedToWriteChangelog, + FailedToParseChangelog, } #[derive(Debug, Clone, PartialEq, Eq)] From 7a55f50adf20001cc81d36351fed556b3a32ae5f Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 18:19:14 +0100 Subject: [PATCH 36/49] Extract changelog text into the annotation --- justfile | 2 +- tools/src/release.rs | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/justfile b/justfile index 575f9c3a..201ced58 100644 --- a/justfile +++ b/justfile @@ -72,7 +72,7 @@ release +args: (_run-tool "release" args) _run-tool +tool: (cd tools && cargo build) - tools/target/debug/tools {{tool}} + "$CARGO_TARGET_DIR/debug/tools" {{tool}} _build-rom folder name: #!/usr/bin/env bash diff --git a/tools/src/release.rs b/tools/src/release.rs index a6f82c6b..f8671db4 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -69,16 +69,16 @@ pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { .success()); } - // assert!(Command::new("just") - // .arg("ci") - // .current_dir(&root_directory) - // .status() - // .map_err(|_| Error::JustCiFailed)? - // .success()); + assert!(Command::new("just") + .arg("ci") + .current_dir(&root_directory) + .status() + .map_err(|_| Error::JustCiFailed)? + .success()); let changelog_text = update_changelog(&root_directory, version)?; - println!("Content of changelog:\n\n{changelog_text}"); + println!("Content of changelog:\n{changelog_text}"); if !dry_run { execute_git_command( @@ -92,7 +92,7 @@ pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { "-a", &version.to_string(), "-m", - &format!("v{version}"), + &format!("#v{version}\n{changelog_text}"), ], )?; } @@ -184,9 +184,10 @@ fn update_changelog(root_directory: &Path, new_version: &Version) -> Result Date: Sun, 2 Oct 2022 18:19:41 +0100 Subject: [PATCH 37/49] cargo update isn't needed --- tools/src/release.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tools/src/release.rs b/tools/src/release.rs index f8671db4..30ae3d2e 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -50,25 +50,6 @@ pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { update_to_version(&root_directory, toml_file, version)?; } - for toml_file in project_toml_files - .iter() - .chain(std::iter::once(&agb_cargo_toml)) - { - let directory_name = toml_file.parent().unwrap(); - - println!( - "Running cargo update in {}", - directory_name.to_string_lossy() - ); - - assert!(Command::new("cargo") - .arg("update") - .current_dir(directory_name) - .status() - .map_err(|_| Error::CargoUpdateFailed)? - .success()); - } - assert!(Command::new("just") .arg("ci") .current_dir(&root_directory) From 3b3ec14f63e03516e0dd07c7e420a93a53b6e446 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 18:25:01 +0100 Subject: [PATCH 38/49] Remove agb-tests folder from the workspace --- .vscode/agb.code-workspace | 3 --- 1 file changed, 3 deletions(-) diff --git a/.vscode/agb.code-workspace b/.vscode/agb.code-workspace index 0d33085f..548a9811 100644 --- a/.vscode/agb.code-workspace +++ b/.vscode/agb.code-workspace @@ -30,9 +30,6 @@ { "path": "../template" }, - { - "path": "../agb-tests" - }, { "path": "../book" }, From 2d23f0a36c77476b04d32012e89fca16554148a0 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 18:44:48 +0100 Subject: [PATCH 39/49] Implement size_hint for the hashmap iterators --- agb/src/hash_map.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/agb/src/hash_map.rs b/agb/src/hash_map.rs index 45939093..11cf4519 100644 --- a/agb/src/hash_map.rs +++ b/agb/src/hash_map.rs @@ -351,6 +351,10 @@ impl<'a, K, V, ALLOCATOR: ClonableAllocator> Iterator for Iter<'a, K, V, ALLOCAT } } } + + fn size_hint(&self) -> (usize, Option) { + (self.map.len(), Some(self.map.len())) + } } impl<'a, K, V, ALLOCATOR: ClonableAllocator> IntoIterator for &'a HashMap { @@ -388,6 +392,10 @@ impl Iterator for IterOwned } } } + + fn size_hint(&self) -> (usize, Option) { + (self.map.len(), Some(self.map.len())) + } } /// An iterator over entries of a [`HashMap`] From c658b6afe5708ff17a42eb2c3c347bb6e0ce0dd6 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 18:55:21 +0100 Subject: [PATCH 40/49] Add implementation of retain to HashMap --- agb/src/hash_map.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/agb/src/hash_map.rs b/agb/src/hash_map.rs index 11cf4519..9a73c4a8 100644 --- a/agb/src/hash_map.rs +++ b/agb/src/hash_map.rs @@ -196,6 +196,14 @@ impl HashMap { self.nodes.nodes.iter_mut().filter_map(Node::key_value_mut) } + /// Retains only the elements specified by the predicate `f`. + pub fn retain(&mut self, f: F) + where + F: FnMut(&K, &mut V) -> bool, + { + self.nodes.retain(f); + } + /// Returns `true` if the map contains no elements #[must_use] pub fn is_empty(&self) -> bool { @@ -737,6 +745,27 @@ impl NodeStorage { inserted_location } + fn retain(&mut self, mut f: F) + where + F: FnMut(&K, &mut V) -> bool, + { + let num_nodes = self.nodes.len(); + let mut i = 0; + + while i < num_nodes { + let node = &mut self.nodes[i]; + + if let Some((k, v)) = node.key_value_mut() { + if !f(k, v) { + self.remove_from_location(i); + continue; + } + } + + i += 1; + } + } + fn remove_from_location(&mut self, location: usize) -> V { let mut current_location = location; self.number_of_items -= 1; @@ -1441,5 +1470,21 @@ mod test { assert_eq!(map[&2], 1); } + + #[test_case] + fn test_retain(_gba: &mut Gba) { + let mut map = HashMap::new(); + + for i in 0..100 { + map.insert(i, i); + } + + map.retain(|k, _| k % 2 == 0); + + assert_eq!(map[&2], 2); + assert_eq!(map.get(&3), None); + + assert_eq!(map.iter().count(), 50); // force full iteration + } } } From 9534ccc59c1081cd9218bea7270d6deff0f5878f Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 18:55:57 +0100 Subject: [PATCH 41/49] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92c2f064..ce9189b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for the blend mode of the GBA. Blending allows for alpha blending between layers and fading to black and white. - Added a new agb::sync module that contains GBA-specific synchronization primitives. - Added support for save files. +- Added implementation of `HashMap.retain()`. ### Changes - Many of the places that originally disabled IRQs now use the `sync` module, reducing the chance of missed interrupts. From 01862901c26399c8f1affc6ac77162ec37b64913 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 18:57:39 +0100 Subject: [PATCH 42/49] Mention the size_hint implementation in the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9189b2..4db58884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changes - Many of the places that originally disabled IRQs now use the `sync` module, reducing the chance of missed interrupts. +- HashMap iterators now implement `size_hint` which should result in slightly better generation of code using those iterators. ### Fixed - Fixed the fast magnitude function in agb_fixnum. This is also used in fast_normalise. Previously only worked for positive (x, y). From a44b8a4013e82ebd7eff9c45bcca30d59ff255a0 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 19:00:59 +0100 Subject: [PATCH 43/49] Add comment explaining the `continue` statement --- agb/src/hash_map.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/agb/src/hash_map.rs b/agb/src/hash_map.rs index 9a73c4a8..e338a17e 100644 --- a/agb/src/hash_map.rs +++ b/agb/src/hash_map.rs @@ -758,6 +758,10 @@ impl NodeStorage { if let Some((k, v)) = node.key_value_mut() { if !f(k, v) { self.remove_from_location(i); + + // Need to continue before adding 1 to i because remove from location could + // put the element which was next into the ith location in the nodes array, + // so we need to check if that one needs removing too. continue; } } From 78554862c84156274d3897a602b097e85c9a583b Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 19:05:29 +0100 Subject: [PATCH 44/49] Move the tests to a better location --- agb/src/hash_map.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/agb/src/hash_map.rs b/agb/src/hash_map.rs index e338a17e..d38e60c2 100644 --- a/agb/src/hash_map.rs +++ b/agb/src/hash_map.rs @@ -1263,6 +1263,22 @@ mod test { drop_registry.assert_dropped_n_times(id1, 2); } + #[test_case] + fn test_retain(_gba: &mut Gba) { + let mut map = HashMap::new(); + + for i in 0..100 { + map.insert(i, i); + } + + map.retain(|k, _| k % 2 == 0); + + assert_eq!(map[&2], 2); + assert_eq!(map.get(&3), None); + + assert_eq!(map.iter().count(), 50); // force full iteration + } + // Following test cases copied from the rust source // https://github.com/rust-lang/rust/blob/master/library/std/src/collections/hash/map/tests.rs mod rust_std_tests { From 4bf3e0d59749a0febbcb90c72a4179a285333043 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 19:10:50 +0100 Subject: [PATCH 45/49] Add tests for size_hint and ensure .iter() returns a better iterator --- agb/src/hash_map.rs | 80 +++++++++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/agb/src/hash_map.rs b/agb/src/hash_map.rs index d38e60c2..d537b4cf 100644 --- a/agb/src/hash_map.rs +++ b/agb/src/hash_map.rs @@ -188,7 +188,11 @@ impl HashMap { /// An iterator visiting all key-value pairs in an arbitrary order pub fn iter(&self) -> impl Iterator { - self.nodes.nodes.iter().filter_map(Node::key_value_ref) + Iter { + map: self, + at: 0, + num_found: 0, + } } /// An iterator visiting all key-value pairs in an arbitrary order, with mutable references to the values @@ -340,6 +344,7 @@ where pub struct Iter<'a, K: 'a, V: 'a, ALLOCATOR: ClonableAllocator> { map: &'a HashMap, at: usize, + num_found: usize, } impl<'a, K, V, ALLOCATOR: ClonableAllocator> Iterator for Iter<'a, K, V, ALLOCATOR> { @@ -355,13 +360,17 @@ impl<'a, K, V, ALLOCATOR: ClonableAllocator> Iterator for Iter<'a, K, V, ALLOCAT self.at += 1; if node.has_value() { + self.num_found += 1; return Some((node.key_ref().unwrap(), node.value_ref().unwrap())); } } } fn size_hint(&self) -> (usize, Option) { - (self.map.len(), Some(self.map.len())) + ( + self.map.len() - self.num_found, + Some(self.map.len() - self.num_found), + ) } } @@ -370,7 +379,11 @@ impl<'a, K, V, ALLOCATOR: ClonableAllocator> IntoIterator for &'a HashMap; fn into_iter(self) -> Self::IntoIter { - Iter { map: self, at: 0 } + Iter { + map: self, + at: 0, + num_found: 0, + } } } @@ -381,6 +394,7 @@ impl<'a, K, V, ALLOCATOR: ClonableAllocator> IntoIterator for &'a HashMap { map: HashMap, at: usize, + num_found: usize, } impl Iterator for IterOwned { @@ -396,13 +410,17 @@ impl Iterator for IterOwned self.at += 1; if let Some((k, v, _)) = maybe_kv { + self.num_found += 1; return Some((k, v)); } } } fn size_hint(&self) -> (usize, Option) { - (self.map.len(), Some(self.map.len())) + ( + self.map.len() - self.num_found, + Some(self.map.len() - self.num_found), + ) } } @@ -415,7 +433,11 @@ impl IntoIterator for HashMap; fn into_iter(self) -> Self::IntoIter { - IterOwned { map: self, at: 0 } + IterOwned { + map: self, + at: 0, + num_found: 0, + } } } @@ -1279,6 +1301,38 @@ mod test { assert_eq!(map.iter().count(), 50); // force full iteration } + #[test_case] + fn test_size_hint_iter(_gba: &mut Gba) { + let mut map = HashMap::new(); + + for i in 0..100 { + map.insert(i, i); + } + + let mut iter = map.iter(); + assert_eq!(iter.size_hint(), (100, Some(100))); + + iter.next(); + + assert_eq!(iter.size_hint(), (99, Some(99))); + } + + #[test_case] + fn test_size_hint_into_iter(_gba: &mut Gba) { + let mut map = HashMap::new(); + + for i in 0..100 { + map.insert(i, i); + } + + let mut iter = map.into_iter(); + assert_eq!(iter.size_hint(), (100, Some(100))); + + iter.next(); + + assert_eq!(iter.size_hint(), (99, Some(99))); + } + // Following test cases copied from the rust source // https://github.com/rust-lang/rust/blob/master/library/std/src/collections/hash/map/tests.rs mod rust_std_tests { @@ -1490,21 +1544,5 @@ mod test { assert_eq!(map[&2], 1); } - - #[test_case] - fn test_retain(_gba: &mut Gba) { - let mut map = HashMap::new(); - - for i in 0..100 { - map.insert(i, i); - } - - map.retain(|k, _| k % 2 == 0); - - assert_eq!(map[&2], 2); - assert_eq!(map.get(&3), None); - - assert_eq!(map.iter().count(), 50); // force full iteration - } } } From cdad8afe702de1baba9361ebab88210a6806a153 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 19:59:21 +0100 Subject: [PATCH 46/49] Add some badges to the readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index df80be43..578a72b6 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ ## Rust for the Game Boy Advance +[![Docs](https://docs.rs/agb/badge.svg)](https://docs.rs/agb/latest/agb) +[![Build](https://github.com/agbrs/agb/actions/workflows/build-and-test.yml/badge.svg?query=branch%3Amaster)](https://github.com/agbrs/agb/actions/workflows/build-and-test.yml) +[![Licence](https://img.shields.io/crates/l/agb)](https://www.mozilla.org/en-US/MPL/2.0/) +[![Crates.io](https://img.shields.io/crates/v/agb)](https://crates.io/crates/agb) + ![AGB logo](.github/logo.png) This is a library for making games on the Game Boy Advance using the Rust From 5bb1c3b162b4e56083f5dd2f8986e1d4bd65a20a Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sun, 2 Oct 2022 20:10:46 +0100 Subject: [PATCH 47/49] Use the correct way to specify the branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 578a72b6..6da2f92a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Rust for the Game Boy Advance [![Docs](https://docs.rs/agb/badge.svg)](https://docs.rs/agb/latest/agb) -[![Build](https://github.com/agbrs/agb/actions/workflows/build-and-test.yml/badge.svg?query=branch%3Amaster)](https://github.com/agbrs/agb/actions/workflows/build-and-test.yml) +[![Build](https://github.com/agbrs/agb/actions/workflows/build-and-test.yml/badge.svg?branch=master)](https://github.com/agbrs/agb/actions/workflows/build-and-test.yml) [![Licence](https://img.shields.io/crates/l/agb)](https://www.mozilla.org/en-US/MPL/2.0/) [![Crates.io](https://img.shields.io/crates/v/agb)](https://crates.io/crates/agb) From c90e8de893703ac6dcc69eafdbb7c62156db2386 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 8 Oct 2022 11:34:43 +0100 Subject: [PATCH 48/49] clippy autofix --- agb/src/agb_alloc/bump_allocator.rs | 2 +- agb/src/display/object.rs | 8 ++++---- agb/src/display/tiled/vram_manager.rs | 2 +- agb/src/save/flash.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/agb/src/agb_alloc/bump_allocator.rs b/agb/src/agb_alloc/bump_allocator.rs index 717ba45e..0ea27d2d 100644 --- a/agb/src/agb_alloc/bump_allocator.rs +++ b/agb/src/agb_alloc/bump_allocator.rs @@ -43,7 +43,7 @@ impl BumpAllocator { let resulting_ptr = ptr + amount_to_add; let new_current_ptr = resulting_ptr + layout.size(); - if new_current_ptr as usize >= (self.start_end.borrow(cs).end)() { + if new_current_ptr >= (self.start_end.borrow(cs).end)() { return None; } diff --git a/agb/src/display/object.rs b/agb/src/display/object.rs index 3888a8af..08892e33 100644 --- a/agb/src/display/object.rs +++ b/agb/src/display/object.rs @@ -627,7 +627,7 @@ impl ObjectController { unsafe { (OBJECT_ATTRIBUTE_MEMORY as *mut u16) - .add((i as usize) * 4) + .add(i * 4) .write_volatile(HIDDEN_VALUE); } @@ -794,7 +794,7 @@ impl ObjectController { }); let loan = Loan { - index: index as u8, + index, phantom: PhantomData, }; @@ -916,8 +916,8 @@ impl<'a> Object<'a> { /// [ObjectController::commit] is called. pub fn set_x(&mut self, x: u16) -> &mut Self { let object_inner = unsafe { self.object_inner() }; - object_inner.attrs.a1a.set_x(x.rem_euclid(1 << 9) as u16); - object_inner.attrs.a1s.set_x(x.rem_euclid(1 << 9) as u16); + object_inner.attrs.a1a.set_x(x.rem_euclid(1 << 9)); + object_inner.attrs.a1s.set_x(x.rem_euclid(1 << 9)); self } diff --git a/agb/src/display/tiled/vram_manager.rs b/agb/src/display/tiled/vram_manager.rs index 6caa8956..9bd9cd0e 100644 --- a/agb/src/display/tiled/vram_manager.rs +++ b/agb/src/display/tiled/vram_manager.rs @@ -224,7 +224,7 @@ impl VRamManager { slice::from_raw_parts_mut( tiles .as_mut_ptr() - .add((index * tile_format.tile_size()) as usize) + .add(index * tile_format.tile_size()) .cast(), tile_format.tile_size() / core::mem::size_of::(), ) diff --git a/agb/src/save/flash.rs b/agb/src/save/flash.rs index 0384926b..28b21e50 100644 --- a/agb/src/save/flash.rs +++ b/agb/src/save/flash.rs @@ -51,7 +51,7 @@ fn set_bank(bank: u8) -> Result<(), Error> { Err(Error::OutOfBounds) } else if bank != CURRENT_BANK.read() { issue_flash_command(CMD_SET_BANK); - FLASH_PORT_BANK.set(bank as u8); + FLASH_PORT_BANK.set(bank); CURRENT_BANK.write(bank); Ok(()) } else { From 467ebb024028d2c250904efc96cc7f8f5cfaf2b6 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 8 Oct 2022 11:48:11 +0100 Subject: [PATCH 49/49] cargo clippy fixes in examples --- examples/hyperspace-roll/src/battle.rs | 2 +- examples/the-purple-night/src/main.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/hyperspace-roll/src/battle.rs b/examples/hyperspace-roll/src/battle.rs index 3fd3b870..b4f6cf96 100644 --- a/examples/hyperspace-roll/src/battle.rs +++ b/examples/hyperspace-roll/src/battle.rs @@ -207,7 +207,7 @@ impl RolledDice { let heal = *face_counts.entry(Face::Heal).or_default(); if heal != 0 { actions.push(Action::PlayerHeal { - amount: ((heal * (heal + 1)) / 2) as u32, + amount: (heal * (heal + 1)) / 2, }); } diff --git a/examples/the-purple-night/src/main.rs b/examples/the-purple-night/src/main.rs index 07fc59cc..54c9a6f6 100644 --- a/examples/the-purple-night/src/main.rs +++ b/examples/the-purple-night/src/main.rs @@ -129,7 +129,7 @@ impl<'a> Level<'a> { let factor: Number = Number::new(1) / Number::new(8); let (x, y) = (v * factor).floor().get(); - if (x < 0 || x > tilemap::WIDTH as i32) || (y < 0 || y > tilemap::HEIGHT as i32) { + if !(0..=tilemap::WIDTH).contains(&x) || !(0..=tilemap::HEIGHT).contains(&y) { return Some(Rect::new((x * 8, y * 8).into(), (8, 8).into())); } let position = tilemap::WIDTH as usize * y as usize + x as usize; @@ -1878,7 +1878,7 @@ enum MoveState { impl<'a> Game<'a> { fn has_just_reached_end(&self) -> bool { match self.boss { - BossState::NotSpawned => self.offset.x.floor() + 248 >= tilemap::WIDTH as i32 * 8, + BossState::NotSpawned => self.offset.x.floor() + 248 >= tilemap::WIDTH * 8, _ => false, } } @@ -1901,13 +1901,13 @@ impl<'a> Game<'a> { if self.has_just_reached_end() { sfx.boss(); - self.offset.x = (tilemap::WIDTH as i32 * 8 - 248).into(); + self.offset.x = (tilemap::WIDTH * 8 - 248).into(); self.move_state = MoveState::PinnedAtEnd; self.boss = BossState::Active(Boss::new(object_controller, self.offset)) } } MoveState::PinnedAtEnd => { - self.offset.x = (tilemap::WIDTH as i32 * 8 - 248).into(); + self.offset.x = (tilemap::WIDTH * 8 - 248).into(); } MoveState::FollowingPlayer => { Game::update_sunrise(vram, self.sunrise_timer); @@ -1917,8 +1917,8 @@ impl<'a> Game<'a> { let difference = self.player.entity.position.x - (self.offset.x + WIDTH / 2); self.offset.x += difference / 8; - if self.offset.x > (tilemap::WIDTH as i32 * 8 - 248).into() { - self.offset.x = (tilemap::WIDTH as i32 * 8 - 248).into(); + if self.offset.x > (tilemap::WIDTH * 8 - 248).into() { + self.offset.x = (tilemap::WIDTH * 8 - 248).into(); } else if self.offset.x < 8.into() { self.offset.x = 8.into(); self.move_state = MoveState::Ending;