1use wgpu::util::DeviceExt;
2
3use crate::Error;
4use crate::common;
5use crate::context::Context;
6use crate::scan::Scanner;
7use crate::sort::pipeline::SortPipeline;
8
9const RADIX_PASSES: u32 = 16;
10const WORKSPACE_GROWTH_BYTES: u64 = 16 * 1024 * 1024;
11const UNIFORM_SIZE_BYTES: u64 = 16;
12
13struct SortWorkspace {
14 capacity_bytes: u64,
15 scratch: wgpu::Buffer,
16 histogram: wgpu::Buffer,
17 scanned_histogram: wgpu::Buffer,
18}
19
20#[derive(Clone, Copy)]
21struct PreparedSort {
22 num_items: u32,
23 num_blocks: u32,
24 size_bytes: u64,
25}
26
27pub struct Sorter {
29 device: wgpu::Device,
30 queue: wgpu::Queue,
31 scanner: Scanner,
32 pipeline: SortPipeline,
33 workspace: Option<SortWorkspace>,
34}
35
36impl Sorter {
37 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
39 Self {
40 device: device.clone(),
41 queue: queue.clone(),
42 scanner: Scanner::new(device, queue),
43 pipeline: SortPipeline::new(device),
44 workspace: None,
45 }
46 }
47
48 pub fn from_context(ctx: &Context) -> Self {
50 Self::new(&ctx.device, &ctx.queue)
51 }
52
53 pub async fn sort(&mut self, input: &[u32]) -> Result<Vec<u32>, Error> {
55 if input.is_empty() {
56 return Ok(Vec::new());
57 }
58
59 let num_items = common::math::checked_u32(input.len() as u64)?;
60 let size_bytes = common::math::checked_byte_size(input.len() as u64, 4)?;
61 let input_buffer = common::buffers::create_storage_buffer(&self.device, input);
62 let output_buffer = common::buffers::create_empty_storage_buffer(&self.device, size_bytes);
63
64 self.sort_gpu_to_gpu(&input_buffer, &output_buffer, num_items)?;
65 common::buffers::download_buffer(&self.device, &self.queue, &output_buffer, size_bytes)
66 .await
67 }
68
69 pub fn sort_gpu_to_gpu(
71 &mut self,
72 input: &wgpu::Buffer,
73 output: &wgpu::Buffer,
74 num_items: u32,
75 ) -> Result<(), Error> {
76 let mut encoder = self
77 .device
78 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
79 label: Some("Radix Sort"),
80 });
81 self.record_sort(&mut encoder, input, output, num_items)?;
82 self.queue.submit(Some(encoder.finish()));
83 Ok(())
84 }
85
86 pub fn record_sort(
88 &mut self,
89 encoder: &mut wgpu::CommandEncoder,
90 input: &wgpu::Buffer,
91 output: &wgpu::Buffer,
92 num_items: u32,
93 ) -> Result<(), Error> {
94 if num_items == 0 {
95 return Ok(());
96 }
97
98 let problem = self.describe_sort(num_items)?;
99 common::buffers::validate_buffer(
100 input,
101 "sort input",
102 problem.size_bytes,
103 wgpu::BufferUsages::STORAGE,
104 )?;
105 common::buffers::validate_buffer(
106 output,
107 "sort output",
108 problem.size_bytes,
109 wgpu::BufferUsages::STORAGE,
110 )?;
111
112 self.ensure_workspace(problem.size_bytes)?;
113 self.record_radix_passes(encoder, input, output, problem)
114 }
115
116 fn describe_sort(&self, num_items: u32) -> Result<PreparedSort, Error> {
117 let size_bytes = common::math::checked_byte_size(u64::from(num_items), 4)?;
118 let items_per_block = self.pipeline.vt * self.pipeline.block_size;
119 let num_blocks = num_items.div_ceil(items_per_block);
120
121 Ok(PreparedSort {
122 num_items,
123 num_blocks,
124 size_bytes,
125 })
126 }
127
128 fn ensure_workspace(&mut self, size_bytes: u64) -> Result<(), Error> {
129 let needs_allocation = self
130 .workspace
131 .as_ref()
132 .is_none_or(|workspace| workspace.capacity_bytes < size_bytes);
133 if needs_allocation {
134 self.allocate_workspace(size_bytes)?;
135 }
136 Ok(())
137 }
138
139 fn record_radix_passes(
140 &mut self,
141 encoder: &mut wgpu::CommandEncoder,
142 input: &wgpu::Buffer,
143 output: &wgpu::Buffer,
144 problem: PreparedSort,
145 ) -> Result<(), Error> {
146 let max_dispatch = 65_535;
147 let x_groups = problem.num_blocks.min(max_dispatch);
148 let y_groups = problem.num_blocks.div_ceil(max_dispatch);
149 let histogram_items = problem
150 .num_blocks
151 .checked_mul(4)
152 .ok_or(Error::SizeOverflow)?;
153
154 let workspace = self.workspace.as_ref().expect("sort workspace is prepared");
155 let scanner = &mut self.scanner;
156 let (uniform, uniform_stride) = create_uniform_buffer(&self.device, problem);
157
158 for radix_pass in 0..RADIX_PASSES {
159 let (source, destination) = pass_buffers(radix_pass, input, output, &workspace.scratch);
160 let uniform_offset = u64::from(radix_pass) * uniform_stride;
161 let reduce_bind_group = create_sort_bind_group(
162 &self.device,
163 &self.pipeline.bind_group_layout,
164 "Reduce Bind Group",
165 (source, &workspace.histogram, destination),
166 &uniform,
167 uniform_offset,
168 );
169 let scatter_bind_group = create_sort_bind_group(
170 &self.device,
171 &self.pipeline.bind_group_layout,
172 "Scatter Bind Group",
173 (source, &workspace.scanned_histogram, destination),
174 &uniform,
175 uniform_offset,
176 );
177
178 record_compute_pass(
179 encoder,
180 &self.pipeline.reduce_pipeline,
181 &reduce_bind_group,
182 x_groups,
183 y_groups,
184 );
185 scanner.record_scan(
186 encoder,
187 &workspace.histogram,
188 &workspace.scanned_histogram,
189 histogram_items,
190 )?;
191 record_compute_pass(
192 encoder,
193 &self.pipeline.scatter_pipeline,
194 &scatter_bind_group,
195 x_groups,
196 y_groups,
197 );
198 }
199
200 Ok(())
201 }
202
203 fn allocate_workspace(&mut self, requested_size: u64) -> Result<(), Error> {
204 let capacity = if requested_size < WORKSPACE_GROWTH_BYTES {
205 requested_size
206 .max(4)
207 .checked_next_power_of_two()
208 .ok_or(Error::SizeOverflow)?
209 } else {
210 common::math::checked_align_to(requested_size, WORKSPACE_GROWTH_BYTES)?
211 };
212 let limits = self.device.limits();
213 let buffer_limit = limits
214 .max_buffer_size
215 .min(u64::from(limits.max_storage_buffer_binding_size));
216 if capacity > buffer_limit {
217 return Err(Error::BufferLimitExceeded {
218 requested: capacity,
219 limit: buffer_limit,
220 });
221 }
222
223 let items_per_block = u64::from(self.pipeline.vt * self.pipeline.block_size);
224 let max_blocks = (capacity / 4).div_ceil(items_per_block);
225 let histogram_bytes = common::math::checked_byte_size(max_blocks, 16)?;
226 let histogram_capacity = common::math::checked_align_to(histogram_bytes, 256)?;
227
228 self.workspace = Some(SortWorkspace {
229 capacity_bytes: capacity,
230 scratch: common::buffers::create_empty_storage_buffer(&self.device, capacity),
231 histogram: common::buffers::create_empty_storage_buffer(
232 &self.device,
233 histogram_capacity,
234 ),
235 scanned_histogram: common::buffers::create_empty_storage_buffer(
236 &self.device,
237 histogram_capacity,
238 ),
239 });
240 Ok(())
241 }
242}
243
244fn pass_buffers<'a>(
245 radix_pass: u32,
246 input: &'a wgpu::Buffer,
247 output: &'a wgpu::Buffer,
248 scratch: &'a wgpu::Buffer,
249) -> (&'a wgpu::Buffer, &'a wgpu::Buffer) {
250 if radix_pass == 0 {
251 (input, scratch)
252 } else if radix_pass.is_multiple_of(2) {
253 (output, scratch)
254 } else {
255 (scratch, output)
256 }
257}
258
259fn create_uniform_buffer(device: &wgpu::Device, problem: PreparedSort) -> (wgpu::Buffer, u64) {
260 let uniform_stride =
261 u64::from(device.limits().min_uniform_buffer_offset_alignment).max(UNIFORM_SIZE_BYTES);
262 let words_per_uniform = (uniform_stride / size_of::<u32>() as u64) as usize;
263 let mut data = vec![0_u32; words_per_uniform * RADIX_PASSES as usize];
264
265 for radix_pass in 0..RADIX_PASSES as usize {
266 let offset = radix_pass * words_per_uniform;
267 data[offset..offset + 4].copy_from_slice(&[
268 radix_pass as u32 * 2,
269 problem.num_items,
270 problem.num_blocks,
271 0,
272 ]);
273 }
274
275 let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
276 label: Some("Sort Uniform"),
277 contents: bytemuck::cast_slice(&data),
278 usage: wgpu::BufferUsages::UNIFORM,
279 });
280 (buffer, uniform_stride)
281}
282
283fn create_sort_bind_group(
284 device: &wgpu::Device,
285 layout: &wgpu::BindGroupLayout,
286 label: &'static str,
287 buffers: (&wgpu::Buffer, &wgpu::Buffer, &wgpu::Buffer),
288 uniform: &wgpu::Buffer,
289 uniform_offset: u64,
290) -> wgpu::BindGroup {
291 let (source, histogram, destination) = buffers;
292 device.create_bind_group(&wgpu::BindGroupDescriptor {
293 label: Some(label),
294 layout,
295 entries: &[
296 wgpu::BindGroupEntry {
297 binding: 0,
298 resource: source.as_entire_binding(),
299 },
300 wgpu::BindGroupEntry {
301 binding: 1,
302 resource: histogram.as_entire_binding(),
303 },
304 wgpu::BindGroupEntry {
305 binding: 2,
306 resource: destination.as_entire_binding(),
307 },
308 wgpu::BindGroupEntry {
309 binding: 3,
310 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
311 buffer: uniform,
312 offset: uniform_offset,
313 size: wgpu::BufferSize::new(UNIFORM_SIZE_BYTES),
314 }),
315 },
316 ],
317 })
318}
319
320fn record_compute_pass(
321 encoder: &mut wgpu::CommandEncoder,
322 pipeline: &wgpu::ComputePipeline,
323 bind_group: &wgpu::BindGroup,
324 x_groups: u32,
325 y_groups: u32,
326) {
327 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
328 pass.set_pipeline(pipeline);
329 pass.set_bind_group(0, bind_group, &[]);
330 pass.dispatch_workgroups(x_groups, y_groups, 1);
331}