//! Triple-buffered sim snapshot: the render thread's read-only feed //! (docs/04 §4.5, docs/05 §5.2 step 1). //! //! The render thread never touches live sim state (docs/04 §4.10 rule 3). //! Each tick the sim publishes a [`Snapshot`] — visible light plane + //! actor list, ~200 KB at the 120×36 reference size (120×36 light bytes + //! actor records) — and the renderer reads the latest published frame. //! //! Contention semantics (docs/05 §5.2 step 1): [`TripleBuffer::read`] takes //! `&self` and never blocks. On contention the render side re-uses the last //! published frame — tearing beats stalling. The sim side //! ([`TripleBuffer::write`] + [`publish`](TripleBuffer::publish)) never //! waits on the renderer either. This type is the single-threaded storage + //! rotation policy; when the threaded handoff lands, the parent must guard //! the `latest` index with an atomic (e.g. `AtomicUsize`, `SeqCst`) — the //! slot discipline (write into a back buffer, publish, read latest) stays //! exactly as modeled here. //! //! Parent-unification notes (parallel workstream `ray.rs` / `shade.rs`): //! - `Snapshot.cells_light` is the 24-step light level per cell the shade //! step multiplies in (docs/05 §5.1 "Light as truth", §5.2 step 3). //! - `SnapActor.glyph` / `fg` are the billboard contract: `ray.rs` //! depth-tests them against the ray depth buffer brightest-first, and //! `shade.rs` owns species+posture+phase glyph selection (docs/05 §5.1 //! "Actors as glyphs-with-posture", §5.2 step 4). Status overlays //! (burning `*`, blessed halo, cursed drip) ride on `glyph`/`fg` too. /// Per-frame bytes carried by one actor record (estimate; see [`byte_size`]). pub const SNAP_ACTOR_BYTES: usize = 32; /// Fixed per-snapshot bookkeeping overhead in the [`byte_size`] estimate. pub const SNAPSHOT_OVERHEAD_BYTES: usize = 128; /// One actor as the renderer sees it: billboard contract for `ray.rs` / /// `shade.rs` (docs/05 §5.2 step 4). Continuous sim position; the renderer /// interpolates between ticks (see `camera::interpolate`). #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct SnapActor { /// Sim entity id (stable across ticks for interpolation). pub id: u64, /// Continuous world position in metres. pub pos: [f32; 3], /// Pre-selected billboard glyph (species + posture + anim phase). pub glyph: char, /// Pre-shaded foreground truecolor. pub fg: [u8; 3], } /// One published sim frame (docs/04 §4.5, docs/05 §5.2 step 1). #[derive(Clone, Debug, Default, PartialEq)] pub struct Snapshot { /// Frame width in cells. pub w: usize, /// Frame height in cells. pub h: usize, /// Sim tick this frame was published at (docs/04 §4.2: 10 Hz). pub tick: u64, /// Per-cell 24-step light level, row-major, `w*h` bytes. pub cells_light: Vec, /// Visible actors for billboard compositing. pub actors: Vec, } impl Snapshot { /// Blank frame with a zeroed `w*h` light plane. pub fn new(w: usize, h: usize) -> Self { Self { w, h, tick: 0, cells_light: vec![0; w.saturating_mul(h)], actors: Vec::new(), } } } /// Estimated bytes of one [`Snapshot`] for `w`×`h` cells and `actors` /// records: `w*h` light bytes + [`SNAP_ACTOR_BYTES`] per actor + /// [`SNAPSHOT_OVERHEAD_BYTES`]. At 120×36 + 64 actors this is ≈6 KB /// (inside the ~200 KB step-1 budget, docs/05 §5.2); the triple total at /// 240×70 + 512 actors stays ≤ 1 MB (docs/05 §5.2 §"Performance budget"). pub fn byte_size(w: usize, h: usize, actors: usize) -> usize { w.saturating_mul(h) .saturating_add(actors.saturating_mul(SNAP_ACTOR_BYTES)) .saturating_add(SNAPSHOT_OVERHEAD_BYTES) } /// Triple-buffered snapshot store (docs/04 §4.5). /// /// Slot discipline: the sim fills the back buffer via [`write`](Self::write), /// publishes it with [`publish`](Self::publish), and the renderer reads the /// latest published frame via [`read`](Self::read). `read` never blocks /// `write` and `write` never blocks `read`. pub struct TripleBuffer { bufs: [Snapshot; 3], latest: usize, back: usize, } impl TripleBuffer { /// Three blank `w`×`h` frames; [`read`](Self::read) yields tick 0 until /// the first [`publish`](Self::publish). pub fn new(w: usize, h: usize) -> Self { Self { bufs: [ Snapshot::new(w, h), Snapshot::new(w, h), Snapshot::new(w, h), ], latest: 0, back: 1, } } /// Sim side: exclusive access to the back buffer. Mutate, then call /// [`publish`](Self::publish) to make it visible to [`read`](Self::read). pub fn write(&mut self) -> &mut Snapshot { &mut self.bufs[self.back] } /// Sim side: publish the back buffer and rotate in a fresh one. pub fn publish(&mut self) { self.latest = self.back; self.back = (self.latest + 1) % 3; } /// Render side: the latest published snapshot. Non-blocking by /// construction (`&self`); on contention the caller re-uses this frame /// (docs/05 §5.2 step 1: tearing beats stalling). pub fn read(&self) -> &Snapshot { &self.bufs[self.latest] } } #[cfg(test)] mod tests { use super::*; fn actor(id: u64, tick_tag: f32) -> SnapActor { SnapActor { id, pos: [tick_tag, 0.0, 0.0], glyph: '@', fg: [255, 214, 64], } } #[test] fn write_publish_read_visibility() { let mut tb = TripleBuffer::new(8, 4); tb.write().tick = 42; tb.write().actors.push(actor(7, 1.0)); tb.publish(); let r = tb.read(); assert_eq!(r.tick, 42); assert_eq!(r.actors.len(), 1); assert_eq!(r.actors[0].id, 7); } #[test] fn unpublished_write_stays_invisible() { let mut tb = TripleBuffer::new(8, 4); tb.publish(); // tick 0 baseline tb.write().tick = 99; assert_eq!(tb.read().tick, 0, "read must see last published frame"); tb.publish(); assert_eq!(tb.read().tick, 99); } #[test] fn read_never_blocks_write() { // Sequential proof of the non-blocking contract: interleaved // read/write/publish over many frames always shows the latest // published tick and never stalls either side. let mut tb = TripleBuffer::new(8, 4); for t in 1..=10u64 { let _ = tb.read(); tb.write().tick = t; tb.publish(); assert_eq!(tb.read().tick, t); } } #[test] fn byte_size_budgets() { // Reference frame fits the ~200 KB step-1 budget (docs/05 §5.2). assert!(byte_size(120, 36, 64) <= 200_000); // Triple total at max size fits the ≤ 1 MB budget (docs/05 §5.2). assert!(3 * byte_size(240, 70, 512) <= 1_000_000); // Light plane dominates: one byte per cell plus actor records. assert_eq!( byte_size(10, 10, 2), 100 + 2 * SNAP_ACTOR_BYTES + SNAPSHOT_OVERHEAD_BYTES ); } #[test] fn default_and_new_are_sane() { let d = Snapshot::default(); assert_eq!((d.w, d.h, d.tick), (0, 0, 0)); assert!(d.cells_light.is_empty() && d.actors.is_empty()); let s = Snapshot::new(8, 4); assert_eq!(s.cells_light.len(), 32); let tb = TripleBuffer::new(8, 4); assert_eq!(tb.read().tick, 0); assert_eq!(tb.read().cells_light.len(), 32); } }