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 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}