Skip to main content

ginger/
seqlock.rs

1//! Seqlock-based atomic pairs for the atomic multi-threading method.
2//!
3//! `AtomicU128` is unstable on stable Rust, and two separate `AtomicU64`s
4//! allow torn (x, y) reads. A seqlock — a version counter plus two bit-pattern
5//! `AtomicU64`s — gives torn-free whole-pair reads without 128-bit atomics.
6//!
7//! Each slot is single-writer, multi-reader: the owning thread calls `store`
8//! while every other thread only calls `load`. The writer bumps the sequence
9//! to odd while updating, then to even when stable; readers retry until they
10//! observe a stable (even) sequence with matching reads of both components.
11
12use crate::vector2::Vector2;
13use num_complex::Complex;
14use std::sync::atomic::{AtomicU64, Ordering};
15
16/// Seqlock over a pair of `f64` values (single-writer, multi-reader).
17///
18/// Writer protocol: `seq` becomes odd while `c0`/`c1` are being updated, then
19/// even again once the pair is stable. Readers spin until they read a stable
20/// even sequence number, guaranteeing the two components are from one write.
21pub struct AtomicF64x2 {
22    seq: AtomicU64,
23    c0: AtomicU64,
24    c1: AtomicU64,
25}
26
27impl AtomicF64x2 {
28    /// Create a seqlock initialized with the given pair.
29    pub fn new(x: f64, y: f64) -> Self {
30        AtomicF64x2 {
31            seq: AtomicU64::new(0),
32            c0: AtomicU64::new(x.to_bits()),
33            c1: AtomicU64::new(y.to_bits()),
34        }
35    }
36
37    /// Torn-free read of the pair; retries while the writer is mid-update.
38    pub fn load(&self) -> [f64; 2] {
39        loop {
40            let s1 = self.seq.load(Ordering::Acquire);
41            let c0 = f64::from_bits(self.c0.load(Ordering::Relaxed));
42            let c1 = f64::from_bits(self.c1.load(Ordering::Relaxed));
43            let s2 = self.seq.load(Ordering::Acquire);
44            if s1 == s2 && s1 % 2 == 0 {
45                return [c0, c1];
46            }
47        }
48    }
49
50    /// Store a new pair; must be called by the single writer of this slot.
51    pub fn store(&self, x: f64, y: f64) {
52        let s = self.seq.load(Ordering::Relaxed);
53        self.seq.store(s + 1, Ordering::Release); // odd: writing
54        self.c0.store(x.to_bits(), Ordering::Relaxed);
55        self.c1.store(y.to_bits(), Ordering::Relaxed);
56        self.seq.store(s + 2, Ordering::Release); // even: stable
57    }
58}
59
60/// Atomic `Complex<f64>` (seqlock over re/im) — whole-pair atomicity.
61pub struct AtomicComplex(AtomicF64x2);
62
63impl AtomicComplex {
64    /// Create an atomic complex value.
65    #[inline]
66    pub fn new(z: Complex<f64>) -> Self {
67        AtomicComplex(AtomicF64x2::new(z.re, z.im))
68    }
69
70    /// Torn-free read of the complex value.
71    pub fn load(&self) -> Complex<f64> {
72        let [re, im] = self.0.load();
73        Complex::new(re, im)
74    }
75
76    /// Store a new complex value; single-writer per slot.
77    #[inline]
78    pub fn store(&self, z: Complex<f64>) {
79        self.0.store(z.re, z.im);
80    }
81}
82
83/// Atomic `Vector2<f64>` (seqlock over x/y) — whole-pair atomicity.
84pub struct AtomicVec2(AtomicF64x2);
85
86impl AtomicVec2 {
87    /// Create an atomic vector value.
88    #[inline]
89    pub fn new(v: Vector2<f64>) -> Self {
90        AtomicVec2(AtomicF64x2::new(v.x_, v.y_))
91    }
92
93    /// Torn-free read of the vector value.
94    pub fn load(&self) -> Vector2<f64> {
95        let [x, y] = self.0.load();
96        Vector2::new(x, y)
97    }
98
99    /// Store a new vector value; single-writer per slot.
100    #[inline]
101    pub fn store(&self, v: Vector2<f64>) {
102        self.0.store(v.x_, v.y_);
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_atomic_f64x2_roundtrip() {
112        let slot = AtomicF64x2::new(1.0, 2.0);
113        assert_eq!(slot.load(), [1.0, 2.0]);
114        slot.store(3.0, 4.0);
115        assert_eq!(slot.load(), [3.0, 4.0]);
116    }
117
118    #[test]
119    fn test_atomic_complex_roundtrip() {
120        let slot = AtomicComplex::new(Complex::new(1.0, -2.5));
121        assert_eq!(slot.load(), Complex::new(1.0, -2.5));
122        slot.store(Complex::new(-3.0, 0.5));
123        assert_eq!(slot.load(), Complex::new(-3.0, 0.5));
124    }
125
126    #[test]
127    fn test_atomic_vec2_roundtrip() {
128        let slot = AtomicVec2::new(Vector2::new(1.0, 2.0));
129        assert_eq!(slot.load(), Vector2::new(1.0, 2.0));
130        slot.store(Vector2::new(3.0, 4.0));
131        assert_eq!(slot.load(), Vector2::new(3.0, 4.0));
132    }
133}