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    /// Device memory budget in bytes, 0 when unknown.
19    pub(crate) gpu_memory_budget: std::sync::atomic::AtomicU64,
20    pub(crate) readback_buffers: crate::Shared<crate::pool::ReadbackBufferPool>,
21    pub(crate) uniform_buffers: crate::Shared<crate::uniform::UniformBufferPool>,
22    pub(crate) retirement: crate::Shared<crate::retire::Retirement>,
23    pub(crate) pipelines: crate::pipeline::PipelineCache,
24    pub(crate) diagnostics: crate::diagnostics::SharedDiagnostics,
25    pub(crate) quality: Mutex<QualityState>,
26    pub(crate) result_cache: Mutex<crate::result_cache::ResultCache>,
27    pub(crate) vector_state: Mutex<crate::vector::VectorState>,
28    pub(crate) timestamp_probe: std::sync::OnceLock<Option<crate::runtime_effect::TimestampProbe>>,
29    /// Timestamp queries for sampled export frames, created on first use.
30    pub(crate) frame_gpu_timer: std::sync::OnceLock<Option<crate::frame_gpu_time::FrameGpuTimer>>,
31    pub(crate) pass_timing: std::sync::atomic::AtomicBool,
32    /// Test-only: composite every layer full-frame as the culling reference.
33    #[cfg(test)]
34    pub(crate) full_frame_layers: std::sync::atomic::AtomicBool,
35    /// Test-only: max layers per composite pass when nonzero (1 = batching reference).
36    #[cfg(test)]
37    pub(crate) composite_batch_limit: std::sync::atomic::AtomicUsize,
38    /// Test-only: tile rows per vector atlas page when nonzero.
39    #[cfg(test)]
40    pub(crate) vector_page_rows: std::sync::atomic::AtomicU32,
41    /// Test-only: composite pipeline log.
42    #[cfg(test)]
43    pub(crate) composite_pipelines: Mutex<crate::composite::CompositePipelineLog>,
44    pub(crate) retired_leases: Arc<crate::yuv::RetiredLeases>,
45    pub(crate) gpu_wait_timeout: Duration,
46    /// Read once because every field read crosses into JavaScript on the browser backend.
47    pub(crate) limits: wgpu::Limits,
48    pub(crate) features: wgpu::Features,
49    /// Bound to a generator's unused image inputs.
50    pub(crate) empty_input: wgpu::TextureView,
51    pub(crate) canaries: crate::canary::Canaries,
52    pub(crate) submission_canaries: crate::canary::SubmissionCanaries,
53    /// GPU errors the embedding saw outside the renderer's own scopes.
54    pub(crate) gpu_errors: crate::GpuErrorReports,
55    pub(crate) frame_lock: crate::Shared<FrameLock>,
56    pub(crate) submission_clock: Arc<crate::submission_clock::SubmissionClock>,
57    pub(crate) last_frame_submissions: Mutex<Option<crate::FrameSubmissions>>,
58    pub(crate) calibration: std::sync::OnceLock<crate::registry::RegisteredEffect>,
59}
60
61/// A command encoder open until submitted.
62///
63/// Pooled resources released meanwhile are destroyed only after submission (see
64/// [`crate::retire`]). It also holds the frame's error scopes, so it must stay on the creating
65/// thread, and the [`FrameLock`]. A heavy frame may be split into several command buffers
66/// ([`crate::submission_planner`]); finished ones wait here for the submission.
67#[derive(Debug)]
68pub(crate) struct RenderEncoder {
69    encoder: wgpu::CommandEncoder,
70    finished: Vec<wgpu::CommandBuffer>,
71    planner: crate::submission_planner::SubmissionPlanner,
72    device: wgpu::Device,
73    label: &'static str,
74    errors: crate::gpu_errors::FrameErrorScope,
75    scope: crate::retire::EncodeScope,
76    frame: FrameGuard,
77}
78
79/// Lets one thread at a time record and submit renderer work.
80///
81/// A graph returns intermediates to the pool once their last reader is encoded, while the
82/// passes that wrote them are still unsubmitted. Holding this lock from encoder creation to
83/// submission stops another thread from reusing such a texture and submitting first.
84/// Re-entrant for the holding thread.
85#[derive(Debug, Default)]
86pub(crate) struct FrameLock {
87    owner: Mutex<Option<(std::thread::ThreadId, usize)>>,
88    released: std::sync::Condvar,
89}
90
91#[derive(Debug)]
92pub(crate) struct FrameGuard {
93    lock: crate::Shared<FrameLock>,
94}
95
96impl FrameLock {
97    pub(crate) fn hold(self: &crate::Shared<Self>) -> FrameGuard {
98        let me = std::thread::current().id();
99        let mut owner = self
100            .owner
101            .lock()
102            .unwrap_or_else(std::sync::PoisonError::into_inner);
103        loop {
104            match owner.as_mut() {
105                None => {
106                    *owner = Some((me, 1));
107                    break;
108                }
109                Some((thread, depth)) if *thread == me => {
110                    *depth += 1;
111                    break;
112                }
113                Some(_) => {
114                    owner = self
115                        .released
116                        .wait(owner)
117                        .unwrap_or_else(std::sync::PoisonError::into_inner);
118                }
119            }
120        }
121        drop(owner);
122        FrameGuard {
123            lock: crate::Shared::clone(self),
124        }
125    }
126}
127
128impl Drop for FrameGuard {
129    fn drop(&mut self) {
130        let mut owner = self
131            .lock
132            .owner
133            .lock()
134            .unwrap_or_else(std::sync::PoisonError::into_inner);
135        if let Some((_, depth)) = owner.as_mut() {
136            *depth -= 1;
137            if *depth == 0 {
138                *owner = None;
139                drop(owner);
140                self.lock.released.notify_all();
141            }
142        }
143    }
144}
145
146impl std::ops::Deref for RenderEncoder {
147    type Target = wgpu::CommandEncoder;
148
149    fn deref(&self) -> &Self::Target {
150        &self.encoder
151    }
152}
153
154impl std::ops::DerefMut for RenderEncoder {
155    fn deref_mut(&mut self) -> &mut Self::Target {
156        &mut self.encoder
157    }
158}
159
160impl RenderEncoder {
161    pub(crate) fn finish(self) -> RenderCommands {
162        let mut buffers = self.finished;
163        buffers.push(self.encoder.finish());
164        RenderCommands {
165            buffers,
166            planner: self.planner,
167            errors: self.errors,
168            scope: self.scope,
169            frame: self.frame,
170        }
171    }
172
173    pub(crate) const fn planner(&mut self) -> &mut crate::submission_planner::SubmissionPlanner {
174        &mut self.planner
175    }
176
177    /// Charges a pass to the plan and returns its band count, finishing the command buffer first
178    /// when the plan says so.
179    pub(crate) fn place_pass(
180        &mut self,
181        cost: crate::effect_cost::EffectCost,
182        pixels: u64,
183        bandable: bool,
184    ) -> u32 {
185        let placement = self.planner.pass(cost, pixels, bandable);
186        if placement.cut_before {
187            self.cut();
188        }
189        placement.bands
190    }
191
192    pub(crate) fn next_band(&mut self) {
193        self.planner.next_band();
194        self.cut();
195    }
196
197    fn cut(&mut self) {
198        #[allow(clippy::disallowed_methods)]
199        let next = self
200            .device
201            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
202                label: Some(self.label),
203            });
204        let done = std::mem::replace(&mut self.encoder, next);
205        self.finished.push(done.finish());
206    }
207}
208
209#[derive(Debug)]
210pub(crate) struct RenderCommands {
211    buffers: Vec<wgpu::CommandBuffer>,
212    planner: crate::submission_planner::SubmissionPlanner,
213    errors: crate::gpu_errors::FrameErrorScope,
214    scope: crate::retire::EncodeScope,
215    frame: FrameGuard,
216}
217
218/// Classifies a loss by the submission times of its renderer.
219#[derive(Clone, Debug)]
220pub struct LossClassifier(Arc<crate::submission_clock::SubmissionClock>);
221
222impl LossClassifier {
223    /// Classifies `error` as [`Renderer::classify_loss`] does.
224    #[must_use]
225    pub fn classify(&self, error: RenderError) -> RenderError {
226        match error {
227            RenderError::GpuWorkDropped
228            | RenderError::GpuDeviceLost
229            | RenderError::GpuTimeout
230            | RenderError::ExternalTextureSync(Some(alpha2_media::GpuFault::DeviceLost)) => self
231                .0
232                .hung()
233                .map_or(error, |report| crate::canary::hung_error(&report)),
234            error => error,
235        }
236    }
237
238    /// The recent submission that kept the GPU busy longest.
239    #[must_use]
240    pub fn longest_submission(&self) -> Option<crate::SubmissionReport> {
241        self.0.longest()
242    }
243}
244
245struct SubmittedBuffers {
246    index: wgpu::SubmissionIndex,
247    timings: SubmitTimings,
248    sequences: std::ops::Range<u64>,
249}
250
251pub(crate) struct MeasuredSubmit {
252    pub(crate) index: wgpu::SubmissionIndex,
253    pub(crate) sequences: std::ops::Range<u64>,
254    pub(crate) timings: SubmitTimings,
255    pub(crate) lease_retire: Duration,
256}
257
258/// Quality controls consumed by graph compilation and pass execution.
259#[derive(Copy, Clone, Debug, Eq, PartialEq)]
260#[non_exhaustive]
261pub struct RenderQualityControls {
262    /// Maximum samples for filter effects and layer minification. Analytic edge coverage stays
263    /// on even with a one-sample budget.
264    pub max_filter_samples: u32,
265    /// Temporal samples used by motion-aware runners.
266    pub motion_blur_subframes: u32,
267}
268
269impl RenderQualityControls {
270    /// Upper bound on filter samples; every pass caps there, so a larger budget changes nothing.
271    pub const MAX_FILTER_SAMPLES: u32 = 64;
272}
273
274impl Default for RenderQualityControls {
275    fn default() -> Self {
276        Self {
277            max_filter_samples: Self::MAX_FILTER_SAMPLES,
278            motion_blur_subframes: 1,
279        }
280    }
281}
282
283#[derive(Copy, Clone, Debug)]
284pub(crate) struct QualityState {
285    applied: RenderQualityControls,
286    requested_filter_samples: u32,
287}
288
289impl QualityState {
290    fn new(limit: u32) -> Self {
291        let requested = RenderQualityControls::default();
292        Self {
293            applied: RenderQualityControls {
294                max_filter_samples: requested.max_filter_samples.min(limit),
295                ..requested
296            },
297            requested_filter_samples: requested.max_filter_samples,
298        }
299    }
300}
301
302impl Renderer {
303    pub(crate) fn validate_size(&self, size: RenderSize) -> Result<(), RenderError> {
304        RenderSize::new(size.width, size.height)?;
305        let maximum = self.limits.max_texture_dimension_2d;
306        if size.width > maximum || size.height > maximum {
307            return Err(RenderError::DimensionTooLarge);
308        }
309        Ok(())
310    }
311
312    /// Creates a renderer that owns the supplied GPU context.
313    #[must_use]
314    pub fn new(gpu: GpuContext) -> Self {
315        #[allow(clippy::disallowed_methods)]
316        let limits = gpu.device().limits();
317        let uniform_alignment = u64::from(limits.min_uniform_buffer_offset_alignment.max(1));
318        let features = gpu.device().features();
319        let filter_sample_limit = crate::filter_sample_limit(gpu.capabilities().tier);
320        let retirement = crate::Shared::new(crate::retire::Retirement::default());
321        let gpu_errors = crate::GpuErrorReports::default();
322        let submission_clock = Arc::new(crate::submission_clock::SubmissionClock::default());
323        let canaries = crate::canary::Canaries::new(
324            gpu_errors.clone(),
325            &retirement,
326            Arc::clone(&submission_clock),
327        );
328        let empty_input =
329            crate::pool::default_view(&gpu.device().create_texture(&wgpu::TextureDescriptor {
330                label: Some("alpha2 generator empty input"),
331                size: wgpu::Extent3d {
332                    width: 1,
333                    height: 1,
334                    depth_or_array_layers: 1,
335                },
336                mip_level_count: 1,
337                sample_count: 1,
338                dimension: wgpu::TextureDimension::D2,
339                format: wgpu::TextureFormat::Rgba8Unorm,
340                usage: wgpu::TextureUsages::TEXTURE_BINDING,
341                view_formats: &[],
342            }));
343        Self {
344            gpu,
345            blend_space: BlendSpace::default(),
346            effects: crate::registry::EffectRegistry::default(),
347            effect_resources: crate::effect_resource::EffectResourceRegistry::default(),
348            textures: crate::Shared::new(crate::pool::TexturePool::new(crate::Shared::clone(
349                &retirement,
350            ))),
351            upload_textures: crate::Shared::new(crate::pool::UploadTexturePool::new(
352                crate::Shared::clone(&retirement),
353            )),
354            gpu_memory_budget: std::sync::atomic::AtomicU64::new(0),
355            readback_buffers: crate::Shared::new(crate::pool::ReadbackBufferPool::new(
356                crate::Shared::clone(&retirement),
357            )),
358            uniform_buffers: crate::Shared::new(crate::uniform::UniformBufferPool::new(
359                uniform_alignment,
360                crate::Shared::clone(&retirement),
361            )),
362            retirement,
363            pipelines: crate::pipeline::PipelineCache::default(),
364            diagnostics: Arc::new(Mutex::new(crate::diagnostics::DiagnosticsState::default())),
365            quality: Mutex::new(QualityState::new(filter_sample_limit)),
366            result_cache: Mutex::new(crate::result_cache::ResultCache::default()),
367            vector_state: Mutex::default(),
368            timestamp_probe: std::sync::OnceLock::new(),
369            frame_gpu_timer: std::sync::OnceLock::new(),
370            pass_timing: std::sync::atomic::AtomicBool::new(false),
371            #[cfg(test)]
372            full_frame_layers: std::sync::atomic::AtomicBool::new(false),
373            #[cfg(test)]
374            composite_batch_limit: std::sync::atomic::AtomicUsize::new(0),
375            #[cfg(test)]
376            vector_page_rows: std::sync::atomic::AtomicU32::new(0),
377            #[cfg(test)]
378            composite_pipelines: Mutex::new(crate::composite::CompositePipelineLog::default()),
379            retired_leases: Arc::new(crate::yuv::RetiredLeases::default()),
380            gpu_wait_timeout: crate::error::DEFAULT_GPU_WAIT_TIMEOUT,
381            limits,
382            features,
383            empty_input,
384            canaries,
385            submission_canaries: crate::canary::SubmissionCanaries::default(),
386            gpu_errors,
387            frame_lock: crate::Shared::default(),
388            submission_clock,
389            last_frame_submissions: Mutex::new(None),
390            calibration: std::sync::OnceLock::new(),
391        }
392    }
393
394    /// `None` on a device without timestamp queries, and in a browser.
395    pub(crate) fn frame_gpu_timer(&self) -> Option<&crate::frame_gpu_time::FrameGpuTimer> {
396        self.frame_gpu_timer
397            .get_or_init(|| {
398                (cfg!(not(target_arch = "wasm32"))
399                    && self.features.contains(wgpu::Features::TIMESTAMP_QUERY))
400                .then(|| {
401                    crate::frame_gpu_time::FrameGpuTimer::new(self.gpu.device(), self.gpu.queue())
402                })
403            })
404            .as_ref()
405    }
406
407    /// GPU time of the export frames sampled since the last call, then resets the totals.
408    ///
409    /// Every eighth frame of [`Self::render_graph_nv12_timed`] or
410    /// [`Self::render_graph_yuv420p_timed`] is timed and read back later without waiting.
411    ///
412    /// # Errors
413    /// The device has no timestamp queries.
414    pub fn take_frame_gpu_times(&self) -> Result<crate::FrameGpuTimes, &'static str> {
415        let timer = self
416            .frame_gpu_timer()
417            .ok_or("the device has no timestamp queries")?;
418        let _ = self.gpu.device().poll(wgpu::PollType::Poll);
419        Ok(timer.take())
420    }
421
422    /// Where an embedding reports errors from its uncaptured-error handler.
423    ///
424    /// Needed only in a browser, where error scopes resolve after the frame returns. After a
425    /// report the renderer drops its cached results and pooled resources, and treats a missing
426    /// readback canary as that error rather than as dropped work.
427    #[must_use]
428    pub fn gpu_error_reports(&self) -> crate::GpuErrorReports {
429        self.gpu_errors.clone()
430    }
431
432    /// This renderer, using `reports` for GPU errors the embedding saw before it existed.
433    #[must_use]
434    pub fn with_gpu_error_reports(mut self, reports: crate::GpuErrorReports) -> Self {
435        self.canaries.clear();
436        self.canaries = crate::canary::Canaries::new(
437            reports.clone(),
438            &self.retirement,
439            Arc::clone(&self.submission_clock),
440        );
441        self.gpu_errors = reports;
442        self
443    }
444
445    /// Drops pooled resources after the GPU reported an error that may involve them.
446    fn forget_untrusted_resources(&self) {
447        self.retirement.doubt();
448        self.textures.clear();
449        self.upload_textures.clear();
450        self.readback_buffers.clear();
451        self.uniform_buffers.clear();
452        self.canaries.clear();
453    }
454
455    /// Tier, intermediate format, adapter and limits of the GPU.
456    #[must_use]
457    pub fn gpu_capabilities(&self) -> &alpha2_gpu::Capabilities {
458        self.gpu.capabilities()
459    }
460
461    /// The device's limits.
462    #[must_use]
463    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
464    pub const fn limits(&self) -> &wgpu::Limits {
465        &self.limits
466    }
467
468    /// Sets how long a native GPU wait may take before [`RenderError::GpuTimeout`].
469    pub fn set_gpu_wait_timeout(&mut self, timeout: Duration) {
470        self.gpu_wait_timeout = timeout;
471    }
472
473    /// Why the device can no longer run work, once known.
474    ///
475    /// Either a readback canary showed dropped work ([`RenderError::GpuWorkDropped`]) or resources
476    /// could no longer be created ([`RenderError::GpuDeviceLost`]). Later readbacks and NV12 packs
477    /// fail with this error; a host should report it and restart the process.
478    #[must_use]
479    pub fn gpu_lost(&self) -> Option<RenderError> {
480        self.canaries.lost()
481    }
482
483    /// Marks the frame later submissions render, for [`RenderError::GpuDeviceHung`] reports.
484    pub fn mark_frame(&self, frame: Option<crate::FrameMark>) {
485        self.submission_clock.mark_frame(frame);
486    }
487
488    /// The recent submission, possibly still running, that kept the GPU busy longest.
489    #[must_use]
490    pub fn longest_submission(&self) -> Option<crate::SubmissionReport> {
491        self.submission_clock.longest()
492    }
493
494    /// Classifies `error` using this renderer's submission times.
495    ///
496    /// A device loss or timeout becomes [`RenderError::GpuDeviceHung`] when a recent submission
497    /// ran for at least half the GPU watchdog ([`alpha2_gpu::hung_threshold_ms`]). Other errors
498    /// pass through.
499    #[must_use]
500    pub fn classify_loss(&self, error: RenderError) -> RenderError {
501        self.loss_classifier().classify(error)
502    }
503
504    /// [`Renderer::classify_loss`] as an owned handle, for readbacks finished on an event loop.
505    #[must_use]
506    pub fn loss_classifier(&self) -> LossClassifier {
507        LossClassifier(Arc::clone(&self.submission_clock))
508    }
509
510    pub(crate) fn ensure_gpu_runs_work(&self) -> Result<(), RenderError> {
511        self.canaries.lost().map_or(Ok(()), Err)
512    }
513
514    /// Checks canaries of earlier NV12 packs and repacks that have mapped, without waiting.
515    pub(crate) fn check_submitted_work(&self) -> Result<(), RenderError> {
516        // Map callbacks run only on poll; a hardware-encoder frame loop may not poll otherwise.
517        let _ = self.gpu.device().poll(wgpu::PollType::Poll);
518        self.submission_canaries.check_mapped()?;
519        self.ensure_gpu_runs_work()
520    }
521
522    /// Waits for every submitted NV12 pack and repack and checks that the GPU ran it.
523    ///
524    /// These submissions read nothing back, so a canary is the only proof the planes were written.
525    /// Exports call this after the last frame. Safe while another thread polls the device (the
526    /// macOS `VideoToolbox` worker does).
527    ///
528    /// # Errors
529    /// [`RenderError::GpuWorkDropped`] when a canary is missing, or [`RenderError::GpuTimeout`]
530    /// when work or a mapping report does not arrive in time.
531    #[cfg(not(target_arch = "wasm32"))]
532    pub fn confirm_gpu_work(&self) -> Result<(), RenderError> {
533        let deadline = web_time::Instant::now() + self.gpu_wait_timeout;
534        self.gpu
535            .device()
536            .poll(wgpu::PollType::Wait {
537                submission_index: None,
538                timeout: Some(self.gpu_wait_timeout),
539            })
540            .map_err(crate::error::poll_error)?;
541        let settled = self.settle(&self.submission_canaries.reports, deadline, || {
542            self.check_submitted_work()?;
543            Ok(self.submission_canaries.outstanding() == 0)
544        })?;
545        if settled {
546            Ok(())
547        } else {
548            Err(RenderError::GpuTimeout)
549        }
550    }
551
552    /// How long a native wait on GPU work may take.
553    #[must_use]
554    pub const fn gpu_wait_timeout(&self) -> Duration {
555        self.gpu_wait_timeout
556    }
557
558    /// Releases every native lease whose GPU work has finished, returning how many.
559    ///
560    /// A decoder surface returns only after the submission sampling it completes, so a caller out
561    /// of surface budget can call this and retry. Unsubmitted work is not waited on.
562    ///
563    /// # Errors
564    /// A readback error if the device cannot be polled.
565    #[cfg(not(target_arch = "wasm32"))]
566    pub fn reclaim_external_leases(&self) -> Result<usize, RenderError> {
567        let deadline = web_time::Instant::now() + self.gpu_wait_timeout;
568        self.gpu
569            .device()
570            .poll(wgpu::PollType::Wait {
571                submission_index: None,
572                timeout: Some(self.gpu_wait_timeout),
573            })
574            .map_err(crate::error::poll_error)?;
575        self.settle(&self.retired_leases.parks, deadline, || {
576            Ok(self.retired_leases.awaited() == 0)
577        })?;
578        Ok(self.retired_leases.drain())
579    }
580
581    /// Submits command buffers, then releases completed native leases.
582    ///
583    /// Leases must not be released inside `wgpu`'s submit: a shared-queue backend holds the decoder
584    /// queue lock there, and freeing a frame takes `FFmpeg`'s frame lock, which `FFmpeg` decode
585    /// threads take in the opposite order.
586    ///
587    /// # Errors
588    /// [`RenderError::Gpu`] when a scope caught an error; then nothing in the submission ran.
589    pub(crate) fn submit<I>(&self, commands: I) -> Result<wgpu::SubmissionIndex, RenderError>
590    where
591        I: IntoIterator<Item = RenderCommands>,
592    {
593        self.submit_measured(commands, false)
594            .map(|submitted| submitted.index)
595    }
596
597    /// Opens a command encoder and the error scopes its submission closes.
598    ///
599    /// Open it before acquiring the frame's targets so a failed allocation is the frame's error.
600    pub(crate) fn command_encoder(&self, label: &'static str) -> RenderEncoder {
601        if self.gpu_errors.take_unhandled() {
602            // The browser reported an error for earlier work; cached results may be bad.
603            self.clear_result_cache_for(crate::CacheInvalidation::GpuError);
604            self.forget_untrusted_resources();
605        }
606        // Wait for other frames before opening any scope of this one.
607        let frame = self.frame_lock.hold();
608        // Open first so nothing the encoder uses can be destroyed under it.
609        let scope = self.retirement.open();
610        let errors = crate::gpu_errors::FrameErrorScope::open(
611            self.gpu.device(),
612            crate::Shared::clone(&self.retirement),
613        );
614        #[allow(clippy::disallowed_methods)] // The one place a renderer encoder is created.
615        let encoder = self
616            .gpu
617            .device()
618            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(label) });
619        RenderEncoder {
620            encoder,
621            finished: Vec::new(),
622            planner: crate::submission_planner::SubmissionPlanner::default(),
623            device: self.gpu.device().clone(),
624            label,
625            errors,
626            scope,
627            frame,
628        }
629    }
630
631    /// Defers destruction of released pool resources until the returned scope is dropped.
632    pub(crate) fn open_encode(&self) -> crate::retire::EncodeScope {
633        self.retirement.open()
634    }
635
636    /// Submits and reports the queue wait, submit and lease drain times.
637    ///
638    /// With `measure` false every span is zero; the submission clock still records it.
639    ///
640    /// # Errors
641    /// [`RenderError::Gpu`] when a scope caught an error; pooled resources are no longer trusted.
642    pub(crate) fn submit_measured<I>(
643        &self,
644        commands: I,
645        measure: bool,
646    ) -> Result<MeasuredSubmit, RenderError>
647    where
648        I: IntoIterator<Item = RenderCommands>,
649    {
650        let mut command_buffers = Vec::new();
651        let mut errors = Vec::new();
652        let mut scopes = Vec::new();
653        let mut frames = Vec::new();
654        let mut planners = Vec::new();
655        for commands in commands {
656            command_buffers.extend(commands.buffers.into_iter().map(|buffer| (buffer, None)));
657            if let Some(buffer_ms) = commands.planner.buffer_ms() {
658                let first = command_buffers.len() - buffer_ms.len();
659                for (slot, ms) in command_buffers[first..].iter_mut().zip(buffer_ms) {
660                    slot.1 = Some(ms);
661                }
662            }
663            planners.push(commands.planner);
664            errors.push(commands.errors);
665            scopes.push(commands.scope);
666            frames.push(commands.frame);
667        }
668        let submitted = self.submit_buffers(command_buffers, measure);
669        self.note_frame_submissions(&planners, &submitted);
670        let SubmittedBuffers {
671            index,
672            timings,
673            sequences,
674        } = submitted;
675        // Scopes close after the submit, in reverse order.
676        let mut caught = None;
677        for scope in errors.into_iter().rev() {
678            caught = match (caught, scope.close()) {
679                (Some(alpha2_gpu::GpuErrorKind::OutOfMemory), _) | (_, None) => caught,
680                (_, kind) => kind,
681            };
682        }
683        drop(scopes);
684        drop(frames);
685        let drain_started = measure.then(web_time::Instant::now);
686        self.retired_leases.drain();
687        if let Some(kind) = caught {
688            self.forget_untrusted_resources();
689            return Err(RenderError::Gpu { kind });
690        }
691        Ok(MeasuredSubmit {
692            index,
693            sequences,
694            timings,
695            lease_retire: drain_started.map_or(Duration::ZERO, |started| started.elapsed()),
696        })
697    }
698
699    /// Submits `buffers` in one submission, or one per buffer when the frame was split so the GPU
700    /// can be preempted between them. Every submission is timed for loss classification.
701    fn submit_buffers(
702        &self,
703        buffers: Vec<(wgpu::CommandBuffer, Option<f64>)>,
704        measure: bool,
705    ) -> SubmittedBuffers {
706        let submit = |buffers: Vec<wgpu::CommandBuffer>, predicted_ms: Option<f64>| {
707            let (index, timings) = if measure {
708                self.gpu.submit_timed(buffers)
709            } else {
710                (self.gpu.submit(buffers), SubmitTimings::default())
711            };
712            let sequence = self
713                .submission_clock
714                .record(web_time::Instant::now(), predicted_ms);
715            let clock = Arc::clone(&self.submission_clock);
716            self.gpu.on_submitted_work_done(move || {
717                clock.complete(sequence, web_time::Instant::now());
718            });
719            (index, timings, sequence)
720        };
721        if buffers.len() <= 1 || buffers.iter().all(|(_, ms)| ms.is_none()) {
722            let predicted_ms = buffers
723                .iter()
724                .filter_map(|(_, ms)| *ms)
725                .reduce(|a, b| a + b);
726            let (index, timings, sequence) = submit(
727                buffers.into_iter().map(|(buffer, _)| buffer).collect(),
728                predicted_ms,
729            );
730            return SubmittedBuffers {
731                index,
732                timings,
733                sequences: sequence..sequence + 1,
734            };
735        }
736        let mut total = SubmitTimings::default();
737        let mut last = None;
738        let mut first_sequence = None;
739        for (buffer, predicted_ms) in buffers {
740            let (index, timings, sequence) = submit(vec![buffer], predicted_ms);
741            total.queue_lock_wait = total
742                .queue_lock_wait
743                .saturating_add(timings.queue_lock_wait);
744            total.queue_submit = total.queue_submit.saturating_add(timings.queue_submit);
745            first_sequence.get_or_insert(sequence);
746            last = Some((index, sequence));
747        }
748        let (index, last_sequence) = last.expect("at least two buffers");
749        SubmittedBuffers {
750            index,
751            timings: total,
752            sequences: first_sequence.unwrap_or(last_sequence)..last_sequence + 1,
753        }
754    }
755
756    /// Records frame submissions and folds measured GPU time into the device rate on completion.
757    fn note_frame_submissions(
758        &self,
759        planners: &[crate::submission_planner::SubmissionPlanner],
760        submitted: &SubmittedBuffers,
761    ) {
762        let Some(planner) = planners.iter().find(|planner| planner.is_frame()) else {
763            return;
764        };
765        let submissions =
766            u32::try_from(submitted.sequences.end - submitted.sequences.start).unwrap_or(u32::MAX);
767        let buffer_ms = planner.buffer_ms();
768        let report = crate::FrameSubmissions {
769            submissions,
770            predicted_ms: buffer_ms.as_ref().map(|buffers| buffers.iter().sum()),
771            cost: planner.cost(),
772        };
773        crate::diagnostics::record_frame_plan(
774            &self.diagnostics,
775            &report,
776            buffer_ms
777                .iter()
778                .flatten()
779                .copied()
780                .fold(None, |most: Option<f64>, ms| {
781                    Some(most.map_or(ms, |most| most.max(ms)))
782                }),
783        );
784        *self
785            .last_frame_submissions
786            .lock()
787            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(report);
788        if planner.is_planned() {
789            let clock = Arc::clone(&self.submission_clock);
790            let rate = Arc::clone(self.gpu.rate());
791            let sequences = submitted.sequences.clone();
792            let unit_pixels = report.cost.unit_pixels();
793            self.gpu.on_submitted_work_done(move || {
794                if let Some(measured_ms) = clock.gpu_ms(sequences) {
795                    rate.observe(measured_ms, unit_pixels);
796                }
797            });
798        }
799    }
800
801    /// How the last frame was submitted; each frame's is returned once.
802    pub fn take_frame_submissions(&self) -> Option<crate::FrameSubmissions> {
803        self.last_frame_submissions
804            .lock()
805            .unwrap_or_else(std::sync::PoisonError::into_inner)
806            .take()
807    }
808
809    /// Submission split counts since the last call, then resets them.
810    pub fn take_gpu_submission_stats(&self) -> crate::GpuSubmissionStats {
811        crate::diagnostics::take_gpu_submission_stats(&self.diagnostics)
812    }
813
814    /// Returns whether per-pass timing is being collected.
815    #[must_use]
816    pub fn pass_timing_enabled(&self) -> bool {
817        self.pass_timing.load(std::sync::atomic::Ordering::Relaxed)
818    }
819
820    /// Enables or disables per-pass timing collection. Disabled by default.
821    ///
822    /// While off, no clock is read and no timestamp resources are created. Turning it off drops
823    /// the retained timing history. Other diagnostics are always available.
824    pub fn set_pass_timing_enabled(&self, enabled: bool) {
825        self.pass_timing
826            .store(enabled, std::sync::atomic::Ordering::Relaxed);
827        if !enabled {
828            crate::diagnostics::end_timing_run(&self.diagnostics);
829        }
830    }
831
832    /// The timing run a pass encoded now records into, or `None` while timing is off. Read before
833    /// the flag: a run that ends after this read drops the pass's record.
834    pub(crate) fn pass_timing_epoch(&self) -> Option<u64> {
835        let epoch = crate::diagnostics::timing_epoch(&self.diagnostics);
836        self.pass_timing_enabled().then_some(epoch)
837    }
838
839    /// Capability-negotiated storage precision. Graph images hold premultiplied sRGB; layer blends
840    /// may use linear light.
841    #[must_use]
842    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
843    pub fn intermediate_format(&self) -> wgpu::TextureFormat {
844        match self.gpu.capabilities().intermediate_format {
845            IntermediateFormat::Rgba16Float => wgpu::TextureFormat::Rgba16Float,
846            _ => wgpu::TextureFormat::Rgba8Unorm,
847        }
848    }
849
850    /// Returns allocation and reuse counters for intermediate render targets.
851    #[must_use]
852    pub fn texture_pool_stats(&self) -> crate::TexturePoolStats {
853        self.textures.stats()
854    }
855
856    /// Acquires a pooled RGBA8 source-upload texture.
857    ///
858    /// Submissions using it must be issued before the handle is dropped so later uploads stay
859    /// queue-ordered.
860    ///
861    /// # Errors
862    /// The dimensions exceed device limits.
863    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
864    pub fn acquire_rgba8_upload_texture(
865        &self,
866        size: RenderSize,
867    ) -> Result<crate::UploadTexture, RenderError> {
868        self.validate_size(size)?;
869        Ok(self.upload_textures.acquire(
870            self.gpu.device(),
871            size,
872            wgpu::TextureFormat::Rgba8Unorm,
873            "alpha2 RGBA upload texture",
874        ))
875    }
876
877    /// Destroys all currently idle intermediate textures.
878    pub fn clear_texture_pool(&self) {
879        self.textures.clear();
880    }
881
882    /// Destroys GPU memory held only for reuse (idle pools and kept graph results) and returns the
883    /// bytes freed.
884    ///
885    /// Resources are destroyed rather than dropped so browser `WebGPU` frees them immediately.
886    /// Nothing still in use is released, and submitted work completes first, so this is safe
887    /// between calls.
888    pub fn release_idle_memory(&self) -> u64 {
889        let before = self.retirement.retired_bytes();
890        self.clear_result_cache();
891        self.textures.clear();
892        self.upload_textures.clear();
893        self.readback_buffers.clear();
894        self.uniform_buffers.clear();
895        self.canaries.clear();
896        self.vector_state
897            .lock()
898            .unwrap_or_else(std::sync::PoisonError::into_inner)
899            .cache
900            .clear();
901        self.retirement.retired_bytes().saturating_sub(before)
902    }
903
904    /// Sets the soft byte budget for idle intermediate textures, evicting down to it now.
905    pub fn set_texture_budget_bytes(&self, budget_bytes: u64) {
906        self.textures
907            .set_budget(budget_bytes.min(self.pool_budget_cap()));
908    }
909
910    /// Records the adapter's memory budget (`None` if unreported) and caps idle pools at a quarter
911    /// of it. Returns whether a pool budget was lowered.
912    pub fn set_gpu_memory_budget(&self, budget_bytes: Option<u64>) -> bool {
913        self.gpu_memory_budget.store(
914            budget_bytes.unwrap_or(0),
915            std::sync::atomic::Ordering::Relaxed,
916        );
917        let cap = self.pool_budget_cap();
918        let textures = self.textures.stats().budget_bytes;
919        let uploads = self.upload_textures.stats().budget_bytes;
920        if textures > cap {
921            self.textures.set_budget(cap);
922        }
923        if uploads > cap {
924            self.upload_textures.set_budget(cap);
925        }
926        textures > cap || uploads > cap
927    }
928
929    /// The adapter's device memory budget, when reported.
930    #[must_use]
931    pub fn gpu_memory_budget(&self) -> Option<u64> {
932        match self
933            .gpu_memory_budget
934            .load(std::sync::atomic::Ordering::Relaxed)
935        {
936            0 => None,
937            bytes => Some(bytes),
938        }
939    }
940
941    /// A quarter of the device memory budget, or unlimited when unknown.
942    fn pool_budget_cap(&self) -> u64 {
943        self.gpu_memory_budget().map_or(u64::MAX, |bytes| bytes / 4)
944    }
945
946    /// Applies filter and temporal sampling controls.
947    ///
948    /// The filter budget is capped at [`Self::filter_sample_limit`]; a reduction is reported as
949    /// [`crate::CapabilityDiagnostic::FilterSamplesReduced`]. Kept results are dropped only when
950    /// the applied values change.
951    pub fn set_quality_controls(&self, max_filter_samples: u32, motion_blur_subframes: u32) {
952        let requested = max_filter_samples.max(1);
953        let next = RenderQualityControls {
954            max_filter_samples: requested.min(self.filter_sample_limit()),
955            motion_blur_subframes: motion_blur_subframes.max(1),
956        };
957        let mut quality = self
958            .quality
959            .lock()
960            .unwrap_or_else(std::sync::PoisonError::into_inner);
961        quality.requested_filter_samples = requested;
962        if quality.applied != next {
963            quality.applied = next;
964            self.clear_result_cache_for(crate::CacheInvalidation::Quality);
965        }
966    }
967
968    /// [`RenderQualityControls::MAX_FILTER_SAMPLES`], or 16 on a Tier-B device.
969    #[must_use]
970    pub fn filter_sample_limit(&self) -> u32 {
971        crate::filter_sample_limit(self.gpu.capabilities().tier)
972    }
973
974    /// Lets kept graph results use up to `budget_bytes` (see
975    /// [`crate::RenderGraph::set_cache_intent`]); zero, the default, disables the cache. The
976    /// texture pool budget does not count these results.
977    pub fn set_result_cache_budget(&self, budget_bytes: u64) {
978        self.lock_result_cache().set_budget(budget_bytes);
979    }
980
981    /// Drops every kept graph result.
982    pub fn clear_result_cache(&self) {
983        self.clear_result_cache_for(crate::CacheInvalidation::Explicit);
984    }
985
986    /// The result cache's counters.
987    #[must_use]
988    pub fn result_cache_stats(&self) -> crate::ResultCacheStats {
989        self.lock_result_cache().stats()
990    }
991
992    /// Changes whenever a graph with an unchanged [`Self::graph_cache_key`] may render differently
993    /// (any result-cache clear or reported GPU error). Equal keys under an equal epoch mean equal
994    /// pixels.
995    #[must_use]
996    pub fn content_epoch(&self) -> u64 {
997        self.lock_result_cache()
998            .clears()
999            .wrapping_add(self.gpu_errors.count())
1000    }
1001
1002    pub(crate) fn clear_result_cache_for(&self, reason: crate::CacheInvalidation) {
1003        self.lock_result_cache().clear(reason);
1004    }
1005
1006    pub(crate) fn lock_result_cache(
1007        &self,
1008    ) -> std::sync::MutexGuard<'_, crate::result_cache::ResultCache> {
1009        self.result_cache
1010            .lock()
1011            .unwrap_or_else(std::sync::PoisonError::into_inner)
1012    }
1013
1014    /// Returns the active render quality controls.
1015    #[must_use]
1016    pub fn quality_controls(&self) -> RenderQualityControls {
1017        self.quality_state().applied
1018    }
1019
1020    fn quality_state(&self) -> QualityState {
1021        *self
1022            .quality
1023            .lock()
1024            .unwrap_or_else(std::sync::PoisonError::into_inner)
1025    }
1026
1027    fn degradations(&self) -> Vec<crate::CapabilityDiagnostic> {
1028        let quality = self.quality_state();
1029        let (requested, applied) = (
1030            quality.requested_filter_samples,
1031            quality.applied.max_filter_samples,
1032        );
1033        if requested > applied {
1034            vec![crate::CapabilityDiagnostic::FilterSamplesReduced { requested, applied }]
1035        } else {
1036            Vec::new()
1037        }
1038    }
1039
1040    /// Returns a stable snapshot for telemetry or benchmark output.
1041    #[must_use]
1042    pub fn diagnostics(&self) -> crate::RenderDiagnostics {
1043        let quality = self.quality_controls();
1044        crate::diagnostics::snapshot(
1045            &self.diagnostics,
1046            self.gpu.capabilities(),
1047            &crate::diagnostics::ResourceDiagnostics {
1048                textures: self.texture_pool_stats(),
1049                upload_textures: self.upload_textures.stats(),
1050                readback_buffers: self.readback_buffers.stats(),
1051                uniform_buffers: self.uniform_buffers.stats(),
1052                pipelines: self.pipeline_cache_stats(),
1053                result_cache: self.result_cache_stats(),
1054            },
1055            quality.max_filter_samples,
1056            quality.motion_blur_subframes,
1057            self.degradations(),
1058        )
1059    }
1060
1061    /// Pipeline creations since the last call, oldest first (at most 256), with start and end
1062    /// times.
1063    pub fn take_pipeline_creations(&self) -> Vec<crate::PipelineCreation> {
1064        crate::diagnostics::take_pipeline_creations(&self.diagnostics)
1065    }
1066
1067    /// Records a host presentation outcome without imposing a product clock.
1068    pub fn record_frame_outcome(&self, presented: bool, repeated: bool) {
1069        crate::diagnostics::record_frame(&self.diagnostics, presented, repeated);
1070    }
1071
1072    /// Adds time spent waiting for a host decoder or refill operation.
1073    pub fn record_decoder_wait(&self, duration: Duration) {
1074        crate::diagnostics::record_decoder_wait(&self.diagnostics, duration);
1075    }
1076
1077    /// Returns the number of cached runtime and built-in pipelines.
1078    #[must_use]
1079    pub fn pipeline_cache_stats(&self) -> crate::PipelineCacheStats {
1080        crate::PipelineCacheStats {
1081            runtime_effects: self.effects.len(),
1082            builtins: self.pipelines.builtin_count(),
1083        }
1084    }
1085
1086    /// Selects the single color space used by all layer blending.
1087    #[must_use]
1088    pub const fn with_blend_space(mut self, blend_space: BlendSpace) -> Self {
1089        self.blend_space = blend_space;
1090        self
1091    }
1092
1093    /// Returns the configured layer-compositing color space.
1094    #[must_use]
1095    pub const fn blend_space(&self) -> BlendSpace {
1096        self.blend_space
1097    }
1098
1099    /// Clears an offscreen RGBA texture and submits it to the GPU queue.
1100    ///
1101    /// # Errors
1102    ///
1103    /// Returns [`RenderError`] for an invalid size or color.
1104    pub fn render_solid(
1105        &self,
1106        size: RenderSize,
1107        color: Color,
1108    ) -> Result<RenderedTexture, RenderError> {
1109        self.validate_size(size)?;
1110        if !color.is_valid() {
1111            return Err(RenderError::InvalidColor);
1112        }
1113        let format = self.intermediate_format();
1114        let mut encoder = self.command_encoder("alpha2 solid encoder");
1115        let texture = self.textures.acquire(
1116            self.gpu.device(),
1117            size,
1118            format,
1119            "alpha2 solid render target",
1120        );
1121        {
1122            let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1123                label: Some("alpha2 solid clear pass"),
1124                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1125                    view: texture.view(),
1126                    depth_slice: None,
1127                    resolve_target: None,
1128                    ops: wgpu::Operations {
1129                        load: wgpu::LoadOp::Clear(wgpu::Color {
1130                            r: color.red,
1131                            g: color.green,
1132                            b: color.blue,
1133                            a: color.alpha,
1134                        }),
1135                        store: wgpu::StoreOp::Store,
1136                    },
1137                })],
1138                depth_stencil_attachment: None,
1139                timestamp_writes: None,
1140                occlusion_query_set: None,
1141                multiview_mask: None,
1142            });
1143        }
1144        self.submit([encoder.finish()])?;
1145        Ok(RenderedTexture::pooled(size, format, texture))
1146    }
1147}