1use std::fmt;
2use std::time::Duration;
3
4use web_time::Instant;
5use wgpu::{
6 Color, CommandEncoder, LoadOp, Operations, RenderPass, RenderPassColorAttachment,
7 RenderPassDepthStencilAttachment, RenderPassDescriptor, StoreOp, TextureFormat, TextureView,
8};
9
10use crate::device::{GpuContext, LossSignal, UncapturedGpuError};
11use crate::gpu_timer::SectionTimer;
12use crate::view::DEPTH_CLEAR;
13use crate::DepthConvention;
14
15#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
16pub enum PassStage {
17 Background,
18 Scene,
19 Overlay,
20}
21
22#[derive(Copy, Clone, Debug, PartialEq, Eq)]
23pub enum PassPhase {
24 Attach,
25 Record,
26}
27
28impl fmt::Display for PassPhase {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::Attach => f.write_str("attach"),
32 Self::Record => f.write_str("record"),
33 }
34 }
35}
36
37#[derive(Debug, thiserror::Error)]
38pub enum PassExecutionError {
39 #[error("pass `{pass}` failed during {phase}: {source:#}")]
40 Pass {
41 pass: &'static str,
42 phase: PassPhase,
43 #[source]
44 source: anyhow::Error,
45 },
46 #[error("GPU work outside a pass failed: {source:#}")]
47 Backend {
48 #[source]
49 source: anyhow::Error,
50 },
51}
52
53#[derive(Copy, Clone, Debug, PartialEq, Eq)]
54pub struct FrameFormat {
55 pub color: TextureFormat,
56 pub depth: TextureFormat,
57}
58
59pub struct FrameTarget<'a> {
60 pub color: &'a TextureView,
61 pub depth: Option<&'a TextureView>,
62 pub size: (u32, u32),
63}
64
65#[derive(Copy, Clone, Debug, PartialEq, Eq)]
66pub enum ColorLoad {
67 Load,
68 Clear,
69}
70
71pub trait FramePass {
72 fn name(&self) -> &'static str;
73
74 fn stage(&self) -> PassStage;
75
76 fn color_load(&self) -> ColorLoad {
77 ColorLoad::Load
78 }
79
80 fn depth_convention(&self) -> Option<DepthConvention> {
82 None
83 }
84
85 fn depth_read(&self) -> Option<DepthConvention> {
86 None
87 }
88
89 fn shares_pass(&self) -> bool {
91 false
92 }
93
94 fn clear_color(&self) -> Color {
96 Color::BLACK
97 }
98
99 fn prepare(
101 &mut self,
102 _encoder: &mut CommandEncoder,
103 _target: &FrameTarget<'_>,
104 ) -> anyhow::Result<()> {
105 Ok(())
106 }
107
108 fn draw(
110 &mut self,
111 _pass: &mut RenderPass<'_>,
112 _target: &FrameTarget<'_>,
113 ) -> anyhow::Result<()> {
114 Ok(())
115 }
116
117 fn record(
118 &mut self,
119 encoder: &mut CommandEncoder,
120 target: &FrameTarget<'_>,
121 ) -> anyhow::Result<()>;
122
123 fn attach(&mut self, gpu: &GpuContext, frame: FrameFormat) -> anyhow::Result<()>;
124}
125
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub enum PassError {
128 DepthConvention {
129 pass: &'static str,
130 declared: DepthConvention,
131 frame: DepthConvention,
132 },
133 DepthRead {
134 pass: &'static str,
135 declared: DepthConvention,
136 frame: DepthConvention,
137 },
138 ColorClear {
139 pass: &'static str,
140 stage: PassStage,
141 },
142}
143
144impl fmt::Display for PassError {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 Self::DepthConvention {
148 pass,
149 declared,
150 frame,
151 } => write!(
152 f,
153 "pass `{pass}` writes depth under {declared:?}; this frame compares under {frame:?}"
154 ),
155 Self::DepthRead {
156 pass,
157 declared,
158 frame,
159 } => write!(
160 f,
161 "pass `{pass}` reads depth under {declared:?}; this frame compares under {frame:?}"
162 ),
163 Self::ColorClear { pass, stage } => write!(
164 f,
165 "pass `{pass}` clears the shared color target in the {stage:?} stage; only {:?} clears it",
166 PassStage::Background
167 ),
168 }
169 }
170}
171
172impl std::error::Error for PassError {}
173
174#[derive(Copy, Clone, Debug, PartialEq, Eq)]
175pub enum GpuTime {
176 Unavailable,
178 Measured(Duration),
179}
180
181#[derive(Copy, Clone, Debug, PartialEq, Eq)]
182pub struct Section {
183 pub name: &'static str,
184 pub cpu: Duration,
186 pub gpu: GpuTime,
187}
188
189pub struct PassSchedule {
190 convention: DepthConvention,
191 passes: Vec<Box<dyn FramePass>>,
192 sections: Vec<Section>,
193 timer: Option<SectionTimer>,
194 signal: Option<std::sync::Arc<LossSignal>>,
195 unattached: Option<&'static str>,
196}
197
198impl PassSchedule {
199 pub fn new(convention: DepthConvention) -> Self {
200 Self {
201 convention,
202 passes: Vec::new(),
203 sections: Vec::new(),
204 timer: None,
205 signal: None,
206 unattached: None,
207 }
208 }
209
210 pub fn convention(&self) -> DepthConvention {
211 self.convention
212 }
213
214 pub fn unattached(&self) -> Option<&'static str> {
215 self.unattached
216 }
217
218 pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
219 self.passes.iter().map(|pass| pass.name())
220 }
221
222 pub fn sections(&self) -> &[Section] {
223 &self.sections
224 }
225
226 pub fn register(&mut self, pass: Box<dyn FramePass>) -> Result<(), PassError> {
227 if let Some(declared) = pass.depth_convention() {
228 if declared != self.convention {
229 return Err(PassError::DepthConvention {
230 pass: pass.name(),
231 declared,
232 frame: self.convention,
233 });
234 }
235 }
236 if let Some(declared) = pass.depth_read() {
237 if declared != self.convention {
238 return Err(PassError::DepthRead {
239 pass: pass.name(),
240 declared,
241 frame: self.convention,
242 });
243 }
244 }
245 let stage = pass.stage();
246 if pass.color_load() == ColorLoad::Clear && stage != PassStage::Background {
247 return Err(PassError::ColorClear {
248 pass: pass.name(),
249 stage,
250 });
251 }
252 let at = self.passes.partition_point(|held| held.stage() <= stage);
253 self.passes.insert(at, pass);
254 Ok(())
255 }
256
257 pub fn attach(
258 &mut self,
259 gpu: &GpuContext,
260 frame: FrameFormat,
261 ) -> Result<(), PassExecutionError> {
262 self.signal = Some(gpu.loss_signal());
263 self.timer = SectionTimer::new(&gpu.device, &gpu.queue);
264 let signal = self.signal.as_deref();
265 for pass in self.passes.iter_mut() {
266 let name = pass.name();
267 if let Err(error) =
268 run_pass(signal, name, PassPhase::Attach, || pass.attach(gpu, frame))
269 {
270 self.unattached = Some(name);
271 return Err(error);
272 }
273 }
274 self.unattached = None;
275 Ok(())
276 }
277
278 pub fn begin_frame(&mut self) {
279 self.sections.clear();
280 if let Some(timer) = self.timer.as_mut() {
281 timer.begin_frame();
282 }
283 }
284
285 pub fn section(
286 &mut self,
287 name: &'static str,
288 encoder: &mut CommandEncoder,
289 body: impl FnOnce(&mut CommandEncoder),
290 ) {
291 time_section(&mut self.timer, &mut self.sections, name, encoder, body);
292 }
293
294 pub fn record(
295 &mut self,
296 stage: PassStage,
297 encoder: &mut CommandEncoder,
298 target: &FrameTarget<'_>,
299 ) -> Result<(), PassExecutionError> {
300 if self.unattached.is_some() {
301 return Ok(());
302 }
303 let timer = &mut self.timer;
304 let sections = &mut self.sections;
305 let signal = self.signal.as_deref();
306 for pass in self.passes.iter_mut().filter(|pass| pass.stage() == stage) {
307 record_alone(signal, timer, sections, pass.as_mut(), encoder, target)?;
308 }
309 Ok(())
310 }
311
312 pub fn record_scene(
314 &mut self,
315 encoder: &mut CommandEncoder,
316 target: &FrameTarget<'_>,
317 background: Color,
318 ) -> Result<(), PassExecutionError> {
319 let depth_clear = depth_clear(self.convention);
320 if self.unattached.is_some() {
321 self.clear(encoder, target, background, depth_clear);
322 return Ok(());
323 }
324 let mut plan = Plan::new(
325 &self.passes,
326 target.depth.is_some(),
327 background,
328 depth_clear,
329 );
330 while let Some(step) = plan.next(&self.passes) {
331 match step {
332 Step::Clear => self.clear(encoder, target, background, depth_clear),
333 Step::Alone(index) => record_alone(
334 self.signal.as_deref(),
335 &mut self.timer,
336 &mut self.sections,
337 self.passes[index].as_mut(),
338 encoder,
339 target,
340 )?,
341 Step::Shared(run) => self.record_shared(run, encoder, target)?,
342 }
343 }
344 Ok(())
345 }
346
347 fn clear(
348 &mut self,
349 encoder: &mut CommandEncoder,
350 target: &FrameTarget<'_>,
351 background: Color,
352 depth_clear: f32,
353 ) {
354 self.section("present-clear", encoder, |encoder| {
355 encoder.begin_render_pass(&RenderPassDescriptor {
356 label: Some("loam-render present clear"),
357 color_attachments: &[Some(RenderPassColorAttachment {
358 view: target.color,
359 depth_slice: None,
360 resolve_target: None,
361 ops: Operations {
362 load: LoadOp::Clear(background),
363 store: StoreOp::Store,
364 },
365 })],
366 depth_stencil_attachment: target.depth.map(|view| {
367 RenderPassDepthStencilAttachment {
368 view,
369 depth_ops: Some(Operations {
370 load: LoadOp::Clear(depth_clear),
371 store: StoreOp::Store,
372 }),
373 stencil_ops: None,
374 }
375 }),
376 timestamp_writes: None,
377 occlusion_query_set: None,
378 });
379 });
380 }
381
382 fn record_shared(
383 &mut self,
384 run: Run,
385 encoder: &mut CommandEncoder,
386 target: &FrameTarget<'_>,
387 ) -> Result<(), PassExecutionError> {
388 let _scope = loam_time::frame_trace::scope(SCENE_PASS);
389 let started = Instant::now();
390 let signal = self.signal.as_deref();
391 let timer = &mut self.timer;
392 let sections = &mut self.sections;
393 let passes = &mut self.passes[run.start..run.end];
394 let first = sections.len();
395 sections.push(Section {
396 name: SCENE_PASS,
397 cpu: Duration::ZERO,
398 gpu: GpuTime::Unavailable,
399 });
400 for pass in passes.iter_mut() {
401 let name = pass.name();
402 let begun = Instant::now();
403 let outcome = run_pass(signal, name, PassPhase::Record, || {
404 pass.prepare(encoder, target)
405 });
406 sections.push(Section {
407 name,
408 cpu: begun.elapsed(),
409 gpu: GpuTime::Unavailable,
410 });
411 outcome?;
412 }
413 let slot = timer
415 .as_mut()
416 .and_then(|timer| timer.open(encoder, SCENE_PASS));
417 let mut outcome = Ok(());
418 {
419 let mut render = encoder.begin_render_pass(&RenderPassDescriptor {
420 label: Some("loam-render scene pass"),
421 color_attachments: &[Some(RenderPassColorAttachment {
422 view: target.color,
423 depth_slice: None,
424 resolve_target: None,
425 ops: Operations {
426 load: run.color,
427 store: StoreOp::Store,
428 },
429 })],
430 depth_stencil_attachment: target.depth.map(|view| {
431 RenderPassDepthStencilAttachment {
432 view,
433 depth_ops: Some(Operations {
434 load: run.depth,
435 store: run.depth_store,
436 }),
437 stencil_ops: None,
438 }
439 }),
440 timestamp_writes: None,
441 occlusion_query_set: None,
442 });
443 for (offset, pass) in passes.iter_mut().enumerate() {
444 let begun = Instant::now();
445 outcome = run_pass(signal, pass.name(), PassPhase::Record, || {
446 pass.draw(&mut render, target)
447 });
448 sections[first + 1 + offset].cpu += begun.elapsed();
449 if outcome.is_err() {
450 break;
451 }
452 }
453 }
454 let gpu = match (timer.as_mut(), slot) {
455 (Some(timer), Some(slot)) => {
456 timer.close(encoder, slot);
457 timer
458 .elapsed(slot)
459 .map_or(GpuTime::Unavailable, GpuTime::Measured)
460 }
461 _ => GpuTime::Unavailable,
462 };
463 sections[first].cpu = started.elapsed();
464 sections[first].gpu = gpu;
465 outcome
466 }
467
468 pub fn end_frame(&mut self, encoder: &mut CommandEncoder) {
469 if let Some(timer) = self.timer.as_mut() {
470 timer.resolve(encoder);
471 }
472 }
473
474 pub fn after_submit(&mut self) {
475 if let Some(timer) = self.timer.as_mut() {
476 timer.after_submit();
477 }
478 }
479}
480
481fn run_pass<T>(
482 signal: Option<&LossSignal>,
483 pass: &'static str,
484 phase: PassPhase,
485 body: impl FnOnce() -> anyhow::Result<T>,
486) -> Result<T, PassExecutionError> {
487 if let Some(source) = signal.and_then(LossSignal::take_error) {
488 return Err(backend_error(source));
489 }
490 let outcome = body();
491 if let Some(source) = signal.and_then(LossSignal::take_error) {
492 return Err(PassExecutionError::Pass {
493 pass,
494 phase,
495 source: source.into(),
496 });
497 }
498 outcome.map_err(|source| PassExecutionError::Pass {
499 pass,
500 phase,
501 source,
502 })
503}
504
505fn backend_error(source: UncapturedGpuError) -> PassExecutionError {
506 PassExecutionError::Backend {
507 source: source.into(),
508 }
509}
510
511fn time_section<T>(
512 timer: &mut Option<SectionTimer>,
513 sections: &mut Vec<Section>,
514 name: &'static str,
515 encoder: &mut CommandEncoder,
516 body: impl FnOnce(&mut CommandEncoder) -> T,
517) -> T {
518 let _scope = loam_time::frame_trace::scope(name);
519 let slot = timer.as_mut().and_then(|timer| timer.open(encoder, name));
520 let started = Instant::now();
521 let outcome = body(encoder);
522 let cpu = started.elapsed();
523 let gpu = match (timer.as_mut(), slot) {
524 (Some(timer), Some(slot)) => {
525 timer.close(encoder, slot);
526 timer
527 .elapsed(slot)
528 .map_or(GpuTime::Unavailable, GpuTime::Measured)
529 }
530 _ => GpuTime::Unavailable,
531 };
532 sections.push(Section { name, cpu, gpu });
533 outcome
534}
535
536fn record_alone(
537 signal: Option<&LossSignal>,
538 timer: &mut Option<SectionTimer>,
539 sections: &mut Vec<Section>,
540 pass: &mut dyn FramePass,
541 encoder: &mut CommandEncoder,
542 target: &FrameTarget<'_>,
543) -> Result<(), PassExecutionError> {
544 let name = pass.name();
545 run_pass(signal, name, PassPhase::Record, || {
546 time_section(timer, sections, name, encoder, |encoder| {
547 pass.record(encoder, target)
548 })
549 })
550}
551
552const SCENE_PASS: &str = "scene-pass";
553
554fn depth_clear(convention: DepthConvention) -> f32 {
555 match convention {
556 DepthConvention::StandardZ => 1.0,
557 DepthConvention::ReversedZ => DEPTH_CLEAR,
558 }
559}
560
561#[derive(Copy, Clone, Debug, PartialEq)]
562enum Step {
563 Clear,
564 Alone(usize),
565 Shared(Run),
566}
567
568#[derive(Copy, Clone, Debug, PartialEq)]
569struct Run {
570 start: usize,
571 end: usize,
572 color: LoadOp<Color>,
573 depth: LoadOp<f32>,
574 depth_store: StoreOp,
575}
576
577struct Plan {
578 scene_end: usize,
579 shareable: bool,
580 background: Color,
581 depth_clear: f32,
582 at: usize,
583 opened: bool,
584 depth_written: bool,
585}
586
587impl Plan {
588 fn new(
589 passes: &[Box<dyn FramePass>],
590 shareable: bool,
591 background: Color,
592 depth_clear: f32,
593 ) -> Self {
594 Self {
595 scene_end: passes.partition_point(|pass| pass.stage() < PassStage::Overlay),
596 shareable,
597 background,
598 depth_clear,
599 at: 0,
600 opened: false,
601 depth_written: false,
602 }
603 }
604
605 fn joins(&self, pass: &dyn FramePass) -> bool {
606 self.shareable && pass.shares_pass()
607 }
608
609 fn next(&mut self, passes: &[Box<dyn FramePass>]) -> Option<Step> {
610 let scene = &passes[..self.scene_end];
611 if !self.opened {
612 self.opened = true;
613 if !scene.first().is_some_and(|pass| self.joins(pass.as_ref())) {
614 return Some(Step::Clear);
615 }
616 }
617 let start = self.at;
618 let first = scene.get(start)?;
619 if !self.joins(first.as_ref()) {
620 self.at += 1;
621 self.depth_written |= first.depth_convention().is_some();
622 return Some(Step::Alone(start));
623 }
624 let end = start
625 + 1
626 + scene[start + 1..]
627 .iter()
628 .take_while(|pass| {
629 self.joins(pass.as_ref()) && pass.color_load() == ColorLoad::Load
630 })
631 .count();
632 let color = match first.color_load() {
633 ColorLoad::Clear => LoadOp::Clear(first.clear_color()),
634 ColorLoad::Load if start == 0 => LoadOp::Clear(self.background),
635 ColorLoad::Load => LoadOp::Load,
636 };
637 let depth = if self.depth_written && first.color_load() == ColorLoad::Load {
638 LoadOp::Load
639 } else {
640 LoadOp::Clear(self.depth_clear)
641 };
642 let kept = passes[end..].iter().enumerate().any(|(offset, pass)| {
644 end + offset >= self.scene_end
645 || !self.joins(pass.as_ref())
646 || pass.depth_convention().is_some()
647 || pass.depth_read().is_some()
648 });
649 let writes = scene[start..end]
650 .iter()
651 .any(|pass| pass.depth_convention().is_some());
652 self.depth_written = kept && (self.depth_written || writes);
653 self.at = end;
654 Some(Step::Shared(Run {
655 start,
656 end,
657 color,
658 depth,
659 depth_store: if kept {
660 StoreOp::Store
661 } else {
662 StoreOp::Discard
663 },
664 }))
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 struct WrongVertexInput;
673
674 impl FramePass for WrongVertexInput {
675 fn name(&self) -> &'static str {
676 "wrong vertex input"
677 }
678
679 fn stage(&self) -> PassStage {
680 PassStage::Scene
681 }
682
683 fn record(
684 &mut self,
685 _encoder: &mut CommandEncoder,
686 _target: &FrameTarget<'_>,
687 ) -> anyhow::Result<()> {
688 Ok(())
689 }
690
691 fn attach(&mut self, gpu: &GpuContext, frame: FrameFormat) -> anyhow::Result<()> {
692 let shader = gpu
693 .device
694 .create_shader_module(wgpu::ShaderModuleDescriptor {
695 label: Some("wrong vertex input shader"),
696 source: wgpu::ShaderSource::Wgsl(
697 r#"
698struct VertexOutput {
699 @builtin(position) position: vec4<f32>,
700}
701
702@vertex
703fn vertex(@location(0) position: vec3<f32>) -> VertexOutput {
704 return VertexOutput(vec4<f32>(position, 1.0));
705}
706
707@fragment
708fn fragment() -> @location(0) vec4<f32> {
709 return vec4<f32>(1.0);
710}
711"#
712 .into(),
713 ),
714 });
715 let _pipeline = gpu
716 .device
717 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
718 label: Some("wrong vertex input pipeline"),
719 layout: None,
720 vertex: wgpu::VertexState {
721 module: &shader,
722 entry_point: Some("vertex"),
723 buffers: &[wgpu::VertexBufferLayout {
724 array_stride: 8,
725 step_mode: wgpu::VertexStepMode::Vertex,
726 attributes: &[wgpu::VertexAttribute {
727 format: wgpu::VertexFormat::Uint32x2,
728 offset: 0,
729 shader_location: 0,
730 }],
731 }],
732 compilation_options: Default::default(),
733 },
734 fragment: Some(wgpu::FragmentState {
735 module: &shader,
736 entry_point: Some("fragment"),
737 targets: &[Some(wgpu::ColorTargetState {
738 format: frame.color,
739 blend: None,
740 write_mask: wgpu::ColorWrites::ALL,
741 })],
742 compilation_options: Default::default(),
743 }),
744 primitive: Default::default(),
745 depth_stencil: None,
746 multisample: Default::default(),
747 multiview: None,
748 cache: None,
749 });
750 Ok(())
751 }
752 }
753
754 struct Probe {
755 name: &'static str,
756 convention: Option<DepthConvention>,
757 stage: PassStage,
758 }
759
760 impl FramePass for Probe {
761 fn name(&self) -> &'static str {
762 self.name
763 }
764
765 fn stage(&self) -> PassStage {
766 self.stage
767 }
768
769 fn depth_convention(&self) -> Option<DepthConvention> {
770 self.convention
771 }
772
773 fn record(
774 &mut self,
775 _encoder: &mut CommandEncoder,
776 _target: &FrameTarget<'_>,
777 ) -> anyhow::Result<()> {
778 Ok(())
779 }
780
781 fn attach(&mut self, _gpu: &GpuContext, _frame: FrameFormat) -> anyhow::Result<()> {
782 Ok(())
783 }
784 }
785
786 #[test]
787 fn an_uncaptured_pipeline_error_names_the_pass_and_validation_cause() {
788 let gpu = crate::device::noop_context();
789 let mut schedule = PassSchedule::new(DepthConvention::ReversedZ);
790 schedule.register(Box::new(WrongVertexInput)).unwrap();
791 let error = schedule
792 .attach(
793 &gpu,
794 FrameFormat {
795 color: TextureFormat::Rgba8Unorm,
796 depth: TextureFormat::Depth32Float,
797 },
798 )
799 .unwrap_err();
800 let PassExecutionError::Pass {
801 pass,
802 phase,
803 source,
804 } = error
805 else {
806 panic!("pipeline error was not attributed to its pass");
807 };
808 assert_eq!(pass, "wrong vertex input");
809 assert_eq!(phase, PassPhase::Attach);
810 let cause = format!("{source:#}");
811 assert!(
812 cause.contains("Input type is not compatible"),
813 "unexpected validation cause: {cause}"
814 );
815 }
816
817 fn probe(name: &'static str) -> Box<Probe> {
818 Box::new(Probe {
819 name,
820 convention: None,
821 stage: PassStage::Scene,
822 })
823 }
824
825 #[test]
826 fn a_pass_writing_depth_under_the_other_convention_is_refused_at_registration() {
827 let mut schedule = PassSchedule::new(DepthConvention::ReversedZ);
828 let refused = schedule.register(Box::new(Probe {
829 name: "standard-z overlay",
830 convention: Some(DepthConvention::StandardZ),
831 stage: PassStage::Scene,
832 }));
833 assert_eq!(
834 refused,
835 Err(PassError::DepthConvention {
836 pass: "standard-z overlay",
837 declared: DepthConvention::StandardZ,
838 frame: DepthConvention::ReversedZ,
839 })
840 );
841 assert_eq!(schedule.names().count(), 0);
842 }
843
844 struct Clearing {
845 stage: PassStage,
846 }
847
848 impl FramePass for Clearing {
849 fn name(&self) -> &'static str {
850 "clearing"
851 }
852
853 fn stage(&self) -> PassStage {
854 self.stage
855 }
856
857 fn color_load(&self) -> ColorLoad {
858 ColorLoad::Clear
859 }
860
861 fn record(
862 &mut self,
863 _encoder: &mut CommandEncoder,
864 _target: &FrameTarget<'_>,
865 ) -> anyhow::Result<()> {
866 Ok(())
867 }
868
869 fn attach(&mut self, _gpu: &GpuContext, _frame: FrameFormat) -> anyhow::Result<()> {
870 Ok(())
871 }
872 }
873
874 struct Reader;
875
876 impl FramePass for Reader {
877 fn name(&self) -> &'static str {
878 "reader"
879 }
880
881 fn stage(&self) -> PassStage {
882 PassStage::Scene
883 }
884
885 fn depth_read(&self) -> Option<DepthConvention> {
886 Some(DepthConvention::StandardZ)
887 }
888
889 fn record(
890 &mut self,
891 _encoder: &mut CommandEncoder,
892 _target: &FrameTarget<'_>,
893 ) -> anyhow::Result<()> {
894 Ok(())
895 }
896
897 fn attach(&mut self, _gpu: &GpuContext, _frame: FrameFormat) -> anyhow::Result<()> {
898 Ok(())
899 }
900 }
901
902 struct Flaky {
903 fail: std::sync::Arc<std::sync::atomic::AtomicBool>,
904 recorded: std::sync::Arc<std::sync::atomic::AtomicU32>,
905 }
906
907 impl FramePass for Flaky {
908 fn name(&self) -> &'static str {
909 "flaky"
910 }
911
912 fn stage(&self) -> PassStage {
913 PassStage::Scene
914 }
915
916 fn record(
917 &mut self,
918 _encoder: &mut CommandEncoder,
919 _target: &FrameTarget<'_>,
920 ) -> anyhow::Result<()> {
921 self.recorded
922 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
923 Ok(())
924 }
925
926 fn attach(&mut self, _gpu: &GpuContext, _frame: FrameFormat) -> anyhow::Result<()> {
927 if self.fail.load(std::sync::atomic::Ordering::Relaxed) {
928 anyhow::bail!("the pass could not build its resources");
929 }
930 Ok(())
931 }
932 }
933
934 #[test]
935 fn a_pass_that_clears_the_shared_color_target_is_refused_outside_the_background() {
936 let mut schedule = PassSchedule::new(DepthConvention::ReversedZ);
937 assert!(matches!(
938 schedule.register(Box::new(Clearing {
939 stage: PassStage::Scene
940 })),
941 Err(PassError::ColorClear {
942 pass: "clearing",
943 stage: PassStage::Scene
944 })
945 ));
946 assert!(matches!(
947 schedule.register(Box::new(Clearing {
948 stage: PassStage::Overlay
949 })),
950 Err(PassError::ColorClear { .. })
951 ));
952 schedule
953 .register(Box::new(Clearing {
954 stage: PassStage::Background,
955 }))
956 .expect("a background pass may clear");
957 }
958
959 #[test]
960 fn a_depth_reader_under_the_other_convention_is_refused_at_registration() {
961 let mut schedule = PassSchedule::new(DepthConvention::ReversedZ);
962 assert!(matches!(
963 schedule.register(Box::new(Reader)),
964 Err(PassError::DepthRead {
965 pass: "reader",
966 declared: DepthConvention::StandardZ,
967 frame: DepthConvention::ReversedZ
968 })
969 ));
970 }
971
972 #[test]
973 fn a_failed_attach_suppresses_recording_until_every_pass_attaches() {
974 use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
975 use std::sync::Arc;
976
977 let gpu = crate::device::noop_context();
978 let fail = Arc::new(AtomicBool::new(true));
979 let recorded = Arc::new(AtomicU32::new(0));
980 let mut schedule = PassSchedule::new(DepthConvention::ReversedZ);
981 schedule
982 .register(Box::new(Flaky {
983 fail: fail.clone(),
984 recorded: recorded.clone(),
985 }))
986 .unwrap();
987 let frame = FrameFormat {
988 color: TextureFormat::Rgba8Unorm,
989 depth: TextureFormat::Depth32Float,
990 };
991 let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
992 label: Some("flaky target"),
993 size: wgpu::Extent3d {
994 width: 8,
995 height: 8,
996 depth_or_array_layers: 1,
997 },
998 mip_level_count: 1,
999 sample_count: 1,
1000 dimension: wgpu::TextureDimension::D2,
1001 format: frame.color,
1002 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1003 view_formats: &[],
1004 });
1005 let color = texture.create_view(&wgpu::TextureViewDescriptor::default());
1006 let target = FrameTarget {
1007 color: &color,
1008 depth: None,
1009 size: (8, 8),
1010 };
1011
1012 assert!(schedule.attach(&gpu, frame).is_err());
1013 assert_eq!(schedule.unattached(), Some("flaky"));
1014 let mut encoder = gpu
1015 .device
1016 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
1017 schedule
1018 .record(PassStage::Scene, &mut encoder, &target)
1019 .expect("recording is a no-op while a pass is unattached");
1020 assert_eq!(recorded.load(Ordering::Relaxed), 0);
1021
1022 fail.store(false, Ordering::Relaxed);
1023 schedule.attach(&gpu, frame).expect("the retry attaches");
1024 assert_eq!(schedule.unattached(), None);
1025 schedule
1026 .record(PassStage::Scene, &mut encoder, &target)
1027 .expect("recorded");
1028 assert_eq!(recorded.load(Ordering::Relaxed), 1);
1029 }
1030
1031 #[test]
1032 fn a_stage_orders_a_pass_ahead_of_one_registered_before_it() {
1033 let mut schedule = PassSchedule::new(DepthConvention::ReversedZ);
1034 schedule
1035 .register(Box::new(Probe {
1036 name: "overlay",
1037 convention: None,
1038 stage: PassStage::Overlay,
1039 }))
1040 .unwrap();
1041 schedule.register(probe("scene")).unwrap();
1042 schedule
1043 .register(Box::new(Probe {
1044 name: "background",
1045 convention: None,
1046 stage: PassStage::Background,
1047 }))
1048 .unwrap();
1049 assert_eq!(
1050 schedule.names().collect::<Vec<_>>(),
1051 ["background", "scene", "overlay"]
1052 );
1053 }
1054
1055 struct Planned {
1056 stage: PassStage,
1057 shares: bool,
1058 load: ColorLoad,
1059 writes: bool,
1060 reads: bool,
1061 }
1062
1063 impl FramePass for Planned {
1064 fn name(&self) -> &'static str {
1065 "planned"
1066 }
1067
1068 fn stage(&self) -> PassStage {
1069 self.stage
1070 }
1071
1072 fn color_load(&self) -> ColorLoad {
1073 self.load
1074 }
1075
1076 fn depth_convention(&self) -> Option<DepthConvention> {
1077 self.writes.then_some(DepthConvention::ReversedZ)
1078 }
1079
1080 fn depth_read(&self) -> Option<DepthConvention> {
1081 self.reads.then_some(DepthConvention::ReversedZ)
1082 }
1083
1084 fn shares_pass(&self) -> bool {
1085 self.shares
1086 }
1087
1088 fn clear_color(&self) -> Color {
1089 HORIZON
1090 }
1091
1092 fn record(
1093 &mut self,
1094 _encoder: &mut CommandEncoder,
1095 _target: &FrameTarget<'_>,
1096 ) -> anyhow::Result<()> {
1097 Ok(())
1098 }
1099
1100 fn attach(&mut self, _gpu: &GpuContext, _frame: FrameFormat) -> anyhow::Result<()> {
1101 Ok(())
1102 }
1103 }
1104
1105 const HORIZON: Color = Color::GREEN;
1106 const BACKGROUND: Color = Color::BLUE;
1107
1108 fn sky() -> Box<dyn FramePass> {
1109 Box::new(Planned {
1110 stage: PassStage::Background,
1111 shares: true,
1112 load: ColorLoad::Clear,
1113 writes: true,
1114 reads: false,
1115 })
1116 }
1117
1118 fn faces() -> Box<dyn FramePass> {
1119 Box::new(Planned {
1120 stage: PassStage::Scene,
1121 shares: true,
1122 load: ColorLoad::Load,
1123 writes: true,
1124 reads: true,
1125 })
1126 }
1127
1128 fn unshared(stage: PassStage, reads: bool) -> Box<dyn FramePass> {
1129 Box::new(Planned {
1130 stage,
1131 shares: false,
1132 load: ColorLoad::Load,
1133 writes: false,
1134 reads,
1135 })
1136 }
1137
1138 fn steps(passes: &[Box<dyn FramePass>]) -> Vec<Step> {
1139 let mut plan = Plan::new(passes, true, BACKGROUND, DEPTH_CLEAR);
1140 std::iter::from_fn(|| plan.next(passes)).collect()
1141 }
1142
1143 fn shared(
1144 passes: std::ops::Range<usize>,
1145 color: LoadOp<Color>,
1146 depth: LoadOp<f32>,
1147 depth_store: StoreOp,
1148 ) -> Step {
1149 Step::Shared(Run {
1150 start: passes.start,
1151 end: passes.end,
1152 color,
1153 depth,
1154 depth_store,
1155 })
1156 }
1157
1158 #[test]
1159 fn a_shared_pass_that_opens_the_frame_clears_the_depth_the_last_frame_left() {
1160 assert_eq!(
1161 steps(&[faces(), faces()]),
1162 [shared(
1163 0..2,
1164 LoadOp::Clear(BACKGROUND),
1165 LoadOp::Clear(DEPTH_CLEAR),
1166 StoreOp::Discard
1167 )]
1168 );
1169 }
1170
1171 #[test]
1172 fn a_depth_reader_in_the_overlay_keeps_the_scene_depth_stored() {
1173 let mut passes = vec![sky(), faces(), faces()];
1174 let opened = |depth_store| {
1175 [shared(
1176 0..3,
1177 LoadOp::Clear(HORIZON),
1178 LoadOp::Clear(DEPTH_CLEAR),
1179 depth_store,
1180 )]
1181 };
1182 assert_eq!(steps(&passes), opened(StoreOp::Discard));
1183 passes.push(unshared(PassStage::Overlay, true));
1184 assert_eq!(steps(&passes), opened(StoreOp::Store));
1185 }
1186
1187 #[test]
1188 fn a_sky_after_a_depth_writing_pass_clears_depth_as_its_own_pass_did() {
1189 let writer = Box::new(Planned {
1190 stage: PassStage::Background,
1191 shares: false,
1192 load: ColorLoad::Load,
1193 writes: true,
1194 reads: false,
1195 });
1196 assert_eq!(
1197 steps(&[writer, sky(), faces()]),
1198 [
1199 Step::Clear,
1200 Step::Alone(0),
1201 shared(
1202 1..3,
1203 LoadOp::Clear(HORIZON),
1204 LoadOp::Clear(DEPTH_CLEAR),
1205 StoreOp::Discard
1206 ),
1207 ]
1208 );
1209 }
1210
1211 #[test]
1212 fn a_pass_that_does_not_share_splits_the_run_and_loads_what_was_drawn_before_it() {
1213 assert_eq!(
1214 steps(&[sky(), unshared(PassStage::Scene, false), faces()]),
1215 [
1216 shared(
1217 0..1,
1218 LoadOp::Clear(HORIZON),
1219 LoadOp::Clear(DEPTH_CLEAR),
1220 StoreOp::Store
1221 ),
1222 Step::Alone(1),
1223 shared(2..3, LoadOp::Load, LoadOp::Load, StoreOp::Discard),
1224 ]
1225 );
1226 }
1227}