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#[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) 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 pub(crate) frame_gpu_timer: std::sync::OnceLock<Option<crate::frame_gpu_time::FrameGpuTimer>>,
31 pub(crate) pass_timing: std::sync::atomic::AtomicBool,
32 #[cfg(test)]
34 pub(crate) full_frame_layers: std::sync::atomic::AtomicBool,
35 #[cfg(test)]
37 pub(crate) composite_batch_limit: std::sync::atomic::AtomicUsize,
38 #[cfg(test)]
40 pub(crate) vector_page_rows: std::sync::atomic::AtomicU32,
41 #[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 pub(crate) limits: wgpu::Limits,
48 pub(crate) features: wgpu::Features,
49 pub(crate) empty_input: wgpu::TextureView,
51 pub(crate) canaries: crate::canary::Canaries,
52 pub(crate) submission_canaries: crate::canary::SubmissionCanaries,
53 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#[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#[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 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#[derive(Clone, Debug)]
220pub struct LossClassifier(Arc<crate::submission_clock::SubmissionClock>);
221
222impl LossClassifier {
223 #[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 #[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#[derive(Copy, Clone, Debug, Eq, PartialEq)]
260#[non_exhaustive]
261pub struct RenderQualityControls {
262 pub max_filter_samples: u32,
265 pub motion_blur_subframes: u32,
267}
268
269impl RenderQualityControls {
270 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 #[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 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 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 #[must_use]
428 pub fn gpu_error_reports(&self) -> crate::GpuErrorReports {
429 self.gpu_errors.clone()
430 }
431
432 #[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 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 #[must_use]
457 pub fn gpu_capabilities(&self) -> &alpha2_gpu::Capabilities {
458 self.gpu.capabilities()
459 }
460
461 #[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 pub fn set_gpu_wait_timeout(&mut self, timeout: Duration) {
470 self.gpu_wait_timeout = timeout;
471 }
472
473 #[must_use]
479 pub fn gpu_lost(&self) -> Option<RenderError> {
480 self.canaries.lost()
481 }
482
483 pub fn mark_frame(&self, frame: Option<crate::FrameMark>) {
485 self.submission_clock.mark_frame(frame);
486 }
487
488 #[must_use]
490 pub fn longest_submission(&self) -> Option<crate::SubmissionReport> {
491 self.submission_clock.longest()
492 }
493
494 #[must_use]
500 pub fn classify_loss(&self, error: RenderError) -> RenderError {
501 self.loss_classifier().classify(error)
502 }
503
504 #[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 pub(crate) fn check_submitted_work(&self) -> Result<(), RenderError> {
516 let _ = self.gpu.device().poll(wgpu::PollType::Poll);
518 self.submission_canaries.check_mapped()?;
519 self.ensure_gpu_runs_work()
520 }
521
522 #[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 #[must_use]
554 pub const fn gpu_wait_timeout(&self) -> Duration {
555 self.gpu_wait_timeout
556 }
557
558 #[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 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 pub(crate) fn command_encoder(&self, label: &'static str) -> RenderEncoder {
601 if self.gpu_errors.take_unhandled() {
602 self.clear_result_cache_for(crate::CacheInvalidation::GpuError);
604 self.forget_untrusted_resources();
605 }
606 let frame = self.frame_lock.hold();
608 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)] 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 pub(crate) fn open_encode(&self) -> crate::retire::EncodeScope {
633 self.retirement.open()
634 }
635
636 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 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 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 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 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 pub fn take_gpu_submission_stats(&self) -> crate::GpuSubmissionStats {
811 crate::diagnostics::take_gpu_submission_stats(&self.diagnostics)
812 }
813
814 #[must_use]
816 pub fn pass_timing_enabled(&self) -> bool {
817 self.pass_timing.load(std::sync::atomic::Ordering::Relaxed)
818 }
819
820 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 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 #[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 #[must_use]
852 pub fn texture_pool_stats(&self) -> crate::TexturePoolStats {
853 self.textures.stats()
854 }
855
856 #[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 pub fn clear_texture_pool(&self) {
879 self.textures.clear();
880 }
881
882 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 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 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 #[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 fn pool_budget_cap(&self) -> u64 {
943 self.gpu_memory_budget().map_or(u64::MAX, |bytes| bytes / 4)
944 }
945
946 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 #[must_use]
970 pub fn filter_sample_limit(&self) -> u32 {
971 crate::filter_sample_limit(self.gpu.capabilities().tier)
972 }
973
974 pub fn set_result_cache_budget(&self, budget_bytes: u64) {
978 self.lock_result_cache().set_budget(budget_bytes);
979 }
980
981 pub fn clear_result_cache(&self) {
983 self.clear_result_cache_for(crate::CacheInvalidation::Explicit);
984 }
985
986 #[must_use]
988 pub fn result_cache_stats(&self) -> crate::ResultCacheStats {
989 self.lock_result_cache().stats()
990 }
991
992 #[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 #[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 #[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 pub fn take_pipeline_creations(&self) -> Vec<crate::PipelineCreation> {
1064 crate::diagnostics::take_pipeline_creations(&self.diagnostics)
1065 }
1066
1067 pub fn record_frame_outcome(&self, presented: bool, repeated: bool) {
1069 crate::diagnostics::record_frame(&self.diagnostics, presented, repeated);
1070 }
1071
1072 pub fn record_decoder_wait(&self, duration: Duration) {
1074 crate::diagnostics::record_decoder_wait(&self.diagnostics, duration);
1075 }
1076
1077 #[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 #[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 #[must_use]
1095 pub const fn blend_space(&self) -> BlendSpace {
1096 self.blend_space
1097 }
1098
1099 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}