use core::time::Duration;
use crate::sidecar::sync::{AtomicU32, Ordering};
pub const SPIN_WINDOW: Duration = Duration::from_micros(50);
pub const SLEEP_QUANTUM: Duration = Duration::from_micros(20);
#[repr(u32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Turn {
Sidecar = 0,
Core = 1,
}
impl Turn {
const fn other(self) -> Self {
match self {
Self::Sidecar => Self::Core,
Self::Core => Self::Sidecar,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum WaitError {
#[error("the peer exited before handing over the turn")]
PeerGone,
#[error("the turn word holds {0}, which is neither side")]
CorruptTurn(u32),
#[error("the wait was cancelled")]
Cancelled,
}
#[repr(C, align(64))]
#[derive(Debug)]
pub struct TurnWord(AtomicU32);
impl TurnWord {
pub(crate) fn new(holder: Turn) -> Self {
Self(AtomicU32::new(holder as u32))
}
pub fn hand_over(&self, to: Turn) {
self.0.store(to as u32, Ordering::Release);
}
#[cfg(not(loom))]
#[expect(
clippy::disallowed_types,
clippy::disallowed_methods,
reason = "pacing the wait for the peer; virtual time is untouched"
)]
pub fn wait_for(
&self,
me: Turn,
mut check: impl FnMut() -> Result<(), WaitError>,
) -> Result<(), WaitError> {
let mut spin_start = None;
loop {
if self.is_held_by(me)? {
return Ok(());
}
let spin_start = spin_start.get_or_insert_with(std::time::Instant::now);
if spin_start.elapsed() < SPIN_WINDOW {
core::hint::spin_loop();
continue;
}
check()?;
std::thread::sleep(SLEEP_QUANTUM);
}
}
#[cfg(loom)]
pub fn wait_for(
&self,
me: Turn,
mut check: impl FnMut() -> Result<(), WaitError>,
) -> Result<(), WaitError> {
while !self.is_held_by(me)? {
check()?;
loom::thread::yield_now();
}
Ok(())
}
fn is_held_by(&self, me: Turn) -> Result<bool, WaitError> {
let holder = self.0.load(Ordering::Acquire);
if holder == me as u32 {
Ok(true)
} else if holder == me.other() as u32 {
Ok(false)
} else {
Err(WaitError::CorruptTurn(holder))
}
}
}
#[cfg(not(loom))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wait_returns_at_once_when_the_turn_is_already_mine() {
let word = TurnWord::new(Turn::Core);
assert_eq!(
word.wait_for(Turn::Core, || Err(WaitError::PeerGone)),
Ok(())
);
}
#[test]
fn wait_ends_with_the_error_the_check_returns() {
let word = TurnWord::new(Turn::Sidecar);
for ending in [WaitError::PeerGone, WaitError::Cancelled] {
assert_eq!(word.wait_for(Turn::Core, || Err(ending)), Err(ending));
}
}
#[test]
fn wait_rejects_a_turn_word_that_names_neither_side() {
let word = TurnWord::new(Turn::Sidecar);
word.0.store(2, Ordering::Relaxed);
assert_eq!(
word.wait_for(Turn::Core, || Ok(())),
Err(WaitError::CorruptTurn(2))
);
}
#[test]
#[expect(
clippy::disallowed_methods,
reason = "the peer is paced past the spin window"
)]
fn wait_survives_a_hand_over_after_the_spin_window() {
const PEER_DELAY: Duration = Duration::from_millis(5);
let word = TurnWord::new(Turn::Sidecar);
std::thread::scope(|scope| {
scope.spawn(|| {
std::thread::sleep(PEER_DELAY);
word.hand_over(Turn::Core);
});
assert_eq!(word.wait_for(Turn::Core, || Ok(())), Ok(()));
});
}
}
#[cfg(loom)]
#[cfg(test)]
mod loom_tests {
use crate::soc::Width;
use loom::sync::Arc;
use super::*;
use crate::sidecar::layout::{ControlBlock, InterruptLine};
use crate::sidecar::message::{CoreMessage, SidecarMessage};
#[test]
fn turn_hand_over_publishes_the_slot_and_the_levels() {
const READ: SidecarMessage = SidecarMessage::Read {
address: 0x4002_0014,
width: Width::W32,
instruction_count: 12,
program_counter: 0x0800_0100,
virtual_time_ns: 48,
};
const REPLY: CoreMessage = CoreMessage::Reply {
value: 0x55,
deadline_ns: 1_000_000,
};
let line = InterruptLine::new(70).unwrap();
loom::model(move || {
let block = Arc::new(ControlBlock::new());
let sidecar_block = Arc::clone(&block);
let sidecar = loom::thread::spawn(move || {
READ.encode(&sidecar_block.sidecar_slot);
sidecar_block.turn.hand_over(Turn::Core);
sidecar_block
.turn
.wait_for(Turn::Sidecar, || Ok(()))
.unwrap();
assert_eq!(CoreMessage::decode(&sidecar_block.core_slot), Ok(REPLY));
assert!(sidecar_block.interrupt_levels.is_raised(line));
});
block.turn.wait_for(Turn::Core, || Ok(())).unwrap();
assert_eq!(SidecarMessage::decode(&block.sidecar_slot), Ok(READ));
REPLY.encode(&block.core_slot);
block.interrupt_levels.raise(line);
block.turn.hand_over(Turn::Sidecar);
sidecar.join().unwrap();
});
}
}