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