Implement a sync API for working with global mutable state. (#107)

* Add IntelliJ workspace files to the .gitignore.

* Add a simple make_example script for Linux.

* Create a `sync` module with many GBA-specific sync utilties.

* Fix overflow error in debug mode in the hello_world crate.

* Fixes to DMA.

* Code cleanup for the sync module.

* Run rustfmt on new sync code.

* Fix up some names and documentation in the sync module.

* Add a few changes suggested by thomcc for the locks.

* Added needed compiler fences to `InitOnce::try_get`.
* Change the error in `RawMutex::raw_unlock` to better reflect the cause.

* Add a proper issue link to the __sync_synchronize hack.

* Disable interrupts during `InitOnce::try_get`.

* Fix some bad wording in the comments for `InitOnce::try_get`

* Use the new target in `cfg` checks to see if we're on GBA.

* Change registers used for transfer_align4_arm for the different target.

* Cleanup on sync_api changes for the target change.
This commit is contained in:
Alissa Rao 2021-02-21 15:57:26 -08:00 committed by GitHub
parent 5114f89448
commit 2aa59bb341
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 631 additions and 74 deletions

6
.gitignore vendored
View file

@ -14,4 +14,8 @@ Cargo.lock
crt0.o crt0.o
# Don't track VSCode Workspace settings # Don't track VSCode Workspace settings
/.vscode /.vscode
# Don't track IntelliJ Workspaces
/.idea
/*.ida

View file

@ -52,8 +52,8 @@ fn main(_argc: isize, _argv: *const *const u8) -> isize {
let this_frame_keys = read_key_input(); let this_frame_keys = read_key_input();
// adjust game state and wait for vblank // adjust game state and wait for vblank
px = px.wrapping_add(2 * this_frame_keys.x_tribool() as usize); px = px.wrapping_add((2 * this_frame_keys.x_tribool() as i32) as usize);
py = py.wrapping_add(2 * this_frame_keys.y_tribool() as usize); py = py.wrapping_add((2 * this_frame_keys.y_tribool() as i32) as usize);
if this_frame_keys.l() { if this_frame_keys.l() {
color = Color(color.0.rotate_left(5)); color = Color(color.0.rotate_left(5));
} }

11
make_example.sh Executable file
View file

@ -0,0 +1,11 @@
#!/bin/sh
if [ "$1" = "" ]; then
echo "Usage: $0 [example to build]"
exit 1
fi
cargo build --example $1 --release || exit 1
arm-none-eabi-objcopy -O binary target/thumbv4-none-agb/release/examples/$1 target/$1.gba || exit 1
gbafix target/$1.gba || exit 1
echo "ROM built successfully!"

View file

@ -8,7 +8,7 @@
//! whatever value is necessary for that function). Some functions also perform //! whatever value is necessary for that function). Some functions also perform
//! necessary checks to save you from yourself, such as not dividing by zero. //! necessary checks to save you from yourself, such as not dividing by zero.
#![cfg_attr(not(all(target_vendor = "nintendo", target_env = "agb")), allow(unused_variables))] #![cfg_attr(not(target_arch = "arm"), allow(unused_variables))]
use core::mem; use core::mem;
use super::*; use super::*;
@ -55,11 +55,11 @@ use io::irq::IrqFlags;
/// perform UB, but such a scenario might exist. /// perform UB, but such a scenario might exist.
#[inline(always)] #[inline(always)]
pub unsafe fn soft_reset() -> ! { pub unsafe fn soft_reset() -> ! {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
asm!("swi 0x00", options(noreturn)) asm!("swi 0x00", options(noreturn))
} }
@ -92,11 +92,11 @@ pub unsafe fn soft_reset() -> ! {
/// that. If you do then you return to nothing and have a bad time. /// that. If you do then you return to nothing and have a bad time.
#[inline(always)] #[inline(always)]
pub unsafe fn register_ram_reset(flags: RegisterRAMResetFlags) { pub unsafe fn register_ram_reset(flags: RegisterRAMResetFlags) {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
asm!("swi 0x01", in("r0") flags.0); asm!("swi 0x01", in("r0") flags.0);
} }
@ -126,11 +126,11 @@ impl RegisterRAMResetFlags {
/// any enabled interrupt triggers. /// any enabled interrupt triggers.
#[inline(always)] #[inline(always)]
pub fn halt() { pub fn halt() {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
unsafe { unsafe {
asm!("swi 0x02"); asm!("swi 0x02");
@ -148,11 +148,11 @@ pub fn halt() {
/// optional externals such as rumble and infra-red. /// optional externals such as rumble and infra-red.
#[inline(always)] #[inline(always)]
pub fn stop() { pub fn stop() {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
unsafe { unsafe {
asm!("swi 0x03"); asm!("swi 0x03");
@ -175,11 +175,11 @@ pub fn stop() {
/// the usual interrupt acknowledgement. /// the usual interrupt acknowledgement.
#[inline(always)] #[inline(always)]
pub fn interrupt_wait(ignore_current_flags: bool, target_flags: IrqFlags) { pub fn interrupt_wait(ignore_current_flags: bool, target_flags: IrqFlags) {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
unsafe { unsafe {
asm!( asm!(
@ -198,11 +198,11 @@ pub fn interrupt_wait(ignore_current_flags: bool, target_flags: IrqFlags) {
/// [`interrupt_wait`](interrupt_wait) outlines. /// [`interrupt_wait`](interrupt_wait) outlines.
#[inline(always)] #[inline(always)]
pub fn vblank_interrupt_wait() { pub fn vblank_interrupt_wait() {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
unsafe { unsafe {
asm!( asm!(
@ -222,11 +222,11 @@ pub fn vblank_interrupt_wait() {
#[inline(always)] #[inline(always)]
pub fn div_rem(numerator: i32, denominator: i32) -> (i32, i32) { pub fn div_rem(numerator: i32, denominator: i32) -> (i32, i32) {
assert!(denominator != 0); assert!(denominator != 0);
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
(numerator / denominator, numerator % denominator) (numerator / denominator, numerator % denominator)
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
let div_out: i32; let div_out: i32;
let rem_out: i32; let rem_out: i32;
@ -265,11 +265,11 @@ pub fn rem(numerator: i32, denominator: i32) -> i32 {
/// by `2n` bits to get `n` more bits of fractional precision in your output. /// by `2n` bits to get `n` more bits of fractional precision in your output.
#[inline(always)] #[inline(always)]
pub fn sqrt(val: u32) -> u16 { pub fn sqrt(val: u32) -> u16 {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
0 // TODO: simulate this properly when not on GBA 0 // TODO: simulate this properly when not on GBA
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
let out: u32; let out: u32;
unsafe { unsafe {
@ -293,11 +293,11 @@ pub fn sqrt(val: u32) -> u16 {
/// Accuracy suffers if `theta` is less than `-pi/4` or greater than `pi/4`. /// Accuracy suffers if `theta` is less than `-pi/4` or greater than `pi/4`.
#[inline(always)] #[inline(always)]
pub fn atan(theta: i16) -> i16 { pub fn atan(theta: i16) -> i16 {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
0 // TODO: simulate this properly when not on GBA 0 // TODO: simulate this properly when not on GBA
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
let out: i16; let out: i16;
unsafe { unsafe {
@ -322,11 +322,11 @@ pub fn atan(theta: i16) -> i16 {
/// integral, 14 bits for fractional. /// integral, 14 bits for fractional.
#[inline(always)] #[inline(always)]
pub fn atan2(y: i16, x: i16) -> u16 { pub fn atan2(y: i16, x: i16) -> u16 {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
0 // TODO: simulate this properly when not on GBA 0 // TODO: simulate this properly when not on GBA
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
let out: u16; let out: u16;
unsafe { unsafe {
@ -353,11 +353,11 @@ pub fn atan2(y: i16, x: i16) -> u16 {
/// * Both pointers must be aligned /// * Both pointers must be aligned
#[inline(always)] #[inline(always)]
pub unsafe fn cpu_set16(src: *const u16, dest: *mut u16, count: u32, fixed_source: bool) { pub unsafe fn cpu_set16(src: *const u16, dest: *mut u16, count: u32, fixed_source: bool) {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
let control = count + ((fixed_source as u32) << 24); let control = count + ((fixed_source as u32) << 24);
asm!( asm!(
@ -380,11 +380,11 @@ pub unsafe fn cpu_set16(src: *const u16, dest: *mut u16, count: u32, fixed_sourc
/// * Both pointers must be aligned /// * Both pointers must be aligned
#[inline(always)] #[inline(always)]
pub unsafe fn cpu_set32(src: *const u32, dest: *mut u32, count: u32, fixed_source: bool) { pub unsafe fn cpu_set32(src: *const u32, dest: *mut u32, count: u32, fixed_source: bool) {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
let control = count + ((fixed_source as u32) << 24) + (1 << 26); let control = count + ((fixed_source as u32) << 24) + (1 << 26);
asm!( asm!(
@ -408,11 +408,11 @@ pub unsafe fn cpu_set32(src: *const u32, dest: *mut u32, count: u32, fixed_sourc
/// * Both pointers must be aligned /// * Both pointers must be aligned
#[inline(always)] #[inline(always)]
pub unsafe fn cpu_fast_set(src: *const u32, dest: *mut u32, count: u32, fixed_source: bool) { pub unsafe fn cpu_fast_set(src: *const u32, dest: *mut u32, count: u32, fixed_source: bool) {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
let control = count + ((fixed_source as u32) << 24); let control = count + ((fixed_source as u32) << 24);
asm!( asm!(
@ -434,11 +434,11 @@ pub unsafe fn cpu_fast_set(src: *const u32, dest: *mut u32, count: u32, fixed_so
/// some other value I guess you're probably running on an emulator that just /// some other value I guess you're probably running on an emulator that just
/// broke the fourth wall. /// broke the fourth wall.
pub fn get_bios_checksum() -> u32 { pub fn get_bios_checksum() -> u32 {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
let out: u32; let out: u32;
unsafe { unsafe {
@ -473,11 +473,11 @@ pub fn get_bios_checksum() -> u32 {
/// ///
/// The final sound level setting will be `level` * `0x200`. /// The final sound level setting will be `level` * `0x200`.
pub fn sound_bias(level: u32) { pub fn sound_bias(level: u32) {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
unsafe { unsafe {
asm!("swi 0x19", in("r0") level); asm!("swi 0x19", in("r0") level);
@ -513,11 +513,11 @@ pub fn sound_bias(level: u32) {
/// * 10: 40137 /// * 10: 40137
/// * 11: 42048 /// * 11: 42048
pub fn sound_driver_mode(mode: u32) { pub fn sound_driver_mode(mode: u32) {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
unsafe { unsafe {
asm!("swi 0x1B", in("r0") mode); asm!("swi 0x1B", in("r0") mode);
@ -535,11 +535,11 @@ pub fn sound_driver_mode(mode: u32) {
/// executed." --what? /// executed." --what?
#[inline(always)] #[inline(always)]
pub fn sound_driver_main() { pub fn sound_driver_main() {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
unsafe { unsafe {
asm!("swi 0x1C"); asm!("swi 0x1C");
@ -553,11 +553,11 @@ pub fn sound_driver_main() {
/// vblank interrupt (every 1/60th of a second). /// vblank interrupt (every 1/60th of a second).
#[inline(always)] #[inline(always)]
pub fn sound_driver_vsync() { pub fn sound_driver_vsync() {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
unsafe { unsafe {
asm!("swi 0x1D"); asm!("swi 0x1D");
@ -573,11 +573,11 @@ pub fn sound_driver_vsync() {
/// --what? /// --what?
#[inline(always)] #[inline(always)]
pub fn sound_channel_clear() { pub fn sound_channel_clear() {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
unsafe { unsafe {
asm!("swi 0x1E"); asm!("swi 0x1E");
@ -596,11 +596,11 @@ pub fn sound_channel_clear() {
/// noise. /// noise.
#[inline(always)] #[inline(always)]
pub fn sound_driver_vsync_off() { pub fn sound_driver_vsync_off() {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
unsafe { unsafe {
asm!("swi 0x28"); asm!("swi 0x28");
@ -615,11 +615,11 @@ pub fn sound_driver_vsync_off() {
/// interrupt followed by a `sound_driver_vsync` within 2/60th of a second. /// interrupt followed by a `sound_driver_vsync` within 2/60th of a second.
#[inline(always)] #[inline(always)]
pub fn sound_driver_vsync_on() { pub fn sound_driver_vsync_on() {
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] #[cfg(not(target_arch = "arm"))]
{ {
unimplemented!() unimplemented!("This function is not supported on this target.")
} }
#[cfg(all(target_vendor = "nintendo", target_env = "agb"))] #[cfg(target_arch = "arm")]
{ {
unsafe { unsafe {
asm!("swi 0x29"); asm!("swi 0x29");

View file

@ -143,7 +143,8 @@ impl DMA0 {
/// ///
/// The source pointer must be aligned and valid to read from. /// The source pointer must be aligned and valid to read from.
pub unsafe fn set_source(src: *const u32) { pub unsafe fn set_source(src: *const u32) {
Self::DMA0SAD.write(src) crate::sync::memory_read_hint(src);
Self::DMA0SAD.write(src);
} }
/// Assigns the destination register. /// Assigns the destination register.
@ -154,7 +155,8 @@ impl DMA0 {
/// ///
/// The source pointer must be aligned and valid to write to. /// The source pointer must be aligned and valid to write to.
pub unsafe fn set_dest(dest: *mut u32) { pub unsafe fn set_dest(dest: *mut u32) {
Self::DMA0DAD.write(dest) Self::DMA0DAD.write(dest);
crate::sync::memory_write_hint(dest);
} }
/// Assigns the count register. /// Assigns the count register.
@ -204,7 +206,8 @@ impl DMA1 {
/// ///
/// The source pointer must be aligned and valid to read from. /// The source pointer must be aligned and valid to read from.
pub unsafe fn set_source(src: *const u32) { pub unsafe fn set_source(src: *const u32) {
Self::DMA1SAD.write(src) crate::sync::memory_read_hint(src);
Self::DMA1SAD.write(src);
} }
/// Assigns the destination register. /// Assigns the destination register.
@ -215,7 +218,8 @@ impl DMA1 {
/// ///
/// The source pointer must be aligned and valid to write to. /// The source pointer must be aligned and valid to write to.
pub unsafe fn set_dest(dest: *mut u32) { pub unsafe fn set_dest(dest: *mut u32) {
Self::DMA1DAD.write(dest) Self::DMA1DAD.write(dest);
crate::sync::memory_write_hint(dest);
} }
/// Assigns the count register. /// Assigns the count register.
@ -265,7 +269,8 @@ impl DMA2 {
/// ///
/// The source pointer must be aligned and valid to read from. /// The source pointer must be aligned and valid to read from.
pub unsafe fn set_source(src: *const u32) { pub unsafe fn set_source(src: *const u32) {
Self::DMA2SAD.write(src) crate::sync::memory_read_hint(src);
Self::DMA2SAD.write(src);
} }
/// Assigns the destination register. /// Assigns the destination register.
@ -276,7 +281,8 @@ impl DMA2 {
/// ///
/// The source pointer must be aligned and valid to write to. /// The source pointer must be aligned and valid to write to.
pub unsafe fn set_dest(dest: *mut u32) { pub unsafe fn set_dest(dest: *mut u32) {
Self::DMA2DAD.write(dest) Self::DMA2DAD.write(dest);
crate::sync::memory_write_hint(dest);
} }
/// Assigns the count register. /// Assigns the count register.
@ -327,7 +333,8 @@ impl DMA3 {
/// ///
/// The source pointer must be aligned and valid to read from. /// The source pointer must be aligned and valid to read from.
pub unsafe fn set_source(src: *const u32) { pub unsafe fn set_source(src: *const u32) {
Self::DMA3SAD.write(src) crate::sync::memory_read_hint(src);
Self::DMA3SAD.write(src);
} }
/// Assigns the destination register. /// Assigns the destination register.
@ -338,7 +345,8 @@ impl DMA3 {
/// ///
/// The source pointer must be aligned and valid to write to. /// The source pointer must be aligned and valid to write to.
pub unsafe fn set_dest(dest: *mut u32) { pub unsafe fn set_dest(dest: *mut u32) {
Self::DMA3DAD.write(dest) Self::DMA3DAD.write(dest);
crate::sync::memory_write_hint(dest);
} }
/// Assigns the count register. /// Assigns the count register.
@ -380,18 +388,24 @@ impl DMA3 {
.with_use_32bit(true) .with_use_32bit(true)
.with_enabled(true); .with_enabled(true);
// TODO: destination checking against SRAM // TODO: destination checking against SRAM
crate::sync::memory_read_hint(src);
Self::DMA3SAD.write(src); Self::DMA3SAD.write(src);
Self::DMA3DAD.write(dest); Self::DMA3DAD.write(dest);
Self::DMA3CNT_L.write(count); Self::DMA3CNT_L.write(count);
Self::DMA3CNT_H.write(FILL_CONTROL); Self::DMA3CNT_H.write(FILL_CONTROL);
crate::sync::memory_write_hint(dest);
// Note(Lokathor): Once DMA is set to activate it takes 2 cycles for it to // Note(Lokathor): Once DMA is set to activate it takes 2 cycles for it to
// kick in. You can do any non-DMA thing you like before that, but since // kick in. You can do any non-DMA thing you like before that, but since
// it's only two cycles we just insert two NOP instructions to ensure that // it's only two cycles we just insert two NOP instructions to ensure that
// successive calls to `fill32` or other DMA methods don't interfere with // successive calls to `fill32` or other DMA methods don't interfere with
// each other. // each other.
asm!(" asm!(
"
NOP NOP
NOP NOP
", options(nomem, nostack)); ",
options(nomem, nostack)
);
} }
} }

View file

@ -1,5 +1,5 @@
#![cfg_attr(not(test), no_std)] #![cfg_attr(not(test), no_std)]
#![feature(asm)] #![feature(asm, isa_attribute)]
#![allow(clippy::cast_lossless)] #![allow(clippy::cast_lossless)]
#![deny(clippy::float_arithmetic)] #![deny(clippy::float_arithmetic)]
#![warn(missing_docs)] #![warn(missing_docs)]
@ -44,6 +44,8 @@ pub mod sram;
pub mod mgba; pub mod mgba;
pub mod sync;
extern "C" { extern "C" {
/// This marks the end of the `.data` and `.bss` sections in IWRAM. /// This marks the end of the `.data` and `.bss` sections in IWRAM.
/// ///

63
src/sync.rs Normal file
View file

@ -0,0 +1,63 @@
//! A module containing functions and utilities useful for synchronizing state.
use crate::io::irq::{IrqEnableSetting, IME};
mod locks;
mod statics;
pub use locks::*;
pub use statics::*;
/// 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>
///
/// Not public API, obviously.
#[doc(hidden)]
#[deprecated]
#[allow(dead_code)]
#[no_mangle]
#[inline(always)]
pub unsafe extern "C" fn __sync_synchronize() {}
/// Runs a function with IRQs disabled.
///
/// This should not be done without good reason, as IRQs are usually important
/// for game functionality.
pub fn with_irqs_disabled<T>(mut func: impl FnOnce() -> T) -> T {
let current_ime = IME.read();
IME.write(IrqEnableSetting::IRQ_NO);
// prevents the contents of the function from being reordered before IME is disabled.
memory_write_hint(&mut func);
let mut result = func();
// prevents the contents of the function from being reordered after IME is reenabled.
memory_write_hint(&mut result);
IME.write(current_ime);
result
}

202
src/sync/locks.rs Normal file
View file

@ -0,0 +1,202 @@
use super::*;
use core::{
cell::UnsafeCell,
mem::MaybeUninit,
ops::{Deref, DerefMut},
ptr,
sync::atomic::{compiler_fence, Ordering},
};
#[inline(never)]
fn already_locked() -> ! {
panic!("This lock has already been locked by another thread.")
}
/// 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.
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, opps.
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 {}
/// 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.
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.
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.
with_irqs_disabled(|| -> 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> 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> {}

261
src/sync/statics.rs Normal file
View file

@ -0,0 +1,261 @@
#![cfg_attr(not(target_arch = "arm"), allow(unused_variables))]
use super::*;
use core::{cell::UnsafeCell, mem, mem::MaybeUninit, 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.
#[cfg(target_arch = "arm")]
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. Obviously.
} 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 <= 36 && align % 4 == 0 {
// We can do the same up to 36 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.
with_irqs_disabled(|| ptr::write_volatile(dst, ptr::read_volatile(src)));
}
}
#[cfg(target_arch = "arm")]
#[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 == 5, 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.");
}
}
#[cfg(target_arch = "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 == 20, we have to switch to ARM due to lack of
// accessible registers in THUMB mode.
asm!(
"ldmia {0}!, {{r2-r5,r8}}",
"stmia {1}!, {{r2-r5,r8}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _, out("r8") _,
)
} else if size <= 24 {
asm!(
"ldmia {0}!, {{r2-r5,r8-r9}}",
"stmia {1}!, {{r2-r5,r8-r9}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _, out("r8") _, out("r9") _,
)
} else if size <= 28 {
asm!(
"ldmia {0}!, {{r2-r5,r8-r10}}",
"stmia {1}!, {{r2-r5,r8-r10}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _, out("r8") _, out("r9") _,
out("r10") _,
)
} else if size <= 32 {
asm!(
"ldmia {0}!, {{r2-r5,r8-r10,r12}}",
"stmia {1}!, {{r2-r5,r8-r10,r12}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _, out("r8") _, out("r9") _,
out("r10") _, out("r12") _,
)
} else if size <= 36 {
asm!(
"ldmia {0}!, {{r2-r5,r8-r10,r12,r14}}",
"stmia {1}!, {{r2-r5,r8-r10,r12,r14}}",
inout(reg) src, inout(reg) dst,
out("r2") _, out("r3") _, out("r4") _, out("r5") _, out("r8") _, out("r9") _,
out("r10") _, out("r12") _, out("r14") _,
)
} else {
unimplemented!("Copy too large for use of ldmia/stmia.");
}
}
/// The internal function for swapping the current value of a [`Static`] with
/// another value.
#[cfg(target_arch = "arm")]
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
with_irqs_disabled(|| {
let cur = ptr::read_volatile(dst);
ptr::write_volatile(dst, ptr::read_volatile(src));
cur
})
}
}
#[cfg(target_arch = "arm")]
#[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
}
#[cfg(target_arch = "arm")]
#[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
}
#[cfg(not(target_arch = "arm"))]
unsafe fn exchange<T>(dst: *mut T, src: *const T) -> T {
unimplemented!("This function is not supported on this target.")
}
#[cfg(not(target_arch = "arm"))]
unsafe fn transfer<T: Copy>(dst: *mut T, src: *const T) {
unimplemented!("This function is not supported on this target.")
}
/// 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 [`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 36 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 [`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.
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: MaybeUninit<T> = 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> {}