Skip to main content

molgfx_render/engine/
sequence.rs

1//! Bounded, history-preserving off-screen sequence submission.
2
3use super::image::{ImagePurpose, PendingImage};
4use super::{Engine, Image, ImageConfig};
5use crate::RenderError;
6use molgfx_core::Scene;
7use molgfx_gpu::{Device, Queue as _};
8use molgfx_math::Camera;
9use std::collections::VecDeque;
10use std::fmt;
11
12/// Fixed output and readback limits for one deterministic frame sequence.
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub struct SequenceConfig {
15    /// Dimensions shared by every frame.
16    pub image: ImageConfig,
17    /// Nanoseconds represented by one timestamp tick.
18    pub timebase_nanoseconds: u64,
19    /// Bounded number of submitted frames awaiting readback.
20    pub max_in_flight: u8,
21}
22
23impl SequenceConfig {
24    /// Creates a nanosecond timebase for a fixed integer frame rate.
25    ///
26    /// # Errors
27    ///
28    /// Rejects zero frame rates and rates that do not map to a positive
29    /// integral nanosecond interval.
30    pub fn at_fps(
31        image: ImageConfig,
32        frames_per_second: u32,
33        max_in_flight: u8,
34    ) -> Result<Self, RenderError> {
35        let timebase_nanoseconds = 1_000_000_000_u64
36            .checked_div(u64::from(frames_per_second))
37            .filter(|value| *value > 0)
38            .ok_or(RenderError::InvalidSequence {
39                reason: "frame rate must map to a positive nanosecond interval",
40            })?;
41        let config = Self {
42            image,
43            timebase_nanoseconds,
44            max_in_flight,
45        };
46        config.validate()?;
47        Ok(config)
48    }
49
50    fn validate(self) -> Result<(), RenderError> {
51        if self.timebase_nanoseconds == 0 {
52            return Err(RenderError::InvalidSequence {
53                reason: "timebase must be positive",
54            });
55        }
56        if !(2..=3).contains(&self.max_in_flight) {
57            return Err(RenderError::InvalidSequence {
58                reason: "max_in_flight must be two or three",
59            });
60        }
61        Ok(())
62    }
63}
64
65/// Stable identity and timestamp for one submitted sequence frame.
66#[derive(Clone, Copy, PartialEq, Eq, Debug)]
67pub struct FrameTicket {
68    /// Monotonic frame number within this sequence.
69    pub index: u64,
70    /// Caller timestamp in configured timebase ticks.
71    pub timestamp: u64,
72}
73
74/// One completed, ordered sequence frame.
75#[derive(Clone, PartialEq, Eq, Debug)]
76pub struct SequenceFrame {
77    /// Submission identity retained through readback.
78    pub ticket: FrameTicket,
79    /// Tightly packed caller-owned pixels.
80    pub image: Image,
81}
82
83struct PendingSequence<D: Device> {
84    ticket: FrameTicket,
85    image: PendingImage<D>,
86}
87
88/// A bounded sequence pipeline for one engine/backend type.
89///
90/// Submissions preserve temporal history and do not wait for readback. `poll`
91/// resolves only a completed oldest frame, preserving presentation order.
92pub struct SequenceRenderer<D: Device> {
93    config: SequenceConfig,
94    pending: VecDeque<PendingSequence<D>>,
95    next_index: u64,
96    last_timestamp: Option<u64>,
97}
98
99impl<D: Device> fmt::Debug for SequenceRenderer<D> {
100    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101        formatter
102            .debug_struct("SequenceRenderer")
103            .field("config", &self.config)
104            .field("pending", &self.pending.len())
105            .field("next_index", &self.next_index)
106            .field("last_timestamp", &self.last_timestamp)
107            .finish_non_exhaustive()
108    }
109}
110
111impl<D: Device> Engine<D> {
112    /// Opens a bounded history-preserving sequence pipeline.
113    ///
114    /// # Errors
115    ///
116    /// Rejects an invalid timebase or in-flight depth.
117    pub fn sequence(&self, config: SequenceConfig) -> Result<SequenceRenderer<D>, RenderError> {
118        config.validate()?;
119        config
120            .image
121            .validate(self.device.capabilities().max_texture_dim)?;
122        Ok(SequenceRenderer {
123            config,
124            pending: VecDeque::with_capacity(usize::from(config.max_in_flight)),
125            next_index: 0,
126            last_timestamp: None,
127        })
128    }
129
130    /// Submits one frame to an open sequence without waiting for readback.
131    ///
132    /// # Errors
133    ///
134    /// Returns backpressure at the configured depth, non-monotonic timestamps,
135    /// or a typed renderer or device error.
136    pub fn submit_sequence_frame(
137        &mut self,
138        sequence: &mut SequenceRenderer<D>,
139        scene: &Scene,
140        camera: &Camera,
141        timestamp: u64,
142    ) -> Result<FrameTicket, RenderError> {
143        sequence.submit(self, scene, camera, timestamp)
144    }
145
146    /// Resolves every pending frame of an open sequence in submission order.
147    ///
148    /// # Errors
149    ///
150    /// Returns device loss or image-layout failures.
151    pub fn finish_sequence(
152        &mut self,
153        sequence: SequenceRenderer<D>,
154    ) -> Result<Vec<SequenceFrame>, RenderError> {
155        sequence.finish(self)
156    }
157
158    /// Resolves the oldest pending frame when its submission has completed.
159    ///
160    /// # Errors
161    ///
162    /// Returns device loss or image-layout failures.
163    pub fn poll_sequence_frame(
164        &mut self,
165        sequence: &mut SequenceRenderer<D>,
166    ) -> Result<Option<SequenceFrame>, RenderError> {
167        sequence.poll(self)
168    }
169
170    /// Resolves the oldest pending frame, waiting for its submission.
171    ///
172    /// This is the blocking drain a producer uses when the pipeline is full:
173    /// it resolves exactly one frame in submission order and returns.
174    ///
175    /// # Errors
176    ///
177    /// Returns device loss or image-layout failures, and an invalid-sequence
178    /// error when nothing is pending.
179    pub fn drain_sequence_frame(
180        &mut self,
181        sequence: &mut SequenceRenderer<D>,
182    ) -> Result<SequenceFrame, RenderError> {
183        sequence.resolve_front(self)
184    }
185
186    /// Frames one sequence submission may leave unresolved.
187    #[must_use]
188    pub const fn sequence_in_flight(sequence: &SequenceRenderer<D>) -> u8 {
189        sequence.config.max_in_flight
190    }
191
192    /// Frames submitted to an open sequence that have not been resolved.
193    #[must_use]
194    pub fn pending_sequence_frames(sequence: &SequenceRenderer<D>) -> usize {
195        sequence.pending()
196    }
197}
198
199impl<D: Device> SequenceRenderer<D> {
200    /// Submits one frame without waiting for GPU readback.
201    ///
202    /// # Errors
203    ///
204    /// Returns backpressure at the configured depth and rejects non-monotonic
205    /// timestamps.
206    pub fn submit(
207        &mut self,
208        engine: &mut Engine<D>,
209        scene: &Scene,
210        camera: &Camera,
211        timestamp: u64,
212    ) -> Result<FrameTicket, RenderError> {
213        if self.pending.len() == usize::from(self.config.max_in_flight) {
214            return Err(RenderError::SequenceBackpressure {
215                max_in_flight: self.config.max_in_flight,
216            });
217        }
218        if self
219            .last_timestamp
220            .is_some_and(|previous| timestamp <= previous)
221        {
222            return Err(RenderError::InvalidSequence {
223                reason: "timestamps must increase strictly",
224            });
225        }
226        let ticket = FrameTicket {
227            index: self.next_index,
228            timestamp,
229        };
230        self.next_index = self
231            .next_index
232            .checked_add(1)
233            .ok_or(RenderError::InvalidSequence {
234                reason: "frame index exhausted",
235            })?;
236        let image = engine.render_image_to_buffer(
237            scene,
238            camera,
239            self.config.image,
240            ImagePurpose::SequenceFrame,
241        )?;
242        self.pending.push_back(PendingSequence { ticket, image });
243        self.last_timestamp = Some(timestamp);
244        Ok(ticket)
245    }
246
247    /// Resolves the oldest frame only when its tracked submission has completed.
248    ///
249    /// # Errors
250    ///
251    /// Returns device loss or image-layout failures.
252    pub fn poll(&mut self, engine: &mut Engine<D>) -> Result<Option<SequenceFrame>, RenderError> {
253        let Some(front) = self.pending.front() else {
254            return Ok(None);
255        };
256        let completed = engine.queue.completed_fence(&engine.device)?;
257        if completed < front.image.completion() {
258            return Ok(None);
259        }
260        self.resolve_front(engine).map(Some)
261    }
262
263    /// Drains every outstanding frame in submission order.
264    ///
265    /// # Errors
266    ///
267    /// Returns the first device or image-layout failure.
268    pub fn finish(mut self, engine: &mut Engine<D>) -> Result<Vec<SequenceFrame>, RenderError> {
269        let mut frames = Vec::with_capacity(self.pending.len());
270        while !self.pending.is_empty() {
271            frames.push(self.resolve_front(engine)?);
272        }
273        Ok(frames)
274    }
275
276    /// Number of submitted frames still owning readback buffers.
277    #[must_use]
278    pub fn pending(&self) -> usize {
279        self.pending.len()
280    }
281
282    fn resolve_front(&mut self, engine: &mut Engine<D>) -> Result<SequenceFrame, RenderError> {
283        let Some(pending) = self.pending.pop_front() else {
284            return Err(RenderError::InvalidSequence {
285                reason: "no sequence frame is pending",
286            });
287        };
288        let (buffer, size) = pending.image.readback();
289        let mapped = engine
290            .queue
291            .read_buffer_blocking(&engine.device, buffer, 0, size)?;
292        let image = pending.image.resolve(mapped, engine.target_format)?;
293        Ok(SequenceFrame {
294            ticket: pending.ticket,
295            image,
296        })
297    }
298}
299
300#[cfg(test)]
301#[path = "sequence_tests.rs"]
302mod tests;