Use portable atomics and other similar libraries (#565)

I tried switching out our statics to use the standard portable atomics,
critical section, and once cell crates.
One of the save test fails in release mode for me, not sure why but
these tests failing always suggests magic is going on.

- [x] Changelog updated / no changelog update needed
This commit is contained in:
Corwin 2024-04-09 22:23:40 +01:00 committed by GitHub
commit 2bacef161b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 300 additions and 708 deletions

View file

@ -22,10 +22,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Many macros now emit statics rather than consts OR can be used as statics OR - Many macros now emit statics rather than consts OR can be used as statics OR
have had examples changed to use statics. You should use statics where possble have had examples changed to use statics. You should use statics where possible
for assets as consts can lead to them being included multiple times in the for assets as consts can lead to them being included multiple times in the
ROM. ROM.
- Fixnums are now implemented with `num_traits` trait definitions. - Fixnums are now implemented with `num_traits` trait definitions.
- Rather than having our own sync with Statics, use the standard portable
atomics crate. These are reexported for convenience.
## [0.19.1] - 2024/03/06 ## [0.19.1] - 2024/03/06

View file

@ -19,9 +19,11 @@ agb_sound_converter = { version = "0.19.1", path = "../agb-sound-converter" }
agb_macros = { version = "0.19.1", path = "../agb-macros" } agb_macros = { version = "0.19.1", path = "../agb-macros" }
agb_fixnum = { version = "0.19.1", path = "../agb-fixnum" } agb_fixnum = { version = "0.19.1", path = "../agb-fixnum" }
agb_hashmap = { version = "0.19.1", path = "../agb-hashmap" } agb_hashmap = { version = "0.19.1", path = "../agb-hashmap" }
bare-metal = "1"
bilge = "0.2" bilge = "0.2"
qrcodegen-no-heap = "1.8" qrcodegen-no-heap = "1.8"
portable-atomic = { version = "1.6.0", default-features = false, features = ["unsafe-assume-single-core"] }
once_cell = { version = "1.19.0", default-features = false, features = ["critical-section"] }
critical-section = { version = "1.1.2", features = ["restore-state-u16"] }
[package.metadata.docs.rs] [package.metadata.docs.rs]
default-target = "thumbv4t-none-eabi" default-target = "thumbv4t-none-eabi"

View file

@ -1,17 +1,17 @@
#![no_std] #![no_std]
#![no_main] #![no_main]
use agb::sync::Static; use portable_atomic::{AtomicU32, Ordering};
static COUNT: Static<u32> = Static::new(0); static COUNT: AtomicU32 = AtomicU32::new(0);
#[agb::entry] #[agb::entry]
fn main(_gba: agb::Gba) -> ! { fn main(_gba: agb::Gba) -> ! {
let _a = unsafe { let _a = unsafe {
agb::interrupt::add_interrupt_handler(agb::interrupt::Interrupt::VBlank, |_| { agb::interrupt::add_interrupt_handler(agb::interrupt::Interrupt::VBlank, |_| {
let cur_count = COUNT.read(); let cur_count = COUNT.load(Ordering::SeqCst);
agb::println!("Hello, world, frame = {}", cur_count); agb::println!("Hello, world, frame = {}", cur_count);
COUNT.write(cur_count + 1); COUNT.store(cur_count + 1, Ordering::SeqCst);
}) })
}; };
loop {} loop {}

View file

@ -9,9 +9,9 @@ use agb::{
tiled::{RegularBackgroundSize, TileFormat}, tiled::{RegularBackgroundSize, TileFormat},
}, },
fixnum::FixedNum, fixnum::FixedNum,
interrupt::{free, Interrupt}, interrupt::Interrupt,
}; };
use bare_metal::{CriticalSection, Mutex}; use critical_section::{CriticalSection, Mutex};
struct BackCosines { struct BackCosines {
cosines: [u16; 32], cosines: [u16; 32],
@ -37,7 +37,7 @@ fn main(mut gba: agb::Gba) -> ! {
let _a = unsafe { let _a = unsafe {
agb::interrupt::add_interrupt_handler(Interrupt::HBlank, |key: CriticalSection| { agb::interrupt::add_interrupt_handler(Interrupt::HBlank, |key: CriticalSection| {
let mut back = BACK.borrow(key).borrow_mut(); let mut back = BACK.borrow_ref_mut(key);
let deflection = back.cosines[back.row % 32]; let deflection = back.cosines[back.row % 32];
((0x0400_0010) as *mut u16).write_volatile(deflection); ((0x0400_0010) as *mut u16).write_volatile(deflection);
back.row += 1; back.row += 1;
@ -49,8 +49,8 @@ fn main(mut gba: agb::Gba) -> ! {
loop { loop {
vblank.wait_for_vblank(); vblank.wait_for_vblank();
free(|key| { critical_section::with(|key| {
let mut back = BACK.borrow(key).borrow_mut(); let mut back = BACK.borrow_ref_mut(key);
back.row = 0; back.row = 0;
time += 1; time += 1;
for (r, a) in back.cosines.iter_mut().enumerate() { for (r, a) in back.cosines.iter_mut().enumerate() {

View file

@ -191,7 +191,7 @@ pub(crate) fn dma3_exclusive<R>(f: impl FnOnce() -> R) -> R {
const DMA1_CTRL_HI: MemoryMapped<u16> = unsafe { MemoryMapped::new(dma_control_addr(1) + 2) }; const DMA1_CTRL_HI: MemoryMapped<u16> = unsafe { MemoryMapped::new(dma_control_addr(1) + 2) };
const DMA2_CTRL_HI: MemoryMapped<u16> = unsafe { MemoryMapped::new(dma_control_addr(2) + 2) }; const DMA2_CTRL_HI: MemoryMapped<u16> = unsafe { MemoryMapped::new(dma_control_addr(2) + 2) };
crate::interrupt::free(|_| { critical_section::with(|_| {
let dma0_ctl = DMA0_CTRL_HI.get(); let dma0_ctl = DMA0_CTRL_HI.get();
let dma1_ctl = DMA1_CTRL_HI.get(); let dma1_ctl = DMA1_CTRL_HI.get();
let dma2_ctl = DMA2_CTRL_HI.get(); let dma2_ctl = DMA2_CTRL_HI.get();

View file

@ -1,9 +1,10 @@
use core::{cell::Cell, marker::PhantomPinned, pin::Pin}; use core::{cell::Cell, marker::PhantomPinned, pin::Pin};
use alloc::boxed::Box; use alloc::boxed::Box;
use bare_metal::CriticalSection; use critical_section::{CriticalSection, RawRestoreState};
use portable_atomic::{AtomicBool, AtomicUsize, Ordering};
use crate::{display::DISPLAY_STATUS, memory_mapped::MemoryMapped, sync::Static}; use crate::{display::DISPLAY_STATUS, memory_mapped::MemoryMapped};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum Interrupt { pub enum Interrupt {
@ -234,7 +235,7 @@ fn interrupt_to_root(interrupt: Interrupt) -> &'static InterruptRoot {
/// * The closure must be static because forgetting the interrupt handler would /// * The closure must be static because forgetting the interrupt handler would
/// cause a use after free. /// cause a use after free.
/// ///
/// [`CriticalSection`]: bare_metal::CriticalSection /// [`CriticalSection`]: critical_section::CriticalSection
/// ///
/// # Examples /// # Examples
/// ///
@ -242,7 +243,7 @@ fn interrupt_to_root(interrupt: Interrupt) -> &'static InterruptRoot {
/// # #![no_std] /// # #![no_std]
/// # #![no_main] /// # #![no_main]
/// # fn foo() { /// # fn foo() {
/// use bare_metal::CriticalSection; /// use critical_section::CriticalSection;
/// use agb::interrupt::{add_interrupt_handler, Interrupt}; /// use agb::interrupt::{add_interrupt_handler, Interrupt};
/// // Safety: doesn't allocate /// // Safety: doesn't allocate
/// let _a = unsafe { /// let _a = unsafe {
@ -257,7 +258,7 @@ pub unsafe fn add_interrupt_handler(
handler: impl Fn(CriticalSection) + Send + Sync + 'static, handler: impl Fn(CriticalSection) + Send + Sync + 'static,
) -> InterruptHandler { ) -> InterruptHandler {
fn do_with_inner(interrupt: Interrupt, inner: Pin<Box<InterruptInner>>) -> InterruptHandler { fn do_with_inner(interrupt: Interrupt, inner: Pin<Box<InterruptInner>>) -> InterruptHandler {
free(|_| { critical_section::with(|_| {
let root = interrupt_to_root(interrupt); let root = interrupt_to_root(interrupt);
root.add(); root.add();
let mut c = root.next.get(); let mut c = root.next.get();
@ -283,32 +284,23 @@ pub unsafe fn add_interrupt_handler(
do_with_inner(interrupt, inner) do_with_inner(interrupt, inner)
} }
/// How you can access mutexes outside of interrupts by being given a struct MyCriticalSection;
/// [`CriticalSection`] critical_section::set_impl!(MyCriticalSection);
///
/// [`CriticalSection`]: bare_metal::CriticalSection
pub fn free<F, R>(mut f: F) -> R
where
F: FnOnce(CriticalSection) -> R,
{
let enabled = INTERRUPTS_ENABLED.get();
disable_interrupts(); unsafe impl critical_section::Impl for MyCriticalSection {
unsafe fn acquire() -> RawRestoreState {
let irq = INTERRUPTS_ENABLED.get();
INTERRUPTS_ENABLED.set(0);
irq
}
// prevents the contents of the function from being reordered before IME is disabled. unsafe fn release(token: RawRestoreState) {
crate::sync::memory_write_hint(&mut f); INTERRUPTS_ENABLED.set(token);
}
let mut r = f(unsafe { CriticalSection::new() });
// prevents the contents of the function from being reordered after IME is re-enabled.
crate::sync::memory_write_hint(&mut r);
INTERRUPTS_ENABLED.set(enabled);
r
} }
static NUM_VBLANKS: Static<usize> = Static::new(0); // overflows after 2.27 years static NUM_VBLANKS: AtomicUsize = AtomicUsize::new(0); // overflows after 2.27 years
static HAS_CREATED_INTERRUPT: Static<bool> = Static::new(false); static HAS_CREATED_INTERRUPT: AtomicBool = AtomicBool::new(false);
#[non_exhaustive] #[non_exhaustive]
pub struct VBlank { pub struct VBlank {
@ -320,29 +312,28 @@ impl VBlank {
/// interrupt syscall. /// interrupt syscall.
#[must_use] #[must_use]
pub fn get() -> Self { pub fn get() -> Self {
if !HAS_CREATED_INTERRUPT.read() { if !HAS_CREATED_INTERRUPT.swap(true, Ordering::SeqCst) {
// safety: we don't allocate in the interrupt // safety: we don't allocate in the interrupt
let handler = unsafe { let handler = unsafe {
add_interrupt_handler(Interrupt::VBlank, |_| { add_interrupt_handler(Interrupt::VBlank, |_| {
NUM_VBLANKS.write(NUM_VBLANKS.read() + 1); NUM_VBLANKS.store(NUM_VBLANKS.load(Ordering::SeqCst) + 1, Ordering::SeqCst);
}) })
}; };
core::mem::forget(handler); core::mem::forget(handler);
HAS_CREATED_INTERRUPT.write(true);
} }
VBlank { VBlank {
last_waited_number: Cell::new(NUM_VBLANKS.read()), last_waited_number: Cell::new(NUM_VBLANKS.load(Ordering::SeqCst)),
} }
} }
/// Pauses CPU until vblank interrupt is triggered where code execution is /// Pauses CPU until vblank interrupt is triggered where code execution is
/// resumed. /// resumed.
pub fn wait_for_vblank(&self) { pub fn wait_for_vblank(&self) {
let last_waited_number = self.last_waited_number.get(); let last_waited_number = self.last_waited_number.get();
self.last_waited_number.set(NUM_VBLANKS.read() + 1); self.last_waited_number
.set(NUM_VBLANKS.load(Ordering::SeqCst) + 1);
if last_waited_number < NUM_VBLANKS.read() { if last_waited_number < NUM_VBLANKS.load(Ordering::SeqCst) {
return; return;
} }
@ -373,6 +364,8 @@ pub fn profiler(timer: &mut crate::timer::Timer, period: u16) -> InterruptHandle
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use portable_atomic::AtomicU8;
use super::*; use super::*;
#[test_case] #[test_case]
@ -383,4 +376,21 @@ mod tests {
"interrupt table should be able to store gamepak interrupt" "interrupt table should be able to store gamepak interrupt"
); );
} }
#[test_case]
fn interrupts_disabled_in_critical_section(_gba: &mut crate::Gba) {
critical_section::with(|_| {
assert_eq!(INTERRUPTS_ENABLED.get(), 0);
});
}
#[test_case]
fn atomic_check(_gba: &mut crate::Gba) {
static ATOMIC: AtomicU8 = AtomicU8::new(8);
for i in 0..=255 {
ATOMIC.store(i, Ordering::SeqCst);
assert_eq!(ATOMIC.load(Ordering::SeqCst), i);
}
}
} }

View file

@ -175,7 +175,7 @@ mod single;
/// Implements sound output. /// Implements sound output.
pub mod sound; pub mod sound;
/// A module containing functions and utilities useful for synchronizing state. /// A module containing functions and utilities useful for synchronizing state.
pub mod sync; mod sync;
/// System BIOS calls / syscalls. /// System BIOS calls / syscalls.
pub mod syscall; pub mod syscall;
/// Interactions with the internal timers /// Interactions with the internal timers
@ -189,6 +189,12 @@ pub use no_game::no_game;
pub(crate) mod arena; pub(crate) mod arena;
mod global_asm; mod global_asm;
pub mod external {
pub use critical_section;
pub use once_cell;
pub use portable_atomic;
}
pub use {agb_alloc::ExternalAllocator, agb_alloc::InternalAllocator}; pub use {agb_alloc::ExternalAllocator, agb_alloc::InternalAllocator};
#[cfg(not(any(test, feature = "testing")))] #[cfg(not(any(test, feature = "testing")))]

View file

@ -6,13 +6,13 @@ use crate::{
backtrace, backtrace,
display::{bitmap3::Bitmap3, busy_wait_for_vblank, HEIGHT, WIDTH}, display::{bitmap3::Bitmap3, busy_wait_for_vblank, HEIGHT, WIDTH},
dma::dma3_exclusive, dma::dma3_exclusive,
interrupt, mgba, syscall, mgba, syscall,
}; };
mod text; mod text;
pub fn render_backtrace(trace: &backtrace::Frames, info: &PanicInfo) -> ! { pub fn render_backtrace(trace: &backtrace::Frames, info: &PanicInfo) -> ! {
interrupt::free(|_cs| { critical_section::with(|_cs| {
dma3_exclusive(|| { dma3_exclusive(|| {
// SAFETY: This is not fine, but we're crashing anyway. The loop at the end should stop anything bad happening // SAFETY: This is not fine, but we're crashing anyway. The loop at the end should stop anything bad happening
let mut gba = unsafe { crate::Gba::new_in_entry() }; let mut gba = unsafe { crate::Gba::new_in_entry() };

View file

@ -1,8 +1,4 @@
use core::cell::RefCell; use portable_atomic::{AtomicU128, Ordering};
use bare_metal::Mutex;
use crate::interrupt::free;
/// A fast pseudo-random number generator. Note that the output of the /// A fast pseudo-random number generator. Note that the output of the
/// random number generator for a given seed is guaranteed stable /// random number generator for a given seed is guaranteed stable
@ -58,13 +54,22 @@ impl Default for RandomNumberGenerator {
} }
} }
static GLOBAL_RNG: Mutex<RefCell<RandomNumberGenerator>> = static GLOBAL_RNG: AtomicU128 = AtomicU128::new(unsafe {
Mutex::new(RefCell::new(RandomNumberGenerator::new())); core::mem::transmute::<[u32; 4], u128>(RandomNumberGenerator::new().state)
});
/// Using a global random number generator, provides the next random number /// Using a global random number generator, provides the next random number
#[must_use] #[must_use]
pub fn gen() -> i32 { pub fn gen() -> i32 {
free(|cs| GLOBAL_RNG.borrow(cs).borrow_mut().gen()) let data: u128 = GLOBAL_RNG.load(Ordering::SeqCst);
let data_u32: [u32; 4] = unsafe { core::mem::transmute(data) };
let mut rng = RandomNumberGenerator { state: data_u32 };
let value = rng.gen();
GLOBAL_RNG.store(
unsafe { core::mem::transmute::<[u32; 4], u128>(rng.state) },
Ordering::SeqCst,
);
value
} }
#[cfg(test)] #[cfg(test)]

View file

@ -5,11 +5,13 @@
// TODO: Setup cartridge read timings for faster Flash access. // TODO: Setup cartridge read timings for faster Flash access.
use once_cell::sync::OnceCell;
use portable_atomic::{AtomicU8, Ordering};
use crate::memory_mapped::{MemoryMapped, MemoryMapped1DArray}; use crate::memory_mapped::{MemoryMapped, MemoryMapped1DArray};
use crate::save::asm_utils::*; use crate::save::asm_utils::*;
use crate::save::utils::Timeout; use crate::save::utils::Timeout;
use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess};
use crate::sync::{InitOnce, Static};
use core::cmp; use core::cmp;
// Volatile address ports for flash // Volatile address ports for flash
@ -45,14 +47,14 @@ fn issue_flash_command(c2: u8) {
} }
/// A simple thing to avoid excessive bank switches /// A simple thing to avoid excessive bank switches
static CURRENT_BANK: Static<u8> = Static::new(!0);
fn set_bank(bank: u8) -> Result<(), Error> { fn set_bank(bank: u8) -> Result<(), Error> {
static CURRENT_BANK: AtomicU8 = AtomicU8::new(!0);
if bank == 0xFF { if bank == 0xFF {
Err(Error::OutOfBounds) Err(Error::OutOfBounds)
} else if bank != CURRENT_BANK.read() { } else if bank != CURRENT_BANK.load(Ordering::SeqCst) {
issue_flash_command(CMD_SET_BANK); issue_flash_command(CMD_SET_BANK);
FLASH_PORT_BANK.set(bank); FLASH_PORT_BANK.set(bank);
CURRENT_BANK.write(bank); CURRENT_BANK.store(bank, Ordering::SeqCst);
Ok(()) Ok(())
} else { } else {
Ok(()) Ok(())
@ -239,10 +241,16 @@ impl FlashChipType {
} }
} }
} }
static CHIP_INFO: InitOnce<&'static ChipInfo> = InitOnce::new();
fn cached_chip_info() -> Result<&'static ChipInfo, Error> { fn cached_chip_info() -> Result<&'static ChipInfo, Error> {
static CHIP_INFO: OnceCell<&'static ChipInfo> = OnceCell::new();
for _ in 0..100 {
unsafe { core::arch::asm!("nop") };
}
CHIP_INFO CHIP_INFO
.try_get(|| -> Result<_, Error> { Ok(FlashChipType::detect()?.chip_info()) }) .get_or_try_init(|| -> Result<_, Error> { Ok(FlashChipType::detect()?.chip_info()) })
.cloned() .cloned()
} }
@ -380,7 +388,7 @@ impl ChipInfo {
buf: &[u8], buf: &[u8],
timeout: &mut Timeout, timeout: &mut Timeout,
) -> Result<(), Error> { ) -> Result<(), Error> {
crate::interrupt::free(|_| { critical_section::with(|_| {
issue_flash_command(CMD_WRITE); issue_flash_command(CMD_WRITE);
for i in 0..128 { for i in 0..128 {
FLASH_DATA.set(offset + i, buf[i]); FLASH_DATA.set(offset + i, buf[i]);

View file

@ -86,7 +86,7 @@
//! size. //! size.
use crate::save::utils::Timeout; use crate::save::utils::Timeout;
use crate::sync::{Mutex, RawMutexGuard}; use crate::sync::{Lock, RawLockGuard};
use crate::timer::Timer; use crate::timer::Timer;
use core::ops::Range; use core::ops::Range;
@ -179,7 +179,7 @@ trait RawSaveAccess: Sync {
fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error>; fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error>;
} }
static CURRENT_SAVE_ACCESS: Mutex<Option<&'static dyn RawSaveAccess>> = Mutex::new(None); static CURRENT_SAVE_ACCESS: Lock<Option<&'static dyn RawSaveAccess>> = Lock::new(None);
fn set_save_implementation(access_impl: &'static dyn RawSaveAccess) { fn set_save_implementation(access_impl: &'static dyn RawSaveAccess) {
let mut access = CURRENT_SAVE_ACCESS.lock(); let mut access = CURRENT_SAVE_ACCESS.lock();
@ -196,7 +196,7 @@ fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> {
/// Allows reading and writing of save media. /// Allows reading and writing of save media.
pub struct SaveData { pub struct SaveData {
_lock: RawMutexGuard<'static>, _lock: RawLockGuard<'static>,
access: &'static dyn RawSaveAccess, access: &'static dyn RawSaveAccess,
info: &'static MediaInfo, info: &'static MediaInfo,
timeout: utils::Timeout, timeout: utils::Timeout,
@ -356,19 +356,19 @@ mod marker {
#[inline(always)] #[inline(always)]
pub fn emit_eeprom_marker() { pub fn emit_eeprom_marker() {
crate::sync::memory_read_hint(&EEPROM); core::hint::black_box(&EEPROM);
} }
#[inline(always)] #[inline(always)]
pub fn emit_sram_marker() { pub fn emit_sram_marker() {
crate::sync::memory_read_hint(&SRAM); core::hint::black_box(&SRAM);
} }
#[inline(always)] #[inline(always)]
pub fn emit_flash_512k_marker() { pub fn emit_flash_512k_marker() {
crate::sync::memory_read_hint(&FLASH512K); core::hint::black_box(&FLASH512K);
} }
#[inline(always)] #[inline(always)]
pub fn emit_flash_1m_marker() { pub fn emit_flash_1m_marker() {
crate::sync::memory_read_hint(&FLASH1M); core::hint::black_box(&FLASH1M);
} }
} }

View file

@ -1,8 +1,10 @@
//! A package containing useful utilities for writing save accessors. //! A package containing useful utilities for writing save accessors.
use super::Error; use super::Error;
use crate::sync::{RawMutex, RawMutexGuard}; use crate::{
use crate::timer::{Divider, Timer}; sync::{RawLock, RawLockGuard},
timer::{Divider, Timer},
};
/// A timeout type used to prevent hardware errors in save media from hanging /// A timeout type used to prevent hardware errors in save media from hanging
/// the game. /// the game.
@ -50,8 +52,8 @@ impl Drop for Timeout {
} }
} }
pub fn lock_media_access() -> Result<RawMutexGuard<'static>, Error> { pub fn lock_media_access() -> Result<RawLockGuard<'static>, Error> {
static LOCK: RawMutex = RawMutex::new(); static LOCK: RawLock = RawLock::new();
match LOCK.try_lock() { match LOCK.try_lock() {
Some(x) => Ok(x), Some(x) => Ok(x),
None => Err(Error::MediaInUse), None => Err(Error::MediaInUse),

View file

@ -4,7 +4,7 @@ use core::pin::Pin;
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::vec::Vec; use alloc::vec::Vec;
use bare_metal::{CriticalSection, Mutex}; use critical_section::{CriticalSection, Mutex};
use super::hw::LeftOrRight; use super::hw::LeftOrRight;
use super::{hw, Frequency}; use super::{hw, Frequency};
@ -13,7 +13,6 @@ use super::{SoundChannel, SoundPriority};
use crate::InternalAllocator; use crate::InternalAllocator;
use crate::{ use crate::{
fixnum::Num, fixnum::Num,
interrupt::free,
interrupt::{add_interrupt_handler, InterruptHandler}, interrupt::{add_interrupt_handler, InterruptHandler},
timer::Divider, timer::Divider,
timer::Timer, timer::Timer,
@ -395,11 +394,11 @@ impl MixerBuffer {
} }
fn should_calculate(&self) -> bool { fn should_calculate(&self) -> bool {
free(|cs| self.state.borrow(cs).borrow().should_calculate()) critical_section::with(|cs| self.state.borrow_ref_mut(cs).should_calculate())
} }
fn swap(&self, cs: CriticalSection) { fn swap(&self, cs: CriticalSection) {
let buffer = self.state.borrow(cs).borrow_mut().playing_advanced(); let buffer = self.state.borrow_ref_mut(cs).playing_advanced();
let left_buffer = buffer; let left_buffer = buffer;
// SAFETY: starting pointer is fine, resulting pointer also fine because buffer has length buffer_size() * 2 by construction // SAFETY: starting pointer is fine, resulting pointer also fine because buffer has length buffer_size() * 2 by construction
@ -435,7 +434,8 @@ impl MixerBuffer {
} }
} }
let write_buffer = free(|cs| self.state.borrow(cs).borrow_mut().active_advanced()); let write_buffer =
critical_section::with(|cs| self.state.borrow_ref_mut(cs).active_advanced());
unsafe { unsafe {
agb_rs__mixer_collapse( agb_rs__mixer_collapse(

169
agb/src/sync.rs Normal file
View file

@ -0,0 +1,169 @@
use core::cell::UnsafeCell;
use core::ops::{Deref, DerefMut};
use portable_atomic::{AtomicBool, Ordering};
#[inline(never)]
fn already_locked() -> ! {
panic!("IRQ and main thread are attempting to access the same Lock!")
}
/// A lock that prevents code from running in both an IRQ and normal code at
/// the same time.
///
/// Note that this does not support blocking like a typical mutex, and instead
/// mainly exists for memory safety reasons.
pub struct RawLock(AtomicBool);
impl RawLock {
/// Creates a new lock.
#[must_use]
pub const fn new() -> Self {
RawLock(AtomicBool::new(false))
}
/// Locks the lock and returns whether a lock was successfully acquired.
fn raw_lock(&self) -> bool {
if self.0.swap(true, Ordering::Acquire) {
// value was already true, oops.
false
} else {
// prevent any weird reordering, and continue
true
}
}
/// Unlocks the lock.
fn raw_unlock(&self) {
if !self.0.swap(false, Ordering::Release) {
panic!("Internal error: Attempt to unlock a `RawLock` which is not locked.")
}
}
/// Returns a guard for this lock, or `None` if there is another lock active.
pub fn try_lock(&self) -> Option<RawLockGuard<'_>> {
if self.raw_lock() {
Some(RawLockGuard(self))
} else {
None
}
}
}
unsafe impl Send for RawLock {}
unsafe impl Sync for RawLock {}
/// A guard representing an active lock on an [`RawLock`].
pub struct RawLockGuard<'a>(&'a RawLock);
impl<'a> Drop for RawLockGuard<'a> {
fn drop(&mut self) {
self.0.raw_unlock();
}
}
/// A lock that protects an object from being accessed in both an IRQ and
/// normal code at once.
///
/// Note that this does not support blocking like a typical mutex, and instead
/// mainly exists for memory safety reasons.
pub struct Lock<T> {
raw: RawLock,
data: UnsafeCell<T>,
}
impl<T> Lock<T> {
/// Creates a new lock containing a given value.
#[must_use]
pub const fn new(t: T) -> Self {
Lock {
raw: RawLock::new(),
data: UnsafeCell::new(t),
}
}
/// Returns a guard for this lock, or panics if there is another lock active.
pub fn lock(&self) -> LockGuard<'_, T> {
self.try_lock().unwrap_or_else(|| already_locked())
}
/// Returns a guard for this lock or `None` if there is another lock active.
pub fn try_lock(&self) -> Option<LockGuard<'_, T>> {
if self.raw.raw_lock() {
Some(LockGuard {
underlying: self,
ptr: self.data.get(),
})
} else {
None
}
}
}
unsafe impl<T> Send for Lock<T> {}
unsafe impl<T> Sync for Lock<T> {}
/// A guard representing an active lock on an [`Lock`].
pub struct LockGuard<'a, T> {
underlying: &'a Lock<T>,
ptr: *mut T,
}
impl<'a, T> Drop for LockGuard<'a, T> {
fn drop(&mut self) {
self.underlying.raw.raw_unlock();
}
}
impl<'a, T> Deref for LockGuard<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { &*self.ptr }
}
}
impl<'a, T> DerefMut for LockGuard<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { &mut *self.ptr }
}
}
/// An internal function used as a temporary hack to get `compiler_fence`
/// working. While this call is not properly inlined, working is better than not
/// working at all.
///
/// This seems to be a problem caused by Rust issue #62256:
/// <https://github.com/rust-lang/rust/issues/62256>
///
/// # Safety
///
/// **WARNING FOR ANYONE WHO FINDS THIS**: This implementation will *only* be
/// correct on the GBA, and should not be used on any other platform. The GBA
/// is very old, and has no atomics to begin with - only a main thread and
/// interrupts. On any more recent CPU, this implementation is extremely
/// unlikely to be sound.
///
/// Not public API, obviously.
#[doc(hidden)]
#[deprecated]
#[allow(dead_code)]
#[no_mangle]
#[inline(always)]
pub unsafe extern "C" fn __sync_synchronize() {}
#[cfg(test)]
mod tests {
use once_cell::sync::OnceCell;
#[derive(Default)]
#[allow(dead_code)]
struct Storage([u32; 16 / 4]);
#[test_case]
fn check_init_once(_: &mut crate::Gba) {
static CELL: OnceCell<Storage> = OnceCell::new();
core::hint::black_box(CELL.get_or_init(Default::default));
}
#[test_case]
fn check_init_once_many(_: &mut crate::Gba) {
static CELL: OnceCell<Storage> = OnceCell::new();
for _ in 0..1000 {
core::hint::black_box(CELL.get_or_init(Default::default));
}
}
}

View file

@ -1,223 +0,0 @@
use crate::sync::Static;
use core::cell::UnsafeCell;
use core::mem::MaybeUninit;
use core::ops::{Deref, DerefMut};
use core::ptr;
use core::sync::atomic::{compiler_fence, Ordering};
#[inline(never)]
fn already_locked() -> ! {
panic!("IRQ and main thread are attempting to access the same Mutex!")
}
/// A mutex that prevents code from running in both an IRQ and normal code at
/// the same time.
///
/// Note that this does not support blocking like a typical mutex, and instead
/// mainly exists for memory safety reasons.
pub struct RawMutex(Static<bool>);
impl RawMutex {
/// Creates a new lock.
#[must_use]
pub const fn new() -> Self {
RawMutex(Static::new(false))
}
/// Locks the mutex and returns whether a lock was successfully acquired.
fn raw_lock(&self) -> bool {
if self.0.replace(true) {
// value was already true, oops.
false
} else {
// prevent any weird reordering, and continue
compiler_fence(Ordering::Acquire);
true
}
}
/// Unlocks the mutex.
fn raw_unlock(&self) {
compiler_fence(Ordering::Release);
if !self.0.replace(false) {
panic!("Internal error: Attempt to unlock a `RawMutex` which is not locked.")
}
}
/// Returns a guard for this lock, or panics if there is another lock active.
pub fn lock(&self) -> RawMutexGuard<'_> {
self.try_lock().unwrap_or_else(|| already_locked())
}
/// Returns a guard for this lock, or `None` if there is another lock active.
pub fn try_lock(&self) -> Option<RawMutexGuard<'_>> {
if self.raw_lock() {
Some(RawMutexGuard(self))
} else {
None
}
}
}
unsafe impl Send for RawMutex {}
unsafe impl Sync for RawMutex {}
impl Default for RawMutex {
fn default() -> Self {
Self::new()
}
}
/// A guard representing an active lock on an [`RawMutex`].
pub struct RawMutexGuard<'a>(&'a RawMutex);
impl<'a> Drop for RawMutexGuard<'a> {
fn drop(&mut self) {
self.0.raw_unlock();
}
}
/// A mutex that protects an object from being accessed in both an IRQ and
/// normal code at once.
///
/// Note that this does not support blocking like a typical mutex, and instead
/// mainly exists for memory safety reasons.
pub struct Mutex<T> {
raw: RawMutex,
data: UnsafeCell<T>,
}
impl<T> Mutex<T> {
/// Creates a new lock containing a given value.
#[must_use]
pub const fn new(t: T) -> Self {
Mutex {
raw: RawMutex::new(),
data: UnsafeCell::new(t),
}
}
/// Returns a guard for this lock, or panics if there is another lock active.
pub fn lock(&self) -> MutexGuard<'_, T> {
self.try_lock().unwrap_or_else(|| already_locked())
}
/// Returns a guard for this lock or `None` if there is another lock active.
pub fn try_lock(&self) -> Option<MutexGuard<'_, T>> {
if self.raw.raw_lock() {
Some(MutexGuard {
underlying: self,
ptr: self.data.get(),
})
} else {
None
}
}
}
unsafe impl<T> Send for Mutex<T> {}
unsafe impl<T> Sync for Mutex<T> {}
/// A guard representing an active lock on an [`Mutex`].
pub struct MutexGuard<'a, T> {
underlying: &'a Mutex<T>,
ptr: *mut T,
}
impl<'a, T> Drop for MutexGuard<'a, T> {
fn drop(&mut self) {
self.underlying.raw.raw_unlock();
}
}
impl<'a, T> Deref for MutexGuard<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { &*self.ptr }
}
}
impl<'a, T> DerefMut for MutexGuard<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { &mut *self.ptr }
}
}
enum Void {}
/// A helper type that ensures a particular value is only initialized once.
pub struct InitOnce<T> {
is_initialized: Static<bool>,
value: UnsafeCell<MaybeUninit<T>>,
}
impl<T> InitOnce<T> {
/// Creates a new uninitialized object.
#[must_use]
pub const fn new() -> Self {
InitOnce {
is_initialized: Static::new(false),
value: UnsafeCell::new(MaybeUninit::uninit()),
}
}
/// Gets the contents of this state, or initializes it if it has not already
/// been initialized.
///
/// The initializer function is guaranteed to only be called once.
///
/// This function disables IRQs while it is initializing the inner value.
/// While this can cause audio skipping and other similar issues, it is
/// not normally a problem as interrupts will only be disabled once per
/// `InitOnce` during the life cycle of the program.
pub fn get(&self, initializer: impl FnOnce() -> T) -> &T {
match self.try_get(|| -> Result<T, Void> { Ok(initializer()) }) {
Ok(v) => v,
_ => unimplemented!(),
}
}
/// Gets the contents of this state, or initializes it if it has not already
/// been initialized.
///
/// The initializer function is guaranteed to only be called once if it
/// returns `Ok`. If it returns `Err`, it will be called again in the
/// future until an attempt at initialization succeeds.
///
/// This function disables IRQs while it is initializing the inner value.
/// While this can cause audio skipping and other similar issues, it is
/// not normally a problem as interrupts will only be disabled once per
/// `InitOnce` during the life cycle of the program.
pub fn try_get<E>(&self, initializer: impl FnOnce() -> Result<T, E>) -> Result<&T, E> {
unsafe {
if !self.is_initialized.read() {
// We disable interrupts to make this simpler, since this is likely to
// only occur once in a program anyway.
crate::interrupt::free(|_| -> Result<(), E> {
// We check again to make sure this function wasn't called in an
// interrupt between the first check and when interrupts were
// actually disabled.
if !self.is_initialized.read() {
// Do the actual initialization.
ptr::write_volatile((*self.value.get()).as_mut_ptr(), initializer()?);
self.is_initialized.write(true);
}
Ok(())
})?;
}
compiler_fence(Ordering::Acquire);
Ok(&*(*self.value.get()).as_mut_ptr())
}
}
}
impl<T> Default for InitOnce<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Drop for InitOnce<T> {
fn drop(&mut self) {
if self.is_initialized.read() {
// drop the value inside the `MaybeUninit`
unsafe {
ptr::read((*self.value.get()).as_ptr());
}
}
}
}
unsafe impl<T: Send> Send for InitOnce<T> {}
unsafe impl<T: Sync> Sync for InitOnce<T> {}

View file

@ -1,50 +0,0 @@
mod locks;
mod statics;
pub use locks::*;
pub use statics::*;
use core::arch::asm;
/// Marks that a pointer is read without actually reading from this.
///
/// This uses an [`asm!`] instruction that marks the parameter as being read,
/// requiring the compiler to treat this function as if anything could be
/// done to it.
#[inline(always)]
pub fn memory_read_hint<T>(val: *const T) {
unsafe { asm!("/* {0} */", in(reg) val, options(readonly, nostack)) }
}
/// Marks that a pointer is read or written to without actually writing to it.
///
/// This uses an [`asm!`] instruction that marks the parameter as being read
/// and written, requiring the compiler to treat this function as if anything
/// could be done to it.
#[inline(always)]
pub fn memory_write_hint<T>(val: *mut T) {
unsafe { asm!("/* {0} */", in(reg) val, options(nostack)) }
}
/// An internal function used as a temporary hack to get `compiler_fence`
/// working. While this call is not properly inlined, working is better than not
/// working at all.
///
/// This seems to be a problem caused by Rust issue #62256:
/// <https://github.com/rust-lang/rust/issues/62256>
///
/// # Safety
///
/// **WARNING FOR ANYONE WHO FINDS THIS**: This implementation will *only* be
/// correct on the GBA, and should not be used on any other platform. The GBA
/// is very old, and has no atomics to begin with - only a main thread and
/// interrupts. On any more recent CPU, this implementation is extremely
/// unlikely to be sound.
///
/// Not public API, obviously.
#[doc(hidden)]
#[deprecated]
#[allow(dead_code)]
#[no_mangle]
#[inline(always)]
pub unsafe extern "C" fn __sync_synchronize() {}

View file

@ -1,337 +0,0 @@
use core::arch::asm;
use core::cell::UnsafeCell;
use core::mem;
use core::ptr;
/// The internal function for replacing a `Copy` (really `!Drop`) value in a
/// [`Static`]. This uses assembly to use an `stmia` instruction to ensure
/// an IRQ cannot occur during the write operation.
unsafe fn transfer<T: Copy>(dst: *mut T, src: *const T) {
let align = mem::align_of::<T>();
let size = mem::size_of::<T>();
if size == 0 {
// Do nothing with ZSTs.
} else if size <= 16 && align % 4 == 0 {
// We can do an 4-byte aligned transfer up to 16 bytes.
transfer_align4_thumb(dst, src);
} else if size <= 40 && align % 4 == 0 {
// We can do the same up to 40 bytes, but we need to switch to ARM.
transfer_align4_arm(dst, src);
} else if size <= 2 && align % 2 == 0 {
// We can do a 2-byte aligned transfer up to 2 bytes.
asm!(
"ldrh {2},[{0}]",
"strh {2},[{1}]",
in(reg) src, in(reg) dst, out(reg) _,
);
} else if size == 1 {
// We can do a simple byte copy.
asm!(
"ldrb {2},[{0}]",
"strb {2},[{1}]",
in(reg) src, in(reg) dst, out(reg) _,
);
} else {
// When we don't have an optimized path, we just disable IRQs.
crate::interrupt::free(|_| ptr::write_volatile(dst, ptr::read_volatile(src)));
}
}
#[allow(unused_assignments)]
unsafe fn transfer_align4_thumb<T: Copy>(mut dst: *mut T, mut src: *const T) {
let size = mem::size_of::<T>();
if size <= 4 {
// We use assembly here regardless to just do the word aligned copy. This
// ensures it's done with a single ldr/str instruction.
asm!(
"ldr {2},[{0}]",
"str {2},[{1}]",
inout(reg) src, in(reg) dst, out(reg) _,
);
} else if size <= 8 {
// Starting at size == 8, we begin using ldmia/stmia to load/save multiple
// words in one instruction, avoiding IRQs from interrupting our operation.
asm!(
"ldmia {0}!, {{r2-r3}}",
"stmia {1}!, {{r2-r3}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _,
);
} else if size <= 12 {
asm!(
"ldmia {0}!, {{r2-r4}}",
"stmia {1}!, {{r2-r4}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _,
);
} else if size <= 16 {
asm!(
"ldmia {0}!, {{r2-r5}}",
"stmia {1}!, {{r2-r5}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _,
);
} else {
unimplemented!("This should be done via transfer_arm.");
}
}
#[instruction_set(arm::a32)]
#[allow(unused_assignments)]
unsafe fn transfer_align4_arm<T: Copy>(mut dst: *mut T, mut src: *const T) {
let size = mem::size_of::<T>();
if size <= 16 {
unimplemented!("This should be done via transfer_thumb.");
} else if size <= 20 {
// Starting at size == 16, we have to switch to ARM due to lack of
// accessible registers in THUMB mode.
asm!(
"ldmia {0}!, {{r2-r5,r7}}",
"stmia {1}!, {{r2-r5,r7}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _, out("r7") _,
);
} else if size <= 24 {
asm!(
"ldmia {0}!, {{r2-r5,r7-r8}}",
"stmia {1}!, {{r2-r5,r7-r8}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _, out("r7") _,
out("r8") _,
);
} else if size <= 28 {
asm!(
"ldmia {0}!, {{r2-r5,r7-r9}}",
"stmia {1}!, {{r2-r5,r7-r9}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _, out("r7") _,
out("r8") _, out("r9") _,
);
} else if size <= 32 {
asm!(
"ldmia {0}!, {{r2-r5,r7-r10}}",
"stmia {1}!, {{r2-r5,r7-r10}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _, out("r7") _,
out("r8") _, out("r9") _, out("r10") _,
);
} else if size <= 36 {
asm!(
"ldmia {0}!, {{r2-r5,r7-r10,r12}}",
"stmia {1}!, {{r2-r5,r7-r10,r12}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _, out("r7") _,
out("r8") _, out("r9") _, out("r10") _, out("r12") _,
);
} else if size <= 40 {
asm!(
"ldmia {0}!, {{r2-r5,r7-r10,r12,r14}}",
"stmia {1}!, {{r2-r5,r7-r10,r12,r14}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _, out("r7") _,
out("r8") _, out("r9") _, out("r10") _, out("r12") _, out("r14") _,
);
} else {
// r13 is sp, and r15 is pc. Neither are usable
unimplemented!("Copy too large for use of ldmia/stmia.");
}
}
/// The internal function for swapping the current value of a [`Static`] with
/// another value.
unsafe fn exchange<T>(dst: *mut T, src: *const T) -> T {
let align = mem::align_of::<T>();
let size = mem::size_of::<T>();
if size == 0 {
// Do nothing with ZSTs.
ptr::read(dst)
} else if size <= 4 && align % 4 == 0 {
// Swap a single word with the SWP instruction.
let val = ptr::read(src as *const u32);
let new_val = exchange_align4_arm(dst, val);
ptr::read(&new_val as *const _ as *const T)
} else if size == 1 {
// Swap a byte with the SWPB instruction.
let val = ptr::read(src as *const u8);
let new_val = exchange_align1_arm(dst, val);
ptr::read(&new_val as *const _ as *const T)
} else {
// fallback
crate::interrupt::free(|_| {
let cur = ptr::read_volatile(dst);
ptr::write_volatile(dst, ptr::read_volatile(src));
cur
})
}
}
#[instruction_set(arm::a32)]
unsafe fn exchange_align4_arm<T>(dst: *mut T, i: u32) -> u32 {
let out;
asm!("swp {2}, {1}, [{0}]", in(reg) dst, in(reg) i, lateout(reg) out);
out
}
#[instruction_set(arm::a32)]
unsafe fn exchange_align1_arm<T>(dst: *mut T, i: u8) -> u8 {
let out;
asm!("swpb {2}, {1}, [{0}]", in(reg) dst, in(reg) i, lateout(reg) out);
out
}
/// A helper that implements static variables.
///
/// It ensures that even if you use the same static variable in both an IRQ
/// and normal code, the IRQ will never observe an invalid value of the
/// variable.
///
/// This type only works with owned values. If you need to work with borrows,
/// consider using [`sync::Mutex`](`crate::sync::Mutex`) instead.
///
/// ## Performance
///
/// Writing or reading from a static variable is efficient under the following
/// conditions:
///
/// * The type is aligned to 4 bytes and can be stored in 40 bytes or less.
/// * The type is aligned to 2 bytes and can be stored in 2 bytes.
/// * The type is can be stored in a single byte.
///
/// Replacing the current value of the static variable is efficient under the
/// following conditions:
///
/// * The type is aligned to 4 bytes and can be stored in 4 bytes or less.
/// * The type is can be stored in a single byte.
///
/// When these conditions are not met, static variables are handled using a
/// fallback routine that disables IRQs and does a normal copy. This can be
/// dangerous as disabling IRQs can cause your program to miss out on important
/// interrupts such as V-Blank.
///
/// Consider using [`sync::Mutex`](`crate::sync::Mutex`) instead if you need to
/// use a large amount of operations that would cause IRQs to be disabled. Also
/// consider using `#[repr(align(4))]` to force proper alignment for your type.
pub struct Static<T> {
data: UnsafeCell<T>,
}
impl<T> Static<T> {
/// Creates a new static variable.
pub const fn new(val: T) -> Self {
Static {
data: UnsafeCell::new(val),
}
}
/// Replaces the current value of the static variable with another, and
/// returns the old value.
#[allow(clippy::needless_pass_by_value)] // critical for safety
pub fn replace(&self, val: T) -> T {
unsafe { exchange(self.data.get(), &val) }
}
/// Extracts the interior value of the static variable.
pub fn into_inner(self) -> T {
self.data.into_inner()
}
}
impl<T: Copy> Static<T> {
/// Writes a new value into this static variable.
pub fn write(&self, val: T) {
unsafe { transfer(self.data.get(), &val) }
}
/// Reads a value from this static variable.
pub fn read(&self) -> T {
unsafe {
let mut out: mem::MaybeUninit<T> = mem::MaybeUninit::uninit();
transfer(out.as_mut_ptr(), self.data.get());
out.assume_init()
}
}
}
impl<T: Default> Default for Static<T> {
fn default() -> Self {
Static::new(T::default())
}
}
unsafe impl<T> Send for Static<T> {}
unsafe impl<T> Sync for Static<T> {}
#[cfg(test)]
mod test {
use crate::interrupt::Interrupt;
use crate::sync::Static;
use crate::timer::Divider;
use crate::Gba;
macro_rules! generate_concurrency_test {
($count:literal, $gba:ident) => {{
(|gba: &mut Gba| {
const SENTINEL: [u32; $count] = [0x12345678; $count];
static VALUE: Static<[u32; $count]> = Static::new(SENTINEL);
// set up a timer and an interrupt that uses the timer
let mut timer = gba.timers.timers().timer2;
timer.set_cascade(false);
timer.set_divider(Divider::Divider1);
timer.set_overflow_amount(1049);
timer.set_interrupt(true);
timer.set_enabled(true);
let _int = unsafe {
crate::interrupt::add_interrupt_handler(Interrupt::Timer2, |_| {
VALUE.write(SENTINEL);
})
};
// the actual main test loop
let mut interrupt_seen = false;
let mut no_interrupt_seen = false;
for i in 0..250000 {
// write to the static
let new_value = [i; $count];
VALUE.write(new_value);
// check the current value
let current = VALUE.read();
if current == new_value {
no_interrupt_seen = true;
} else if current == SENTINEL {
interrupt_seen = true;
} else {
panic!("Unexpected value found in `Static`.");
}
// we return as soon as we've seen both the value written by the main thread
// and interrupt
if interrupt_seen && no_interrupt_seen {
timer.set_enabled(false);
return;
}
if i % 8192 == 0 && i != 0 {
timer.set_overflow_amount(1049 + (i / 64) as u16);
}
}
panic!("Concurrency test timed out: {}", $count)
})($gba);
}};
}
#[test_case]
fn write_read_concurrency_test(gba: &mut Gba) {
generate_concurrency_test!(1, gba);
generate_concurrency_test!(2, gba);
generate_concurrency_test!(3, gba);
generate_concurrency_test!(4, gba);
generate_concurrency_test!(5, gba);
generate_concurrency_test!(6, gba);
generate_concurrency_test!(7, gba);
generate_concurrency_test!(8, gba);
generate_concurrency_test!(9, gba);
generate_concurrency_test!(10, gba);
}
}

View file

@ -1,10 +1,10 @@
use agb::save::{Error, MediaInfo}; use agb::save::{Error, MediaInfo};
use agb::sync::InitOnce;
use core::cmp; use core::cmp;
use once_cell::sync::OnceCell;
fn init_sram(gba: &mut agb::Gba) -> &'static MediaInfo { fn init_sram(gba: &mut agb::Gba) -> &'static MediaInfo {
static ONCE: InitOnce<MediaInfo> = InitOnce::new(); static ONCE: OnceCell<MediaInfo> = OnceCell::new();
ONCE.get(|| { ONCE.get_or_init(|| {
crate::save_setup(gba); crate::save_setup(gba);
gba.save.access().unwrap().media_info().clone() gba.save.access().unwrap().media_info().clone()
}) })

View file

@ -1,8 +1,8 @@
use agb::external::portable_atomic::{AtomicU32, Ordering};
use agb::save::{Error, SaveManager}; use agb::save::{Error, SaveManager};
use agb::sync::Static;
use agb::Gba; use agb::Gba;
static HIGH_SCORE: Static<u32> = Static::new(0); static HIGH_SCORE: AtomicU32 = AtomicU32::new(0);
pub fn init_save(gba: &mut Gba) -> Result<(), Error> { pub fn init_save(gba: &mut Gba) -> Result<(), Error> {
gba.save.init_sram(); gba.save.init_sram();
@ -21,24 +21,22 @@ pub fn init_save(gba: &mut Gba) -> Result<(), Error> {
access.read(1, &mut buffer)?; access.read(1, &mut buffer)?;
let high_score = u32::from_le_bytes(buffer); let high_score = u32::from_le_bytes(buffer);
if high_score > 100 { let score = if high_score > 100 { 0 } else { high_score };
HIGH_SCORE.write(0)
} else { HIGH_SCORE.store(score, Ordering::SeqCst);
HIGH_SCORE.write(high_score)
}
} }
Ok(()) Ok(())
} }
pub fn load_high_score() -> u32 { pub fn load_high_score() -> u32 {
HIGH_SCORE.read() HIGH_SCORE.load(Ordering::SeqCst)
} }
pub fn save_high_score(save: &mut SaveManager, score: u32) -> Result<(), Error> { pub fn save_high_score(save: &mut SaveManager, score: u32) -> Result<(), Error> {
save.access()? save.access()?
.prepare_write(1..5)? .prepare_write(1..5)?
.write(1, &score.to_le_bytes())?; .write(1, &score.to_le_bytes())?;
HIGH_SCORE.write(score); HIGH_SCORE.store(score, Ordering::SeqCst);
Ok(()) Ok(())
} }

View file

@ -1,10 +1,10 @@
use agb::external::portable_atomic::{AtomicU32, Ordering};
use agb::{ use agb::{
save::{Error, SaveManager}, save::{Error, SaveManager},
sync::Static,
Gba, Gba,
}; };
static MAXIMUM_LEVEL: Static<u32> = Static::new(0); static MAXIMUM_LEVEL: AtomicU32 = AtomicU32::new(0);
pub fn init_save(gba: &mut Gba) -> Result<(), Error> { pub fn init_save(gba: &mut Gba) -> Result<(), Error> {
gba.save.init_sram(); gba.save.init_sram();
@ -24,9 +24,9 @@ pub fn init_save(gba: &mut Gba) -> Result<(), Error> {
let max_level = u32::from_le_bytes(buffer); let max_level = u32::from_le_bytes(buffer);
if max_level > 100 { if max_level > 100 {
MAXIMUM_LEVEL.write(0) MAXIMUM_LEVEL.store(0, Ordering::SeqCst)
} else { } else {
MAXIMUM_LEVEL.write(max_level) MAXIMUM_LEVEL.store(max_level, Ordering::SeqCst)
} }
} }
@ -34,13 +34,13 @@ pub fn init_save(gba: &mut Gba) -> Result<(), Error> {
} }
pub fn load_max_level() -> u32 { pub fn load_max_level() -> u32 {
MAXIMUM_LEVEL.read() MAXIMUM_LEVEL.load(Ordering::SeqCst)
} }
pub fn save_max_level(save: &mut SaveManager, level: u32) -> Result<(), Error> { pub fn save_max_level(save: &mut SaveManager, level: u32) -> Result<(), Error> {
save.access()? save.access()?
.prepare_write(1..5)? .prepare_write(1..5)?
.write(1, &level.to_le_bytes())?; .write(1, &level.to_le_bytes())?;
MAXIMUM_LEVEL.write(level); MAXIMUM_LEVEL.store(level, Ordering::SeqCst);
Ok(()) Ok(())
} }