Skip to main content

alpha2_engine/
player.rs

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