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}