Skip to main content

alpha2_engine/
player.rs

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/// Result of one real-time presentation attempt; `I` is [`FrameRenderer::Image`].
14#[derive(Debug)]
15pub struct PlaybackFrame<'a, I = RenderedTexture> {
16    /// Timeline time requested by the host clock.
17    pub requested_time: MediaTime,
18    /// Timeline time of the returned texture, absent before the first success.
19    pub presented_time: Option<MediaTime>,
20    /// Current or held picture. It remains owned by the player.
21    pub texture: Option<&'a I>,
22    /// Sources unavailable for the requested time.
23    pub missing_sources: Vec<SourceId>,
24    /// Whether the texture is the previous successful frame.
25    pub repeated: bool,
26    /// Whether the output equals the last one returned, so nothing was rendered and the host
27    /// need not present it again.
28    pub unchanged: bool,
29}
30
31/// Render size, frame-graph cache key and renderer content epoch of the last returned texture.
32type ShownKey = (RenderSize, RenderCacheKey, u64);
33
34/// Stateful real-time runner holding the last picture drawn. `I` is [`FrameRenderer::Image`]: a
35/// [`RenderedTexture`] on the GPU or an `Arc<CpuImage>` on the [`alpha2_render::CpuRenderer`].
36///
37/// Defaults to [`MissingFramePolicy::HoldPrevious`]; see [`Player::with_missing_frame_policy`].
38#[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    /// What `last_frame` shows, when its key names everything it depends on.
45    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    /// The size frames of `project` render at, for preparing captions.
62    ///
63    /// # Errors
64    /// Returns an error when the quality profile cannot size the project.
65    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    /// Creates a player with explicit runtime quality.
73    ///
74    /// # Errors
75    /// Returns [`EngineError::InvalidQualityProfile`] for invalid controls.
76    pub fn new(quality: QualityProfile) -> Result<Self, EngineError> {
77        quality.validate()?;
78        Ok(Self {
79            quality,
80            ..Self::default()
81        })
82    }
83
84    /// Sets how many future frame boundaries prefetch requests include, clamped to 120.
85    #[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    /// How many future frame boundaries prefetch requests include.
96    #[must_use]
97    pub const fn prefetch_frames(&self) -> u32 {
98        self.prefetch_frames
99    }
100
101    /// Returns the active quality profile.
102    #[must_use]
103    pub const fn quality(&self) -> QualityProfile {
104        self.quality
105    }
106
107    /// Sets what [`Player::render_at`] returns while source frames are missing.
108    ///
109    /// [`MissingFramePolicy::HoldPrevious`] repeats the last successful texture;
110    /// [`MissingFramePolicy::Wait`] withholds it so the host can show a loading state. The held
111    /// frame is retained either way.
112    #[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    /// Returns the policy used when source frames are unavailable.
119    #[must_use]
120    pub const fn missing_frame_policy(&self) -> MissingFramePolicy {
121        self.missing_frame_policy
122    }
123
124    /// Collects deduplicated decode requests for the current and future frames.
125    ///
126    /// # Errors
127    /// Returns evaluation or timeline overflow errors.
128    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    /// Evaluates and attempts to present the host clock's current frame.
153    ///
154    /// With inputs missing, the last successful texture is returned with `repeated = true` under
155    /// [`MissingFramePolicy::HoldPrevious`] and withheld under [`MissingFramePolicy::Wait`].
156    /// `texture` is absent before the first success.
157    ///
158    /// A frame whose size, cache key and content epoch match the last returned texture (with every
159    /// source keyed) is not rendered and returns `unchanged = true`. Unkeyed sources always render.
160    ///
161    /// # Errors
162    /// Returns timeline evaluation, quality, or rendering failures, and
163    /// [`EngineError::UnregisteredEffect`] naming an effect the frame draws
164    /// that `renderer` has not registered.
165    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                // Read after the render, which may itself have cleared the result cache.
196                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            // The held texture is this frame's too.
201            *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        // A hold is not this frame: the next complete one is presented, whatever its key.
213        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    /// Drops the held frame, for example after a seek or project swap.
234    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}