Skip to main content

fidget_wgpu/
lib.rs

1//! Shader generation and WGPU-based image rendering
2//!
3//! # API design
4//! Using GPUs is complicated<sup>[citation needed]</sup>.  The APIs in this
5//! crate try to strike a balance between ease of use and efficiency.  As
6//! always, feel free to open an issue or discussion if the APIs don't work for
7//! you; they were codesigned along with
8//! [Halfspace](https://github.com/mkeeter/halfspace), and may not yet be
9//! suitable for every use case.
10//!
11//! ## Object types
12//! All of the modules use similar patterns of objects:
13//!
14//! - The [`Gpu`] object is passed around to provide device and queues
15//! - A `Context` object contains pipelines
16//! - A `Workspace` object contains buffers used when rendering
17//! - An output buffer can be read back to the CPU or passed to a subsequent
18//!   render pipeline
19//!
20//! ### Context objects
21//! A context object contains GPU pipelines and allow users to dispatch work to
22//! the GPU.  Voxel and pixel rendering are managed by [`voxel::Context`] and
23//! [`pixel::Context`] respectively.  Post-processing is done by
24//! [`voxel::effects::Context`] and [`pixel::effects::Context`].
25//!
26//! Users are expected to create one (of each) context object per thread or
27//! worker, since GPU resources can't be shared.
28//!
29//! Context objects have two flavors of functions.  At the highest level, `run`
30//! and `run_async` functions perform rendering and copy data back to the CPU
31//! (e.g. [`voxel::Context::run`] and [`run_async`](voxel::Context::run_async)).
32//! To simply submit work to the GPU, use a `submit` function (e.g.
33//! [`voxel::Context::submit`]).
34//!
35//! ### Workspace objects
36//! Workspace objects contain all of the buffers that are used when dispatching
37//! work to the GPU.  They are also per-thread (or per-worker).  You may have
38//! more than one per thread if you want to dispatch multiple jobs
39//! simultaneously; it's your computer.
40//!
41//! Workspaces resize themselves automatically when used in rendering.  They
42//! typically have [`size()`](voxel::Workspace::size) (active bytes) and
43//! [`capacity()`](voxel::Workspace::capacity) (total allocated bytes)
44//! functions; users may want to check for overly large ratios and recreate
45//! workspaces.
46//!
47//! Workspaces are stateful; after they are used in a `submit` function, they
48//! will contain data in a GPU buffer.  The output buffer is typically accessed
49//! with the `output()` function, e.g. [`voxel::Workspace::output`].
50//!
51//! ### Reading data from buffers
52//! The output of a workspace is a [`FlexBuffer`](crate::buf::FlexBuffer)
53//! (indeed, they are used pervasively throughout this crate).  Output buffers
54//! are created with `STORAGE | COPY_SRC`.  Reading data back to the CPU is a
55//! three-part process:
56//!
57//! - Create a CPU-readable buffer with [`Gpu::read_buffer_for`]
58//! - Copy data with [`Gpu::copy`]
59//! - Map the readable buffer with [`Gpu::map`] or [`Gpu::map_async`]
60//!
61//! For quick debugging, [`Gpu::read_vec`] does all of these steps.  You
62//! wouldn't want to use in a tight loop, since it allocates a GPU buffer on
63//! each call.
64#![warn(missing_docs)]
65
66use fidget_bytecode::{Bytecode, ReservedRegister};
67use fidget_core::{
68    eval::Function,
69    shape::{MissingVar, ShapeVars},
70    var::{Var, VarMap},
71    vm::VmShape,
72};
73use fidget_raster::RenderSize;
74
75use heck::ToShoutySnakeCase;
76use zerocopy::{FromBytes, Immutable, IntoBytes};
77
78pub mod buf;
79pub mod color;
80pub mod pixel;
81pub mod voxel;
82
83/// Re-export the `wgpu` module
84pub use wgpu;
85
86pub(crate) mod shaders {
87    use super::*;
88
89    pub const COMMON: &str = include_str!("shaders/common.wgsl");
90    pub const DUMMY_STACK: &str = include_str!("shaders/dummy_stack.wgsl");
91    pub const FLOAT_OPS: &str = include_str!("shaders/float_ops.wgsl");
92    pub const GRAD_OPS: &str = include_str!("shaders/grad_ops.wgsl");
93    pub const INTERVAL_OPS: &str = include_str!("shaders/interval_ops.wgsl");
94    pub const STACK: &str = include_str!("shaders/stack.wgsl");
95    pub const TAPE_INTERPRETER: &str =
96        include_str!("shaders/tape_interpreter.wgsl");
97    pub const TAPE_SIMPLIFY: &str = include_str!("shaders/tape_simplify.wgsl");
98
99    /// Returns a set of constant definitions for each opcode
100    pub fn opcode_constants() -> String {
101        let mut out = String::new();
102        for (op, i) in fidget_bytecode::iter_ops() {
103            out += &format!(
104                "const OP_{}: u32 = {i};\n",
105                op.to_shouty_snake_case()
106            );
107        }
108        out
109    }
110}
111
112/// Number of [`TapeWord`] words in the tape data flexible array
113pub(crate) const TAPE_DATA_CAPACITY: usize = 8 * 1024 * 1024; // 8M words, 64 MiB
114
115#[repr(C)]
116pub(crate) struct TapeWord {
117    op: u32,
118    imm: u32,
119}
120
121////////////////////////////////////////////////////////////////////////////////
122
123/// Error type for GPU initialization
124#[derive(Debug, thiserror::Error)]
125pub enum InitError {
126    /// Error when requesting an adapter
127    #[error(transparent)]
128    Adapter(#[from] wgpu::RequestAdapterError),
129
130    /// Error when requesting a device
131    #[error(transparent)]
132    Device(#[from] wgpu::RequestDeviceError),
133}
134
135/// Handle to a GPU device
136#[derive(Clone)]
137pub struct Gpu {
138    /// GPU device
139    pub device: wgpu::Device,
140    /// GPU queue
141    pub queue: wgpu::Queue,
142}
143
144impl Gpu {
145    /// Returns a [`Gpu`] object with customized settings
146    ///
147    /// Non-default settings are as follows:
148    /// - We request a [`wgpu::PowerPreference::HighPerformance`] adapter
149    /// - We enable the [`wgpu::Features::TIMESTAMP_QUERY`] feature
150    ///
151    /// This is a helper function for simplicity; more sophisticated systems
152    /// will likely construct the adapter, device, and queue themselves.
153    pub async fn init() -> Result<Gpu, InitError> {
154        let instance = wgpu::Instance::default();
155        let adapter = instance
156            .request_adapter(&wgpu::RequestAdapterOptions {
157                power_preference: wgpu::PowerPreference::HighPerformance,
158                ..wgpu::RequestAdapterOptions::default()
159            })
160            .await?;
161        let (device, queue) = adapter
162            .request_device(&wgpu::DeviceDescriptor {
163                required_features: wgpu::Features::TIMESTAMP_QUERY,
164                ..wgpu::DeviceDescriptor::default()
165            })
166            .await?;
167        Ok(Gpu { device, queue })
168    }
169
170    /// Returns a [`Gpu`] object with default settings
171    ///
172    /// This is useful for CI, where `TIMESTAMP_QUERY` is unsupported
173    #[doc(hidden)]
174    pub async fn init_basic() -> Result<Gpu, InitError> {
175        let instance = wgpu::Instance::default();
176        let adapter = instance
177            .request_adapter(&wgpu::RequestAdapterOptions::default())
178            .await?;
179        let (device, queue) = adapter
180            .request_device(&wgpu::DeviceDescriptor::default())
181            .await?;
182        Ok(Gpu { device, queue })
183    }
184
185    /// Returns a readable buffer for the given buffer
186    ///
187    /// See [`copy`](Self::copy) and [`map`](Self::map) for how to use the
188    /// resulting read buffer.
189    pub fn read_buffer_for<T>(
190        &self,
191        buf: &buf::FlexBuffer<T>,
192    ) -> buf::ReadBuffer<T>
193    where
194        T: buf::BufferTag,
195    {
196        buf::ReadBuffer::new(
197            &self.device,
198            format!("{} (read)", buf.name()),
199            buf.size(),
200        )
201        .expect("buf.size should always be a valid size for ReadBuffer::new")
202    }
203
204    /// Builds a new readable buffer
205    ///
206    /// The buffer has an arbitrary starting size and will be resized when used
207    /// in [`copy`](Self::copy).
208    ///
209    /// If the target buffer is known, use
210    /// [`read_buffer_for`](Self::read_buffer_for) to avoid this reallocation.
211    pub fn read_buffer<T>(&self, name: &str) -> buf::ReadBuffer<T>
212    where
213        T: buf::BufferTag,
214        T::S: TryFrom<u32>,
215    {
216        let Ok(size) = 64u32.try_into() else {
217            panic!("could not build size");
218        };
219        buf::ReadBuffer::new(&self.device, name, size)
220            .expect("64 should always be a valid size for ReadBuffer::new")
221    }
222
223    /// Helper function to read from a buffer to a `Vec`
224    ///
225    /// Under the hood, this function simply calls
226    /// [`read_buffer_for`](Self::read_buffer_for),
227    /// [`copy`](Self::copy), [`map`](Self::map), and
228    /// [`to_vec`](buf::MappedBuffer::to_vec).
229    ///
230    /// Note that this function allocates a GPU buffer; if the same source
231    /// buffer is being read repeatedly, it's recommended to build the read
232    /// buffer *once* and then call `copy`, `map`, `to_vec` repeatedly.
233    #[cfg(not(target_arch = "wasm32"))]
234    pub fn read_vec<T: buf::BufferTag>(
235        &self,
236        buf: &buf::FlexBuffer<T>,
237    ) -> Vec<T::T>
238    where
239        T::T: FromBytes + Immutable + Clone + Copy,
240    {
241        let mut scratch = self.read_buffer_for(buf);
242        self.copy(buf, &mut scratch);
243        let data = self.map(&mut scratch);
244        data.to_vec()
245    }
246
247    /// Copies from a GPU-resident buffer to a host-mappable buffer
248    ///
249    /// The host-mappable destination buffer is resized to fit the data.
250    pub fn copy<A>(
251        &self,
252        src: &buf::FlexBuffer<A>,
253        dst: &mut buf::ReadBuffer<A>,
254    ) where
255        A: buf::BufferTag,
256    {
257        let mut encoder = self.device.create_command_encoder(
258            &wgpu::CommandEncoderDescriptor {
259                label: Some("read_buffer"),
260            },
261        );
262        self.encode_copy(src, dst, &mut encoder);
263        self.queue.submit(Some(encoder.finish()));
264    }
265
266    /// Low-level command to submit a buffer copy to a command encoder
267    ///
268    /// See [`copy`](Self::copy) for details
269    pub fn encode_copy<A>(
270        &self,
271        src: &buf::FlexBuffer<A>,
272        dst: &mut buf::ReadBuffer<A>,
273        encoder: &mut wgpu::CommandEncoder,
274    ) where
275        A: buf::BufferTag,
276    {
277        dst.grow_to_fit(&self.device, src.size())
278            .expect("dst buffer should be resizable to match src buffer");
279        encoder.copy_buffer_to_buffer(
280            src.data(),
281            0,
282            dst.data(),
283            0,
284            src.size_bytes(),
285        );
286    }
287
288    /// Blocking function to build a new mapped buffer
289    #[cfg(any(not(target_arch = "wasm32"), doc))]
290    pub fn map<'a, T>(
291        &self,
292        image: &'a mut buf::ReadBuffer<T>,
293    ) -> buf::MappedBuffer<'a, T>
294    where
295        T: buf::BufferTag,
296        T::T: Immutable + FromBytes + Copy,
297    {
298        pollster::block_on(self.map_async(image))
299    }
300
301    /// Async function to build a new mapped buffer
302    ///
303    /// This can be called on either native or web platforms.  On native
304    /// platforms, the single `await` is trivial (guaranteed to always be
305    /// ready); on the web, the mapping sends us back to the event loop until
306    /// it's ready.
307    pub async fn map_async<'a, T>(
308        &self,
309        data: &'a mut buf::ReadBuffer<T>,
310    ) -> buf::MappedBuffer<'a, T>
311    where
312        T: buf::BufferTag,
313        T::T: Immutable + FromBytes + Copy,
314    {
315        let (tx, rx) = flume::bounded(1);
316        let slice = data.map_async(move |_| tx.send(()).unwrap());
317        self.device
318            .poll(wgpu::PollType::wait_indefinitely())
319            .unwrap();
320        rx.recv_async().await.unwrap();
321        buf::MappedBuffer::new(data, slice)
322    }
323
324    /// Blocking function to build a new mapped image
325    #[cfg(any(not(target_arch = "wasm32"), doc))]
326    pub fn map_image<'a, T>(
327        &self,
328        data: &'a mut buf::ReadBuffer<T>,
329    ) -> buf::MappedImage<'a, T>
330    where
331        T: buf::BufferTag,
332        T::T: Immutable + FromBytes + Copy,
333        T::S: RenderSize,
334    {
335        pollster::block_on(self.map_image_async(data))
336    }
337
338    /// Async function to build a new mapped image
339    ///
340    /// This can be called on either native or web platforms.  On native
341    /// platforms, the single `await` is trivial (guaranteed to always be
342    /// ready); on the web, the mapping sends us back to the event loop until
343    /// it's ready.
344    pub async fn map_image_async<'a, T>(
345        &self,
346        data: &'a mut buf::ReadBuffer<T>,
347    ) -> buf::MappedImage<'a, T>
348    where
349        T: buf::BufferTag,
350        T::T: Immutable + FromBytes + Copy,
351        T::S: RenderSize,
352    {
353        self.map_async(data).await.into()
354    }
355}
356
357////////////////////////////////////////////////////////////////////////////////
358
359/// Container of multiple pipelines, parameterized by register count
360pub(crate) struct RegPipeline {
361    thunk: Box<dyn Fn(u8) -> wgpu::ComputePipeline>,
362    cache:
363        [std::cell::OnceCell<wgpu::ComputePipeline>; REG_PIPELINE_SIZES.len()],
364}
365
366const REG_PIPELINE_SIZES: &[u8] = &[8, 16, 32, 64, 128, 192, 255];
367
368impl RegPipeline {
369    pub fn build(builder: Box<dyn Fn(u8) -> wgpu::ComputePipeline>) -> Self {
370        Self {
371            thunk: builder,
372            cache: std::array::from_fn(|_| Default::default()),
373        }
374    }
375
376    /// Returns the pipeline with sufficient registers to render `reg_count`
377    ///
378    /// # Panics
379    /// If `reg_count` is 256 (which is not allowed in bytecode tapes)
380    pub fn get(&self, reg_count: u8) -> &wgpu::ComputePipeline {
381        let (i, r) = REG_PIPELINE_SIZES
382            .iter()
383            .enumerate()
384            .find(|(_i, r)| reg_count <= **r)
385            .expect("bytecode tape cannot use more than 255 registers");
386        self.cache[i].get_or_init(|| (*self.thunk)(*r))
387    }
388}
389
390////////////////////////////////////////////////////////////////////////////////
391
392/// Shape for rendering on the GPU
393///
394/// Note that this object does not allocate any memory on the GPU itself; it
395/// stores a bytecode-serialized version of the shape, which is copied to the
396/// GPU during rendering.
397pub struct RenderShape {
398    /// Copy of our shape (kept around for access to the variable map)
399    shape: VmShape,
400    /// Serialized bytecode for the shape
401    bytecode: Bytecode,
402}
403
404/// Error type when constructing a [`RenderShape`]
405#[derive(Debug, thiserror::Error)]
406pub enum RenderShapeError {
407    /// The shape doesn't fit in the GPU tape buffer
408    #[error(
409        "shape bytecode is {0} tape words (8 bytes each), which exceeds \
410        buffer capacity of {TAPE_DATA_CAPACITY} tape words"
411    )]
412    TooLong(usize),
413    /// The shape uses a reserved register
414    #[error(transparent)]
415    RegisterError(#[from] ReservedRegister),
416}
417
418impl RenderShape {
419    /// Builds a new render shape
420    pub fn new(shape: &VmShape) -> Result<Self, RenderShapeError> {
421        // Generate bytecode for the root tape
422        let bytecode = Bytecode::new(shape.inner().data())?;
423        if bytecode.len() / 2 > TAPE_DATA_CAPACITY {
424            return Err(RenderShapeError::TooLong(bytecode.len() / 2));
425        }
426
427        Ok(Self {
428            shape: shape.clone(),
429            bytecode,
430        })
431    }
432
433    /// Helper function to return XYZ variable indices
434    fn axes(&self) -> [u32; 3] {
435        let vars = self.shape.inner().vars();
436        [Var::X, Var::Y, Var::Z]
437            .map(|a| vars.get(&a).map(|v| v as u32).unwrap_or(u32::MAX))
438    }
439
440    /// Copies variables into a variables buffer
441    ///
442    /// Returns `true` if the buffer size changed, which could invalidate cached
443    /// bind groups.
444    fn copy_vars(
445        &self,
446        gpu: &Gpu,
447        vars: &ShapeVars<f32>,
448        buf: &mut buf::FlexBuffer<voxel::VarsBufferTag>,
449    ) -> Result<CopyVarsChanged, CopyVarsError> {
450        copy_vars(gpu, self.shape.inner().vars(), vars, buf)
451    }
452}
453
454pub(crate) fn copy_vars(
455    gpu: &Gpu,
456    vs: &VarMap,
457    vars: &ShapeVars<f32>,
458    buf: &mut buf::FlexBuffer<voxel::VarsBufferTag>,
459) -> Result<CopyVarsChanged, CopyVarsError> {
460    let mut changed = CopyVarsChanged::BufferUnchanged;
461    if vs.has_free_vars() {
462        // Do an initial pass to check for errors before resizing the buffer
463        for (v, _i) in vs.iter() {
464            match v {
465                Var::X | Var::Y | Var::Z => (),
466                Var::V(vi) => {
467                    if vars.get(vi).is_none() {
468                        return Err(MissingVar { var: vi }.into());
469                    };
470                }
471            }
472        }
473        // If we have to change the vars buffer size, then we'll return
474        // `true` indicating that things have changed and bind groups should
475        // be invalidated.  TODO: only do this if we grow the buffer, since
476        // binding an overly-large buffer is fine?
477        let r = buf.grow_to_fit(&gpu.device, vs.len())?;
478        if !matches!(r, std::cmp::Ordering::Equal) {
479            changed = CopyVarsChanged::BufferChanged;
480        }
481        let mut writer = gpu
482            .queue
483            .write_buffer_with(
484                buf.data(),
485                0,
486                ((vs.len() * std::mem::size_of::<f32>()) as u64)
487                    .try_into()
488                    .unwrap(),
489            )
490            .unwrap();
491        for (v, i) in vs.iter() {
492            match v {
493                Var::X | Var::Y | Var::Z => (),
494                Var::V(vi) => {
495                    let value = vars.get(vi).unwrap(); // checked above
496                    let offset = i * std::mem::size_of::<f32>();
497                    writer
498                        .slice(offset..offset + 4)
499                        .copy_from_slice(value.as_bytes());
500                }
501            }
502        }
503    }
504    Ok(changed)
505}
506
507#[derive(thiserror::Error, Debug)]
508enum CopyVarsError {
509    #[error(transparent)]
510    BufferSize(#[from] buf::BufferSizeError),
511    #[error(transparent)]
512    MissingVar(#[from] MissingVar),
513}
514
515#[must_use]
516#[derive(Copy, Clone, Debug)]
517enum CopyVarsChanged {
518    BufferChanged,
519    BufferUnchanged,
520}
521
522////////////////////////////////////////////////////////////////////////////////
523
524/// Helper function for use in unit tests
525#[cfg(test)]
526fn compile_shader(src: &str, desc: &str) {
527    let mut v = naga::valid::Validator::new(
528        naga::valid::ValidationFlags::all(),
529        naga::valid::Capabilities::all(),
530    );
531
532    // This isn't the best formatting, but it will at least include the
533    // relevant text.
534    let m = naga::front::wgsl::parse_str(src).unwrap_or_else(|e| {
535        if let Some(i) = e.location(src) {
536            let pos = i.offset as usize..(i.offset + i.length) as usize;
537            panic!(
538                "shader compilation failed\n{src}\n{}",
539                e.emit_to_string_with_path(&src[pos], desc)
540            );
541        } else {
542            panic!(
543                "shader compilation failed\n{src}\n{}",
544                e.emit_to_string(desc)
545            );
546        }
547    });
548    if let Err(e) = v.validate(&m) {
549        let (pos, desc) = e.spans().next().unwrap();
550        panic!(
551            "shader compilation failed\n{src}\n{}",
552            e.emit_to_string_with_path(&src[pos.to_range().unwrap()], desc)
553        );
554    }
555}
556
557#[cfg(test)]
558mod test {
559    use super::*;
560    use std::collections::HashSet;
561
562    #[test]
563    fn shader_has_all_ops() {
564        for (op, _) in fidget_bytecode::iter_ops() {
565            let op = format!("OP_{}", op.to_shouty_snake_case());
566            assert!(
567                shaders::TAPE_INTERPRETER.contains(&op),
568                "tape interpreter is missing {op}"
569            );
570            assert!(
571                shaders::TAPE_SIMPLIFY.contains(&op),
572                "tape simplification is missing {op}"
573            );
574        }
575    }
576
577    pub(crate) fn compare_struct_layout<T: facet::Facet<'static>>(
578        shader: &str,
579        struct_name: &str,
580    ) {
581        let module = naga::front::wgsl::parse_str(shader).expect("valid WGSL");
582        let (members, span) = module
583            .types
584            .iter()
585            .find_map(|(_, ty)| {
586                if ty.name.as_deref() == Some(struct_name)
587                    && let naga::TypeInner::Struct { members, span } = &ty.inner
588                {
589                    Some((members, *span))
590                } else {
591                    None
592                }
593            })
594            .expect("could not find struct");
595
596        // If the last member of the struct is a dynamically sized array, we'll
597        // treat the beginning offset of the array as our struct size.
598        let dynamic_array_offset = members.last().and_then(|m| {
599            let ty = &module.types[m.ty];
600            let naga::TypeInner::Array {
601                base: _,
602                size: naga::ir::ArraySize::Dynamic,
603                stride: _,
604            } = &ty.inner
605            else {
606                return None;
607            };
608            Some(m.offset)
609        });
610        if let Some(dynamic_array_offset) = dynamic_array_offset {
611            assert_eq!(
612                dynamic_array_offset as usize,
613                std::mem::size_of::<T>(),
614                "dynamic array offset is incorrect; invalid last member size?"
615            );
616        } else {
617            assert_eq!(
618                span as usize,
619                std::mem::size_of::<T>(),
620                "overall size is incorrect"
621            );
622        }
623
624        let facet::Type::User(facet::UserType::Struct(shape)) = T::SHAPE.ty
625        else {
626            panic!("must build a struct");
627        };
628
629        // Check field sizes and offset between Rust and WGSL
630        let mut shape_field_names = HashSet::new();
631        for field in shape.fields {
632            let field_name = field.name;
633            shape_field_names.insert(field_name);
634            let wgsl_member = members
635                .iter()
636                .find(|m| m.name.as_deref() == Some(field_name))
637                .unwrap_or_else(|| {
638                    panic!("field `{field_name}` missing in WGSL struct")
639                });
640            assert_eq!(
641                wgsl_member.offset as usize, field.offset,
642                "offset mismatch for field `{field_name}`"
643            );
644            assert_eq!(
645                module.types[wgsl_member.ty].inner.size(module.to_ctx())
646                    as usize,
647                field.shape().layout.sized_layout().unwrap().size(),
648                "size mismatch for field `{field_name}`"
649            );
650        }
651        let slice_len = if dynamic_array_offset.is_some() {
652            members.len() - 1
653        } else {
654            members.len()
655        };
656        for m in &members[..slice_len] {
657            let field_name =
658                m.name.as_ref().expect("cannot check unnamed WGSL fields");
659            assert!(
660                shape_field_names.contains(field_name.as_str()),
661                "field `{field_name}` missing in Rust struct"
662            );
663        }
664    }
665}