Skip to main content

spark_signals/shared/
shared_slot_buffer.rs

1// ============================================================================
2// spark-signals - SharedSlotBuffer
3//
4// Reactive typed arrays backed by shared memory. get() tracks dependencies,
5// set() writes to shared memory + notifies reactive graph + notifies cross-side.
6//
7// This is Layer 1 of the Cross-Language Reactive Shared Memory architecture.
8// ============================================================================
9
10use std::marker::PhantomData;
11use std::rc::Rc;
12
13use crate::core::types::{AnySource, SourceInner};
14use crate::reactivity::tracking::track_read;
15use crate::shared::notify::Notifier;
16
17// =============================================================================
18// SHARED SLOT BUFFER
19// =============================================================================
20
21/// A reactive typed array backed by shared memory.
22///
23/// - `get(index)` performs a reactive read (tracks dependency via `track_read`)
24/// - `set(index, value)` writes to shared memory + marks reactions dirty + notifies
25/// - `peek(index)` reads without reactive tracking
26///
27/// The buffer owns no allocation — it operates on external memory via raw pointers.
28///
29/// # Type Parameters
30///
31/// - `T`: Element type (must be Copy + PartialEq for equality checking)
32pub struct SharedSlotBuffer<T: Copy + PartialEq + 'static> {
33    ptr: *mut T,
34    len: usize,
35    dirty: Option<*mut u8>,
36    default_value: T,
37    notifier: Box<dyn Notifier>,
38    /// Coarse-grained reactive source (any index changed)
39    source: Rc<SourceInner<u32>>, // value is a version counter
40    _marker: PhantomData<T>,
41}
42
43impl<T: Copy + PartialEq + 'static> SharedSlotBuffer<T> {
44    /// Create a new SharedSlotBuffer over external memory.
45    ///
46    /// # Safety
47    ///
48    /// - `ptr` must point to valid memory with at least `len * size_of::<T>()` bytes
49    /// - The memory must remain valid for the lifetime of this buffer
50    /// - If `dirty` is Some, it must point to valid memory with at least `len` bytes
51    pub unsafe fn new(
52        ptr: *mut T,
53        len: usize,
54        default_value: T,
55        notifier: impl Notifier,
56    ) -> Self {
57        Self {
58            ptr,
59            len,
60            dirty: None,
61            default_value,
62            notifier: Box::new(notifier),
63            source: Rc::new(SourceInner::new(0u32)),
64            _marker: PhantomData,
65        }
66    }
67
68    /// Create with dirty flags.
69    ///
70    /// # Safety
71    ///
72    /// Same as `new()`, plus `dirty` must point to valid memory with `len` bytes.
73    pub unsafe fn with_dirty(
74        ptr: *mut T,
75        len: usize,
76        dirty: *mut u8,
77        default_value: T,
78        notifier: impl Notifier,
79    ) -> Self {
80        Self {
81            ptr,
82            len,
83            dirty: Some(dirty),
84            default_value,
85            notifier: Box::new(notifier),
86            source: Rc::new(SourceInner::new(0u32)),
87            _marker: PhantomData,
88        }
89    }
90
91    /// Reactive read — tracks dependency via the reactive graph.
92    #[inline]
93    pub fn get(&self, index: usize) -> T {
94        debug_assert!(index < self.len, "SharedSlotBuffer: index out of bounds");
95        track_read(self.source.clone() as Rc<dyn AnySource>);
96        unsafe { *self.ptr.add(index) }
97    }
98
99    /// Non-reactive read.
100    #[inline]
101    pub fn peek(&self, index: usize) -> T {
102        debug_assert!(index < self.len, "SharedSlotBuffer: index out of bounds");
103        unsafe { *self.ptr.add(index) }
104    }
105
106    /// Write + mark reactions dirty + set dirty flag + notify cross-side.
107    #[inline]
108    pub fn set(&self, index: usize, value: T) {
109        debug_assert!(index < self.len, "SharedSlotBuffer: index out of bounds");
110
111        let current = unsafe { *self.ptr.add(index) };
112        if current == value {
113            return; // equality check
114        }
115
116        // Write to shared memory
117        unsafe { *self.ptr.add(index) = value; }
118
119        // Set dirty flag
120        if let Some(dirty) = self.dirty {
121            unsafe { *dirty.add(index) = 1; }
122        }
123
124        // Update reactive source version
125        let new_version = self.source.get() + 1;
126        self.source.set(new_version);
127
128        // Notify cross-side
129        self.notifier.notify();
130    }
131
132    /// Batch write — single notification at end.
133    pub fn set_batch(&self, updates: &[(usize, T)]) {
134        let mut changed = false;
135
136        for &(index, value) in updates {
137            debug_assert!(index < self.len, "SharedSlotBuffer: index out of bounds");
138
139            let current = unsafe { *self.ptr.add(index) };
140            if current != value {
141                unsafe { *self.ptr.add(index) = value; }
142                if let Some(dirty) = self.dirty {
143                    unsafe { *dirty.add(index) = 1; }
144                }
145                changed = true;
146            }
147        }
148
149        if changed {
150            let new_version = self.source.get() + 1;
151            self.source.set(new_version);
152            self.notifier.notify();
153        }
154    }
155
156    /// Notify the Rust reactive graph that the other side changed data.
157    /// Call this after waking from a cross-side notification.
158    pub fn notify_changed(&self) {
159        let new_version = self.source.get() + 1;
160        self.source.set(new_version);
161    }
162
163    /// Get the coarse-grained reactive source (for building deriveds that depend on this buffer).
164    pub fn source(&self) -> Rc<SourceInner<u32>> {
165        self.source.clone()
166    }
167
168    /// Reset index to default value.
169    pub fn clear(&self, index: usize) {
170        self.set(index, self.default_value);
171    }
172
173    /// Get buffer length (capacity).
174    pub fn len(&self) -> usize {
175        self.len
176    }
177
178    /// Check if buffer is empty.
179    pub fn is_empty(&self) -> bool {
180        self.len == 0
181    }
182}
183
184// =============================================================================
185// TESTS
186// =============================================================================
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::shared::notify::NoopNotifier;
192
193    #[test]
194    fn basic_get_set() {
195        let mut data = vec![0.0f32; 8];
196        let buf = unsafe {
197            SharedSlotBuffer::new(data.as_mut_ptr(), data.len(), 0.0, NoopNotifier)
198        };
199
200        assert_eq!(buf.peek(0), 0.0);
201        buf.set(0, 42.0);
202        assert_eq!(buf.peek(0), 42.0);
203        assert_eq!(buf.len(), 8);
204    }
205
206    #[test]
207    fn equality_check_skips_write() {
208        let mut data = vec![10.0f32; 4];
209        let buf = unsafe {
210            SharedSlotBuffer::new(data.as_mut_ptr(), data.len(), 0.0, NoopNotifier)
211        };
212
213        // Set same value — should be a no-op
214        buf.set(0, 10.0);
215        // No way to directly observe the skip, but it shouldn't panic or change anything
216        assert_eq!(buf.peek(0), 10.0);
217
218        // Set different value
219        buf.set(0, 20.0);
220        assert_eq!(buf.peek(0), 20.0);
221    }
222
223    #[test]
224    fn dirty_flags() {
225        let mut data = vec![0i32; 4];
226        let mut dirty = vec![0u8; 4];
227        let buf = unsafe {
228            SharedSlotBuffer::with_dirty(
229                data.as_mut_ptr(),
230                data.len(),
231                dirty.as_mut_ptr(),
232                0,
233                NoopNotifier,
234            )
235        };
236
237        assert_eq!(dirty[0], 0);
238        buf.set(0, 42);
239        assert_eq!(dirty[0], 1);
240        assert_eq!(dirty[1], 0);
241
242        buf.set(2, 99);
243        assert_eq!(dirty[2], 1);
244    }
245
246    #[test]
247    fn batch_set() {
248        let mut data = vec![0u32; 8];
249        let buf = unsafe {
250            SharedSlotBuffer::new(data.as_mut_ptr(), data.len(), 0, NoopNotifier)
251        };
252
253        buf.set_batch(&[(0, 10), (3, 30), (7, 70)]);
254        assert_eq!(buf.peek(0), 10);
255        assert_eq!(buf.peek(1), 0);
256        assert_eq!(buf.peek(3), 30);
257        assert_eq!(buf.peek(7), 70);
258    }
259
260    #[test]
261    fn clear_resets_to_default() {
262        let mut data = vec![0.0f32; 4];
263        let buf = unsafe {
264            SharedSlotBuffer::new(data.as_mut_ptr(), data.len(), -1.0, NoopNotifier)
265        };
266
267        buf.set(0, 42.0);
268        assert_eq!(buf.peek(0), 42.0);
269
270        buf.clear(0);
271        assert_eq!(buf.peek(0), -1.0);
272    }
273}