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;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct SequenceConfig {
pub image: ImageConfig,
pub timebase_nanoseconds: u64,
pub max_in_flight: u8,
}
impl SequenceConfig {
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(())
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct FrameTicket {
pub index: u64,
pub timestamp: u64,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct SequenceFrame {
pub ticket: FrameTicket,
pub image: Image,
}
struct PendingSequence<D: Device> {
ticket: FrameTicket,
image: PendingImage<D>,
}
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> {
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,
})
}
}
impl<D: Device> SequenceRenderer<D> {
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)
}
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)
}
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)
}
#[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;