use wgpu::util::DeviceExt;
pub const CLUSTER_X_TILES: u32 = 16;
pub const CLUSTER_Y_TILES: u32 = 9;
pub const CLUSTER_Z_SLICES: u32 = 24;
pub const CLUSTER_COUNT: u32 = CLUSTER_X_TILES * CLUSTER_Y_TILES * CLUSTER_Z_SLICES;
pub const MAX_LIGHT_INDICES: u32 = 32 * 1024;
pub const SMALL_N_THRESHOLD: u32 = 16;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ClusterGridUniform {
pub dimensions: [u32; 4],
pub depth: [f32; 4],
pub screen: [f32; 4],
pub proj_scale: [f32; 4],
pub view: [[f32; 4]; 4],
}
impl Default for ClusterGridUniform {
fn default() -> Self {
Self {
dimensions: [
CLUSTER_X_TILES,
CLUSTER_Y_TILES,
CLUSTER_Z_SLICES,
CLUSTER_COUNT,
],
depth: [0.1, 1000.0, (1000.0_f32 / 0.1_f32).ln(), 0.0],
screen: [1.0, 1.0, 1.0, 0.0],
proj_scale: [1.0, 1.0, 0.0, 0.0],
view: glam::Mat4::IDENTITY.to_cols_array_2d(),
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ActiveLightView {
pub view_pos_range: [f32; 4],
pub type_pad: [u32; 4],
pub spot_data: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct ClearParams {
cluster_count: u32,
index_count: u32,
_pad0: u32,
_pad1: u32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ClusterStats {
pub total_cells: u32,
pub non_empty_cells: u32,
pub max_punctual: u32,
pub median_punctual: u32,
pub p99_punctual: u32,
pub mean_punctual: f32,
pub total_index_slots_used: u32,
pub max_index_slots: u32,
pub active_light_count: u32,
pub fallback_active: bool,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ClusterCell {
pub offset: u32,
pub count: u32,
pub punctual_count: u32,
pub _pad: u32,
}
pub struct ClusteredResources {
pub grid_uniform_buf: wgpu::Buffer,
pub cluster_grid_buf: wgpu::Buffer,
pub light_index_buf: wgpu::Buffer,
pub active_lights_buf: wgpu::Buffer,
pub global_offset_buf: wgpu::Buffer,
stats_staging_buf: wgpu::Buffer,
clear_bind_group: wgpu::BindGroup,
clear_pipeline: wgpu::ComputePipeline,
build_bind_group: wgpu::BindGroup,
build_pipeline: wgpu::ComputePipeline,
#[allow(dead_code)]
clear_params_buf: wgpu::Buffer,
}
impl ClusteredResources {
pub fn new(device: &wgpu::Device) -> Self {
let grid_uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cluster_grid_uniform_buf"),
contents: bytemuck::cast_slice(&[ClusterGridUniform::default()]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let cluster_grid_bytes = (CLUSTER_COUNT as u64) * std::mem::size_of::<ClusterCell>() as u64;
let cluster_grid_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cluster_grid_buf"),
size: cluster_grid_bytes,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let light_index_bytes = (MAX_LIGHT_INDICES as u64) * 4;
let light_index_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cluster_light_index_buf"),
size: light_index_bytes,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let active_lights_bytes = (crate::resources::MAX_SCENE_LIGHTS as u64)
* std::mem::size_of::<ActiveLightView>() as u64;
let active_lights_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cluster_active_lights_buf"),
size: active_lights_bytes,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let global_offset_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cluster_global_offset_buf"),
size: 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let stats_staging_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cluster_stats_staging_buf"),
size: cluster_grid_bytes,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let clear_params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cluster_clear_params_buf"),
contents: bytemuck::cast_slice(&[ClearParams {
cluster_count: CLUSTER_COUNT,
index_count: MAX_LIGHT_INDICES,
_pad0: 0,
_pad1: 0,
}]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let storage_entry = |binding: u32, read_only: bool| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
};
let uniform_entry = |binding: u32| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
};
let clear_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("cluster_clear_bgl"),
entries: &[
storage_entry(0, false), storage_entry(1, false), storage_entry(2, false), uniform_entry(3), ],
});
let clear_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cluster_clear_bind_group"),
layout: &clear_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: cluster_grid_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: light_index_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: global_offset_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: clear_params_buf.as_entire_binding(),
},
],
});
let clear_shader = crate::resources::builders::wgsl_module(
device,
"cluster_clear_shader",
crate::resources::builders::wgsl_source!("cluster_clear"),
);
let clear_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("cluster_clear_pipeline_layout"),
bind_group_layouts: &[&clear_bgl],
push_constant_ranges: &[],
});
let clear_pipeline = crate::resources::builders::compute_pipeline(
device,
"cluster_clear_pipeline",
&clear_layout,
&clear_shader,
"main",
);
let build_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("cluster_build_bgl"),
entries: &[
storage_entry(0, false), storage_entry(1, false), storage_entry(2, false), uniform_entry(3), storage_entry(4, true), ],
});
let build_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cluster_build_bind_group"),
layout: &build_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: cluster_grid_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: light_index_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: global_offset_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: grid_uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: active_lights_buf.as_entire_binding(),
},
],
});
let build_shader = crate::resources::builders::wgsl_module(
device,
"cluster_build_shader",
crate::resources::builders::wgsl_source!("cluster_build"),
);
let build_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("cluster_build_pipeline_layout"),
bind_group_layouts: &[&build_bgl],
push_constant_ranges: &[],
});
let build_pipeline = crate::resources::builders::compute_pipeline(
device,
"cluster_build_pipeline",
&build_layout,
&build_shader,
"main",
);
Self {
grid_uniform_buf,
cluster_grid_buf,
light_index_buf,
active_lights_buf,
global_offset_buf,
stats_staging_buf,
clear_bind_group,
clear_pipeline,
build_bind_group,
build_pipeline,
clear_params_buf,
}
}
pub fn read_stats(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
active_light_count: u32,
fallback_active: bool,
) -> ClusterStats {
let bytes = (CLUSTER_COUNT as u64) * std::mem::size_of::<ClusterCell>() as u64;
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("cluster_stats_copy_encoder"),
});
encoder.copy_buffer_to_buffer(&self.cluster_grid_buf, 0, &self.stats_staging_buf, 0, bytes);
queue.submit(std::iter::once(encoder.finish()));
let slice = self.stats_staging_buf.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| {});
let _ = device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: Some(std::time::Duration::from_secs(5)),
});
let stats = {
let data = slice.get_mapped_range();
let cells: &[ClusterCell] = bytemuck::cast_slice(&data);
compute_stats(cells, active_light_count, fallback_active)
};
self.stats_staging_buf.unmap();
stats
}
pub fn write_grid_uniform(&self, queue: &wgpu::Queue, uniform: &ClusterGridUniform) {
queue.write_buffer(&self.grid_uniform_buf, 0, bytemuck::cast_slice(&[*uniform]));
}
pub fn write_active_lights(&self, queue: &wgpu::Queue, lights: &[ActiveLightView]) {
if lights.is_empty() {
return;
}
let n = lights.len().min(crate::resources::MAX_SCENE_LIGHTS);
queue.write_buffer(
&self.active_lights_buf,
0,
bytemuck::cast_slice(&lights[..n]),
);
}
pub fn dispatch_frame(&self, encoder: &mut wgpu::CommandEncoder, active_light_count: u32) {
{
let clear_workgroups = MAX_LIGHT_INDICES.max(CLUSTER_COUNT).div_ceil(64);
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("cluster_clear_pass"),
timestamp_writes: None,
});
pass.set_pipeline(&self.clear_pipeline);
pass.set_bind_group(0, &self.clear_bind_group, &[]);
pass.dispatch_workgroups(clear_workgroups, 1, 1);
}
if active_light_count == 0 {
return;
}
{
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("cluster_build_pass"),
timestamp_writes: None,
});
pass.set_pipeline(&self.build_pipeline);
pass.set_bind_group(0, &self.build_bind_group, &[]);
pass.dispatch_workgroups(CLUSTER_COUNT, 1, 1);
}
}
}
fn compute_stats(
cells: &[ClusterCell],
active_light_count: u32,
fallback_active: bool,
) -> ClusterStats {
let total_cells = cells.len() as u32;
let mut total_index_slots_used: u32 = 0;
let mut punctuals: Vec<u32> = Vec::with_capacity(cells.len());
let mut max_punctual: u32 = 0;
let mut non_empty: u32 = 0;
for c in cells {
total_index_slots_used = total_index_slots_used.saturating_add(c.count);
if c.punctual_count > 0 {
non_empty += 1;
punctuals.push(c.punctual_count);
if c.punctual_count > max_punctual {
max_punctual = c.punctual_count;
}
}
}
punctuals.sort_unstable();
let median = if punctuals.is_empty() {
0
} else {
punctuals[punctuals.len() / 2]
};
let p99 = if punctuals.is_empty() {
0
} else {
let idx = ((punctuals.len() as f32) * 0.99) as usize;
punctuals[idx.min(punctuals.len() - 1)]
};
let mean = if punctuals.is_empty() {
0.0
} else {
let sum: u64 = punctuals.iter().map(|&v| v as u64).sum();
(sum as f32) / (punctuals.len() as f32)
};
ClusterStats {
total_cells,
non_empty_cells: non_empty,
max_punctual,
median_punctual: median,
p99_punctual: p99,
mean_punctual: mean,
total_index_slots_used,
max_index_slots: MAX_LIGHT_INDICES,
active_light_count,
fallback_active,
}
}