Skip to main content

loam_render/
pass.rs

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    /// `None` means the pass writes no depth; `Some` must match the frame's convention to register.
81    fn depth_convention(&self) -> Option<DepthConvention> {
82        None
83    }
84
85    fn depth_read(&self) -> Option<DepthConvention> {
86        None
87    }
88
89    /// True joins the stage's shared render pass, so `attach` must build pipelines against the frame's color and depth formats.
90    fn shares_pass(&self) -> bool {
91        false
92    }
93
94    /// Read before `prepare`, and only when `color_load` is `Clear`.
95    fn clear_color(&self) -> Color {
96        Color::BLACK
97    }
98
99    /// Encoder work for a sharing pass; runs just before its render pass opens, not at frame start.
100    fn prepare(
101        &mut self,
102        _encoder: &mut CommandEncoder,
103        _target: &FrameTarget<'_>,
104    ) -> anyhow::Result<()> {
105        Ok(())
106    }
107
108    /// Runs inside the shared pass, which owns the load ops; viewport and scissor carry over from earlier draws.
109    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    /// No timer, no free slot, or no result mapped yet; `Measured` can hold zero for a trivial pass.
177    Unavailable,
178    Measured(Duration),
179}
180
181#[derive(Copy, Clone, Debug, PartialEq, Eq)]
182pub struct Section {
183    pub name: &'static str,
184    /// Time spent recording the section into the encoder, not running it.
185    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    /// Replaces `record` for Background and Scene and includes the frame clear.
313    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        // Timestamps resolve only at render pass boundaries, so members share one GPU slot.
414        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        // Only a sharing pass is held to its depth_read declaration.
643        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}