1use wgpu::util::DeviceExt;
14
15pub const CLUSTER_X_TILES: u32 = 16;
17pub const CLUSTER_Y_TILES: u32 = 9;
19pub const CLUSTER_Z_SLICES: u32 = 24;
21pub const CLUSTER_COUNT: u32 = CLUSTER_X_TILES * CLUSTER_Y_TILES * CLUSTER_Z_SLICES;
23pub const MAX_LIGHT_INDICES: u32 = 32 * 1024;
27pub const SMALL_N_THRESHOLD: u32 = 16;
31
32#[repr(C)]
41#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
42pub struct ClusterGridUniform {
43 pub dimensions: [u32; 4],
45 pub depth: [f32; 4],
47 pub screen: [f32; 4],
51 pub proj_scale: [f32; 4],
54 pub view: [[f32; 4]; 4],
57}
58
59impl Default for ClusterGridUniform {
60 fn default() -> Self {
61 Self {
62 dimensions: [CLUSTER_X_TILES, CLUSTER_Y_TILES, CLUSTER_Z_SLICES, CLUSTER_COUNT],
63 depth: [0.1, 1000.0, (1000.0_f32 / 0.1_f32).ln(), 0.0],
64 screen: [1.0, 1.0, 1.0, 0.0],
65 proj_scale: [1.0, 1.0, 0.0, 0.0],
66 view: glam::Mat4::IDENTITY.to_cols_array_2d(),
67 }
68 }
69}
70
71#[repr(C)]
80#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
81pub struct ActiveLightView {
82 pub view_pos_range: [f32; 4],
84 pub type_pad: [u32; 4],
86 pub spot_data: [f32; 4],
88}
89
90#[repr(C)]
92#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
93struct ClearParams {
94 cluster_count: u32,
95 index_count: u32,
96 _pad0: u32,
97 _pad1: u32,
98}
99
100#[derive(Debug, Clone, Copy, Default)]
109pub struct ClusterStats {
110 pub total_cells: u32,
112 pub non_empty_cells: u32,
114 pub max_punctual: u32,
116 pub median_punctual: u32,
118 pub p99_punctual: u32,
121 pub mean_punctual: f32,
123 pub total_index_slots_used: u32,
126 pub max_index_slots: u32,
128 pub active_light_count: u32,
130 pub fallback_active: bool,
134}
135
136#[repr(C)]
137#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
138pub struct ClusterCell {
139 pub offset: u32,
142 pub count: u32,
146 pub punctual_count: u32,
150 pub _pad: u32,
152}
153
154pub struct ClusteredResources {
156 pub grid_uniform_buf: wgpu::Buffer,
158 pub cluster_grid_buf: wgpu::Buffer,
160 pub light_index_buf: wgpu::Buffer,
162 pub active_lights_buf: wgpu::Buffer,
165 pub global_offset_buf: wgpu::Buffer,
168 stats_staging_buf: wgpu::Buffer,
171 clear_bind_group: wgpu::BindGroup,
173 clear_pipeline: wgpu::ComputePipeline,
175 build_bind_group: wgpu::BindGroup,
177 build_pipeline: wgpu::ComputePipeline,
179 #[allow(dead_code)]
181 clear_params_buf: wgpu::Buffer,
182}
183
184impl ClusteredResources {
185 pub fn new(device: &wgpu::Device) -> Self {
188 let grid_uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
189 label: Some("cluster_grid_uniform_buf"),
190 contents: bytemuck::cast_slice(&[ClusterGridUniform::default()]),
191 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
192 });
193
194 let cluster_grid_bytes =
195 (CLUSTER_COUNT as u64) * std::mem::size_of::<ClusterCell>() as u64;
196 let cluster_grid_buf = device.create_buffer(&wgpu::BufferDescriptor {
197 label: Some("cluster_grid_buf"),
198 size: cluster_grid_bytes,
199 usage: wgpu::BufferUsages::STORAGE
200 | wgpu::BufferUsages::COPY_DST
201 | wgpu::BufferUsages::COPY_SRC,
202 mapped_at_creation: false,
203 });
204
205 let light_index_bytes = (MAX_LIGHT_INDICES as u64) * 4;
206 let light_index_buf = device.create_buffer(&wgpu::BufferDescriptor {
207 label: Some("cluster_light_index_buf"),
208 size: light_index_bytes,
209 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
210 mapped_at_creation: false,
211 });
212
213 let active_lights_bytes =
214 (crate::resources::MAX_SCENE_LIGHTS as u64) * std::mem::size_of::<ActiveLightView>() as u64;
215 let active_lights_buf = device.create_buffer(&wgpu::BufferDescriptor {
216 label: Some("cluster_active_lights_buf"),
217 size: active_lights_bytes,
218 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
219 mapped_at_creation: false,
220 });
221
222 let global_offset_buf = device.create_buffer(&wgpu::BufferDescriptor {
223 label: Some("cluster_global_offset_buf"),
224 size: 4,
225 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
226 mapped_at_creation: false,
227 });
228
229 let stats_staging_buf = device.create_buffer(&wgpu::BufferDescriptor {
230 label: Some("cluster_stats_staging_buf"),
231 size: cluster_grid_bytes,
232 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
233 mapped_at_creation: false,
234 });
235
236 let clear_params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
237 label: Some("cluster_clear_params_buf"),
238 contents: bytemuck::cast_slice(&[ClearParams {
239 cluster_count: CLUSTER_COUNT,
240 index_count: MAX_LIGHT_INDICES,
241 _pad0: 0,
242 _pad1: 0,
243 }]),
244 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
245 });
246
247 let storage_entry =
248 |binding: u32, read_only: bool| wgpu::BindGroupLayoutEntry {
249 binding,
250 visibility: wgpu::ShaderStages::COMPUTE,
251 ty: wgpu::BindingType::Buffer {
252 ty: wgpu::BufferBindingType::Storage { read_only },
253 has_dynamic_offset: false,
254 min_binding_size: None,
255 },
256 count: None,
257 };
258 let uniform_entry = |binding: u32| wgpu::BindGroupLayoutEntry {
259 binding,
260 visibility: wgpu::ShaderStages::COMPUTE,
261 ty: wgpu::BindingType::Buffer {
262 ty: wgpu::BufferBindingType::Uniform,
263 has_dynamic_offset: false,
264 min_binding_size: None,
265 },
266 count: None,
267 };
268
269 let clear_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
270 label: Some("cluster_clear_bgl"),
271 entries: &[
272 storage_entry(0, false), storage_entry(1, false), storage_entry(2, false), uniform_entry(3), ],
277 });
278
279 let clear_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
280 label: Some("cluster_clear_bind_group"),
281 layout: &clear_bgl,
282 entries: &[
283 wgpu::BindGroupEntry {
284 binding: 0,
285 resource: cluster_grid_buf.as_entire_binding(),
286 },
287 wgpu::BindGroupEntry {
288 binding: 1,
289 resource: light_index_buf.as_entire_binding(),
290 },
291 wgpu::BindGroupEntry {
292 binding: 2,
293 resource: global_offset_buf.as_entire_binding(),
294 },
295 wgpu::BindGroupEntry {
296 binding: 3,
297 resource: clear_params_buf.as_entire_binding(),
298 },
299 ],
300 });
301
302 let clear_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
303 label: Some("cluster_clear_shader"),
304 source: wgpu::ShaderSource::Wgsl(
305 include_str!(concat!(env!("OUT_DIR"), "/cluster_clear.wgsl")).into(),
306 ),
307 });
308 let clear_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
309 label: Some("cluster_clear_pipeline_layout"),
310 bind_group_layouts: &[&clear_bgl],
311 push_constant_ranges: &[],
312 });
313 let clear_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
314 label: Some("cluster_clear_pipeline"),
315 layout: Some(&clear_layout),
316 module: &clear_shader,
317 entry_point: Some("main"),
318 compilation_options: wgpu::PipelineCompilationOptions::default(),
319 cache: None,
320 });
321
322 let build_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
325 label: Some("cluster_build_bgl"),
326 entries: &[
327 storage_entry(0, false), storage_entry(1, false), storage_entry(2, false), uniform_entry(3), storage_entry(4, true), ],
333 });
334 let build_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
335 label: Some("cluster_build_bind_group"),
336 layout: &build_bgl,
337 entries: &[
338 wgpu::BindGroupEntry { binding: 0, resource: cluster_grid_buf.as_entire_binding() },
339 wgpu::BindGroupEntry { binding: 1, resource: light_index_buf.as_entire_binding() },
340 wgpu::BindGroupEntry { binding: 2, resource: global_offset_buf.as_entire_binding() },
341 wgpu::BindGroupEntry { binding: 3, resource: grid_uniform_buf.as_entire_binding() },
342 wgpu::BindGroupEntry { binding: 4, resource: active_lights_buf.as_entire_binding() },
343 ],
344 });
345 let build_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
346 label: Some("cluster_build_shader"),
347 source: wgpu::ShaderSource::Wgsl(
348 include_str!(concat!(env!("OUT_DIR"), "/cluster_build.wgsl")).into(),
349 ),
350 });
351 let build_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
352 label: Some("cluster_build_pipeline_layout"),
353 bind_group_layouts: &[&build_bgl],
354 push_constant_ranges: &[],
355 });
356 let build_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
357 label: Some("cluster_build_pipeline"),
358 layout: Some(&build_layout),
359 module: &build_shader,
360 entry_point: Some("main"),
361 compilation_options: wgpu::PipelineCompilationOptions::default(),
362 cache: None,
363 });
364
365 Self {
366 grid_uniform_buf,
367 cluster_grid_buf,
368 light_index_buf,
369 active_lights_buf,
370 global_offset_buf,
371 stats_staging_buf,
372 clear_bind_group,
373 clear_pipeline,
374 build_bind_group,
375 build_pipeline,
376 clear_params_buf,
377 }
378 }
379
380 pub fn read_stats(
385 &self,
386 device: &wgpu::Device,
387 queue: &wgpu::Queue,
388 active_light_count: u32,
389 fallback_active: bool,
390 ) -> ClusterStats {
391 let bytes =
392 (CLUSTER_COUNT as u64) * std::mem::size_of::<ClusterCell>() as u64;
393 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
394 label: Some("cluster_stats_copy_encoder"),
395 });
396 encoder.copy_buffer_to_buffer(
397 &self.cluster_grid_buf,
398 0,
399 &self.stats_staging_buf,
400 0,
401 bytes,
402 );
403 queue.submit(std::iter::once(encoder.finish()));
404
405 let slice = self.stats_staging_buf.slice(..);
406 slice.map_async(wgpu::MapMode::Read, |_| {});
407 let _ = device.poll(wgpu::PollType::Wait {
408 submission_index: None,
409 timeout: Some(std::time::Duration::from_secs(5)),
410 });
411
412 let stats = {
413 let data = slice.get_mapped_range();
414 let cells: &[ClusterCell] = bytemuck::cast_slice(&data);
415 compute_stats(cells, active_light_count, fallback_active)
416 };
417 self.stats_staging_buf.unmap();
418 stats
419 }
420
421 pub fn write_grid_uniform(&self, queue: &wgpu::Queue, uniform: &ClusterGridUniform) {
423 queue.write_buffer(&self.grid_uniform_buf, 0, bytemuck::cast_slice(&[*uniform]));
424 }
425
426 pub fn write_active_lights(&self, queue: &wgpu::Queue, lights: &[ActiveLightView]) {
429 if lights.is_empty() {
430 return;
431 }
432 let n = lights.len().min(crate::resources::MAX_SCENE_LIGHTS);
433 queue.write_buffer(
434 &self.active_lights_buf,
435 0,
436 bytemuck::cast_slice(&lights[..n]),
437 );
438 }
439
440 pub fn dispatch_frame(&self, encoder: &mut wgpu::CommandEncoder, active_light_count: u32) {
444 {
445 let clear_workgroups = MAX_LIGHT_INDICES.max(CLUSTER_COUNT).div_ceil(64);
446 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
447 label: Some("cluster_clear_pass"),
448 timestamp_writes: None,
449 });
450 pass.set_pipeline(&self.clear_pipeline);
451 pass.set_bind_group(0, &self.clear_bind_group, &[]);
452 pass.dispatch_workgroups(clear_workgroups, 1, 1);
453 }
454 if active_light_count == 0 {
455 return;
456 }
457 {
458 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
459 label: Some("cluster_build_pass"),
460 timestamp_writes: None,
461 });
462 pass.set_pipeline(&self.build_pipeline);
463 pass.set_bind_group(0, &self.build_bind_group, &[]);
464 pass.dispatch_workgroups(CLUSTER_COUNT, 1, 1);
466 }
467 }
468}
469
470fn compute_stats(
475 cells: &[ClusterCell],
476 active_light_count: u32,
477 fallback_active: bool,
478) -> ClusterStats {
479 let total_cells = cells.len() as u32;
480 let mut total_index_slots_used: u32 = 0;
481 let mut punctuals: Vec<u32> = Vec::with_capacity(cells.len());
482 let mut max_punctual: u32 = 0;
483 let mut non_empty: u32 = 0;
484 for c in cells {
485 total_index_slots_used = total_index_slots_used.saturating_add(c.count);
486 if c.punctual_count > 0 {
487 non_empty += 1;
488 punctuals.push(c.punctual_count);
489 if c.punctual_count > max_punctual {
490 max_punctual = c.punctual_count;
491 }
492 }
493 }
494 punctuals.sort_unstable();
495
496 let median = if punctuals.is_empty() {
497 0
498 } else {
499 punctuals[punctuals.len() / 2]
500 };
501 let p99 = if punctuals.is_empty() {
502 0
503 } else {
504 let idx = ((punctuals.len() as f32) * 0.99) as usize;
505 punctuals[idx.min(punctuals.len() - 1)]
506 };
507 let mean = if punctuals.is_empty() {
508 0.0
509 } else {
510 let sum: u64 = punctuals.iter().map(|&v| v as u64).sum();
511 (sum as f32) / (punctuals.len() as f32)
512 };
513
514 ClusterStats {
515 total_cells,
516 non_empty_cells: non_empty,
517 max_punctual,
518 median_punctual: median,
519 p99_punctual: p99,
520 mean_punctual: mean,
521 total_index_slots_used,
522 max_index_slots: MAX_LIGHT_INDICES,
523 active_light_count,
524 fallback_active,
525 }
526}