Skip to main content

alpha2_engine/
player.rs

1use alpha2_eval::FrameNeed;
2use alpha2_media::SourceId;
3use alpha2_render::{RenderedTexture, Renderer};
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.
14#[derive(Debug)]
15pub struct PlaybackFrame<'a> {
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 texture. It remains owned by the player.
21    pub texture: Option<&'a RenderedTexture>,
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}
27
28/// Stateful real-time runner.
29///
30/// [`MissingFramePolicy::HoldPrevious`] is the default; a host that would
31/// rather show its own loading state than a stale frame selects
32/// [`MissingFramePolicy::Wait`] through [`Player::with_missing_frame_policy`].
33#[derive(Debug)]
34pub struct Player {
35    quality: QualityProfile,
36    prefetch_frames: u32,
37    missing_frame_policy: MissingFramePolicy,
38    last_frame: Option<(MediaTime, RenderedTexture)>,
39}
40
41impl Default for Player {
42    fn default() -> Self {
43        Self {
44            quality: QualityProfile::preview(),
45            prefetch_frames: 2,
46            missing_frame_policy: MissingFramePolicy::HoldPrevious,
47            last_frame: None,
48        }
49    }
50}
51
52impl Player {
53    /// The size frames of `project` render at: where a host prepares their
54    /// captions.
55    ///
56    /// # Errors
57    /// Returns an error when the quality profile cannot size the project.
58    pub fn render_size(
59        &self,
60        project: &EngineProject,
61    ) -> Result<alpha2_render::RenderSize, EngineError> {
62        self.quality.render_size(&project.project().settings)
63    }
64
65    /// Creates a player with explicit runtime quality.
66    ///
67    /// # Errors
68    /// Returns [`EngineError::InvalidQualityProfile`] for invalid controls.
69    pub fn new(quality: QualityProfile) -> Result<Self, EngineError> {
70        quality.validate()?;
71        Ok(Self {
72            quality,
73            ..Self::default()
74        })
75    }
76
77    /// Sets how many future frame boundaries are included in prefetch requests.
78    /// Values above 120 are clamped to keep one request bounded.
79    #[must_use]
80    pub const fn with_prefetch_frames(mut self, frames: u32) -> Self {
81        self.prefetch_frames = if frames > MAX_PREFETCH_FRAMES {
82            MAX_PREFETCH_FRAMES
83        } else {
84            frames
85        };
86        self
87    }
88
89    /// Returns the active quality profile.
90    #[must_use]
91    pub const fn quality(&self) -> QualityProfile {
92        self.quality
93    }
94
95    /// Sets what [`Player::render_at`] returns while source frames are missing.
96    ///
97    /// [`MissingFramePolicy::HoldPrevious`] repeats the last successful
98    /// texture, which keeps real-time playback moving. [`MissingFramePolicy::Wait`]
99    /// withholds it, so the host can render its own loading state instead of
100    /// presenting a frame that belongs to another timestamp. The held frame is
101    /// retained either way and is presented again as soon as a render succeeds.
102    #[must_use]
103    pub const fn with_missing_frame_policy(mut self, policy: MissingFramePolicy) -> Self {
104        self.missing_frame_policy = policy;
105        self
106    }
107
108    /// Returns the policy used when source frames are unavailable.
109    #[must_use]
110    pub const fn missing_frame_policy(&self) -> MissingFramePolicy {
111        self.missing_frame_policy
112    }
113
114    /// Collects deduplicated decode requests for the current and future frames.
115    ///
116    /// # Errors
117    /// Returns evaluation or timeline overflow errors.
118    pub fn prefetch_needs(
119        &self,
120        project: &EngineProject,
121        time: MediaTime,
122    ) -> Result<Vec<FrameNeed>, EngineError> {
123        let rate = project.project().settings.frame_rate;
124        let first = rate
125            .frame_at_or_before(time)
126            .ok_or(EngineError::OutOfRange)?;
127        let mut needs = Vec::new();
128        for (_, frame_time) in
129            frame_times(rate, first, self.prefetch_frames.saturating_add(1), None)?
130        {
131            for need in project.frame_needs(frame_time)? {
132                if !needs.iter().any(|existing: &FrameNeed| {
133                    existing.source_id == need.source_id && existing.media_time == need.media_time
134                }) {
135                    needs.push(need);
136                }
137            }
138        }
139        Ok(needs)
140    }
141
142    /// Evaluates and attempts to present the host clock's current frame.
143    ///
144    /// When inputs are missing, the most recent successful texture is returned
145    /// with `repeated = true` under [`MissingFramePolicy::HoldPrevious`], and
146    /// withheld under [`MissingFramePolicy::Wait`]. Before the first successful
147    /// frame, `texture` is absent and the host can display its own loading
148    /// state.
149    ///
150    /// # Errors
151    /// Returns timeline evaluation, quality, or rendering failures, and
152    /// [`EngineError::UnregisteredEffect`] naming an effect the frame draws
153    /// that `renderer` has not registered.
154    pub fn render_at<'a>(
155        &'a mut self,
156        project: &EngineProject,
157        renderer: &Renderer,
158        frames: &dyn FrameProvider,
159        time: MediaTime,
160    ) -> Result<PlaybackFrame<'a>, EngineError> {
161        let frame = project.evaluate(time)?;
162        crate::frame::check_frame_effects(renderer, &frame)?;
163        let size = self.quality.render_size(&project.project().settings)?;
164        renderer.set_texture_budget_bytes(self.quality.texture_budget_bytes);
165        renderer.set_quality_controls(
166            self.quality.max_filter_samples,
167            self.quality.motion_blur_subframes,
168        );
169        let attempt = renderer.render_frame(size, &frame, frames)?;
170        if let Some(texture) = attempt.texture {
171            self.last_frame = Some((time, texture));
172            let (presented_time, texture) =
173                self.last_frame.as_ref().ok_or(EngineError::OutOfRange)?;
174            renderer.record_frame_outcome(true, false);
175            return Ok(PlaybackFrame {
176                requested_time: time,
177                presented_time: Some(*presented_time),
178                texture: Some(texture),
179                missing_sources: Vec::new(),
180                repeated: false,
181            });
182        }
183
184        let (presented_time, texture) = match self.missing_frame_policy {
185            MissingFramePolicy::HoldPrevious => self
186                .last_frame
187                .as_ref()
188                .map_or((None, None), |(time, texture)| (Some(*time), Some(texture))),
189            // The held frame stays owned by the player so the next successful render, or a policy
190            // change, can present it again.
191            MissingFramePolicy::Wait => (None, None),
192        };
193        renderer.record_frame_outcome(texture.is_some(), texture.is_some());
194        Ok(PlaybackFrame {
195            requested_time: time,
196            presented_time,
197            texture,
198            missing_sources: attempt.report.missing_sources,
199            repeated: texture.is_some(),
200        })
201    }
202
203    /// Drops the held frame, for example after a seek or project swap.
204    pub fn reset(&mut self) {
205        self.last_frame = None;
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn prefetch_configuration_is_bounded() {
215        let player = Player::default().with_prefetch_frames(u32::MAX);
216        assert_eq!(player.prefetch_frames, MAX_PREFETCH_FRAMES);
217    }
218
219    #[test]
220    fn hold_previous_is_the_default_missing_frame_policy() {
221        assert_eq!(
222            Player::default().missing_frame_policy(),
223            MissingFramePolicy::HoldPrevious
224        );
225    }
226
227    #[test]
228    fn the_missing_frame_policy_is_configurable() {
229        let player = Player::default().with_missing_frame_policy(MissingFramePolicy::Wait);
230        assert_eq!(player.missing_frame_policy(), MissingFramePolicy::Wait);
231        assert_eq!(
232            player
233                .with_missing_frame_policy(MissingFramePolicy::HoldPrevious)
234                .missing_frame_policy(),
235            MissingFramePolicy::HoldPrevious
236        );
237    }
238
239    #[test]
240    fn configuring_the_policy_preserves_quality_and_prefetch() {
241        let player = Player::new(QualityProfile::export())
242            .expect("valid quality")
243            .with_prefetch_frames(7)
244            .with_missing_frame_policy(MissingFramePolicy::Wait);
245        assert_eq!(player.quality(), QualityProfile::export());
246        assert_eq!(player.prefetch_frames, 7);
247        assert_eq!(player.missing_frame_policy(), MissingFramePolicy::Wait);
248    }
249}