Skip to main content

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