Skip to main content

subdiv_kernels/
wgpu.rs

1//! GPU (wgpu) compute path for uniform stencil evaluation.
2//!
3//! Ported from opensubdiv-petite's `osd::wgpu` (which mirrors OpenSubdiv's
4//! `glslComputeKernel.glsl`), specialised to this crate's CSR [`StencilTable`]
5//! and to positions/primvar evaluation. The limit-derivative (b1) path -- du/dv
6//! and second derivatives -- is a separate future addition.
7//!
8//! This module is behind the `gpu` feature. The output of
9//! [`GpuContext::evaluate`] is bit-close to [`StencilTable::interpolate`] for
10//! `[f32; components]` data (same ops, run on the GPU).
11
12use std::borrow::Cow;
13
14use bytemuck::{Pod, Zeroable, bytes_of};
15use wgpu::util::DeviceExt;
16
17use crate::{KernelError, StencilTable};
18
19/// Canonical WGSL source for the stencil-eval compute kernel.
20pub const STENCIL_EVAL_WGSL: &str = include_str!("../shaders/stencil_eval.wgsl");
21
22/// Maximum primvar components per element the kernel supports (matches the
23/// shader's `MAX_LENGTH`).
24pub const MAX_COMPONENTS: u32 = 32;
25
26const DEFAULT_WORKGROUP_SIZE: u32 = 64;
27
28/// A headless wgpu device + queue for running the compute kernel.
29///
30/// Callers that already own a `wgpu::Device` should build [`StencilEvalPipeline`]
31/// and [`StencilTableGpu`] directly; this is a convenience for standalone use
32/// and tests. [`new`](Self::new) returns `None` when no adapter is available.
33#[derive(Debug)]
34pub struct GpuContext {
35    /// The wgpu device.
36    pub device: wgpu::Device,
37    /// The wgpu queue.
38    pub queue: wgpu::Queue,
39}
40
41impl GpuContext {
42    /// Create a headless compute context, or `None` if no GPU adapter is
43    /// available (so callers can gracefully fall back to the CPU path).
44    pub fn new() -> Option<Self> {
45        pollster::block_on(Self::new_async())
46    }
47
48    /// Async variant of [`new`](Self::new).
49    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        // The kernel binds 5 storage buffers; downlevel defaults allow only 4.
61        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    /// Evaluate `table` over a tightly-packed input buffer of
82    /// `components`-vectors, returning the packed output.
83    ///
84    /// Equivalent to [`StencilTable::interpolate`] for `[f32; components]` data.
85    /// Builds all transient GPU resources per call; for repeated evaluation hold
86    /// a [`StencilEvalPipeline`] + [`StencilTableGpu`] and reuse buffers via
87    /// [`evaluate_stencils`].
88    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    /// Sparse re-evaluation: recompute only the `affected` output rows from
138    /// `input`, splicing them into `prior_output` (the previous dense result).
139    /// Pair with `RefinementResult::affected_outputs` for incremental edits.
140    ///
141    /// The result equals a full dense re-evaluation when `affected` covers every
142    /// output row that actually changed.
143    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        // Seed dst with the prior output; only affected rows get overwritten.
174        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/// Layout of a primvar buffer for the kernel, in floats.
210#[derive(Debug, Clone, Copy)]
211pub struct BufferDescriptor {
212    /// Offset to the first element, in floats.
213    pub offset: u32,
214    /// Stride between consecutive elements, in floats.
215    pub stride: u32,
216    /// Components per element (e.g. 3 for xyz).
217    pub length: u32,
218}
219
220impl BufferDescriptor {
221    /// A tightly-packed buffer of `components`-vectors starting at offset 0.
222    pub fn packed(components: u32) -> Self {
223        Self {
224            offset: 0,
225            stride: components,
226            length: components,
227        }
228    }
229}
230
231/// GPU-resident CSR stencil table (row offsets, indices, weights).
232#[derive(Debug)]
233pub struct StencilTableGpu {
234    output_count: u32,
235    offsets: wgpu::Buffer,
236    indices: wgpu::Buffer,
237    weights: wgpu::Buffer,
238}
239
240/// Upload a [`StencilTable`] into GPU storage buffers.
241impl<'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    /// Number of output rows this table produces.
266    pub fn output_count(&self) -> u32 {
267        self.output_count
268    }
269}
270
271/// Upload `bytes` as a storage buffer, substituting a 4-byte zero buffer when
272/// empty (wgpu rejects zero-sized buffers).
273fn 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 to 32 bytes: a uniform struct rounds up to a 16-byte multiple.
298    _pad: u32,
299}
300
301/// Compute pipeline + bind-group layout for stencil evaluation.
302#[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    /// Build the pipeline with the default workgroup size (64).
313    pub fn new(device: &wgpu::Device) -> Self {
314        Self::with_workgroup_size(device, DEFAULT_WORKGROUP_SIZE)
315    }
316
317    /// Build the pipeline with a specific workgroup size.
318    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        // Bindings 0..=5, shared by the dense and indexed kernels.
336        let base_entries = vec![
337            // 0: uniform params
338            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            // 1: src (read-only)
351            storage_ro(1),
352            // 2: dst (read-write)
353            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            // 3: offsets, 4: indices, 5: weights
364            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        // The indexed kernel adds binding 6: the indirection buffer.
375        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    /// Encode a stencil-evaluation dispatch for the output rows in
423    /// `batch_range` into `encoder`.
424    #[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(&params),
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    /// Encode a sparse (indexed) dispatch: recompute only the output rows named
507    /// by `indirection` (e.g. from `affected_outputs`), leaving the other rows of
508    /// `dst_buffer` untouched. `count` is the number of indirection entries.
509    #[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(&params),
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/// One-shot: encode, submit, and wait. The result lands in `dst_buffer`
596/// (which must be at least `output_count * dst_desc.stride` floats).
597#[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
627/// Copy a GPU buffer back to a `Vec<f32>` (blocking).
628fn 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}