Skip to main content

concinnity_core/render/
ops.rs

1//! Recorded backend effects: simulation systems queue their GPU mutations as
2//! ops instead of calling the backend directly, and the submit path replays
3//! them in record order before the frame's draw. Ordering across systems is
4//! preserved by the single queue, so the GPU-visible result matches the old
5//! direct calls exactly. Ops own their payloads, so a queue can cross a thread
6//! boundary with the snapshot that carries it.
7
8use crate::gfx::chunk_coord::ChunkCoord;
9use crate::render::backend::RenderBackend;
10use alloc::boxed::Box;
11use alloc::vec::Vec;
12
13type BackendOp = Box<dyn FnOnce(&mut dyn RenderBackend, &mut ReplayOutcome) + Send>;
14
15/// An op whose failure the simulation side must observe and roll back;
16/// fire-and-forget ops log at replay instead.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum OpFailure {
19    /// A streamed-mesh upload was refused (transient region exhaustion); the
20    /// streamer rolls the mesh back to unloaded and retries later.
21    MeshUpload {
22        /// The streamed mesh that failed to upload.
23        stream_id: usize,
24    },
25    /// A chunk-mesh add failed; the chunk's tracking and draw slot roll back.
26    ChunkAdd {
27        /// The chunk whose mesh add failed.
28        coord: ChunkCoord,
29    },
30}
31
32/// What one queue replay produced, for the simulation side.
33#[derive(Debug, Default)]
34pub struct ReplayOutcome {
35    /// Failures the simulation side must roll back.
36    pub failures: Vec<OpFailure>,
37    /// An op hit device-memory exhaustion; feeds the streaming valve.
38    pub memory_pressure: bool,
39}
40
41/// Backend effects recorded by simulation systems, replayed in order by the
42/// submit path. The buffer keeps its capacity across frames.
43#[derive(Default)]
44pub struct RenderOps {
45    ops: Vec<BackendOp>,
46}
47
48impl core::fmt::Debug for RenderOps {
49    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
50        f.debug_struct("RenderOps")
51            .field("len", &self.ops.len())
52            .finish()
53    }
54}
55
56impl RenderOps {
57    /// Record a fire-and-forget backend effect.
58    pub fn record(&mut self, op: impl FnOnce(&mut dyn RenderBackend) + Send + 'static) {
59        self.ops.push(Box::new(move |backend, _| op(backend)));
60    }
61
62    /// Record an effect that reports into the replay outcome (failure
63    /// identity, memory pressure).
64    pub fn record_with(
65        &mut self,
66        op: impl FnOnce(&mut dyn RenderBackend, &mut ReplayOutcome) + Send + 'static,
67    ) {
68        self.ops.push(Box::new(op));
69    }
70
71    /// Whether nothing has been recorded.
72    pub fn is_empty(&self) -> bool {
73        self.ops.is_empty()
74    }
75
76    /// Ops recorded so far.
77    pub fn len(&self) -> usize {
78        self.ops.len()
79    }
80
81    /// Move this queue's ops onto the end of `dst`, leaving this queue empty
82    /// with its capacity intact.
83    pub fn drain_into(&mut self, dst: &mut RenderOps) {
84        dst.ops.append(&mut self.ops);
85    }
86
87    /// Replay every op in record order, draining the queue.
88    pub fn replay(&mut self, backend: &mut dyn RenderBackend) -> ReplayOutcome {
89        let mut outcome = ReplayOutcome::default();
90        for op in self.ops.drain(..) {
91            op(backend, &mut outcome);
92        }
93        outcome
94    }
95
96    /// Drop every recorded op, keeping the buffer's capacity.
97    pub fn clear(&mut self) {
98        self.ops.clear();
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    use alloc::vec;
107    // Replay hands ops the backend in record order and drains the queue.
108    // The mock records call order through the outcome's failure list.
109    #[test]
110    fn replay_runs_ops_in_record_order_and_drains() {
111        let mut ops = RenderOps::default();
112        for i in 0..3 {
113            ops.record_with(move |_, out| {
114                out.failures.push(OpFailure::MeshUpload { stream_id: i });
115            });
116        }
117        assert_eq!(ops.len(), 3);
118        let mut backend = crate::render::backend::test_stub::StubBackend;
119        let outcome = ops.replay(&mut backend);
120        let order: Vec<usize> = outcome
121            .failures
122            .iter()
123            .map(|f| match f {
124                OpFailure::MeshUpload { stream_id } => *stream_id,
125                _ => usize::MAX,
126            })
127            .collect();
128        assert_eq!(order, vec![0, 1, 2]);
129        assert!(ops.is_empty(), "replay drains the queue");
130    }
131
132    #[test]
133    fn drain_into_appends_preserving_order() {
134        let mut a = RenderOps::default();
135        let mut b = RenderOps::default();
136        a.record_with(|_, out| out.failures.push(OpFailure::MeshUpload { stream_id: 1 }));
137        b.record_with(|_, out| out.failures.push(OpFailure::MeshUpload { stream_id: 2 }));
138        a.drain_into(&mut b);
139        assert!(a.is_empty());
140        let mut backend = crate::render::backend::test_stub::StubBackend;
141        let outcome = b.replay(&mut backend);
142        assert_eq!(
143            outcome.failures,
144            vec![
145                OpFailure::MeshUpload { stream_id: 2 },
146                OpFailure::MeshUpload { stream_id: 1 },
147            ]
148        );
149    }
150}