Skip to main content

loam_render/
triangle_pass.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use glam::Mat4;
5use loam_math::{EuclideanR3, Projection};
6use loam_runtime::{Eye, Rigid, Version};
7use loam_shape::TriangleMesh;
8use wgpu::{Color, CommandEncoder, Device, Queue, RenderPass};
9
10use crate::device::GpuContext;
11use crate::pass::{ColorLoad, FrameFormat, FramePass, FrameTarget, PassStage};
12use crate::sky_ground::SKY_HORIZON;
13use crate::view::placed_view_projection;
14use crate::{
15    DepthConvention, DepthMode, FragmentShading, Ground, SkyGroundNode, SkyGroundUniforms,
16    TriangleRasterNode, Viewport,
17};
18
19#[derive(Default)]
20struct Input {
21    mesh: TriangleMesh<3>,
22    source_version: Option<Version>,
23    revision: u64,
24    view_projection: Mat4,
25    ground: Option<Ground>,
26    declared: Option<ColorLoad>,
27    uploads: u64,
28}
29
30#[derive(Copy, Clone, Debug, PartialEq, Eq)]
31pub struct GroundLocked {
32    pub declared: ColorLoad,
33}
34
35impl std::fmt::Display for GroundLocked {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(
38            f,
39            "a registered triangle pass declared {:?}, so the ground cannot be added or removed",
40            self.declared
41        )
42    }
43}
44
45impl std::error::Error for GroundLocked {}
46
47#[derive(Clone, Default)]
48pub struct TriangleFeed {
49    input: Rc<RefCell<Input>>,
50}
51
52impl TriangleFeed {
53    pub fn pass(&self, shading: FragmentShading) -> Box<dyn FramePass> {
54        let load = {
55            let mut input = self.input.borrow_mut();
56            let load = ground_load(input.ground);
57            input.declared = Some(load);
58            load
59        };
60        Box::new(TrianglePass {
61            input: self.input.clone(),
62            shading,
63            load,
64            built: None,
65        })
66    }
67
68    pub fn set_view(&self, eye: &Eye, placement: Rigid) {
69        self.input.borrow_mut().view_projection = placed_view_projection(eye, placement);
70    }
71
72    pub fn set_ground(&self, ground: Option<Ground>) -> Result<(), GroundLocked> {
73        let mut input = self.input.borrow_mut();
74        let load = ground_load(ground);
75        if let Some(declared) = input.declared.filter(|declared| *declared != load) {
76            return Err(GroundLocked { declared });
77        }
78        input.ground = ground;
79        Ok(())
80    }
81
82    pub fn edit(&self, build: impl FnOnce(&mut TriangleMesh<3>)) {
83        let mut input = self.input.borrow_mut();
84        build(&mut input.mesh);
85        input.source_version = None;
86        input.revision += 1;
87    }
88
89    /// Tracks one source by version; switching sources requires a distinct version.
90    pub fn set_mesh(&self, version: Version, mesh: &TriangleMesh<3>) {
91        let mut input = self.input.borrow_mut();
92        if input.source_version == Some(version) {
93            return;
94        }
95        input.source_version = Some(version);
96        input.mesh.vertices.clone_from(&mesh.vertices);
97        input.mesh.indices.clone_from(&mesh.indices);
98        input.mesh.colors.clone_from(&mesh.colors);
99        input.revision += 1;
100    }
101
102    pub fn uploads(&self) -> u64 {
103        self.input.borrow().uploads
104    }
105
106    pub fn triangles(&self) -> usize {
107        self.input.borrow().mesh.indices.len()
108    }
109}
110
111struct Built {
112    device: Device,
113    queue: Queue,
114    triangles: TriangleRasterNode,
115    sky_ground: SkyGroundNode,
116    uploaded: Option<u64>,
117}
118
119fn ground_load(ground: Option<Ground>) -> ColorLoad {
120    match ground {
121        Some(_) => ColorLoad::Clear,
122        None => ColorLoad::Load,
123    }
124}
125
126struct TrianglePass {
127    input: Rc<RefCell<Input>>,
128    shading: FragmentShading,
129    load: ColorLoad,
130    built: Option<Built>,
131}
132
133impl FramePass for TrianglePass {
134    fn name(&self) -> &'static str {
135        "triangles"
136    }
137
138    fn stage(&self) -> PassStage {
139        PassStage::Scene
140    }
141
142    fn color_load(&self) -> ColorLoad {
143        self.load
144    }
145
146    fn depth_convention(&self) -> Option<DepthConvention> {
147        Some(DepthConvention::ReversedZ)
148    }
149
150    fn depth_read(&self) -> Option<DepthConvention> {
151        Some(DepthConvention::ReversedZ)
152    }
153
154    fn shares_pass(&self) -> bool {
155        true
156    }
157
158    fn clear_color(&self) -> Color {
159        SKY_HORIZON
160    }
161
162    fn prepare(
163        &mut self,
164        _encoder: &mut CommandEncoder,
165        target: &FrameTarget<'_>,
166    ) -> anyhow::Result<()> {
167        let Some(built) = self.built.as_mut() else {
168            return Ok(());
169        };
170        let mut input = self.input.borrow_mut();
171        if built.uploaded != Some(input.revision) {
172            built.uploaded = Some(input.revision);
173            input.uploads += 1;
174            built.triangles.upload::<EuclideanR3, 3>(
175                &built.device,
176                &built.queue,
177                &input.mesh,
178                &Projection::Identity,
179            );
180        }
181        if let (ColorLoad::Clear, Some(ground)) = (self.load, input.ground) {
182            built.sky_ground.set_uniforms(
183                &built.queue,
184                &SkyGroundUniforms::new(
185                    input.view_projection,
186                    Viewport::full([target.size.0, target.size.1]),
187                    crate::sky_ground::DEFAULT_SKY,
188                    ground,
189                ),
190            );
191        }
192        built
193            .triangles
194            .set_camera(&built.queue, input.view_projection);
195        Ok(())
196    }
197
198    fn draw(&mut self, pass: &mut RenderPass<'_>, _target: &FrameTarget<'_>) -> anyhow::Result<()> {
199        let Some(built) = self.built.as_ref() else {
200            return Ok(());
201        };
202        if let (ColorLoad::Clear, Some(_)) = (self.load, self.input.borrow().ground) {
203            built.sky_ground.draw(pass, None);
204        }
205        built.triangles.draw(pass, None);
206        Ok(())
207    }
208
209    fn record(
210        &mut self,
211        encoder: &mut CommandEncoder,
212        target: &FrameTarget<'_>,
213    ) -> anyhow::Result<()> {
214        let Some(depth) = target.depth else {
215            return Ok(());
216        };
217        self.prepare(encoder, target)?;
218        let Some(built) = self.built.as_ref() else {
219            return Ok(());
220        };
221        if let (ColorLoad::Clear, Some(_)) = (self.load, self.input.borrow().ground) {
222            built.sky_ground.record(encoder, target.color, depth, None);
223        }
224        built
225            .triangles
226            .record(encoder, target.color, Some(depth), None);
227        Ok(())
228    }
229
230    fn attach(&mut self, gpu: &GpuContext, frame: FrameFormat) -> anyhow::Result<()> {
231        let triangles = TriangleRasterNode::new(
232            gpu,
233            frame.color,
234            DepthMode::ReadWrite {
235                format: frame.depth,
236            },
237            DepthConvention::ReversedZ,
238            self.shading,
239        )?;
240        let sky_ground = SkyGroundNode::new(
241            &gpu.device,
242            frame.color,
243            frame.depth,
244            DepthConvention::ReversedZ,
245        );
246        self.built = Some(Built {
247            device: gpu.device.clone(),
248            queue: gpu.queue.clone(),
249            triangles,
250            sky_ground,
251            uploaded: None,
252        });
253        Ok(())
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use wgpu::{
260        BackendOptions, Backends, BufferDescriptor, BufferUsages, Color, CommandEncoderDescriptor,
261        Extent3d, Instance, InstanceDescriptor, LoadOp, NoopBackendOptions, Operations, PollType,
262        RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, StoreOp,
263        TexelCopyBufferInfo, TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect,
264        TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView,
265        TextureViewDescriptor,
266    };
267
268    use super::*;
269    use crate::device::FeatureRequest;
270    use crate::view::{projective_depth, DEPTH_CLEAR, DEPTH_FORMAT};
271
272    const SIZE: (u32, u32) = (64, 64);
273    const COLOR: TextureFormat = TextureFormat::Rgba8Unorm;
274
275    fn frame_format() -> FrameFormat {
276        FrameFormat {
277            color: COLOR,
278            depth: DEPTH_FORMAT,
279        }
280    }
281
282    fn noop_context() -> GpuContext {
283        let instance = Instance::new(&InstanceDescriptor {
284            backends: Backends::NOOP,
285            backend_options: BackendOptions {
286                noop: NoopBackendOptions { enable: true },
287                ..Default::default()
288            },
289            ..Default::default()
290        });
291        pollster::block_on(GpuContext::new(instance, FeatureRequest::default(), None))
292            .expect("the noop backend always yields a context")
293    }
294
295    fn attachment(gpu: &GpuContext, format: TextureFormat, usage: TextureUsages) -> wgpu::Texture {
296        gpu.device.create_texture(&TextureDescriptor {
297            label: Some("triangle pass probe"),
298            size: Extent3d {
299                width: SIZE.0,
300                height: SIZE.1,
301                depth_or_array_layers: 1,
302            },
303            mip_level_count: 1,
304            sample_count: 1,
305            dimension: TextureDimension::D2,
306            format,
307            usage: TextureUsages::RENDER_ATTACHMENT | usage,
308            view_formats: &[],
309        })
310    }
311
312    fn one_frame(
313        gpu: &GpuContext,
314        pass: &mut dyn FramePass,
315        color: &TextureView,
316        depth: &TextureView,
317    ) {
318        let mut encoder = gpu
319            .device
320            .create_command_encoder(&CommandEncoderDescriptor { label: None });
321        encoder.begin_render_pass(&RenderPassDescriptor {
322            label: Some("clear"),
323            color_attachments: &[Some(RenderPassColorAttachment {
324                view: color,
325                depth_slice: None,
326                resolve_target: None,
327                ops: Operations {
328                    load: LoadOp::Clear(Color::BLACK),
329                    store: StoreOp::Store,
330                },
331            })],
332            depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
333                view: depth,
334                depth_ops: Some(Operations {
335                    load: LoadOp::Clear(DEPTH_CLEAR),
336                    store: StoreOp::Store,
337                }),
338                stencil_ops: None,
339            }),
340            timestamp_writes: None,
341            occlusion_query_set: None,
342        });
343        pass.record(
344            &mut encoder,
345            &FrameTarget {
346                color,
347                depth: Some(depth),
348                size: SIZE,
349            },
350        )
351        .expect("record");
352        gpu.queue.submit(Some(encoder.finish()));
353    }
354
355    fn triangle(feed: &TriangleFeed, z: f32, color: [f32; 4]) {
356        feed.edit(|mesh| {
357            mesh.vertices = vec![[-0.4, -0.4, z], [0.4, -0.4, z], [0.0, 0.4, z]];
358            mesh.colors = vec![color; 3];
359            mesh.indices = vec![[0, 1, 2]];
360        });
361    }
362
363    fn ground() -> Ground {
364        Ground {
365            y: 0.0,
366            dark: [0.1; 3],
367            light: [0.3; 3],
368            fog_per_unit: 0.0,
369            fog_start: 0.0,
370            visible: true,
371        }
372    }
373
374    #[test]
375    fn a_ground_cannot_appear_or_vanish_after_the_pass_declared_its_color_load() {
376        let feed = TriangleFeed::default();
377        let _pass = feed.pass(FragmentShading::FaceNormalLambert);
378        let refused = feed.set_ground(Some(ground()));
379        assert!(
380            matches!(
381                refused,
382                Err(GroundLocked {
383                    declared: ColorLoad::Load
384                })
385            ),
386            "a ground added after registration was accepted: {refused:?}"
387        );
388        feed.set_ground(None)
389            .expect("keeping the declared load is allowed");
390
391        let grounded = TriangleFeed::default();
392        grounded
393            .set_ground(Some(ground()))
394            .expect("no pass is built yet");
395        let _pass = grounded.pass(FragmentShading::FaceNormalLambert);
396        assert!(grounded.set_ground(None).is_err());
397        grounded
398            .set_ground(Some(ground()))
399            .expect("replacing a declared ground keeps the load");
400    }
401
402    #[test]
403    fn an_unchanged_mesh_is_not_uploaded_again_on_the_next_frame() {
404        let gpu = noop_context();
405        let feed = TriangleFeed::default();
406        let mut source = loam_runtime::Value::new(TriangleMesh {
407            vertices: vec![[-0.4, -0.4, -2.0], [0.4, -0.4, -2.0], [0.0, 0.4, -2.0]],
408            colors: vec![[1.0; 4]; 3],
409            indices: vec![[0, 1, 2]],
410        });
411        feed.set_mesh(source.version(), source.get());
412        let mut pass = feed.pass(FragmentShading::FaceNormalLambert);
413        pass.attach(&gpu, frame_format()).expect("attach");
414        let color =
415            attachment(&gpu, COLOR, TextureUsages::empty()).create_view(&Default::default());
416        let depth =
417            attachment(&gpu, DEPTH_FORMAT, TextureUsages::empty()).create_view(&Default::default());
418
419        one_frame(&gpu, pass.as_mut(), &color, &depth);
420        feed.set_mesh(source.version(), source.get());
421        one_frame(&gpu, pass.as_mut(), &color, &depth);
422        source.get_mut().vertices[2][2] = -3.0;
423        feed.set_mesh(source.version(), source.get());
424        one_frame(&gpu, pass.as_mut(), &color, &depth);
425
426        assert_eq!(
427            feed.uploads(),
428            2,
429            "three frames with one source change uploaded {} times",
430            feed.uploads()
431        );
432    }
433
434    #[test]
435    fn a_new_device_uploads_the_mesh_again_without_an_edit() {
436        let gpu = noop_context();
437        let feed = TriangleFeed::default();
438        triangle(&feed, -2.0, [1.0; 4]);
439        let mut pass = feed.pass(FragmentShading::FaceNormalLambert);
440        pass.attach(&gpu, frame_format()).expect("attach");
441        let color =
442            attachment(&gpu, COLOR, TextureUsages::empty()).create_view(&Default::default());
443        let depth =
444            attachment(&gpu, DEPTH_FORMAT, TextureUsages::empty()).create_view(&Default::default());
445        one_frame(&gpu, pass.as_mut(), &color, &depth);
446
447        let lost = noop_context();
448        pass.attach(&lost, frame_format()).expect("reattach");
449        let color =
450            attachment(&lost, COLOR, TextureUsages::empty()).create_view(&Default::default());
451        let depth = attachment(&lost, DEPTH_FORMAT, TextureUsages::empty())
452            .create_view(&Default::default());
453        one_frame(&lost, pass.as_mut(), &color, &depth);
454
455        assert_eq!(
456            feed.uploads(),
457            2,
458            "the recovered pass drew from the lost device's buffers"
459        );
460    }
461
462    #[test]
463    #[ignore = "requires a working wgpu adapter; run with --include-ignored"]
464    fn a_lit_triangle_writes_its_image_pixel_and_projective_depth_gpu_probe() {
465        let gpu = pollster::block_on(GpuContext::new(
466            Instance::default(),
467            FeatureRequest::default(),
468            None,
469        ))
470        .expect("gpu context");
471        let feed = TriangleFeed::default();
472        const Z: f32 = -2.0;
473        triangle(&feed, Z, [1.0, 1.0, 1.0, 1.0]);
474        let eye = Eye {
475            aspect: SIZE.0 as f32 / SIZE.1 as f32,
476            ..Eye::default()
477        };
478        feed.set_view(&eye, Rigid::IDENTITY);
479
480        let mut pass = feed.pass(FragmentShading::FaceNormalLambert);
481        pass.attach(&gpu, frame_format()).expect("attach");
482        let color_texture = attachment(&gpu, COLOR, TextureUsages::COPY_SRC);
483        let depth_texture = attachment(&gpu, DEPTH_FORMAT, TextureUsages::COPY_SRC);
484        one_frame(
485            &gpu,
486            pass.as_mut(),
487            &color_texture.create_view(&TextureViewDescriptor::default()),
488            &depth_texture.create_view(&TextureViewDescriptor::default()),
489        );
490
491        let centre = (SIZE.0 / 2, SIZE.1 / 2);
492        let outside = (1, 1);
493        let colors = read_back::<4>(&gpu, &color_texture, COLOR);
494        let depths = read_back::<4>(&gpu, &depth_texture, DEPTH_FORMAT);
495        let at = |(x, y): (u32, u32), rows: &Vec<[u8; 4]>| rows[(y * SIZE.0 + x) as usize];
496        let depth_at = |p: (u32, u32)| f32::from_le_bytes(at(p, &depths));
497
498        let key = glam::Vec3::new(0.55, 0.85, 0.45).normalize();
499        let lambert = 0.3 + 0.7 * key.z.abs();
500        let lit = (lambert * 255.0).round() as i32;
501        let drawn = at(centre, &colors);
502        assert!(
503            (i32::from(drawn[0]) - lit).abs() <= 2,
504            "the lit triangle wrote {drawn:?} where the shader's lambert term gives {lit}"
505        );
506        assert_eq!(
507            at(outside, &colors),
508            [0, 0, 0, 255],
509            "the triangle covered a pixel outside its image position"
510        );
511        assert!(
512            (depth_at(centre) - projective_depth(glam::Vec3::new(0.0, 0.0, Z), eye.near)).abs()
513                < 1e-6,
514            "the triangle wrote depth {} for the image point at z = {Z}",
515            depth_at(centre)
516        );
517        assert_eq!(depth_at(outside), DEPTH_CLEAR);
518    }
519
520    fn read_back<const N: usize>(
521        gpu: &GpuContext,
522        texture: &wgpu::Texture,
523        format: TextureFormat,
524    ) -> Vec<[u8; N]> {
525        let row = SIZE.0 as usize * N;
526        let padded = row.next_multiple_of(256);
527        let staging = gpu.device.create_buffer(&BufferDescriptor {
528            label: Some("triangle pass readback"),
529            size: (padded * SIZE.1 as usize) as u64,
530            usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
531            mapped_at_creation: false,
532        });
533        let mut encoder = gpu
534            .device
535            .create_command_encoder(&CommandEncoderDescriptor { label: None });
536        encoder.copy_texture_to_buffer(
537            TexelCopyTextureInfo {
538                texture,
539                mip_level: 0,
540                origin: wgpu::Origin3d::ZERO,
541                aspect: match format {
542                    TextureFormat::Depth32Float => TextureAspect::DepthOnly,
543                    _ => TextureAspect::All,
544                },
545            },
546            TexelCopyBufferInfo {
547                buffer: &staging,
548                layout: TexelCopyBufferLayout {
549                    offset: 0,
550                    bytes_per_row: Some(padded as u32),
551                    rows_per_image: Some(SIZE.1),
552                },
553            },
554            Extent3d {
555                width: SIZE.0,
556                height: SIZE.1,
557                depth_or_array_layers: 1,
558            },
559        );
560        gpu.queue.submit(Some(encoder.finish()));
561        staging.slice(..).map_async(wgpu::MapMode::Read, |_| {});
562        gpu.device
563            .poll(PollType::wait_indefinitely())
564            .expect("poll");
565        let view = staging.slice(..).get_mapped_range();
566        let mut rows = Vec::with_capacity((SIZE.0 * SIZE.1) as usize);
567        for y in 0..SIZE.1 as usize {
568            for x in 0..SIZE.0 as usize {
569                let at = y * padded + x * N;
570                let mut texel = [0u8; N];
571                texel.copy_from_slice(&view[at..at + N]);
572                rows.push(texel);
573            }
574        }
575        rows
576    }
577}