1use craballoc::{
4 prelude::{GpuArray, Hybrid, SlabAllocator, SlabAllocatorError},
5 runtime::WgpuRuntime,
6 slab::SlabBuffer,
7};
8use crabslab::{Array, Slab};
9use glam::UVec2;
10use snafu::{OptionExt, Snafu};
11
12use crate::{bindgroup::ManagedBindGroup, texture::Texture};
13
14use super::shader::DepthPyramidDescriptor;
15
16#[derive(Debug, Snafu)]
17pub enum CullingError {
18 #[snafu(display(
19 "Texture is not a depth texture, expected '{:?}' but saw '{seen:?}'",
20 Texture::DEPTH_FORMAT
21 ))]
22 NotADepthTexture { seen: wgpu::TextureFormat },
23
24 #[snafu(display("Missing depth pyramid mip {index}"))]
25 MissingMip { index: usize },
26
27 #[snafu(display("{source}"))]
28 SlabError { source: SlabAllocatorError },
29
30 #[snafu(display("Could not read mip {index}"))]
31 ReadMip { index: usize },
32}
33
34impl From<SlabAllocatorError> for CullingError {
35 fn from(source: SlabAllocatorError) -> Self {
36 CullingError::SlabError { source }
37 }
38}
39
40pub struct ComputeCulling {
42 pipeline: wgpu::ComputePipeline,
43
44 pyramid_slab_buffer: SlabBuffer<wgpu::Buffer>,
45 stage_slab_buffer: SlabBuffer<wgpu::Buffer>,
46 indirect_slab_buffer: SlabBuffer<wgpu::Buffer>,
47
48 bindgroup_layout: wgpu::BindGroupLayout,
49 bindgroup: ManagedBindGroup,
50
51 pub(crate) compute_depth_pyramid: ComputeDepthPyramid,
52}
53
54impl ComputeCulling {
55 const LABEL: Option<&'static str> = Some("compute-culling");
56
57 fn new_bindgroup(
58 stage_slab_buffer: &wgpu::Buffer,
59 hzb_slab_buffer: &wgpu::Buffer,
60 indirect_buffer: &wgpu::Buffer,
61 layout: &wgpu::BindGroupLayout,
62 device: &wgpu::Device,
63 ) -> wgpu::BindGroup {
64 device.create_bind_group(&wgpu::BindGroupDescriptor {
65 label: Self::LABEL,
66 layout,
67 entries: &[
68 wgpu::BindGroupEntry {
69 binding: 0,
70 resource: wgpu::BindingResource::Buffer(
71 stage_slab_buffer.as_entire_buffer_binding(),
72 ),
73 },
74 wgpu::BindGroupEntry {
75 binding: 1,
76 resource: wgpu::BindingResource::Buffer(
77 hzb_slab_buffer.as_entire_buffer_binding(),
78 ),
79 },
80 wgpu::BindGroupEntry {
81 binding: 2,
82 resource: wgpu::BindingResource::Buffer(
83 indirect_buffer.as_entire_buffer_binding(),
84 ),
85 },
86 ],
87 })
88 }
89
90 pub fn new(
91 runtime: impl AsRef<WgpuRuntime>,
92 stage_slab_buffer: &SlabBuffer<wgpu::Buffer>,
93 indirect_slab_buffer: &SlabBuffer<wgpu::Buffer>,
94 depth_texture: &Texture,
95 ) -> Self {
96 let runtime = runtime.as_ref();
97 let device = &runtime.device;
98 let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
99 label: Self::LABEL,
100 entries: &[
101 wgpu::BindGroupLayoutEntry {
102 binding: 0,
103 visibility: wgpu::ShaderStages::COMPUTE,
104 ty: wgpu::BindingType::Buffer {
105 ty: wgpu::BufferBindingType::Storage { read_only: true },
106 has_dynamic_offset: false,
107 min_binding_size: None,
108 },
109 count: None,
110 },
111 wgpu::BindGroupLayoutEntry {
112 binding: 1,
113 visibility: wgpu::ShaderStages::COMPUTE,
114 ty: wgpu::BindingType::Buffer {
115 ty: wgpu::BufferBindingType::Storage { read_only: true },
116 has_dynamic_offset: false,
117 min_binding_size: None,
118 },
119 count: None,
120 },
121 wgpu::BindGroupLayoutEntry {
122 binding: 2,
123 visibility: wgpu::ShaderStages::COMPUTE,
124 ty: wgpu::BindingType::Buffer {
125 ty: wgpu::BufferBindingType::Storage { read_only: false },
126 has_dynamic_offset: false,
127 min_binding_size: None,
128 },
129 count: None,
130 },
131 ],
132 });
133 let linkage = crate::linkage::compute_culling::linkage(device);
134 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
135 label: Self::LABEL,
136 layout: Some(
137 &device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
138 label: Self::LABEL,
139 bind_group_layouts: &[&bindgroup_layout],
140 push_constant_ranges: &[],
141 }),
142 ),
143 module: &linkage.module,
144 entry_point: Some(linkage.entry_point),
145 compilation_options: wgpu::PipelineCompilationOptions::default(),
146 cache: None,
147 });
148 let compute_depth_pyramid = ComputeDepthPyramid::new(runtime, depth_texture);
149 let pyramid_slab_buffer = compute_depth_pyramid
150 .compute_copy_depth
151 .pyramid_slab_buffer
152 .clone();
153 let bindgroup = Self::new_bindgroup(
154 stage_slab_buffer,
155 &pyramid_slab_buffer,
156 indirect_slab_buffer,
157 &bindgroup_layout,
158 device,
159 );
160 Self {
161 pipeline,
162 bindgroup_layout,
163 bindgroup: ManagedBindGroup::from(bindgroup),
164 compute_depth_pyramid,
165 pyramid_slab_buffer,
166 stage_slab_buffer: stage_slab_buffer.clone(),
167 indirect_slab_buffer: indirect_slab_buffer.clone(),
168 }
169 }
170
171 fn runtime(&self) -> &WgpuRuntime {
172 self.compute_depth_pyramid.depth_pyramid.slab.runtime()
173 }
174
175 pub fn run(&mut self, indirect_draw_count: u32, depth_texture: &Texture) {
176 log::trace!(
177 "indirect_draw_count: {indirect_draw_count}, sample_count: {}",
178 depth_texture.texture.sample_count()
179 );
180 self.compute_depth_pyramid.run(depth_texture);
182
183 let stage_slab_invalid = self.stage_slab_buffer.update_if_invalid();
184 let indirect_slab_invalid = self.indirect_slab_buffer.update_if_invalid();
185 let pyramid_slab_invalid = self.pyramid_slab_buffer.update_if_invalid();
186 let should_recreate_bindgroup =
187 stage_slab_invalid || indirect_slab_invalid || pyramid_slab_invalid;
188 log::trace!("stage_slab_invalid: {stage_slab_invalid}");
189 log::trace!("indirect_slab_invalid: {indirect_slab_invalid}");
190 log::trace!("pyramid_slab_invalid: {pyramid_slab_invalid}");
191 let bindgroup = self.bindgroup.get(should_recreate_bindgroup, || {
192 log::debug!("recreating compute-culling bindgroup");
193 Self::new_bindgroup(
194 &self.stage_slab_buffer,
195 &self.pyramid_slab_buffer,
196 &self.indirect_slab_buffer,
197 &self.bindgroup_layout,
198 self.compute_depth_pyramid.depth_pyramid.slab.device(),
199 )
200 });
201 let runtime = self.runtime();
202 let mut encoder = runtime
203 .device
204 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Self::LABEL });
205 {
206 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
207 label: Self::LABEL,
208 timestamp_writes: None,
209 });
210 compute_pass.set_pipeline(&self.pipeline);
211 compute_pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]);
212 compute_pass.dispatch_workgroups(indirect_draw_count / 16 + 1, 1, 1);
213 }
214 runtime.queue.submit(Some(encoder.finish()));
215 }
216}
217
218pub struct DepthPyramid {
219 slab: SlabAllocator<WgpuRuntime>,
220 desc: Hybrid<DepthPyramidDescriptor>,
221 mip: GpuArray<Array<f32>>,
222 mip_data: Vec<GpuArray<f32>>,
223}
224
225impl DepthPyramid {
226 const LABEL: &str = "depth-pyramid";
227
228 fn allocate(
229 size: UVec2,
230 desc: &Hybrid<DepthPyramidDescriptor>,
231 slab: &SlabAllocator<WgpuRuntime>,
232 ) -> (Vec<GpuArray<f32>>, GpuArray<Array<f32>>) {
233 let mip_levels = size.min_element().ilog2();
234 let mip_data = (0..mip_levels)
235 .map(|i| {
236 let width = size.x >> i;
237 let height = size.y >> i;
238 slab.new_array(vec![0f32; (width * height) as usize])
239 .into_gpu_only()
240 })
241 .collect::<Vec<_>>();
242 let mip = slab.new_array(mip_data.iter().map(|m| m.array()));
243 desc.set(DepthPyramidDescriptor {
244 size,
245 mip_level: 0,
246 mip: mip.array(),
247 });
248 (mip_data, mip.into_gpu_only())
249 }
250
251 pub fn new(runtime: impl AsRef<WgpuRuntime>, size: UVec2) -> Self {
252 let slab = SlabAllocator::new(runtime, Self::LABEL, wgpu::BufferUsages::empty());
253 let desc = slab.new_value(DepthPyramidDescriptor::default());
254 let (mip_data, mip) = Self::allocate(size, &desc, &slab);
255
256 Self {
257 slab,
258 desc,
259 mip_data,
260 mip,
261 }
262 }
263
264 pub fn resize(&mut self, size: UVec2) {
265 log::trace!("resizing depth pyramid to {size}");
266 let mip = self.slab.new_array(vec![]);
268 self.mip_data = vec![];
269 self.desc.modify(|desc| desc.mip = mip.array());
270 self.mip = mip.into_gpu_only();
271
272 self.slab.commit();
274
275 let (mip_data, mip) = Self::allocate(size, &self.desc, &self.slab);
277 self.mip_data = mip_data;
278 self.mip = mip;
279
280 self.slab.commit();
282 }
283
284 pub fn size(&self) -> UVec2 {
285 self.desc.get().size
286 }
287
288 pub async fn read_images(&self) -> Result<Vec<image::GrayImage>, CullingError> {
289 let size = self.size();
290 let slab_data = self.slab.read(0..).await?;
291 let mut images = vec![];
292 let mut min = f32::MAX;
293 let mut max = f32::MIN;
294 for (i, mip) in self.mip_data.iter().enumerate() {
295 let depth_data: Vec<u8> = slab_data
296 .read_vec(mip.array())
297 .into_iter()
298 .map(|depth: f32| {
299 if i == 0 {
300 min = min.min(depth);
301 max = max.max(depth);
302 }
303 crate::color::f32_to_u8(depth)
304 })
305 .collect();
306 log::trace!("min: {min}");
307 log::trace!("max: {max}");
308 let width = size.x >> i;
309 let height = size.y >> i;
310 let image = image::GrayImage::from_raw(width, height, depth_data)
311 .context(ReadMipSnafu { index: i })?;
312 images.push(image);
313 }
314 Ok(images)
315 }
316}
317
318struct ComputeCopyDepth {
320 pipeline: wgpu::ComputePipeline,
321 bindgroup_layout: wgpu::BindGroupLayout,
322 sample_count: u32,
323 pyramid_slab_buffer: SlabBuffer<wgpu::Buffer>,
324 bindgroup: ManagedBindGroup,
325}
326
327impl ComputeCopyDepth {
328 const LABEL: Option<&'static str> = Some("compute-occlusion-copy-depth-to-pyramid");
329
330 fn create_bindgroup_layout(device: &wgpu::Device, sample_count: u32) -> wgpu::BindGroupLayout {
331 if sample_count > 1 {
332 log::trace!(
333 "creating bindgroup layout with {sample_count} multisampled depth for {}",
334 Self::LABEL.unwrap()
335 );
336 } else {
337 log::trace!(
338 "creating bindgroup layout without multisampling for {}",
339 Self::LABEL.unwrap()
340 );
341 }
342 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
343 label: Self::LABEL,
344 entries: &[
345 wgpu::BindGroupLayoutEntry {
347 binding: 0,
348 visibility: wgpu::ShaderStages::COMPUTE,
349 ty: wgpu::BindingType::Buffer {
350 ty: wgpu::BufferBindingType::Storage { read_only: false },
351 has_dynamic_offset: false,
352 min_binding_size: None,
353 },
354 count: None,
355 },
356 wgpu::BindGroupLayoutEntry {
358 binding: 1,
359 visibility: wgpu::ShaderStages::COMPUTE,
360 ty: wgpu::BindingType::Texture {
361 sample_type: wgpu::TextureSampleType::Depth,
362 view_dimension: wgpu::TextureViewDimension::D2,
363 multisampled: sample_count > 1,
364 },
365 count: None,
366 },
367 ],
368 })
369 }
370
371 fn create_pipeline(
372 device: &wgpu::Device,
373 bindgroup_layout: &wgpu::BindGroupLayout,
374 multisampled: bool,
375 ) -> wgpu::ComputePipeline {
376 let linkage = if multisampled {
377 log::trace!("creating multisampled shader for {}", Self::LABEL.unwrap());
378 crate::linkage::compute_copy_depth_to_pyramid_multisampled::linkage(device)
379 } else {
380 log::trace!(
381 "creating shader without multisampling for {}",
382 Self::LABEL.unwrap()
383 );
384 crate::linkage::compute_copy_depth_to_pyramid::linkage(device)
385 };
386 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
387 label: Self::LABEL,
388 layout: Some(
389 &device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
390 label: Self::LABEL,
391 bind_group_layouts: &[bindgroup_layout],
392 push_constant_ranges: &[],
393 }),
394 ),
395 module: &linkage.module,
396 entry_point: Some(linkage.entry_point),
397 compilation_options: wgpu::PipelineCompilationOptions::default(),
398 cache: None,
399 })
400 }
401
402 fn create_bindgroup(
403 device: &wgpu::Device,
404 layout: &wgpu::BindGroupLayout,
405 pyramid_desc_buffer: &wgpu::Buffer,
406 depth_texture_view: &wgpu::TextureView,
407 ) -> wgpu::BindGroup {
408 device.create_bind_group(&wgpu::BindGroupDescriptor {
409 label: Self::LABEL,
410 layout,
411 entries: &[
412 wgpu::BindGroupEntry {
413 binding: 0,
414 resource: wgpu::BindingResource::Buffer(
415 pyramid_desc_buffer.as_entire_buffer_binding(),
416 ),
417 },
418 wgpu::BindGroupEntry {
419 binding: 1,
420 resource: wgpu::BindingResource::TextureView(depth_texture_view),
421 },
422 ],
423 })
424 }
425
426 pub fn new(depth_pyramid: &DepthPyramid, depth_texture: &Texture) -> Self {
427 let device = depth_pyramid.slab.device();
428 let sample_count = depth_texture.texture.sample_count();
429 let bindgroup_layout = Self::create_bindgroup_layout(device, sample_count);
430 let pipeline = Self::create_pipeline(device, &bindgroup_layout, sample_count > 1);
431 let pyramid_slab_buffer = depth_pyramid.slab.commit();
432 let buffer = Self::create_bindgroup(
433 device,
434 &bindgroup_layout,
435 &pyramid_slab_buffer,
436 &depth_texture.view,
437 );
438 Self {
439 pipeline,
440 bindgroup: ManagedBindGroup::from(buffer),
441 bindgroup_layout,
442 pyramid_slab_buffer,
443 sample_count,
444 }
445 }
446
447 pub fn run(&mut self, pyramid: &mut DepthPyramid, depth_texture: &Texture) {
448 let _ = pyramid.desc.modify(|desc| {
449 desc.mip_level = 0;
450 desc.size
451 });
452
453 let runtime = pyramid.slab.runtime().clone();
454 let sample_count = depth_texture.texture.sample_count();
455 let sample_count_mismatch = sample_count != self.sample_count;
456 if sample_count_mismatch {
457 log::debug!(
458 "sample count changed from {} to {}, updating {} bindgroup layout and pipeline",
459 self.sample_count,
460 sample_count,
461 Self::LABEL.unwrap()
462 );
463 self.sample_count = sample_count;
464 self.bindgroup_layout = Self::create_bindgroup_layout(&runtime.device, sample_count);
465 self.pipeline =
466 Self::create_pipeline(&runtime.device, &self.bindgroup_layout, sample_count > 1);
467 }
468
469 let extent = depth_texture.texture.size();
470 let size = UVec2::new(extent.width, extent.height);
471 let size_changed = size != pyramid.size();
472 if size_changed {
473 pyramid.resize(size);
474 }
475
476 let _ = pyramid.slab.commit();
478 let should_recreate_bindgroup =
479 self.pyramid_slab_buffer.update_if_invalid() || sample_count_mismatch || size_changed;
480 let bindgroup = self.bindgroup.get(should_recreate_bindgroup, || {
481 Self::create_bindgroup(
482 &runtime.device,
483 &self.bindgroup_layout,
484 &self.pyramid_slab_buffer,
485 &depth_texture.view,
486 )
487 });
488
489 let mut encoder = runtime
490 .device
491 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Self::LABEL });
492 {
493 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
494 label: Self::LABEL,
495 ..Default::default()
496 });
497 compute_pass.set_pipeline(&self.pipeline);
498 compute_pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]);
499 let x = size.x / 16 + 1;
500 let y = size.y / 16 + 1;
501 let z = 1;
502 compute_pass.dispatch_workgroups(x, y, z);
503 }
504 pyramid.slab.queue().submit(Some(encoder.finish()));
505 }
506}
507
508struct ComputeDownsampleDepth {
510 pipeline: wgpu::ComputePipeline,
511 pyramid_slab_buffer: SlabBuffer<wgpu::Buffer>,
512 bindgroup: wgpu::BindGroup,
513 bindgroup_layout: wgpu::BindGroupLayout,
514}
515
516impl ComputeDownsampleDepth {
517 const LABEL: Option<&'static str> = Some("compute-occlusion-downsample-depth");
518
519 fn create_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
520 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
521 label: Self::LABEL,
522 entries: &[
523 wgpu::BindGroupLayoutEntry {
525 binding: 0,
526 visibility: wgpu::ShaderStages::COMPUTE,
527 ty: wgpu::BindingType::Buffer {
528 ty: wgpu::BufferBindingType::Storage { read_only: false },
529 has_dynamic_offset: false,
530 min_binding_size: None,
531 },
532 count: None,
533 },
534 ],
535 })
536 }
537
538 fn create_pipeline(
539 device: &wgpu::Device,
540 bindgroup_layout: &wgpu::BindGroupLayout,
541 ) -> wgpu::ComputePipeline {
542 let linkage = crate::linkage::compute_downsample_depth_pyramid::linkage(device);
543 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
544 label: Self::LABEL,
545 layout: Some(
546 &device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
547 label: Self::LABEL,
548 bind_group_layouts: &[bindgroup_layout],
549 push_constant_ranges: &[],
550 }),
551 ),
552 module: &linkage.module,
553 entry_point: Some(linkage.entry_point),
554 compilation_options: wgpu::PipelineCompilationOptions::default(),
555 cache: None,
556 })
557 }
558
559 fn create_bindgroup(
560 device: &wgpu::Device,
561 layout: &wgpu::BindGroupLayout,
562 pyramid_desc_buffer: &wgpu::Buffer,
563 ) -> wgpu::BindGroup {
564 device.create_bind_group(&wgpu::BindGroupDescriptor {
565 label: Self::LABEL,
566 layout,
567 entries: &[wgpu::BindGroupEntry {
568 binding: 0,
569 resource: wgpu::BindingResource::Buffer(
570 pyramid_desc_buffer.as_entire_buffer_binding(),
571 ),
572 }],
573 })
574 }
575
576 pub fn new(pyramid: &DepthPyramid) -> Self {
577 let device = pyramid.slab.device();
578 let bindgroup_layout = Self::create_bindgroup_layout(device);
579 let pipeline = Self::create_pipeline(device, &bindgroup_layout);
580 let pyramid_slab_buffer = pyramid.slab.commit();
581 let bindgroup = Self::create_bindgroup(device, &bindgroup_layout, &pyramid_slab_buffer);
582 Self {
583 pipeline,
584 bindgroup,
585 bindgroup_layout,
586 pyramid_slab_buffer,
587 }
588 }
589
590 pub fn run(&mut self, pyramid: &DepthPyramid) {
591 let device = pyramid.slab.device();
592
593 if self.pyramid_slab_buffer.update_if_invalid() {
594 self.bindgroup =
595 Self::create_bindgroup(device, &self.bindgroup_layout, &self.pyramid_slab_buffer);
596 }
597
598 for i in 1..pyramid.mip_data.len() {
599 log::trace!("downsampling to mip {i}..{}", pyramid.mip_data.len());
600 let size = pyramid.desc.modify(|desc| {
602 desc.mip_level = i as u32;
603 desc.size
604 });
605 pyramid.slab.commit();
607 debug_assert!(
608 self.pyramid_slab_buffer.is_valid(),
609 "pyramid slab should never resize here"
610 );
611
612 let mut encoder = device
613 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Self::LABEL });
614 {
615 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
616 label: Self::LABEL,
617 ..Default::default()
618 });
619 compute_pass.set_pipeline(&self.pipeline);
620 compute_pass.set_bind_group(0, Some(&self.bindgroup), &[]);
621 let w = size.x >> i;
622 let h = size.y >> i;
623 let x = w / 16 + 1;
624 let y = h / 16 + 1;
625 let z = 1;
626 compute_pass.dispatch_workgroups(x, y, z);
627 }
628 pyramid.slab.queue().submit(Some(encoder.finish()));
629 }
630 }
631}
632
633pub struct ComputeDepthPyramid {
635 pub(crate) depth_pyramid: DepthPyramid,
636 compute_copy_depth: ComputeCopyDepth,
637 compute_downsample_depth: ComputeDownsampleDepth,
638}
639
640impl ComputeDepthPyramid {
641 const _LABEL: Option<&'static str> = Some("compute-depth-pyramid");
642
643 pub fn new(runtime: impl AsRef<WgpuRuntime>, depth_texture: &Texture) -> Self {
644 let runtime = runtime.as_ref();
645 let depth_pyramid = DepthPyramid::new(runtime, depth_texture.size());
646 let compute_copy_depth = ComputeCopyDepth::new(&depth_pyramid, depth_texture);
647 let compute_downsample_depth = ComputeDownsampleDepth::new(&depth_pyramid);
648 Self {
649 depth_pyramid,
650 compute_copy_depth,
651 compute_downsample_depth,
652 }
653 }
654
655 pub fn run(&mut self, depth_texture: &Texture) {
658 let extent = depth_texture.texture.size();
659 let size = UVec2::new(extent.width, extent.height);
660 if size != self.depth_pyramid.size() {
661 log::debug!("depth texture size changed");
662 self.depth_pyramid.resize(size);
663 }
664
665 self.compute_copy_depth
666 .run(&mut self.depth_pyramid, depth_texture);
667
668 self.compute_downsample_depth.run(&self.depth_pyramid);
669 }
670}
671
672#[cfg(test)]
673mod test {
674 use std::collections::HashMap;
675
676 use crate::{
677 bvol::BoundingSphere,
678 context::Context,
679 cull::shader::DepthPyramidDescriptor,
680 draw::DrawIndirectArgs,
681 geometry::{Geometry, Vertex},
682 math::hex_to_vec4,
683 primitive::shader::PrimitiveDescriptor,
684 test::BlockOnFuture,
685 };
686 use crabslab::{Array, GrowableSlab, Id, Slab};
687 use glam::{Mat4, Quat, UVec2, UVec3, Vec2, Vec3, Vec4};
688
689 #[test]
690 fn occlusion_culling_sanity() {
691 let ctx = Context::headless(100, 100).block();
692 let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));
693 let camera_position = Vec3::new(0.0, 9.0, 9.0);
694 let _camera = stage.new_camera().with_projection_and_view(
695 Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 1.0, 24.0),
696 Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y),
697 );
698 let _rez = stage
699 .new_primitive()
700 .with_vertices(stage.new_vertices(crate::test::gpu_cube_vertices()))
701 .with_transform(
702 stage
703 .new_transform()
704 .with_scale(Vec3::new(6.0, 6.0, 6.0))
705 .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),
706 );
707
708 let frame = ctx.get_next_frame().unwrap();
709 stage.render(&frame.view());
710 frame.present();
711
712 let frame = ctx.get_next_frame().unwrap();
713 stage.render(&frame.view());
714 let img = frame.read_image().block().unwrap();
715 img_diff::save("cull/pyramid/frame.png", img);
716 frame.present();
717
718 let depth_texture = stage.get_depth_texture();
719 let depth_img = depth_texture.read_image().block().unwrap().unwrap();
720 img_diff::save("cull/pyramid/depth.png", depth_img);
721
722 let pyramid_images = futures_lite::future::block_on(
723 stage
724 .draw_calls
725 .read()
726 .unwrap()
727 .drawing_strategy
728 .as_indirect()
729 .unwrap()
730 .read_hzb_images(),
731 )
732 .unwrap();
733 for (i, img) in pyramid_images.into_iter().enumerate() {
734 img_diff::save(format!("cull/pyramid/mip_{i}.png"), img);
735 }
736 }
737
738 #[test]
739 fn depth_pyramid_descriptor_sanity() {
740 let mut slab = vec![];
741 let size = UVec2::new(64, 32);
742 let mip_levels = size.min_element().ilog2();
743 let desc_id = slab.allocate::<DepthPyramidDescriptor>();
744 let mips_array = slab.allocate_array::<Array<f32>>(mip_levels as usize);
745 let mip_data_arrays = (0..mip_levels)
746 .map(|i| {
747 let w = size.x >> i;
748 let h = size.y >> i;
749 let len = (w * h) as usize;
750 log::info!("allocating {len} f32s for mip {i} of size {w}x{h}");
751 let array = slab.allocate_array::<f32>(len);
752 let mut data: Vec<f32> = vec![];
753 for _y in 0..h {
754 for x in 0..w {
755 data.push(x as f32);
756 }
757 }
758 slab.write_array(array, &data);
759 array
760 })
761 .collect::<Vec<_>>();
762 slab.write_array(mips_array, &mip_data_arrays);
763 let desc = DepthPyramidDescriptor {
764 size: UVec2::new(64, 32),
765 mip_level: 0,
766 mip: mips_array,
767 };
768 slab.write(desc_id, &desc);
769
770 for mip_level in 0..mip_levels {
772 let size = desc.size_at(mip_level);
773 log::info!("mip {mip_level} is size {size}");
774 for y in 0..size.y {
775 for x in 0..size.x {
776 let id = desc.id_of_depth(mip_level, UVec2::new(x, y), &slab);
777 let depth = slab.read(id);
778 assert_eq!(x as f32, depth, "depth should be x value");
779 }
780 }
781 }
782 }
783
784 #[test]
785 fn occlusion_culling_debugging() {
786 let ctx = Context::headless(128, 128).block();
787 let stage = ctx
788 .new_stage()
789 .with_lighting(false)
790 .with_bloom(false)
791 .with_background_color(Vec4::splat(1.0));
792 let _camera = {
793 let fovy = std::f32::consts::FRAC_PI_4;
794 let aspect = 1.0;
795 let znear = 0.1;
796 let zfar = 100.0;
797 let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);
798 let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO, Vec3::Y);
800 stage
801 .new_camera()
802 .with_projection_and_view(projection, view)
803 };
804
805 let save_render = |s: &str| {
806 let frame = ctx.get_next_frame().unwrap();
807 stage.render(&frame.view());
808 let img = frame.read_image().block().unwrap();
809 img_diff::save(format!("cull/debugging_{s}.png"), img);
810 frame.present();
811 };
812
813 let mut names = HashMap::<Id<PrimitiveDescriptor>, String>::default();
815
816 let _ycubes = [
818 (Vec2::new(-1.0, 1.0), "top_left"),
819 (Vec2::new(1.0, 1.0), "top_right"),
820 (Vec2::new(1.0, -1.0), "bottom_right"),
821 (Vec2::new(-1.0, -1.0), "bottom_left"),
822 ]
823 .map(|(offset, suffix)| {
824 let yellow = hex_to_vec4(0xFFE6A5FF);
825 let renderlet = stage
826 .new_primitive()
827 .with_transform(
828 stage
829 .new_transform()
830 .with_translation((offset * 10.0).extend(-20.0))
832 .with_scale(Vec3::splat(2.0)),
834 )
835 .with_vertices(stage.new_vertices(crate::math::unit_cube().into_iter().map(
836 |(p, n)| {
837 Vertex::default()
838 .with_position(p)
839 .with_normal(n)
840 .with_color(yellow)
841 },
842 )))
843 .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length()));
844 names.insert(renderlet.id(), format!("yellow_cube_{suffix}"));
845 renderlet
846 });
847
848 save_render("0_yellow_cubes");
849
850 let _floor = {
852 let golden = hex_to_vec4(0xFFBF61FF);
853 let renderlet = stage
854 .new_primitive()
855 .with_transform(
856 stage
857 .new_transform()
858 .with_rotation(Quat::from_rotation_x(std::f32::consts::FRAC_PI_2))
860 .with_translation(Vec3::new(0.0, -5.0, -10.0))
862 .with_scale(Vec3::new(100.0, 100.0, 1.0)),
864 )
865 .with_vertices(
866 stage.new_vertices(
867 crate::math::UNIT_QUAD_CCW
868 .map(|p| Vertex::default().with_position(p).with_color(golden)),
869 ),
870 )
871 .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec2::splat(0.5).length()));
872 names.insert(renderlet.id(), "floor".into());
873 renderlet
874 };
875
876 save_render("1_floor");
877
878 let _gcube = {
880 let green = hex_to_vec4(0x8ABFA3FF);
881 let renderlet = stage
882 .new_primitive()
883 .with_transform(
884 stage
885 .new_transform()
886 .with_translation(Vec3::new(0.0, 0.0, -10.0))
888 .with_scale(Vec3::splat(5.0)),
890 )
891 .with_vertices(stage.new_vertices(crate::math::unit_cube().into_iter().map(
892 |(p, n)| {
893 Vertex::default()
894 .with_position(p)
895 .with_normal(n)
896 .with_color(green)
897 },
898 )))
899 .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length()));
900 stage.add_primitive(&renderlet);
901 names.insert(renderlet.id(), "green_cube".into());
902 renderlet
903 };
904
905 save_render("2_green_cube");
906
907 let _pcube = {
909 let purple = hex_to_vec4(0x605678FF);
910 let renderlet = stage
911 .new_primitive()
912 .with_transform(
913 stage
914 .new_transform()
915 .with_translation(Vec3::new(0.0, 0.0, -3.0))
917 .with_scale(Vec3::splat(5.0)),
919 )
920 .with_vertices(stage.new_vertices(crate::math::unit_cube().into_iter().map(
921 |(p, n)| {
922 Vertex::default()
923 .with_position(p)
924 .with_normal(n)
925 .with_color(purple)
926 },
927 )))
928 .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length()));
929 names.insert(renderlet.id(), "purple_cube".into());
930 renderlet
931 };
932
933 save_render("3_purple_cube");
936 save_render("3_purple_cube");
937
938 let mut depth_img = stage
940 .get_depth_texture()
941 .read_image()
942 .block()
943 .unwrap()
944 .unwrap();
945 img_diff::normalize_gray_img(&mut depth_img);
946 img_diff::save("cull/debugging_4_depth.png", depth_img);
947
948 let pyramid_images = futures_lite::future::block_on(
950 stage
951 .draw_calls
952 .read()
953 .unwrap()
954 .drawing_strategy
955 .as_indirect()
956 .unwrap()
957 .read_hzb_images(),
958 )
959 .unwrap();
960 for (i, mut img) in pyramid_images.into_iter().enumerate() {
961 img_diff::normalize_gray_img(&mut img);
962 img_diff::save(format!("cull/debugging_pyramid_mip_{i}.png"), img);
963 }
964
965 let stage_slab = futures_lite::future::block_on({
968 let geometry: &Geometry = stage.as_ref();
969 geometry.slab_allocator().read(..)
970 })
971 .unwrap();
972 let draw_calls = stage.draw_calls.read().unwrap();
973 let indirect_draws = draw_calls.drawing_strategy.as_indirect().unwrap();
974 let depth_pyramid_slab = futures_lite::future::block_on(
977 indirect_draws
978 .compute_culling
979 .compute_depth_pyramid
980 .depth_pyramid
981 .slab
982 .read(..),
983 )
984 .unwrap();
985 let mut args_slab = futures_lite::future::block_on(indirect_draws.slab.read(..)).unwrap();
987 let args: &mut [DrawIndirectArgs] = bytemuck::cast_slice_mut(&mut args_slab);
988 let num_draw_calls = draw_calls.draw_count();
990
991 let mut pnames = names.iter().collect::<Vec<_>>();
993 pnames.sort();
994 for (id, name) in pnames.into_iter() {
995 log::info!("id: {id:?}, name: {name}");
996 }
997
998 for i in 0..num_draw_calls as u32 {
999 let renderlet_id = Id::<PrimitiveDescriptor>::new(args[i as usize].first_instance);
1000 let name = names.get(&renderlet_id).unwrap();
1001 if name != "green_cube" {
1002 continue;
1003 }
1004 log::info!("");
1005 log::info!("name: {name}");
1006 crate::cull::shader::compute_culling(
1007 &stage_slab,
1008 &depth_pyramid_slab,
1009 args,
1010 UVec3::new(i, 0, 0),
1011 );
1012 }
1013 }
1014}