1use alpha2_eval::FrameNeed;
2use alpha2_media::SourceId;
3use alpha2_render::{CacheEligibility, RenderCacheKey, RenderSize, RenderedTexture};
4use alpha2_time::MediaTime;
5
6use crate::schedule::frame_times;
7use crate::{
8 EngineError, EngineProject, FrameProvider, FrameRenderer, MissingFramePolicy, QualityProfile,
9};
10
11const MAX_PREFETCH_FRAMES: u32 = 120;
12
13#[derive(Debug)]
15pub struct PlaybackFrame<'a, I = RenderedTexture> {
16 pub requested_time: MediaTime,
18 pub presented_time: Option<MediaTime>,
20 pub texture: Option<&'a I>,
22 pub missing_sources: Vec<SourceId>,
24 pub repeated: bool,
26 pub unchanged: bool,
29}
30
31type ShownKey = (RenderSize, RenderCacheKey, u64);
33
34#[derive(Debug)]
39pub struct Player<I = RenderedTexture> {
40 quality: QualityProfile,
41 prefetch_frames: u32,
42 missing_frame_policy: MissingFramePolicy,
43 last_frame: Option<(MediaTime, I)>,
44 shown: Option<ShownKey>,
46}
47
48impl<I> Default for Player<I> {
49 fn default() -> Self {
50 Self {
51 quality: QualityProfile::preview(),
52 prefetch_frames: 2,
53 missing_frame_policy: MissingFramePolicy::HoldPrevious,
54 last_frame: None,
55 shown: None,
56 }
57 }
58}
59
60impl<I> Player<I> {
61 pub fn render_size(
66 &self,
67 project: &EngineProject,
68 ) -> Result<alpha2_render::RenderSize, EngineError> {
69 self.quality.render_size(&project.project().settings)
70 }
71
72 pub fn new(quality: QualityProfile) -> Result<Self, EngineError> {
77 quality.validate()?;
78 Ok(Self {
79 quality,
80 ..Self::default()
81 })
82 }
83
84 #[must_use]
86 pub const fn with_prefetch_frames(mut self, frames: u32) -> Self {
87 self.prefetch_frames = if frames > MAX_PREFETCH_FRAMES {
88 MAX_PREFETCH_FRAMES
89 } else {
90 frames
91 };
92 self
93 }
94
95 #[must_use]
97 pub const fn prefetch_frames(&self) -> u32 {
98 self.prefetch_frames
99 }
100
101 #[must_use]
103 pub const fn quality(&self) -> QualityProfile {
104 self.quality
105 }
106
107 #[must_use]
113 pub const fn with_missing_frame_policy(mut self, policy: MissingFramePolicy) -> Self {
114 self.missing_frame_policy = policy;
115 self
116 }
117
118 #[must_use]
120 pub const fn missing_frame_policy(&self) -> MissingFramePolicy {
121 self.missing_frame_policy
122 }
123
124 pub fn prefetch_needs(
129 &self,
130 project: &EngineProject,
131 time: MediaTime,
132 ) -> Result<Vec<FrameNeed>, EngineError> {
133 let rate = project.project().settings.frame_rate;
134 let first = rate
135 .frame_at_or_before(time)
136 .ok_or(EngineError::OutOfRange)?;
137 let mut needs = Vec::new();
138 for (_, frame_time) in
139 frame_times(rate, first, self.prefetch_frames.saturating_add(1), None)?
140 {
141 for need in project.frame_needs(frame_time)? {
142 if !needs.iter().any(|existing: &FrameNeed| {
143 existing.source_id == need.source_id && existing.media_time == need.media_time
144 }) {
145 needs.push(need);
146 }
147 }
148 }
149 Ok(needs)
150 }
151
152 pub fn render_at<'a, R: FrameRenderer<Image = I> + ?Sized>(
166 &'a mut self,
167 project: &EngineProject,
168 renderer: &R,
169 frames: &dyn FrameProvider,
170 time: MediaTime,
171 ) -> Result<PlaybackFrame<'a, I>, EngineError> {
172 let frame = project.evaluate(time)?;
173 crate::frame::check_frame_effects(renderer, &frame)?;
174 let size = self.quality.render_size(&project.project().settings)?;
175 renderer.set_texture_budget_bytes(self.quality.texture_budget_bytes);
176 renderer.set_quality_controls(
177 self.quality.max_filter_samples,
178 self.quality.motion_blur_subframes,
179 );
180 let built = renderer.build_frame_graph(size, &frame, frames)?;
181 if let Some(graph) = built.graph {
182 let key = match graph.output().map(|output| graph.cache_eligibility(output)) {
183 Some(Ok(CacheEligibility::Eligible)) => Some(
184 renderer
185 .graph_cache_key(&graph)
186 .map_err(alpha2_render::GraphError::into_render_error)?,
187 ),
188 _ => None,
189 };
190 let unchanged = self.last_frame.is_some()
191 && key.is_some_and(|key| self.shown == Some((size, key, renderer.content_epoch())));
192 if !unchanged {
193 let texture = renderer.render_frame_graph(&graph, &built.pending)?;
194 self.last_frame = Some((time, texture));
195 self.shown = key.map(|key| (size, key, renderer.content_epoch()));
197 }
198 let (presented_time, texture) =
199 self.last_frame.as_mut().ok_or(EngineError::OutOfRange)?;
200 *presented_time = time;
202 renderer.record_frame_outcome(true, false);
203 return Ok(PlaybackFrame {
204 requested_time: time,
205 presented_time: Some(*presented_time),
206 texture: Some(texture),
207 missing_sources: Vec::new(),
208 repeated: false,
209 unchanged,
210 });
211 }
212 self.shown = None;
214
215 let (presented_time, texture) = match self.missing_frame_policy {
216 MissingFramePolicy::HoldPrevious => self
217 .last_frame
218 .as_ref()
219 .map_or((None, None), |(time, texture)| (Some(*time), Some(texture))),
220 MissingFramePolicy::Wait => (None, None),
221 };
222 renderer.record_frame_outcome(texture.is_some(), texture.is_some());
223 Ok(PlaybackFrame {
224 requested_time: time,
225 presented_time,
226 texture,
227 missing_sources: built.report.missing_sources,
228 repeated: texture.is_some(),
229 unchanged: false,
230 })
231 }
232
233 pub fn reset(&mut self) {
235 self.last_frame = None;
236 self.shown = None;
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn prefetch_configuration_is_bounded() {
246 let player = Player::<RenderedTexture>::default().with_prefetch_frames(u32::MAX);
247 assert_eq!(player.prefetch_frames, MAX_PREFETCH_FRAMES);
248 }
249
250 #[test]
251 fn hold_previous_is_the_default_missing_frame_policy() {
252 assert_eq!(
253 Player::<RenderedTexture>::default().missing_frame_policy(),
254 MissingFramePolicy::HoldPrevious
255 );
256 }
257
258 #[test]
259 fn the_missing_frame_policy_is_configurable() {
260 let player = Player::<RenderedTexture>::default()
261 .with_missing_frame_policy(MissingFramePolicy::Wait);
262 assert_eq!(player.missing_frame_policy(), MissingFramePolicy::Wait);
263 assert_eq!(
264 player
265 .with_missing_frame_policy(MissingFramePolicy::HoldPrevious)
266 .missing_frame_policy(),
267 MissingFramePolicy::HoldPrevious
268 );
269 }
270
271 #[test]
272 fn configuring_the_policy_preserves_quality_and_prefetch() {
273 let player = Player::<RenderedTexture>::new(QualityProfile::export())
274 .expect("valid quality")
275 .with_prefetch_frames(7)
276 .with_missing_frame_policy(MissingFramePolicy::Wait);
277 assert_eq!(player.quality(), QualityProfile::export());
278 assert_eq!(player.prefetch_frames, 7);
279 assert_eq!(player.missing_frame_policy(), MissingFramePolicy::Wait);
280 }
281}