Skip to main content

fidget_wgpu/voxel/
mod.rs

1//! GPU-accelerated 3D rendering
2//!
3//! # Theory
4//!
5//! This module implements an algorithm similar to the one described in
6//! [Massively Parallel Rendering of Complex Closed-Form Implicit Surfaces (Keeter '20)](https://www.mattkeeter.com/research/mpr/).
7//! The rest of this section is intended for people who have read that paper
8//! ("MPR" for short).
9//!
10//! We use interval arithmetic on a high-fanout hierarchy of tiles (64³, 16³,
11//! 4³), followed by voxel and normal evaluation.  At each stage of interval
12//! arithmetic, we compute a **simplified tape** for each tile containing only
13//! portions of the expression which are active.
14//!
15//! After the root tile evaluation, tiles are sparse.  Tiles and tapes use an
16//! atomic bump allocator to claim portions of a fixed buffer.  The tile buffer
17//! is always sized to fit all possible tiles; the tape buffer can run out of
18//! space, in which case we fall back to the previous (unsimplified) tape.
19//!
20//! ## Changes versus MPR
21//!
22//! There are a few notable changes compared to the MPR paper and reference
23//! implementation.
24//!
25//! First, modern GPU APIs support indirect dispatch based on buffers on the GPU
26//! itself.  This saves a round-trip: the 64³ shader can compute a dispatch size
27//! for the 16³ shader and store it in a buffer (and so on for subsequent
28//! stages).
29//!
30//! In a more significant change, evaluation is broken into **strata**:
31//!
32//! - The initial pass of 64³ tiles renders all of those tiles, any which are
33//!   active are accumulated into a set of `depth / 64` strata
34//! - Strata are evaluated one at a time in z-sorted order; this is where 16³,
35//!   4³, voxel, and normal evaluation happens.  You can think of this as doing
36//!   raymarching on 64³ voxels at a time.
37//!
38//! Strata-sorted evaluation has a few advantages:
39//!
40//! - We can statically allocate enough space for all tiles: all 64³ in the
41//!   image, then all 16³ and 4³ tiles in a single strata.  It would be
42//!   prohibitive to allocate storage for all 4³ tiles in the entire volume, but
43//!   doing per-strata evaluation reduces the memory scaling from N³ to N².
44//! - We get some amount of Z culling, because each pass can bail out if the
45//!   result in the heightmap fully covers the tile
46//!
47//! # Practice
48//! There are four core objects, each with different lifetimes
49//!
50//! - [`Context`] contains all of the pipelines used for 3D rendering.  It
51//!   is very expensive to build and should be constructed once per thread /
52//!   worker.
53//! - [`RenderShape`] contains serialized bytecode to render a particular shape.
54//!   Best practice is to rebuild it only when a shape changes (i.e. not once
55//!   per frame), although in practice it's pretty fast to construct.
56//! - [`Buffers`] contains GPU buffers needed for rendering at a particular
57//!   image size.  It is primarily expensive in GPU memory, as it contains
58//!   several full-frame buffers.  Best practice is to construct one [`Buffers`]
59//!   object per worker context (or per simultaneous render); if image sizes
60//!   change, it can be resized with [`Context::set_buffers_image_size`] (which
61//!   will grow buffers, but does not shrink them).  Systems with high
62//!   variability in image size may want to periodically compare
63//!   [`size`](Buffers::size) versus [`capacity`](Buffers::capacity) and fully
64//!   reallocate buffers (by constructing a new `Buffers` object) if they get
65//!   too out of whack.
66//! - [`RenderConfig`] sets the transform matrix for rendering.  This is cheap
67//!   to construct and could be built once per frame
68//!
69//! With all that out of the way, usage is pretty simple:
70//! - Build a [`Context`]
71//! - Use [`Context::shape`] to convert from a [`VmShape`] to a [`RenderShape`]
72//! - Use [`Context::buffers`] to get [`Buffers`] at a particular image size
73//! - Use [`Context::image_buffer`] to get an [`ImageReadBuffer`]
74//! - Call [`Context::run`] or [`Context::run_async`] to get an image
75//!
76//! ## Sync and async operation
77//!
78//! GPU operations are asynchronous; operations are submitted to a queue, and
79//! are completed at some point in the future.  [`Context::run`] blocks until
80//! operations are complete, but is only valid on the desktop; it uses
81//! [`wgpu::Device::poll`], which is a no-op on the web.
82//! [`Context::run_async`] is the async equivalent, and is only valid in WebGPU.
83//! These functions are feature-flagged and available depending on compile
84//! target (native versus WebAssembly).
85//!
86//! ## Low-level building blocks
87//!
88//! [`Context::run`] and `run_async` do four things:
89//!
90//! - Run the GPU kernels to produce an output image, which is a
91//!   [`GeometryPixel`] array in a GPU storage buffer
92//! - Copy from that GPU storage buffer to a mappable buffer (for read-back)
93//! - Map that buffer into a [`MappedImage`]
94//! - Read image data back to the CPU
95//!
96//! Lower-level building blocks are also available: [`Context::submit`] submits
97//! the render operations to the GPU, and [`Context::map_image`] /
98//! [`Context::map_image_async`] map the image buffer back to the GPU.
99//!
100//! To reuse the image buffer within a more complex GPU pipeline – without
101//! copying to the mappable buffer or CPU – [`Context::submit`] may be called
102//! with `None` for its `out` argument.  In this case, the output is available
103//! in [`Buffers::image_storage_buffer`] for subsequent pipelines.
104
105use crate::{
106    Gpu,
107    buf::{
108        ArrayBuffer, BufferItemCount, BufferSizeError, BufferType, ImageBuffer,
109        buffer_ro, buffer_ro_dyn, buffer_rw,
110    },
111    opcode_constants, tag,
112};
113use fidget_bytecode::{Bytecode, ReservedRegister};
114use fidget_core::{
115    eval::Function,
116    render::{ImageSize, VoxelSize},
117    shape::{MissingVar, ShapeVars},
118    var::Var,
119    vm::VmShape,
120};
121use fidget_raster::voxel::{GeometryPixel, Image};
122use std::{collections::BTreeMap, num::NonZeroU64};
123use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
124
125const COMMON_SHADER: &str = include_str!("shaders/common.wgsl");
126const VOXEL_TILES_SHADER: &str = include_str!("shaders/voxel_tiles.wgsl");
127const STACK_SHADER: &str = include_str!("shaders/stack.wgsl");
128const DUMMY_STACK_SHADER: &str = include_str!("shaders/dummy_stack.wgsl");
129const INTERVAL_TILES_SHADER: &str = include_str!("shaders/interval_tiles.wgsl");
130const REPACK_SHADER: &str = include_str!("shaders/repack.wgsl");
131const SORT_SHADER: &str = include_str!("shaders/sort.wgsl");
132const INTERVAL_ROOT_SHADER: &str = include_str!("shaders/interval_root.wgsl");
133const INTERVAL_OPS_SHADER: &str = include_str!("shaders/interval_ops.wgsl");
134const CLEAR_SHADER: &str = include_str!("shaders/clear.wgsl");
135const MERGE_SHADER: &str = include_str!("shaders/merge.wgsl");
136const NORMALS_SHADER: &str = include_str!("shaders/normals.wgsl");
137const TAPE_INTERPRETER: &str = include_str!("shaders/tape_interpreter.wgsl");
138const TAPE_SIMPLIFY: &str = include_str!("shaders/tape_simplify.wgsl");
139
140/// Error type when resizing intermediate tile buffers
141#[derive(Debug, thiserror::Error)]
142#[error("failed to resize `{buf}` tile buffer")]
143pub struct TileBuffersError {
144    /// Buffer which failed to resize
145    pub buf: TileBufferName,
146    /// Error returned by buffer resizing
147    #[source]
148    pub err: BufferSizeError,
149}
150
151/// Names of buffers used by the intermediate tile rendering pass
152///
153/// This is only used for error reporting
154#[derive(Debug)]
155#[expect(missing_docs)]
156pub enum TileBufferName {
157    Tiles,
158    Sorted,
159    Zmin,
160}
161
162impl std::fmt::Display for TileBufferName {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        let s = match self {
165            TileBufferName::Tiles => "tiles",
166            TileBufferName::Sorted => "sorted",
167            TileBufferName::Zmin => "zmin",
168        };
169        s.fmt(f)
170    }
171}
172
173/// Error type when resizing root tile buffers
174#[derive(Debug, thiserror::Error)]
175#[error("failed to resize `{buf}` root tile buffer")]
176pub struct RootTileBuffersError {
177    /// Buffer which failed to resize
178    pub buf: RootTileBufferName,
179    /// Error returned by buffer resizing
180    #[source]
181    pub err: BufferSizeError,
182}
183
184/// Names of buffers used by the root tile rendering pass (for error reporting)
185#[derive(Debug)]
186#[expect(missing_docs)]
187pub enum RootTileBufferName {
188    Tiles,
189    Strata,
190    Zmin,
191    Zmax,
192}
193
194impl std::fmt::Display for RootTileBufferName {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        let s = match self {
197            RootTileBufferName::Tiles => "tiles",
198            RootTileBufferName::Strata => "strata",
199            RootTileBufferName::Zmin => "zmin",
200            RootTileBufferName::Zmax => "zmax",
201        };
202        s.fmt(f)
203    }
204}
205
206/// Names of all buffers, used for error reporting
207#[derive(Debug)]
208#[expect(missing_docs)]
209pub enum BufferName {
210    /// Tiles from the 64³ root tile pass
211    Tile64(RootTileBufferName),
212    /// Tiles from the 16³ intermediate tile pass
213    Tile16(TileBufferName),
214    /// Tiles from the 4³ intermediate tile pass
215    Tile4(TileBufferName),
216    TileTapes,
217    Voxels,
218    Heightmap,
219    Geom,
220    Image,
221}
222
223impl std::fmt::Display for BufferName {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        match self {
226            BufferName::Tile64(buf) => write!(f, "`{buf}` tile64"),
227            BufferName::Tile16(buf) => write!(f, "`{buf}` tile16"),
228            BufferName::Tile4(buf) => write!(f, "`{buf}` tile4"),
229            BufferName::TileTapes => write!(f, "`tile tapes`"),
230            BufferName::Voxels => write!(f, "`voxels`"),
231            BufferName::Heightmap => write!(f, "`heightmap`"),
232            BufferName::Geom => write!(f, "`geom`"),
233            BufferName::Image => write!(f, "`image`"),
234        }
235    }
236}
237
238/// Error returned when resizing a [`Buffers`] object
239#[derive(Debug, thiserror::Error)]
240#[error("failed to build {buf} buffer when requesting size {requested:?}")]
241pub struct BuffersError {
242    /// Requested size
243    pub requested: VoxelSize,
244    /// Buffer which failed to resize
245    pub buf: BufferName,
246    /// Error returned by buffer resizing
247    pub err: BufferSizeError,
248}
249
250////////////////////////////////////////////////////////////////////////////////
251
252/// Settings for 3D rendering
253///
254/// Note that this object only contains the world-to-model transform; the image
255/// size is set by the [`Buffers`] object passed into [`run`](Context::run) or
256/// [`run_async`](Context::run_async).
257#[derive(Copy, Clone)]
258pub struct RenderConfig {
259    /// World-to-model transform
260    pub world_to_model: nalgebra::Matrix4<f32>,
261}
262
263impl Default for RenderConfig {
264    fn default() -> Self {
265        Self {
266            world_to_model: nalgebra::Matrix4::identity(),
267        }
268    }
269}
270
271////////////////////////////////////////////////////////////////////////////////
272
273/// Doppelganger of the WGSL `struct Config`
274///
275/// Fields are carefully ordered to require no internal padding (enforced by
276/// `zerocopy` derives)
277#[derive(Debug, IntoBytes, Immutable, FromBytes, KnownLayout)]
278#[repr(C)]
279struct Config {
280    /// Screen-to-model transform matrix
281    mat: [f32; 16],
282
283    /// Input index of X, Y, Z axes
284    ///
285    /// `u32::MAX` is used as a marker if an axis is unused
286    axes: [u32; 3],
287
288    /// Initial offset in `tape_data`
289    tape_data_offset: u32,
290
291    /// Render size, rounded up to the nearest multiple of 64
292    render_size: [u32; 3],
293
294    /// Number of words in the trailing tape buffer
295    tape_data_capacity: u32,
296
297    /// Image size (not rounded)
298    image_size: [u32; 3],
299
300    /// Length of the root tape
301    root_tape_len: u32,
302    // This is followed by a flexible array member containing tape data
303}
304
305/// A render size is rounded up to the next multiple of 64 on every axis
306///
307/// The internal `VoxelSize` stores divided-by-64 values, so that the render
308/// size cannot be constructed with an invalid state.
309#[derive(Copy, Clone, Debug)]
310struct TileRenderSize(VoxelSize);
311
312impl From<VoxelSize> for TileRenderSize {
313    fn from(image_size: VoxelSize) -> Self {
314        let nx = image_size.width().div_ceil(64);
315        let ny = image_size.height().div_ceil(64);
316        let nz = image_size.depth().div_ceil(64);
317        Self(VoxelSize::new(nx, ny, nz))
318    }
319}
320
321impl TileRenderSize {
322    /// Number of tiles in the X axis
323    fn nx(&self) -> u32 {
324        self.0.width()
325    }
326
327    /// Number of tiles in the Y axis
328    fn ny(&self) -> u32 {
329        self.0.height()
330    }
331
332    /// Number of tiles in the Z axis
333    fn nz(&self) -> u32 {
334        self.0.depth()
335    }
336
337    /// Number of voxels in the X axis (always a multiple of 64)
338    fn width(&self) -> u32 {
339        self.0.width() * 64
340    }
341
342    /// Number of voxels in the Y axis (always a multiple of 64)
343    fn height(&self) -> u32 {
344        self.0.height() * 64
345    }
346
347    /// Number of voxels in the Z axis (always a multiple of 64)
348    fn depth(&self) -> u32 {
349        self.0.depth() * 64
350    }
351
352    /// Number of pixels in total
353    fn pixels(&self) -> usize {
354        self.width() as usize * self.height() as usize
355    }
356}
357
358/// Number of [`TapeWord`] words in the tape data flexible array
359const TAPE_DATA_CAPACITY: usize = 8 * 1024 * 1024; // 8M words, 64 MiB
360
361#[repr(C)]
362struct TapeWord {
363    op: u32,
364    imm: u32,
365}
366
367/// Returns a shader for interval root tiles
368fn interval_root_shader(reg_count: u8) -> String {
369    let mut shader_code = opcode_constants();
370    shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
371    shader_code += INTERVAL_ROOT_SHADER;
372    shader_code += INTERVAL_OPS_SHADER;
373    shader_code += COMMON_SHADER;
374    shader_code += crate::COMMON_SHADER;
375    shader_code += TAPE_INTERPRETER;
376    shader_code += STACK_SHADER;
377    shader_code += TAPE_SIMPLIFY;
378    shader_code
379}
380
381/// Returns a shader for interval root tile repacking
382fn repack_shader() -> String {
383    let mut shader_code = String::new();
384    shader_code += REPACK_SHADER;
385    shader_code += COMMON_SHADER;
386    shader_code += crate::COMMON_SHADER;
387    shader_code
388}
389
390/// Returns a shader for interval tile sorting
391fn sort_shader() -> String {
392    let mut shader_code = String::new();
393    shader_code += SORT_SHADER;
394    shader_code += COMMON_SHADER;
395    shader_code += crate::COMMON_SHADER;
396    shader_code
397}
398
399/// Returns a shader for interval tile evaluation
400fn interval_tiles_shader(reg_count: u8) -> String {
401    let mut shader_code = opcode_constants();
402    shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
403    shader_code += INTERVAL_TILES_SHADER;
404    shader_code += INTERVAL_OPS_SHADER;
405    shader_code += COMMON_SHADER;
406    shader_code += crate::COMMON_SHADER;
407    shader_code += TAPE_INTERPRETER;
408    shader_code += STACK_SHADER;
409    shader_code += TAPE_SIMPLIFY;
410    shader_code
411}
412
413/// Returns a shader for voxel tile evaluation
414fn voxel_tiles_shader(reg_count: u8) -> String {
415    let mut shader_code = opcode_constants();
416    shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
417    shader_code += VOXEL_TILES_SHADER;
418    shader_code += COMMON_SHADER;
419    shader_code += crate::COMMON_SHADER;
420    shader_code += TAPE_INTERPRETER;
421    shader_code += DUMMY_STACK_SHADER;
422    shader_code
423}
424
425/// Returns a shader for normals evaluation
426fn normals_shader(reg_count: u8) -> String {
427    let mut shader_code = opcode_constants();
428    shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
429    shader_code += NORMALS_SHADER;
430    shader_code += COMMON_SHADER;
431    shader_code += crate::COMMON_SHADER;
432    shader_code += TAPE_INTERPRETER;
433    shader_code += DUMMY_STACK_SHADER;
434    shader_code
435}
436
437/// Returns a shader for merging images
438fn merge_shader() -> String {
439    MERGE_SHADER.to_owned() + COMMON_SHADER + crate::COMMON_SHADER
440}
441
442/// Returns a shader for clearing counters in between strata passes
443fn clear_shader() -> String {
444    CLEAR_SHADER.to_owned() + COMMON_SHADER + crate::COMMON_SHADER
445}
446
447////////////////////////////////////////////////////////////////////////////////
448
449/// Container of multiple pipelines, parameterized by register count
450struct RegPipeline(BTreeMap<u8, wgpu::ComputePipeline>);
451
452impl RegPipeline {
453    fn build<F: Fn(u8) -> wgpu::ComputePipeline>(builder: F) -> Self {
454        let mut out = BTreeMap::new();
455        for reg_count in [8, 16, 32, 64, 128, 192, 255] {
456            out.insert(reg_count, builder(reg_count));
457        }
458        Self(out)
459    }
460
461    /// Returns the pipeline with sufficient registers to render `reg_count`
462    ///
463    /// # Panics
464    /// If `reg_count` is 256 (which is not allowed in bytecode tapes)
465    fn get(&self, reg_count: u8) -> &wgpu::ComputePipeline {
466        let (r, v) = self
467            .0
468            .range(reg_count..)
469            .next()
470            .expect("bytecode tape cannot use more than 255 registers");
471        assert!(*r >= reg_count);
472        v
473    }
474}
475
476/// Root context, which produces a list of 64³ tiles
477struct RootContext {
478    /// Pipelines for 64³ tile evaluation
479    root_pipeline: RegPipeline,
480
481    /// Bind group layout
482    bind_group_layout: wgpu::BindGroupLayout,
483}
484
485/// Per-strata offset in the root tiles list
486///
487/// This must be equivalent to `strata_size_bytes` in the interval root shader
488fn strata_size_bytes(render_size: TileRenderSize) -> usize {
489    let nx = usize::try_from(render_size.nx()).unwrap();
490    let ny = usize::try_from(render_size.ny()).unwrap();
491    // Snap to `min_storage_buffer_offset_alignment`
492    ((nx * ny + 4) * std::mem::size_of::<u32>()).next_multiple_of(256)
493}
494
495impl RootContext {
496    fn new(
497        device: &wgpu::Device,
498        common_bind_group_layout: &wgpu::BindGroupLayout,
499        vars_bind_group_layout: &wgpu::BindGroupLayout,
500    ) -> Self {
501        // Create bind group layout and bind group
502        let bind_group_layout =
503            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
504                label: None,
505                entries: &[
506                    buffer_rw(0), // tiles_out
507                    buffer_rw(1), // tile64_zmax
508                ],
509            });
510
511        let root_pipeline = RegPipeline::build(|reg_count| {
512            let shader_code = interval_root_shader(reg_count);
513            let pipeline_layout = device.create_pipeline_layout(
514                &wgpu::PipelineLayoutDescriptor {
515                    label: None,
516                    bind_group_layouts: &[
517                        Some(common_bind_group_layout),
518                        Some(vars_bind_group_layout),
519                        Some(&bind_group_layout),
520                    ],
521                    immediate_size: 0u32,
522                },
523            );
524            let shader_module =
525                device.create_shader_module(wgpu::ShaderModuleDescriptor {
526                    label: None,
527                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
528                });
529            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
530                label: Some(&format!("interval root ({reg_count})")),
531                layout: Some(&pipeline_layout),
532                module: &shader_module,
533                entry_point: Some("interval_root_main"),
534                compilation_options: Default::default(),
535                cache: None,
536            })
537        });
538
539        Self {
540            bind_group_layout,
541            root_pipeline,
542        }
543    }
544
545    fn run(
546        &self,
547        ctx: &Context,
548        buffers: &Buffers,
549        reg_count: u8,
550        render_size: TileRenderSize,
551        compute_pass: &mut wgpu::ComputePass,
552    ) {
553        let bind_group = buffers.bind_groups.root(ctx, buffers);
554        compute_pass.set_pipeline(self.root_pipeline.get(reg_count));
555        compute_pass.set_bind_group(2, bind_group, &[]);
556
557        // Workgroup is 4x4x4, so we divide by 4 here on each axis
558        let nx = render_size.nx().div_ceil(4);
559        let ny = render_size.ny().div_ceil(4);
560        let nz = render_size.nz().div_ceil(4);
561        compute_pass.dispatch_workgroups(nx, ny, nz);
562    }
563}
564
565/// Repack context, which strata-sorts a list of 64³ tiles
566struct RepackContext {
567    /// Pipeline for 64³ tile packing
568    repack_pipeline: wgpu::ComputePipeline,
569
570    /// Bind group layout
571    bind_group_layout: wgpu::BindGroupLayout,
572}
573
574impl RepackContext {
575    fn new(
576        device: &wgpu::Device,
577        common_bind_group_layout: &wgpu::BindGroupLayout,
578        vars_bind_group_layout: &wgpu::BindGroupLayout,
579    ) -> Self {
580        // Create bind group layout and bind group
581        let bind_group_layout =
582            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
583                label: None,
584                entries: &[
585                    buffer_ro(0), // tiles_out
586                    buffer_ro(1), // tile64_zmin
587                    buffer_rw(2), // strata_tiles
588                ],
589            });
590
591        let repack_pipeline = {
592            let shader_code = repack_shader();
593            let pipeline_layout = device.create_pipeline_layout(
594                &wgpu::PipelineLayoutDescriptor {
595                    label: None,
596                    bind_group_layouts: &[
597                        Some(common_bind_group_layout),
598                        Some(vars_bind_group_layout),
599                        Some(&bind_group_layout),
600                    ],
601                    immediate_size: 0u32,
602                },
603            );
604            let shader_module =
605                device.create_shader_module(wgpu::ShaderModuleDescriptor {
606                    label: None,
607                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
608                });
609            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
610                label: Some("repack"),
611                layout: Some(&pipeline_layout),
612                module: &shader_module,
613                entry_point: Some("repack_main"),
614                compilation_options: Default::default(),
615                cache: None,
616            })
617        };
618
619        Self {
620            bind_group_layout,
621            repack_pipeline,
622        }
623    }
624
625    fn run(
626        &self,
627        ctx: &Context,
628        buffers: &Buffers,
629        render_size: TileRenderSize,
630        compute_pass: &mut wgpu::ComputePass,
631    ) {
632        let bind_group = buffers.bind_groups.repack(ctx, buffers);
633
634        compute_pass.set_pipeline(&self.repack_pipeline);
635        compute_pass.set_bind_group(2, bind_group, &[]);
636
637        // Workgroup is 64x1x1, so we divide on the X axis.  It doesn't matter
638        // much; we just need one thread per possible output tile from the
639        // previous stage, i.e. `(nx * ny * nz)` total threads.  This could be
640        // optimized further with indirect dispatch, but ehhhhhhh
641        let nx = render_size.nx().div_ceil(64);
642        let ny = render_size.ny();
643        let nz = render_size.nz();
644        compute_pass.dispatch_workgroups(nx, ny, nz);
645    }
646}
647
648////////////////////////////////////////////////////////////////////////////////
649
650struct IntervalContext {
651    /// Pipeline for 64³ -> 16³ tile evaluation
652    interval64_pipeline: RegPipeline,
653
654    /// Pipeline to sort 16³ tiles
655    sort16_pipeline: wgpu::ComputePipeline,
656
657    /// Pipeline for 16³ -> 4³ tile evaluation
658    interval16_pipeline: RegPipeline,
659
660    /// Pipeline to sort 4³ tiles
661    sort4_pipeline: wgpu::ComputePipeline,
662
663    /// Bind group layout for interval pipelines
664    interval_bind_group_layout: wgpu::BindGroupLayout,
665
666    /// Bind group layout for sort pipelines
667    sort_bind_group_layout: wgpu::BindGroupLayout,
668}
669
670impl IntervalContext {
671    fn new(
672        device: &wgpu::Device,
673        common_bind_group_layout: &wgpu::BindGroupLayout,
674        vars_bind_group_layout: &wgpu::BindGroupLayout,
675    ) -> Self {
676        // Create bind group layout and bind group
677        let interval_bind_group_layout =
678            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
679                label: None,
680                entries: &[
681                    buffer_ro_dyn(0), // tiles_in
682                    buffer_ro(1),     // tile_zmin
683                    buffer_rw(2),     // subtiles_out
684                    buffer_rw(3),     // subtile_zmin
685                    buffer_rw(4),     // subtile_zhist
686                ],
687            });
688
689        let interval_pipeline_layout =
690            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
691                label: Some("interval pipeline layout"),
692                bind_group_layouts: &[
693                    Some(common_bind_group_layout),
694                    Some(vars_bind_group_layout),
695                    Some(&interval_bind_group_layout),
696                ],
697                immediate_size: 0u32,
698            });
699
700        let interval64_pipeline = RegPipeline::build(|reg_count| {
701            let shader_code = interval_tiles_shader(reg_count);
702            // SAFETY: the shader is carefully written
703            let shader_module = unsafe {
704                device.create_shader_module_trusted(
705                    wgpu::ShaderModuleDescriptor {
706                        label: Some(&format!(
707                            "interval64 tiles shader ({reg_count})"
708                        )),
709                        source: wgpu::ShaderSource::Wgsl(shader_code.into()),
710                    },
711                    wgpu::ShaderRuntimeChecks {
712                        bounds_checks: false,
713                        force_loop_bounding: false,
714                        ray_query_initialization_tracking: false,
715                        task_shader_dispatch_tracking: false,
716                        mesh_shader_primitive_indices_clamp: false,
717                    },
718                )
719            };
720            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
721                label: Some(&format!("interval64 ({reg_count})")),
722                layout: Some(&interval_pipeline_layout),
723                module: &shader_module,
724                entry_point: Some("interval_tile_main"),
725                compilation_options: wgpu::PipelineCompilationOptions {
726                    constants: &[("TILE_SIZE", 64.0), ("SUBTILE_SIZE", 16.0)],
727                    ..Default::default()
728                },
729                cache: None,
730            })
731        });
732
733        let interval16_pipeline = RegPipeline::build(|reg_count| {
734            let shader_code = interval_tiles_shader(reg_count);
735            // SAFETY: the shader is carefully written
736            let shader_module = unsafe {
737                device.create_shader_module_trusted(
738                    wgpu::ShaderModuleDescriptor {
739                        label: Some(&format!(
740                            "interval16 tiles shader ({reg_count})"
741                        )),
742                        source: wgpu::ShaderSource::Wgsl(shader_code.into()),
743                    },
744                    wgpu::ShaderRuntimeChecks {
745                        bounds_checks: false,
746                        force_loop_bounding: false,
747                        ray_query_initialization_tracking: false,
748                        task_shader_dispatch_tracking: false,
749                        mesh_shader_primitive_indices_clamp: false,
750                    },
751                )
752            };
753            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
754                label: Some(&format!("interval16 ({reg_count})")),
755                layout: Some(&interval_pipeline_layout),
756                module: &shader_module,
757                entry_point: Some("interval_tile_main"),
758                compilation_options: wgpu::PipelineCompilationOptions {
759                    constants: &[("TILE_SIZE", 16.0), ("SUBTILE_SIZE", 4.0)],
760                    ..Default::default()
761                },
762                cache: None,
763            })
764        });
765
766        let sort_bind_group_layout =
767            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
768                label: Some("sort bind group layout"),
769                entries: &[
770                    buffer_ro(0), // subtiles_out
771                    buffer_rw(1), // z_hist
772                    buffer_rw(2), // sorted_subtiles
773                ],
774            });
775        let sort_pipeline_layout =
776            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
777                label: Some("sort pipeline layout"),
778                bind_group_layouts: &[
779                    Some(common_bind_group_layout),
780                    Some(vars_bind_group_layout),
781                    Some(&sort_bind_group_layout),
782                ],
783                immediate_size: 0u32,
784            });
785
786        let shader_code = sort_shader();
787        // SAFETY: the shader is carefully written
788        let shader_module = unsafe {
789            device.create_shader_module_trusted(
790                wgpu::ShaderModuleDescriptor {
791                    label: Some("sort shader module"),
792                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
793                },
794                wgpu::ShaderRuntimeChecks {
795                    bounds_checks: false,
796                    force_loop_bounding: false,
797                    ray_query_initialization_tracking: false,
798                    task_shader_dispatch_tracking: false,
799                    mesh_shader_primitive_indices_clamp: false,
800                },
801            )
802        };
803        let sort16_pipeline =
804            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
805                label: Some("sort16"),
806                layout: Some(&sort_pipeline_layout),
807                module: &shader_module,
808                entry_point: Some("sort_main"),
809                compilation_options: wgpu::PipelineCompilationOptions {
810                    constants: &[("SUBTILE_SIZE", 16.0)],
811                    ..Default::default()
812                },
813                cache: None,
814            });
815        let sort4_pipeline =
816            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
817                label: Some("sort4"),
818                layout: Some(&sort_pipeline_layout),
819                module: &shader_module,
820                entry_point: Some("sort_main"),
821                compilation_options: wgpu::PipelineCompilationOptions {
822                    constants: &[("SUBTILE_SIZE", 4.0)],
823                    ..Default::default()
824                },
825                cache: None,
826            });
827
828        Self {
829            interval_bind_group_layout,
830            sort_bind_group_layout,
831            interval64_pipeline,
832            sort16_pipeline,
833            interval16_pipeline,
834            sort4_pipeline,
835        }
836    }
837
838    fn run(
839        &self,
840        ctx: &Context,
841        buffers: &Buffers,
842        strata: u64,
843        reg_count: u8,
844        compute_pass: &mut wgpu::ComputePass,
845    ) {
846        let strata_bytes = u64::try_from(buffers.strata_size_bytes()).unwrap();
847        let offset_bytes = strata * strata_bytes;
848        let bind_group16 = buffers.bind_groups.interval16(ctx, buffers);
849        compute_pass.set_pipeline(self.interval64_pipeline.get(reg_count));
850        compute_pass.set_bind_group(
851            2,
852            bind_group16,
853            &[u32::try_from(offset_bytes).unwrap()],
854        );
855        compute_pass.dispatch_workgroups_indirect(
856            buffers.tile64.strata.data(),
857            offset_bytes,
858        );
859
860        let bind_group_sort16 = buffers.bind_groups.sort16(ctx, buffers);
861        compute_pass.set_pipeline(&self.sort16_pipeline);
862        compute_pass.set_bind_group(2, bind_group_sort16, &[]);
863        compute_pass
864            .dispatch_workgroups_indirect(buffers.tile16.tiles.data(), 0);
865
866        let bind_group4 = buffers.bind_groups.interval4(ctx, buffers);
867        compute_pass.set_pipeline(self.interval16_pipeline.get(reg_count));
868        compute_pass.set_bind_group(2, bind_group4, &[0]);
869        compute_pass
870            .dispatch_workgroups_indirect(buffers.tile16.sorted.data(), 0);
871
872        let bind_group_sort4 = buffers.bind_groups.sort4(ctx, buffers);
873        compute_pass.set_pipeline(&self.sort4_pipeline);
874        compute_pass.set_bind_group(2, bind_group_sort4, &[]);
875        compute_pass
876            .dispatch_workgroups_indirect(buffers.tile4.tiles.data(), 0);
877    }
878}
879
880////////////////////////////////////////////////////////////////////////////////
881
882struct VoxelContext {
883    /// Bind group layout
884    bind_group_layout: wgpu::BindGroupLayout,
885
886    /// Pipeline for interpreted voxel evaluation
887    voxel_pipeline: RegPipeline,
888}
889
890impl VoxelContext {
891    fn new(
892        device: &wgpu::Device,
893        common_bind_group_layout: &wgpu::BindGroupLayout,
894        vars_bind_group_layout: &wgpu::BindGroupLayout,
895    ) -> Self {
896        // Create bind group layout and bind group
897        let bind_group_layout =
898            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
899                label: Some("voxel bind group layout"),
900                entries: &[
901                    buffer_ro(0), // tiles4_in
902                    buffer_ro(1), // tile4_zmin
903                    buffer_rw(2), // result
904                ],
905            });
906
907        let voxel_pipeline = RegPipeline::build(|reg_count| {
908            let shader_code = voxel_tiles_shader(reg_count);
909            let pipeline_layout = device.create_pipeline_layout(
910                &wgpu::PipelineLayoutDescriptor {
911                    label: Some("voxel pipeline layout"),
912                    bind_group_layouts: &[
913                        Some(common_bind_group_layout),
914                        Some(vars_bind_group_layout),
915                        Some(&bind_group_layout),
916                    ],
917                    immediate_size: 0u32,
918                },
919            );
920            // SAFETY: The shader is careful, good luck
921            let shader_module = unsafe {
922                device.create_shader_module_trusted(
923                    wgpu::ShaderModuleDescriptor {
924                        label: Some("voxel shader module"),
925                        source: wgpu::ShaderSource::Wgsl(shader_code.into()),
926                    },
927                    wgpu::ShaderRuntimeChecks {
928                        bounds_checks: false,
929                        force_loop_bounding: false,
930                        ray_query_initialization_tracking: false,
931                        task_shader_dispatch_tracking: false,
932                        mesh_shader_primitive_indices_clamp: false,
933                    },
934                )
935            };
936            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
937                label: Some(&format!("voxels ({reg_count})")),
938                layout: Some(&pipeline_layout),
939                module: &shader_module,
940                entry_point: Some("voxel_ray_main"),
941                compilation_options: Default::default(),
942                cache: None,
943            })
944        });
945
946        Self {
947            bind_group_layout,
948            voxel_pipeline,
949        }
950    }
951
952    fn run(
953        &self,
954        ctx: &Context,
955        buffers: &Buffers,
956        reg_count: u8,
957        compute_pass: &mut wgpu::ComputePass,
958    ) {
959        let bind_group = buffers.bind_groups.voxel(ctx, buffers);
960        compute_pass.set_pipeline(self.voxel_pipeline.get(reg_count));
961        compute_pass.set_bind_group(2, bind_group, &[]);
962
963        // Each workgroup is 4x4x4, i.e. covering a 4x4 splat of pixels with 4x
964        // workers in the Z direction.
965        compute_pass
966            .dispatch_workgroups_indirect(buffers.tile4.sorted.data(), 0);
967    }
968}
969
970struct NormalsContext {
971    /// Bind group layout
972    bind_group_layout: wgpu::BindGroupLayout,
973
974    /// Pipeline for normal evaluation
975    normals_pipeline: RegPipeline,
976}
977
978impl NormalsContext {
979    fn new(
980        device: &wgpu::Device,
981        common_bind_group_layout: &wgpu::BindGroupLayout,
982        vars_bind_group_layout: &wgpu::BindGroupLayout,
983    ) -> Self {
984        // Create bind group layout and bind group
985        let bind_group_layout =
986            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
987                label: Some("normals bind group layout"),
988                entries: &[
989                    buffer_ro(0), // image_heightmap
990                    buffer_rw(1), // image_out
991                ],
992            });
993
994        let normals_pipeline = RegPipeline::build(|reg_count| {
995            let shader_code = normals_shader(reg_count);
996            let pipeline_layout = device.create_pipeline_layout(
997                &wgpu::PipelineLayoutDescriptor {
998                    label: Some("normals pipeline"),
999                    bind_group_layouts: &[
1000                        Some(common_bind_group_layout),
1001                        Some(vars_bind_group_layout),
1002                        Some(&bind_group_layout),
1003                    ],
1004                    immediate_size: 0u32,
1005                },
1006            );
1007            let shader_module =
1008                device.create_shader_module(wgpu::ShaderModuleDescriptor {
1009                    label: Some("normals shader module"),
1010                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
1011                });
1012            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1013                label: Some(&format!("normals ({reg_count})")),
1014                layout: Some(&pipeline_layout),
1015                module: &shader_module,
1016                entry_point: Some("normals_main"),
1017                compilation_options: Default::default(),
1018                cache: None,
1019            })
1020        });
1021
1022        Self {
1023            bind_group_layout,
1024            normals_pipeline,
1025        }
1026    }
1027
1028    fn run(
1029        &self,
1030        ctx: &Context,
1031        buffers: &Buffers,
1032        reg_count: u8,
1033        compute_pass: &mut wgpu::ComputePass,
1034    ) {
1035        let bind_group = buffers.bind_groups.normals(ctx, buffers);
1036        compute_pass.set_pipeline(self.normals_pipeline.get(reg_count));
1037        compute_pass.set_bind_group(2, bind_group, &[]);
1038
1039        compute_pass.dispatch_workgroups(
1040            buffers.image_size.width().div_ceil(8),
1041            buffers.image_size.height().div_ceil(8),
1042            1,
1043        );
1044    }
1045}
1046
1047////////////////////////////////////////////////////////////////////////////////
1048
1049/// Context for 3D (combined heightmap and normal) rendering
1050pub struct Context {
1051    gpu: Gpu,
1052    has_timestamps: bool,
1053
1054    /// Bind group layout for the common bind group (used by all stages)
1055    common_bind_group_layout: wgpu::BindGroupLayout,
1056
1057    /// Bind group layout for the vars bind group (also by all stages)
1058    vars_bind_group_layout: wgpu::BindGroupLayout,
1059
1060    root_ctx: RootContext,
1061    repack_ctx: RepackContext,
1062    interval_ctx: IntervalContext,
1063    voxel_ctx: VoxelContext,
1064    normals_ctx: NormalsContext,
1065    merge_ctx: MergeContext,
1066    reset_ctx: ResetContext,
1067    clear_ctx: ClearContext,
1068}
1069
1070tag!(TilesBufferTag, u32, STORAGE | INDIRECT);
1071tag!(SortedBufferTag, u32, STORAGE | INDIRECT);
1072tag!(ZminBufferTag, u32, STORAGE | COPY_DST);
1073
1074struct TileBuffers<const N: u64> {
1075    /// Tiles written by the stage outputting N³ tiles
1076    tiles: ArrayBuffer<TilesBufferTag>,
1077    /// Sorted version of [`tiles`](Self::tiles)
1078    sorted: ArrayBuffer<SortedBufferTag>,
1079    /// Minimum Z height at each XY tile
1080    zmin: ImageBuffer<ZminBufferTag>,
1081}
1082
1083impl<const N: u64> TileBuffers<N> {
1084    /// Returns a new `TileBuffers` object
1085    fn new(
1086        device: &wgpu::Device,
1087        render_size: TileRenderSize,
1088    ) -> Result<Self, TileBuffersError> {
1089        let tile_buf_size = Self::tile_buf_size(render_size);
1090        let tiles =
1091            ArrayBuffer::new(device, format!("active_tile{N}"), tile_buf_size)
1092                .map_err(|err| TileBuffersError {
1093                    buf: TileBufferName::Tiles,
1094                    err,
1095                })?;
1096        let sorted =
1097            ArrayBuffer::new(device, format!("sorted_tile{N}"), tile_buf_size)
1098                .map_err(|err| TileBuffersError {
1099                    buf: TileBufferName::Sorted,
1100                    err,
1101                })?;
1102        let zmin = ImageBuffer::new(
1103            device,
1104            format!("tile{N}_zmin"),
1105            Self::zmin_buf_size(render_size),
1106        )
1107        .map_err(|err| TileBuffersError {
1108            buf: TileBufferName::Zmin,
1109            err,
1110        })?;
1111
1112        Ok(Self {
1113            tiles,
1114            sorted,
1115            zmin,
1116        })
1117    }
1118
1119    fn tile_buf_size(render_size: TileRenderSize) -> usize {
1120        let n = usize::try_from(N).unwrap();
1121        let nx = usize::try_from(render_size.width()).unwrap() / n;
1122        let ny = usize::try_from(render_size.height()).unwrap() / n;
1123        let nz = 64 / n;
1124        // wg_dispatch: [u32; 3]
1125        // count: u32,
1126        4 + nx * ny * nz
1127    }
1128
1129    fn zmin_buf_size(render_size: TileRenderSize) -> ImageSize {
1130        ImageSize::new(
1131            render_size.width() / u32::try_from(N).unwrap(),
1132            render_size.height() / u32::try_from(N).unwrap(),
1133        )
1134    }
1135
1136    fn grow_to_fit(
1137        &mut self,
1138        device: &wgpu::Device,
1139        render_size: TileRenderSize,
1140    ) -> Result<(), TileBuffersError> {
1141        let TileBuffers {
1142            tiles,
1143            sorted,
1144            zmin,
1145        } = self;
1146        let tile_buf_size = Self::tile_buf_size(render_size);
1147        tiles.grow_to_fit(device, tile_buf_size).map_err(|err| {
1148            TileBuffersError {
1149                buf: TileBufferName::Tiles,
1150                err,
1151            }
1152        })?;
1153        sorted.grow_to_fit(device, tile_buf_size).map_err(|err| {
1154            TileBuffersError {
1155                buf: TileBufferName::Sorted,
1156                err,
1157            }
1158        })?;
1159        zmin.grow_to_fit(device, Self::zmin_buf_size(render_size))
1160            .map_err(|err| TileBuffersError {
1161                buf: TileBufferName::Zmin,
1162                err,
1163            })?;
1164
1165        Ok(())
1166    }
1167
1168    /// Returns the number of bytes in use by these buffers
1169    ///
1170    /// See [`self.capacity`](Self::capacity) for total bytes allocated
1171    pub fn size(&self) -> u64 {
1172        // Destructure to make sure we take all members into account
1173        let TileBuffers {
1174            tiles,
1175            sorted,
1176            zmin,
1177        } = self;
1178        tiles.size_bytes() + sorted.size_bytes() + zmin.size_bytes()
1179    }
1180
1181    /// Returns the number of bytes allocated by these buffers
1182    pub fn capacity(&self) -> u64 {
1183        // Destructure to make sure we take all members into account
1184        let TileBuffers {
1185            tiles,
1186            sorted,
1187            zmin,
1188        } = self;
1189        tiles.capacity() + sorted.capacity() + zmin.capacity()
1190    }
1191}
1192
1193tag!(RootTilesBufferTag, u32, STORAGE | COPY_DST);
1194tag!(RootStrataBufferTag, u8, STORAGE | INDIRECT | COPY_DST);
1195tag!(RootZminBufferTag, u32, STORAGE | COPY_DST);
1196tag!(RootZmaxBufferTag, u32, STORAGE | COPY_DST);
1197
1198/// Root tile buffers store strata-packed tile lists
1199struct RootTileBuffers {
1200    /// Initial output tiles
1201    tiles: ArrayBuffer<RootTilesBufferTag>,
1202    /// Strata-sorted output tiles
1203    strata: ArrayBuffer<RootStrataBufferTag>,
1204    zmin: ImageBuffer<RootZminBufferTag>,
1205    zmax: ImageBuffer<RootZmaxBufferTag>,
1206}
1207
1208impl RootTileBuffers {
1209    /// Build a new root tiles buffer, which stores strata-packed tile lists
1210    fn new(
1211        device: &wgpu::Device,
1212        render_size: TileRenderSize,
1213    ) -> Result<Self, RootTileBuffersError> {
1214        // Root tile buffers are always 64³ voxels
1215        const N: usize = 64;
1216
1217        // Allocate enough words to write all of the output tiles
1218        let tiles = ArrayBuffer::new(
1219            device,
1220            format!("tiles_out{N}"),
1221            Self::tiles_buf_size(render_size),
1222        )
1223        .map_err(|err| RootTileBuffersError {
1224            buf: RootTileBufferName::Tiles,
1225            err,
1226        })?;
1227
1228        let strata = ArrayBuffer::new(
1229            device,
1230            format!("strata_tile{N}"),
1231            Self::strata_buf_size(render_size),
1232        )
1233        .map_err(|err| RootTileBuffersError {
1234            buf: RootTileBufferName::Strata,
1235            err,
1236        })?;
1237
1238        let z_buf_size = Self::z_buf_size(render_size);
1239        let zmin =
1240            ImageBuffer::new(device, format!("tile{N}_zmin"), z_buf_size)
1241                .map_err(|err| RootTileBuffersError {
1242                    buf: RootTileBufferName::Zmin,
1243                    err,
1244                })?;
1245        let zmax =
1246            ImageBuffer::new(device, format!("tile{N}_zmax"), z_buf_size)
1247                .map_err(|err| RootTileBuffersError {
1248                    buf: RootTileBufferName::Zmax,
1249                    err,
1250                })?;
1251        Ok(Self {
1252            tiles,
1253            strata,
1254            zmin,
1255            zmax,
1256        })
1257    }
1258
1259    fn tiles_buf_size(render_size: TileRenderSize) -> usize {
1260        let nx = usize::try_from(render_size.nx()).unwrap();
1261        let ny = usize::try_from(render_size.ny()).unwrap();
1262        let nz = usize::try_from(render_size.nz()).unwrap();
1263        // wg_dispatch: [u32; 3] (unused)
1264        // count: u32,
1265        4 + nx * ny * nz
1266    }
1267
1268    fn strata_buf_size(render_size: TileRenderSize) -> usize {
1269        let nz = usize::try_from(render_size.nz()).unwrap();
1270        let strata_size = strata_size_bytes(render_size);
1271        strata_size * nz
1272    }
1273
1274    fn z_buf_size(render_size: TileRenderSize) -> ImageSize {
1275        ImageSize::new(render_size.nx(), render_size.ny())
1276    }
1277
1278    /// Grows all of the buffers to fit a particular render size
1279    fn grow_to_fit(
1280        &mut self,
1281        device: &wgpu::Device,
1282        render_size: TileRenderSize,
1283    ) -> Result<(), RootTileBuffersError> {
1284        // Destructure to make sure we take all members into account
1285        let RootTileBuffers {
1286            tiles,
1287            strata,
1288            zmin,
1289            zmax,
1290        } = self;
1291        tiles
1292            .grow_to_fit(device, Self::tiles_buf_size(render_size))
1293            .map_err(|err| RootTileBuffersError {
1294                buf: RootTileBufferName::Tiles,
1295                err,
1296            })?;
1297        strata
1298            .grow_to_fit(device, Self::strata_buf_size(render_size))
1299            .map_err(|err| RootTileBuffersError {
1300                buf: RootTileBufferName::Strata,
1301                err,
1302            })?;
1303
1304        let z_buf_size = Self::z_buf_size(render_size);
1305        zmin.grow_to_fit(device, z_buf_size).map_err(|err| {
1306            RootTileBuffersError {
1307                buf: RootTileBufferName::Zmin,
1308                err,
1309            }
1310        })?;
1311        zmax.grow_to_fit(device, z_buf_size).map_err(|err| {
1312            RootTileBuffersError {
1313                buf: RootTileBufferName::Zmax,
1314                err,
1315            }
1316        })?;
1317
1318        Ok(())
1319    }
1320
1321    /// Returns the number of bytes in use by buffers
1322    pub fn size(&self) -> u64 {
1323        // Destructure to make sure we take all members into account
1324        let RootTileBuffers {
1325            tiles,
1326            strata,
1327            zmin,
1328            zmax,
1329        } = self;
1330        tiles.size_bytes()
1331            + strata.size_bytes()
1332            + zmin.size_bytes()
1333            + zmax.size_bytes()
1334    }
1335
1336    /// Returns the number of bytes allocated to buffers
1337    pub fn capacity(&self) -> u64 {
1338        // Destructure to make sure we take all members into account
1339        let RootTileBuffers {
1340            tiles,
1341            strata,
1342            zmin,
1343            zmax,
1344        } = self;
1345        tiles.capacity() + strata.capacity() + zmin.capacity() + zmax.capacity()
1346    }
1347}
1348
1349/// Shape for rendering
1350///
1351/// This object is constructed by [`Context::shape`] and may only be used with
1352/// that particular [`Context`].
1353pub struct RenderShape {
1354    /// Copy of our shape (kept around for access to the variable map)
1355    shape: VmShape,
1356    /// Map from X, Y, Z (by index) to the variable slot
1357    axes: [u32; 3],
1358    /// Serialized bytecode for the shape
1359    bytecode: Bytecode,
1360    /// GPU buffer to contain variables
1361    ///
1362    /// This doesn't live in [`Buffers`] because it's dynamically sized based on
1363    /// the shape; everything in `Buffers` is based on image size.
1364    vars: wgpu::Buffer,
1365    /// Lazily-constructed bind group for the vars array
1366    ///
1367    /// This is not cached in a buffer-specific [`BindGroups`] object because it
1368    /// is shape-specific.
1369    vars_bind_group: std::cell::OnceCell<wgpu::BindGroup>,
1370}
1371
1372/// Error type when constructing a [`RenderShape`]
1373#[derive(Debug, thiserror::Error)]
1374pub enum RenderShapeError {
1375    /// The shape doesn't fit in the GPU tape buffer
1376    #[error(
1377        "shape bytecode is {0} tape words (8 bytes each), which exceeds \
1378        buffer capacity of {TAPE_DATA_CAPACITY} tape words"
1379    )]
1380    TooLong(usize),
1381    /// The shape uses a reserved register
1382    #[error(transparent)]
1383    RegisterError(#[from] ReservedRegister),
1384}
1385
1386impl RenderShape {
1387    fn new(
1388        shape: &VmShape,
1389        device: &wgpu::Device,
1390    ) -> Result<Self, RenderShapeError> {
1391        // Generate bytecode for the root tape
1392        let bytecode = Bytecode::new(shape.inner().data())?;
1393        if bytecode.len() / 2 > TAPE_DATA_CAPACITY {
1394            return Err(RenderShapeError::TooLong(bytecode.len() / 2));
1395        }
1396
1397        // Create the 4x4 transform matrix
1398        let vars = shape.inner().vars();
1399        let axes = [Var::X, Var::Y, Var::Z]
1400            .map(|a| vars.get(&a).map(|v| v as u32).unwrap_or(u32::MAX));
1401
1402        // Build a buffer for non-XYZ vars.  This buffer includes slots for XYZ
1403        // as well, but we special-case them in evaluation.
1404        let vars = device.create_buffer(&wgpu::BufferDescriptor {
1405            label: Some("vars"),
1406            size: u64::try_from(std::mem::size_of::<f32>() * vars.len())
1407                .unwrap(),
1408            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1409            mapped_at_creation: false,
1410        });
1411
1412        Ok(Self {
1413            shape: shape.clone(),
1414            axes,
1415            bytecode,
1416            vars,
1417            vars_bind_group: Default::default(),
1418        })
1419    }
1420
1421    fn vars_bind_group(&self, ctx: &Context) -> &wgpu::BindGroup {
1422        self.vars_bind_group.get_or_init(|| {
1423            ctx.gpu
1424                .device
1425                .create_bind_group(&wgpu::BindGroupDescriptor {
1426                    label: Some("vars bind group"),
1427                    layout: &ctx.vars_bind_group_layout,
1428                    entries: &[wgpu::BindGroupEntry {
1429                        binding: 0,
1430                        resource: self.vars.as_entire_binding(),
1431                    }],
1432                })
1433        })
1434    }
1435}
1436
1437tag!(TileTapesBufferTag, u32, STORAGE | COPY_DST);
1438tag!(VoxelsBufferTag, u32, STORAGE | COPY_DST);
1439tag!(pub GeomBufferTag, GeometryPixel, STORAGE | COPY_SRC | COPY_DST,
1440    "Tag for a on-GPU buffer storing [`GeometryPixel`] values");
1441
1442/// Buffers for rendering, which control the rendered image size
1443///
1444/// This object is constructed by [`Context::buffers`] and may only be used with
1445/// that particular [`Context`].
1446///
1447/// A successfully constructed `Buffers` object also guarantees infallible
1448/// construction of an [`ImageReadBuffer`] object of the same size.
1449pub struct Buffers {
1450    /// Image render size
1451    ///
1452    /// Note that the tile buffers below round up to the nearest root tile
1453    /// (64³ voxels).
1454    image_size: VoxelSize,
1455
1456    /// Config and tape data buffer (constant size)
1457    config_buf: wgpu::Buffer,
1458
1459    /// Buffer for z histogram counters
1460    ///
1461    /// This is laid out as follows:
1462    ///
1463    /// - 4 `u32` words (for the 16³ pass)
1464    /// - 240 bytes of padding
1465    /// - 16 `u32` words (for the 4³ pass)
1466    z_hist_buf: wgpu::Buffer,
1467
1468    /// Map from tile to the relevant tape (as a start index)
1469    tile_tapes: ArrayBuffer<TileTapesBufferTag>,
1470
1471    /// Root tile Z heights (64³)
1472    tile64: RootTileBuffers,
1473
1474    /// Z heights for filled first-stage tiles (16³)
1475    tile16: TileBuffers<16>,
1476
1477    /// Z heights for filled second-stage tiles (4³)
1478    tile4: TileBuffers<4>,
1479
1480    /// Z heights for voxels
1481    voxels: ArrayBuffer<VoxelsBufferTag>,
1482
1483    /// Buffer of [`GeometryPixel`] data, generated by the normal pass
1484    geom: ImageBuffer<GeomBufferTag>,
1485
1486    /// Query set for timestamps
1487    ///
1488    /// This must be present if and only if the parent context has timestamps
1489    /// enabled (per [`Context::has_timestamps`])
1490    timestamps: Option<wgpu::QuerySet>,
1491
1492    /// Buffer into which we resolve the timestamp query
1493    ts_buf: wgpu::Buffer,
1494
1495    /// Cached bind groups
1496    bind_groups: BindGroups,
1497}
1498
1499/// Buffer for reading data back from the GPU
1500///
1501/// This object is constructed by [`Context::image_buffer`] and may only be used
1502/// with that particular [`Context`].
1503///
1504/// Once mapped, this is wrapped by a [`MappedImage`]
1505pub struct ImageReadBuffer {
1506    /// Image render size
1507    image_size: VoxelSize,
1508
1509    /// Result buffer that can be read back from the CPU
1510    ///
1511    /// This is mostly image pixels (as [`GeometryPixel`] values), but also
1512    /// contains two trailing `u64` values for timestamps.
1513    buffer: ImageReadArrayBuffer,
1514}
1515
1516impl ImageReadBuffer {
1517    fn new(
1518        device: &wgpu::Device,
1519        name: String,
1520        image_size: VoxelSize,
1521    ) -> Result<Self, BufferSizeError> {
1522        Ok(Self {
1523            image_size,
1524            buffer: ImageReadArrayBuffer::new(
1525                device,
1526                name,
1527                Buffers::image_buf_size(image_size),
1528            )?,
1529        })
1530    }
1531
1532    fn grow_to_fit(
1533        &mut self,
1534        device: &wgpu::Device,
1535        image_size: VoxelSize,
1536    ) -> Result<(), BufferSizeError> {
1537        self.image_size = image_size;
1538        self.buffer
1539            .grow_to_fit(device, Buffers::image_buf_size(image_size))
1540    }
1541}
1542
1543tag!(ImageReadTag, u8, COPY_DST | MAP_READ);
1544type ImageReadArrayBuffer = ArrayBuffer<ImageReadTag>;
1545
1546/// Cached bind groups (constructed on-demand)
1547#[derive(Default)]
1548struct BindGroups {
1549    common: std::cell::OnceCell<wgpu::BindGroup>,
1550    merge: std::cell::OnceCell<wgpu::BindGroup>,
1551    root: std::cell::OnceCell<wgpu::BindGroup>,
1552    repack: std::cell::OnceCell<wgpu::BindGroup>,
1553    interval16: std::cell::OnceCell<wgpu::BindGroup>,
1554    sort16: std::cell::OnceCell<wgpu::BindGroup>,
1555    interval4: std::cell::OnceCell<wgpu::BindGroup>,
1556    sort4: std::cell::OnceCell<wgpu::BindGroup>,
1557    voxel: std::cell::OnceCell<wgpu::BindGroup>,
1558    normals: std::cell::OnceCell<wgpu::BindGroup>,
1559    clear: std::cell::OnceCell<wgpu::BindGroup>,
1560}
1561
1562impl BindGroups {
1563    fn common(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
1564        self.common.get_or_init(|| {
1565            ctx.gpu
1566                .device
1567                .create_bind_group(&wgpu::BindGroupDescriptor {
1568                    label: Some("common bind group"),
1569                    layout: &ctx.common_bind_group_layout,
1570                    entries: &[
1571                        wgpu::BindGroupEntry {
1572                            binding: 0,
1573                            resource: buffers.config_buf.as_entire_binding(),
1574                        },
1575                        wgpu::BindGroupEntry {
1576                            binding: 1,
1577                            resource: buffers.tile_tapes.bind_active(),
1578                        },
1579                    ],
1580                })
1581        })
1582    }
1583
1584    fn clear(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
1585        self.clear.get_or_init(|| {
1586            ctx.gpu
1587                .device
1588                .create_bind_group(&wgpu::BindGroupDescriptor {
1589                    label: Some("clear bind group"),
1590                    layout: &ctx.clear_ctx.bind_group_layout,
1591                    entries: &[
1592                        wgpu::BindGroupEntry {
1593                            binding: 0,
1594                            resource: buffers
1595                                .tile16
1596                                .tiles
1597                                .data()
1598                                .slice(0..16)
1599                                .into(),
1600                        },
1601                        wgpu::BindGroupEntry {
1602                            binding: 1,
1603                            resource: buffers
1604                                .tile16
1605                                .sorted
1606                                .data()
1607                                .slice(0..16)
1608                                .into(),
1609                        },
1610                        wgpu::BindGroupEntry {
1611                            binding: 2,
1612                            resource: buffers
1613                                .tile4
1614                                .tiles
1615                                .data()
1616                                .slice(0..16)
1617                                .into(),
1618                        },
1619                        wgpu::BindGroupEntry {
1620                            binding: 3,
1621                            resource: buffers
1622                                .tile4
1623                                .sorted
1624                                .data()
1625                                .slice(0..16)
1626                                .into(),
1627                        },
1628                        wgpu::BindGroupEntry {
1629                            binding: 4,
1630                            resource: buffers.z_hist_buf.as_entire_binding(),
1631                        },
1632                    ],
1633                })
1634        })
1635    }
1636
1637    fn merge(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
1638        self.merge.get_or_init(|| {
1639            ctx.gpu
1640                .device
1641                .create_bind_group(&wgpu::BindGroupDescriptor {
1642                    label: Some("merge bind group"),
1643                    layout: &ctx.merge_ctx.bind_group_layout,
1644                    entries: &[
1645                        wgpu::BindGroupEntry {
1646                            binding: 0,
1647                            resource: buffers.tile64.zmin.bind_active(),
1648                        },
1649                        wgpu::BindGroupEntry {
1650                            binding: 1,
1651                            resource: buffers.tile16.zmin.bind_active(),
1652                        },
1653                        wgpu::BindGroupEntry {
1654                            binding: 2,
1655                            resource: buffers.tile4.zmin.bind_active(),
1656                        },
1657                        wgpu::BindGroupEntry {
1658                            binding: 3,
1659                            resource: buffers.voxels.bind_active(),
1660                        },
1661                    ],
1662                })
1663        })
1664    }
1665
1666    fn root(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
1667        self.root.get_or_init(|| {
1668            ctx.gpu
1669                .device
1670                .create_bind_group(&wgpu::BindGroupDescriptor {
1671                    label: Some("interval root bind group"),
1672                    layout: &ctx.root_ctx.bind_group_layout,
1673                    entries: &[
1674                        wgpu::BindGroupEntry {
1675                            binding: 0,
1676                            resource: buffers.tile64.tiles.bind_active(),
1677                        },
1678                        wgpu::BindGroupEntry {
1679                            binding: 1,
1680                            resource: buffers.tile64.zmax.bind_active(),
1681                        },
1682                    ],
1683                })
1684        })
1685    }
1686
1687    fn repack(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
1688        self.repack.get_or_init(|| {
1689            ctx.gpu
1690                .device
1691                .create_bind_group(&wgpu::BindGroupDescriptor {
1692                    label: Some("repack bind group"),
1693                    layout: &ctx.repack_ctx.bind_group_layout,
1694                    entries: &[
1695                        wgpu::BindGroupEntry {
1696                            binding: 0,
1697                            resource: buffers.tile64.tiles.bind_active(),
1698                        },
1699                        wgpu::BindGroupEntry {
1700                            binding: 1,
1701                            resource: buffers.tile64.zmax.bind_active(),
1702                        },
1703                        wgpu::BindGroupEntry {
1704                            binding: 2,
1705                            resource: buffers.tile64.strata.bind_active(),
1706                        },
1707                    ],
1708                })
1709        })
1710    }
1711
1712    fn interval16(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
1713        let strata_bytes = u64::try_from(buffers.strata_size_bytes()).unwrap();
1714        self.interval16.get_or_init(|| {
1715            ctx.gpu
1716                .device
1717                .create_bind_group(&wgpu::BindGroupDescriptor {
1718                    label: Some("interval16 bind group"),
1719                    layout: &ctx.interval_ctx.interval_bind_group_layout,
1720                    entries: &[
1721                        wgpu::BindGroupEntry {
1722                            binding: 0,
1723                            resource: buffers
1724                                .tile64
1725                                .strata
1726                                .data()
1727                                .slice(0..strata_bytes) // dynamic offset!
1728                                .into(),
1729                        },
1730                        wgpu::BindGroupEntry {
1731                            binding: 1,
1732                            resource: buffers.tile64.zmin.bind_active(),
1733                        },
1734                        wgpu::BindGroupEntry {
1735                            binding: 2,
1736                            resource: buffers.tile16.tiles.bind_active(),
1737                        },
1738                        wgpu::BindGroupEntry {
1739                            binding: 3,
1740                            resource: buffers.tile16.zmin.bind_active(),
1741                        },
1742                        wgpu::BindGroupEntry {
1743                            binding: 4,
1744                            resource: buffers.z_hist_buf.slice(0..16).into(),
1745                        },
1746                    ],
1747                })
1748        })
1749    }
1750
1751    fn sort16(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
1752        self.sort16.get_or_init(|| {
1753            Self::sort_bind_group(
1754                ctx,
1755                &buffers.tile16,
1756                buffers.z_hist_buf.slice(0..16).into(),
1757            )
1758        })
1759    }
1760
1761    fn sort4(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
1762        self.sort4.get_or_init(|| {
1763            Self::sort_bind_group(
1764                ctx,
1765                &buffers.tile4,
1766                buffers.z_hist_buf.slice(256..320).into(),
1767            )
1768        })
1769    }
1770
1771    fn sort_bind_group<const N: u64>(
1772        ctx: &Context,
1773        tile_bufs: &TileBuffers<N>,
1774        z_hist: wgpu::BindingResource,
1775    ) -> wgpu::BindGroup {
1776        ctx.gpu
1777            .device
1778            .create_bind_group(&wgpu::BindGroupDescriptor {
1779                label: Some(&format!("sort{N} bind group")),
1780                layout: &ctx.interval_ctx.sort_bind_group_layout,
1781                entries: &[
1782                    wgpu::BindGroupEntry {
1783                        binding: 0,
1784                        resource: tile_bufs.tiles.bind_active(),
1785                    },
1786                    wgpu::BindGroupEntry {
1787                        binding: 1,
1788                        resource: z_hist,
1789                    },
1790                    wgpu::BindGroupEntry {
1791                        binding: 2,
1792                        resource: tile_bufs.sorted.bind_active(),
1793                    },
1794                ],
1795            })
1796    }
1797
1798    fn interval4(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
1799        self.interval4.get_or_init(|| {
1800            ctx.gpu
1801                .device
1802                .create_bind_group(&wgpu::BindGroupDescriptor {
1803                    label: Some("interval4 bind group"),
1804                    layout: &ctx.interval_ctx.interval_bind_group_layout,
1805                    entries: &[
1806                        wgpu::BindGroupEntry {
1807                            binding: 0,
1808                            resource: buffers.tile16.sorted.bind_active(),
1809                        },
1810                        wgpu::BindGroupEntry {
1811                            binding: 1,
1812                            resource: buffers.tile16.zmin.bind_active(),
1813                        },
1814                        wgpu::BindGroupEntry {
1815                            binding: 2,
1816                            resource: buffers.tile4.tiles.bind_active(),
1817                        },
1818                        wgpu::BindGroupEntry {
1819                            binding: 3,
1820                            resource: buffers.tile4.zmin.bind_active(),
1821                        },
1822                        wgpu::BindGroupEntry {
1823                            binding: 4,
1824                            resource: buffers.z_hist_buf.slice(256..320).into(),
1825                        },
1826                    ],
1827                })
1828        })
1829    }
1830
1831    fn voxel(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
1832        self.voxel.get_or_init(|| {
1833            ctx.gpu
1834                .device
1835                .create_bind_group(&wgpu::BindGroupDescriptor {
1836                    label: Some("voxel bind group"),
1837                    layout: &ctx.voxel_ctx.bind_group_layout,
1838                    entries: &[
1839                        wgpu::BindGroupEntry {
1840                            binding: 0,
1841                            resource: buffers.tile4.sorted.bind_active(),
1842                        },
1843                        wgpu::BindGroupEntry {
1844                            binding: 1,
1845                            resource: buffers.tile4.zmin.bind_active(),
1846                        },
1847                        wgpu::BindGroupEntry {
1848                            binding: 2,
1849                            resource: buffers.voxels.bind_active(),
1850                        },
1851                    ],
1852                })
1853        })
1854    }
1855
1856    fn normals(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
1857        self.normals.get_or_init(|| {
1858            ctx.gpu
1859                .device
1860                .create_bind_group(&wgpu::BindGroupDescriptor {
1861                    label: Some("normals bind group"),
1862                    layout: &ctx.normals_ctx.bind_group_layout,
1863                    entries: &[
1864                        wgpu::BindGroupEntry {
1865                            binding: 0,
1866                            resource: buffers.voxels.bind_active(),
1867                        },
1868                        wgpu::BindGroupEntry {
1869                            binding: 1,
1870                            resource: buffers.geom.bind_active(),
1871                        },
1872                    ],
1873                })
1874        })
1875    }
1876}
1877
1878impl Buffers {
1879    /// Returns the current image size
1880    pub fn image_size(&self) -> VoxelSize {
1881        self.image_size
1882    }
1883
1884    /// Returns a handle to the image storage buffer
1885    ///
1886    /// This is intended for subsequent shaders which want to use the
1887    /// [`GeometryPixel`] image data without copying to the CPU.  It requires a
1888    /// exclusive borrow of the `Buffers` object (and then extends that
1889    /// lifetime) so that other callers can't simultaneously touch the buffer.
1890    pub fn image_storage_buffer(&mut self) -> &ImageBuffer<GeomBufferTag> {
1891        &self.geom
1892    }
1893
1894    fn new(
1895        device: &wgpu::Device,
1896        image_size: VoxelSize,
1897        has_timestamps: bool,
1898    ) -> Result<Self, BuffersError> {
1899        // The config buffer is statically sized, so we can check it here
1900        static_assertions::const_assert!(
1901            (std::mem::size_of::<Config>()
1902                + TAPE_DATA_CAPACITY * std::mem::size_of::<TapeWord>())
1903                as u64
1904                <= BufferType::Storage.max_size()
1905        );
1906
1907        // Check that we can build an `ImageReadBuffer` of the appropriate
1908        // size (even though they are stored separately)
1909        ImageReadArrayBuffer::check_size(Self::image_buf_size(image_size))
1910            .map_err(|err| BuffersError {
1911                requested: image_size,
1912                buf: BufferName::Image,
1913                err,
1914            })?;
1915
1916        let config_buf = device.create_buffer(&wgpu::BufferDescriptor {
1917            label: Some("config"),
1918            size: (std::mem::size_of::<Config>()
1919                + TAPE_DATA_CAPACITY * std::mem::size_of::<TapeWord>())
1920                as u64,
1921            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1922            mapped_at_creation: false,
1923        });
1924
1925        let render_size = TileRenderSize::from(image_size);
1926        let voxels = ArrayBuffer::new(
1927            device,
1928            "voxels".to_string(),
1929            Self::voxels_buf_size(render_size),
1930        )
1931        .map_err(|err| BuffersError {
1932            requested: image_size,
1933            buf: BufferName::Voxels,
1934            err,
1935        })?;
1936        let tile_tapes = ArrayBuffer::new(
1937            device,
1938            "tile tape".to_string(),
1939            Self::tile_tapes_buf_size(render_size),
1940        )
1941        .map_err(|err| BuffersError {
1942            requested: image_size,
1943            buf: BufferName::TileTapes,
1944            err,
1945        })?;
1946
1947        let geom = ImageBuffer::new(
1948            device,
1949            "geom".to_string(),
1950            Self::geom_buf_size(image_size),
1951        )
1952        .map_err(|err| BuffersError {
1953            requested: image_size,
1954            buf: BufferName::Geom,
1955            err,
1956        })?;
1957
1958        let ts_buf = device.create_buffer(&wgpu::BufferDescriptor {
1959            label: Some("ts"),
1960            size: 2 * std::mem::size_of::<u64>() as u64,
1961            usage: wgpu::BufferUsages::QUERY_RESOLVE
1962                | wgpu::BufferUsages::COPY_SRC,
1963            mapped_at_creation: false,
1964        });
1965
1966        let tile64 =
1967            RootTileBuffers::new(device, render_size).map_err(|e| {
1968                BuffersError {
1969                    requested: image_size,
1970                    buf: BufferName::Tile64(e.buf),
1971                    err: e.err,
1972                }
1973            })?;
1974        let tile16 = TileBuffers::new(device, render_size).map_err(|e| {
1975            BuffersError {
1976                requested: image_size,
1977                buf: BufferName::Tile16(e.buf),
1978                err: e.err,
1979            }
1980        })?;
1981        let tile4 = TileBuffers::new(device, render_size).map_err(|e| {
1982            BuffersError {
1983                requested: image_size,
1984                buf: BufferName::Tile4(e.buf),
1985                err: e.err,
1986            }
1987        })?;
1988
1989        let timestamps = if has_timestamps {
1990            Some(device.create_query_set(&wgpu::QuerySetDescriptor {
1991                label: Some("timestamp query set"),
1992                ty: wgpu::QueryType::Timestamp,
1993                count: 2,
1994            }))
1995        } else {
1996            None
1997        };
1998
1999        // z_hist_buf never changes size
2000        let z_hist_buf = device.create_buffer(&wgpu::BufferDescriptor {
2001            label: Some("tiles_zhist"),
2002            size: u64::try_from(
2003                (4 * std::mem::size_of::<u32>()).next_multiple_of(256)
2004                    + (16 * std::mem::size_of::<u32>()),
2005            )
2006            .unwrap(),
2007            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2008            mapped_at_creation: false,
2009        });
2010
2011        Ok(Self {
2012            config_buf,
2013            image_size,
2014            tile_tapes,
2015            tile64,
2016            tile16,
2017            tile4,
2018            voxels,
2019            geom,
2020            timestamps,
2021            z_hist_buf,
2022            ts_buf,
2023            bind_groups: Default::default(),
2024        })
2025    }
2026
2027    fn render_size(&self) -> TileRenderSize {
2028        self.image_size.into()
2029    }
2030
2031    /// Returns the size of one strata (in bytes)
2032    fn strata_size_bytes(&self) -> usize {
2033        strata_size_bytes(self.render_size())
2034    }
2035
2036    /// Returns the number of bytes in the `tile_tapes` buffer
2037    ///
2038    /// The tile tape array is... complicated
2039    ///
2040    /// The first `nx * ny * nz` words are tape indices for the root tiles
2041    /// (64³), densely allocated in x / y / z order.  This is
2042    /// straight-forward.
2043    ///
2044    /// After that point, it gets weirder.  At any given point in time, we're
2045    /// evaluating a single strata (i.e. a 64-voxel deep slice of the image).
2046    /// We allocated enough tape words for that strata, also in x / y / z
2047    /// order, but z is limited to either 0..4 for the 16³ subtiles, or
2048    /// 0..16 for 4³ subtiles.
2049    ///
2050    /// In other words, it looks something like this:
2051    ///
2052    /// ```text
2053    /// | index | index | index | ... |     densely packed 64³ tape indices
2054    /// | index | index | index | ... |     16² XY tiles × 4  Z positions
2055    /// | index | index | index | ... |     4²  XY tiles × 16 Z positions
2056    /// ```
2057    fn tile_tapes_buf_size(render_size: TileRenderSize) -> usize {
2058        let nx = usize::try_from(render_size.nx()).unwrap();
2059        let ny = usize::try_from(render_size.ny()).unwrap();
2060        let nz = usize::try_from(render_size.nz()).unwrap();
2061
2062        // Each tile contains 16³ and 4³ subtiles
2063        let xy_size = (64usize / 4).pow(3) + (64usize / 16).pow(3);
2064
2065        // Total size computation:
2066        //    nx * ny * nz + (nx * ny * xy_size)
2067        // => nx * ny * (nz + xy_size)
2068        nx.checked_mul(ny)
2069            .unwrap()
2070            .checked_mul(nz.checked_add(xy_size).unwrap())
2071            .unwrap()
2072    }
2073
2074    fn voxels_buf_size(render_size: TileRenderSize) -> usize {
2075        render_size.pixels()
2076    }
2077
2078    /// Returns the image size for the `geom` buffer
2079    fn geom_buf_size(image_size: VoxelSize) -> ImageSize {
2080        ImageSize::new(image_size.width(), image_size.height())
2081    }
2082
2083    /// Returns image buffer size (in bytes)
2084    fn image_buf_size(image_size: VoxelSize) -> usize {
2085        Self::geom_buf_size(image_size)
2086            .item_count()
2087            // Convert from GeometryPixel item count to bytes
2088            .checked_mul(std::mem::size_of::<GeometryPixel>())
2089            .unwrap()
2090            // Allocate an extra 16 bytes for timestamp queries
2091            .checked_add(16)
2092            .unwrap()
2093    }
2094
2095    /// Resizes to render the target image size
2096    ///
2097    /// Internal buffers are resized to fit (only getting larger)
2098    ///
2099    /// This function also checks that the size is appropriate for an
2100    /// [`ImageReadBuffer`] (though we do not store such an object), so that
2101    /// later functions can resize it infallibly.
2102    fn set_image_size(
2103        &mut self,
2104        device: &wgpu::Device,
2105        image_size: VoxelSize,
2106    ) -> Result<(), BuffersError> {
2107        let render_size = TileRenderSize::from(image_size);
2108        let Buffers {
2109            image_size: image_size_ref,
2110            config_buf: _,
2111            z_hist_buf: _,
2112            tile_tapes,
2113            tile64,
2114            tile16,
2115            tile4,
2116            voxels,
2117            geom,
2118            timestamps: _,
2119            ts_buf: _,
2120            bind_groups,
2121        } = self;
2122        // Clear our cached bind groups if the image sizes is changing
2123        if *image_size_ref != image_size {
2124            *bind_groups = Default::default();
2125        }
2126        *image_size_ref = image_size;
2127        tile_tapes
2128            .grow_to_fit(device, Self::tile_tapes_buf_size(render_size))
2129            .map_err(|err| BuffersError {
2130                requested: image_size,
2131                buf: BufferName::TileTapes,
2132                err,
2133            })?;
2134        tile64
2135            .grow_to_fit(device, render_size)
2136            .map_err(|e| BuffersError {
2137                requested: image_size,
2138                buf: BufferName::Tile64(e.buf),
2139                err: e.err,
2140            })?;
2141        tile16
2142            .grow_to_fit(device, render_size)
2143            .map_err(|e| BuffersError {
2144                requested: image_size,
2145                buf: BufferName::Tile16(e.buf),
2146                err: e.err,
2147            })?;
2148        tile4
2149            .grow_to_fit(device, render_size)
2150            .map_err(|e| BuffersError {
2151                requested: image_size,
2152                buf: BufferName::Tile4(e.buf),
2153                err: e.err,
2154            })?;
2155
2156        voxels
2157            .grow_to_fit(device, Self::voxels_buf_size(render_size))
2158            .map_err(|err| BuffersError {
2159                requested: image_size,
2160                buf: BufferName::Voxels,
2161                err,
2162            })?;
2163        geom.grow_to_fit(device, Self::geom_buf_size(image_size))
2164            .map_err(|err| BuffersError {
2165                requested: image_size,
2166                buf: BufferName::Geom,
2167                err,
2168            })?;
2169
2170        // Check that we can build an `ImageReadBuffer` of the appropriate
2171        // size (even though they are stored separately)
2172        ImageReadArrayBuffer::check_size(Self::image_buf_size(image_size))
2173            .map_err(|err| BuffersError {
2174                requested: image_size,
2175                buf: BufferName::Image,
2176                err,
2177            })?;
2178
2179        Ok(())
2180    }
2181
2182    /// Returns total allocated size (in bytes)
2183    pub fn capacity(&self) -> u64 {
2184        // Destructure to make sure we take all members into account
2185        let Buffers {
2186            image_size: _,
2187            config_buf,
2188            z_hist_buf,
2189            tile_tapes,
2190            tile64,
2191            tile16,
2192            tile4,
2193            voxels,
2194            geom,
2195            timestamps: _,
2196            ts_buf,
2197            bind_groups: _,
2198        } = self;
2199        config_buf.size()
2200            + z_hist_buf.size()
2201            + tile_tapes.capacity()
2202            + tile64.capacity()
2203            + tile16.capacity()
2204            + tile4.capacity()
2205            + voxels.capacity()
2206            + geom.capacity()
2207            + ts_buf.size()
2208    }
2209
2210    /// Returns total active size (in bytes)
2211    pub fn size(&self) -> u64 {
2212        // Destructure to make sure we take all members into account
2213        let Buffers {
2214            image_size: _,
2215            config_buf,
2216            z_hist_buf,
2217            tile_tapes,
2218            tile64,
2219            tile16,
2220            tile4,
2221            voxels,
2222            geom,
2223            timestamps: _,
2224            ts_buf,
2225            bind_groups: _,
2226        } = self;
2227        config_buf.size()
2228            + z_hist_buf.size()
2229            + tile_tapes.size_bytes()
2230            + tile64.size()
2231            + tile16.size()
2232            + tile4.size()
2233            + voxels.size_bytes()
2234            + geom.size_bytes()
2235            + ts_buf.size()
2236    }
2237}
2238
2239impl Context {
2240    /// Build a new 3D rendering context, given a device and queue
2241    ///
2242    /// If render timestamps are desirable, then the device should be
2243    /// initialized with [`wgpu::Features::TIMESTAMP_QUERY`].
2244    pub fn new(gpu: &Gpu) -> Self {
2245        let has_timestamps = gpu
2246            .device
2247            .features()
2248            .contains(wgpu::Features::TIMESTAMP_QUERY);
2249        if !has_timestamps {
2250            log::warn!(
2251                "WGPU device is missing `TIMESTAMP_QUERY`; \
2252                 timestamps are disabled"
2253            );
2254        }
2255
2256        // Create bind group layout and bind group
2257        let common_bind_group_layout = gpu.device.create_bind_group_layout(
2258            &wgpu::BindGroupLayoutDescriptor {
2259                label: Some("common bind group layout"),
2260                entries: &[
2261                    buffer_rw(0), // config (including tape buffer)
2262                    buffer_rw(1), // tile_tape (hierarchical)
2263                ],
2264            },
2265        );
2266        let vars_bind_group_layout = gpu.device.create_bind_group_layout(
2267            &wgpu::BindGroupLayoutDescriptor {
2268                label: Some("vars bind group layout"),
2269                entries: &[
2270                    buffer_ro(0), // vars
2271                ],
2272            },
2273        );
2274
2275        let root_ctx = RootContext::new(
2276            &gpu.device,
2277            &common_bind_group_layout,
2278            &vars_bind_group_layout,
2279        );
2280        let repack_ctx = RepackContext::new(
2281            &gpu.device,
2282            &common_bind_group_layout,
2283            &vars_bind_group_layout,
2284        );
2285        let interval_ctx = IntervalContext::new(
2286            &gpu.device,
2287            &common_bind_group_layout,
2288            &vars_bind_group_layout,
2289        );
2290        let voxel_ctx = VoxelContext::new(
2291            &gpu.device,
2292            &common_bind_group_layout,
2293            &vars_bind_group_layout,
2294        );
2295        let normals_ctx = NormalsContext::new(
2296            &gpu.device,
2297            &common_bind_group_layout,
2298            &vars_bind_group_layout,
2299        );
2300        let merge_ctx = MergeContext::new(
2301            &gpu.device,
2302            &common_bind_group_layout,
2303            &vars_bind_group_layout,
2304        );
2305        let reset_ctx = ResetContext::new();
2306        let clear_ctx = ClearContext::new(
2307            &gpu.device,
2308            &common_bind_group_layout,
2309            &vars_bind_group_layout,
2310        );
2311
2312        Self {
2313            gpu: gpu.clone(),
2314            has_timestamps,
2315            common_bind_group_layout,
2316            vars_bind_group_layout,
2317            root_ctx,
2318            repack_ctx,
2319            interval_ctx,
2320            voxel_ctx,
2321            normals_ctx,
2322            merge_ctx,
2323            reset_ctx,
2324            clear_ctx,
2325        }
2326    }
2327
2328    /// Builds a new [`Buffers`] object for the given render size
2329    ///
2330    /// An image rendered with the resulting buffers will have the given width
2331    /// and height; `image_size.depth()` sets the number of voxels to evaluate
2332    /// within each pixel of the image (stacked into a column going into the
2333    /// screen).
2334    pub fn buffers(
2335        &self,
2336        image_size: VoxelSize,
2337    ) -> Result<Buffers, BuffersError> {
2338        Buffers::new(&self.gpu.device, image_size, self.has_timestamps)
2339    }
2340
2341    /// Returns an [`ImageReadBuffer`], sized to read from a [`Buffers`] object
2342    ///
2343    /// This is infallible because the [`Buffers`] constructor also ensures that
2344    /// the image size is appropriate for the image read buffer (even though
2345    /// it's constructed separately).
2346    pub fn image_buffer(&self, buffers: &Buffers) -> ImageReadBuffer {
2347        ImageReadBuffer::new(
2348            &self.gpu.device,
2349            "image".to_owned(),
2350            buffers.image_size,
2351        )
2352        .expect(
2353            "buffers.image_size should always be \
2354             a valid size for ImageReadBuffer::new",
2355        )
2356    }
2357
2358    /// Builds a new [`RenderShape`] object for the given shape
2359    pub fn shape(
2360        &self,
2361        shape: &VmShape,
2362    ) -> Result<RenderShape, RenderShapeError> {
2363        RenderShape::new(shape, &self.gpu.device)
2364    }
2365
2366    /// Renders the image, with a blocking wait to read pixel data from the GPU
2367    ///
2368    /// This function is not present when built for the `wasm32` target
2369    #[cfg(not(target_arch = "wasm32"))]
2370    pub fn run(
2371        &self,
2372        shape: &RenderShape,
2373        buffers: &Buffers,
2374        out: &mut ImageReadBuffer,
2375        settings: RenderConfig,
2376    ) -> Result<Image, MissingVar> {
2377        self.run_with_vars(shape, &Default::default(), buffers, out, settings)
2378    }
2379
2380    /// Renders the image, with a blocking wait to read pixel data from the GPU
2381    ///
2382    /// This function is not present when built for the `wasm32` target
2383    #[cfg(not(target_arch = "wasm32"))]
2384    pub fn run_with_vars(
2385        &self,
2386        shape: &RenderShape,
2387        vars: &ShapeVars<f32>,
2388        buffers: &Buffers,
2389        out: &mut ImageReadBuffer,
2390        settings: RenderConfig,
2391    ) -> Result<Image, MissingVar> {
2392        self.submit_with_vars(shape, vars, buffers, Some(out), &settings)?;
2393        let image = self.map_image(out);
2394        Ok(image.image())
2395    }
2396
2397    /// Renders the image, with a blocking wait to read pixel data from the GPU
2398    ///
2399    /// This function is only relevant for the web target
2400    #[cfg(any(target_arch = "wasm32", doc))]
2401    pub async fn run_async(
2402        &self,
2403        shape: &RenderShape,
2404        buffers: &Buffers,
2405        out: &mut ImageReadBuffer,
2406        settings: RenderConfig,
2407    ) -> Result<Image, MissingVar> {
2408        self.run_with_vars_async(
2409            shape,
2410            &Default::default(),
2411            buffers,
2412            out,
2413            settings,
2414        )
2415        .await
2416    }
2417
2418    /// Renders the image, with a blocking wait to read pixel data from the GPU
2419    ///
2420    /// This function is only relevant for the web target
2421    #[cfg(any(target_arch = "wasm32", doc))]
2422    pub async fn run_with_vars_async(
2423        &self,
2424        shape: &RenderShape,
2425        vars: &ShapeVars<f32>,
2426        buffers: &Buffers,
2427        out: &mut ImageReadBuffer,
2428        settings: RenderConfig,
2429    ) -> Result<Image, MissingVar> {
2430        self.submit_with_vars(shape, vars, buffers, Some(out), &settings)?;
2431        let image = self.map_image_async(out).await;
2432        Ok(image.image())
2433    }
2434
2435    /// Submits a single image to be rendered on the GPU
2436    ///
2437    /// The resulting image (as a buffer of [`GeometryPixel`] data) is available
2438    /// on the GPU in
2439    /// [`buffers.image_storage_buffer()`](Buffers::image_storage_buffer).
2440    ///
2441    /// If `out` is present, then the rendered image is also copied to that
2442    /// [`ImageReadBuffer`] (which may then be mapped for CPU reading by
2443    /// [`map_image`](Self::map_image) or
2444    /// [`map_image_async`](Self::map_image_async)).
2445    pub fn submit(
2446        &self,
2447        shape: &RenderShape,
2448        buffers: &mut Buffers,
2449        out: Option<&mut ImageReadBuffer>,
2450        settings: &RenderConfig,
2451    ) -> Result<(), MissingVar> {
2452        self.submit_with_vars(
2453            shape,
2454            &Default::default(),
2455            buffers,
2456            out,
2457            settings,
2458        )
2459    }
2460
2461    /// Submits a single image to be rendered on the GPU, with extra variables
2462    ///
2463    /// See [`submit`](Self::submit) for additional details.
2464    pub fn submit_with_vars(
2465        &self,
2466        shape: &RenderShape,
2467        vars: &ShapeVars<f32>,
2468        buffers: &Buffers,
2469        out: Option<&mut ImageReadBuffer>,
2470        settings: &RenderConfig,
2471    ) -> Result<(), MissingVar> {
2472        let render_size = TileRenderSize::from(buffers.image_size);
2473
2474        let mat =
2475            settings.world_to_model * buffers.image_size.screen_to_world();
2476
2477        // Divide by 2 to go from `u32` -> `TapeWord`
2478        let start_offset = u32::try_from(shape.bytecode.len()).unwrap() / 2;
2479        let config = Config {
2480            mat: mat.data.as_slice().try_into().unwrap(),
2481            axes: shape.axes,
2482            render_size: [
2483                render_size.width(),
2484                render_size.height(),
2485                render_size.depth(),
2486            ],
2487            tape_data_capacity: TAPE_DATA_CAPACITY.try_into().unwrap(),
2488            image_size: [
2489                buffers.image_size.width(),
2490                buffers.image_size.height(),
2491                buffers.image_size.depth(),
2492            ],
2493            tape_data_offset: start_offset,
2494            root_tape_len: start_offset,
2495        };
2496
2497        {
2498            // We load the `Config` and shape tape data.
2499            let config_len = std::mem::size_of_val(&config);
2500            let mut writer = self
2501                .gpu
2502                .queue
2503                .write_buffer_with(
2504                    &buffers.config_buf,
2505                    0,
2506                    ((config_len + shape.bytecode.as_bytes().len()) as u64)
2507                        .try_into()
2508                        .unwrap(),
2509                )
2510                .unwrap();
2511            writer
2512                .slice(..config_len)
2513                .copy_from_slice(config.as_bytes());
2514            writer
2515                .slice(config_len..)
2516                .copy_from_slice(shape.bytecode.as_bytes());
2517        }
2518
2519        // Copy vars (if present)
2520        if let Some(var_size) = NonZeroU64::new(shape.vars.size()) {
2521            let mut writer = self
2522                .gpu
2523                .queue
2524                .write_buffer_with(&shape.vars, 0, var_size)
2525                .unwrap();
2526            for (v, i) in shape.shape.inner().vars().iter() {
2527                match v {
2528                    Var::X | Var::Y | Var::Z => (),
2529                    Var::V(vi) => {
2530                        let Some(value) = vars.get(vi) else {
2531                            return Err(MissingVar { var: vi });
2532                        };
2533                        let offset = i * std::mem::size_of::<f32>();
2534                        writer
2535                            .slice(offset..offset + 4)
2536                            .copy_from_slice(value.as_bytes());
2537                    }
2538                }
2539            }
2540        }
2541
2542        // Create a command encoder and dispatch the compute work
2543        let mut encoder = self.gpu.device.create_command_encoder(
2544            &wgpu::CommandEncoderDescriptor { label: None },
2545        );
2546
2547        // Initial buffer reset pass
2548        self.reset_ctx.run(&mut encoder, buffers);
2549
2550        let mut compute_pass =
2551            encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
2552                label: None,
2553                timestamp_writes: buffers.timestamps.as_ref().map(
2554                    |query_set| wgpu::ComputePassTimestampWrites {
2555                        query_set,
2556                        beginning_of_pass_write_index: Some(0),
2557                        end_of_pass_write_index: Some(1),
2558                    },
2559                ),
2560            });
2561
2562        // Build the common config buffer
2563        let common_bind_group = buffers.bind_groups.common(self, buffers);
2564        compute_pass.set_bind_group(0, common_bind_group, &[]);
2565        let vars_bind_group = shape.vars_bind_group(self);
2566        compute_pass.set_bind_group(1, vars_bind_group, &[]);
2567
2568        // Populate root tiles (64x64x64, densely packed)
2569        self.root_ctx.run(
2570            self,
2571            buffers,
2572            shape.bytecode.reg_count(),
2573            render_size,
2574            &mut compute_pass,
2575        );
2576        // Repack root tiles into strata
2577        self.repack_ctx
2578            .run(self, buffers, render_size, &mut compute_pass);
2579
2580        // Evaluate tiles in reverse-Z order by strata (64 voxels deep)
2581        let strata_count = u64::from(render_size.depth()).div_ceil(64);
2582        for strata in 0..strata_count {
2583            self.interval_ctx.run(
2584                self,
2585                buffers,
2586                strata,
2587                shape.bytecode.reg_count(),
2588                &mut compute_pass,
2589            );
2590            self.voxel_ctx.run(
2591                self,
2592                buffers,
2593                shape.bytecode.reg_count(),
2594                &mut compute_pass,
2595            );
2596
2597            // Merge filled tiles from large -> small, populating the heightmap
2598            self.merge_ctx.run(self, buffers, &mut compute_pass);
2599            self.normals_ctx.run(
2600                self,
2601                buffers,
2602                shape.bytecode.reg_count(),
2603                &mut compute_pass,
2604            );
2605
2606            self.clear_ctx.run(self, buffers, &mut compute_pass);
2607        }
2608        drop(compute_pass);
2609
2610        // Resolve the raw GPU ticks into the resolve buffer, then copy them
2611        // into the last 16 bytes of the image buffer
2612        if let Some(image) = out {
2613            image
2614                .grow_to_fit(&self.gpu.device, buffers.image_size)
2615                .expect(
2616                    "buffers.image_size should always be \
2617                 a valid size for ImageReadBuffer::grow_to_fit",
2618                );
2619            if let Some(timestamps) = &buffers.timestamps {
2620                encoder.resolve_query_set(timestamps, 0..2, &buffers.ts_buf, 0);
2621                encoder.copy_buffer_to_buffer(
2622                    &buffers.ts_buf,
2623                    0,
2624                    image.buffer.data(),
2625                    buffers.geom.size_bytes(), // offset past the image data
2626                    buffers.ts_buf.size(),
2627                );
2628            }
2629
2630            // Copy from the STORAGE | COPY_SRC -> COPY_DST | MAP_READ buffer
2631            encoder.copy_buffer_to_buffer(
2632                buffers.geom.data(),
2633                0,
2634                image.buffer.data(),
2635                0,
2636                buffers.geom.size_bytes(),
2637            );
2638        }
2639
2640        // Submit the commands and wait for the GPU to complete
2641        self.gpu.queue.submit(Some(encoder.finish()));
2642        Ok(())
2643    }
2644
2645    /// Synchronously maps an image read buffer
2646    ///
2647    /// The image read buffer should be populated by passing it as an argument
2648    /// when calling [`submit`](Self::submit).
2649    ///
2650    /// The image is borrowed exclusively to avoid double-mapping
2651    ///
2652    /// This is a blocking function suitable for use on the desktop
2653    #[cfg(not(target_arch = "wasm32"))]
2654    pub fn map_image<'a>(
2655        &self,
2656        image: &'a mut ImageReadBuffer,
2657    ) -> MappedImage<'a> {
2658        let slice = image.buffer.map_async(|_| {});
2659        self.gpu
2660            .device
2661            .poll(wgpu::PollType::wait_indefinitely())
2662            .unwrap();
2663        MappedImage {
2664            image,
2665            slice,
2666            ns_per_tick: if self.has_timestamps {
2667                Some(self.gpu.queue.get_timestamp_period())
2668            } else {
2669                None
2670            },
2671        }
2672    }
2673
2674    /// Asynchronously maps an image read buffer
2675    ///
2676    /// The image read buffer should be populated by passing it as an argument
2677    /// when calling [`submit`](Self::submit).
2678    ///
2679    /// The image is borrowed exclusively to avoid double-mapping
2680    ///
2681    /// This is an `async` function suitable for use in WebAssembly.
2682    #[cfg(any(target_arch = "wasm32", doc))]
2683    pub async fn map_image_async<'a>(
2684        &self,
2685        image: &'a mut ImageReadBuffer,
2686    ) -> MappedImage<'a> {
2687        let (tx, rx) = flume::bounded(0);
2688        let slice = image.buffer.map_async(move |_| tx.send(()).unwrap());
2689        rx.recv_async().await.unwrap();
2690        MappedImage {
2691            image,
2692            slice,
2693            ns_per_tick: if self.has_timestamps {
2694                Some(self.gpu.queue.get_timestamp_period())
2695            } else {
2696                None
2697            },
2698        }
2699    }
2700
2701    /// Resizes buffers to the given image size
2702    ///
2703    /// Buffer allocations may grow but do not shrink; delete and recreate
2704    /// buffers if their capacity exceeds their size to a significant degree.
2705    pub fn set_buffers_image_size(
2706        &self,
2707        buffers: &mut Buffers,
2708        image_size: VoxelSize,
2709    ) -> Result<(), BuffersError> {
2710        buffers.set_image_size(&self.gpu.device, image_size)
2711    }
2712}
2713
2714/// Handle to a mapped image, which unmaps the image when dropped
2715pub struct MappedImage<'a> {
2716    image: &'a ImageReadBuffer,
2717    slice: wgpu::BufferSlice<'a>,
2718
2719    /// Nanoseconds per tick, for resolving timestamps
2720    ns_per_tick: Option<f32>,
2721}
2722
2723impl Drop for MappedImage<'_> {
2724    fn drop(&mut self) {
2725        self.image.buffer.data().unmap();
2726    }
2727}
2728
2729impl MappedImage<'_> {
2730    /// Returns the image's data
2731    pub fn image(&self) -> Image {
2732        // Get the pixel-populated image
2733        let result = <[GeometryPixel]>::ref_from_bytes(
2734            &self.slice.get_mapped_range()[..self.image_bytes()],
2735        )
2736        .unwrap()
2737        .to_owned();
2738        Image::build(result, self.image.image_size).unwrap()
2739    }
2740
2741    /// Returns the time spent in the compute pass
2742    ///
2743    /// This may be 0 on platforms which advertise `TIMESTAMP_QUERY` but do not
2744    /// actually populate timestamps, and will be `None` if the context does not
2745    /// have `TIMESTAMP_QUERY` enabled.
2746    pub fn time(&self) -> Option<std::time::Duration> {
2747        self.ns_per_tick.map(|ns_per_tick| {
2748            let slice = self.slice.get_mapped_range();
2749            let ts =
2750                <[u64]>::ref_from_bytes(&slice[self.image_bytes()..]).unwrap();
2751            std::time::Duration::from_nanos(
2752                (ts[1].saturating_sub(ts[0]) as f64 * ns_per_tick as f64)
2753                    as u64,
2754            )
2755        })
2756    }
2757
2758    fn image_bytes(&self) -> usize {
2759        (self.image.image_size.width() as usize)
2760            * (self.image.image_size.height() as usize)
2761            * std::mem::size_of::<GeometryPixel>()
2762    }
2763}
2764
2765struct ClearContext {
2766    bind_group_layout: wgpu::BindGroupLayout,
2767    pipeline: wgpu::ComputePipeline,
2768}
2769
2770impl ClearContext {
2771    fn new(
2772        device: &wgpu::Device,
2773        common_bind_group_layout: &wgpu::BindGroupLayout,
2774        vars_bind_group_layout: &wgpu::BindGroupLayout,
2775    ) -> Self {
2776        // Create bind group layout and bind group
2777        let bind_group_layout =
2778            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2779                label: Some("clear bind group layout"),
2780                entries: &[
2781                    buffer_rw(0), // tile16_count
2782                    buffer_rw(1), // tile16_sort
2783                    buffer_rw(2), // tile4_count
2784                    buffer_rw(3), // tile4_sort
2785                    buffer_rw(4), // zhist_buf
2786                ],
2787            });
2788
2789        // Create the compute pipeline
2790        let pipeline_layout =
2791            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2792                label: Some("clear pipeline layout"),
2793                bind_group_layouts: &[
2794                    Some(common_bind_group_layout),
2795                    Some(vars_bind_group_layout),
2796                    Some(&bind_group_layout),
2797                ],
2798                immediate_size: 0u32,
2799            });
2800
2801        // Compile the shader
2802        let shader_code = clear_shader();
2803        let shader_module =
2804            device.create_shader_module(wgpu::ShaderModuleDescriptor {
2805                label: Some("clear shader module"),
2806                source: wgpu::ShaderSource::Wgsl(shader_code.into()),
2807            });
2808
2809        let pipeline =
2810            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
2811                label: Some("clear"),
2812                layout: Some(&pipeline_layout),
2813                module: &shader_module,
2814                entry_point: Some("clear_main"),
2815                compilation_options: Default::default(),
2816                cache: None,
2817            });
2818
2819        Self {
2820            pipeline,
2821            bind_group_layout,
2822        }
2823    }
2824
2825    fn run(
2826        &self,
2827        ctx: &Context,
2828        buffers: &Buffers,
2829        compute_pass: &mut wgpu::ComputePass,
2830    ) {
2831        let bind_group = buffers.bind_groups.clear(ctx, buffers);
2832        compute_pass.set_pipeline(&self.pipeline);
2833        compute_pass.set_bind_group(2, bind_group, &[]);
2834        compute_pass.dispatch_workgroups(1, 1, 1);
2835    }
2836}
2837
2838struct MergeContext {
2839    bind_group_layout: wgpu::BindGroupLayout,
2840    pipeline: wgpu::ComputePipeline,
2841}
2842
2843impl MergeContext {
2844    fn new(
2845        device: &wgpu::Device,
2846        common_bind_group_layout: &wgpu::BindGroupLayout,
2847        vars_bind_group_layout: &wgpu::BindGroupLayout,
2848    ) -> Self {
2849        let shader_code = merge_shader();
2850
2851        // Create bind group layout and bind group
2852        let bind_group_layout =
2853            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2854                label: Some("merge bind group layout"),
2855                entries: &[
2856                    buffer_rw(0), // tile64_zmin
2857                    buffer_rw(1), // tile16_zmin
2858                    buffer_rw(2), // tile4_zmin
2859                    buffer_rw(3), // voxels
2860                ],
2861            });
2862
2863        // Create the compute pipeline
2864        let pipeline_layout =
2865            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2866                label: Some("merge pipeline layout"),
2867                bind_group_layouts: &[
2868                    Some(common_bind_group_layout),
2869                    Some(vars_bind_group_layout),
2870                    Some(&bind_group_layout),
2871                ],
2872                immediate_size: 0u32,
2873            });
2874
2875        // Compile the shader
2876        let shader_module =
2877            device.create_shader_module(wgpu::ShaderModuleDescriptor {
2878                label: Some("merge shader module"),
2879                source: wgpu::ShaderSource::Wgsl(shader_code.into()),
2880            });
2881
2882        let pipeline =
2883            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
2884                label: Some("merge"),
2885                layout: Some(&pipeline_layout),
2886                module: &shader_module,
2887                entry_point: Some("merge_main"),
2888                compilation_options: Default::default(),
2889                cache: None,
2890            });
2891
2892        Self {
2893            pipeline,
2894            bind_group_layout,
2895        }
2896    }
2897
2898    fn run(
2899        &self,
2900        ctx: &Context,
2901        buffers: &Buffers,
2902        compute_pass: &mut wgpu::ComputePass,
2903    ) {
2904        let render_size = buffers.render_size();
2905        let bind_group = buffers.bind_groups.merge(ctx, buffers);
2906        compute_pass.set_pipeline(&self.pipeline);
2907        compute_pass.set_bind_group(2, bind_group, &[]);
2908        compute_pass.dispatch_workgroups(
2909            render_size.width().div_ceil(8),
2910            render_size.height().div_ceil(8),
2911            1,
2912        );
2913    }
2914}
2915
2916struct ResetContext;
2917
2918impl ResetContext {
2919    fn new() -> Self {
2920        ResetContext
2921    }
2922
2923    fn run(&self, encoder: &mut wgpu::CommandEncoder, buffers: &Buffers) {
2924        // Clear only the `count` member of the tile64 `tiles_out` buffer
2925        encoder.clear_buffer(buffers.tile64.tiles.data(), 12, Some(4));
2926
2927        // Per-strata counters may now be at a different location in memory if
2928        // we're using the buffers for multiple renders of different sizes!  To
2929        // be safe, we'll clear them here, rather than in a render pass.
2930        let strata_size_bytes = buffers.strata_size_bytes();
2931        for s in 0..buffers.render_size().nz() {
2932            encoder.clear_buffer(
2933                buffers.tile64.strata.data(),
2934                u64::from(s) * u64::try_from(strata_size_bytes).unwrap(),
2935                Some(16),
2936            );
2937        }
2938
2939        // Clear all of the heightmaps and output maps
2940        buffers.tile64.zmin.clear(encoder);
2941        buffers.tile64.zmax.clear(encoder);
2942        buffers.tile16.zmin.clear(encoder);
2943        buffers.tile4.zmin.clear(encoder);
2944        buffers.voxels.clear(encoder);
2945        buffers.geom.clear(encoder);
2946
2947        // Clear the whole tile tape map (TODO is this needed?)
2948        buffers.tile_tapes.clear(encoder);
2949
2950        // tiles / sorted counters and z_hist are reset in clear shader
2951    }
2952}
2953
2954#[cfg(test)]
2955mod test {
2956    use super::*;
2957    use heck::ToShoutySnakeCase;
2958
2959    #[test]
2960    fn shader_has_all_ops() {
2961        for (op, _) in fidget_bytecode::iter_ops() {
2962            let op = format!("OP_{}", op.to_shouty_snake_case());
2963            assert!(
2964                TAPE_INTERPRETER.contains(&op),
2965                "tape interpreter is missing {op}"
2966            );
2967            assert!(
2968                TAPE_SIMPLIFY.contains(&op),
2969                "tape simplification is missing {op}"
2970            );
2971        }
2972    }
2973
2974    #[test]
2975    fn compile_shaders() {
2976        for (src, desc) in [
2977            (interval_root_shader(16), "interval root"),
2978            (interval_tiles_shader(16), "interval tiles"),
2979            (voxel_tiles_shader(16), "voxel tiles"),
2980            (normals_shader(16), "normals tiles"),
2981            (repack_shader(), "repack"),
2982            (sort_shader(), "sort"),
2983            (merge_shader(), "merge"),
2984            (clear_shader(), "clear"),
2985        ] {
2986            crate::compile_shader(&src, desc);
2987        }
2988    }
2989}