Skip to main content

loam_render/
present.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use glam::Vec2;
5use loam_runtime::{DomainId, Eye, PublishedView, Rigid, SegmentRecord, Stamp, ViewTarget};
6use wgpu::{Color, CommandEncoder, Device, Queue, RenderPass, TextureFormat, TextureView};
7
8use crate::depth::DepthBuffer;
9use crate::device::GpuContext;
10use crate::pass::{
11    FrameFormat, FramePass, FrameTarget, PassError, PassExecutionError, PassSchedule, PassStage,
12    Section,
13};
14use crate::triangle_pass::TriangleFeed;
15use crate::view::DEPTH_FORMAT;
16use crate::{DepthConvention, DepthMode, FragmentShading, LineRasterNode};
17
18struct ViewLines {
19    opaque: LineRasterNode,
20    translucent: LineRasterNode,
21    scratch: Vec<SegmentRecord>,
22    uploaded: Option<(DomainId, ViewTarget, Stamp)>,
23    camera: Option<(glam::Mat4, Vec2)>,
24}
25
26impl ViewLines {
27    fn new(device: &Device, format: TextureFormat) -> Self {
28        Self {
29            opaque: LineRasterNode::new(
30                device,
31                format,
32                DepthMode::ReadWrite {
33                    format: DEPTH_FORMAT,
34                },
35                DepthConvention::ReversedZ,
36            ),
37            translucent: LineRasterNode::new(
38                device,
39                format,
40                DepthMode::ReadOnly {
41                    format: DEPTH_FORMAT,
42                },
43                DepthConvention::ReversedZ,
44            ),
45            scratch: Vec::new(),
46            uploaded: None,
47            camera: None,
48        }
49    }
50
51    fn set_camera(&mut self, queue: &Queue, camera: glam::Mat4, viewport: Vec2) {
52        if self.camera == Some((camera, viewport)) {
53            return;
54        }
55        self.camera = Some((camera, viewport));
56        self.opaque.set_camera(queue, camera, viewport);
57        self.translucent.set_camera(queue, camera, viewport);
58    }
59
60    fn upload_segments(&mut self, device: &Device, queue: &Queue, segments: &[SegmentRecord]) {
61        self.scratch.clear();
62        self.scratch
63            .extend(segments.iter().copied().filter(segment_is_opaque));
64        self.opaque.upload_segments(device, queue, &self.scratch);
65        self.scratch.clear();
66        self.scratch.extend(
67            segments
68                .iter()
69                .copied()
70                .filter(|segment| !segment_is_opaque(segment)),
71        );
72        self.translucent
73            .upload_segments(device, queue, &self.scratch);
74    }
75}
76
77struct PublishedLines {
78    views: Rc<RefCell<Vec<ViewLines>>>,
79}
80
81impl FramePass for PublishedLines {
82    fn name(&self) -> &'static str {
83        "present-draw"
84    }
85
86    fn stage(&self) -> PassStage {
87        PassStage::Scene
88    }
89
90    fn depth_convention(&self) -> Option<DepthConvention> {
91        Some(DepthConvention::ReversedZ)
92    }
93
94    fn depth_read(&self) -> Option<DepthConvention> {
95        Some(DepthConvention::ReversedZ)
96    }
97
98    fn shares_pass(&self) -> bool {
99        true
100    }
101
102    fn draw(&mut self, pass: &mut RenderPass<'_>, _target: &FrameTarget<'_>) -> anyhow::Result<()> {
103        let views = self.views.borrow();
104        for slot in views.iter() {
105            slot.opaque.draw(pass, None);
106        }
107        for slot in views.iter() {
108            slot.translucent.draw(pass, None);
109        }
110        Ok(())
111    }
112
113    fn record(
114        &mut self,
115        encoder: &mut CommandEncoder,
116        target: &FrameTarget<'_>,
117    ) -> anyhow::Result<()> {
118        let Some(depth) = target.depth else {
119            return Ok(());
120        };
121        let views = self.views.borrow();
122        for slot in views.iter() {
123            slot.opaque.record(encoder, target.color, Some(depth), None);
124        }
125        for slot in views.iter() {
126            slot.translucent
127                .record(encoder, target.color, Some(depth), None);
128        }
129        Ok(())
130    }
131
132    fn attach(&mut self, _gpu: &GpuContext, _frame: FrameFormat) -> anyhow::Result<()> {
133        Ok(())
134    }
135}
136
137pub struct Presenter {
138    uploads: u64,
139    format: TextureFormat,
140    depth: Option<DepthBuffer>,
141    views: Rc<RefCell<Vec<ViewLines>>>,
142    fills: TriangleFeed,
143    filled: Vec<(DomainId, ViewTarget)>,
144    schedule: PassSchedule,
145}
146
147impl Presenter {
148    pub fn new(format: TextureFormat) -> Result<Self, PassError> {
149        let fills = TriangleFeed::default();
150        let views = Rc::new(RefCell::new(Vec::new()));
151        let mut schedule = PassSchedule::new(DepthConvention::ReversedZ);
152        schedule.register(fills.pass(FragmentShading::FaceNormalLambert))?;
153        schedule.register(Box::new(PublishedLines {
154            views: views.clone(),
155        }))?;
156        Ok(Self {
157            format,
158            uploads: 0,
159            depth: None,
160            views,
161            fills,
162            filled: Vec::new(),
163            schedule,
164        })
165    }
166
167    pub fn attach(&mut self, gpu: &GpuContext) -> Result<(), PassExecutionError> {
168        self.depth = None;
169        self.views.borrow_mut().clear();
170        self.schedule.attach(
171            gpu,
172            FrameFormat {
173                color: self.format,
174                depth: DEPTH_FORMAT,
175            },
176        )
177    }
178
179    pub fn register_pass(&mut self, pass: Box<dyn FramePass>) -> Result<(), PassError> {
180        self.schedule.register(pass)
181    }
182
183    pub fn uploads(&self) -> u64 {
184        self.uploads
185    }
186
187    pub fn sections(&self) -> &[Section] {
188        self.schedule.sections()
189    }
190
191    pub fn after_submit(&mut self) {
192        self.schedule.after_submit();
193    }
194
195    pub fn upload(
196        &mut self,
197        device: &Device,
198        queue: &Queue,
199        eye: &Eye,
200        viewport: Vec2,
201        views: &[PublishedView],
202    ) {
203        let _scope = loam_time::frame_trace::scope("present-upload");
204        let mut line_views = self.views.borrow_mut();
205        while line_views.len() < views.len() {
206            line_views.push(ViewLines::new(device, self.format));
207        }
208        line_views.truncate(views.len());
209        let mut rebuilt = false;
210        for (slot, view) in line_views.iter_mut().zip(views) {
211            let camera = crate::view::placed_view_projection(eye, view.placement);
212            slot.set_camera(queue, camera, viewport);
213            let published = (view.domain, view.target, view.records.built());
214            if slot.uploaded == Some(published) {
215                continue;
216            }
217            slot.uploaded = Some(published);
218            rebuilt = true;
219            self.uploads += 1;
220            slot.upload_segments(device, queue, view.records.segments());
221        }
222        drop(line_views);
223        self.fills.set_view(eye, Rigid::IDENTITY);
224        let listed = self.filled.len() == views.len()
225            && (self.filled.iter().zip(views))
226                .all(|(held, view)| *held == (view.domain, view.target));
227        if rebuilt || !listed {
228            self.filled.clear();
229            self.filled
230                .extend(views.iter().map(|view| (view.domain, view.target)));
231            self.fills.edit(|mesh| {
232                mesh.vertices.clear();
233                mesh.colors.clear();
234                mesh.indices.clear();
235                for view in views {
236                    for triangle in view.records.triangles() {
237                        let base = mesh.vertices.len() as u32;
238                        for corner in triangle.vertices {
239                            mesh.vertices.push(view.placement.apply(corner));
240                            mesh.colors.push(triangle.color);
241                        }
242                        mesh.indices.push([base, base + 1, base + 2]);
243                    }
244                }
245            });
246        }
247    }
248
249    pub fn record_scene(
250        &mut self,
251        device: &Device,
252        encoder: &mut CommandEncoder,
253        target: &TextureView,
254        size: (u32, u32),
255        background: Color,
256    ) -> Result<(), PassExecutionError> {
257        self.schedule.begin_frame();
258        DepthBuffer::ensure(&mut self.depth, device, DEPTH_FORMAT, size);
259        let Some(depth) = self.depth.as_ref() else {
260            return Ok(());
261        };
262        let frame = FrameTarget {
263            color: target,
264            depth: Some(&depth.view),
265            size,
266        };
267        self.schedule.record_scene(encoder, &frame, background)
268    }
269
270    /// Call after `record_scene` with the same target and encoder.
271    pub fn record_overlays(
272        &mut self,
273        encoder: &mut CommandEncoder,
274        target: &TextureView,
275        size: (u32, u32),
276    ) -> Result<(), PassExecutionError> {
277        let Some(depth) = self.depth.as_ref() else {
278            return Ok(());
279        };
280        let frame = FrameTarget {
281            color: target,
282            depth: Some(&depth.view),
283            size,
284        };
285        self.schedule.record(PassStage::Overlay, encoder, &frame)?;
286        self.schedule.end_frame(encoder);
287        Ok(())
288    }
289}
290
291fn segment_is_opaque(segment: &SegmentRecord) -> bool {
292    segment.start_color[3] >= 1.0 && segment.end_color[3] >= 1.0
293}
294
295#[cfg(test)]
296mod tests {
297    use crate::pass::ColorLoad;
298    use loam_math::EuclideanR4;
299    use loam_runtime::{
300        DomainBuilder, Instance, LogCapacity, Material, Pose, PreparedGeometry, Projection4,
301        Records, Session, SimConfig, SpawnBundle, ViewSpec,
302    };
303    use wgpu::{
304        BackendOptions, Backends, InstanceDescriptor, LoadOp, NoopBackendOptions, Operations,
305        RenderPassColorAttachment, RenderPassDescriptor, StoreOp, TextureDescriptor,
306        TextureDimension, TextureFormat, TextureUsages,
307    };
308
309    use super::*;
310
311    #[global_allocator]
312    static COUNTING_ALLOCATOR: loam_time::alloc::CountingAllocator<std::alloc::System> =
313        loam_time::alloc::CountingAllocator::new(std::alloc::System);
314
315    loam_runtime::stores! {
316        #[derive(Default)]
317        pub struct Spun {
318            spin: Store<f32>,
319        }
320    }
321
322    fn noop_device() -> (Device, Queue) {
323        let instance = wgpu::Instance::new(&InstanceDescriptor {
324            backends: Backends::NOOP,
325            backend_options: BackendOptions {
326                noop: NoopBackendOptions { enable: true },
327                ..Default::default()
328            },
329            ..Default::default()
330        });
331        let adapter = pollster::block_on(instance.request_adapter(&Default::default()))
332            .expect("noop backend always yields an adapter");
333        pollster::block_on(adapter.request_device(&Default::default()))
334            .expect("noop adapter always yields a device")
335    }
336
337    #[test]
338    fn warmed_publish_allocates_or_the_presenter_reuploads_unchanged_records() {
339        let mut session = Session::new(Spun::default(), SimConfig::default());
340        let r4 = session
341            .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
342        let edges = session.prepare(PreparedGeometry::Lines4 {
343            segments: (0..64)
344                .map(|i| {
345                    let t = i as f32 * 0.01;
346                    [[t, 0.0, 0.0, 0.0], [t, 1.0, 0.0, 0.5]]
347                })
348                .collect(),
349        });
350        let white = session.add_material(Material::lines([1.0; 4], 2.0));
351        let root = session.views().root();
352        let spun = session.dispatch(|d| {
353            let eye = d
354                .spawn(SpawnBundle::new().at(r4, Pose::at(glam::Vec4::ZERO)))
355                .unwrap();
356            d.domains
357                .typed(r4)
358                .unwrap()
359                .add_view(ViewSpec::new(root, eye, Projection4 { focal: 2.0 }))
360                .unwrap();
361            d.spawn(
362                SpawnBundle::new()
363                    .at(r4, Pose::at(glam::Vec4::NEG_Z * 4.0))
364                    .instance(Instance::new(edges, white))
365                    .row(0.0_f32),
366            )
367            .unwrap()
368        });
369
370        let (device, queue) = noop_device();
371        let target = device
372            .create_texture(&TextureDescriptor {
373                label: None,
374                size: wgpu::Extent3d {
375                    width: 64,
376                    height: 64,
377                    depth_or_array_layers: 1,
378                },
379                mip_level_count: 1,
380                sample_count: 1,
381                dimension: TextureDimension::D2,
382                format: TextureFormat::Rgba8Unorm,
383                usage: TextureUsages::RENDER_ATTACHMENT,
384                view_formats: &[],
385            })
386            .create_view(&Default::default());
387        let mut presenter = Presenter::new(TextureFormat::Rgba8Unorm).expect("presenter");
388        let mut records = Records::default();
389        let eye = Eye::default();
390        let spin = |session: &mut Session<Spun>| {
391            if let Ok(domain) = session.domains_mut().typed(r4) {
392                if let Some(pose) = domain.poses().get(spun) {
393                    let mut point = pose.point;
394                    point.x += 0.01;
395                    domain.set_point(spun, point).unwrap();
396                }
397            }
398        };
399        let present = |session: &mut Session<Spun>,
400                       records: &mut Records,
401                       presenter: &mut Presenter,
402                       encoder: &mut CommandEncoder| {
403            records.publish(session).unwrap();
404            let published = records.lend().unwrap();
405            presenter.upload(&device, &queue, &eye, Vec2::splat(64.0), &published.views);
406            records.release(published);
407            presenter
408                .record_scene(&device, encoder, &target, (64, 64), Color::BLACK)
409                .expect("recorded the scene");
410            presenter
411                .record_overlays(encoder, &target, (64, 64))
412                .expect("recorded the overlays");
413        };
414
415        let mut encoder = device.create_command_encoder(&Default::default());
416        for _ in 0..8 {
417            spin(&mut session);
418            present(&mut session, &mut records, &mut presenter, &mut encoder);
419        }
420
421        let published = loam_time::alloc::bytes_allocated_by(|| {
422            for _ in 0..16 {
423                spin(&mut session);
424                records.publish(&mut session).unwrap();
425            }
426        })
427        .expect("the counting allocator is installed");
428        assert_eq!(
429            published, 0,
430            "16 warmed publications asked the allocator for {published} bytes"
431        );
432
433        present(&mut session, &mut records, &mut presenter, &mut encoder);
434        let before = presenter.uploads();
435        let idle = loam_time::alloc::bytes_allocated_by(|| {
436            for _ in 0..16 {
437                present(&mut session, &mut records, &mut presenter, &mut encoder);
438            }
439            assert_eq!(
440                presenter.uploads(),
441                before,
442                "an idle presenter uploaded a view"
443            );
444        })
445        .expect("the counting allocator is installed");
446        let busy = loam_time::alloc::bytes_allocated_by(|| {
447            for _ in 0..16 {
448                spin(&mut session);
449                present(&mut session, &mut records, &mut presenter, &mut encoder);
450            }
451            assert_eq!(
452                presenter.uploads(),
453                before + 16,
454                "sixteen changed publications uploaded the view {} times",
455                presenter.uploads() - before
456            );
457        })
458        .expect("the counting allocator is installed");
459        assert!(
460            idle < busy,
461            "the presenter uploads records it already holds: {idle} bytes idle against {busy} busy"
462        );
463    }
464
465    #[test]
466    fn a_view_dropped_from_the_publication_takes_its_section_fills_with_it() {
467        use loam_runtime::{Section4, ViewId};
468
469        let mut session = Session::new(Spun::default(), SimConfig::default());
470        let r4 = session
471            .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
472        let geometry = session.prepare(PreparedGeometry::Polytope4 {
473            polytope: loam_shape::polytope::Polytope4::Tesseract,
474            scale: 0.7,
475        });
476        let body = session.add_material(Material::lines([1.0; 4], 1.0));
477        let cut = session.add_material(Material::lines([1.0, 0.85, 0.35, 1.0], 2.0));
478        let root = session.views().root();
479        session.dispatch(|d| {
480            let eye = d
481                .spawn(SpawnBundle::new().at(r4, Pose::at(glam::Vec4::ZERO)))
482                .expect("the eye spawned");
483            d.spawn(
484                SpawnBundle::new()
485                    .at(r4, Pose::at(glam::Vec4::NEG_Z * 4.0))
486                    .instance(Instance::new(geometry, body).sectioned(cut))
487                    .row(0.0_f32),
488            )
489            .expect("the body spawned");
490            let domain = d.domains.typed(r4).expect("the r4 domain");
491            let _: ViewId = domain
492                .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))
493                .expect("the first view");
494            domain
495                .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))
496                .expect("the second view")
497        });
498
499        let (device, queue) = noop_device();
500        let mut presenter = Presenter::new(TextureFormat::Rgba8Unorm).expect("presenter");
501        let mut records = Records::default();
502        let eye = Eye::default();
503        records.publish(&mut session).expect("published");
504        let published = records.lend().expect("the buffer is free");
505
506        presenter.upload(&device, &queue, &eye, Vec2::splat(64.0), &published.views);
507        let both = presenter.fills.triangles();
508        assert_eq!(
509            both,
510            24 * 2,
511            "each of the two cut layers fans the tesseract's six straddling cells into four triangles, not {both}"
512        );
513
514        presenter.upload(
515            &device,
516            &queue,
517            &eye,
518            Vec2::splat(64.0),
519            &published.views[..1],
520        );
521        assert_eq!(
522            presenter.fills.triangles(),
523            24,
524            "the dropped view left its section fills on screen"
525        );
526        records.release(published);
527    }
528
529    #[test]
530    fn dropping_the_first_of_two_equally_stamped_views_skips_the_survivors_lines() {
531        let mut session = Session::new(Spun::default(), SimConfig::default());
532        let white = session.add_material(Material::lines([1.0; 4], 1.0));
533        let root = session.views().root();
534        for (name, polytope) in [
535            ("wide", loam_shape::polytope::Polytope4::Tesseract),
536            ("narrow", loam_shape::polytope::Polytope4::Pentatope),
537        ] {
538            let domain = session.register_domain(
539                DomainBuilder::new(name, EuclideanR4).tracked(LogCapacity::default()),
540            );
541            let geometry = session.prepare(PreparedGeometry::Polytope4 {
542                polytope,
543                scale: 0.7,
544            });
545            session.dispatch(|d| {
546                let eye = d
547                    .spawn(SpawnBundle::new().at(domain, Pose::at(glam::Vec4::ZERO)))
548                    .expect("the eye spawned");
549                d.spawn(
550                    SpawnBundle::new()
551                        .at(domain, Pose::at(glam::Vec4::NEG_Z * 4.0))
552                        .instance(Instance::new(geometry, white))
553                        .row(0.0_f32),
554                )
555                .expect("the body spawned");
556                d.domains
557                    .typed(domain)
558                    .expect("the domain")
559                    .add_view(ViewSpec::new(root, eye, Projection4 { focal: 2.0 }))
560                    .expect("the view")
561            });
562        }
563
564        let (device, queue) = noop_device();
565        let mut presenter = Presenter::new(TextureFormat::Rgba8Unorm).expect("presenter");
566        let mut records = Records::default();
567        let eye = Eye::default();
568        records.publish(&mut session).expect("published");
569        let published = records.lend().expect("the buffer is free");
570        let dropped = &published.views[0].records;
571        let kept = &published.views[1].records;
572        assert!(
573            dropped.built() == kept.built() && dropped.segments().len() != kept.segments().len(),
574            "the probe needs two equally stamped views whose lines differ"
575        );
576        let survivor = kept.segments().len() as u32;
577
578        presenter.upload(&device, &queue, &eye, Vec2::splat(64.0), &published.views);
579        presenter.upload(
580            &device,
581            &queue,
582            &eye,
583            Vec2::splat(64.0),
584            &published.views[1..],
585        );
586        let line_views = presenter.views.borrow();
587        assert_eq!(
588            line_views[0].opaque.segment_count() + line_views[0].translucent.segment_count(),
589            survivor,
590            "the first slot still holds the dropped view's lines"
591        );
592        records.release(published);
593    }
594
595    const PROBE_SIZE: u32 = 64;
596    const PAINT: Color = Color {
597        r: 1.0,
598        g: 0.0,
599        b: 0.0,
600        a: 1.0,
601    };
602
603    struct Paint;
604
605    impl FramePass for Paint {
606        fn name(&self) -> &'static str {
607            "paint"
608        }
609
610        fn stage(&self) -> PassStage {
611            PassStage::Background
612        }
613
614        fn color_load(&self) -> ColorLoad {
615            ColorLoad::Clear
616        }
617
618        fn record(
619            &mut self,
620            encoder: &mut CommandEncoder,
621            target: &FrameTarget<'_>,
622        ) -> anyhow::Result<()> {
623            encoder.begin_render_pass(&RenderPassDescriptor {
624                label: Some("paint"),
625                color_attachments: &[Some(RenderPassColorAttachment {
626                    view: target.color,
627                    depth_slice: None,
628                    resolve_target: None,
629                    ops: Operations {
630                        load: LoadOp::Clear(PAINT),
631                        store: StoreOp::Store,
632                    },
633                })],
634                depth_stencil_attachment: None,
635                timestamp_writes: None,
636                occlusion_query_set: None,
637            });
638            Ok(())
639        }
640
641        fn attach(&mut self, _gpu: &GpuContext, _frame: FrameFormat) -> anyhow::Result<()> {
642            Ok(())
643        }
644    }
645
646    fn pixel_at(gpu: &GpuContext, texture: &wgpu::Texture, at: [u32; 2]) -> [u8; 4] {
647        let row = PROBE_SIZE * 4;
648        let staging = gpu.device.create_buffer(&wgpu::BufferDescriptor {
649            label: None,
650            size: u64::from(row) * u64::from(PROBE_SIZE),
651            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
652            mapped_at_creation: false,
653        });
654        let mut encoder = gpu.device.create_command_encoder(&Default::default());
655        encoder.copy_texture_to_buffer(
656            wgpu::TexelCopyTextureInfo {
657                texture,
658                mip_level: 0,
659                origin: wgpu::Origin3d::ZERO,
660                aspect: wgpu::TextureAspect::All,
661            },
662            wgpu::TexelCopyBufferInfo {
663                buffer: &staging,
664                layout: wgpu::TexelCopyBufferLayout {
665                    offset: 0,
666                    bytes_per_row: Some(row),
667                    rows_per_image: None,
668                },
669            },
670            wgpu::Extent3d {
671                width: PROBE_SIZE,
672                height: PROBE_SIZE,
673                depth_or_array_layers: 1,
674            },
675        );
676        gpu.queue.submit(Some(encoder.finish()));
677        let slice = staging.slice(..);
678        let (sender, receiver) = std::sync::mpsc::channel();
679        slice.map_async(wgpu::MapMode::Read, move |result| {
680            let _ = sender.send(result);
681        });
682        gpu.device
683            .poll(wgpu::PollType::Wait {
684                submission_index: None,
685                timeout: None,
686            })
687            .expect("the readback polled");
688        receiver
689            .recv()
690            .expect("the map callback ran")
691            .expect("the staging buffer mapped");
692        let data = slice.get_mapped_range();
693        let offset = ((at[1] * PROBE_SIZE + at[0]) * 4) as usize;
694        let pixel = [
695            data[offset],
696            data[offset + 1],
697            data[offset + 2],
698            data[offset + 3],
699        ];
700        drop(data);
701        staging.unmap();
702        pixel
703    }
704
705    #[test]
706    #[ignore = "requires a working wgpu adapter; run with --include-ignored"]
707    fn a_pass_before_the_scene_reaches_the_presented_frame_gpu_probe() {
708        let gpu = pollster::block_on(GpuContext::new(
709            wgpu::Instance::default(),
710            crate::device::FeatureRequest::default(),
711            None,
712        ))
713        .expect("a wgpu adapter");
714        let texture = gpu.device.create_texture(&TextureDescriptor {
715            label: None,
716            size: wgpu::Extent3d {
717                width: PROBE_SIZE,
718                height: PROBE_SIZE,
719                depth_or_array_layers: 1,
720            },
721            mip_level_count: 1,
722            sample_count: 1,
723            dimension: TextureDimension::D2,
724            format: TextureFormat::Rgba8Unorm,
725            usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
726            view_formats: &[],
727        });
728        let view = texture.create_view(&Default::default());
729        let mut presenter = Presenter::new(TextureFormat::Rgba8Unorm).expect("presenter");
730        presenter
731            .register_pass(Box::new(Paint))
732            .expect("registered");
733        presenter.attach(&gpu).expect("attached");
734        let mut encoder = gpu.device.create_command_encoder(&Default::default());
735        presenter
736            .record_scene(
737                &gpu.device,
738                &mut encoder,
739                &view,
740                (PROBE_SIZE, PROBE_SIZE),
741                Color::BLACK,
742            )
743            .expect("recorded the scene");
744        presenter
745            .record_overlays(&mut encoder, &view, (PROBE_SIZE, PROBE_SIZE))
746            .expect("recorded the overlays");
747        gpu.queue.submit(Some(encoder.finish()));
748
749        assert_eq!(
750            pixel_at(&gpu, &texture, [0, 0]),
751            [255, 0, 0, 255],
752            "the presenter cleared the frame after the pass that runs before the scene"
753        );
754    }
755
756    #[test]
757    #[ignore = "requires a working wgpu adapter; run with --include-ignored"]
758    fn published_translucency_follows_all_opaque_geometry_gpu_probe() {
759        let gpu = pollster::block_on(GpuContext::new(
760            wgpu::Instance::default(),
761            crate::device::FeatureRequest::default(),
762            None,
763        ))
764        .expect("a wgpu adapter");
765        let texture = gpu.device.create_texture(&TextureDescriptor {
766            label: None,
767            size: wgpu::Extent3d {
768                width: PROBE_SIZE,
769                height: PROBE_SIZE,
770                depth_or_array_layers: 1,
771            },
772            mip_level_count: 1,
773            sample_count: 1,
774            dimension: TextureDimension::D2,
775            format: TextureFormat::Rgba8Unorm,
776            usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
777            view_formats: &[],
778        });
779        let target = texture.create_view(&Default::default());
780        let mut presenter = Presenter::new(TextureFormat::Rgba8Unorm).expect("presenter");
781        presenter.attach(&gpu).expect("attached");
782        let eye = Eye::default();
783        let camera = crate::view::placed_view_projection(&eye, Rigid::IDENTITY);
784        presenter.fills.set_view(&eye, Rigid::IDENTITY);
785        presenter.fills.edit(|mesh| {
786            mesh.vertices
787                .extend([[-2.0, -1.5, -4.0], [2.0, -1.5, -4.0], [0.0, 2.0, -4.0]]);
788            mesh.colors.extend([[0.0, 1.0, 0.0, 1.0]; 3]);
789            mesh.indices.push([0, 1, 2]);
790        });
791
792        let mut translucent = ViewLines::new(&gpu.device, TextureFormat::Rgba8Unorm);
793        translucent.set_camera(&gpu.queue, camera, Vec2::splat(PROBE_SIZE as f32));
794        translucent.upload_segments(
795            &gpu.device,
796            &gpu.queue,
797            &[SegmentRecord {
798                start: [-1.0, 0.0, -3.0],
799                end: [1.0, 0.0, -3.0],
800                start_color: [1.0, 0.0, 0.0, 0.01],
801                end_color: [1.0, 0.0, 0.0, 0.01],
802                width_px: 12.0,
803                ..Default::default()
804            }],
805        );
806        let mut opaque = ViewLines::new(&gpu.device, TextureFormat::Rgba8Unorm);
807        opaque.set_camera(&gpu.queue, camera, Vec2::splat(PROBE_SIZE as f32));
808        opaque.upload_segments(
809            &gpu.device,
810            &gpu.queue,
811            &[SegmentRecord {
812                start: [0.0, -0.75, -3.5],
813                end: [0.0, 0.75, -3.5],
814                start_color: [0.0, 0.0, 1.0, 1.0],
815                end_color: [0.0, 0.0, 1.0, 1.0],
816                width_px: 8.0,
817                ..Default::default()
818            }],
819        );
820        presenter.views.borrow_mut().extend([translucent, opaque]);
821
822        let mut encoder = gpu.device.create_command_encoder(&Default::default());
823        presenter
824            .record_scene(
825                &gpu.device,
826                &mut encoder,
827                &target,
828                (PROBE_SIZE, PROBE_SIZE),
829                Color::BLACK,
830            )
831            .expect("recorded the scene");
832        presenter
833            .record_overlays(&mut encoder, &target, (PROBE_SIZE, PROBE_SIZE))
834            .expect("recorded the overlays");
835        gpu.queue.submit(Some(encoder.finish()));
836
837        let surface = pixel_at(&gpu, &texture, [42, 32]);
838        assert!(
839            surface[0] > 0 && surface[1] > 80,
840            "the translucent line exposed the background through the fill: {surface:?}"
841        );
842        let crossing = pixel_at(&gpu, &texture, [32, 32]);
843        assert!(
844            crossing[0] > 0 && crossing[2] > 200,
845            "a later view drew its opaque line after the translucent batch: {crossing:?}"
846        );
847    }
848}