renderling/light/
tiling.rs

1//! This module implements light tiling, a technique used in rendering to efficiently manage and apply lighting effects across a scene.
2//!
3//! Light tiling divides the rendering surface into a grid of tiles, allowing for the efficient computation of lighting effects by processing each tile independently. This approach helps in optimizing the rendering pipeline by reducing the number of lighting calculations needed, especially in complex scenes with multiple light sources.
4//!
5//! The `LightTiling` struct and its associated methods provide the necessary functionality to set up and execute light tiling operations. It includes the creation of compute pipelines for clearing tiles, computing minimum and maximum depths, and binning lights into tiles.
6//!
7//! For more detailed information on light tiling and its implementation, refer to [this blog post](https://renderling.xyz/articles/live/light_tiling.html).
8
9use core::sync::atomic::AtomicUsize;
10use std::sync::Arc;
11
12use craballoc::{
13    slab::SlabBuffer,
14    value::{GpuArrayContainer, Hybrid, HybridArrayContainer, IsContainer},
15};
16use crabslab::Id;
17use glam::{UVec2, UVec3};
18
19use crate::{
20    bindgroup::ManagedBindGroup,
21    light::{
22        shader::{LightTile, LightTilingDescriptor},
23        Lighting,
24    },
25    stage::Stage,
26};
27
28/// Shaders and resources for conducting light tiling.
29///
30/// This struct takes a container type variable in order to allow
31/// tests to read and write [`LightTile`] values on the GPU.
32///
33/// For info on what light tiling is, see
34/// <https://renderling.xyz/articles/live/light_tiling.html>.
35pub struct LightTiling<Ct: IsContainer = GpuArrayContainer> {
36    pub(crate) tiling_descriptor: Hybrid<LightTilingDescriptor>,
37    /// Container is a type variable for testing, as we have to load
38    /// the tiles with known values from the CPU.
39    tiles: Ct::Container<LightTile>,
40    /// Cache of the id of the Stage's depth texture.
41    ///
42    /// Used to invalidate our tiling bindgroup.
43    depth_texture_id: Arc<AtomicUsize>,
44
45    bindgroup: ManagedBindGroup,
46    bindgroup_layout: Arc<wgpu::BindGroupLayout>,
47    bindgroup_creation_time: Arc<AtomicUsize>,
48
49    clear_tiles_pipeline: Arc<wgpu::ComputePipeline>,
50    compute_min_max_depth_pipeline: Arc<wgpu::ComputePipeline>,
51    compute_bins_pipeline: Arc<wgpu::ComputePipeline>,
52}
53
54const LABEL: Option<&'static str> = Some("light-tiling");
55
56impl<Ct: IsContainer> LightTiling<Ct> {
57    fn create_bindgroup_layout(device: &wgpu::Device, multisampled: bool) -> wgpu::BindGroupLayout {
58        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
59            label: LABEL,
60            entries: &[
61                // Geometry slab
62                wgpu::BindGroupLayoutEntry {
63                    binding: 0,
64                    visibility: wgpu::ShaderStages::COMPUTE,
65                    ty: wgpu::BindingType::Buffer {
66                        ty: wgpu::BufferBindingType::Storage { read_only: true },
67                        has_dynamic_offset: false,
68                        min_binding_size: None,
69                    },
70                    count: None,
71                },
72                // Lighting slab
73                wgpu::BindGroupLayoutEntry {
74                    binding: 1,
75                    visibility: wgpu::ShaderStages::COMPUTE,
76                    ty: wgpu::BindingType::Buffer {
77                        ty: wgpu::BufferBindingType::Storage { read_only: false },
78                        has_dynamic_offset: false,
79                        min_binding_size: None,
80                    },
81                    count: None,
82                },
83                // Depth texture
84                wgpu::BindGroupLayoutEntry {
85                    binding: 2,
86                    visibility: wgpu::ShaderStages::COMPUTE,
87                    ty: wgpu::BindingType::Texture {
88                        sample_type: wgpu::TextureSampleType::Depth,
89                        view_dimension: wgpu::TextureViewDimension::D2,
90                        multisampled,
91                    },
92                    count: None,
93                },
94            ],
95        })
96    }
97
98    fn create_clear_tiles_pipeline(
99        device: &wgpu::Device,
100        multisampled: bool,
101    ) -> wgpu::ComputePipeline {
102        const LABEL: Option<&'static str> = Some("light-tiling-clear-tiles");
103        let module = crate::linkage::light_tiling_clear_tiles::linkage(device);
104        let (pipeline_layout, _) = Self::create_layouts(device, multisampled);
105        device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
106            label: LABEL,
107            layout: Some(&pipeline_layout),
108            module: &module.module,
109            entry_point: Some(module.entry_point),
110            compilation_options: wgpu::PipelineCompilationOptions::default(),
111            cache: None,
112        })
113    }
114
115    fn create_compute_min_max_depth_pipeline(
116        device: &wgpu::Device,
117        multisampled: bool,
118    ) -> wgpu::ComputePipeline {
119        const LABEL: Option<&'static str> = Some("light-tiling-compute-min-max-depth");
120        let module = crate::linkage::light_tiling_compute_tile_min_and_max_depth::linkage(device);
121        let (pipeline_layout, _) = Self::create_layouts(device, multisampled);
122        device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
123            label: LABEL,
124            layout: Some(&pipeline_layout),
125            module: &module.module,
126            entry_point: Some(module.entry_point),
127            compilation_options: wgpu::PipelineCompilationOptions::default(),
128            cache: None,
129        })
130    }
131
132    fn create_compute_bins_pipeline(
133        device: &wgpu::Device,
134        multisampled: bool,
135    ) -> wgpu::ComputePipeline {
136        const LABEL: Option<&'static str> = Some("light-tiling-compute-bins");
137        let module = crate::linkage::light_tiling_bin_lights::linkage(device);
138        let (pipeline_layout, _) = Self::create_layouts(device, multisampled);
139        device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
140            label: LABEL,
141            layout: Some(&pipeline_layout),
142            module: &module.module,
143            entry_point: Some(module.entry_point),
144            compilation_options: wgpu::PipelineCompilationOptions::default(),
145            cache: None,
146        })
147    }
148
149    /// All pipelines share the same layout, so we do it here, once.
150    fn create_layouts(
151        device: &wgpu::Device,
152        multisampled: bool,
153    ) -> (wgpu::PipelineLayout, wgpu::BindGroupLayout) {
154        let bindgroup_layout = Self::create_bindgroup_layout(device, multisampled);
155        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
156            label: LABEL,
157            bind_group_layouts: &[&bindgroup_layout],
158            push_constant_ranges: &[],
159        });
160        (pipeline_layout, bindgroup_layout)
161    }
162
163    pub(crate) fn prepare(&self, lighting: &Lighting, depth_texture_size: UVec2) {
164        self.tiling_descriptor.modify(|d| {
165            d.depth_texture_size = depth_texture_size;
166        });
167        lighting.lighting_descriptor.modify(|desc| {
168            desc.light_tiling_descriptor_id = self.tiling_descriptor.id();
169        });
170    }
171
172    pub(crate) fn clear_tiles(
173        &self,
174        encoder: &mut wgpu::CommandEncoder,
175        bindgroup: &wgpu::BindGroup,
176        depth_texture_size: UVec2,
177    ) {
178        let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
179            label: Some("light-tiling-clear-tiles"),
180            timestamp_writes: None,
181        });
182        compute_pass.set_pipeline(&self.clear_tiles_pipeline);
183        compute_pass.set_bind_group(0, bindgroup, &[]);
184
185        let tile_size = self.tiling_descriptor.get().tile_size;
186        let dims_f32 = depth_texture_size.as_vec2() / tile_size as f32;
187        let workgroups = (dims_f32 / 16.0).ceil().as_uvec2();
188        let x = workgroups.x;
189        let y = workgroups.y;
190        let z = 1;
191        compute_pass.dispatch_workgroups(x, y, z);
192    }
193
194    const WORKGROUP_SIZE: UVec3 = UVec3::new(16, 16, 1);
195
196    pub(crate) fn compute_min_max_depth(
197        &self,
198        encoder: &mut wgpu::CommandEncoder,
199        bindgroup: &wgpu::BindGroup,
200        depth_texture_size: UVec2,
201    ) {
202        let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
203            label: Some("light-tiling-compute-min-max-depth"),
204            timestamp_writes: None,
205        });
206        compute_pass.set_pipeline(&self.compute_min_max_depth_pipeline);
207        compute_pass.set_bind_group(0, bindgroup, &[]);
208
209        let x = (depth_texture_size.x / Self::WORKGROUP_SIZE.x) + 1;
210        let y = (depth_texture_size.y / Self::WORKGROUP_SIZE.y) + 1;
211        let z = 1;
212        compute_pass.dispatch_workgroups(x, y, z);
213    }
214
215    pub(crate) fn compute_bins(
216        &self,
217        encoder: &mut wgpu::CommandEncoder,
218        bindgroup: &wgpu::BindGroup,
219        depth_texture_size: UVec2,
220    ) {
221        let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
222            label: Some("light-tiling-compute-bins"),
223            timestamp_writes: None,
224        });
225        compute_pass.set_pipeline(&self.compute_bins_pipeline);
226        compute_pass.set_bind_group(0, bindgroup, &[]);
227
228        let tile_size = self.tiling_descriptor.get().tile_size;
229        let x = (depth_texture_size.x / tile_size) + 1;
230        let y = (depth_texture_size.y / tile_size) + 1;
231        let z = 1;
232        compute_pass.dispatch_workgroups(x, y, z);
233    }
234
235    /// Get the bindgroup.
236    pub fn get_bindgroup(
237        &self,
238        device: &wgpu::Device,
239        geometry_slab: &SlabBuffer<wgpu::Buffer>,
240        lighting_slab: &SlabBuffer<wgpu::Buffer>,
241        depth_texture: &crate::texture::Texture,
242    ) -> Arc<wgpu::BindGroup> {
243        // UNWRAP: safe because we know there are elements
244        let latest_buffer_creation = [geometry_slab.creation_time(), lighting_slab.creation_time()]
245            .into_iter()
246            .max()
247            .unwrap();
248        let prev_buffer_creation = self
249            .bindgroup_creation_time
250            .swap(latest_buffer_creation, std::sync::atomic::Ordering::Relaxed);
251        let prev_depth_texture_id = self
252            .depth_texture_id
253            .swap(depth_texture.id(), std::sync::atomic::Ordering::Relaxed);
254        let should_invalidate = prev_buffer_creation < latest_buffer_creation
255            || prev_depth_texture_id < depth_texture.id();
256        self.bindgroup.get(should_invalidate, || {
257            device.create_bind_group(&wgpu::BindGroupDescriptor {
258                label: Some("light-tiling"),
259                layout: &self.bindgroup_layout,
260                entries: &[
261                    wgpu::BindGroupEntry {
262                        binding: 0,
263                        resource: geometry_slab.as_entire_binding(),
264                    },
265                    wgpu::BindGroupEntry {
266                        binding: 1,
267                        resource: lighting_slab.as_entire_binding(),
268                    },
269                    wgpu::BindGroupEntry {
270                        binding: 2,
271                        resource: wgpu::BindingResource::TextureView(&depth_texture.view),
272                    },
273                ],
274            })
275        })
276    }
277
278    /// Set the minimum illuminance, in lux, to determine if a light illuminates a tile.
279    pub fn set_minimum_illuminance(&self, minimum_illuminance_lux: f32) {
280        self.tiling_descriptor.modify(|desc| {
281            desc.minimum_illuminance_lux = minimum_illuminance_lux;
282        });
283    }
284
285    /// Run light tiling, resulting in edits to the lighting slab.
286    pub fn run(&self, stage: &Stage) {
287        let depth_texture = stage.depth_texture.read().unwrap();
288        let depth_texture_size = depth_texture.size();
289        let lighting = stage.as_ref();
290        self.prepare(lighting, depth_texture_size);
291
292        let light_slab = &lighting.light_slab;
293        let geometry_slab = &lighting.geometry_slab;
294        let runtime = light_slab.runtime();
295        let label = Some("light-tiling-run");
296        let bindgroup = self.get_bindgroup(
297            &runtime.device,
298            &geometry_slab.commit(),
299            &light_slab.commit(),
300            &depth_texture,
301        );
302
303        let mut encoder = runtime
304            .device
305            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
306        {
307            self.clear_tiles(&mut encoder, bindgroup.as_ref(), depth_texture_size);
308            self.compute_min_max_depth(&mut encoder, bindgroup.as_ref(), depth_texture_size);
309            self.compute_bins(&mut encoder, bindgroup.as_ref(), depth_texture_size);
310        }
311        runtime.queue.submit(Some(encoder.finish()));
312    }
313
314    pub fn tiles(&self) -> &Ct::Container<LightTile> {
315        &self.tiles
316    }
317
318    #[cfg(test)]
319    /// Read the tiles from the light slab.
320    pub(crate) async fn read_tiles(&self, lighting: &Lighting) -> Vec<LightTile> {
321        lighting
322            .light_slab
323            .read_array(self.tiling_descriptor.get().tiles_array)
324            .await
325            .unwrap()
326    }
327
328    #[cfg(test)]
329    #[allow(dead_code)]
330    pub(crate) fn read_tile(&self, lighting: &Lighting, tile_coord: UVec2) -> LightTile {
331        let desc = self.tiling_descriptor.get();
332        let tile_index = tile_coord.y * desc.tile_grid_size().x + tile_coord.x;
333        let tile_id = desc.tiles_array.at(tile_index as usize);
334        futures_lite::future::block_on(lighting.light_slab.read_one(tile_id)).unwrap()
335    }
336
337    #[cfg(test)]
338    /// Returns a tuple containing an image of depth mins, depth maximums and number of lights.
339    pub(crate) async fn read_images(
340        &self,
341        lighting: &Lighting,
342    ) -> (image::GrayImage, image::GrayImage, image::GrayImage) {
343        use crabslab::Slab;
344
345        use crate::light::shader::dequantize_depth_u32_to_f32;
346
347        let tile_dimensions = self.tiling_descriptor.get().tile_grid_size();
348        let slab = lighting.light_slab.read(..).await.unwrap();
349        let tiling_descriptor_id_in_lighting = lighting
350            .lighting_descriptor
351            .get()
352            .light_tiling_descriptor_id;
353        let tiling_descriptor_id = self.tiling_descriptor.id();
354        assert_eq!(tiling_descriptor_id_in_lighting, tiling_descriptor_id);
355        let desc = slab.read(
356            lighting
357                .lighting_descriptor
358                .get()
359                .light_tiling_descriptor_id,
360        );
361        let should_be_len = tile_dimensions.x * tile_dimensions.y;
362        if should_be_len != desc.tiles_array.len() as u32 {
363            log::error!(
364                "LightTilingDescriptor's tiles array is borked: {:?}\n\
365                   expected {should_be_len} tiles\n\
366                   tile_dimensions: {tile_dimensions}",
367                desc.tiles_array,
368            );
369        }
370        let mut mins_img = image::GrayImage::new(tile_dimensions.x, tile_dimensions.y);
371        let mut maxs_img = image::GrayImage::new(tile_dimensions.x, tile_dimensions.y);
372        let mut lights_img = image::GrayImage::new(tile_dimensions.x, tile_dimensions.y);
373        slab.read_vec(desc.tiles_array)
374            .into_iter()
375            .enumerate()
376            .for_each(|(i, tile)| {
377                let x = i as u32 % tile_dimensions.x;
378                let y = i as u32 / tile_dimensions.x;
379                let min = dequantize_depth_u32_to_f32(tile.depth_min);
380                let max = dequantize_depth_u32_to_f32(tile.depth_max);
381
382                mins_img.get_pixel_mut(x, y).0[0] = crate::math::scaled_f32_to_u8(min);
383                maxs_img.get_pixel_mut(x, y).0[0] = crate::math::scaled_f32_to_u8(max);
384                lights_img.get_pixel_mut(x, y).0[0] = crate::math::scaled_f32_to_u8(
385                    tile.next_light_index as f32 / tile.lights_array.len() as f32,
386                );
387            });
388
389        (mins_img, maxs_img, lights_img)
390    }
391}
392
393/// Parameters for tuning light tiling.
394#[derive(Debug, Clone, Copy)]
395pub struct LightTilingConfig {
396    /// The size of each tile, in pixels.
397    ///
398    /// Default is `16`.
399    pub tile_size: u32,
400    /// The maximum number of lights per tile.
401    ///
402    /// Default is `32`.
403    pub max_lights_per_tile: u32,
404    /// The minimum illuminance, in lux.
405    ///
406    /// Used to determine the radius of illumination of a light,
407    /// which is then used to determine if a light illuminates a tile.
408    ///
409    /// * Moonlight: < 1 lux.
410    ///   - Full moon on a clear night: 0.25 lux.
411    ///   - Quarter moon: 0.01 lux
412    ///   - Starlight overcast moonless night sky: 0.0001 lux.
413    /// * General indoor lighting: Around 100 to 300 lux.                                   
414    /// * Office lighting: Typically around 300 to 500 lux.                                 
415    /// * Reading or task lighting: Around 500 to 750 lux.                                  
416    /// * Detailed work (e.g., drafting, surgery): 1000 lux or more.
417    ///
418    /// Default is `0.1`.
419    pub minimum_illuminance: f32,
420}
421
422impl Default for LightTilingConfig {
423    fn default() -> Self {
424        LightTilingConfig {
425            tile_size: 16,
426            max_lights_per_tile: 32,
427            minimum_illuminance: 0.1,
428        }
429    }
430}
431
432impl LightTiling<HybridArrayContainer> {
433    /// Creates a new [`LightTiling`] struct with a `HybridArray` of tiles.
434    pub(crate) fn new_hybrid(
435        lighting: &Lighting,
436        multisampled: bool,
437        depth_texture_size: UVec2,
438        config: LightTilingConfig,
439    ) -> Self {
440        log::trace!("creating LightTiling");
441        let lighting_slab = lighting.slab_allocator();
442        let runtime = lighting_slab.runtime();
443        let desc = LightTilingDescriptor {
444            depth_texture_size,
445            tile_size: config.tile_size,
446            minimum_illuminance_lux: config.minimum_illuminance,
447            ..Default::default()
448        };
449        let tiling_descriptor = lighting_slab.new_value(desc);
450        lighting.lighting_descriptor.modify(|desc| {
451            desc.light_tiling_descriptor_id = tiling_descriptor.id();
452        });
453        log::trace!("created tiling descriptor: {tiling_descriptor:#?}");
454        let tiled_size = desc.tile_grid_size();
455        log::trace!("  grid size: {tiled_size}");
456        let mut tiles = Vec::new();
457        for _ in 0..tiled_size.x * tiled_size.y {
458            let lights =
459                lighting_slab.new_array(vec![Id::NONE; config.max_lights_per_tile as usize]);
460            tiles.push(LightTile {
461                lights_array: lights.array(),
462                ..Default::default()
463            });
464        }
465        let tiles = lighting_slab.new_array(tiles);
466        tiling_descriptor.modify(|d| {
467            let tiles_array = tiles.array();
468            log::trace!("  setting tiles array: {tiles_array:?}");
469            d.tiles_array = tiles_array;
470        });
471        let clear_tiles_pipeline = Arc::new(Self::create_clear_tiles_pipeline(
472            &runtime.device,
473            multisampled,
474        ));
475        let compute_min_max_depth_pipeline = Arc::new(Self::create_compute_min_max_depth_pipeline(
476            &runtime.device,
477            multisampled,
478        ));
479        let compute_bins_pipeline = Arc::new(Self::create_compute_bins_pipeline(
480            &runtime.device,
481            multisampled,
482        ));
483        let bindgroup_layout =
484            Arc::new(Self::create_bindgroup_layout(&runtime.device, multisampled));
485
486        Self {
487            tiling_descriptor,
488            tiles,
489            // The inner bindgroup is created on-demand
490            bindgroup: ManagedBindGroup::default(),
491            bindgroup_creation_time: Default::default(),
492            bindgroup_layout,
493            depth_texture_id: Default::default(),
494            clear_tiles_pipeline,
495            compute_min_max_depth_pipeline,
496            compute_bins_pipeline,
497        }
498    }
499}
500
501impl LightTiling {
502    /// Creates a new [`LightTiling`] struct.
503    pub fn new(
504        lighting: &Lighting,
505        multisampled: bool,
506        depth_texture_size: UVec2,
507        config: LightTilingConfig,
508    ) -> Self {
509        // Note to self, I wish we had `fmap` here.
510        let LightTiling {
511            tiling_descriptor,
512            tiles,
513            bindgroup_creation_time,
514            depth_texture_id,
515            bindgroup_layout,
516            bindgroup,
517            clear_tiles_pipeline,
518            compute_min_max_depth_pipeline,
519            compute_bins_pipeline,
520        } = LightTiling::new_hybrid(lighting, multisampled, depth_texture_size, config);
521        Self {
522            tiling_descriptor,
523            tiles: tiles.into_gpu_only(),
524            depth_texture_id,
525            bindgroup,
526            bindgroup_layout,
527            bindgroup_creation_time,
528            clear_tiles_pipeline,
529            compute_min_max_depth_pipeline,
530            compute_bins_pipeline,
531        }
532    }
533}