Skip to main content

alpha2_render/
renderer.rs

1use crate::{BlendSpace, Color, RenderError, RenderSize, RenderedTexture};
2use alpha2_gpu::GpuContext;
3use alpha2_gpu::IntermediateFormat;
4use alpha2_gpu::SubmitTimings;
5use std::sync::{Arc, Mutex};
6use std::time::Duration;
7
8/// Renders graphs of sources, effects, transitions and composites on one GPU
9/// device, and owns the pipelines, pools and caches they use.
10#[derive(Debug)]
11pub struct Renderer {
12    pub(crate) gpu: GpuContext,
13    pub(crate) blend_space: BlendSpace,
14    pub(crate) effects: crate::registry::EffectRegistry,
15    pub(crate) effect_resources: crate::effect_resource::EffectResourceRegistry,
16    pub(crate) textures: crate::Shared<crate::pool::TexturePool>,
17    pub(crate) upload_textures: crate::Shared<crate::pool::UploadTexturePool>,
18    pub(crate) readback_buffers: crate::Shared<crate::pool::ReadbackBufferPool>,
19    pub(crate) uniform_buffers: crate::Shared<crate::uniform::UniformBufferPool>,
20    /// Destroys what the pools let go of, once no encode uses it.
21    pub(crate) retirement: crate::Shared<crate::retire::Retirement>,
22    pub(crate) pipelines: crate::pipeline::PipelineCache,
23    pub(crate) diagnostics: crate::diagnostics::SharedDiagnostics,
24    pub(crate) quality: Mutex<QualityState>,
25    pub(crate) result_cache: Mutex<crate::result_cache::ResultCache>,
26    pub(crate) timestamp_probe: std::sync::OnceLock<Option<crate::runtime_effect::TimestampProbe>>,
27    pub(crate) pass_timing: std::sync::atomic::AtomicBool,
28    /// Test-only: composite every layer as a full-frame pass, culling
29    /// nothing, as the reference the bounded passes must match.
30    #[cfg(test)]
31    pub(crate) full_frame_layers: std::sync::atomic::AtomicBool,
32    /// Test-only: the most layers a composite pass takes when nonzero, so
33    /// one layer per pass is the reference batched passes must match. A
34    /// nonzero limit composites as a GPU does on any device, small layers
35    /// with the loop pipeline.
36    #[cfg(test)]
37    pub(crate) composite_batch_limit: std::sync::atomic::AtomicUsize,
38    /// Test-only: the pipeline each composite pass drew with, and how many
39    /// composite pipelines were created.
40    #[cfg(test)]
41    pub(crate) composite_pipelines: Mutex<crate::composite::CompositePipelineLog>,
42    pub(crate) retired_leases: Arc<crate::yuv::RetiredLeases>,
43    pub(crate) gpu_wait_timeout: Duration,
44    /// The device's limits, read once: they never change for a device, and
45    /// on the browser backend every read crosses into JavaScript per field.
46    pub(crate) limits: wgpu::Limits,
47    /// The device's enabled features, read once for the same reason.
48    pub(crate) features: wgpu::Features,
49    /// The texture bound to a generator's unused image inputs.
50    pub(crate) empty_input: wgpu::TextureView,
51    /// What each YUV readback's staging buffer must end in, and whether one
52    /// did not.
53    pub(crate) canaries: crate::canary::Canaries,
54    /// The canaries of submitted NV12 packs and repacks not checked yet.
55    pub(crate) submission_canaries: crate::canary::SubmissionCanaries,
56    /// GPU errors the embedding saw outside the renderer's own scopes.
57    pub(crate) gpu_errors: crate::GpuErrorReports,
58    /// Held by each renderer encoder until it is submitted.
59    pub(crate) frame_lock: crate::Shared<FrameLock>,
60}
61
62/// A command encoder of this renderer's, open until its commands are
63/// submitted. Pooled resources it uses that are let go of meanwhile are
64/// destroyed only after that (see [`crate::retire`]), so every encoder the
65/// renderer records into is one of these.
66///
67/// It also holds the validation and out-of-memory scopes that its
68/// submission closes (see [`crate::gpu_errors`]), so it stays on the thread
69/// that created it.
70///
71/// And it holds the renderer's frame lock (see [`FrameLock`]) from creation
72/// until that submission.
73#[derive(Debug)]
74pub(crate) struct RenderEncoder {
75    encoder: wgpu::CommandEncoder,
76    errors: crate::gpu_errors::FrameErrorScope,
77    scope: crate::retire::EncodeScope,
78    frame: FrameGuard,
79}
80
81/// Lets one thread at a time record and submit renderer work.
82///
83/// A graph hands each intermediate back to the texture pool as soon as its
84/// last reader is encoded, so a later pass of the same frame can reuse it.
85/// The passes that wrote it are still in the unsubmitted encoder. A renderer
86/// is `Sync` natively, and another thread that took that texture, drew into
87/// it and submitted first would have its kept picture overwritten when the
88/// first frame's submission ran. Holding this lock from an encoder's
89/// creation until its submission keeps every other thread's submission
90/// after it, so the reuse stays within the frame that released the texture.
91///
92/// It is re-entrant: the thread that holds it may open another renderer
93/// encoder. Natively renders on one renderer from several threads queue
94/// here; they share one queue, so their GPU work was ordered anyway.
95#[derive(Debug, Default)]
96pub(crate) struct FrameLock {
97    owner: Mutex<Option<(std::thread::ThreadId, usize)>>,
98    released: std::sync::Condvar,
99}
100
101/// One hold of a [`FrameLock`], released when dropped.
102#[derive(Debug)]
103pub(crate) struct FrameGuard {
104    lock: crate::Shared<FrameLock>,
105}
106
107impl FrameLock {
108    /// Waits until no other thread holds the lock, then holds it.
109    pub(crate) fn hold(self: &crate::Shared<Self>) -> FrameGuard {
110        let me = std::thread::current().id();
111        let mut owner = self
112            .owner
113            .lock()
114            .unwrap_or_else(std::sync::PoisonError::into_inner);
115        loop {
116            match owner.as_mut() {
117                None => {
118                    *owner = Some((me, 1));
119                    break;
120                }
121                Some((thread, depth)) if *thread == me => {
122                    *depth += 1;
123                    break;
124                }
125                Some(_) => {
126                    owner = self
127                        .released
128                        .wait(owner)
129                        .unwrap_or_else(std::sync::PoisonError::into_inner);
130                }
131            }
132        }
133        drop(owner);
134        FrameGuard {
135            lock: crate::Shared::clone(self),
136        }
137    }
138}
139
140impl Drop for FrameGuard {
141    fn drop(&mut self) {
142        let mut owner = self
143            .lock
144            .owner
145            .lock()
146            .unwrap_or_else(std::sync::PoisonError::into_inner);
147        if let Some((_, depth)) = owner.as_mut() {
148            *depth -= 1;
149            if *depth == 0 {
150                *owner = None;
151                drop(owner);
152                self.lock.released.notify_all();
153            }
154        }
155    }
156}
157
158impl std::ops::Deref for RenderEncoder {
159    type Target = wgpu::CommandEncoder;
160
161    fn deref(&self) -> &Self::Target {
162        &self.encoder
163    }
164}
165
166impl std::ops::DerefMut for RenderEncoder {
167    fn deref_mut(&mut self) -> &mut Self::Target {
168        &mut self.encoder
169    }
170}
171
172impl RenderEncoder {
173    /// Finishes recording; the encode stays open until [`Renderer::submit`].
174    pub(crate) fn finish(self) -> RenderCommands {
175        RenderCommands {
176            buffer: self.encoder.finish(),
177            errors: self.errors,
178            scope: self.scope,
179            frame: self.frame,
180        }
181    }
182}
183
184/// A finished [`RenderEncoder`] waiting to be submitted.
185#[derive(Debug)]
186pub(crate) struct RenderCommands {
187    buffer: wgpu::CommandBuffer,
188    errors: crate::gpu_errors::FrameErrorScope,
189    scope: crate::retire::EncodeScope,
190    frame: FrameGuard,
191}
192
193/// One queue submission and the spans a caller asked to be measured.
194pub(crate) struct MeasuredSubmit {
195    pub(crate) index: wgpu::SubmissionIndex,
196    pub(crate) timings: SubmitTimings,
197    pub(crate) lease_retire: Duration,
198}
199
200/// Quality controls consumed by graph compilation and pass execution.
201#[derive(Copy, Clone, Debug, Eq, PartialEq)]
202#[non_exhaustive]
203pub struct RenderQualityControls {
204    /// Maximum samples available to filter effects and layer minification.
205    /// Analytic edge coverage remains enabled even with a one-sample budget.
206    pub max_filter_samples: u32,
207    /// Deterministic temporal samples used by motion-aware runners.
208    pub motion_blur_subframes: u32,
209}
210
211impl RenderQualityControls {
212    /// The most filter samples any pass takes: the runtime passes, layer
213    /// minification and the blur shaders all stop there, so a larger budget
214    /// renders the same pixels. [`Renderer::set_quality_controls`] applies at
215    /// most this, or less where the device's tier takes less
216    /// ([`Renderer::filter_sample_limit`]).
217    pub const MAX_FILTER_SAMPLES: u32 = 64;
218}
219
220impl Default for RenderQualityControls {
221    fn default() -> Self {
222        Self {
223            max_filter_samples: Self::MAX_FILTER_SAMPLES,
224            motion_blur_subframes: 1,
225        }
226    }
227}
228
229/// The quality controls a renderer applies, and the filter sample budget it
230/// was asked for, which the limit may have reduced.
231#[derive(Copy, Clone, Debug)]
232pub(crate) struct QualityState {
233    applied: RenderQualityControls,
234    requested_filter_samples: u32,
235}
236
237impl QualityState {
238    /// The default controls, under `limit`.
239    fn new(limit: u32) -> Self {
240        let requested = RenderQualityControls::default();
241        Self {
242            applied: RenderQualityControls {
243                max_filter_samples: requested.max_filter_samples.min(limit),
244                ..requested
245            },
246            requested_filter_samples: requested.max_filter_samples,
247        }
248    }
249}
250
251impl Renderer {
252    pub(crate) fn validate_size(&self, size: RenderSize) -> Result<(), RenderError> {
253        RenderSize::new(size.width, size.height)?;
254        let maximum = self.limits.max_texture_dimension_2d;
255        if size.width > maximum || size.height > maximum {
256            return Err(RenderError::DimensionTooLarge);
257        }
258        Ok(())
259    }
260
261    /// Creates a renderer that owns the supplied GPU context.
262    #[must_use]
263    pub fn new(gpu: GpuContext) -> Self {
264        // The one read of the device's limits; everything else uses the copy.
265        #[allow(clippy::disallowed_methods)]
266        let limits = gpu.device().limits();
267        let uniform_alignment = u64::from(limits.min_uniform_buffer_offset_alignment.max(1));
268        let features = gpu.device().features();
269        let filter_sample_limit = crate::filter_sample_limit(gpu.capabilities().tier);
270        let retirement = crate::Shared::new(crate::retire::Retirement::default());
271        let gpu_errors = crate::GpuErrorReports::default();
272        let canaries = crate::canary::Canaries::new(gpu_errors.clone(), &retirement);
273        let empty_input =
274            crate::pool::default_view(&gpu.device().create_texture(&wgpu::TextureDescriptor {
275                label: Some("alpha2 generator empty input"),
276                size: wgpu::Extent3d {
277                    width: 1,
278                    height: 1,
279                    depth_or_array_layers: 1,
280                },
281                mip_level_count: 1,
282                sample_count: 1,
283                dimension: wgpu::TextureDimension::D2,
284                format: wgpu::TextureFormat::Rgba8Unorm,
285                usage: wgpu::TextureUsages::TEXTURE_BINDING,
286                view_formats: &[],
287            }));
288        Self {
289            gpu,
290            blend_space: BlendSpace::default(),
291            effects: crate::registry::EffectRegistry::default(),
292            effect_resources: crate::effect_resource::EffectResourceRegistry::default(),
293            textures: crate::Shared::new(crate::pool::TexturePool::new(crate::Shared::clone(
294                &retirement,
295            ))),
296            upload_textures: crate::Shared::new(crate::pool::UploadTexturePool::new(
297                crate::Shared::clone(&retirement),
298            )),
299            readback_buffers: crate::Shared::new(crate::pool::ReadbackBufferPool::new(
300                crate::Shared::clone(&retirement),
301            )),
302            uniform_buffers: crate::Shared::new(crate::uniform::UniformBufferPool::new(
303                uniform_alignment,
304                crate::Shared::clone(&retirement),
305            )),
306            retirement,
307            pipelines: crate::pipeline::PipelineCache::default(),
308            diagnostics: Arc::new(Mutex::new(crate::diagnostics::DiagnosticsState::default())),
309            quality: Mutex::new(QualityState::new(filter_sample_limit)),
310            result_cache: Mutex::new(crate::result_cache::ResultCache::default()),
311            timestamp_probe: std::sync::OnceLock::new(),
312            pass_timing: std::sync::atomic::AtomicBool::new(false),
313            #[cfg(test)]
314            full_frame_layers: std::sync::atomic::AtomicBool::new(false),
315            #[cfg(test)]
316            composite_batch_limit: std::sync::atomic::AtomicUsize::new(0),
317            #[cfg(test)]
318            composite_pipelines: Mutex::new(crate::composite::CompositePipelineLog::default()),
319            retired_leases: Arc::new(crate::yuv::RetiredLeases::default()),
320            gpu_wait_timeout: crate::error::DEFAULT_GPU_WAIT_TIMEOUT,
321            limits,
322            features,
323            empty_input,
324            canaries,
325            submission_canaries: crate::canary::SubmissionCanaries::default(),
326            gpu_errors,
327            frame_lock: crate::Shared::default(),
328        }
329    }
330
331    /// Where an embedding reports the GPU errors its device's
332    /// uncaptured-error handler receives, on a backend whose error scopes
333    /// resolve only after a frame has returned: the browser. The renderer
334    /// then drops the results it kept and the resources it pooled before
335    /// its next frame, since the error may have come from any of them, and
336    /// reads a readback canary missing after such a report as that error
337    /// rather than as a device that drops work.
338    ///
339    /// A native embedding does not need it: there every frame's own scopes
340    /// resolve before the frame returns, and fail it
341    /// ([`RenderError::Gpu`]).
342    #[must_use]
343    pub fn gpu_error_reports(&self) -> crate::GpuErrorReports {
344        self.gpu_errors.clone()
345    }
346
347    /// This renderer, hearing of GPU errors through `reports` (see
348    /// [`Renderer::gpu_error_reports`]): for an embedding that installs its
349    /// device's error handler before the renderer exists.
350    #[must_use]
351    pub fn with_gpu_error_reports(mut self, reports: crate::GpuErrorReports) -> Self {
352        self.canaries.clear();
353        self.canaries = crate::canary::Canaries::new(reports.clone(), &self.retirement);
354        self.gpu_errors = reports;
355        self
356    }
357
358    /// Stops trusting what the pools hold, after the GPU reported an error
359    /// for work that may have used it, and destroys what they keep idle.
360    fn forget_untrusted_resources(&self) {
361        self.retirement.doubt();
362        self.textures.clear();
363        self.upload_textures.clear();
364        self.readback_buffers.clear();
365        self.uniform_buffers.clear();
366        self.canaries.clear();
367    }
368
369    /// What the GPU this renderer drives offers: tier, intermediate format,
370    /// adapter, and limits.
371    #[must_use]
372    pub fn gpu_capabilities(&self) -> &alpha2_gpu::Capabilities {
373        self.gpu.capabilities()
374    }
375
376    /// The device's limits, as read when the renderer was created.
377    #[must_use]
378    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
379    pub const fn limits(&self) -> &wgpu::Limits {
380        &self.limits
381    }
382
383    /// Sets how long a native wait on GPU work may take before it fails with
384    /// [`RenderError::GpuTimeout`]. Readbacks begun afterwards use it.
385    pub fn set_gpu_wait_timeout(&mut self, timeout: Duration) {
386        self.gpu_wait_timeout = timeout;
387    }
388
389    /// Why this renderer's device can no longer run work, once it found
390    /// out: it completed work without running it, which a readback's missing
391    /// canary showed ([`RenderError::GpuWorkDropped`]), or it could not make
392    /// the resources a frame needs any more ([`RenderError::GpuDeviceLost`]).
393    /// From then on this renderer's readbacks and NV12 packs fail with that
394    /// error; a host should answer what it was doing and start a new process.
395    #[must_use]
396    pub fn gpu_lost(&self) -> Option<RenderError> {
397        self.canaries.lost()
398    }
399
400    /// Fails once the device has been found unable to run work.
401    pub(crate) fn ensure_gpu_runs_work(&self) -> Result<(), RenderError> {
402        self.canaries.lost().map_or(Ok(()), Err)
403    }
404
405    /// Checks the canaries of earlier NV12 packs and repacks that have
406    /// mapped, without waiting for the rest, and fails once the device has
407    /// been found dropping work.
408    pub(crate) fn check_submitted_work(&self) -> Result<(), RenderError> {
409        // Mapping callbacks run only when the device is polled; a frame loop fed by a
410        // hardware encoder may not poll it otherwise.
411        let _ = self.gpu.device().poll(wgpu::PollType::Poll);
412        self.submission_canaries.check_mapped()?;
413        self.ensure_gpu_runs_work()
414    }
415
416    /// Waits for every NV12 pack and repack submitted so far and checks that
417    /// the GPU ran each of them.
418    ///
419    /// Those submissions read nothing back, so a device that completes work
420    /// without running it (see [`RenderError::GpuWorkDropped`]) hands the
421    /// encoder planes it never wrote. Each frame checks the canaries of the
422    /// ones before it that have finished; an export calls this after its
423    /// last frame, so that the last few are checked before it reports
424    /// success.
425    ///
426    /// It may run while another thread polls the same device, as the macOS
427    /// `VideoToolbox` encode worker does: a canary's mapping then reports on
428    /// that thread, and this waits for the report.
429    ///
430    /// # Errors
431    /// Returns [`RenderError::GpuWorkDropped`] when a canary is missing, or
432    /// [`RenderError::GpuTimeout`] when the work does not complete, or a
433    /// canary's mapping does not report, within the wait timeout.
434    #[cfg(not(target_arch = "wasm32"))]
435    pub fn confirm_gpu_work(&self) -> Result<(), RenderError> {
436        let deadline = web_time::Instant::now() + self.gpu_wait_timeout;
437        self.gpu
438            .device()
439            .poll(wgpu::PollType::Wait {
440                submission_index: None,
441                timeout: Some(self.gpu_wait_timeout),
442            })
443            .map_err(crate::error::poll_error)?;
444        let settled = self.settle(&self.submission_canaries.reports, deadline, || {
445            self.check_submitted_work()?;
446            Ok(self.submission_canaries.outstanding() == 0)
447        })?;
448        if settled {
449            Ok(())
450        } else {
451            Err(RenderError::GpuTimeout)
452        }
453    }
454
455    /// How long a native wait on GPU work may take.
456    #[must_use]
457    pub const fn gpu_wait_timeout(&self) -> Duration {
458        self.gpu_wait_timeout
459    }
460
461    /// Releases every native lease whose GPU work has finished.
462    ///
463    /// Waiting on the queue is what makes progress possible: a decoder surface
464    /// is only returned once the submission sampling it completes, so a caller
465    /// that has run out of surface budget can call this and retry rather than
466    /// failing the export. Surfaces held by work that has not been submitted
467    /// yet are unaffected, so this never waits for something that cannot
468    /// happen.
469    ///
470    /// A completed submission's leases are parked by its queue callback,
471    /// which runs on whichever thread's poll collected it; this waits, up to
472    /// the wait timeout, for every submitted one to be parked, then drops
473    /// them.
474    ///
475    /// Returns how many leases it released.
476    ///
477    /// # Errors
478    /// Returns a readback error if the device cannot be polled.
479    ///
480    /// Native only: a browser cannot block on the queue, and its external
481    /// frames are released by the event loop.
482    #[cfg(not(target_arch = "wasm32"))]
483    pub fn reclaim_external_leases(&self) -> Result<usize, RenderError> {
484        let deadline = web_time::Instant::now() + self.gpu_wait_timeout;
485        self.gpu
486            .device()
487            .poll(wgpu::PollType::Wait {
488                submission_index: None,
489                timeout: Some(self.gpu_wait_timeout),
490            })
491            .map_err(crate::error::poll_error)?;
492        // Whatever is still awaited at the deadline stays parked for a later reclaim.
493        self.settle(&self.retired_leases.parks, deadline, || {
494            Ok(self.retired_leases.awaited() == 0)
495        })?;
496        Ok(self.retired_leases.drain())
497    }
498
499    /// Submits command buffers and then releases every completed native lease.
500    ///
501    /// A decoder surface must not be handed back to `FFmpeg` from inside
502    /// `wgpu`'s submission, because a shared-queue backend holds the decoder's
503    /// queue lock for the whole call while freeing a frame takes `FFmpeg`'s
504    /// frame lock. `FFmpeg`'s own decode threads take those two in the
505    /// opposite order, so doing it inside the submission deadlocks the export.
506    /// Completed leases are therefore parked by the queue callback and dropped
507    /// here, once the queue lock is released.
508    ///
509    /// # Errors
510    /// Returns [`RenderError::Gpu`] when the GPU reported an error for the
511    /// work recorded under the commands' scopes, which rejects the whole
512    /// submission: nothing in it ran.
513    pub(crate) fn submit<I>(&self, commands: I) -> Result<wgpu::SubmissionIndex, RenderError>
514    where
515        I: IntoIterator<Item = RenderCommands>,
516    {
517        self.submit_measured(commands, false)
518            .map(|submitted| submitted.index)
519    }
520
521    /// Opens a command encoder whose encode lasts until it is submitted, and
522    /// the error scopes that submission closes.
523    ///
524    /// Open it before acquiring what the frame draws into, so an allocation
525    /// that fails is the frame's error.
526    pub(crate) fn command_encoder(&self, label: &'static str) -> RenderEncoder {
527        if self.gpu_errors.take_unhandled() {
528            // An error the browser reported for earlier work: whatever it came from may be kept.
529            self.clear_result_cache_for(crate::CacheInvalidation::GpuError);
530            self.forget_untrusted_resources();
531        }
532        // Before anything else: waiting for another thread's frame holds no scope of this one.
533        let frame = self.frame_lock.hold();
534        // The scope opens first, so nothing the encoder uses can be destroyed under it.
535        let scope = self.retirement.open();
536        let errors = crate::gpu_errors::FrameErrorScope::open(
537            self.gpu.device(),
538            crate::Shared::clone(&self.retirement),
539        );
540        #[allow(clippy::disallowed_methods)] // The one place a renderer encoder is created.
541        let encoder = self
542            .gpu
543            .device()
544            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(label) });
545        RenderEncoder {
546            encoder,
547            errors,
548            scope,
549            frame,
550        }
551    }
552
553    /// Keeps whatever the pools let go of from being destroyed until the
554    /// returned scope is dropped: for renderer work recorded into an encoder
555    /// the caller submits.
556    pub(crate) fn open_encode(&self) -> crate::retire::EncodeScope {
557        self.retirement.open()
558    }
559
560    /// Submits and reports the queue wait, the submit, and the lease drain.
561    ///
562    /// With `measure` false no clock is read and every reported span is zero,
563    /// which is what [`Renderer::submit`] relies on.
564    ///
565    /// # Errors
566    /// Returns [`RenderError::Gpu`] when a scope of the commands caught an
567    /// error: the GPU rejected the submission, and nothing the renderer
568    /// pooled before it is trusted any more. The caller returns it before
569    /// keeping a result or tracking a canary of the rejected work.
570    pub(crate) fn submit_measured<I>(
571        &self,
572        commands: I,
573        measure: bool,
574    ) -> Result<MeasuredSubmit, RenderError>
575    where
576        I: IntoIterator<Item = RenderCommands>,
577    {
578        let mut command_buffers = Vec::new();
579        let mut errors = Vec::new();
580        let mut scopes = Vec::new();
581        let mut frames = Vec::new();
582        for commands in commands {
583            command_buffers.push(commands.buffer);
584            errors.push(commands.errors);
585            scopes.push(commands.scope);
586            frames.push(commands.frame);
587        }
588        let (index, timings) = if measure {
589            self.gpu.submit_timed(command_buffers)
590        } else {
591            (self.gpu.submit(command_buffers), SubmitTimings::default())
592        };
593        // The submission reports its own errors, so its scopes close after it, in reverse order.
594        let mut caught = None;
595        for scope in errors.into_iter().rev() {
596            caught = match (caught, scope.close()) {
597                (Some(alpha2_gpu::GpuErrorKind::OutOfMemory), _) | (_, None) => caught,
598                (_, kind) => kind,
599            };
600        }
601        // Submitted: what these encodes kept alive may be destroyed, and another frame may go.
602        drop(scopes);
603        drop(frames);
604        let drain_started = measure.then(web_time::Instant::now);
605        self.retired_leases.drain();
606        if let Some(kind) = caught {
607            self.forget_untrusted_resources();
608            return Err(RenderError::Gpu { kind });
609        }
610        Ok(MeasuredSubmit {
611            index,
612            timings,
613            lease_retire: drain_started.map_or(Duration::ZERO, |started| started.elapsed()),
614        })
615    }
616
617    /// Returns whether per-pass timing is being collected.
618    #[must_use]
619    pub fn pass_timing_enabled(&self) -> bool {
620        self.pass_timing.load(std::sync::atomic::Ordering::Relaxed)
621    }
622
623    /// Enables or disables per-pass timing collection. Disabled by default.
624    ///
625    /// Timing is a profiling aid, not part of rendering. While it is off no
626    /// clock is read, no pass record is taken, and no GPU timestamp resources
627    /// are created, so the render path carries none of its cost. Turning it off
628    /// also drops the retained history that [`Renderer::diagnostics`] reports,
629    /// and the GPU timestamp of a pass encoded before then, whose read lands
630    /// later, is dropped with it.
631    ///
632    /// Every other diagnostic (adapter identity, texture pool and pipeline
633    /// counters, and frame outcomes) stays available at all times because it
634    /// costs nothing per pass.
635    pub fn set_pass_timing_enabled(&self, enabled: bool) {
636        self.pass_timing
637            .store(enabled, std::sync::atomic::Ordering::Relaxed);
638        if !enabled {
639            crate::diagnostics::end_timing_run(&self.diagnostics);
640        }
641    }
642
643    /// The timing run a pass encoded now records into, or `None` while pass
644    /// timing is off. Read before the flag: a run that ends after this read
645    /// drops the pass's record even if the flag was still on.
646    pub(crate) fn pass_timing_epoch(&self) -> Option<u64> {
647        let epoch = crate::diagnostics::timing_epoch(&self.diagnostics);
648        self.pass_timing_enabled().then_some(epoch)
649    }
650
651    /// Capability-negotiated storage precision. Render graph images contain
652    /// premultiplied sRGB values; layer blend arithmetic may use linear light.
653    #[must_use]
654    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
655    pub fn intermediate_format(&self) -> wgpu::TextureFormat {
656        match self.gpu.capabilities().intermediate_format {
657            IntermediateFormat::Rgba16Float => wgpu::TextureFormat::Rgba16Float,
658            _ => wgpu::TextureFormat::Rgba8Unorm,
659        }
660    }
661
662    /// Returns allocation and reuse counters for intermediate render targets.
663    #[must_use]
664    pub fn texture_pool_stats(&self) -> crate::TexturePoolStats {
665        self.textures.stats()
666    }
667
668    /// Acquires a source-upload texture with RGBA8 storage.
669    ///
670    /// The texture and its default view return to a bounded renderer-owned pool
671    /// when the handle is dropped. Queue submissions that use it must be issued
672    /// before dropping the handle so later uploads remain queue-ordered.
673    ///
674    /// # Errors
675    /// Returns an error when the requested dimensions exceed device limits.
676    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
677    pub fn acquire_rgba8_upload_texture(
678        &self,
679        size: RenderSize,
680    ) -> Result<crate::UploadTexture, RenderError> {
681        self.validate_size(size)?;
682        Ok(self.upload_textures.acquire(
683            self.gpu.device(),
684            size,
685            wgpu::TextureFormat::Rgba8Unorm,
686            "alpha2 RGBA upload texture",
687        ))
688    }
689
690    /// Destroys all currently idle intermediate textures.
691    pub fn clear_texture_pool(&self) {
692        self.textures.clear();
693    }
694
695    /// Gives back the GPU memory the renderer holds only for reuse: idle
696    /// intermediate and upload textures, idle readback, uniform and canary
697    /// buffers, and kept graph results. Each is destroyed, not only dropped, so the
698    /// browser's WebGPU frees it now rather than at garbage collection.
699    /// Nothing a render, a readback, or a caller still holds is released (a
700    /// kept result the caller also holds stays with the caller), and work
701    /// already submitted completes first, so this is safe at any point
702    /// between calls. Later work allocates again what it needs.
703    ///
704    /// Returns the bytes this destroyed. If another thread is recording
705    /// renderer work at that moment, the destruction waits for that work to
706    /// be submitted, and is counted here all the same.
707    pub fn release_idle_memory(&self) -> u64 {
708        let before = self.retirement.retired_bytes();
709        self.clear_result_cache();
710        self.textures.clear();
711        self.upload_textures.clear();
712        self.readback_buffers.clear();
713        self.uniform_buffers.clear();
714        self.canaries.clear();
715        self.retirement.retired_bytes().saturating_sub(before)
716    }
717
718    /// Sets the soft byte budget for idle reusable intermediate textures.
719    ///
720    /// Existing idle textures are evicted immediately until the pool is within
721    /// the new budget. In-flight textures are considered when they return.
722    pub fn set_texture_budget_bytes(&self, budget_bytes: u64) {
723        self.textures.set_budget(budget_bytes);
724    }
725
726    /// Applies filter and temporal sampling controls.
727    ///
728    /// The filter sample budget is applied up to
729    /// [`Self::filter_sample_limit`], since no pass takes more; a reduced
730    /// budget is reported in [`Self::diagnostics`] as
731    /// [`crate::CapabilityDiagnostic::FilterSamplesReduced`], and the plan,
732    /// the diagnostics and the result cache all see the applied one. Kept
733    /// results are dropped only when what is applied changes.
734    pub fn set_quality_controls(&self, max_filter_samples: u32, motion_blur_subframes: u32) {
735        let requested = max_filter_samples.max(1);
736        let next = RenderQualityControls {
737            max_filter_samples: requested.min(self.filter_sample_limit()),
738            motion_blur_subframes: motion_blur_subframes.max(1),
739        };
740        let mut quality = self
741            .quality
742            .lock()
743            .unwrap_or_else(std::sync::PoisonError::into_inner);
744        quality.requested_filter_samples = requested;
745        if quality.applied != next {
746            quality.applied = next;
747            self.clear_result_cache_for(crate::CacheInvalidation::Quality);
748        }
749    }
750
751    /// The most filter samples a pass of this renderer takes:
752    /// [`RenderQualityControls::MAX_FILTER_SAMPLES`], or 16 on a Tier-B
753    /// device (see [`crate::filter_sample_limit`]).
754    #[must_use]
755    pub fn filter_sample_limit(&self) -> u32 {
756        crate::filter_sample_limit(self.gpu.capabilities().tier)
757    }
758
759    /// Lets kept graph results occupy up to `budget_bytes` (see
760    /// [`crate::RenderGraph::set_cache_intent`]); zero, the default, turns
761    /// the cache off and drops what it holds. Kept results come out of the
762    /// same GPU memory as the texture pool, which does not count them.
763    pub fn set_result_cache_budget(&self, budget_bytes: u64) {
764        self.lock_result_cache().set_budget(budget_bytes);
765    }
766
767    /// Drops every kept graph result.
768    pub fn clear_result_cache(&self) {
769        self.clear_result_cache_for(crate::CacheInvalidation::Explicit);
770    }
771
772    /// The result cache's counters.
773    #[must_use]
774    pub fn result_cache_stats(&self) -> crate::ResultCacheStats {
775        self.lock_result_cache().stats()
776    }
777
778    pub(crate) fn clear_result_cache_for(&self, reason: crate::CacheInvalidation) {
779        self.lock_result_cache().clear(reason);
780    }
781
782    pub(crate) fn lock_result_cache(
783        &self,
784    ) -> std::sync::MutexGuard<'_, crate::result_cache::ResultCache> {
785        self.result_cache
786            .lock()
787            .unwrap_or_else(std::sync::PoisonError::into_inner)
788    }
789
790    /// Returns the active render quality controls.
791    #[must_use]
792    pub fn quality_controls(&self) -> RenderQualityControls {
793        self.quality_state().applied
794    }
795
796    fn quality_state(&self) -> QualityState {
797        *self
798            .quality
799            .lock()
800            .unwrap_or_else(std::sync::PoisonError::into_inner)
801    }
802
803    /// The fallbacks this renderer applies to what its caller asked for.
804    fn degradations(&self) -> Vec<crate::CapabilityDiagnostic> {
805        let quality = self.quality_state();
806        let (requested, applied) = (
807            quality.requested_filter_samples,
808            quality.applied.max_filter_samples,
809        );
810        if requested > applied {
811            vec![crate::CapabilityDiagnostic::FilterSamplesReduced { requested, applied }]
812        } else {
813            Vec::new()
814        }
815    }
816
817    /// Returns a stable snapshot for telemetry or benchmark output.
818    #[must_use]
819    pub fn diagnostics(&self) -> crate::RenderDiagnostics {
820        let quality = self.quality_controls();
821        crate::diagnostics::snapshot(
822            &self.diagnostics,
823            self.gpu.capabilities(),
824            &crate::diagnostics::ResourceDiagnostics {
825                textures: self.texture_pool_stats(),
826                upload_textures: self.upload_textures.stats(),
827                readback_buffers: self.readback_buffers.stats(),
828                uniform_buffers: self.uniform_buffers.stats(),
829                pipelines: self.pipeline_cache_stats(),
830                result_cache: self.result_cache_stats(),
831            },
832            quality.max_filter_samples,
833            quality.motion_blur_subframes,
834            self.degradations(),
835        )
836    }
837
838    /// Records a host presentation outcome without imposing a product clock.
839    pub fn record_frame_outcome(&self, presented: bool, repeated: bool) {
840        crate::diagnostics::record_frame(&self.diagnostics, presented, repeated);
841    }
842
843    /// Adds time spent waiting for a host decoder or refill operation.
844    pub fn record_decoder_wait(&self, duration: Duration) {
845        crate::diagnostics::record_decoder_wait(&self.diagnostics, duration);
846    }
847
848    /// Returns the number of cached runtime and built-in pipelines.
849    #[must_use]
850    pub fn pipeline_cache_stats(&self) -> crate::PipelineCacheStats {
851        crate::PipelineCacheStats {
852            runtime_effects: self.effects.len(),
853            builtins: self.pipelines.builtin_count(),
854        }
855    }
856
857    /// Selects the single color space used by all layer blending.
858    #[must_use]
859    pub const fn with_blend_space(mut self, blend_space: BlendSpace) -> Self {
860        self.blend_space = blend_space;
861        self
862    }
863
864    /// Returns the configured layer-compositing color space.
865    #[must_use]
866    pub const fn blend_space(&self) -> BlendSpace {
867        self.blend_space
868    }
869
870    /// Clears an offscreen RGBA texture and submits it to the GPU queue.
871    ///
872    /// # Errors
873    ///
874    /// Returns [`RenderError`] for an invalid size or color.
875    pub fn render_solid(
876        &self,
877        size: RenderSize,
878        color: Color,
879    ) -> Result<RenderedTexture, RenderError> {
880        self.validate_size(size)?;
881        if !color.is_valid() {
882            return Err(RenderError::InvalidColor);
883        }
884        let format = self.intermediate_format();
885        let mut encoder = self.command_encoder("alpha2 solid encoder");
886        let texture = self.textures.acquire(
887            self.gpu.device(),
888            size,
889            format,
890            "alpha2 solid render target",
891        );
892        {
893            let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
894                label: Some("alpha2 solid clear pass"),
895                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
896                    view: texture.view(),
897                    depth_slice: None,
898                    resolve_target: None,
899                    ops: wgpu::Operations {
900                        load: wgpu::LoadOp::Clear(wgpu::Color {
901                            r: color.red,
902                            g: color.green,
903                            b: color.blue,
904                            a: color.alpha,
905                        }),
906                        store: wgpu::StoreOp::Store,
907                    },
908                })],
909                depth_stencil_attachment: None,
910                timestamp_writes: None,
911                occlusion_query_set: None,
912                multiview_mask: None,
913            });
914        }
915        self.submit([encoder.finish()])?;
916        Ok(RenderedTexture::pooled(size, format, texture))
917    }
918}