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: [
63 CLUSTER_X_TILES,
64 CLUSTER_Y_TILES,
65 CLUSTER_Z_SLICES,
66 CLUSTER_COUNT,
67 ],
68 depth: [0.1, 1000.0, (1000.0_f32 / 0.1_f32).ln(), 0.0],
69 screen: [1.0, 1.0, 1.0, 0.0],
70 proj_scale: [1.0, 1.0, 0.0, 0.0],
71 view: glam::Mat4::IDENTITY.to_cols_array_2d(),
72 }
73 }
74}
75
76#[repr(C)]
85#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
86pub struct ActiveLightView {
87 pub view_pos_range: [f32; 4],
89 pub type_pad: [u32; 4],
91 pub spot_data: [f32; 4],
93}
94
95#[repr(C)]
97#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
98struct ClearParams {
99 cluster_count: u32,
100 index_count: u32,
101 _pad0: u32,
102 _pad1: u32,
103}
104
105#[derive(Debug, Clone, Copy, Default)]
114pub struct ClusterStats {
115 pub total_cells: u32,
117 pub non_empty_cells: u32,
119 pub max_punctual: u32,
121 pub median_punctual: u32,
123 pub p99_punctual: u32,
126 pub mean_punctual: f32,
128 pub total_index_slots_used: u32,
131 pub max_index_slots: u32,
133 pub active_light_count: u32,
135 pub fallback_active: bool,
139}
140
141#[repr(C)]
144#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
145pub struct ClusterCell {
146 pub offset: u32,
149 pub count: u32,
153 pub punctual_count: u32,
157 pub _pad: u32,
159}
160
161pub struct ClusteredResources {
163 pub grid_uniform_buf: wgpu::Buffer,
165 pub cluster_grid_buf: wgpu::Buffer,
167 pub light_index_buf: wgpu::Buffer,
169 pub active_lights_buf: wgpu::Buffer,
172 pub global_offset_buf: wgpu::Buffer,
175 stats_staging_buf: wgpu::Buffer,
178 clear_bind_group: wgpu::BindGroup,
180 clear_pipeline: wgpu::ComputePipeline,
182 build_bind_group: wgpu::BindGroup,
184 build_pipeline: wgpu::ComputePipeline,
186 #[allow(dead_code)]
188 clear_params_buf: wgpu::Buffer,
189}
190
191impl ClusteredResources {
192 pub fn new(device: &wgpu::Device) -> Self {
195 let grid_uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
196 label: Some("cluster_grid_uniform_buf"),
197 contents: bytemuck::cast_slice(&[ClusterGridUniform::default()]),
198 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
199 });
200
201 let cluster_grid_bytes = (CLUSTER_COUNT as u64) * std::mem::size_of::<ClusterCell>() as u64;
202 let cluster_grid_buf = device.create_buffer(&wgpu::BufferDescriptor {
203 label: Some("cluster_grid_buf"),
204 size: cluster_grid_bytes,
205 usage: wgpu::BufferUsages::STORAGE
206 | wgpu::BufferUsages::COPY_DST
207 | wgpu::BufferUsages::COPY_SRC,
208 mapped_at_creation: false,
209 });
210
211 let light_index_bytes = (MAX_LIGHT_INDICES as u64) * 4;
212 let light_index_buf = device.create_buffer(&wgpu::BufferDescriptor {
213 label: Some("cluster_light_index_buf"),
214 size: light_index_bytes,
215 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
216 mapped_at_creation: false,
217 });
218
219 let active_lights_bytes = (crate::resources::MAX_SCENE_LIGHTS as u64)
220 * std::mem::size_of::<ActiveLightView>() as u64;
221 let active_lights_buf = device.create_buffer(&wgpu::BufferDescriptor {
222 label: Some("cluster_active_lights_buf"),
223 size: active_lights_bytes,
224 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
225 mapped_at_creation: false,
226 });
227
228 let global_offset_buf = device.create_buffer(&wgpu::BufferDescriptor {
229 label: Some("cluster_global_offset_buf"),
230 size: 4,
231 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
232 mapped_at_creation: false,
233 });
234
235 let stats_staging_buf = device.create_buffer(&wgpu::BufferDescriptor {
236 label: Some("cluster_stats_staging_buf"),
237 size: cluster_grid_bytes,
238 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
239 mapped_at_creation: false,
240 });
241
242 let clear_params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
243 label: Some("cluster_clear_params_buf"),
244 contents: bytemuck::cast_slice(&[ClearParams {
245 cluster_count: CLUSTER_COUNT,
246 index_count: MAX_LIGHT_INDICES,
247 _pad0: 0,
248 _pad1: 0,
249 }]),
250 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
251 });
252
253 let storage_entry = |binding: u32, read_only: bool| wgpu::BindGroupLayoutEntry {
254 binding,
255 visibility: wgpu::ShaderStages::COMPUTE,
256 ty: wgpu::BindingType::Buffer {
257 ty: wgpu::BufferBindingType::Storage { read_only },
258 has_dynamic_offset: false,
259 min_binding_size: None,
260 },
261 count: None,
262 };
263 let uniform_entry = |binding: u32| wgpu::BindGroupLayoutEntry {
264 binding,
265 visibility: wgpu::ShaderStages::COMPUTE,
266 ty: wgpu::BindingType::Buffer {
267 ty: wgpu::BufferBindingType::Uniform,
268 has_dynamic_offset: false,
269 min_binding_size: None,
270 },
271 count: None,
272 };
273
274 let clear_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
275 label: Some("cluster_clear_bgl"),
276 entries: &[
277 storage_entry(0, false), storage_entry(1, false), storage_entry(2, false), uniform_entry(3), ],
282 });
283
284 let clear_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
285 label: Some("cluster_clear_bind_group"),
286 layout: &clear_bgl,
287 entries: &[
288 wgpu::BindGroupEntry {
289 binding: 0,
290 resource: cluster_grid_buf.as_entire_binding(),
291 },
292 wgpu::BindGroupEntry {
293 binding: 1,
294 resource: light_index_buf.as_entire_binding(),
295 },
296 wgpu::BindGroupEntry {
297 binding: 2,
298 resource: global_offset_buf.as_entire_binding(),
299 },
300 wgpu::BindGroupEntry {
301 binding: 3,
302 resource: clear_params_buf.as_entire_binding(),
303 },
304 ],
305 });
306
307 let clear_shader = crate::resources::builders::wgsl_module(
308 device,
309 "cluster_clear_shader",
310 crate::resources::builders::wgsl_source!("cluster_clear"),
311 );
312 let clear_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
313 label: Some("cluster_clear_pipeline_layout"),
314 bind_group_layouts: &[&clear_bgl],
315 push_constant_ranges: &[],
316 });
317 let clear_pipeline = crate::resources::builders::compute_pipeline(
318 device,
319 "cluster_clear_pipeline",
320 &clear_layout,
321 &clear_shader,
322 "main",
323 );
324
325 let build_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
328 label: Some("cluster_build_bgl"),
329 entries: &[
330 storage_entry(0, false), storage_entry(1, false), storage_entry(2, false), uniform_entry(3), storage_entry(4, true), ],
336 });
337 let build_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
338 label: Some("cluster_build_bind_group"),
339 layout: &build_bgl,
340 entries: &[
341 wgpu::BindGroupEntry {
342 binding: 0,
343 resource: cluster_grid_buf.as_entire_binding(),
344 },
345 wgpu::BindGroupEntry {
346 binding: 1,
347 resource: light_index_buf.as_entire_binding(),
348 },
349 wgpu::BindGroupEntry {
350 binding: 2,
351 resource: global_offset_buf.as_entire_binding(),
352 },
353 wgpu::BindGroupEntry {
354 binding: 3,
355 resource: grid_uniform_buf.as_entire_binding(),
356 },
357 wgpu::BindGroupEntry {
358 binding: 4,
359 resource: active_lights_buf.as_entire_binding(),
360 },
361 ],
362 });
363 let build_shader = crate::resources::builders::wgsl_module(
364 device,
365 "cluster_build_shader",
366 crate::resources::builders::wgsl_source!("cluster_build"),
367 );
368 let build_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
369 label: Some("cluster_build_pipeline_layout"),
370 bind_group_layouts: &[&build_bgl],
371 push_constant_ranges: &[],
372 });
373 let build_pipeline = crate::resources::builders::compute_pipeline(
374 device,
375 "cluster_build_pipeline",
376 &build_layout,
377 &build_shader,
378 "main",
379 );
380
381 Self {
382 grid_uniform_buf,
383 cluster_grid_buf,
384 light_index_buf,
385 active_lights_buf,
386 global_offset_buf,
387 stats_staging_buf,
388 clear_bind_group,
389 clear_pipeline,
390 build_bind_group,
391 build_pipeline,
392 clear_params_buf,
393 }
394 }
395
396 pub fn read_stats(
401 &self,
402 device: &wgpu::Device,
403 queue: &wgpu::Queue,
404 active_light_count: u32,
405 fallback_active: bool,
406 ) -> ClusterStats {
407 let bytes = (CLUSTER_COUNT as u64) * std::mem::size_of::<ClusterCell>() as u64;
408 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
409 label: Some("cluster_stats_copy_encoder"),
410 });
411 encoder.copy_buffer_to_buffer(&self.cluster_grid_buf, 0, &self.stats_staging_buf, 0, bytes);
412 queue.submit(std::iter::once(encoder.finish()));
413
414 let slice = self.stats_staging_buf.slice(..);
415 slice.map_async(wgpu::MapMode::Read, |_| {});
416 let _ = device.poll(wgpu::PollType::Wait {
417 submission_index: None,
418 timeout: Some(std::time::Duration::from_secs(5)),
419 });
420
421 let stats = {
422 let data = slice.get_mapped_range();
423 let cells: &[ClusterCell] = bytemuck::cast_slice(&data);
424 compute_stats(cells, active_light_count, fallback_active)
425 };
426 self.stats_staging_buf.unmap();
427 stats
428 }
429
430 pub fn write_grid_uniform(&self, queue: &wgpu::Queue, uniform: &ClusterGridUniform) {
432 queue.write_buffer(&self.grid_uniform_buf, 0, bytemuck::cast_slice(&[*uniform]));
433 }
434
435 pub fn write_active_lights(&self, queue: &wgpu::Queue, lights: &[ActiveLightView]) {
438 if lights.is_empty() {
439 return;
440 }
441 let n = lights.len().min(crate::resources::MAX_SCENE_LIGHTS);
442 queue.write_buffer(
443 &self.active_lights_buf,
444 0,
445 bytemuck::cast_slice(&lights[..n]),
446 );
447 }
448
449 pub fn dispatch_frame(&self, encoder: &mut wgpu::CommandEncoder, active_light_count: u32) {
453 {
454 let clear_workgroups = MAX_LIGHT_INDICES.max(CLUSTER_COUNT).div_ceil(64);
455 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
456 label: Some("cluster_clear_pass"),
457 timestamp_writes: None,
458 });
459 pass.set_pipeline(&self.clear_pipeline);
460 pass.set_bind_group(0, &self.clear_bind_group, &[]);
461 pass.dispatch_workgroups(clear_workgroups, 1, 1);
462 }
463 if active_light_count == 0 {
464 return;
465 }
466 {
467 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
468 label: Some("cluster_build_pass"),
469 timestamp_writes: None,
470 });
471 pass.set_pipeline(&self.build_pipeline);
472 pass.set_bind_group(0, &self.build_bind_group, &[]);
473 pass.dispatch_workgroups(CLUSTER_COUNT, 1, 1);
475 }
476 }
477}
478
479fn compute_stats(
484 cells: &[ClusterCell],
485 active_light_count: u32,
486 fallback_active: bool,
487) -> ClusterStats {
488 let total_cells = cells.len() as u32;
489 let mut total_index_slots_used: u32 = 0;
490 let mut punctuals: Vec<u32> = Vec::with_capacity(cells.len());
491 let mut max_punctual: u32 = 0;
492 let mut non_empty: u32 = 0;
493 for c in cells {
494 total_index_slots_used = total_index_slots_used.saturating_add(c.count);
495 if c.punctual_count > 0 {
496 non_empty += 1;
497 punctuals.push(c.punctual_count);
498 if c.punctual_count > max_punctual {
499 max_punctual = c.punctual_count;
500 }
501 }
502 }
503 punctuals.sort_unstable();
504
505 let median = if punctuals.is_empty() {
506 0
507 } else {
508 punctuals[punctuals.len() / 2]
509 };
510 let p99 = if punctuals.is_empty() {
511 0
512 } else {
513 let idx = ((punctuals.len() as f32) * 0.99) as usize;
514 punctuals[idx.min(punctuals.len() - 1)]
515 };
516 let mean = if punctuals.is_empty() {
517 0.0
518 } else {
519 let sum: u64 = punctuals.iter().map(|&v| v as u64).sum();
520 (sum as f32) / (punctuals.len() as f32)
521 };
522
523 ClusterStats {
524 total_cells,
525 non_empty_cells: non_empty,
526 max_punctual,
527 median_punctual: median,
528 p99_punctual: p99,
529 mean_punctual: mean,
530 total_index_slots_used,
531 max_index_slots: MAX_LIGHT_INDICES,
532 active_light_count,
533 fallback_active,
534 }
535}