Skip to main content

renderling/light/
tiling.rs

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