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