bevy_hikari/
light.rs

1use crate::{
2    mesh_material::{
3        MeshMaterialBindGroup, MeshMaterialBindGroupLayout, MeshMaterialSystems,
4        TextureBindGroupLayout,
5    },
6    prepass::{DeferredBindGroup, PrepassBindGroup, PrepassPipeline, PrepassTextures},
7    view::{FrameCounter, FrameUniform, PreviousViewUniformOffset},
8    HikariSettings, NoiseTextures, LIGHT_SHADER_HANDLE, WORKGROUP_SIZE,
9};
10use bevy::{
11    pbr::ViewLightsUniformOffset,
12    prelude::*,
13    render::{
14        camera::ExtractedCamera,
15        extract_component::DynamicUniformIndex,
16        render_asset::RenderAssets,
17        render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
18        render_resource::*,
19        renderer::{RenderContext, RenderDevice, RenderQueue},
20        texture::{FallbackImage, TextureCache},
21        view::ViewUniformOffset,
22        RenderApp, RenderStage,
23    },
24    utils::HashMap,
25};
26use itertools::multizip;
27use serde::Serialize;
28
29pub const ALBEDO_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rgba16Float;
30pub const VARIANCE_TEXTURE_FORMAT: TextureFormat = TextureFormat::R32Float;
31pub const RENDER_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rgba16Float;
32
33pub struct LightPlugin;
34impl Plugin for LightPlugin {
35    fn build(&self, app: &mut App) {
36        if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
37            render_app
38                .init_resource::<ReservoirCache>()
39                .init_resource::<SpecializedComputePipelines<LightPipeline>>()
40                .add_system_to_stage(
41                    RenderStage::Prepare,
42                    prepare_light_pipeline.after(MeshMaterialSystems::PrepareAssets),
43                )
44                .add_system_to_stage(RenderStage::Prepare, prepare_light_textures)
45                .add_system_to_stage(RenderStage::Queue, queue_light_bind_groups)
46                .add_system_to_stage(RenderStage::Queue, queue_light_pipelines);
47        }
48    }
49}
50
51#[derive(Debug, Default, Clone, Copy, ShaderType)]
52pub struct GpuPackedReservoir {
53    pub radiance: UVec2,
54    pub random: UVec2,
55    pub visible_position: Vec4,
56    pub sample_position: Vec4,
57    pub visible_normal: u32,
58    pub sample_normal: u32,
59    pub reservoir: UVec2,
60}
61
62#[derive(Default, Resource, Clone, ShaderType)]
63pub struct GpuReservoirBuffer {
64    #[size(runtime)]
65    pub data: Vec<GpuPackedReservoir>,
66}
67
68#[derive(Default, Resource, Deref, DerefMut)]
69pub struct ReservoirCache(HashMap<Entity, Vec<StorageBuffer<GpuReservoirBuffer>>>);
70
71#[derive(Resource)]
72pub struct LightPipeline {
73    pub view_layout: BindGroupLayout,
74    pub deferred_layout: BindGroupLayout,
75    pub mesh_material_layout: BindGroupLayout,
76
77    pub texture_count: u32,
78    pub texture_layout: BindGroupLayout,
79
80    pub noise_layout: BindGroupLayout,
81    pub render_layout: BindGroupLayout,
82    pub reservoir_layout: BindGroupLayout,
83}
84
85#[repr(C)]
86#[derive(Default, Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, FromPrimitive)]
87#[serde(rename_all = "snake_case")]
88pub enum LightEntryPoint {
89    #[default]
90    DirectLit = 0,
91    IndirectLitAmbient = 1,
92    SpatialReuse = 2,
93    FullScreenAlbedo = 3,
94}
95
96bitflags::bitflags! {
97    #[repr(transparent)]
98    pub struct LightPipelineKey: u32 {
99        const ENTRY_POINT_BITS      = LightPipelineKey::ENTRY_POINT_MASK_BITS;
100        const EMISSIVE_LIT_BIT      = 1 << LightPipelineKey::EMISSIVE_LIT_SHIFT_BITS;
101        const RENDER_EMISSIVE_BIT   = 1 << LightPipelineKey::RENDER_EMISSIVE_SHIFT_BITS;
102        const MULTIPLE_BOUNCES_BIT  = 1 << LightPipelineKey::MULTIPLE_BOUNCES_SHIFT_BITS;
103        const TEXTURE_COUNT_BITS    = LightPipelineKey::TEXTURE_COUNT_MASK_BITS << LightPipelineKey::TEXTURE_COUNT_SHIFT_BITS;
104    }
105}
106
107impl LightPipelineKey {
108    const ENTRY_POINT_MASK_BITS: u32 = 0xF;
109    const EMISSIVE_LIT_SHIFT_BITS: u32 = 4;
110    const RENDER_EMISSIVE_SHIFT_BITS: u32 = 5;
111    const MULTIPLE_BOUNCES_SHIFT_BITS: u32 = 6;
112    const TEXTURE_COUNT_MASK_BITS: u32 = 0xFFFF;
113    const TEXTURE_COUNT_SHIFT_BITS: u32 = 32 - 16;
114
115    pub fn from_entry_point(entry_point: LightEntryPoint) -> Self {
116        let entry_point_bits = (entry_point as u32) & Self::ENTRY_POINT_MASK_BITS;
117        Self::from_bits(entry_point_bits).unwrap()
118    }
119
120    pub fn entry_point(&self) -> LightEntryPoint {
121        let entry_point_bits = self.bits & Self::ENTRY_POINT_MASK_BITS;
122        num_traits::FromPrimitive::from_u32(entry_point_bits).unwrap()
123    }
124
125    pub fn from_texture_count(texture_count: u32) -> Self {
126        let texture_count_bits =
127            (texture_count & Self::TEXTURE_COUNT_MASK_BITS) << Self::TEXTURE_COUNT_SHIFT_BITS;
128        Self::from_bits(texture_count_bits).unwrap()
129    }
130
131    pub fn texture_count(&self) -> u32 {
132        (self.bits >> Self::TEXTURE_COUNT_SHIFT_BITS) & Self::TEXTURE_COUNT_MASK_BITS
133    }
134}
135
136impl SpecializedComputePipeline for LightPipeline {
137    type Key = LightPipelineKey;
138
139    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
140        let mut shader_defs = vec![];
141        if key.texture_count() == 0 {
142            shader_defs.push("NO_TEXTURE".into());
143        }
144        if key.contains(LightPipelineKey::EMISSIVE_LIT_BIT) {
145            shader_defs.push("EMISSIVE_LIT".into());
146        }
147        if key.contains(LightPipelineKey::RENDER_EMISSIVE_BIT) {
148            shader_defs.push("RENDER_EMISSIVE".into());
149        }
150        if key.contains(LightPipelineKey::MULTIPLE_BOUNCES_BIT) {
151            shader_defs.push("MULTIPLE_BOUNCES".into());
152        }
153
154        let entry_point = serde_variant::to_variant_name(&key.entry_point())
155            .unwrap()
156            .into();
157
158        ComputePipelineDescriptor {
159            label: None,
160            layout: Some(vec![
161                self.view_layout.clone(),
162                self.deferred_layout.clone(),
163                self.mesh_material_layout.clone(),
164                self.texture_layout.clone(),
165                self.noise_layout.clone(),
166                self.render_layout.clone(),
167                self.reservoir_layout.clone(),
168            ]),
169            shader: LIGHT_SHADER_HANDLE.typed::<Shader>(),
170            shader_defs,
171            entry_point,
172        }
173    }
174}
175
176fn prepare_light_pipeline(
177    mut commands: Commands,
178    render_device: Res<RenderDevice>,
179    mesh_material_layout: Res<MeshMaterialBindGroupLayout>,
180    texture_layout: Res<TextureBindGroupLayout>,
181    prepass_pipeline: Res<PrepassPipeline>,
182) {
183    if !texture_layout.is_changed() {
184        return;
185    }
186
187    let view_layout = prepass_pipeline.view_layout.clone();
188    let mesh_material_layout = mesh_material_layout.clone();
189
190    let texture_count = texture_layout.texture_count;
191    let texture_layout = texture_layout.layout.clone();
192
193    let deferred_layout = PrepassTextures::bind_group_layout(&render_device);
194    let noise_layout = NoiseTextures::bind_group_layout(&render_device);
195
196    let render_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
197        label: None,
198        entries: &[
199            // Albedo Texture
200            BindGroupLayoutEntry {
201                binding: 0,
202                visibility: ShaderStages::COMPUTE,
203                ty: BindingType::StorageTexture {
204                    access: StorageTextureAccess::ReadWrite,
205                    format: ALBEDO_TEXTURE_FORMAT,
206                    view_dimension: TextureViewDimension::D2,
207                },
208                count: None,
209            },
210            // Variance Texture
211            BindGroupLayoutEntry {
212                binding: 1,
213                visibility: ShaderStages::COMPUTE,
214                ty: BindingType::StorageTexture {
215                    access: StorageTextureAccess::ReadWrite,
216                    format: VARIANCE_TEXTURE_FORMAT,
217                    view_dimension: TextureViewDimension::D2,
218                },
219                count: None,
220            },
221            // Render Texture
222            BindGroupLayoutEntry {
223                binding: 2,
224                visibility: ShaderStages::COMPUTE,
225                ty: BindingType::StorageTexture {
226                    access: StorageTextureAccess::ReadWrite,
227                    format: RENDER_TEXTURE_FORMAT,
228                    view_dimension: TextureViewDimension::D2,
229                },
230                count: None,
231            },
232        ],
233    });
234
235    let reservoir_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
236        label: None,
237        entries: &[
238            // Previous Reservoir
239            BindGroupLayoutEntry {
240                binding: 0,
241                visibility: ShaderStages::COMPUTE,
242                ty: BindingType::Buffer {
243                    ty: BufferBindingType::Storage { read_only: true },
244                    has_dynamic_offset: false,
245                    min_binding_size: Some(GpuReservoirBuffer::min_size()),
246                },
247                count: None,
248            },
249            // Current Reservoir
250            BindGroupLayoutEntry {
251                binding: 1,
252                visibility: ShaderStages::COMPUTE,
253                ty: BindingType::Buffer {
254                    ty: BufferBindingType::Storage { read_only: false },
255                    has_dynamic_offset: false,
256                    min_binding_size: Some(GpuReservoirBuffer::min_size()),
257                },
258                count: None,
259            },
260            // Previous Spatial Reservoir
261            BindGroupLayoutEntry {
262                binding: 2,
263                visibility: ShaderStages::COMPUTE,
264                ty: BindingType::Buffer {
265                    ty: BufferBindingType::Storage { read_only: false },
266                    has_dynamic_offset: false,
267                    min_binding_size: Some(GpuReservoirBuffer::min_size()),
268                },
269                count: None,
270            },
271            // Current Spatial Reservoir
272            BindGroupLayoutEntry {
273                binding: 3,
274                visibility: ShaderStages::COMPUTE,
275                ty: BindingType::Buffer {
276                    ty: BufferBindingType::Storage { read_only: false },
277                    has_dynamic_offset: false,
278                    min_binding_size: Some(GpuReservoirBuffer::min_size()),
279                },
280                count: None,
281            },
282        ],
283    });
284
285    commands.insert_resource(LightPipeline {
286        view_layout,
287        deferred_layout,
288        mesh_material_layout,
289        texture_count,
290        texture_layout,
291        noise_layout,
292        render_layout,
293        reservoir_layout,
294    });
295}
296
297#[derive(Component)]
298pub struct LightTextures {
299    /// Index of the current frame's output denoised texture.
300    pub head: usize,
301    pub albedo: TextureView,
302    pub variance: [TextureView; 3],
303    pub render: [TextureView; 3],
304}
305
306#[allow(clippy::too_many_arguments)]
307fn prepare_light_textures(
308    mut commands: Commands,
309    render_device: Res<RenderDevice>,
310    render_queue: Res<RenderQueue>,
311    mut texture_cache: ResMut<TextureCache>,
312    mut reservoir_cache: ResMut<ReservoirCache>,
313    cameras: Query<(Entity, &ExtractedCamera, &FrameCounter, &HikariSettings)>,
314) {
315    for (entity, camera, counter, settings) in &cameras {
316        if let Some(size) = camera.physical_target_size {
317            let texture_usage = TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING;
318            let scale = settings.upscale.ratio().recip();
319            let scaled_size = (scale * size.as_vec2()).ceil().as_uvec2();
320            let mut create_texture = |texture_format, size: UVec2| {
321                let extent = Extent3d {
322                    width: size.x,
323                    height: size.y,
324                    depth_or_array_layers: 1,
325                };
326                texture_cache
327                    .get(
328                        &render_device,
329                        TextureDescriptor {
330                            label: None,
331                            size: extent,
332                            mip_level_count: 1,
333                            sample_count: 1,
334                            dimension: TextureDimension::D2,
335                            format: texture_format,
336                            usage: texture_usage,
337                        },
338                    )
339                    .default_view
340            };
341
342            if match reservoir_cache.get(&entity) {
343                Some(reservoirs) => {
344                    let len = (size.x * size.y) as usize;
345                    reservoirs
346                        .iter()
347                        .any(|buffer| buffer.get().data.len() != len)
348                }
349                None => true,
350            } {
351                // Reservoirs of this entity should be updated.
352                let len = (size.x * size.y) as usize;
353                let reservoirs = (0..10)
354                    .map(|_| {
355                        let mut buffer = StorageBuffer::from(GpuReservoirBuffer {
356                            data: vec![GpuPackedReservoir::default(); len],
357                        });
358                        buffer.write_buffer(&render_device, &render_queue);
359                        buffer
360                    })
361                    .collect();
362                reservoir_cache.insert(entity, reservoirs);
363            }
364
365            macro_rules! create_texture_array {
366                [$texture_format:ident, $size:ident; $count:literal] => {
367                    [(); $count].map(|_| create_texture($texture_format, $size))
368                };
369            }
370
371            let variance = create_texture_array![VARIANCE_TEXTURE_FORMAT, scaled_size; 3];
372            let render = create_texture_array![RENDER_TEXTURE_FORMAT, scaled_size; 3];
373            let albedo = create_texture(ALBEDO_TEXTURE_FORMAT, size);
374
375            commands.entity(entity).insert(LightTextures {
376                head: counter.0 % 2,
377                albedo,
378                variance,
379                render,
380            });
381        }
382    }
383}
384
385#[derive(Resource)]
386pub struct CachedLightPipelines {
387    full_screen_albedo: CachedComputePipelineId,
388    direct_lit: CachedComputePipelineId,
389    direct_emissive: CachedComputePipelineId,
390    indirect: CachedComputePipelineId,
391    indirect_multiple_bounces: CachedComputePipelineId,
392    emissive_spatial_reuse: CachedComputePipelineId,
393    indirect_spatial_reuse: CachedComputePipelineId,
394}
395
396fn queue_light_pipelines(
397    mut commands: Commands,
398    pipeline: Res<LightPipeline>,
399    mut pipelines: ResMut<SpecializedComputePipelines<LightPipeline>>,
400    mut pipeline_cache: ResMut<PipelineCache>,
401) {
402    let key = LightPipelineKey::from_texture_count(pipeline.texture_count);
403
404    let full_screen_albedo = {
405        let key = key | LightPipelineKey::from_entry_point(LightEntryPoint::FullScreenAlbedo);
406        pipelines.specialize(&mut pipeline_cache, &pipeline, key)
407    };
408
409    let direct_lit = {
410        let key = key
411            | LightPipelineKey::from_entry_point(LightEntryPoint::DirectLit)
412            | LightPipelineKey::RENDER_EMISSIVE_BIT;
413        pipelines.specialize(&mut pipeline_cache, &pipeline, key)
414    };
415    let direct_emissive = {
416        let key = key
417            | LightPipelineKey::from_entry_point(LightEntryPoint::DirectLit)
418            | LightPipelineKey::EMISSIVE_LIT_BIT;
419        pipelines.specialize(&mut pipeline_cache, &pipeline, key)
420    };
421
422    let indirect = {
423        let key = key | LightPipelineKey::from_entry_point(LightEntryPoint::IndirectLitAmbient);
424        pipelines.specialize(&mut pipeline_cache, &pipeline, key)
425    };
426    let indirect_multiple_bounces = {
427        let key = key
428            | LightPipelineKey::from_entry_point(LightEntryPoint::IndirectLitAmbient)
429            | LightPipelineKey::MULTIPLE_BOUNCES_BIT;
430        pipelines.specialize(&mut pipeline_cache, &pipeline, key)
431    };
432
433    let emissive_spatial_reuse = {
434        let key = key
435            | LightPipelineKey::from_entry_point(LightEntryPoint::SpatialReuse)
436            | LightPipelineKey::EMISSIVE_LIT_BIT;
437        pipelines.specialize(&mut pipeline_cache, &pipeline, key)
438    };
439    let indirect_spatial_reuse = {
440        let key = key | LightPipelineKey::from_entry_point(LightEntryPoint::SpatialReuse);
441        pipelines.specialize(&mut pipeline_cache, &pipeline, key)
442    };
443
444    commands.insert_resource(CachedLightPipelines {
445        full_screen_albedo,
446        direct_lit,
447        direct_emissive,
448        indirect,
449        indirect_multiple_bounces,
450        emissive_spatial_reuse,
451        indirect_spatial_reuse,
452    })
453}
454
455#[derive(Component, Clone)]
456pub struct LightBindGroup {
457    pub noise: BindGroup,
458    pub render: [BindGroup; 3],
459    pub reservoir: [BindGroup; 3],
460}
461
462#[allow(clippy::too_many_arguments)]
463fn queue_light_bind_groups(
464    mut commands: Commands,
465    render_device: Res<RenderDevice>,
466    pipeline: Res<LightPipeline>,
467    noise: Res<NoiseTextures>,
468    images: Res<RenderAssets<Image>>,
469    fallback: Res<FallbackImage>,
470    reservoir_cache: Res<ReservoirCache>,
471    query: Query<(Entity, &LightTextures), With<ExtractedCamera>>,
472) {
473    for (entity, light) in &query {
474        let reservoirs = reservoir_cache.get(&entity).unwrap();
475        if let Some(reservoir_bindings) = reservoirs
476            .iter()
477            .map(|buffer| buffer.binding())
478            .collect::<Option<Vec<_>>>()
479        {
480            let current = light.head;
481            let previous = 1 - current;
482
483            let noise = match noise.as_bind_group(
484                &pipeline.noise_layout,
485                &render_device,
486                &images,
487                &fallback,
488            ) {
489                Ok(noise) => noise,
490                Err(_) => continue,
491            }
492            .bind_group;
493
494            let render = [0, 1, 2].map(|id| {
495                let variance = &light.variance[id];
496                let render = &light.render[id];
497
498                render_device.create_bind_group(&BindGroupDescriptor {
499                    label: None,
500                    layout: &pipeline.render_layout,
501                    entries: &[
502                        BindGroupEntry {
503                            binding: 0,
504                            resource: BindingResource::TextureView(&light.albedo),
505                        },
506                        BindGroupEntry {
507                            binding: 1,
508                            resource: BindingResource::TextureView(variance),
509                        },
510                        BindGroupEntry {
511                            binding: 2,
512                            resource: BindingResource::TextureView(render),
513                        },
514                    ],
515                })
516            });
517
518            let reservoir = [(0, 4), (2, 4), (6, 8)].map(|(temporal, spatial)| {
519                let current_temporal = reservoir_bindings[current + temporal].clone();
520                let previous_temporal = reservoir_bindings[previous + temporal].clone();
521                let current_spatial = reservoir_bindings[current + spatial].clone();
522                let previous_spatial = reservoir_bindings[previous + spatial].clone();
523
524                render_device.create_bind_group(&BindGroupDescriptor {
525                    label: None,
526                    layout: &pipeline.reservoir_layout,
527                    entries: &[
528                        BindGroupEntry {
529                            binding: 0,
530                            resource: current_temporal,
531                        },
532                        BindGroupEntry {
533                            binding: 1,
534                            resource: previous_temporal,
535                        },
536                        BindGroupEntry {
537                            binding: 2,
538                            resource: current_spatial,
539                        },
540                        BindGroupEntry {
541                            binding: 3,
542                            resource: previous_spatial,
543                        },
544                    ],
545                })
546            });
547
548            commands.entity(entity).insert(LightBindGroup {
549                noise,
550                render,
551                reservoir,
552            });
553        }
554    }
555}
556
557#[allow(clippy::type_complexity)]
558pub struct LightNode {
559    query: QueryState<(
560        &'static ExtractedCamera,
561        &'static DynamicUniformIndex<FrameUniform>,
562        &'static ViewUniformOffset,
563        &'static PreviousViewUniformOffset,
564        &'static ViewLightsUniformOffset,
565        &'static DeferredBindGroup,
566        &'static LightBindGroup,
567        &'static HikariSettings,
568    )>,
569}
570
571impl LightNode {
572    pub const IN_VIEW: &'static str = "view";
573
574    pub fn new(world: &mut World) -> Self {
575        Self {
576            query: world.query_filtered(),
577        }
578    }
579}
580
581impl Node for LightNode {
582    fn input(&self) -> Vec<SlotInfo> {
583        vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)]
584    }
585
586    fn update(&mut self, world: &mut World) {
587        self.query.update_archetypes(world);
588    }
589
590    fn run(
591        &self,
592        graph: &mut RenderGraphContext,
593        render_context: &mut RenderContext,
594        world: &World,
595    ) -> Result<(), NodeRunError> {
596        let entity = graph.get_input_entity(Self::IN_VIEW)?;
597        let (
598            camera,
599            frame_uniform,
600            view_uniform,
601            previous_view_uniform,
602            view_lights,
603            deferred_bind_group,
604            light_bind_group,
605            settings,
606        ) = match self.query.get_manual(world, entity) {
607            Ok(query) => query,
608            Err(_) => return Ok(()),
609        };
610        let view_bind_group = match world.get_resource::<PrepassBindGroup>() {
611            Some(bind_group) => &bind_group.view,
612            None => return Ok(()),
613        };
614        let mesh_material_bind_group = match world.get_resource::<MeshMaterialBindGroup>() {
615            Some(bind_group) => bind_group,
616            None => return Ok(()),
617        };
618
619        let pipelines = world.resource::<CachedLightPipelines>();
620        let pipeline_cache = world.resource::<PipelineCache>();
621
622        let size = camera.physical_target_size.unwrap();
623        let scale = settings.upscale.ratio().recip();
624        let scaled_size = (scale * size.as_vec2()).ceil().as_uvec2();
625
626        let mut pass = render_context
627            .command_encoder
628            .begin_compute_pass(&ComputePassDescriptor::default());
629
630        pass.set_bind_group(
631            0,
632            view_bind_group,
633            &[
634                frame_uniform.index(),
635                view_uniform.offset,
636                previous_view_uniform.offset,
637                view_lights.offset,
638            ],
639        );
640        pass.set_bind_group(1, &deferred_bind_group.0, &[]);
641        pass.set_bind_group(2, &mesh_material_bind_group.mesh_material, &[]);
642        pass.set_bind_group(3, &mesh_material_bind_group.texture, &[]);
643        pass.set_bind_group(4, &light_bind_group.noise, &[]);
644
645        // Full screen albedo pass.
646        if let Some(pipeline) = pipeline_cache.get_compute_pipeline(pipelines.full_screen_albedo) {
647            pass.set_bind_group(5, &light_bind_group.render[0], &[]);
648            pass.set_bind_group(6, &light_bind_group.reservoir[0], &[]);
649            pass.set_pipeline(pipeline);
650
651            let count = (size + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
652            pass.dispatch_workgroups(count.x, count.y, 1);
653        }
654
655        // Direct, emissive and indirect passes.
656        for (render, reservoir, temporal_pipeline, spatial_pipeline, enable_spatial_reuse) in
657            multizip((
658                light_bind_group.render.iter(),
659                light_bind_group.reservoir.iter(),
660                [
661                    &pipelines.direct_lit,
662                    &pipelines.direct_emissive,
663                    match settings.indirect_bounces {
664                        x if x < 2 => &pipelines.indirect,
665                        _ => &pipelines.indirect_multiple_bounces,
666                    },
667                ],
668                [
669                    None,
670                    Some(&pipelines.emissive_spatial_reuse),
671                    Some(&pipelines.indirect_spatial_reuse),
672                ],
673                [
674                    false,
675                    settings.emissive_spatial_reuse,
676                    settings.indirect_spatial_reuse,
677                ],
678            ))
679        {
680            pass.set_bind_group(5, render, &[]);
681            pass.set_bind_group(6, reservoir, &[]);
682
683            if let Some(pipeline) = pipeline_cache.get_compute_pipeline(*temporal_pipeline) {
684                pass.set_pipeline(pipeline);
685
686                let count = (scaled_size + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
687                pass.dispatch_workgroups(count.x, count.y, 1);
688
689                if let Some(pipeline) = spatial_pipeline
690                    .filter(|_| enable_spatial_reuse)
691                    .and_then(|pipeline| pipeline_cache.get_compute_pipeline(*pipeline))
692                {
693                    pass.set_pipeline(pipeline);
694
695                    let count = (scaled_size + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
696                    pass.dispatch_workgroups(count.x, count.y, 1);
697                }
698            }
699        }
700
701        Ok(())
702    }
703}