1use std::borrow::Cow;
13
14use bytemuck::{Pod, Zeroable, bytes_of};
15use wgpu::util::DeviceExt;
16
17use crate::{KernelError, StencilTable};
18
19pub const STENCIL_EVAL_WGSL: &str = include_str!("../shaders/stencil_eval.wgsl");
21
22pub const MAX_COMPONENTS: u32 = 32;
25
26const DEFAULT_WORKGROUP_SIZE: u32 = 64;
27
28#[derive(Debug)]
34pub struct GpuContext {
35 pub device: wgpu::Device,
37 pub queue: wgpu::Queue,
39}
40
41impl GpuContext {
42 pub fn new() -> Option<Self> {
45 pollster::block_on(Self::new_async())
46 }
47
48 pub async fn new_async() -> Option<Self> {
50 let instance = wgpu::Instance::default();
51 let adapter = instance
52 .request_adapter(&wgpu::RequestAdapterOptions {
53 power_preference: wgpu::PowerPreference::HighPerformance,
54 compatible_surface: None,
55 force_fallback_adapter: false,
56 })
57 .await
58 .ok()?;
59
60 let required_limits = wgpu::Limits {
62 max_storage_buffers_per_shader_stage: 8,
63 ..wgpu::Limits::downlevel_defaults()
64 };
65
66 let (device, queue) = adapter
67 .request_device(&wgpu::DeviceDescriptor {
68 label: Some("subdiv-kernels::wgpu"),
69 required_features: wgpu::Features::empty(),
70 required_limits,
71 memory_hints: wgpu::MemoryHints::Performance,
72 trace: wgpu::Trace::Off,
73 experimental_features: wgpu::ExperimentalFeatures::default(),
74 })
75 .await
76 .ok()?;
77
78 Some(Self { device, queue })
79 }
80
81 pub fn evaluate(
89 &self,
90 table: &StencilTable,
91 input: &[f32],
92 components: u32,
93 ) -> Result<Vec<f32>, KernelError> {
94 if components == 0 || components > MAX_COMPONENTS {
95 return Err(KernelError::Gpu(format!(
96 "components {components} out of range 1..={MAX_COMPONENTS}"
97 )));
98 }
99 let device = &self.device;
100 let output_count = table.output_count();
101
102 let gpu_table = StencilTableGpu::from((device, table));
103 let pipeline = StencilEvalPipeline::new(device);
104
105 let src_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
106 label: Some("subdiv-kernels::eval_src"),
107 contents: bytemuck::cast_slice(input),
108 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
109 });
110
111 let dst_len = output_count * components as usize;
112 let dst_size = ((dst_len * std::mem::size_of::<f32>()) as u64).max(4);
113 let dst_buffer = device.create_buffer(&wgpu::BufferDescriptor {
114 label: Some("subdiv-kernels::eval_dst"),
115 size: dst_size,
116 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
117 mapped_at_creation: false,
118 });
119
120 let desc = BufferDescriptor::packed(components);
121 evaluate_stencils(
122 device,
123 &self.queue,
124 &pipeline,
125 &gpu_table,
126 &src_buffer,
127 &dst_buffer,
128 desc,
129 desc,
130 0..output_count as u32,
131 )?;
132
133 let data = readback(device, &self.queue, &dst_buffer, dst_size);
134 Ok(data[..dst_len].to_vec())
135 }
136
137 pub fn evaluate_sparse(
144 &self,
145 table: &StencilTable,
146 input: &[f32],
147 components: u32,
148 affected: &[u32],
149 prior_output: &[f32],
150 ) -> Result<Vec<f32>, KernelError> {
151 if components == 0 || components > MAX_COMPONENTS {
152 return Err(KernelError::Gpu(format!(
153 "components {components} out of range 1..={MAX_COMPONENTS}"
154 )));
155 }
156 let device = &self.device;
157 let dst_len = table.output_count() * components as usize;
158 if prior_output.len() != dst_len {
159 return Err(KernelError::Gpu(format!(
160 "prior_output length {} != output_count*components {dst_len}",
161 prior_output.len()
162 )));
163 }
164
165 let gpu_table = StencilTableGpu::from((device, table));
166 let pipeline = StencilEvalPipeline::new(device);
167
168 let src_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
169 label: Some("subdiv-kernels::sparse_src"),
170 contents: bytemuck::cast_slice(input),
171 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
172 });
173 let dst_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
175 label: Some("subdiv-kernels::sparse_dst"),
176 contents: bytemuck::cast_slice(prior_output),
177 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
178 });
179 let indirection = storage_buffer(
180 device,
181 "subdiv-kernels::sparse_indirection",
182 bytemuck::cast_slice(affected),
183 );
184
185 let desc = BufferDescriptor::packed(components);
186 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
187 label: Some("subdiv-kernels::evaluate_sparse"),
188 });
189 pipeline.encode_indexed(
190 device,
191 &mut encoder,
192 &gpu_table,
193 &src_buffer,
194 &dst_buffer,
195 &indirection,
196 desc,
197 desc,
198 affected.len() as u32,
199 )?;
200 self.queue.submit(std::iter::once(encoder.finish()));
201 device.poll(wgpu::PollType::wait_indefinitely()).ok();
202
203 let dst_size = ((dst_len * std::mem::size_of::<f32>()) as u64).max(4);
204 let data = readback(device, &self.queue, &dst_buffer, dst_size);
205 Ok(data[..dst_len].to_vec())
206 }
207}
208
209#[derive(Debug, Clone, Copy)]
211pub struct BufferDescriptor {
212 pub offset: u32,
214 pub stride: u32,
216 pub length: u32,
218}
219
220impl BufferDescriptor {
221 pub fn packed(components: u32) -> Self {
223 Self {
224 offset: 0,
225 stride: components,
226 length: components,
227 }
228 }
229}
230
231#[derive(Debug)]
233pub struct StencilTableGpu {
234 output_count: u32,
235 offsets: wgpu::Buffer,
236 indices: wgpu::Buffer,
237 weights: wgpu::Buffer,
238}
239
240impl<'a, 'b> From<(&'a wgpu::Device, &'b StencilTable)> for StencilTableGpu {
242 fn from((device, table): (&'a wgpu::Device, &'b StencilTable)) -> Self {
243 Self {
244 output_count: table.output_count() as u32,
245 offsets: storage_buffer(
246 device,
247 "subdiv-kernels::stencil_offsets",
248 bytemuck::cast_slice(&table.offsets),
249 ),
250 indices: storage_buffer(
251 device,
252 "subdiv-kernels::stencil_indices",
253 bytemuck::cast_slice(&table.indices),
254 ),
255 weights: storage_buffer(
256 device,
257 "subdiv-kernels::stencil_weights",
258 bytemuck::cast_slice(&table.weights),
259 ),
260 }
261 }
262}
263
264impl StencilTableGpu {
265 pub fn output_count(&self) -> u32 {
267 self.output_count
268 }
269}
270
271fn storage_buffer(device: &wgpu::Device, label: &str, bytes: &[u8]) -> wgpu::Buffer {
274 const FALLBACK: [u8; 4] = [0u8; 4];
275 let contents = if bytes.is_empty() {
276 &FALLBACK[..]
277 } else {
278 bytes
279 };
280 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
281 label: Some(label),
282 contents,
283 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
284 })
285}
286
287#[repr(C)]
288#[derive(Clone, Copy, Pod, Zeroable)]
289struct ShaderParams {
290 src_offset: u32,
291 dst_offset: u32,
292 src_stride: u32,
293 dst_stride: u32,
294 length: u32,
295 batch_start: u32,
296 batch_end: u32,
297 _pad: u32,
299}
300
301#[derive(Debug)]
303pub struct StencilEvalPipeline {
304 bind_group_layout: wgpu::BindGroupLayout,
305 pipeline: wgpu::ComputePipeline,
306 indexed_bind_group_layout: wgpu::BindGroupLayout,
307 indexed_pipeline: wgpu::ComputePipeline,
308 workgroup_size: u32,
309}
310
311impl StencilEvalPipeline {
312 pub fn new(device: &wgpu::Device) -> Self {
314 Self::with_workgroup_size(device, DEFAULT_WORKGROUP_SIZE)
315 }
316
317 pub fn with_workgroup_size(device: &wgpu::Device, workgroup_size: u32) -> Self {
319 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
320 label: Some("subdiv-kernels::stencil_eval"),
321 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(STENCIL_EVAL_WGSL)),
322 });
323
324 let storage_ro = |binding: u32| wgpu::BindGroupLayoutEntry {
325 binding,
326 visibility: wgpu::ShaderStages::COMPUTE,
327 ty: wgpu::BindingType::Buffer {
328 ty: wgpu::BufferBindingType::Storage { read_only: true },
329 has_dynamic_offset: false,
330 min_binding_size: None,
331 },
332 count: None,
333 };
334
335 let base_entries = vec![
337 wgpu::BindGroupLayoutEntry {
339 binding: 0,
340 visibility: wgpu::ShaderStages::COMPUTE,
341 ty: wgpu::BindingType::Buffer {
342 ty: wgpu::BufferBindingType::Uniform,
343 has_dynamic_offset: false,
344 min_binding_size: std::num::NonZeroU64::new(
345 std::mem::size_of::<ShaderParams>() as u64,
346 ),
347 },
348 count: None,
349 },
350 storage_ro(1),
352 wgpu::BindGroupLayoutEntry {
354 binding: 2,
355 visibility: wgpu::ShaderStages::COMPUTE,
356 ty: wgpu::BindingType::Buffer {
357 ty: wgpu::BufferBindingType::Storage { read_only: false },
358 has_dynamic_offset: false,
359 min_binding_size: None,
360 },
361 count: None,
362 },
363 storage_ro(3),
365 storage_ro(4),
366 storage_ro(5),
367 ];
368
369 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
370 label: Some("subdiv-kernels::stencil_eval_bgl"),
371 entries: &base_entries,
372 });
373
374 let mut indexed_entries = base_entries.clone();
376 indexed_entries.push(storage_ro(6));
377 let indexed_bind_group_layout =
378 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
379 label: Some("subdiv-kernels::stencil_eval_indexed_bgl"),
380 entries: &indexed_entries,
381 });
382
383 let make_pipeline = |bgl: &wgpu::BindGroupLayout, entry: &str, label: &str| {
384 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
385 label: Some(label),
386 bind_group_layouts: &[Some(bgl)],
387 immediate_size: 0,
388 });
389 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
390 label: Some(label),
391 layout: Some(&layout),
392 module: &shader,
393 entry_point: Some(entry),
394 compilation_options: wgpu::PipelineCompilationOptions {
395 constants: &[("WORKGROUP_SIZE", workgroup_size as f64)],
396 zero_initialize_workgroup_memory: true,
397 },
398 cache: None,
399 })
400 };
401
402 let pipeline = make_pipeline(
403 &bind_group_layout,
404 "eval_stencils",
405 "subdiv-kernels::stencil_eval_pipeline",
406 );
407 let indexed_pipeline = make_pipeline(
408 &indexed_bind_group_layout,
409 "eval_stencils_indexed",
410 "subdiv-kernels::stencil_eval_indexed_pipeline",
411 );
412
413 Self {
414 bind_group_layout,
415 pipeline,
416 indexed_bind_group_layout,
417 indexed_pipeline,
418 workgroup_size,
419 }
420 }
421
422 #[allow(clippy::too_many_arguments)]
425 pub fn encode(
426 &self,
427 device: &wgpu::Device,
428 encoder: &mut wgpu::CommandEncoder,
429 gpu_table: &StencilTableGpu,
430 src_buffer: &wgpu::Buffer,
431 dst_buffer: &wgpu::Buffer,
432 src_desc: BufferDescriptor,
433 dst_desc: BufferDescriptor,
434 batch_range: std::ops::Range<u32>,
435 ) -> Result<(), KernelError> {
436 if dst_desc.length > MAX_COMPONENTS {
437 return Err(KernelError::Gpu(format!(
438 "primvar length {} exceeds kernel capacity {MAX_COMPONENTS}",
439 dst_desc.length
440 )));
441 }
442
443 let params = ShaderParams {
444 src_offset: src_desc.offset,
445 dst_offset: dst_desc.offset,
446 src_stride: src_desc.stride,
447 dst_stride: dst_desc.stride,
448 length: dst_desc.length,
449 batch_start: batch_range.start,
450 batch_end: batch_range.end,
451 _pad: 0,
452 };
453 let params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
454 label: Some("subdiv-kernels::stencil_params"),
455 contents: bytes_of(¶ms),
456 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
457 });
458
459 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
460 label: Some("subdiv-kernels::stencil_eval_bg"),
461 layout: &self.bind_group_layout,
462 entries: &[
463 wgpu::BindGroupEntry {
464 binding: 0,
465 resource: params_buf.as_entire_binding(),
466 },
467 wgpu::BindGroupEntry {
468 binding: 1,
469 resource: src_buffer.as_entire_binding(),
470 },
471 wgpu::BindGroupEntry {
472 binding: 2,
473 resource: dst_buffer.as_entire_binding(),
474 },
475 wgpu::BindGroupEntry {
476 binding: 3,
477 resource: gpu_table.offsets.as_entire_binding(),
478 },
479 wgpu::BindGroupEntry {
480 binding: 4,
481 resource: gpu_table.indices.as_entire_binding(),
482 },
483 wgpu::BindGroupEntry {
484 binding: 5,
485 resource: gpu_table.weights.as_entire_binding(),
486 },
487 ],
488 });
489
490 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
491 label: Some("subdiv-kernels::stencil_eval"),
492 timestamp_writes: None,
493 });
494 pass.set_pipeline(&self.pipeline);
495 pass.set_bind_group(0, &bind_group, &[]);
496
497 let invocations = batch_range.end.saturating_sub(batch_range.start);
498 let groups = invocations.div_ceil(self.workgroup_size);
499 if groups > 0 {
500 pass.dispatch_workgroups(groups, 1, 1);
501 }
502 drop(pass);
503 Ok(())
504 }
505
506 #[allow(clippy::too_many_arguments)]
510 pub fn encode_indexed(
511 &self,
512 device: &wgpu::Device,
513 encoder: &mut wgpu::CommandEncoder,
514 gpu_table: &StencilTableGpu,
515 src_buffer: &wgpu::Buffer,
516 dst_buffer: &wgpu::Buffer,
517 indirection: &wgpu::Buffer,
518 src_desc: BufferDescriptor,
519 dst_desc: BufferDescriptor,
520 count: u32,
521 ) -> Result<(), KernelError> {
522 if dst_desc.length > MAX_COMPONENTS {
523 return Err(KernelError::Gpu(format!(
524 "primvar length {} exceeds kernel capacity {MAX_COMPONENTS}",
525 dst_desc.length
526 )));
527 }
528
529 let params = ShaderParams {
530 src_offset: src_desc.offset,
531 dst_offset: dst_desc.offset,
532 src_stride: src_desc.stride,
533 dst_stride: dst_desc.stride,
534 length: dst_desc.length,
535 batch_start: 0,
536 batch_end: count,
537 _pad: 0,
538 };
539 let params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
540 label: Some("subdiv-kernels::stencil_params"),
541 contents: bytes_of(¶ms),
542 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
543 });
544
545 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
546 label: Some("subdiv-kernels::stencil_eval_indexed_bg"),
547 layout: &self.indexed_bind_group_layout,
548 entries: &[
549 wgpu::BindGroupEntry {
550 binding: 0,
551 resource: params_buf.as_entire_binding(),
552 },
553 wgpu::BindGroupEntry {
554 binding: 1,
555 resource: src_buffer.as_entire_binding(),
556 },
557 wgpu::BindGroupEntry {
558 binding: 2,
559 resource: dst_buffer.as_entire_binding(),
560 },
561 wgpu::BindGroupEntry {
562 binding: 3,
563 resource: gpu_table.offsets.as_entire_binding(),
564 },
565 wgpu::BindGroupEntry {
566 binding: 4,
567 resource: gpu_table.indices.as_entire_binding(),
568 },
569 wgpu::BindGroupEntry {
570 binding: 5,
571 resource: gpu_table.weights.as_entire_binding(),
572 },
573 wgpu::BindGroupEntry {
574 binding: 6,
575 resource: indirection.as_entire_binding(),
576 },
577 ],
578 });
579
580 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
581 label: Some("subdiv-kernels::stencil_eval_indexed"),
582 timestamp_writes: None,
583 });
584 pass.set_pipeline(&self.indexed_pipeline);
585 pass.set_bind_group(0, &bind_group, &[]);
586 let groups = count.div_ceil(self.workgroup_size);
587 if groups > 0 {
588 pass.dispatch_workgroups(groups, 1, 1);
589 }
590 drop(pass);
591 Ok(())
592 }
593}
594
595#[allow(clippy::too_many_arguments)]
598pub fn evaluate_stencils(
599 device: &wgpu::Device,
600 queue: &wgpu::Queue,
601 pipeline: &StencilEvalPipeline,
602 gpu_table: &StencilTableGpu,
603 src_buffer: &wgpu::Buffer,
604 dst_buffer: &wgpu::Buffer,
605 src_desc: BufferDescriptor,
606 dst_desc: BufferDescriptor,
607 batch_range: std::ops::Range<u32>,
608) -> Result<(), KernelError> {
609 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
610 label: Some("subdiv-kernels::evaluate_stencils"),
611 });
612 pipeline.encode(
613 device,
614 &mut encoder,
615 gpu_table,
616 src_buffer,
617 dst_buffer,
618 src_desc,
619 dst_desc,
620 batch_range,
621 )?;
622 queue.submit(std::iter::once(encoder.finish()));
623 device.poll(wgpu::PollType::wait_indefinitely()).ok();
624 Ok(())
625}
626
627fn readback(
629 device: &wgpu::Device,
630 queue: &wgpu::Queue,
631 src: &wgpu::Buffer,
632 size_bytes: u64,
633) -> Vec<f32> {
634 let staging = device.create_buffer(&wgpu::BufferDescriptor {
635 label: Some("subdiv-kernels::readback"),
636 size: size_bytes,
637 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
638 mapped_at_creation: false,
639 });
640
641 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
642 label: Some("subdiv-kernels::readback_copy"),
643 });
644 encoder.copy_buffer_to_buffer(src, 0, &staging, 0, size_bytes);
645 queue.submit(std::iter::once(encoder.finish()));
646
647 let slice = staging.slice(..);
648 let (tx, rx) = std::sync::mpsc::channel();
649 slice.map_async(wgpu::MapMode::Read, move |result| {
650 let _ = tx.send(result);
651 });
652 device.poll(wgpu::PollType::wait_indefinitely()).ok();
653 rx.recv()
654 .expect("map_async callback dropped")
655 .expect("buffer map failed");
656
657 let data = bytemuck::cast_slice::<u8, f32>(&slice.get_mapped_range()).to_vec();
658 staging.unmap();
659 data
660}