Skip to main content

concinnity_render/
snapshot.rs

1//! The owned per-frame snapshot the extraction phase fills from world state
2//! and the submission phase consumes. Self-contained by construction: no
3//! borrows into component storage, resources, or the backend, so a frame's
4//! draw inputs can outlive the world borrow that produced them and later
5//! cross a thread boundary. Buffers keep their capacity across frames; a
6//! steady-state extraction allocates nothing.
7
8use crate::render_types::{LineVertex, TextDrawCall};
9use crate::scene_flow::SceneControl;
10use alloc::vec::Vec;
11use concinnity_core::gfx::view_modes::{ShowFlags, ViewMode};
12
13type Mat4 = [[f32; 4]; 4];
14
15/// Camera and frame-wide flags for one frame's draw.
16#[derive(Clone, Copy, Debug)]
17pub struct FrameScalars {
18    /// Seconds since the world started.
19    pub elapsed: f32,
20    /// Vertical field of view in radians.
21    pub fov_y_radians: f32,
22    /// Near clip distance in world units.
23    pub near: f32,
24    /// Far clip distance in world units.
25    pub far: f32,
26    /// View matrix the frame draws with (rebased when a chunk world streams).
27    pub view: Mat4,
28    /// Camera position in the space the frame renders in.
29    pub cam_pos: [f32; 3],
30    /// What the composite presents this frame.
31    pub view_mode: ViewMode,
32    /// Feature passes to run this frame.
33    pub show: ShowFlags,
34    /// `true` when an opaque menu backdrop covers the scene.
35    pub world_hidden: bool,
36    /// `true` while any world-pausing screen is open.
37    pub menu_active: bool,
38}
39
40impl Default for FrameScalars {
41    fn default() -> Self {
42        Self {
43            elapsed: 0.0,
44            fov_y_radians: core::f32::consts::FRAC_PI_4,
45            near: 0.05,
46            far: 200.0,
47            view: concinnity_core::gfx::transform::IDENTITY,
48            cam_pos: [0.0; 3],
49            view_mode: ViewMode::default(),
50            show: ShowFlags::default(),
51            world_hidden: false,
52            menu_active: false,
53        }
54    }
55}
56
57/// Window-interaction intents resolved during extraction, applied verbatim by
58/// submission. `None` means "do not touch the current state this frame".
59#[derive(Clone, Copy, Debug, Default)]
60pub struct UiIntents {
61    /// Hide the OS cursor while an in-engine cursor sprite is shown.
62    pub cursor_hidden: bool,
63    /// Enter or leave menu mode, or `None` to leave it as is.
64    pub menu_mode: Option<bool>,
65    /// Capture or release the cursor, or `None` to leave it as is.
66    pub camera_capture: Option<bool>,
67}
68
69/// Variable-length per-slot updates flattened into one values buffer plus
70/// `(slot, range)` spans, so extraction copies into persistent storage and
71/// submission replays one backend call per span.
72#[derive(Debug, Default)]
73pub struct SpanBuffer<T> {
74    values: Vec<T>,
75    spans: Vec<(usize, u32, u32)>,
76}
77
78impl<T: Copy> SpanBuffer<T> {
79    /// Drop every recorded span, keeping the buffers' capacity.
80    pub fn clear(&mut self) {
81        self.values.clear();
82        self.spans.clear();
83    }
84
85    /// Whether no span has been pushed.
86    pub fn is_empty(&self) -> bool {
87        self.spans.is_empty()
88    }
89
90    /// Append one slot's values as a new span.
91    pub fn push(&mut self, slot: usize, values: &[T]) {
92        let start = self.values.len() as u32;
93        self.values.extend_from_slice(values);
94        self.spans.push((slot, start, values.len() as u32));
95    }
96
97    /// The spans in push order as `(slot, values)`.
98    pub fn iter(&self) -> impl Iterator<Item = (usize, &[T])> {
99        self.spans
100            .iter()
101            .map(|&(slot, start, len)| (slot, &self.values[start as usize..(start + len) as usize]))
102    }
103}
104
105/// One recorded scene-visibility effect, replayed onto the backend at
106/// submission in record order.
107#[derive(Clone, Copy, Debug, PartialEq)]
108pub enum SceneOp {
109    /// Set the scene-transition fade, 0 (clear) to 1 (black).
110    SetFade(f32),
111    /// Show or hide one draw slot.
112    Visibility {
113        /// The draw slot whose visibility changes.
114        draw_idx: usize,
115        /// `true` to show the slot, `false` to hide it.
116        visible: bool,
117    },
118}
119
120/// A [`SceneControl`] that records calls as [`SceneOp`]s instead of driving a
121/// backend, so scene-flow logic can run during extraction.
122pub struct SceneOpRecorder<'a>(pub &'a mut Vec<SceneOp>);
123
124impl SceneControl for SceneOpRecorder<'_> {
125    fn update_visibility(&mut self, draw_idx: usize, visible: bool) {
126        self.0.push(SceneOp::Visibility { draw_idx, visible });
127    }
128
129    fn set_fade(&mut self, fade: f32) {
130        self.0.push(SceneOp::SetFade(fade));
131    }
132}
133
134/// Everything one frame's draw consumes, extracted from world state.
135#[derive(Default)]
136pub struct RenderSnapshot {
137    /// Camera and frame-wide scalars.
138    pub frame: FrameScalars,
139    /// Window-interaction intents.
140    pub ui: UiIntents,
141    /// Backend effects recorded by the simulation systems this tick (spawn
142    /// slot ops, settings appliers, streaming uploads), replayed in record
143    /// order before the frame's draw.
144    pub ops: crate::ops::RenderOps,
145    /// Changed static draw-slot model matrices, in push order (a slot pushed
146    /// twice keeps both entries; the last write wins on the backend).
147    pub models: Vec<(u32, Mat4)>,
148    /// Changed skinned-instance model matrices, in push order.
149    pub skinned_models: Vec<(u32, Mat4)>,
150    /// Updated skinned joint matrices, keyed by skinned instance index.
151    pub poses: SpanBuffer<Mat4>,
152    /// Updated morph-target weights, keyed by skinned instance index.
153    pub morphs: SpanBuffer<f32>,
154    /// The frame's overlay draw list (UI text + sprites), adopted whole from
155    /// the overlay build. `clear` leaves it untouched: extraction replaces the
156    /// list wholesale and hands the spent one back to the overlay build, so
157    /// its buffers recycle instead of dropping here.
158    pub text_calls: Vec<TextDrawCall>,
159    /// Expanded world-space line ribbons for this frame's camera.
160    pub lines: Vec<LineVertex>,
161    /// Scene fade / visibility effects recorded this frame.
162    pub scene_ops: Vec<SceneOp>,
163}
164
165impl RenderSnapshot {
166    /// Reset for a new frame's extraction, keeping buffer capacity.
167    pub fn clear(&mut self) {
168        self.frame = FrameScalars::default();
169        self.ui = UiIntents::default();
170        self.ops.clear();
171        self.models.clear();
172        self.skinned_models.clear();
173        self.poses.clear();
174        self.morphs.clear();
175        self.lines.clear();
176        self.scene_ops.clear();
177    }
178}
179
180// The snapshot must stay owned data so a later render thread can take it.
181const _: () = {
182    const fn require_send<T: Send + 'static>() {}
183    require_send::<RenderSnapshot>()
184};
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    use alloc::vec;
191    #[test]
192    fn span_buffer_round_trips_slots_in_push_order() {
193        let mut spans: SpanBuffer<u32> = SpanBuffer::default();
194        assert!(spans.is_empty());
195        spans.push(7, &[1, 2, 3]);
196        spans.push(2, &[9]);
197        let collected: Vec<(usize, Vec<u32>)> =
198            spans.iter().map(|(slot, v)| (slot, v.to_vec())).collect();
199        assert_eq!(collected, vec![(7, vec![1, 2, 3]), (2, vec![9])]);
200    }
201
202    #[test]
203    fn span_buffer_clear_keeps_capacity() {
204        let mut spans: SpanBuffer<u32> = SpanBuffer::default();
205        spans.push(0, &[1, 2, 3, 4]);
206        spans.push(1, &[5, 6]);
207        let values_ptr = spans.values.as_ptr();
208        spans.clear();
209        assert!(spans.is_empty());
210        spans.push(0, &[1, 2, 3]);
211        assert_eq!(
212            spans.values.as_ptr(),
213            values_ptr,
214            "values buffer reallocated"
215        );
216    }
217
218    #[test]
219    fn recorder_captures_fade_and_visibility_in_order() {
220        let mut ops = Vec::new();
221        {
222            let mut recorder = SceneOpRecorder(&mut ops);
223            recorder.set_fade(0.5);
224            recorder.update_visibility(3, false);
225            recorder.update_visibility(4, true);
226        }
227        assert_eq!(
228            ops,
229            vec![
230                SceneOp::SetFade(0.5),
231                SceneOp::Visibility {
232                    draw_idx: 3,
233                    visible: false
234                },
235                SceneOp::Visibility {
236                    draw_idx: 4,
237                    visible: true
238                },
239            ]
240        );
241    }
242
243    #[test]
244    fn snapshot_clear_resets_contents_and_keeps_capacity() {
245        let mut snap = RenderSnapshot::default();
246        snap.models.push((1, [[0.0; 4]; 4]));
247        snap.scene_ops.push(SceneOp::SetFade(1.0));
248        snap.frame.elapsed = 5.0;
249        let models_ptr = snap.models.as_ptr();
250        snap.clear();
251        assert!(snap.models.is_empty());
252        assert!(snap.scene_ops.is_empty());
253        assert_eq!(snap.frame.elapsed, 0.0);
254        snap.models.push((2, [[0.0; 4]; 4]));
255        assert_eq!(
256            snap.models.as_ptr(),
257            models_ptr,
258            "models buffer reallocated"
259        );
260    }
261}