Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_rt/
critical_section.rs

1//! Critical sections: run code with CPU interrupts masked.
2//!
3//! On the PS1 a critical section clears COP0 SR.IEc for its duration and
4//! puts the previous value back afterwards, so sections nest. psx-rt's own
5//! interrupt handler is assembly that touches no Rust state, so the SDK does
6//! not need one; games that install a Rust IRQ handler do, to share data
7//! with it.
8//!
9//! The shape follows the `critical-section` crate: [`acquire`] and
10//! [`release`] are the raw pair, [`with`] the scoped form, and [`Mutex`]
11//! hands out a shared reference only while a [`CriticalSection`] token is
12//! alive. With psx-rt's `critical-section` feature the same pair is also
13//! registered as that crate's implementation, so `critical_section::with`
14//! in any dependency masks interrupts here too.
15//!
16//! Host builds (tests, tools) serialise through a spin lock instead, so the
17//! same code stays sound under a multi-threaded test runner.
18
19use core::cell::UnsafeCell;
20use core::marker::PhantomData;
21
22/// What [`release`] needs to put back: whether interrupts were enabled.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24#[must_use = "pass it to release()"]
25pub struct RestoreState(bool);
26
27impl RestoreState {
28    /// Whether the section this closes found interrupts enabled.
29    #[inline(always)]
30    pub fn was_enabled(self) -> bool {
31        self.0
32    }
33
34    /// Rebuild a state saved as a `bool` (the `critical-section` crate's
35    /// `restore-state-bool` representation).
36    ///
37    /// Passing it to [`release`] carries the same contract as the original.
38    #[inline(always)]
39    pub fn from_enabled(enabled: bool) -> Self {
40        Self(enabled)
41    }
42}
43
44/// Proof that the current code runs inside a critical section.
45///
46/// Only [`with`] (or `unsafe` [`CriticalSection::new`]) makes one; the
47/// lifetime keeps it from escaping the section.
48#[derive(Clone, Copy, Debug)]
49pub struct CriticalSection<'cs> {
50    _scope: PhantomData<&'cs ()>,
51}
52
53impl CriticalSection<'_> {
54    /// Assert that a critical section is active.
55    ///
56    /// # Safety
57    ///
58    /// Interrupts must stay masked (or, on the host, the section lock held)
59    /// for as long as the returned token, or anything borrowed through it,
60    /// is alive.
61    #[inline(always)]
62    pub unsafe fn new() -> Self {
63        Self {
64            _scope: PhantomData,
65        }
66    }
67}
68
69/// Enter a critical section, returning the state to restore on exit.
70///
71/// # Safety
72///
73/// Each call must be paired with exactly one [`release`] of its result, in
74/// reverse order of acquisition (sections nest like brackets).
75#[inline(always)]
76pub unsafe fn acquire() -> RestoreState {
77    imp::acquire()
78}
79
80/// Leave a critical section entered by [`acquire`].
81///
82/// # Safety
83///
84/// `state` must come from the matching [`acquire`], and every section
85/// entered after it must already have been released.
86#[inline(always)]
87pub unsafe fn release(state: RestoreState) {
88    // SAFETY: forwarded contract.
89    unsafe { imp::release(state) }
90}
91
92/// Run `f` with interrupts masked, then restore the previous state.
93///
94/// Keep `f` short: VBlank and every other interrupt wait until it returns.
95#[inline(always)]
96pub fn with<R>(f: impl FnOnce(CriticalSection<'_>) -> R) -> R {
97    // SAFETY: released below, after `f` and the token it got are done. A
98    // panic in `f` halts the console (and aborts the host test), so the
99    // section is never left half-open with code still running.
100    let state = unsafe { acquire() };
101    // SAFETY: the section is active until `release` below.
102    let result = f(unsafe { CriticalSection::new() });
103    // SAFETY: pairs the acquire above; nested sections inside `f` have
104    // already released.
105    unsafe { release(state) };
106    result
107}
108
109/// Data shared with interrupt code, reachable only inside a critical
110/// section.
111///
112/// For mutation, wrap the value in a [`core::cell::Cell`] or
113/// [`core::cell::RefCell`].
114pub struct Mutex<T> {
115    inner: UnsafeCell<T>,
116}
117
118// SAFETY: the only access is `borrow`, which needs a `CriticalSection`, so
119// no two contexts reach `inner` at once. `T: Send` because the value may be
120// touched from interrupt context as well as the main program.
121unsafe impl<T: Send> Sync for Mutex<T> {}
122
123impl<T> Mutex<T> {
124    /// Wrap `value`.
125    pub const fn new(value: T) -> Self {
126        Self {
127            inner: UnsafeCell::new(value),
128        }
129    }
130
131    /// Borrow the value for as long as the critical section lasts.
132    #[inline(always)]
133    pub fn borrow<'cs>(&'cs self, _cs: CriticalSection<'cs>) -> &'cs T {
134        // SAFETY: interrupts are masked while `_cs` lives, and every other
135        // access also goes through a critical section.
136        unsafe { &*self.inner.get() }
137    }
138
139    /// Mutable access through exclusive ownership, no section needed.
140    #[inline(always)]
141    pub fn get_mut(&mut self) -> &mut T {
142        self.inner.get_mut()
143    }
144
145    /// Unwrap the value.
146    pub fn into_inner(self) -> T {
147        self.inner.into_inner()
148    }
149}
150
151#[cfg(target_arch = "mips")]
152mod imp {
153    use super::RestoreState;
154
155    // The COP0 SR read-modify-write lives in psx-io, which needs it for its
156    // own DPCR update; this is the same pair behind the critical-section API.
157    #[inline(always)]
158    pub(super) fn acquire() -> RestoreState {
159        // SAFETY: `release` pairs it, per `super::acquire`'s contract.
160        RestoreState(unsafe { psx_io::irq::disable_cpu_interrupts() })
161    }
162
163    #[inline(always)]
164    pub(super) unsafe fn release(state: RestoreState) {
165        // SAFETY: `state` came from the matching `acquire`.
166        unsafe { psx_io::irq::restore_cpu_interrupts(state.0) }
167    }
168}
169
170#[cfg(not(target_arch = "mips"))]
171mod imp {
172    extern crate std;
173
174    use super::RestoreState;
175    use core::cell::Cell;
176    use core::sync::atomic::{AtomicBool, Ordering};
177
178    // One global lock; a per-thread depth lets nested sections on the
179    // owning thread pass while other threads wait.
180    static LOCKED: AtomicBool = AtomicBool::new(false);
181    std::thread_local! {
182        static DEPTH: Cell<u32> = const { Cell::new(0) };
183    }
184
185    pub(super) fn acquire() -> RestoreState {
186        if DEPTH.get() > 0 {
187            DEPTH.set(DEPTH.get() + 1);
188            return RestoreState(false);
189        }
190        while LOCKED
191            .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
192            .is_err()
193        {
194            core::hint::spin_loop();
195        }
196        DEPTH.set(1);
197        RestoreState(true)
198    }
199
200    pub(super) unsafe fn release(state: RestoreState) {
201        DEPTH.set(DEPTH.get() - 1);
202        if state.0 {
203            LOCKED.store(false, Ordering::Release);
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use core::cell::Cell;
212
213    #[test]
214    fn sections_nest_and_mutex_borrows_inside_one() {
215        static COUNT: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));
216        with(|cs| {
217            COUNT.borrow(cs).set(COUNT.borrow(cs).get() + 1);
218            with(|inner| COUNT.borrow(inner).set(COUNT.borrow(inner).get() + 1));
219        });
220        assert_eq!(with(|cs| COUNT.borrow(cs).get()), 2);
221    }
222}