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 = device.create_shader_module(wgpu::ShaderModuleDescriptor {
308 label: Some("cluster_clear_shader"),
309 source: wgpu::ShaderSource::Wgsl(
310 include_str!(concat!(env!("OUT_DIR"), "/cluster_clear.wgsl")).into(),
311 ),
312 });
313 let clear_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
314 label: Some("cluster_clear_pipeline_layout"),
315 bind_group_layouts: &[&clear_bgl],
316 push_constant_ranges: &[],
317 });
318 let clear_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
319 label: Some("cluster_clear_pipeline"),
320 layout: Some(&clear_layout),
321 module: &clear_shader,
322 entry_point: Some("main"),
323 compilation_options: wgpu::PipelineCompilationOptions::default(),
324 cache: None,
325 });
326
327 let build_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
330 label: Some("cluster_build_bgl"),
331 entries: &[
332 storage_entry(0, false), storage_entry(1, false), storage_entry(2, false), uniform_entry(3), storage_entry(4, true), ],
338 });
339 let build_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
340 label: Some("cluster_build_bind_group"),
341 layout: &build_bgl,
342 entries: &[
343 wgpu::BindGroupEntry {
344 binding: 0,
345 resource: cluster_grid_buf.as_entire_binding(),
346 },
347 wgpu::BindGroupEntry {
348 binding: 1,
349 resource: light_index_buf.as_entire_binding(),
350 },
351 wgpu::BindGroupEntry {
352 binding: 2,
353 resource: global_offset_buf.as_entire_binding(),
354 },
355 wgpu::BindGroupEntry {
356 binding: 3,
357 resource: grid_uniform_buf.as_entire_binding(),
358 },
359 wgpu::BindGroupEntry {
360 binding: 4,
361 resource: active_lights_buf.as_entire_binding(),
362 },
363 ],
364 });
365 let build_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
366 label: Some("cluster_build_shader"),
367 source: wgpu::ShaderSource::Wgsl(
368 include_str!(concat!(env!("OUT_DIR"), "/cluster_build.wgsl")).into(),
369 ),
370 });
371 let build_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
372 label: Some("cluster_build_pipeline_layout"),
373 bind_group_layouts: &[&build_bgl],
374 push_constant_ranges: &[],
375 });
376 let build_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
377 label: Some("cluster_build_pipeline"),
378 layout: Some(&build_layout),
379 module: &build_shader,
380 entry_point: Some("main"),
381 compilation_options: wgpu::PipelineCompilationOptions::default(),
382 cache: None,
383 });
384
385 Self {
386 grid_uniform_buf,
387 cluster_grid_buf,
388 light_index_buf,
389 active_lights_buf,
390 global_offset_buf,
391 stats_staging_buf,
392 clear_bind_group,
393 clear_pipeline,
394 build_bind_group,
395 build_pipeline,
396 clear_params_buf,
397 }
398 }
399
400 pub fn read_stats(
405 &self,
406 device: &wgpu::Device,
407 queue: &wgpu::Queue,
408 active_light_count: u32,
409 fallback_active: bool,
410 ) -> ClusterStats {
411 let bytes = (CLUSTER_COUNT as u64) * std::mem::size_of::<ClusterCell>() as u64;
412 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
413 label: Some("cluster_stats_copy_encoder"),
414 });
415 encoder.copy_buffer_to_buffer(&self.cluster_grid_buf, 0, &self.stats_staging_buf, 0, bytes);
416 queue.submit(std::iter::once(encoder.finish()));
417
418 let slice = self.stats_staging_buf.slice(..);
419 slice.map_async(wgpu::MapMode::Read, |_| {});
420 let _ = device.poll(wgpu::PollType::Wait {
421 submission_index: None,
422 timeout: Some(std::time::Duration::from_secs(5)),
423 });
424
425 let stats = {
426 let data = slice.get_mapped_range();
427 let cells: &[ClusterCell] = bytemuck::cast_slice(&data);
428 compute_stats(cells, active_light_count, fallback_active)
429 };
430 self.stats_staging_buf.unmap();
431 stats
432 }
433
434 pub fn write_grid_uniform(&self, queue: &wgpu::Queue, uniform: &ClusterGridUniform) {
436 queue.write_buffer(&self.grid_uniform_buf, 0, bytemuck::cast_slice(&[*uniform]));
437 }
438
439 pub fn write_active_lights(&self, queue: &wgpu::Queue, lights: &[ActiveLightView]) {
442 if lights.is_empty() {
443 return;
444 }
445 let n = lights.len().min(crate::resources::MAX_SCENE_LIGHTS);
446 queue.write_buffer(
447 &self.active_lights_buf,
448 0,
449 bytemuck::cast_slice(&lights[..n]),
450 );
451 }
452
453 pub fn dispatch_frame(&self, encoder: &mut wgpu::CommandEncoder, active_light_count: u32) {
457 {
458 let clear_workgroups = MAX_LIGHT_INDICES.max(CLUSTER_COUNT).div_ceil(64);
459 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
460 label: Some("cluster_clear_pass"),
461 timestamp_writes: None,
462 });
463 pass.set_pipeline(&self.clear_pipeline);
464 pass.set_bind_group(0, &self.clear_bind_group, &[]);
465 pass.dispatch_workgroups(clear_workgroups, 1, 1);
466 }
467 if active_light_count == 0 {
468 return;
469 }
470 {
471 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
472 label: Some("cluster_build_pass"),
473 timestamp_writes: None,
474 });
475 pass.set_pipeline(&self.build_pipeline);
476 pass.set_bind_group(0, &self.build_bind_group, &[]);
477 pass.dispatch_workgroups(CLUSTER_COUNT, 1, 1);
479 }
480 }
481}
482
483fn compute_stats(
488 cells: &[ClusterCell],
489 active_light_count: u32,
490 fallback_active: bool,
491) -> ClusterStats {
492 let total_cells = cells.len() as u32;
493 let mut total_index_slots_used: u32 = 0;
494 let mut punctuals: Vec<u32> = Vec::with_capacity(cells.len());
495 let mut max_punctual: u32 = 0;
496 let mut non_empty: u32 = 0;
497 for c in cells {
498 total_index_slots_used = total_index_slots_used.saturating_add(c.count);
499 if c.punctual_count > 0 {
500 non_empty += 1;
501 punctuals.push(c.punctual_count);
502 if c.punctual_count > max_punctual {
503 max_punctual = c.punctual_count;
504 }
505 }
506 }
507 punctuals.sort_unstable();
508
509 let median = if punctuals.is_empty() {
510 0
511 } else {
512 punctuals[punctuals.len() / 2]
513 };
514 let p99 = if punctuals.is_empty() {
515 0
516 } else {
517 let idx = ((punctuals.len() as f32) * 0.99) as usize;
518 punctuals[idx.min(punctuals.len() - 1)]
519 };
520 let mean = if punctuals.is_empty() {
521 0.0
522 } else {
523 let sum: u64 = punctuals.iter().map(|&v| v as u64).sum();
524 (sum as f32) / (punctuals.len() as f32)
525 };
526
527 ClusterStats {
528 total_cells,
529 non_empty_cells: non_empty,
530 max_punctual,
531 median_punctual: median,
532 p99_punctual: p99,
533 mean_punctual: mean,
534 total_index_slots_used,
535 max_index_slots: MAX_LIGHT_INDICES,
536 active_light_count,
537 fallback_active,
538 }
539}