1use 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#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub struct SequenceConfig {
15 pub image: ImageConfig,
17 pub timebase_nanoseconds: u64,
19 pub max_in_flight: u8,
21}
22
23impl SequenceConfig {
24 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#[derive(Clone, Copy, PartialEq, Eq, Debug)]
67pub struct FrameTicket {
68 pub index: u64,
70 pub timestamp: u64,
72}
73
74#[derive(Clone, PartialEq, Eq, Debug)]
76pub struct SequenceFrame {
77 pub ticket: FrameTicket,
79 pub image: Image,
81}
82
83struct PendingSequence<D: Device> {
84 ticket: FrameTicket,
85 image: PendingImage<D>,
86}
87
88pub 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 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 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 pub fn finish_sequence(
152 &mut self,
153 sequence: SequenceRenderer<D>,
154 ) -> Result<Vec<SequenceFrame>, RenderError> {
155 sequence.finish(self)
156 }
157
158 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 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 #[must_use]
188 pub const fn sequence_in_flight(sequence: &SequenceRenderer<D>) -> u8 {
189 sequence.config.max_in_flight
190 }
191
192 #[must_use]
194 pub fn pending_sequence_frames(sequence: &SequenceRenderer<D>) -> usize {
195 sequence.pending()
196 }
197}
198
199impl<D: Device> SequenceRenderer<D> {
200 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 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 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 #[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;