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}