Skip to main content

fidget_wgpu/pixel/
mod.rs

1//! GPU-accelerated 2D rendering
2//!
3//! See the [`voxel`](crate::voxel) module for details docs; this module is
4//! analogous (down to the naming of types).
5use crate::{
6    CopyVarsChanged, CopyVarsError, Gpu, RegPipeline, RenderShape,
7    TAPE_DATA_CAPACITY, TapeWord,
8    buf::{
9        BufferSizeError, BufferType, FlexBuffer, ReadBuffer, buffer_ro,
10        buffer_rw,
11    },
12    shaders, tag,
13    voxel::VarsBufferTag,
14};
15use fidget_core::{
16    render::ImageSize,
17    shape::{MissingVar, ShapeVars},
18};
19use fidget_raster::pixel::{Image, RawDistancePixel};
20use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
21
22pub use fidget_raster::pixel::{RenderConfig, RenderSize};
23pub mod effects;
24
25const COMMON_SHADER: &str = include_str!("shaders/common.wgsl");
26const DISTANCE_PIXEL_SHADER: &str = include_str!("shaders/distance_pixel.wgsl");
27const INTERVAL_INPUT: &str = include_str!("shaders/interval_input.wgsl");
28const TRANSFORM_INPUT: &str = include_str!("shaders/transform_input.wgsl");
29const INTERVAL_ROOT_SHADER: &str = include_str!("shaders/interval_root.wgsl");
30const INTERVAL_TILES_SHADER: &str = include_str!("shaders/interval_tiles.wgsl");
31const PIXEL_TILES_SHADER: &str = include_str!("shaders/pixel_tiles.wgsl");
32const MERGE_SHADER: &str = include_str!("shaders/merge.wgsl");
33
34/// Returns a shader for interval root tiles
35fn interval_root_shader(reg_count: u8) -> String {
36    let mut shader_code = shaders::opcode_constants();
37    shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
38    shader_code += COMMON_SHADER;
39    shader_code += DISTANCE_PIXEL_SHADER;
40    shader_code += INTERVAL_ROOT_SHADER;
41    shader_code += INTERVAL_INPUT;
42    shader_code += TRANSFORM_INPUT;
43    shader_code += shaders::INTERVAL_OPS;
44    shader_code += shaders::COMMON;
45    shader_code += shaders::TAPE_INTERPRETER;
46    shader_code += shaders::STACK;
47    shader_code += shaders::TAPE_SIMPLIFY;
48    shader_code
49}
50
51/// Returns a shader for interval tile -> subtile reduction
52fn interval_tiles_shader(reg_count: u8) -> String {
53    let mut shader_code = shaders::opcode_constants();
54    shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
55    shader_code += COMMON_SHADER;
56    shader_code += DISTANCE_PIXEL_SHADER;
57    shader_code += INTERVAL_TILES_SHADER;
58    shader_code += INTERVAL_INPUT;
59    shader_code += TRANSFORM_INPUT;
60    shader_code += shaders::INTERVAL_OPS;
61    shader_code += shaders::COMMON;
62    shader_code += shaders::TAPE_INTERPRETER;
63    shader_code += shaders::STACK;
64    shader_code += shaders::TAPE_SIMPLIFY;
65    shader_code
66}
67
68/// Returns a shader for pixel tile evaluation
69fn pixel_tiles_shader(reg_count: u8) -> String {
70    let mut shader_code = shaders::opcode_constants();
71    shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
72    shader_code += PIXEL_TILES_SHADER;
73    shader_code += TRANSFORM_INPUT;
74    shader_code += COMMON_SHADER;
75    shader_code += DISTANCE_PIXEL_SHADER;
76    shader_code += shaders::FLOAT_OPS;
77    shader_code += shaders::COMMON;
78    shader_code += shaders::TAPE_INTERPRETER;
79    shader_code += shaders::DUMMY_STACK;
80    shader_code
81}
82
83/// Returns a shader for merging images
84fn merge_shader() -> String {
85    MERGE_SHADER.to_owned()
86        + COMMON_SHADER
87        + DISTANCE_PIXEL_SHADER
88        + shaders::COMMON
89}
90
91////////////////////////////////////////////////////////////////////////////////
92
93/// A render size is rounded up to the next multiple of 64 on every axis
94///
95/// The internal `ImageSize` stores divided-by-64 values, so that the render
96/// size cannot be constructed with an invalid state.
97#[derive(Copy, Clone, Debug)]
98struct TileRenderSize(ImageSize);
99
100impl From<ImageSize> for TileRenderSize {
101    fn from(image_size: ImageSize) -> Self {
102        let nx = image_size.width().div_ceil(64);
103        let ny = image_size.height().div_ceil(64);
104        Self(ImageSize::new(nx, ny))
105    }
106}
107
108impl TileRenderSize {
109    /// Number of tiles in the X axis
110    fn nx(&self) -> u32 {
111        self.0.width()
112    }
113
114    /// Number of tiles in the Y axis
115    fn ny(&self) -> u32 {
116        self.0.height()
117    }
118
119    /// Number of voxels in the X axis (always a multiple of 64)
120    fn width(&self) -> u32 {
121        self.0.width() * 64
122    }
123
124    /// Number of voxels in the Y axis (always a multiple of 64)
125    fn height(&self) -> u32 {
126        self.0.height() * 64
127    }
128}
129
130////////////////////////////////////////////////////////////////////////////////
131
132/// Root context, which produces a list of 64² tiles
133struct RootContext {
134    /// Pipelines for 64² tile evaluation
135    root_pipeline: RegPipeline,
136
137    /// Bind group layout
138    bind_group_layout: wgpu::BindGroupLayout,
139}
140
141impl RootContext {
142    fn new(
143        device: &wgpu::Device,
144        common_bind_group_layout: &wgpu::BindGroupLayout,
145    ) -> Self {
146        // Create bind group layout and bind group
147        let bind_group_layout =
148            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
149                label: None,
150                entries: &[
151                    buffer_rw(0), // tiles_out
152                    buffer_rw(1), // tile_values
153                ],
154            });
155        let pipeline_layout =
156            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
157                label: None,
158                bind_group_layouts: &[
159                    Some(common_bind_group_layout),
160                    Some(&bind_group_layout),
161                ],
162                immediate_size: 0u32,
163            });
164
165        let device_ = device.clone();
166        let root_pipeline = RegPipeline::build(Box::new(move |reg_count| {
167            let shader_code = interval_root_shader(reg_count);
168            let shader_module =
169                device_.create_shader_module(wgpu::ShaderModuleDescriptor {
170                    label: None,
171                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
172                });
173            device_.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
174                label: Some(&format!("interval root ({reg_count})")),
175                layout: Some(&pipeline_layout),
176                module: &shader_module,
177                entry_point: Some("interval_root_main"),
178                compilation_options: Default::default(),
179                cache: None,
180            })
181        }));
182
183        Self {
184            bind_group_layout,
185            root_pipeline,
186        }
187    }
188
189    fn run(
190        &self,
191        ctx: &Context,
192        workspace: &Workspace,
193        reg_count: u8,
194        render_size: TileRenderSize,
195        compute_pass: &mut wgpu::ComputePass,
196    ) {
197        let bind_group = workspace.bind_groups.root_tiles(ctx, workspace);
198        compute_pass.set_pipeline(self.root_pipeline.get(reg_count));
199        compute_pass.set_bind_group(1, bind_group, &[]);
200
201        // Workgroup is 8x8x8, so we divide by 8 here on each axis
202        let nx = render_size.nx().div_ceil(8);
203        let ny = render_size.ny().div_ceil(8);
204        compute_pass.dispatch_workgroups(nx, ny, 1);
205    }
206}
207////////////////////////////////////////////////////////////////////////////////
208
209/// Intermediate tiles context, which produces a list of 8² tiles
210struct IntervalTilesContext {
211    /// Pipelines for 8² tile evaluation
212    tiles_pipeline: RegPipeline,
213
214    /// Bind group layout
215    bind_group_layout: wgpu::BindGroupLayout,
216}
217
218impl IntervalTilesContext {
219    fn new(
220        device: &wgpu::Device,
221        common_bind_group_layout: &wgpu::BindGroupLayout,
222    ) -> Self {
223        // Create bind group layout and bind group
224        let bind_group_layout =
225            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
226                label: None,
227                entries: &[
228                    buffer_ro(0), // tiles_in
229                    buffer_rw(1), // subtiles_out
230                    buffer_rw(2), // subtile_values
231                ],
232            });
233        let pipeline_layout =
234            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
235                label: None,
236                bind_group_layouts: &[
237                    Some(common_bind_group_layout),
238                    Some(&bind_group_layout),
239                ],
240                immediate_size: 0u32,
241            });
242
243        let device_ = device.clone();
244        let tiles_pipeline = RegPipeline::build(Box::new(move |reg_count| {
245            let shader_code = interval_tiles_shader(reg_count);
246            let shader_module =
247                device_.create_shader_module(wgpu::ShaderModuleDescriptor {
248                    label: None,
249                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
250                });
251            device_.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
252                label: Some(&format!("interval tiles ({reg_count})")),
253                layout: Some(&pipeline_layout),
254                module: &shader_module,
255                entry_point: Some("interval_tiles_main"),
256                compilation_options: Default::default(),
257                cache: None,
258            })
259        }));
260
261        Self {
262            bind_group_layout,
263            tiles_pipeline,
264        }
265    }
266
267    fn run(
268        &self,
269        ctx: &Context,
270        workspace: &Workspace,
271        reg_count: u8,
272        compute_pass: &mut wgpu::ComputePass,
273    ) {
274        let bind_group = workspace.bind_groups.interval_tiles(ctx, workspace);
275        compute_pass.set_pipeline(self.tiles_pipeline.get(reg_count));
276        compute_pass.set_bind_group(1, bind_group, &[]);
277
278        // Indirect dispatch based on previous tile output
279        compute_pass
280            .dispatch_workgroups_indirect(workspace.tile64.tiles.data(), 0);
281    }
282}
283
284////////////////////////////////////////////////////////////////////////////////
285
286/// Pixel tiles context, which outputs individual pixels
287struct PixelTilesContext {
288    /// Pipelines for 8² pixel tile evaluation
289    tiles_pipeline: RegPipeline,
290
291    /// Bind group layout
292    bind_group_layout: wgpu::BindGroupLayout,
293}
294
295impl PixelTilesContext {
296    fn new(
297        device: &wgpu::Device,
298        common_bind_group_layout: &wgpu::BindGroupLayout,
299    ) -> Self {
300        // Create bind group layout and bind group
301        let bind_group_layout =
302            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
303                label: None,
304                entries: &[
305                    buffer_ro(0), // tiles_in
306                    buffer_rw(1), // result
307                ],
308            });
309        let pipeline_layout =
310            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
311                label: None,
312                bind_group_layouts: &[
313                    Some(common_bind_group_layout),
314                    Some(&bind_group_layout),
315                ],
316                immediate_size: 0u32,
317            });
318
319        let device_ = device.clone();
320        let tiles_pipeline = RegPipeline::build(Box::new(move |reg_count| {
321            let shader_code = pixel_tiles_shader(reg_count);
322            let shader_module =
323                device_.create_shader_module(wgpu::ShaderModuleDescriptor {
324                    label: None,
325                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
326                });
327            device_.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
328                label: Some(&format!("pixel tiles ({reg_count})")),
329                layout: Some(&pipeline_layout),
330                module: &shader_module,
331                entry_point: Some("pixel_tiles_main"),
332                compilation_options: Default::default(),
333                cache: None,
334            })
335        }));
336
337        Self {
338            bind_group_layout,
339            tiles_pipeline,
340        }
341    }
342
343    fn run(
344        &self,
345        ctx: &Context,
346        workspace: &Workspace,
347        reg_count: u8,
348        compute_pass: &mut wgpu::ComputePass,
349    ) {
350        let bind_group = workspace.bind_groups.pixel_tiles(ctx, workspace);
351        compute_pass.set_pipeline(self.tiles_pipeline.get(reg_count));
352        compute_pass.set_bind_group(1, bind_group, &[]);
353
354        // Indirect dispatch based on previous tile output
355        compute_pass
356            .dispatch_workgroups_indirect(workspace.tile8.tiles.data(), 0);
357    }
358}
359
360////////////////////////////////////////////////////////////////////////////////
361
362/// Doppelganger of the WGSL `struct Config`
363///
364/// Fields are carefully ordered to require no internal padding (enforced by
365/// `zerocopy` derives)
366#[derive(Debug, IntoBytes, Immutable, FromBytes, KnownLayout)]
367#[cfg_attr(test, derive(facet::Facet))]
368#[repr(C)]
369struct Config {
370    /// Screen-to-model transform matrix
371    ///
372    /// This is a 3x3 matrix in WGSL, but each row is padded to 16 bytes, so
373    /// it's a total of 12 floats.
374    mat: [f32; 12],
375
376    /// Input index of X, Y, Z axes
377    ///
378    /// `u32::MAX` is used as a marker if an axis is unused
379    axes: [u32; 3],
380
381    /// Initial offset in `tape_data`
382    tape_data_offset: u32,
383
384    /// Render size, rounded up to the nearest multiple of 64
385    render_size: [u32; 2],
386
387    /// Image size (not rounded)
388    image_size: [u32; 2],
389
390    /// Z position at which to render
391    z: f32,
392
393    /// Flag indicating whether to recurse down to individual pixels
394    pixel_perfect: u32,
395
396    /// Number of words in the trailing tape buffer
397    tape_data_capacity: u32,
398
399    /// Padding
400    _pad: u32,
401    // This is followed by a flexible array member containing tape data
402}
403
404tag!(TileTapesBufferTag, u32, usize, STORAGE | COPY_DST);
405tag!(pub PixelBufferTag, RawDistancePixel, ImageSize, STORAGE | COPY_SRC | COPY_DST,
406    "Tag for a on-GPU buffer storing [`RawDistancePixel`] values");
407
408/// Workspace for rendering
409///
410/// This object is constructed by [`Context::workspace`] and may only be used with
411/// that particular [`Context`].
412pub struct Workspace {
413    /// Image render size
414    ///
415    /// Note that the tile buffers below round up to the nearest root tile
416    /// (64² voxels).
417    image_size: ImageSize,
418
419    /// Config and tape data buffer (constant size)
420    config_buf: wgpu::Buffer,
421
422    /// Scratch space to upload variable values
423    vars_buf: FlexBuffer<VarsBufferTag>,
424
425    /// Map from tile to the relevant tape (as a start index)
426    tile_tapes: FlexBuffer<TileTapesBufferTag>,
427
428    /// Root tile buffers (64²)
429    tile64: TileBuffers<64>,
430
431    /// Second-stage tile buffers (8²)
432    tile8: TileBuffers<8>,
433
434    /// Pixel data
435    pixels: FlexBuffer<PixelBufferTag>,
436
437    /// Cached bind groups
438    bind_groups: BindGroups,
439}
440
441impl Workspace {
442    /// Builds a new set of buffers with a default size
443    ///
444    /// It is expected that these will be resized before being used
445    fn new(device: &wgpu::Device) -> Self {
446        // The config buffer is statically sized, so we can check it here
447        static_assertions::const_assert!(
448            (std::mem::size_of::<Config>()
449                + TAPE_DATA_CAPACITY * std::mem::size_of::<TapeWord>())
450                as u64
451                <= BufferType::Storage.max_size()
452        );
453
454        // Dummy size for infallible construction
455        let image_size = 64.into();
456
457        let config_buf = device.create_buffer(&wgpu::BufferDescriptor {
458            label: Some("config"),
459            size: (std::mem::size_of::<Config>()
460                + TAPE_DATA_CAPACITY * std::mem::size_of::<TapeWord>())
461                as u64,
462            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
463            mapped_at_creation: false,
464        });
465
466        let render_size = TileRenderSize::from(image_size);
467        let tile_tapes = FlexBuffer::new(
468            device,
469            "tile tape",
470            Self::tile_tapes_buf_size(render_size),
471        )
472        .unwrap();
473
474        let pixels = FlexBuffer::new(device, "pixels", image_size).unwrap();
475
476        let tile64 = TileBuffers::new(device, render_size).unwrap();
477        let tile8 = TileBuffers::new(device, render_size).unwrap();
478        let vars_buf = FlexBuffer::new(device, "vars", 4).unwrap();
479
480        Self {
481            config_buf,
482            vars_buf,
483            image_size,
484            tile_tapes,
485            tile64,
486            tile8,
487            pixels,
488            bind_groups: Default::default(),
489        }
490    }
491
492    /// Returns the number of bytes in the `tile_tapes` buffer
493    ///
494    /// This is two levels of densely-allocated tiles; the first set are
495    /// 64² and the second are 8².
496    ///
497    /// In other words, it looks something like this:
498    ///
499    /// ```text
500    /// | index | index | index | ... |     densely packed 64² tape indices
501    /// | index | index | index | ... |     densely packed 8² tape indices
502    /// ```
503    fn tile_tapes_buf_size(render_size: TileRenderSize) -> usize {
504        let nx = usize::try_from(render_size.nx()).unwrap();
505        let ny = usize::try_from(render_size.ny()).unwrap();
506
507        // Total size computation:
508        //    (nx * ny) + (nx * ny * 64)
509        // => nx * ny * 65
510        nx.checked_mul(ny).unwrap().checked_mul(65).unwrap()
511    }
512
513    /// Resizes to render the target image size
514    ///
515    /// Internal buffers are resized to fit (only getting larger)
516    fn set_image_size(
517        &mut self,
518        device: &wgpu::Device,
519        image_size: ImageSize,
520    ) -> Result<(), BufferError> {
521        let render_size = TileRenderSize::from(image_size);
522        let Workspace {
523            image_size: image_size_ref,
524            tile_tapes,
525            tile64,
526            tile8,
527            config_buf: _,
528            vars_buf: _,
529            pixels,
530            bind_groups,
531        } = self;
532        // Clear our cached bind groups if the image sizes is changing
533        if *image_size_ref != image_size {
534            *bind_groups = Default::default();
535        }
536        *image_size_ref = image_size;
537        tile_tapes
538            .grow_to_fit(device, Self::tile_tapes_buf_size(render_size))
539            .map_err(|err| BufferError {
540                buf: BufferName::TileTapes,
541                err,
542            })?;
543        tile64
544            .grow_to_fit(device, render_size)
545            .map_err(|e| BufferError {
546                buf: BufferName::Tile64(e.buf),
547                err: e.err,
548            })?;
549        tile8
550            .grow_to_fit(device, render_size)
551            .map_err(|e| BufferError {
552                buf: BufferName::Tile8(e.buf),
553                err: e.err,
554            })?;
555        pixels
556            .grow_to_fit(device, image_size)
557            .map_err(|err| BufferError {
558                buf: BufferName::Pixel,
559                err,
560            })?;
561        Ok(())
562    }
563
564    /// Returns total allocated size (in bytes)
565    pub fn capacity(&self) -> u64 {
566        // Destructure to make sure we take all members into account
567        let Workspace {
568            image_size: _,
569            config_buf,
570            vars_buf,
571            tile_tapes,
572            tile64,
573            tile8,
574            pixels,
575            bind_groups: _,
576        } = self;
577        config_buf.size()
578            + vars_buf.capacity()
579            + tile_tapes.capacity()
580            + tile64.capacity()
581            + tile8.capacity()
582            + pixels.capacity()
583    }
584
585    /// Returns total active size (in bytes)
586    pub fn size(&self) -> u64 {
587        // Destructure to make sure we take all members into account
588        let Workspace {
589            image_size: _,
590            vars_buf,
591            config_buf,
592            tile_tapes,
593            tile64,
594            tile8,
595            pixels,
596            bind_groups: _,
597        } = self;
598        config_buf.size()
599            + vars_buf.size_bytes()
600            + tile_tapes.size_bytes()
601            + tile64.size()
602            + tile8.size()
603            + pixels.size_bytes()
604    }
605
606    /// Returns a handle to the image storage buffer
607    ///
608    /// This is intended for subsequent shaders which want to use the
609    /// [`RawDistancePixel`] image data without copying to the CPU.  It requires
610    /// an exclusive borrow of the `Workspace` object (and then extends that
611    /// lifetime) so that other callers can't simultaneously touch the buffer.
612    pub fn output(&mut self) -> &FlexBuffer<PixelBufferTag> {
613        &self.pixels
614    }
615}
616
617/// Error returned when resizing buffers in a [`Workspace`] object
618#[derive(Debug, thiserror::Error)]
619#[error("failed to build {buf} buffer")]
620pub struct BufferError {
621    /// Buffer which failed to resize
622    pub buf: BufferName,
623    /// Error returned by buffer resizing
624    #[source]
625    pub err: BufferSizeError,
626}
627
628/// Names of all buffers, used for error reporting
629#[derive(Debug)]
630pub enum BufferName {
631    /// Tiles from the 64² root tile pass
632    Tile64(TileBufferName),
633    /// Tiles from the 8² intermediate tile pass
634    Tile8(TileBufferName),
635    /// Buffer for tile tapes
636    TileTapes,
637    /// GPU-written image pixels (as [`RawDistancePixel`] values)
638    Pixel,
639    /// CPU-mappable image pixels (as [`RawDistancePixel`] values)
640    Image,
641    /// Variables
642    Vars,
643}
644
645impl std::fmt::Display for BufferName {
646    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
647        match self {
648            BufferName::Tile64(buf) => write!(f, "`{buf}` tile64"),
649            BufferName::Tile8(buf) => write!(f, "`{buf}` tile8"),
650            BufferName::TileTapes => write!(f, "`tile tapes`"),
651            BufferName::Pixel => write!(f, "`pixel`"),
652            BufferName::Image => write!(f, "`image`"),
653            BufferName::Vars => write!(f, "`vars`"),
654        }
655    }
656}
657
658/// Error returned when submitting a voxel rasterization job to the GPU
659#[derive(Debug, thiserror::Error)]
660pub enum SubmitError {
661    /// Missing variable when evaluating
662    #[error(transparent)]
663    MissingVar(#[from] MissingVar),
664    /// Error while resizing buffers
665    #[error(transparent)]
666    Buffers(#[from] BufferError),
667}
668
669impl From<CopyVarsError> for SubmitError {
670    fn from(value: CopyVarsError) -> Self {
671        match value {
672            CopyVarsError::MissingVar(v) => Self::MissingVar(v),
673            CopyVarsError::BufferSize(err) => Self::Buffers(BufferError {
674                buf: BufferName::Vars,
675                err,
676            }),
677        }
678    }
679}
680
681/// Cached bind groups (constructed on-demand)
682#[derive(Default)]
683struct BindGroups {
684    common: std::cell::OnceCell<wgpu::BindGroup>,
685    root_tiles: std::cell::OnceCell<wgpu::BindGroup>,
686    interval_tiles: std::cell::OnceCell<wgpu::BindGroup>,
687    pixel_tiles: std::cell::OnceCell<wgpu::BindGroup>,
688    merge: std::cell::OnceCell<wgpu::BindGroup>,
689}
690
691impl BindGroups {
692    fn common(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup {
693        self.common.get_or_init(|| {
694            ctx.gpu
695                .device
696                .create_bind_group(&wgpu::BindGroupDescriptor {
697                    label: Some("common bind group"),
698                    layout: &ctx.common_bind_group_layout,
699                    entries: &[
700                        wgpu::BindGroupEntry {
701                            binding: 0,
702                            resource: workspace.config_buf.as_entire_binding(),
703                        },
704                        wgpu::BindGroupEntry {
705                            binding: 1,
706                            resource: workspace.tile_tapes.bind_active(),
707                        },
708                        wgpu::BindGroupEntry {
709                            binding: 2,
710                            resource: workspace.vars_buf.bind_active(),
711                        },
712                    ],
713                })
714        })
715    }
716
717    fn root_tiles(
718        &self,
719        ctx: &Context,
720        workspace: &Workspace,
721    ) -> &wgpu::BindGroup {
722        self.root_tiles.get_or_init(|| {
723            ctx.gpu
724                .device
725                .create_bind_group(&wgpu::BindGroupDescriptor {
726                    label: Some("interval root bind group"),
727                    layout: &ctx.root_ctx.bind_group_layout,
728                    entries: &[
729                        wgpu::BindGroupEntry {
730                            binding: 0,
731                            resource: workspace.tile64.tiles.bind_active(),
732                        },
733                        wgpu::BindGroupEntry {
734                            binding: 1,
735                            resource: workspace.tile64.values.bind_active(),
736                        },
737                    ],
738                })
739        })
740    }
741
742    fn interval_tiles(
743        &self,
744        ctx: &Context,
745        workspace: &Workspace,
746    ) -> &wgpu::BindGroup {
747        self.interval_tiles.get_or_init(|| {
748            ctx.gpu
749                .device
750                .create_bind_group(&wgpu::BindGroupDescriptor {
751                    label: Some("interval tiles bind group"),
752                    layout: &ctx.tiles_ctx.bind_group_layout,
753                    entries: &[
754                        wgpu::BindGroupEntry {
755                            binding: 0,
756                            resource: workspace.tile64.tiles.bind_active(),
757                        },
758                        wgpu::BindGroupEntry {
759                            binding: 1,
760                            resource: workspace.tile8.tiles.bind_active(),
761                        },
762                        wgpu::BindGroupEntry {
763                            binding: 2,
764                            resource: workspace.tile8.values.bind_active(),
765                        },
766                    ],
767                })
768        })
769    }
770
771    fn pixel_tiles(
772        &self,
773        ctx: &Context,
774        workspace: &Workspace,
775    ) -> &wgpu::BindGroup {
776        self.pixel_tiles.get_or_init(|| {
777            ctx.gpu
778                .device
779                .create_bind_group(&wgpu::BindGroupDescriptor {
780                    label: Some("pixel tiles bind group"),
781                    layout: &ctx.pixels_ctx.bind_group_layout,
782                    entries: &[
783                        wgpu::BindGroupEntry {
784                            binding: 0,
785                            resource: workspace.tile8.tiles.bind_active(),
786                        },
787                        wgpu::BindGroupEntry {
788                            binding: 1,
789                            resource: workspace.pixels.bind_active(),
790                        },
791                    ],
792                })
793        })
794    }
795
796    fn merge(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup {
797        self.merge.get_or_init(|| {
798            ctx.gpu
799                .device
800                .create_bind_group(&wgpu::BindGroupDescriptor {
801                    label: Some("merge bind group"),
802                    layout: &ctx.merge_ctx.bind_group_layout,
803                    entries: &[
804                        wgpu::BindGroupEntry {
805                            binding: 0,
806                            resource: workspace.tile64.values.bind_active(),
807                        },
808                        wgpu::BindGroupEntry {
809                            binding: 1,
810                            resource: workspace.tile8.values.bind_active(),
811                        },
812                        wgpu::BindGroupEntry {
813                            binding: 2,
814                            resource: workspace.pixels.bind_active(),
815                        },
816                    ],
817                })
818        })
819    }
820}
821
822////////////////////////////////////////////////////////////////////////////////
823
824tag!(TilesBufferTag, u32, usize, STORAGE | COPY_DST | INDIRECT);
825tag!(ValuesBufferTag, u32, ImageSize, STORAGE | COPY_DST);
826
827/// Root tile buffers store strata-packed tile lists
828struct TileBuffers<const N: usize> {
829    /// Output tiles
830    tiles: FlexBuffer<TilesBufferTag>,
831
832    /// Tile values (empty / full)
833    values: FlexBuffer<ValuesBufferTag>,
834}
835
836/// Error type when resizing root tile buffers
837#[derive(Debug, thiserror::Error)]
838#[error("failed to resize `{buf}` root tile buffer")]
839pub struct TileBufferError {
840    /// Buffer which failed to resize
841    pub buf: TileBufferName,
842    /// Error returned by buffer resizing
843    #[source]
844    pub err: BufferSizeError,
845}
846
847/// Names of buffers used by the root tile rendering pass (for error reporting)
848#[derive(Debug)]
849#[expect(missing_docs)]
850pub enum TileBufferName {
851    Tiles,
852    Values,
853}
854
855impl std::fmt::Display for TileBufferName {
856    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
857        let s = match self {
858            TileBufferName::Tiles => "tiles",
859            TileBufferName::Values => "values",
860        };
861        s.fmt(f)
862    }
863}
864
865impl<const N: usize> TileBuffers<N> {
866    /// Build a new root tiles buffer, which stores strata-packed tile lists
867    fn new(
868        device: &wgpu::Device,
869        render_size: TileRenderSize,
870    ) -> Result<Self, TileBufferError> {
871        // Allocate enough words to write all of the output tiles
872        let tiles = FlexBuffer::new(
873            device,
874            format!("tiles_out{N}"),
875            Self::tiles_buf_size(render_size),
876        )
877        .map_err(|err| TileBufferError {
878            buf: TileBufferName::Tiles,
879            err,
880        })?;
881
882        let values_buf_size = Self::values_buf_size(render_size);
883        let values =
884            FlexBuffer::new(device, format!("tile{N}_values"), values_buf_size)
885                .map_err(|err| TileBufferError {
886                    buf: TileBufferName::Values,
887                    err,
888                })?;
889        Ok(Self { tiles, values })
890    }
891
892    fn tiles_buf_size(render_size: TileRenderSize) -> usize {
893        let nx = usize::try_from(render_size.nx()).unwrap();
894        let ny = usize::try_from(render_size.ny()).unwrap();
895        // wg_dispatch: [u32; 3] (unused)
896        // count: u32,
897        4 + nx
898            .checked_mul(ny)
899            .unwrap()
900            .checked_mul((64 / N) * (64 / N))
901            .unwrap()
902    }
903
904    fn values_buf_size(render_size: TileRenderSize) -> ImageSize {
905        ImageSize::new(
906            render_size.nx().checked_mul(64 / N as u32).unwrap(),
907            render_size.ny().checked_mul(64 / N as u32).unwrap(),
908        )
909    }
910
911    /// Grows all of the buffers to fit a particular render size
912    fn grow_to_fit(
913        &mut self,
914        device: &wgpu::Device,
915        render_size: TileRenderSize,
916    ) -> Result<(), TileBufferError> {
917        // Destructure to make sure we take all members into account
918        let TileBuffers { tiles, values } = self;
919        tiles
920            .grow_to_fit(device, Self::tiles_buf_size(render_size))
921            .map_err(|err| TileBufferError {
922                buf: TileBufferName::Tiles,
923                err,
924            })?;
925        values
926            .grow_to_fit(device, Self::values_buf_size(render_size))
927            .map_err(|err| TileBufferError {
928                buf: TileBufferName::Values,
929                err,
930            })?;
931
932        Ok(())
933    }
934
935    /// Returns the number of bytes in use by buffers
936    pub fn size(&self) -> u64 {
937        // Destructure to make sure we take all members into account
938        let TileBuffers { tiles, values } = self;
939        tiles.size_bytes() + values.size_bytes()
940    }
941
942    /// Returns the number of bytes allocated to buffers
943    pub fn capacity(&self) -> u64 {
944        // Destructure to make sure we take all members into account
945        let TileBuffers { tiles, values } = self;
946        tiles.capacity() + values.capacity()
947    }
948}
949
950////////////////////////////////////////////////////////////////////////////////
951
952/// Context for 2D (distance field) rendering
953pub struct Context {
954    gpu: Gpu,
955
956    /// Bind group layout for the common bind group (used by all stages)
957    common_bind_group_layout: wgpu::BindGroupLayout,
958
959    /// Context for root tile evaluation (64²)
960    root_ctx: RootContext,
961
962    /// Context for second-stage tile evaluation (generating 8² tiles)
963    tiles_ctx: IntervalTilesContext,
964
965    /// Context for per-pixel evaluation (taking 8² tiles)
966    pixels_ctx: PixelTilesContext,
967
968    /// Context which resets buffers before evaluation
969    reset_ctx: ResetContext,
970
971    /// Context to merge tile and pixel images
972    merge_ctx: MergeContext,
973}
974
975impl Context {
976    /// Build a new 2D rendering context, given a device and queue
977    ///
978    /// If render timestamps are desirable, then the device should be
979    /// initialized with [`wgpu::Features::TIMESTAMP_QUERY`].
980    pub fn new(gpu: &Gpu) -> Self {
981        // Create bind group layout and bind group
982        let common_bind_group_layout = gpu.device.create_bind_group_layout(
983            &wgpu::BindGroupLayoutDescriptor {
984                label: Some("common bind group layout"),
985                entries: &[
986                    buffer_rw(0), // config (including tape buffer)
987                    buffer_rw(1), // tile_tape (hierarchical)
988                    buffer_ro(2), // vars
989                ],
990            },
991        );
992        let root_ctx = RootContext::new(&gpu.device, &common_bind_group_layout);
993        let tiles_ctx =
994            IntervalTilesContext::new(&gpu.device, &common_bind_group_layout);
995        let pixels_ctx =
996            PixelTilesContext::new(&gpu.device, &common_bind_group_layout);
997        let merge_ctx =
998            MergeContext::new(&gpu.device, &common_bind_group_layout);
999
1000        Self {
1001            gpu: gpu.clone(),
1002            common_bind_group_layout,
1003            root_ctx,
1004            tiles_ctx,
1005            pixels_ctx,
1006            merge_ctx,
1007            reset_ctx: ResetContext,
1008        }
1009    }
1010
1011    /// Builds a new [`Workspace`] object for use in rendering
1012    ///
1013    /// The buffers are initialized with a dummy size and resized automatically
1014    /// when passed into any of the runner functions (e.g. [`run`](Self::run) or
1015    /// [`submit`](Self::submit)).
1016    pub fn workspace(&self) -> Workspace {
1017        Workspace::new(&self.gpu.device)
1018    }
1019
1020    /// Renders the image, with a blocking wait to read pixel data from the GPU
1021    ///
1022    /// This function is not present when built for the `wasm32` target
1023    #[cfg(not(target_arch = "wasm32"))]
1024    pub fn run(
1025        &self,
1026        shape: &RenderShape,
1027        workspace: &mut Workspace,
1028        out: &mut ReadBuffer<PixelBufferTag>,
1029        settings: RenderConfig,
1030    ) -> Result<Image, SubmitError> {
1031        self.run_with_vars(shape, &Default::default(), workspace, out, settings)
1032    }
1033
1034    /// Renders the image, with a blocking wait to read pixel data from the GPU
1035    ///
1036    /// This function is not present when built for the `wasm32` target
1037    #[cfg(not(target_arch = "wasm32"))]
1038    pub fn run_with_vars(
1039        &self,
1040        shape: &RenderShape,
1041        vars: &ShapeVars<f32>,
1042        workspace: &mut Workspace,
1043        out: &mut ReadBuffer<PixelBufferTag>,
1044        settings: RenderConfig,
1045    ) -> Result<Image, SubmitError> {
1046        self.submit_with_vars(shape, vars, workspace, &settings)?;
1047        self.gpu.copy(workspace.output(), out);
1048        let image = self.gpu.map_image(out);
1049        Ok(image.image())
1050    }
1051
1052    /// Renders the image, with an async wait to read pixel data from the GPU
1053    pub async fn run_async(
1054        &self,
1055        shape: &RenderShape,
1056        workspace: &mut Workspace,
1057        out: &mut ReadBuffer<PixelBufferTag>,
1058        settings: RenderConfig,
1059    ) -> Result<Image, SubmitError> {
1060        self.run_with_vars_async(
1061            shape,
1062            &Default::default(),
1063            workspace,
1064            out,
1065            settings,
1066        )
1067        .await
1068    }
1069
1070    /// Renders the image, with an async wait to read pixel data from the GPU
1071    pub async fn run_with_vars_async(
1072        &self,
1073        shape: &RenderShape,
1074        vars: &ShapeVars<f32>,
1075        workspace: &mut Workspace,
1076        out: &mut ReadBuffer<PixelBufferTag>,
1077        settings: RenderConfig,
1078    ) -> Result<Image, SubmitError> {
1079        self.submit_with_vars(shape, vars, workspace, &settings)?;
1080        self.gpu.copy(workspace.output(), out);
1081        let image = self.gpu.map_image_async(out).await;
1082        Ok(image.image())
1083    }
1084
1085    /// Submits a single image to be rendered on the GPU
1086    ///
1087    /// The resulting image (as a buffer of [`RawDistancePixel`] data) is
1088    /// available on the GPU in [`buffers.output()`](Workspace::output).
1089    pub fn submit(
1090        &self,
1091        shape: &RenderShape,
1092        workspace: &mut Workspace,
1093        settings: &RenderConfig,
1094    ) -> Result<(), SubmitError> {
1095        self.submit_with_vars(shape, &Default::default(), workspace, settings)
1096    }
1097
1098    /// Submits a single image to be rendered on the GPU, with extra variables
1099    ///
1100    /// See [`submit`](Self::submit) for additional details.
1101    pub fn submit_with_vars(
1102        &self,
1103        shape: &RenderShape,
1104        vars: &ShapeVars<f32>,
1105        workspace: &mut Workspace,
1106        settings: &RenderConfig,
1107    ) -> Result<(), SubmitError> {
1108        workspace.set_image_size(&self.gpu.device, settings.image_size)?;
1109        let render_size = TileRenderSize::from(workspace.image_size);
1110
1111        // The WebGPU config type has a mat3x3f, but that type pads each row to
1112        // 16 bytes, so we'll just use a mat4x4 for simplicity
1113        let mat =
1114            settings.world_to_model * workspace.image_size.screen_to_world();
1115        let mut mat4 = nalgebra::Matrix4x3::<f32>::identity();
1116        mat4.fixed_view_mut::<3, 3>(0, 0).copy_from(&mat);
1117
1118        // Divide by 2 to go from `u32` -> `TapeWord`
1119        let start_offset = u32::try_from(shape.bytecode.len()).unwrap() / 2;
1120        let config = Config {
1121            mat: mat4.data.as_slice().try_into().unwrap(),
1122            axes: shape.axes(),
1123            render_size: [render_size.width(), render_size.height()],
1124            tape_data_capacity: TAPE_DATA_CAPACITY.try_into().unwrap(),
1125            image_size: [
1126                workspace.image_size.width(),
1127                workspace.image_size.height(),
1128            ],
1129            tape_data_offset: start_offset,
1130            z: settings.z,
1131            pixel_perfect: settings.pixel_perfect as u32,
1132            _pad: 0,
1133        };
1134
1135        {
1136            // We load the `Config` and shape tape data.
1137            let config_len = std::mem::size_of_val(&config);
1138            let mut writer = self
1139                .gpu
1140                .queue
1141                .write_buffer_with(
1142                    &workspace.config_buf,
1143                    0,
1144                    ((config_len + shape.bytecode.as_bytes().len()) as u64)
1145                        .try_into()
1146                        .unwrap(),
1147                )
1148                .unwrap();
1149            writer
1150                .slice(..config_len)
1151                .copy_from_slice(config.as_bytes());
1152            writer
1153                .slice(config_len..)
1154                .copy_from_slice(shape.bytecode.as_bytes());
1155        }
1156
1157        // Copy vars (if present), then reset relevant bind groups if the buffer
1158        // size has changed.
1159        if matches!(
1160            shape.copy_vars(&self.gpu, vars, &mut workspace.vars_buf)?,
1161            CopyVarsChanged::BufferChanged
1162        ) {
1163            workspace.bind_groups.common = Default::default();
1164        }
1165
1166        // Create a command encoder and dispatch the compute work
1167        let mut encoder = self.gpu.device.create_command_encoder(
1168            &wgpu::CommandEncoderDescriptor { label: None },
1169        );
1170
1171        // Initial buffer reset pass
1172        self.reset_ctx.run(&mut encoder, workspace);
1173
1174        let mut compute_pass =
1175            encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1176                label: None,
1177                timestamp_writes: None,
1178            });
1179
1180        // Build the common config buffer
1181        let common_bind_group = workspace.bind_groups.common(self, workspace);
1182        compute_pass.set_bind_group(0, common_bind_group, &[]);
1183
1184        // Populate root tiles (64x64x64, densely packed)
1185        self.root_ctx.run(
1186            self,
1187            workspace,
1188            shape.bytecode.reg_count(),
1189            render_size,
1190            &mut compute_pass,
1191        );
1192        self.tiles_ctx.run(
1193            self,
1194            workspace,
1195            shape.bytecode.reg_count(),
1196            &mut compute_pass,
1197        );
1198        self.pixels_ctx.run(
1199            self,
1200            workspace,
1201            shape.bytecode.reg_count(),
1202            &mut compute_pass,
1203        );
1204
1205        // Merge filled tiles from large -> small
1206        self.merge_ctx.run(
1207            self,
1208            workspace,
1209            settings.image_size,
1210            &mut compute_pass,
1211        );
1212        drop(compute_pass);
1213
1214        // Submit the commands and wait for the GPU to complete
1215        self.gpu.queue.submit(Some(encoder.finish()));
1216        Ok(())
1217    }
1218}
1219
1220////////////////////////////////////////////////////////////////////////////////
1221
1222struct MergeContext {
1223    bind_group_layout: wgpu::BindGroupLayout,
1224    pipeline: wgpu::ComputePipeline,
1225}
1226
1227impl MergeContext {
1228    fn new(
1229        device: &wgpu::Device,
1230        common_bind_group_layout: &wgpu::BindGroupLayout,
1231    ) -> Self {
1232        let shader_code = merge_shader();
1233
1234        // Create bind group layout and bind group
1235        let bind_group_layout =
1236            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1237                label: Some("merge bind group layout"),
1238                entries: &[
1239                    buffer_ro(0), // tile64_values
1240                    buffer_ro(1), // tile8_values
1241                    buffer_rw(2), // pixels
1242                ],
1243            });
1244
1245        // Create the compute pipeline
1246        let pipeline_layout =
1247            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1248                label: Some("merge pipeline layout"),
1249                bind_group_layouts: &[
1250                    Some(common_bind_group_layout),
1251                    Some(&bind_group_layout),
1252                ],
1253                immediate_size: 0u32,
1254            });
1255
1256        // Compile the shader
1257        let shader_module =
1258            device.create_shader_module(wgpu::ShaderModuleDescriptor {
1259                label: Some("merge shader module"),
1260                source: wgpu::ShaderSource::Wgsl(shader_code.into()),
1261            });
1262
1263        let pipeline =
1264            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1265                label: Some("merge"),
1266                layout: Some(&pipeline_layout),
1267                module: &shader_module,
1268                entry_point: Some("merge_main"),
1269                compilation_options: Default::default(),
1270                cache: None,
1271            });
1272
1273        Self {
1274            pipeline,
1275            bind_group_layout,
1276        }
1277    }
1278
1279    fn run(
1280        &self,
1281        ctx: &Context,
1282        workspace: &Workspace,
1283        render_size: ImageSize,
1284        compute_pass: &mut wgpu::ComputePass,
1285    ) {
1286        let bind_group = workspace.bind_groups.merge(ctx, workspace);
1287        compute_pass.set_pipeline(&self.pipeline);
1288        compute_pass.set_bind_group(1, bind_group, &[]);
1289        compute_pass.dispatch_workgroups(
1290            render_size.width().div_ceil(8),
1291            render_size.height().div_ceil(8),
1292            1,
1293        );
1294    }
1295}
1296////////////////////////////////////////////////////////////////////////////////
1297
1298struct ResetContext;
1299
1300impl ResetContext {
1301    fn run(&self, encoder: &mut wgpu::CommandEncoder, workspace: &Workspace) {
1302        // Clear `count` and `wg_size` members of the tile output buffers
1303        encoder.clear_buffer(workspace.tile64.tiles.data(), 0, Some(16));
1304        encoder.clear_buffer(workspace.tile8.tiles.data(), 0, Some(16));
1305        workspace.tile64.values.clear(encoder);
1306        workspace.tile8.values.clear(encoder);
1307        workspace.pixels.clear(encoder);
1308
1309        // Clear the whole tile tape map (TODO is this needed?)
1310        workspace.tile_tapes.clear(encoder);
1311    }
1312}
1313
1314////////////////////////////////////////////////////////////////////////////////
1315
1316#[cfg(test)]
1317mod test {
1318    use super::effects::ColorSettings;
1319    use super::*;
1320    use crate::color::{ShapeColor, ShapeColorBuffers};
1321
1322    use fidget_core::{context::Tree, vm::VmShape};
1323
1324    #[test]
1325    fn compile_interval_root_shader() {
1326        crate::compile_shader(&interval_root_shader(16), "interval root");
1327    }
1328
1329    #[test]
1330    fn compile_interval_tiles_shader() {
1331        crate::compile_shader(&interval_tiles_shader(16), "interval tiles");
1332    }
1333
1334    #[test]
1335    fn compile_pixel_tiles_shader() {
1336        crate::compile_shader(&pixel_tiles_shader(16), "pixel tiles");
1337    }
1338
1339    #[test]
1340    fn compile_merge_shader() {
1341        crate::compile_shader(&merge_shader(), "merge");
1342    }
1343
1344    struct RenderOutput {
1345        distance: Image,
1346        color: fidget_raster::Image<u32, ImageSize>,
1347    }
1348
1349    fn render(
1350        shapes: &[(Tree, ShapeColor<Tree>)],
1351        render_config: RenderConfig,
1352        vars: &fidget_core::shape::ShapeVars<f32>,
1353    ) -> RenderOutput {
1354        let gpu = pollster::block_on(Gpu::init_basic()).unwrap();
1355        let pixel_ctx = Context::new(&gpu);
1356        let effects_ctx = effects::Context::new(&gpu);
1357
1358        let mut buf = pixel_ctx.workspace();
1359        let mut merge_buf = effects_ctx.merge_workspace();
1360
1361        // Render and accumulate each shape
1362        for (shape, _) in shapes {
1363            let shape =
1364                RenderShape::new(&VmShape::from(shape.clone())).unwrap();
1365            pixel_ctx
1366                .submit_with_vars(&shape, vars, &mut buf, &render_config)
1367                .unwrap();
1368            effects_ctx
1369                .submit_merge(buf.output(), true, &mut merge_buf)
1370                .unwrap();
1371        }
1372        let shape_colors = shapes
1373            .iter()
1374            .map(|(_, c)| match c {
1375                ShapeColor::Rgb { r, g, b } => ShapeColor::Rgb {
1376                    r: VmShape::from(r.clone()),
1377                    g: VmShape::from(g.clone()),
1378                    b: VmShape::from(b.clone()),
1379                },
1380                ShapeColor::Hsl { h, s, l } => ShapeColor::Hsl {
1381                    h: VmShape::from(h.clone()),
1382                    s: VmShape::from(s.clone()),
1383                    l: VmShape::from(l.clone()),
1384                },
1385            })
1386            .collect::<Vec<_>>();
1387        let shape_colors = ShapeColorBuffers::new(&shape_colors).unwrap();
1388        let mut color_workspace = effects_ctx.color_workspace();
1389
1390        // Compute per-pixel colors
1391        effects_ctx
1392            .submit_color_with_vars(
1393                &mut merge_buf,
1394                ColorSettings {
1395                    z: 0.0,
1396                    only_filled: false,
1397                    world_to_model: render_config.world_to_model,
1398                },
1399                &shape_colors,
1400                &mut color_workspace,
1401                vars,
1402            )
1403            .unwrap();
1404
1405        let mut distance_out = gpu.read_buffer_for(merge_buf.output_distance());
1406        let mut color_out =
1407            gpu.read_buffer_for(merge_buf.output_color().unwrap());
1408        gpu.copy(merge_buf.output_distance(), &mut distance_out);
1409        gpu.copy(merge_buf.output_color().unwrap(), &mut color_out);
1410
1411        RenderOutput {
1412            color: gpu.map_image(&mut color_out).image(),
1413            distance: gpu.map_image(&mut distance_out).image(),
1414        }
1415    }
1416
1417    #[test]
1418    fn pixel_pipeline() {
1419        // We only run in CI if we're on MacOS (because other runners don't have
1420        // GPUs and will fail to build the context).
1421        #[cfg(not(target_os = "macos"))]
1422        if std::env::var("CI").is_ok() {
1423            return;
1424        }
1425
1426        let (x, y, _z) = Tree::axes();
1427        let circle = (x.square() + y.square()).sqrt() - Tree::constant(0.5);
1428
1429        // Test a variety of image sizes for correctness
1430        for image_size in [
1431            RenderSize::new(64, 64),
1432            RenderSize::new(128, 64),
1433            RenderSize::new(64, 128),
1434            RenderSize::new(27, 51),
1435        ] {
1436            let out = render(
1437                &[(
1438                    circle.clone(),
1439                    ShapeColor::Rgb {
1440                        r: Tree::x(),
1441                        g: Tree::y(),
1442                        b: Tree::constant(0.5),
1443                    },
1444                )],
1445                RenderConfig {
1446                    image_size,
1447                    world_to_model: nalgebra::Matrix3::identity(),
1448                    pixel_perfect: false,
1449                    z: 0.0,
1450                },
1451                &Default::default(),
1452            );
1453            assert_eq!(out.color.size(), image_size);
1454            assert_eq!(out.distance.size(), image_size);
1455
1456            // Basic circle inside/outside check
1457            let mat = image_size.screen_to_world();
1458            for j in 0..image_size.height() {
1459                for i in 0..image_size.width() {
1460                    let pos = mat.transform_point(&nalgebra::Point2::new(
1461                        i as f32, j as f32,
1462                    ));
1463                    let p = out.distance[(j as usize, i as usize)];
1464                    let r = (pos.x.powi(2) + pos.y.powi(2)).sqrt();
1465                    if r < 0.5 {
1466                        assert!(
1467                            p.inside(),
1468                            "pixel should be inside at pixel ({i}, {j}) \
1469                            (pos {pos}) with radius {r}"
1470                        );
1471                    } else {
1472                        assert!(
1473                            !p.inside(),
1474                            "pixel should be outside at pixel ({i}, {j}) \
1475                            (pos {pos}) with radius {r}"
1476                        );
1477                    }
1478                }
1479            }
1480
1481            for j in 0..image_size.height() {
1482                for i in 0..image_size.width() {
1483                    let pos = mat.transform_point(&nalgebra::Point2::new(
1484                        i as f32, j as f32,
1485                    ));
1486                    let p = out.color[(j as usize, i as usize)];
1487                    let r = (pos.x.powi(2) + pos.y.powi(2)).sqrt();
1488                    let alpha = if r < 0.5 { 255 } else { 0 };
1489                    let expected_color = u32::from_ne_bytes([
1490                        (pos.x.clamp(0.0, 1.0) * 255.0) as u8,
1491                        (pos.y.clamp(0.0, 1.0) * 255.0) as u8,
1492                        127,
1493                        alpha,
1494                    ]);
1495                    assert_eq!(
1496                        p, expected_color,
1497                        "color mismatch at {i}, {j} ({pos})"
1498                    );
1499                }
1500            }
1501        }
1502    }
1503
1504    #[test]
1505    fn pixel_multiple_images() {
1506        // We only run in CI if we're on MacOS (because other runners don't have
1507        // GPUs and will fail to build the context).
1508        #[cfg(not(target_os = "macos"))]
1509        if std::env::var("CI").is_ok() {
1510            return;
1511        }
1512
1513        let circle_a = ((Tree::x() - 0.5).square() + Tree::y().square()).sqrt()
1514            - Tree::constant(0.25);
1515        let circle_b = ((Tree::x() + 0.5).square() + Tree::y().square()).sqrt()
1516            - Tree::constant(0.25);
1517
1518        // Test a variety of image sizes for correctness
1519        let image_size = RenderSize::new(64, 64);
1520        let out = render(
1521            &[
1522                (
1523                    circle_a,
1524                    ShapeColor::Rgb {
1525                        r: Tree::constant(0.0),
1526                        g: Tree::constant(0.0),
1527                        b: Tree::constant(1.0),
1528                    },
1529                ),
1530                (
1531                    circle_b,
1532                    ShapeColor::Rgb {
1533                        r: Tree::constant(0.0),
1534                        g: Tree::constant(1.0),
1535                        b: Tree::constant(0.0),
1536                    },
1537                ),
1538            ],
1539            RenderConfig {
1540                image_size,
1541                world_to_model: nalgebra::Matrix3::identity(),
1542                pixel_perfect: false,
1543                z: 0.0,
1544            },
1545            &Default::default(),
1546        );
1547        assert_eq!(out.color.size(), image_size);
1548        assert_eq!(out.distance.size(), image_size);
1549
1550        let mut pixels = String::new();
1551        for j in 0..image_size.height() {
1552            for i in 0..image_size.width() {
1553                let p = out.color[(j as usize, i as usize)];
1554                let c = match p.to_ne_bytes() {
1555                    [0, 0, 255, 0] => "b",
1556                    [0, 0, 255, 255] => "B",
1557                    [0, 255, 0, 0] => "g",
1558                    [0, 255, 0, 255] => "G",
1559                    _ => panic!("invalid color {p:?}"),
1560                };
1561                pixels += c;
1562            }
1563            pixels += "\n";
1564        }
1565
1566        assert_eq!(
1567            pixels,
1568            "\
1569            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1570            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1571            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1572            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1573            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1574            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1575            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1576            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1577            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1578            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1579            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1580            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1581            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1582            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1583            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1584            gggggggggggggggggggggggggggggggggggggggbbbbbbbbbbbbbbbbbbggggggg
1585            gggggggggggggggggggggggggggggggggggggggbbbbbbbbbbbbbbbbbbggggggg
1586            gggggggggggggggggggggggggggggggggggggggbbbbbbbbbbbbbbbbbbggggggg
1587            gggggggggggggggggggggggggggggggggggggggbbbbbbbbbbbbbbbbbbggggggg
1588            gggggggggggggggggggggggggggggggggggggggbbbbbbbbbbbbbbbbbbggggggg
1589            gggggggggggggggggggggggggggggggggggggggbbbbbbbbbbbbbbbbbbggggggg
1590            gggggggggggggggggggggggggggggggggggggggbbbbbbbbbbbbbbbbbbggggggg
1591            gggggggggggggggggggggggggggggggggggggggbbbbbbbbbbbbbbbbbbggggggg
1592            gggggggggggggggggggggggggggggggggbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
1593            gggggggggggggGGGGGGGgggggggggggggbbbbbbbbbbbbBBBBBBBbbbbbbbbbbbb
1594            gggggggggggGGGGGGGGGGGgggggggggggbbbbbbbbbbBBBBBBBBBBBbbbbbbbbbb
1595            ggggggggggGGGGGGGGGGGGGggggggggggbbbbbbbbbBBBBBBBBBBBBBbbbbbbbbb
1596            ggggggggggGGGGGGGGGGGGGggggggggggbbbbbbbbbBBBBBBBBBBBBBbbbbbbbbb
1597            gggggggggGGGGGGGGGGGGGGGgggggggggbbbbbbbbBBBBBBBBBBBBBBBbbbbbbbb
1598            gggggggggGGGGGGGGGGGGGGGgggggggggbbbbbbbbBBBBBBBBBBBBBBBbbbbbbbb
1599            gggggggggGGGGGGGGGGGGGGGgggggggggbbbbbbbbBBBBBBBBBBBBBBBbbbbbbbb
1600            gggggggggGGGGGGGGGGGGGGGgggggggggbbbbbbbbBBBBBBBBBBBBBBBbbbbbbbb
1601            gggggggggGGGGGGGGGGGGGGGgggggggggbbbbbbbbBBBBBBBBBBBBBBBbbbbbbbb
1602            gggggggggGGGGGGGGGGGGGGGgggggggggggggggbbBBBBBBBBBBBBBBBgggggggg
1603            gggggggggGGGGGGGGGGGGGGGgggggggggggggggbbBBBBBBBBBBBBBBBbggggggg
1604            ggggggggggGGGGGGGGGGGGGggggggggggggggggbbbBBBBBBBBBBBBBbbggggggg
1605            ggggggggggGGGGGGGGGGGGGggggggggggggggggbbbBBBBBBBBBBBBBbbggggggg
1606            gggggggggggGGGGGGGGGGGgggggggggggggggggbbbbBBBBBBBBBBBbbbggggggg
1607            gggggggggggggGGGGGGGgggggggggggggggggggbbbbbbBBBBBBBbbbbbggggggg
1608            gggggggggggggggggggggggggggggggggggggggbbbbbbbbbbbbbbbbbbggggggg
1609            gggggggggggggggggggggggggggggggggggggggbbbbbbbbbbbbbbbbbbggggggg
1610            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1611            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1612            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1613            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1614            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1615            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1616            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1617            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1618            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1619            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1620            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1621            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1622            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1623            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1624            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1625            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1626            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1627            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1628            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1629            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1630            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1631            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1632            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1633            "
1634            .replace(" ", "")
1635        );
1636    }
1637
1638    #[test]
1639    fn pixel_vars() {
1640        // We only run in CI if we're on MacOS (because other runners don't have
1641        // GPUs and will fail to build the context).
1642        #[cfg(not(target_os = "macos"))]
1643        if std::env::var("CI").is_ok() {
1644            return;
1645        }
1646
1647        let va = fidget_core::var::Var::new();
1648        let vb = fidget_core::var::Var::new();
1649        let vc = fidget_core::var::Var::new();
1650        let vd = fidget_core::var::Var::new();
1651        let circle_a = ((Tree::x() - 0.5).square() + Tree::y().square()).sqrt()
1652            - Tree::from(va);
1653        let circle_b = ((Tree::x() + va).square() + Tree::y().square()).sqrt()
1654            - Tree::from(vb);
1655
1656        // Test a variety of image sizes for correctness
1657        let image_size = RenderSize::new(64, 64);
1658        let mut vars = fidget_core::shape::ShapeVars::new();
1659        vars.insert(va.index().unwrap(), 0.25);
1660        vars.insert(vb.index().unwrap(), 0.55);
1661        vars.insert(vc.index().unwrap(), 0.75);
1662        vars.insert(vd.index().unwrap(), 0.5);
1663        let out = render(
1664            &[
1665                (
1666                    circle_a,
1667                    ShapeColor::Rgb {
1668                        r: Tree::constant(0.0),
1669                        g: Tree::constant(0.0),
1670                        b: Tree::from(vc),
1671                    },
1672                ),
1673                (
1674                    circle_b,
1675                    ShapeColor::Rgb {
1676                        r: Tree::constant(0.0),
1677                        g: Tree::from(vd),
1678                        b: Tree::constant(0.0),
1679                    },
1680                ),
1681            ],
1682            RenderConfig {
1683                image_size,
1684                world_to_model: nalgebra::Matrix3::identity(),
1685                pixel_perfect: false,
1686                z: 0.0,
1687            },
1688            &vars,
1689        );
1690        assert_eq!(out.color.size(), image_size);
1691        assert_eq!(out.distance.size(), image_size);
1692
1693        let mut pixels = String::new();
1694        for j in 0..image_size.height() {
1695            for i in 0..image_size.width() {
1696                let p = out.color[(j as usize, i as usize)];
1697                let c = match p.to_ne_bytes() {
1698                    [0, 0, 191, 0] => "b",
1699                    [0, 0, 191, 255] => "B",
1700                    [0, 127, 0, 0] => "g",
1701                    [0, 127, 0, 255] => "G",
1702                    _ => panic!("invalid color {:?}", p.to_ne_bytes()),
1703                };
1704                pixels += c;
1705            }
1706            pixels += "\n";
1707        }
1708
1709        assert_eq!(
1710            pixels,
1711            "\
1712            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1713            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1714            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1715            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1716            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1717            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1718            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1719            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1720            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1721            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1722            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1723            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1724            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1725            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1726            ggggggggggggggggggggGGGGGGGGGggggggggggggggggggggggggggggggggggg
1727            gggggggggggggggggGGGGGGGGGGGGGGGgggggggggggggbbbbbbbbbbbbggggggg
1728            gggggggggggggggGGGGGGGGGGGGGGGGGGGgggggggggggbbbbbbbbbbbbggggggg
1729            ggggggggggggggGGGGGGGGGGGGGGGGGGGGGgggggggggbbbbbbbbbbbbbggggggg
1730            gggggggggggggGGGGGGGGGGGGGGGGGGGGGGGggggggggbbbbbbbbbbbbbggggggg
1731            ggggggggggggGGGGGGGGGGGGGGGGGGGGGGGGGgggggggbbbbbbbbbbbbbggggggg
1732            gggggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGgggggbbbbbbbbbbbbbbggggggg
1733            ggggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGggggbbbbbbbbbbbbbbggggggg
1734            gggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGgggbbbbbbbbbbbbbbggggggg
1735            gggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGggbbbbbbbbbbbbbbbbbbbbbb
1736            ggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGgbbbBBBBBBBbbbbbbbbbbbb
1737            ggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGgbBBBBBBBBBBBbbbbbbbbbb
1738            ggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGgBBBBBBBBBBBBBbbbbbbbbb
1739            gggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGBBBBBBBBBBBBBbbbbbbbbb
1740            gggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGBBBBBBBBBBBBBBbbbbbbbb
1741            gggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGBBBBBBBBBBBBBBbbbbbbbb
1742            gggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGBBBBBBBBBBBBBBbbbbbbbb
1743            gggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGBBBBBBBBBBBBBBbbbbbbbb
1744            gggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGBBBBBBBBBBBBBBbbbbbbbb
1745            gggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGBBBBBBBBBBBBBBgggggggg
1746            gggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGBBBBBBBBBBBBBBbggggggg
1747            gggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGBBBBBBBBBBBBBbbggggggg
1748            ggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGgBBBBBBBBBBBBBbbggggggg
1749            ggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGgbBBBBBBBBBBBbbbggggggg
1750            ggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGgbbbBBBBBBBbbbbbggggggg
1751            gggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGggbbbbbbbbbbbbbbbggggggg
1752            gggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGggbbbbbbbbbbbbbbbggggggg
1753            ggggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGGGggggggggggggggggggggggggg
1754            gggggggggggGGGGGGGGGGGGGGGGGGGGGGGGGGGgggggggggggggggggggggggggg
1755            ggggggggggggGGGGGGGGGGGGGGGGGGGGGGGGGggggggggggggggggggggggggggg
1756            gggggggggggggGGGGGGGGGGGGGGGGGGGGGGGgggggggggggggggggggggggggggg
1757            ggggggggggggggGGGGGGGGGGGGGGGGGGGGGggggggggggggggggggggggggggggg
1758            gggggggggggggggGGGGGGGGGGGGGGGGGGGgggggggggggggggggggggggggggggg
1759            gggggggggggggggggGGGGGGGGGGGGGGGgggggggggggggggggggggggggggggggg
1760            ggggggggggggggggggggGGGGGGGGGggggggggggggggggggggggggggggggggggg
1761            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1762            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1763            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1764            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1765            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1766            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1767            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1768            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1769            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1770            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1771            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1772            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1773            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1774            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1775            gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
1776            "
1777            .replace(" ", "")
1778        );
1779    }
1780
1781    #[test]
1782    fn pixel_hsl() {
1783        // We only run in CI if we're on MacOS (because other runners don't have
1784        // GPUs and will fail to build the context).
1785        #[cfg(not(target_os = "macos"))]
1786        if std::env::var("CI").is_ok() {
1787            return;
1788        }
1789
1790        let circle_a = ((Tree::x() - 0.5).square() + Tree::y().square()).sqrt()
1791            - Tree::constant(0.25);
1792        let circle_b = ((Tree::x() + 0.5).square() + Tree::y().square()).sqrt()
1793            - Tree::constant(0.25);
1794
1795        // Test a variety of image sizes for correctness
1796        let image_size = RenderSize::new(64, 64);
1797        let out = render(
1798            &[
1799                (
1800                    circle_a,
1801                    ShapeColor::Hsl {
1802                        h: Tree::constant(0.0),
1803                        s: Tree::constant(1.0),
1804                        l: Tree::constant(0.5),
1805                    },
1806                ),
1807                (
1808                    circle_b,
1809                    ShapeColor::Hsl {
1810                        h: Tree::constant(0.5),
1811                        s: Tree::constant(1.0),
1812                        l: Tree::constant(0.5),
1813                    },
1814                ),
1815            ],
1816            RenderConfig {
1817                image_size,
1818                world_to_model: nalgebra::Matrix3::identity(),
1819                pixel_perfect: false,
1820                z: 0.0,
1821            },
1822            &Default::default(),
1823        );
1824        assert_eq!(out.color.size(), image_size);
1825        assert_eq!(out.distance.size(), image_size);
1826
1827        let mut pixels = String::new();
1828        for j in 0..image_size.height() {
1829            for i in 0..image_size.width() {
1830                let p = out.color[(j as usize, i as usize)];
1831                let c = match p.to_ne_bytes() {
1832                    [255, 0, 0, 0] => "r",
1833                    [255, 0, 0, 255] => "R",
1834                    [0, 255, 255, 0] => "c",
1835                    [0, 255, 255, 255] => "C",
1836                    _ => panic!("invalid color {:?}", p.to_ne_bytes()),
1837                };
1838                pixels += c;
1839            }
1840            pixels += "\n";
1841        }
1842
1843        assert_eq!(
1844            pixels,
1845            "\
1846            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1847            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1848            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1849            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1850            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1851            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1852            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1853            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1854            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1855            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1856            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1857            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1858            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1859            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1860            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1861            cccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrccccccc
1862            cccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrccccccc
1863            cccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrccccccc
1864            cccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrccccccc
1865            cccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrccccccc
1866            cccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrccccccc
1867            cccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrccccccc
1868            cccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrccccccc
1869            cccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
1870            cccccccccccccCCCCCCCcccccccccccccrrrrrrrrrrrrRRRRRRRrrrrrrrrrrrr
1871            cccccccccccCCCCCCCCCCCcccccccccccrrrrrrrrrrRRRRRRRRRRRrrrrrrrrrr
1872            ccccccccccCCCCCCCCCCCCCccccccccccrrrrrrrrrRRRRRRRRRRRRRrrrrrrrrr
1873            ccccccccccCCCCCCCCCCCCCccccccccccrrrrrrrrrRRRRRRRRRRRRRrrrrrrrrr
1874            cccccccccCCCCCCCCCCCCCCCcccccccccrrrrrrrrRRRRRRRRRRRRRRRrrrrrrrr
1875            cccccccccCCCCCCCCCCCCCCCcccccccccrrrrrrrrRRRRRRRRRRRRRRRrrrrrrrr
1876            cccccccccCCCCCCCCCCCCCCCcccccccccrrrrrrrrRRRRRRRRRRRRRRRrrrrrrrr
1877            cccccccccCCCCCCCCCCCCCCCcccccccccrrrrrrrrRRRRRRRRRRRRRRRrrrrrrrr
1878            cccccccccCCCCCCCCCCCCCCCcccccccccrrrrrrrrRRRRRRRRRRRRRRRrrrrrrrr
1879            cccccccccCCCCCCCCCCCCCCCcccccccccccccccrrRRRRRRRRRRRRRRRcccccccc
1880            cccccccccCCCCCCCCCCCCCCCcccccccccccccccrrRRRRRRRRRRRRRRRrccccccc
1881            ccccccccccCCCCCCCCCCCCCccccccccccccccccrrrRRRRRRRRRRRRRrrccccccc
1882            ccccccccccCCCCCCCCCCCCCccccccccccccccccrrrRRRRRRRRRRRRRrrccccccc
1883            cccccccccccCCCCCCCCCCCcccccccccccccccccrrrrRRRRRRRRRRRrrrccccccc
1884            cccccccccccccCCCCCCCcccccccccccccccccccrrrrrrRRRRRRRrrrrrccccccc
1885            cccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrccccccc
1886            cccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrccccccc
1887            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1888            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1889            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1890            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1891            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1892            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1893            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1894            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1895            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1896            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1897            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1898            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1899            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1900            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1901            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1902            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1903            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1904            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1905            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1906            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1907            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1908            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1909            cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1910            "
1911            .replace(" ", "")
1912        );
1913    }
1914
1915    #[test]
1916    fn pixel_config_layout() {
1917        // Pick any shader, since `struct Config` is in the common text
1918        crate::test::compare_struct_layout::<Config>(&merge_shader(), "Config");
1919    }
1920}