Skip to main content

alpha2_render/
graph_submit.rs

1use std::time::Duration;
2use web_time::Instant;
3
4use crate::yuv::SourceWorkTicket;
5use crate::{
6    GraphError, PendingSourceWork, RenderError, RenderGraph, RenderedTexture, Renderer,
7    Yuv420pReadback,
8};
9use alpha2_media::YuvEncoding;
10
11/// Where one frame's graph construction and submission spent its time.
12///
13/// The fields are consecutive spans of one frame on one thread, so they sum to
14/// it. A host that reports a single submit cost cannot tell a slow graph from
15/// a contended queue; these separate the two.
16///
17/// `graph_build` belongs to whoever built the [`RenderGraph`]; the renderer's
18/// timed submissions write every later span and leave it as they found it.
19#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
20pub struct FrameSubmitTimings {
21    /// Effect validation, source availability, and render-graph construction.
22    pub graph_build: Duration,
23    /// Encoding source conversion, graph, and readback commands.
24    pub command_encode: Duration,
25    /// Native acquire and layout transition for imported decoder surfaces.
26    pub source_acquire: Duration,
27    /// Blocked on the queue lock shared with the native decoder.
28    pub queue_lock_wait: Duration,
29    /// Inside `wgpu`'s queue submit.
30    pub queue_submit: Duration,
31    /// Releasing decoder surfaces whose submission has completed.
32    pub lease_retire: Duration,
33    /// Building the readback handle and returning pooled textures.
34    pub complete: Duration,
35}
36
37impl FrameSubmitTimings {
38    /// Adds another call's spans to these, saturating on overflow.
39    #[must_use]
40    pub fn merged(self, other: Self) -> Self {
41        Self {
42            graph_build: self.graph_build.saturating_add(other.graph_build),
43            command_encode: self.command_encode.saturating_add(other.command_encode),
44            source_acquire: self.source_acquire.saturating_add(other.source_acquire),
45            queue_lock_wait: self.queue_lock_wait.saturating_add(other.queue_lock_wait),
46            queue_submit: self.queue_submit.saturating_add(other.queue_submit),
47            lease_retire: self.lease_retire.saturating_add(other.lease_retire),
48            complete: self.complete.saturating_add(other.complete),
49        }
50    }
51
52    /// Replaces every span after graph construction with `later`'s.
53    const fn with_submission(self, later: Self) -> Self {
54        Self {
55            graph_build: self.graph_build,
56            ..later
57        }
58    }
59}
60
61/// A lap timer that reads no clock unless a caller asked for timings.
62#[derive(Copy, Clone)]
63struct StageClock(Option<Instant>);
64
65impl StageClock {
66    fn start(enabled: bool) -> Self {
67        Self(enabled.then(Instant::now))
68    }
69
70    /// Returns the span since the previous lap and starts a new one.
71    fn lap(&mut self) -> Duration {
72        let Some(mark) = self.0 else {
73            return Duration::ZERO;
74        };
75        let now = Instant::now();
76        self.0 = Some(now);
77        now.duration_since(mark)
78    }
79}
80
81impl Renderer {
82    /// Renders a graph and folds its sources' pending presentation work into
83    /// the same queue submission.
84    ///
85    /// `sources` are the one-shot presentation passes behind the graph's
86    /// source textures, in the order they should be encoded. Work another
87    /// consumer already submitted is skipped.
88    ///
89    /// # Errors
90    /// Returns [`RenderError`] when the graph or a render target is invalid.
91    pub fn render_graph(
92        &self,
93        graph: &RenderGraph<'_>,
94        sources: &[&PendingSourceWork],
95    ) -> Result<RenderedTexture, RenderError> {
96        // Held past the tickets, which go back to pending if the frame fails.
97        let frame = self.frame_lock.hold();
98        let mut command_encoder = self.command_encoder("alpha2 frame encoder");
99        let source_work = begin_source_work(&frame, sources);
100        for work in &source_work {
101            work.encode(&mut command_encoder);
102        }
103        let graph_work = self
104            .encode_graph(graph, &mut command_encoder)
105            .map_err(GraphError::into_render_error)?;
106        stage_source_submissions(self.gpu.queue(), &source_work)?;
107        graph_work.flush_uniforms(self.gpu.queue());
108        // A rejected submission returns here, before anything of the frame is kept.
109        self.submit([command_encoder.finish()])?;
110        let combined_submission = !source_work.is_empty();
111        for work in source_work {
112            work.complete(self, false);
113        }
114        let texture = self.complete_graph_work(graph_work, !combined_submission);
115        if combined_submission {
116            crate::diagnostics::record_frame_submission(&self.diagnostics);
117        }
118        Ok(texture)
119    }
120
121    /// Renders a graph, converts it to YUV420, and submits both workloads and
122    /// its sources' pending presentation work in one command buffer.
123    ///
124    /// The graph's output size must be even for chroma subsampling.
125    ///
126    /// # Errors
127    /// Returns [`RenderError`] for invalid graph, target, or YUV layout data.
128    pub fn render_graph_yuv420p(
129        &self,
130        graph: &RenderGraph<'_>,
131        sources: &[&PendingSourceWork],
132        encoding: YuvEncoding,
133    ) -> Result<Yuv420pReadback, RenderError> {
134        self.render_graph_yuv420p_measured(graph, sources, encoding, None)
135    }
136
137    /// Renders a graph for YUV420 readback and reports where the call went.
138    ///
139    /// This produces exactly what [`Renderer::render_graph_yuv420p`] produces;
140    /// the only difference is that it reads a clock between stages and writes
141    /// every span after `graph_build` to `timings`.
142    ///
143    /// # Errors
144    /// Returns [`RenderError`] for invalid graph, target, or YUV layout data.
145    pub fn render_graph_yuv420p_timed(
146        &self,
147        graph: &RenderGraph<'_>,
148        sources: &[&PendingSourceWork],
149        encoding: YuvEncoding,
150        timings: &mut FrameSubmitTimings,
151    ) -> Result<Yuv420pReadback, RenderError> {
152        self.render_graph_yuv420p_measured(graph, sources, encoding, Some(timings))
153    }
154
155    /// Renders a graph and packs it straight into caller-owned NV12 plane
156    /// textures, with no readback.
157    ///
158    /// This is [`Renderer::render_graph_yuv420p_timed`] with the staging
159    /// buffer removed: the same sources, the same graph and the same
160    /// conversion, ending in two textures a hardware encoder can be fed from
161    /// instead of a mapped buffer.
162    ///
163    /// # Errors
164    /// Returns [`RenderError`] for invalid graph or plane-target data.
165    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
166    pub fn render_graph_nv12_timed(
167        &self,
168        graph: &RenderGraph<'_>,
169        sources: &[&PendingSourceWork],
170        planes: crate::Nv12PlaneTargets<'_>,
171        encoding: YuvEncoding,
172        timings: &mut FrameSubmitTimings,
173    ) -> Result<(), RenderError> {
174        let mut clock = StageClock::start(true);
175        let mut collected = FrameSubmitTimings::default();
176        let frame = self.frame_lock.hold();
177        let mut command_encoder = self.command_encoder("alpha2 render and NV12 pack encoder");
178        self.check_submitted_work()?;
179        let source_work = begin_source_work(&frame, sources);
180        for work in &source_work {
181            work.encode(&mut command_encoder);
182        }
183        let graph_work = self
184            .encode_graph(graph, &mut command_encoder)
185            .map_err(GraphError::into_render_error)?;
186        let pack =
187            self.encode_nv12_pack(graph_work.texture(), planes, encoding, &mut command_encoder)?;
188        // Nothing reads this frame back, so its GPU proof travels in a buffer of its own.
189        let (canary_buffer, canary) = self.canaries.encode_own(&self.gpu, &mut command_encoder)?;
190        collected.command_encode = clock.lap();
191        stage_source_submissions(self.gpu.queue(), &source_work)?;
192        collected.source_acquire = clock.lap();
193        graph_work.flush_uniforms(self.gpu.queue());
194        let command_buffer = command_encoder.finish();
195        collected.command_encode = collected.command_encode.saturating_add(clock.lap());
196        // A rejected submission wrote no plane and no canary: the frame fails here, untracked.
197        let submitted = self.submit_measured([command_buffer], true)?;
198        self.submission_canaries
199            .track(&self.gpu, canary_buffer, canary);
200        drop(pack);
201        collected.queue_lock_wait = submitted.timings.queue_lock_wait;
202        collected.queue_submit = submitted.timings.queue_submit;
203        collected.lease_retire = submitted.lease_retire;
204        let _ = clock.lap();
205        for work in source_work {
206            work.complete(self, false);
207        }
208        drop(self.complete_graph_work(graph_work, false));
209        crate::diagnostics::record_frame_submission(&self.diagnostics);
210        collected.complete = clock.lap();
211        *timings = timings.with_submission(collected);
212        Ok(())
213    }
214
215    fn render_graph_yuv420p_measured(
216        &self,
217        graph: &RenderGraph<'_>,
218        sources: &[&PendingSourceWork],
219        encoding: YuvEncoding,
220        timings: Option<&mut FrameSubmitTimings>,
221    ) -> Result<Yuv420pReadback, RenderError> {
222        let measure = timings.is_some();
223        let mut clock = StageClock::start(measure);
224        let mut collected = FrameSubmitTimings::default();
225        let frame = self.frame_lock.hold();
226        let mut command_encoder =
227            self.command_encoder("alpha2 render and YUV420P readback encoder");
228        let source_work = begin_source_work(&frame, sources);
229        for work in &source_work {
230            work.encode(&mut command_encoder);
231        }
232        let graph_work = self
233            .encode_graph(graph, &mut command_encoder)
234            .map_err(GraphError::into_render_error)?;
235        let readback_work =
236            self.encode_yuv420p_readback(graph_work.texture(), encoding, &mut command_encoder)?;
237        collected.command_encode = clock.lap();
238        stage_source_submissions(self.gpu.queue(), &source_work)?;
239        collected.source_acquire = clock.lap();
240        graph_work.flush_uniforms(self.gpu.queue());
241        readback_work.flush_uniforms(self.gpu.queue());
242        let command_buffer = command_encoder.finish();
243        // Closing the encoder and flushing uniforms belong to the command-encoding span.
244        collected.command_encode = collected.command_encode.saturating_add(clock.lap());
245        let submitted = self.submit_measured([command_buffer], measure)?;
246        collected.queue_lock_wait = submitted.timings.queue_lock_wait;
247        collected.queue_submit = submitted.timings.queue_submit;
248        collected.lease_retire = submitted.lease_retire;
249        // The submission measured itself; discard this lap rather than count it twice.
250        let _ = clock.lap();
251        let readback = self.complete_yuv420p_readback(
252            readback_work,
253            #[cfg(not(target_arch = "wasm32"))]
254            submitted.index,
255            false,
256        );
257        for work in source_work {
258            work.complete(self, false);
259        }
260        drop(self.complete_graph_work(graph_work, false));
261        crate::diagnostics::record_frame_submission(&self.diagnostics);
262        collected.complete = clock.lap();
263        if let Some(timings) = timings {
264            *timings = timings.with_submission(collected);
265        }
266        Ok(readback)
267    }
268}
269
270/// Flushes source uniforms and stages every external texture's native waits and
271/// signals on the queue submission that is about to sample it.
272///
273/// Every graph submission must run this before `queue.submit`. Skipping it on
274/// one path would let the renderer sample a decoder surface that has neither
275/// been waited on nor transitioned out of its decode layout.
276pub(crate) fn stage_source_submissions(
277    queue: &wgpu::Queue,
278    source_work: &[SourceWorkTicket<'_>],
279) -> Result<(), RenderError> {
280    debug_assert!(
281        source_work
282            .iter()
283            .all(SourceWorkTicket::has_no_staged_signals),
284        "a source lease carried a staged native signal into the next frame"
285    );
286    for work in source_work {
287        work.flush_uniforms(queue);
288        if !work.prepare_submission() {
289            return Err(RenderError::ExternalTextureSync(None));
290        }
291    }
292    // Staging is split from flushing so one native submission carries every source.
293    for work in source_work {
294        work.flush_submission()?;
295    }
296    Ok(())
297}
298
299/// Takes the pending work of `sources` that no other frame has taken.
300///
301/// Only under the frame lock (`frame`), held until the tickets are completed
302/// or dropped: a source whose work is taken is then either submitted or back
303/// to pending by the time another thread's frame looks at it, so "taken"
304/// means "submitted ahead of this frame" and never "being encoded".
305pub(crate) fn begin_source_work<'a>(
306    frame: &crate::renderer::FrameGuard,
307    sources: &[&'a PendingSourceWork],
308) -> Vec<SourceWorkTicket<'a>> {
309    sources
310        .iter()
311        .filter_map(|pending| pending.begin(frame))
312        .collect()
313}