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
131impl<D: Device> SequenceRenderer<D> {
132    /// Submits one frame without waiting for GPU readback.
133    ///
134    /// # Errors
135    ///
136    /// Returns backpressure at the configured depth and rejects non-monotonic
137    /// timestamps.
138    pub fn submit(
139        &mut self,
140        engine: &mut Engine<D>,
141        scene: &Scene,
142        camera: &Camera,
143        timestamp: u64,
144    ) -> Result<FrameTicket, RenderError> {
145        if self.pending.len() == usize::from(self.config.max_in_flight) {
146            return Err(RenderError::SequenceBackpressure {
147                max_in_flight: self.config.max_in_flight,
148            });
149        }
150        if self
151            .last_timestamp
152            .is_some_and(|previous| timestamp <= previous)
153        {
154            return Err(RenderError::InvalidSequence {
155                reason: "timestamps must increase strictly",
156            });
157        }
158        let ticket = FrameTicket {
159            index: self.next_index,
160            timestamp,
161        };
162        self.next_index = self
163            .next_index
164            .checked_add(1)
165            .ok_or(RenderError::InvalidSequence {
166                reason: "frame index exhausted",
167            })?;
168        let image = engine.render_image_to_buffer(
169            scene,
170            camera,
171            self.config.image,
172            ImagePurpose::SequenceFrame,
173        )?;
174        self.pending.push_back(PendingSequence { ticket, image });
175        self.last_timestamp = Some(timestamp);
176        Ok(ticket)
177    }
178
179    /// Resolves the oldest frame only when its tracked submission has completed.
180    ///
181    /// # Errors
182    ///
183    /// Returns device loss or image-layout failures.
184    pub fn poll(&mut self, engine: &mut Engine<D>) -> Result<Option<SequenceFrame>, RenderError> {
185        let Some(front) = self.pending.front() else {
186            return Ok(None);
187        };
188        let completed = engine.queue.completed_fence(&engine.device)?;
189        if completed < front.image.completion() {
190            return Ok(None);
191        }
192        self.resolve_front(engine).map(Some)
193    }
194
195    /// Drains every outstanding frame in submission order.
196    ///
197    /// # Errors
198    ///
199    /// Returns the first device or image-layout failure.
200    pub fn finish(mut self, engine: &mut Engine<D>) -> Result<Vec<SequenceFrame>, RenderError> {
201        let mut frames = Vec::with_capacity(self.pending.len());
202        while !self.pending.is_empty() {
203            frames.push(self.resolve_front(engine)?);
204        }
205        Ok(frames)
206    }
207
208    /// Number of submitted frames still owning readback buffers.
209    #[must_use]
210    pub fn pending(&self) -> usize {
211        self.pending.len()
212    }
213
214    fn resolve_front(&mut self, engine: &mut Engine<D>) -> Result<SequenceFrame, RenderError> {
215        let Some(pending) = self.pending.pop_front() else {
216            return Err(RenderError::InvalidSequence {
217                reason: "no sequence frame is pending",
218            });
219        };
220        let (buffer, size) = pending.image.readback();
221        let mapped = engine
222            .queue
223            .read_buffer_blocking(&engine.device, buffer, 0, size)?;
224        let image = pending.image.resolve(mapped, engine.target_format)?;
225        Ok(SequenceFrame {
226            ticket: pending.ticket,
227            image,
228        })
229    }
230}
231
232#[cfg(test)]
233#[path = "sequence_tests.rs"]
234mod tests;