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