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