1use super::*;
3
4pub(super) const SHADER: &str = include_str!("../shaders/icp_trim.wgsl");
5
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
8pub enum GpuIcpTrimPrecision {
9 CompensatedF32,
12 #[default]
16 DoubleSingle,
17}
18
19#[repr(C)]
20#[derive(Clone, Copy, Pod, Zeroable)]
21pub(super) struct TrimParams {
22 count: u32,
23 groups: u32,
24 step: u32,
25 mantissa_lo: u32,
26 mantissa_hi: u32,
27 shift: u32,
28 bound: f32,
29 targets: u32,
30}
31
32pub(super) fn plan_trim(count: u64, limits: &wgpu::Limits) -> SpatialResult<u32> {
33 let groups = plan(count, limits)?;
34 let bound = limits.max_buffer_size.min(u64::from(limits.max_storage_buffer_binding_size));
35 if u64::from(groups) * 1024 > bound
36 || limits.max_storage_buffers_per_shader_stage < 7
37 || limits.max_bindings_per_bind_group < 8
38 || limits.max_compute_workgroup_storage_size < 2048
39 {
40 return Err(invalid());
41 }
42 Ok(groups)
43}
44
45impl TrimParams {
46 fn new(count: u32, groups: u32, fraction: f64, bound: f32, targets: u32) -> Self {
47 let bits = fraction.to_bits();
48 let exponent = ((bits >> 52) & 2047) as u32;
49 let mantissa = (bits & ((1u64 << 52) - 1)) | if exponent == 0 { 0 } else { 1u64 << 52 };
50 Self {
51 count,
52 groups,
53 step: 0,
54 mantissa_lo: mantissa as u32,
55 mantissa_hi: (mantissa >> 32) as u32,
56 shift: if exponent == 0 { 1074 } else { 1075 - exponent },
57 bound,
58 targets,
59 }
60 }
61}
62
63pub(crate) struct TrimPipelines {
64 reduction: std::sync::OnceLock<IcpReductionPipelines>,
65 precise: std::sync::OnceLock<IcpReductionPipelines>,
66 layout: wgpu::BindGroupLayout,
67 histogram: wgpu::ComputePipeline,
68 select: wgpu::ComputePipeline,
69 wide_select: std::sync::OnceLock<wgpu::ComputePipeline>,
70}
71
72fn use_wide_selector(groups: u32, limits: &wgpu::Limits) -> bool {
73 groups >= 64
74 && limits.max_compute_invocations_per_workgroup >= 256
75 && limits.max_compute_workgroup_size_x >= 256
76}
77
78impl TrimPipelines {
79 pub(super) fn new(device: &wgpu::Device) -> Self {
80 let entries: Vec<_> = (0..4)
81 .map(|binding| wgpu::BindGroupLayoutEntry {
82 binding,
83 visibility: wgpu::ShaderStages::COMPUTE,
84 ty: wgpu::BindingType::Buffer {
85 ty: if binding == 0 {
86 wgpu::BufferBindingType::Uniform
87 } else {
88 wgpu::BufferBindingType::Storage { read_only: binding == 1 }
89 },
90 has_dynamic_offset: false,
91 min_binding_size: None,
92 },
93 count: None,
94 })
95 .collect();
96 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
97 label: Some("icp-trim-layout"),
98 entries: &entries,
99 });
100 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
101 label: Some("icp-trim-layout"),
102 bind_group_layouts: &[&layout],
103 push_constant_ranges: &[],
104 });
105 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
106 label: Some("icp-trim-shader"),
107 source: wgpu::ShaderSource::Wgsl(SHADER.into()),
108 });
109 let [histogram, select] = ["histogram_prefix", "select_digit"].map(|entry| {
110 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
111 label: Some("icp-trim-pipeline"),
112 layout: Some(&pipeline_layout),
113 module: &shader,
114 entry_point: Some(entry),
115 compilation_options: Default::default(),
116 cache: None,
117 })
118 });
119 Self {
120 reduction: std::sync::OnceLock::new(),
121 precise: std::sync::OnceLock::new(),
122 layout,
123 histogram,
124 select,
125 wide_select: std::sync::OnceLock::new(),
126 }
127 }
128
129 fn selector(&self, device: &wgpu::Device, groups: u32) -> &wgpu::ComputePipeline {
130 if !use_wide_selector(groups, &device.limits()) {
131 return &self.select;
132 }
133 self.wide_select.get_or_init(|| {
134 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
135 label: Some("icp-trim-wide-selector"),
136 source: wgpu::ShaderSource::Wgsl(SHADER.into()),
137 });
138 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
139 label: Some("icp-trim-wide-selector-layout"),
140 bind_group_layouts: &[&self.layout],
141 push_constant_ranges: &[],
142 });
143 let constants = std::collections::HashMap::from([
144 ("select_lanes".to_owned(), 256.0),
145 ("select_tree".to_owned(), 1.0),
146 ]);
147 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
148 label: Some("icp-trim-wide-selector"),
149 layout: Some(&layout),
150 module: &shader,
151 entry_point: Some("select_digit"),
152 compilation_options: wgpu::PipelineCompilationOptions {
153 constants: &constants,
154 ..Default::default()
155 },
156 cache: None,
157 })
158 })
159 }
160
161 pub(super) fn reduction(
162 &self,
163 device: &wgpu::Device,
164 precision: GpuIcpTrimPrecision,
165 ) -> &IcpReductionPipelines {
166 match precision {
167 GpuIcpTrimPrecision::CompensatedF32 => self.reduction.get_or_init(|| {
168 IcpReductionPipelines::with_shader(device, &trimmed_shader(), true)
169 }),
170 GpuIcpTrimPrecision::DoubleSingle => self.precise.get_or_init(|| {
171 IcpReductionPipelines::with_shader(device, &precise_shader(), true)
172 }),
173 }
174 }
175
176 pub(super) fn encode(
177 &self,
178 runtime: &WgpuRuntime,
179 encoder: &mut wgpu::CommandEncoder,
180 scratch: &mut ScratchBuffers<'_>,
181 params: TrimParams,
182 matches: &wgpu::Buffer,
183 selection: &wgpu::Buffer,
184 ) {
185 let histograms = scratch.acquire("icp-trim-histograms", u64::from(params.groups) * 1024);
186 let select = self.selector(runtime.device(), params.groups);
187 encoder.clear_buffer(selection, 0, None);
188 let first_index_step = 4 + params.count.saturating_sub(1).leading_zeros() / 8;
191 for step in (0..4).chain(first_index_step..8) {
192 let uniform = runtime.device().create_buffer_init(&wgpu::util::BufferInitDescriptor {
193 label: Some("icp-trim-params"),
194 contents: bytemuck::bytes_of(&TrimParams { step, ..params }),
195 usage: wgpu::BufferUsages::UNIFORM,
196 });
197 let entries: Vec<_> = [&uniform, matches, &histograms, selection]
198 .iter()
199 .enumerate()
200 .map(|(binding, buffer)| wgpu::BindGroupEntry {
201 binding: binding as u32,
202 resource: buffer.as_entire_binding(),
203 })
204 .collect();
205 let group = runtime.device().create_bind_group(&wgpu::BindGroupDescriptor {
206 label: Some("icp-trim-bindings"),
207 layout: &self.layout,
208 entries: &entries,
209 });
210 dispatch(encoder, &self.histogram, &group, params.groups, 1);
211 dispatch(encoder, select, &group, 1, 1);
212 }
213 }
214}
215
216pub(super) fn encode_selection(
217 runtime: &WgpuRuntime,
218 encoder: &mut wgpu::CommandEncoder,
219 scratch: &mut ScratchBuffers<'_>,
220 params: &Params,
221 fraction: f64,
222 matches: &wgpu::Buffer,
223 selection: &wgpu::Buffer,
224) {
225 runtime.icp_trim_pipelines.get().unwrap().encode(
226 runtime,
227 encoder,
228 scratch,
229 TrimParams::new(params.count, params.groups, fraction, params.bound, params.targets),
230 matches,
231 selection,
232 );
233}
234
235fn replace_once(shader: &mut String, from: &str, to: &str) {
236 assert_eq!(shader.matches(from).count(), 1, "ICP trim shader anchor changed: {from}");
237 *shader = shader.replacen(from, to, 1);
238}
239
240fn trimmed_shader() -> String {
243 let mut shader = include_str!("../shaders/icp_reduction.wgsl").replace("\r\n", "\n");
244 shader.push_str("\nstruct Selection { gated:u32, retained:u32, rank:u32, distance:u32, source_index:u32, unused0:u32, unused1:u32, unused2:u32 }\n@group(0) @binding(7) var<storage,read> selection: Selection;\nfn retained_match(i:u32)->bool { let distance=bitcast<u32>(matches[i].distance)&0x7fffffffu; return selection.retained>0u && (distance<selection.distance || (distance==selection.distance && i<=selection.source_index)); }\n");
245 replace_once(&mut shader, "if !valid_match(i) { continue; }\n let p=", "if !valid_match(i) { continue; }\n if term!=15u && !retained_match(i) { continue; }\n let p=");
246 replace_once(
247 &mut shader,
248 "if !valid_match(i) { continue; }\n let s=",
249 "if !valid_match(i) || !retained_match(i) { continue; }\n let s=",
250 );
251 replace_once(&mut shader, "result.count=count;", "result.count=count; result.reserved0=selection.gated;\n if count!=selection.retained { atomicOr(&result.status,2u); }");
252 replace_once(
253 &mut shader,
254 "if count>0u {\n if term<6u",
255 "if count>0u || term==15u {\n if term<6u",
256 );
257 replace_once(&mut shader, "if atomicLoad(&result.status)!=0u || result.count==0u {", "if result.count==0u { result.source_anchor=vec4<f32>(0.0); result.target_anchor=vec4<f32>(0.0); }\n if atomicLoad(&result.status)!=0u || result.reserved0==0u {");
258 replace_once(
259 &mut shader,
260 "result.count=0u; result.source_anchor=",
261 "result.count=0u; result.reserved0=0u; result.source_anchor=",
262 );
263 shader
264}
265
266fn precise_shader() -> String {
267 let mut shader = trimmed_shader();
268 shader.push_str(include_str!("../shaders/icp_trim_moments.wgsl"));
269 replace_once(&mut shader, "var value=0.0;", "var value=vec2<f32>(0.0);");
270 replace_once(
271 &mut shader,
272 "value=((p-anchor)/params.scale)[term];",
273 "value=ds_offset(p[term],anchor[term],params.scale);",
274 );
275 replace_once(
276 &mut shader,
277 "value=((q-params.target_anchor.xyz)/params.scale)[term-3u];",
278 "value=ds_offset(q[term-3u],params.target_anchor[term-3u],params.scale);",
279 );
280 replace_once(&mut shader, "value=dot3(d,d);", "value=vec2<f32>(dot3(d,d),0.0);");
281 replace_once(&mut shader, "if !finite(value) {", "if !finite(value.x) || !finite(value.y) {");
282 replace_once(
283 &mut shader,
284 "total=add(total,value);",
285 "if term==15u { total=add(total,value.x); } else { total=precise_merge(total,value); }",
286 );
287 replace_once(&mut shader, "result.values[term]=merge(vec2<f32>(total.x/f32(count),0.0),vec2<f32>(total.y/f32(count),0.0));", "result.values[term]=ds_divide(total,f32(count));");
288 replace_once(
289 &mut shader,
290 "let mean_s=result.values[row].x+result.values[row].y;",
291 "let mean_s=result.values[row];",
292 );
293 replace_once(
294 &mut shader,
295 "let mean_t=result.values[col+3u].x+result.values[col+3u].y;",
296 "let mean_t=result.values[col+3u];",
297 );
298 replace_once(
299 &mut shader,
300 "let s=(queries[i].xyz-result.source_anchor.xyz)/params.scale;",
301 "let s=ds_offset(queries[i][row],result.source_anchor[row],params.scale);",
302 );
303 replace_once(&mut shader, "let t=(target_points[matches[i].index].xyz-result.target_anchor.xyz)/params.scale;", "let t=ds_offset(target_points[matches[i].index][col],result.target_anchor[col],params.scale);");
304 replace_once(
305 &mut shader,
306 "total=add(total,(s[row]-mean_s)*(t[col]-mean_t));",
307 "total=precise_merge(total,ds_product(precise_merge(s,-mean_s),precise_merge(t,-mean_t)));",
308 );
309 replace_once(&mut shader, "if lane<stride { sums[lane]=merge(sums[lane],sums[lane+stride]); counts[lane]+=counts[lane+stride]; }", "if lane<stride { if term==15u { sums[lane]=merge(sums[lane],sums[lane+stride]); } else { sums[lane]=precise_merge(sums[lane],sums[lane+stride]); } counts[lane]+=counts[lane+stride]; }");
310 let group_merge =
311 "for (var g=0u;g<params.groups;g+=1u) { total=merge(total,partials[g].values[term]); }";
312 assert_eq!(shader.matches(group_merge).count(), 2);
313 shader=shader.replacen(group_merge,"for (var g=0u;g<params.groups;g+=1u) { if term==15u { total=merge(total,partials[g].values[term]); } else { total=precise_merge(total,partials[g].values[term]); } }",1);
314 replace_once(
315 &mut shader,
316 "if lane<stride { sums[lane]=merge(sums[lane],sums[lane+stride]); }",
317 "if lane<stride { sums[lane]=precise_merge(sums[lane],sums[lane+stride]); }",
318 );
319 replace_once(&mut shader, "for (var g=0u;g<params.groups;g+=1u) { total=merge(total,partials[g].values[term]); }", "for (var g=0u;g<params.groups;g+=1u) { total=precise_merge(total,partials[g].values[term]); }");
321 shader
322}
323
324#[cfg(all(test, not(target_arch = "wasm32")))]
325#[path = "icp_trim_tests.rs"]
326mod tests;