Skip to main content

pathland_core/
ring.rs

1//! Producer-side ring helpers operating on the ring region and header cursors.
2//!
3//! The ring is a power-of-two slot array. The guest writes at `writeCursor` and
4//! the host reads from `readCursor`. Both cursors live in the header block.
5
6use alloc::vec::Vec;
7
8use crate::opcode::Opcode;
9use crate::{constants, memory, memory::header};
10
11/// Errors produced by the ring producer.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum RingError {
14    /// The ring is full (consumer has not caught up). The producer SHOULD
15    /// publish a frame before emitting further opcodes.
16    Full,
17}
18
19/// Append `op` to the ring at `writeCursor` and advance the cursor.
20///
21/// `slots` is the full ring region; `mask` wraps the cursor. Fails with
22/// `RingError::Full` when the next slot equals `readCursor`.
23#[inline]
24pub(crate) fn push(slots: &mut [u8], header: &mut [u8], mask: usize, op: &Opcode) -> Result<(), RingError> {
25    let write = cursor(header, memory::OFF_WRITE_CURSOR, mask);
26    let next = (write + 1) & mask;
27    if next == cursor(header, memory::OFF_READ_CURSOR, mask) {
28        return Err(RingError::Full);
29    }
30    let bytes = op.to_bytes();
31    let off = write * Opcode::SIZE;
32    slots[off..off + Opcode::SIZE].copy_from_slice(&bytes);
33    header::set_u32(header, memory::OFF_WRITE_CURSOR, next as u32);
34    Ok(())
35}
36
37/// Read the current cursor value for a header offset, masked.
38#[inline]
39pub(crate) fn cursor(header: &[u8], off: usize, mask: usize) -> usize {
40    header::get_u32(header, off) as usize & mask
41}
42
43/// Emit a CREATE_NODE opcode.
44pub(crate) fn push_create_node(
45    slots: &mut [u8],
46    header: &mut [u8],
47    mask: usize,
48    node_id: u32,
49    component_type: u16,
50) -> Result<(), RingError> {
51    push(
52        slots,
53        header,
54        mask,
55        &Opcode::new(
56            constants::category::TREE,
57            constants::tree::CREATE_NODE,
58            0,
59            node_id,
60            component_type as u32,
61            0,
62        ),
63    )
64}
65
66pub(crate) fn push_delete_node(
67    slots: &mut [u8],
68    header: &mut [u8],
69    mask: usize,
70    node_id: u32,
71) -> Result<(), RingError> {
72    push(
73        slots,
74        header,
75        mask,
76        &Opcode::new(constants::category::TREE, constants::tree::DELETE_NODE, 0, node_id, 0, 0),
77    )
78}
79
80pub(crate) fn push_insert_child(
81    slots: &mut [u8],
82    header: &mut [u8],
83    mask: usize,
84    parent: u32,
85    child: u32,
86    index: u32,
87) -> Result<(), RingError> {
88    push(
89        slots,
90        header,
91        mask,
92        &Opcode::new(
93            constants::category::TREE,
94            constants::tree::INSERT_CHILD,
95            0,
96            parent,
97            child,
98            index,
99        ),
100    )
101}
102
103pub(crate) fn push_remove_child(
104    slots: &mut [u8],
105    header: &mut [u8],
106    mask: usize,
107    parent: u32,
108    child: u32,
109) -> Result<(), RingError> {
110    push(
111        slots,
112        header,
113        mask,
114        &Opcode::new(
115            constants::category::TREE,
116            constants::tree::REMOVE_CHILD,
117            0,
118            parent,
119            child,
120            0,
121        ),
122    )
123}
124
125pub(crate) fn push_move_child(
126    slots: &mut [u8],
127    header: &mut [u8],
128    mask: usize,
129    parent: u32,
130    child: u32,
131    new_index: u32,
132) -> Result<(), RingError> {
133    push(
134        slots,
135        header,
136        mask,
137        &Opcode::new(
138            constants::category::TREE,
139            constants::tree::MOVE_CHILD,
140            0,
141            parent,
142            child,
143            new_index,
144        ),
145    )
146}
147
148pub(crate) fn push_set_property(
149    slots: &mut [u8],
150    header: &mut [u8],
151    mask: usize,
152    node_id: u32,
153    property_id: u16,
154    value_type: u8,
155    value: u32,
156) -> Result<(), RingError> {
157    push(
158        slots,
159        header,
160        mask,
161        &Opcode::new(
162            constants::category::PARAMETER,
163            constants::parameter::SET_PROPERTY,
164            0,
165            node_id,
166            ((value_type as u32) << 16) | property_id as u32,
167            value,
168        ),
169    )
170}
171
172pub(crate) fn push_set_text(
173    slots: &mut [u8],
174    header: &mut [u8],
175    mask: usize,
176    node_id: u32,
177    arena_ref: u32,
178) -> Result<(), RingError> {
179    push(
180        slots,
181        header,
182        mask,
183        &Opcode::new(
184            constants::category::PARAMETER,
185            constants::parameter::SET_TEXT,
186            0,
187            node_id,
188            arena_ref,
189            0,
190        ),
191    )
192}
193
194pub(crate) fn push_reset(
195    slots: &mut [u8],
196    header: &mut [u8],
197    mask: usize,
198) -> Result<(), RingError> {
199    push(
200        slots,
201        header,
202        mask,
203        &Opcode::new(constants::category::META, constants::meta::RESET, 0, 0, 0, 0),
204    )
205}
206
207// ---------------------------------------------------------------------------
208// Host → guest event ring
209// ---------------------------------------------------------------------------
210
211/// Append an `EVENT`-category opcode to the host → guest event ring.
212///
213/// The host (renderer) writes at `eventWriteCursor`; the guest reads from
214/// `eventReadCursor`. Same 16-byte slot layout as the guest → host ring.
215#[inline]
216pub(crate) fn push_event(
217    event_slots: &mut [u8],
218    header: &mut [u8],
219    mask: usize,
220    op: &Opcode,
221) -> Result<(), RingError> {
222    let write = cursor(header, memory::OFF_EVENT_WRITE_CURSOR, mask);
223    let next = (write + 1) & mask;
224    if next == cursor(header, memory::OFF_EVENT_READ_CURSOR, mask) {
225        return Err(RingError::Full);
226    }
227    let bytes = op.to_bytes();
228    let off = write * Opcode::SIZE;
229    event_slots[off..off + Opcode::SIZE].copy_from_slice(&bytes);
230    header::set_u32(header, memory::OFF_EVENT_WRITE_CURSOR, next as u32);
231    Ok(())
232}
233
234/// Drain pending `EVENT`-category opcodes from the host → guest event ring,
235/// advancing `eventReadCursor`. The guest (app/engine) reads raw inputs here.
236///
237/// The masked cursors wrap independently — the writer can lap the reader
238/// numerically — so the pending count is computed modulo the ring size. A
239/// plain `write < read` guard would permanently deadlock the ring the moment
240/// the writer wraps past the reader (the reader never advances, the writer is
241/// stuck "full", and every drain returns empty). `max` bounds how many opcodes
242/// are drained (the rest stay pending for the next call).
243#[inline]
244pub(crate) fn read_events(
245    event_slots: &[u8],
246    header: &mut [u8],
247    mask: usize,
248    max: usize,
249) -> Vec<Opcode> {
250    let read = cursor(header, memory::OFF_EVENT_READ_CURSOR, mask);
251    let write = cursor(header, memory::OFF_EVENT_WRITE_CURSOR, mask);
252    let count = write.wrapping_sub(read) & mask;
253    let n = count.min(max);
254    if n == 0 {
255        return Vec::new();
256    }
257    let mut out = Vec::with_capacity(n);
258    for i in 0..n {
259        let slot = (read + i) & mask;
260        let off = slot * Opcode::SIZE;
261        let bytes: [u8; Opcode::SIZE] =
262            event_slots[off..off + Opcode::SIZE].try_into().unwrap();
263        out.push(Opcode::from_bytes(&bytes));
264    }
265    header::set_u32(header, memory::OFF_EVENT_READ_CURSOR, ((read + n) & mask) as u32);
266    out
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use crate::memory::header;
273    use alloc::vec;
274
275    /// A tiny initialized linear memory with `slot_count` slots and a bare
276    /// arena.
277    fn tiny_mem(slot_count: usize) -> Vec<u8> {
278        let layout = crate::memory::MemoryLayout {
279            slot_count,
280            arena_bytes: 64,
281            event_arena_bytes: 64,
282        };
283        let mut mem = vec![0u8; layout.total_bytes()];
284        crate::init_memory(&mut mem, &layout);
285        mem
286    }
287
288    #[test]
289    fn push_reports_full_at_exact_capacity() {
290        let slot_count = 4usize;
291        let mut mem = tiny_mem(slot_count);
292        let mask = slot_count - 1;
293
294        // With a 4-slot ring, capacity is slot_count - 1 = 3 opcodes.
295        let (head, rest) = mem.split_at_mut(crate::memory::HEADER_SIZE);
296        let (slots, _rest) = rest.split_at_mut(slot_count * Opcode::SIZE);
297
298        for i in 0..3 {
299            push(slots, head, mask, &Opcode::new(crate::category::TREE, 1, 0, i, 0, 0)).unwrap();
300        }
301        assert_eq!(
302            push(slots, head, mask, &Opcode::new(crate::category::TREE, 1, 0, 9, 0, 0)),
303            Err(RingError::Full)
304        );
305        // writeCursor stopped at slot_count - 1, never wrapping.
306        assert_eq!(header::get_u32(head, crate::memory::OFF_WRITE_CURSOR), 3);
307    }
308
309    #[test]
310    fn cursor_masks_out_of_range_header_values() {
311        let mut mem = tiny_mem(8);
312        let (head, _) = mem.split_at_mut(crate::memory::HEADER_SIZE);
313        header::set_u32(head, crate::memory::OFF_WRITE_CURSOR, 17);
314        // mask = 7, so 17 & 7 = 1.
315        assert_eq!(cursor(head, crate::memory::OFF_WRITE_CURSOR, 7), 1);
316    }
317
318    #[test]
319    fn push_event_and_read_events_round_trip() {
320        let slot_count = 4usize;
321        let mut mem = tiny_mem(slot_count);
322        let mask = slot_count - 1;
323
324        let (head, rest) = mem.split_at_mut(crate::memory::HEADER_SIZE);
325        let (_slots, rest) = rest.split_at_mut(slot_count * Opcode::SIZE);
326        let (event_slots, _arena) = rest.split_at_mut(slot_count * Opcode::SIZE);
327
328        let op = Opcode::new(crate::category::EVENT, crate::event::POINTER_UP, 0, 5, 0, 0);
329        push_event(event_slots, head, mask, &op).unwrap();
330
331        let drained = read_events(event_slots, head, mask, usize::MAX);
332        assert_eq!(drained, vec![op]);
333        // Second read is empty (readCursor advanced).
334        assert!(read_events(event_slots, head, mask, usize::MAX).is_empty());
335        assert_eq!(
336            header::get_u32(head, crate::memory::OFF_EVENT_READ_CURSOR),
337            header::get_u32(head, crate::memory::OFF_EVENT_WRITE_CURSOR)
338        );
339    }
340
341    #[test]
342    fn push_event_reports_full_and_recovers() {
343        let slot_count = 2usize;
344        let mut mem = tiny_mem(slot_count);
345        let mask = slot_count - 1;
346
347        let (head, rest) = mem.split_at_mut(crate::memory::HEADER_SIZE);
348        let (_slots, rest) = rest.split_at_mut(slot_count * Opcode::SIZE);
349        let (event_slots, _arena) = rest.split_at_mut(slot_count * Opcode::SIZE);
350
351        let op = Opcode::new(crate::category::EVENT, crate::event::POINTER_UP, 0, 5, 0, 0);
352        assert!(push_event(event_slots, head, mask, &op).is_ok());
353        assert_eq!(
354            push_event(event_slots, head, mask, &op),
355            Err(RingError::Full)
356        );
357
358        // Guest drains, freeing the slot; the host can write again.
359        let drained = read_events(event_slots, head, mask, usize::MAX);
360        assert_eq!(drained.len(), 1);
361        assert!(push_event(event_slots, head, mask, &op).is_ok());
362    }
363
364    #[test]
365    fn read_events_drains_a_wrapped_producer() {
366        let slot_count = 4usize;
367        let mut mem = tiny_mem(slot_count);
368        let mask = slot_count - 1;
369
370        let (head, rest) = mem.split_at_mut(crate::memory::HEADER_SIZE);
371        let (_slots, rest) = rest.split_at_mut(slot_count * Opcode::SIZE);
372        let (event_slots, _arena) = rest.split_at_mut(slot_count * Opcode::SIZE);
373
374        let op = Opcode::new(crate::category::EVENT, crate::event::POINTER_UP, 0, 5, 0, 0);
375        // Write 3 events into slots 0,1,2 (capacity is slot_count - 1 = 3).
376        for _ in 0..3 {
377            push_event(event_slots, head, mask, &op).unwrap();
378        }
379        // Reader drains ONE (bounded), so readCursor advances to 1.
380        assert_eq!(read_events(event_slots, head, mask, 1).len(), 1);
381        // Writer can write again: slot 3, writeCursor wraps 3 -> 0. Now the
382        // masked write(0) is numerically BELOW the masked read(1) — the wrap
383        // that used to deadlock the ring.
384        push_event(event_slots, head, mask, &op).unwrap();
385        assert_eq!(
386            header::get_u32(head, crate::memory::OFF_EVENT_READ_CURSOR),
387            1
388        );
389        assert_eq!(
390            header::get_u32(head, crate::memory::OFF_EVENT_WRITE_CURSOR),
391            0
392        );
393
394        // The wrapped run [1..4) + [0..0) = slots 1,2,3 must drain in order,
395        // and readCursor must advance to the (masked) write cursor.
396        let drained = read_events(event_slots, head, mask, usize::MAX);
397        assert_eq!(drained, vec![op; 3]);
398        assert_eq!(
399            header::get_u32(head, crate::memory::OFF_EVENT_READ_CURSOR),
400            0
401        );
402        // And the writer can wrap again without ever deadlocking.
403        assert!(push_event(event_slots, head, mask, &op).is_ok());
404        assert_eq!(
405            read_events(event_slots, head, mask, usize::MAX).len(),
406            1
407        );
408    }
409}