use crate::gpu::primitives::dispatch::{WORKGROUP, linear_dispatch};
use crate::gpu::primitives::pipeline::{compute_pipeline, storage_buffer, uniform_buffer};
use crate::shader_bindings::primitives::scan::ScanParams;
#[derive(Debug)]
struct Level {
groups: (u32, u32),
scan_bind: wgpu::BindGroup,
add_bind: Option<wgpu::BindGroup>,
}
#[derive(Debug)]
pub struct PrefixScan {
levels: Vec<Level>,
scan_pipeline: wgpu::ComputePipeline,
add_pipeline: wgpu::ComputePipeline,
}
fn level_sizes(n: u32) -> Vec<u32> {
let mut sizes = Vec::new();
let mut level = n.max(1);
loop {
sizes.push(level);
let blocks = level.div_ceil(WORKGROUP);
if blocks <= 1 {
return sizes;
}
level = blocks;
}
}
fn build_pipelines(
device: &wgpu::Device,
label: &str,
) -> (
wgpu::BindGroupLayout,
wgpu::BindGroupLayout,
wgpu::ComputePipeline,
wgpu::ComputePipeline,
) {
let scan_layout =
device.create_bind_group_layout(&crate::shader_bindings::primitives::scan::WgpuBindGroup0::LAYOUT_DESCRIPTOR);
let add_layout = device
.create_bind_group_layout(&crate::shader_bindings::primitives::scan_add::WgpuBindGroup0::LAYOUT_DESCRIPTOR);
let scan_pipeline = compute_pipeline(
device,
&format!("{label}_scan"),
crate::shader_bindings::primitives::scan::SHADER_STRING,
&scan_layout,
);
let add_pipeline = compute_pipeline(
device,
&format!("{label}_scan_add"),
crate::shader_bindings::primitives::scan_add::SHADER_STRING,
&add_layout,
);
(scan_layout, add_layout, scan_pipeline, add_pipeline)
}
impl PrefixScan {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
label: &str,
input: &wgpu::Buffer,
output: &wgpu::Buffer,
n: u32,
) -> Self {
let (scan_layout, add_layout, scan_pipeline, add_pipeline) = build_pipelines(device, label);
let sizes = level_sizes(n);
let upper_outputs: Vec<wgpu::Buffer> = sizes[1..]
.iter()
.enumerate()
.map(|(k, &m)| storage_buffer(device, &format!("{label}_scan_out{}", k + 1), m as usize))
.collect();
let sums: Vec<wgpu::Buffer> = sizes
.iter()
.enumerate()
.map(|(i, &m)| storage_buffer(device, &format!("{label}_scan_sums{i}"), m.div_ceil(WORKGROUP) as usize))
.collect();
let output_at = |i: usize| if i == 0 { output } else { &upper_outputs[i - 1] };
let input_at = |i: usize| if i == 0 { input } else { &sums[i - 1] };
let levels = sizes
.iter()
.enumerate()
.map(|(i, &m)| {
let groups = linear_dispatch(m);
let params = uniform_buffer(
device,
queue,
&format!("{label}_scan_params{i}"),
bytemuck::bytes_of(&ScanParams {
n: m,
groups_x: groups.0,
num_blocks: m.div_ceil(WORKGROUP),
_pad: 0,
}),
);
let scan_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("{label}_scan_bind{i}")),
layout: &scan_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: input_at(i).as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: output_at(i).as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: sums[i].as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: params.as_entire_binding(),
},
],
});
let add_bind = (i + 1 < sizes.len()).then(|| {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("{label}_scan_add_bind{i}")),
layout: &add_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: output_at(i + 1).as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: output_at(i).as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: params.as_entire_binding(),
},
],
})
});
Level {
groups,
scan_bind,
add_bind,
}
})
.collect();
Self {
levels,
scan_pipeline,
add_pipeline,
}
}
pub fn encode(&self, encoder: &mut wgpu::CommandEncoder) {
for level in &self.levels {
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("henad_scan_pass"),
timestamp_writes: None,
});
pass.set_pipeline(&self.scan_pipeline);
pass.set_bind_group(0, &level.scan_bind, &[]);
pass.dispatch_workgroups(level.groups.0, level.groups.1, 1);
}
for level in self.levels.iter().rev() {
let Some(add_bind) = &level.add_bind else {
continue;
};
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("henad_scan_add_pass"),
timestamp_writes: None,
});
pass.set_pipeline(&self.add_pipeline);
pass.set_bind_group(0, add_bind, &[]);
pass.dispatch_workgroups(level.groups.0, level.groups.1, 1);
}
}
}
#[cfg(test)]
mod tests {
use super::{WORKGROUP, level_sizes};
#[test]
fn levels_bottom_out_at_a_single_workgroup() {
assert_eq!(level_sizes(0), vec![1]);
assert_eq!(level_sizes(1), vec![1]);
assert_eq!(level_sizes(WORKGROUP), vec![WORKGROUP]);
assert_eq!(level_sizes(WORKGROUP + 1), vec![WORKGROUP + 1, 2]);
assert_eq!(level_sizes(1_000), vec![1_000, 4]);
for n in [100_000u32, 10_000_000, 100_000_000] {
let sizes = level_sizes(n);
assert_eq!(sizes[0], n);
assert_eq!(
*sizes.last().expect("at least one level"),
sizes.last().copied().unwrap_or(1).min(WORKGROUP),
"the top level must fit one workgroup"
);
assert!(sizes.len() <= 4, "{n} elements should not need {} levels", sizes.len());
}
}
}