use crate::sidecar::sync::{AtomicU8, AtomicU32, AtomicU64, Ordering};
use crate::sidecar::turn::{Turn, TurnWord};
pub const MAGIC: u64 = u64::from_le_bytes(*b"RIGLSSCB");
pub const PROTOCOL_VERSION: Version = Version { major: 1, minor: 0 };
pub const INTERRUPT_LINES: u16 = 1024;
pub(crate) const VERSION_MAJOR_SHIFT: u32 = 16;
#[cfg(not(loom))]
pub(crate) const CACHE_LINE: usize = 64;
#[cfg(not(loom))]
pub(crate) const PAGE: usize = 4096;
const INTERRUPT_WORDS: usize = INTERRUPT_LINES as usize / u64::BITS as usize;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Version {
pub major: u16,
pub minor: u16,
}
impl Version {
#[must_use]
pub fn pack(self) -> u32 {
(u32::from(self.major) << VERSION_MAJOR_SHIFT) | u32::from(self.minor)
}
#[expect(
clippy::cast_possible_truncation,
reason = "each half is sixteen bits wide once shifted and masked"
)]
pub(crate) fn unpack(packed: u32) -> Self {
Self {
major: (packed >> VERSION_MAJOR_SHIFT) as u16,
minor: (packed & u32::from(u16::MAX)) as u16,
}
}
}
impl core::fmt::Display for Version {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(formatter, "{}.{}", self.major, self.minor)
}
}
#[repr(C, align(64))]
#[derive(Debug, Default)]
pub struct Slot {
pub(crate) kind: AtomicU32,
pub(crate) width: AtomicU8,
pub(crate) reason: AtomicU8,
pub(crate) address: AtomicU64,
pub(crate) value: AtomicU64,
pub(crate) instruction_count: AtomicU64,
pub(crate) program_counter: AtomicU64,
pub(crate) virtual_time_ns: AtomicU64,
pub(crate) deadline_ns: AtomicU64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct InterruptLine(u16);
impl InterruptLine {
#[must_use]
pub const fn new(index: u16) -> Option<Self> {
if index < INTERRUPT_LINES {
Some(Self(index))
} else {
None
}
}
fn word(self) -> usize {
usize::from(self.0) / u64::BITS as usize
}
fn mask(self) -> u64 {
1 << (u32::from(self.0) % u64::BITS)
}
}
#[repr(C, align(64))]
#[derive(Debug, Default)]
pub struct InterruptLevels([AtomicU64; INTERRUPT_WORDS]);
impl InterruptLevels {
pub fn raise(&self, line: InterruptLine) {
self.0[line.word()].fetch_or(line.mask(), Ordering::Relaxed);
}
pub fn lower(&self, line: InterruptLine) {
self.0[line.word()].fetch_and(!line.mask(), Ordering::Relaxed);
}
pub fn is_raised(&self, line: InterruptLine) -> bool {
self.0[line.word()].load(Ordering::Relaxed) & line.mask() != 0
}
}
#[repr(C, align(4096))]
#[derive(Debug)]
pub struct ControlBlock {
pub magic: AtomicU64,
pub version: AtomicU32,
pub turn: TurnWord,
pub sidecar_slot: Slot,
pub core_slot: Slot,
pub interrupt_levels: InterruptLevels,
}
impl ControlBlock {
#[must_use]
pub fn new() -> Self {
Self {
magic: AtomicU64::new(MAGIC),
version: AtomicU32::new(PROTOCOL_VERSION.pack()),
turn: TurnWord::new(Turn::Sidecar),
sidecar_slot: Slot::default(),
core_slot: Slot::default(),
interrupt_levels: InterruptLevels::default(),
}
}
#[cfg(not(loom))]
pub(crate) fn initialise(page: &Self) {
page.magic.store(MAGIC, Ordering::Relaxed);
page.version
.store(PROTOCOL_VERSION.pack(), Ordering::Relaxed);
page.turn.hand_over(Turn::Sidecar);
}
}
impl Default for ControlBlock {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(loom))]
const _: () = {
use core::mem::offset_of;
assert!(size_of::<Slot>() == CACHE_LINE);
assert!(align_of::<Slot>() == CACHE_LINE);
assert!(offset_of!(Slot, kind) == 0);
assert!(offset_of!(Slot, width) == 4);
assert!(offset_of!(Slot, reason) == 5);
assert!(offset_of!(Slot, address) == 8);
assert!(offset_of!(Slot, value) == 16);
assert!(offset_of!(Slot, instruction_count) == 24);
assert!(offset_of!(Slot, program_counter) == 32);
assert!(offset_of!(Slot, virtual_time_ns) == 40);
assert!(offset_of!(Slot, deadline_ns) == 48);
assert!(size_of::<TurnWord>() == CACHE_LINE);
assert!(align_of::<TurnWord>() == CACHE_LINE);
assert!(size_of::<InterruptLevels>() == 2 * CACHE_LINE);
assert!(align_of::<InterruptLevels>() == CACHE_LINE);
assert!(size_of::<ControlBlock>() == PAGE);
assert!(align_of::<ControlBlock>() == PAGE);
assert!(offset_of!(ControlBlock, magic) == 0);
assert!(offset_of!(ControlBlock, version) == 8);
assert!(offset_of!(ControlBlock, turn) == CACHE_LINE);
assert!(offset_of!(ControlBlock, sidecar_slot) == 2 * CACHE_LINE);
assert!(offset_of!(ControlBlock, core_slot) == 3 * CACHE_LINE);
assert!(offset_of!(ControlBlock, interrupt_levels) == 4 * CACHE_LINE);
};
#[cfg(not(loom))]
#[cfg(test)]
mod tests {
use core::mem::offset_of;
use std::collections::BTreeMap;
use std::process::Command;
use super::*;
use crate::sidecar::message::{Kind, StopReason};
use crate::sidecar::turn::{SLEEP_QUANTUM, SPIN_WINDOW};
const HEADER_PATH: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/include/rigless_proxy_protocol.h"
);
const HEADER: &str = include_str!("../../../include/rigless_proxy_protocol.h");
const DEFINE_PREFIX: &str = "#define RIGLESS_PROXY_";
fn header_defines() -> BTreeMap<String, u64> {
HEADER
.lines()
.filter_map(|line| line.strip_prefix(DEFINE_PREFIX))
.filter_map(|define| define.split_once(' '))
.map(|(name, value)| {
let value = value.trim().trim_end_matches("ULL");
let parsed = value.strip_prefix("0x").map_or_else(
|| value.parse(),
|hex_digits| u64::from_str_radix(hex_digits, 16),
);
(
name.to_owned(),
parsed.unwrap_or_else(|error| panic!("{name}: {error}")),
)
})
.collect()
}
fn rust_defines() -> BTreeMap<String, u64> {
let as_u64 = |value: usize| u64::try_from(value).expect("sizes and offsets fit in u64");
let table = [
("MAGIC", MAGIC),
("VERSION_MAJOR", u64::from(PROTOCOL_VERSION.major)),
("VERSION_MINOR", u64::from(PROTOCOL_VERSION.minor)),
("VERSION_MAJOR_SHIFT", u64::from(VERSION_MAJOR_SHIFT)),
("CACHE_LINE", as_u64(CACHE_LINE)),
("PAGE", as_u64(PAGE)),
("CONTROL_BLOCK_SIZE", as_u64(size_of::<ControlBlock>())),
("SLOT_SIZE", as_u64(size_of::<Slot>())),
("MAGIC_OFFSET", as_u64(offset_of!(ControlBlock, magic))),
("VERSION_OFFSET", as_u64(offset_of!(ControlBlock, version))),
("TURN_OFFSET", as_u64(offset_of!(ControlBlock, turn))),
(
"SIDECAR_SLOT_OFFSET",
as_u64(offset_of!(ControlBlock, sidecar_slot)),
),
(
"CORE_SLOT_OFFSET",
as_u64(offset_of!(ControlBlock, core_slot)),
),
(
"INTERRUPT_LEVELS_OFFSET",
as_u64(offset_of!(ControlBlock, interrupt_levels)),
),
("INTERRUPT_LINES", u64::from(INTERRUPT_LINES)),
("INTERRUPT_WORDS", as_u64(INTERRUPT_WORDS)),
("SLOT_KIND_OFFSET", as_u64(offset_of!(Slot, kind))),
("SLOT_WIDTH_OFFSET", as_u64(offset_of!(Slot, width))),
("SLOT_REASON_OFFSET", as_u64(offset_of!(Slot, reason))),
("SLOT_ADDRESS_OFFSET", as_u64(offset_of!(Slot, address))),
("SLOT_VALUE_OFFSET", as_u64(offset_of!(Slot, value))),
(
"SLOT_INSTRUCTION_COUNT_OFFSET",
as_u64(offset_of!(Slot, instruction_count)),
),
(
"SLOT_PROGRAM_COUNTER_OFFSET",
as_u64(offset_of!(Slot, program_counter)),
),
(
"SLOT_VIRTUAL_TIME_NS_OFFSET",
as_u64(offset_of!(Slot, virtual_time_ns)),
),
(
"SLOT_DEADLINE_NS_OFFSET",
as_u64(offset_of!(Slot, deadline_ns)),
),
("TURN_SIDECAR", u64::from(Turn::Sidecar as u32)),
("TURN_CORE", u64::from(Turn::Core as u32)),
("KIND_HELLO", u64::from(Kind::Hello as u32)),
("KIND_READ", u64::from(Kind::Read as u32)),
("KIND_WRITE", u64::from(Kind::Write as u32)),
("KIND_STOPPED", u64::from(Kind::Stopped as u32)),
("KIND_RUN_UNTIL", u64::from(Kind::RunUntil as u32)),
("KIND_REPLY", u64::from(Kind::Reply as u32)),
("KIND_SHUTDOWN", u64::from(Kind::Shutdown as u32)),
("REASON_DEADLINE", u64::from(StopReason::Deadline as u8)),
("REASON_IDLE", u64::from(StopReason::Idle as u8)),
(
"SPIN_WINDOW_US",
u64::try_from(SPIN_WINDOW.as_micros()).expect("fits"),
),
(
"SLEEP_QUANTUM_US",
u64::try_from(SLEEP_QUANTUM.as_micros()).expect("fits"),
),
];
table
.into_iter()
.map(|(name, value)| (name.to_owned(), value))
.collect()
}
#[test]
fn header_defines_match_rust() {
let header = header_defines();
let rust = rust_defines();
let header_names: Vec<&String> = header.keys().collect();
let rust_names: Vec<&String> = rust.keys().collect();
assert_eq!(header_names, rust_names, "the define sets differ");
assert_eq!(header, rust);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot spawn a process")]
fn header_compiles_as_c11() {
let status = Command::new("cc")
.args([
"-std=c11",
"-Wall",
"-Wextra",
"-Wpedantic",
"-fsyntax-only",
])
.args(["-x", "c", HEADER_PATH])
.status()
.expect("a C compiler named cc is on the path");
assert!(status.success(), "cc exited with {status}");
}
#[test]
fn version_packs_major_above_minor() {
let version = Version { major: 1, minor: 2 };
assert_eq!(version.pack(), 0x0001_0002);
assert_eq!(Version::unpack(0x0001_0002), version);
}
#[test]
fn version_displays_as_major_dot_minor() {
assert_eq!(Version { major: 1, minor: 2 }.to_string(), "1.2");
}
#[test]
fn interrupt_line_rejects_index_at_the_limit() {
assert!(InterruptLine::new(INTERRUPT_LINES - 1).is_some());
assert!(InterruptLine::new(INTERRUPT_LINES).is_none());
}
#[test]
fn raise_sets_the_bit_in_the_second_word() {
let levels = InterruptLevels::default();
let line = InterruptLine::new(70).expect("70 is below the limit");
levels.raise(line);
let words: Vec<u64> = levels
.0
.iter()
.map(|word| word.load(Ordering::Relaxed))
.collect();
assert_eq!(words[0], 0);
assert_eq!(words[1], 1 << 6);
assert!(words[2..].iter().all(|word| *word == 0));
assert!(levels.is_raised(line));
}
#[test]
fn lower_clears_only_its_own_bit() {
let levels = InterruptLevels::default();
let line = InterruptLine::new(70).expect("70 is below the limit");
let neighbour = InterruptLine::new(71).expect("71 is below the limit");
levels.raise(line);
levels.raise(neighbour);
levels.lower(line);
assert!(!levels.is_raised(line));
assert!(levels.is_raised(neighbour));
assert_eq!(levels.0[1].load(Ordering::Relaxed), 1 << 7);
}
}