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, with `I` the picture the
14/// renderer draws ([`FrameRenderer::Image`]).
15#[derive(Debug)]
16pub struct PlaybackFrame<'a, I = RenderedTexture> {
17    /// Timeline time requested by the host clock.
18    pub requested_time: MediaTime,
19    /// Timeline time of the returned texture, absent before the first success.
20    pub presented_time: Option<MediaTime>,
21    /// Current or held picture. It remains owned by the player.
22    pub texture: Option<&'a I>,
23    /// Sources unavailable for the requested time.
24    pub missing_sources: Vec<SourceId>,
25    /// Whether the texture is the previous successful frame.
26    pub repeated: bool,
27    /// Whether the frame's output equals the one last returned, so nothing
28    /// was rendered: a host that presented that texture need not present it
29    /// again.
30    pub unchanged: bool,
31}
32
33/// What the texture a player last returned shows: the size, the frame
34/// graph's cache key and the renderer's content epoch it was rendered at.
35type ShownKey = (RenderSize, RenderCacheKey, u64);
36
37/// Stateful real-time runner, holding the last picture its renderer drew:
38/// `I` is [`FrameRenderer::Image`], a [`RenderedTexture`] on the GPU and an
39/// `Arc<CpuImage>` on the [`alpha2_render::CpuRenderer`].
40///
41/// [`MissingFramePolicy::HoldPrevious`] is the default; a host that would
42/// rather show its own loading state than a stale frame selects
43/// [`MissingFramePolicy::Wait`] through [`Player::with_missing_frame_policy`].
44#[derive(Debug)]
45pub struct Player<I = RenderedTexture> {
46    quality: QualityProfile,
47    prefetch_frames: u32,
48    missing_frame_policy: MissingFramePolicy,
49    last_frame: Option<(MediaTime, I)>,
50    /// What `last_frame` shows, when its key names everything it depends on.
51    shown: Option<ShownKey>,
52}
53
54impl<I> Default for Player<I> {
55    fn default() -> Self {
56        Self {
57            quality: QualityProfile::preview(),
58            prefetch_frames: 2,
59            missing_frame_policy: MissingFramePolicy::HoldPrevious,
60            last_frame: None,
61            shown: None,
62        }
63    }
64}
65
66impl<I> Player<I> {
67    /// The size frames of `project` render at: where a host prepares their
68    /// captions.
69    ///
70    /// # Errors
71    /// Returns an error when the quality profile cannot size the project.
72    pub fn render_size(
73        &self,
74        project: &EngineProject,
75    ) -> Result<alpha2_render::RenderSize, EngineError> {
76        self.quality.render_size(&project.project().settings)
77    }
78
79    /// Creates a player with explicit runtime quality.
80    ///
81    /// # Errors
82    /// Returns [`EngineError::InvalidQualityProfile`] for invalid controls.
83    pub fn new(quality: QualityProfile) -> Result<Self, EngineError> {
84        quality.validate()?;
85        Ok(Self {
86            quality,
87            ..Self::default()
88        })
89    }
90
91    /// Sets how many future frame boundaries are included in prefetch requests.
92    /// Values above 120 are clamped to keep one request bounded.
93    #[must_use]
94    pub const fn with_prefetch_frames(mut self, frames: u32) -> Self {
95        self.prefetch_frames = if frames > MAX_PREFETCH_FRAMES {
96            MAX_PREFETCH_FRAMES
97        } else {
98            frames
99        };
100        self
101    }
102
103    /// How many future frame boundaries prefetch requests include.
104    #[must_use]
105    pub const fn prefetch_frames(&self) -> u32 {
106        self.prefetch_frames
107    }
108
109    /// Returns the active quality profile.
110    #[must_use]
111    pub const fn quality(&self) -> QualityProfile {
112        self.quality
113    }
114
115    /// Sets what [`Player::render_at`] returns while source frames are missing.
116    ///
117    /// [`MissingFramePolicy::HoldPrevious`] repeats the last successful
118    /// texture, which keeps real-time playback moving. [`MissingFramePolicy::Wait`]
119    /// withholds it, so the host can render its own loading state instead of
120    /// presenting a frame that belongs to another timestamp. The held frame is
121    /// retained either way and is presented again as soon as a render succeeds.
122    #[must_use]
123    pub const fn with_missing_frame_policy(mut self, policy: MissingFramePolicy) -> Self {
124        self.missing_frame_policy = policy;
125        self
126    }
127
128    /// Returns the policy used when source frames are unavailable.
129    #[must_use]
130    pub const fn missing_frame_policy(&self) -> MissingFramePolicy {
131        self.missing_frame_policy
132    }
133
134    /// Collects deduplicated decode requests for the current and future frames.
135    ///
136    /// # Errors
137    /// Returns evaluation or timeline overflow errors.
138    pub fn prefetch_needs(
139        &self,
140        project: &EngineProject,
141        time: MediaTime,
142    ) -> Result<Vec<FrameNeed>, EngineError> {
143        let rate = project.project().settings.frame_rate;
144        let first = rate
145            .frame_at_or_before(time)
146            .ok_or(EngineError::OutOfRange)?;
147        let mut needs = Vec::new();
148        for (_, frame_time) in
149            frame_times(rate, first, self.prefetch_frames.saturating_add(1), None)?
150        {
151            for need in project.frame_needs(frame_time)? {
152                if !needs.iter().any(|existing: &FrameNeed| {
153                    existing.source_id == need.source_id && existing.media_time == need.media_time
154                }) {
155                    needs.push(need);
156                }
157            }
158        }
159        Ok(needs)
160    }
161
162    /// Evaluates and attempts to present the host clock's current frame.
163    ///
164    /// When inputs are missing, the most recent successful texture is returned
165    /// with `repeated = true` under [`MissingFramePolicy::HoldPrevious`], and
166    /// withheld under [`MissingFramePolicy::Wait`]. Before the first successful
167    /// frame, `texture` is absent and the host can display its own loading
168    /// state.
169    ///
170    /// A frame whose output equals the texture last returned (the same render
171    /// size, frame-graph cache key and renderer content epoch, with every
172    /// source keyed) is not rendered: that texture is returned again with
173    /// `unchanged = true`, and no GPU work is encoded. A frame drawn from a
174    /// source without a stable key always renders.
175    ///
176    /// # Errors
177    /// Returns timeline evaluation, quality, or rendering failures, and
178    /// [`EngineError::UnregisteredEffect`] naming an effect the frame draws
179    /// that `renderer` has not registered.
180    pub fn render_at<'a, R: FrameRenderer<Image = I> + ?Sized>(
181        &'a mut self,
182        project: &EngineProject,
183        renderer: &R,
184        frames: &dyn FrameProvider,
185        time: MediaTime,
186    ) -> Result<PlaybackFrame<'a, I>, EngineError> {
187        let frame = project.evaluate(time)?;
188        crate::frame::check_frame_effects(renderer, &frame)?;
189        let size = self.quality.render_size(&project.project().settings)?;
190        renderer.set_texture_budget_bytes(self.quality.texture_budget_bytes);
191        renderer.set_quality_controls(
192            self.quality.max_filter_samples,
193            self.quality.motion_blur_subframes,
194        );
195        let built = renderer.build_frame_graph(size, &frame, frames)?;
196        if let Some(graph) = built.graph {
197            let key = match graph.output().map(|output| graph.cache_eligibility(output)) {
198                Some(Ok(CacheEligibility::Eligible)) => Some(
199                    renderer
200                        .graph_cache_key(&graph)
201                        .map_err(alpha2_render::GraphError::into_render_error)?,
202                ),
203                _ => None,
204            };
205            let unchanged = self.last_frame.is_some()
206                && key.is_some_and(|key| self.shown == Some((size, key, renderer.content_epoch())));
207            if !unchanged {
208                let texture = renderer.render_frame_graph(&graph, &built.pending)?;
209                self.last_frame = Some((time, texture));
210                // Read after the render, which may itself have cleared the result cache.
211                self.shown = key.map(|key| (size, key, renderer.content_epoch()));
212            }
213            let (presented_time, texture) =
214                self.last_frame.as_mut().ok_or(EngineError::OutOfRange)?;
215            // The held texture is this frame's too.
216            *presented_time = time;
217            renderer.record_frame_outcome(true, false);
218            return Ok(PlaybackFrame {
219                requested_time: time,
220                presented_time: Some(*presented_time),
221                texture: Some(texture),
222                missing_sources: Vec::new(),
223                repeated: false,
224                unchanged,
225            });
226        }
227        // A hold is not this frame: the next complete one is presented, whatever its key.
228        self.shown = None;
229
230        let (presented_time, texture) = match self.missing_frame_policy {
231            MissingFramePolicy::HoldPrevious => self
232                .last_frame
233                .as_ref()
234                .map_or((None, None), |(time, texture)| (Some(*time), Some(texture))),
235            // The held frame stays owned by the player so the next successful render, or a policy
236            // change, can present it again.
237            MissingFramePolicy::Wait => (None, None),
238        };
239        renderer.record_frame_outcome(texture.is_some(), texture.is_some());
240        Ok(PlaybackFrame {
241            requested_time: time,
242            presented_time,
243            texture,
244            missing_sources: built.report.missing_sources,
245            repeated: texture.is_some(),
246            unchanged: false,
247        })
248    }
249
250    /// Drops the held frame, for example after a seek or project swap.
251    pub fn reset(&mut self) {
252        self.last_frame = None;
253        self.shown = None;
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn prefetch_configuration_is_bounded() {
263        let player = Player::<RenderedTexture>::default().with_prefetch_frames(u32::MAX);
264        assert_eq!(player.prefetch_frames, MAX_PREFETCH_FRAMES);
265    }
266
267    #[test]
268    fn hold_previous_is_the_default_missing_frame_policy() {
269        assert_eq!(
270            Player::<RenderedTexture>::default().missing_frame_policy(),
271            MissingFramePolicy::HoldPrevious
272        );
273    }
274
275    #[test]
276    fn the_missing_frame_policy_is_configurable() {
277        let player = Player::<RenderedTexture>::default()
278            .with_missing_frame_policy(MissingFramePolicy::Wait);
279        assert_eq!(player.missing_frame_policy(), MissingFramePolicy::Wait);
280        assert_eq!(
281            player
282                .with_missing_frame_policy(MissingFramePolicy::HoldPrevious)
283                .missing_frame_policy(),
284            MissingFramePolicy::HoldPrevious
285        );
286    }
287
288    #[test]
289    fn configuring_the_policy_preserves_quality_and_prefetch() {
290        let player = Player::<RenderedTexture>::new(QualityProfile::export())
291            .expect("valid quality")
292            .with_prefetch_frames(7)
293            .with_missing_frame_policy(MissingFramePolicy::Wait);
294        assert_eq!(player.quality(), QualityProfile::export());
295        assert_eq!(player.prefetch_frames, 7);
296        assert_eq!(player.missing_frame_policy(), MissingFramePolicy::Wait);
297    }
298}