From e7bf9167dfc30d18f593d85a2d925fb8b4b0e2ee Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Sun, 7 Jun 2026 09:05:09 -0400 Subject: [PATCH 1/7] Fiddling with sizes and generics --- fidget-raster/src/lib.rs | 12 +++- fidget-raster/src/pixel.rs | 20 +++--- fidget-raster/src/voxel.rs | 20 +++--- fidget-wgpu/src/voxel.rs | 129 ++++++++++++++++--------------------- 4 files changed, 88 insertions(+), 93 deletions(-) diff --git a/fidget-raster/src/lib.rs b/fidget-raster/src/lib.rs index baf66980..410f7ff0 100644 --- a/fidget-raster/src/lib.rs +++ b/fidget-raster/src/lib.rs @@ -164,13 +164,19 @@ where } /// Helper trait for tiled rendering configuration -pub(crate) trait RenderConfig { - fn width(&self) -> u32; - fn height(&self) -> u32; +pub(crate) trait RenderConfig: RenderSize { fn threads(&self) -> Option<&ThreadPool>; fn is_cancelled(&self) -> bool; } +/// Trait for things that have a width and height in pixels +pub trait RenderSize { + /// Width of the render, in voxels or pixels + fn width(&self) -> u32; + /// Height of the render, in voxels or pixels + fn height(&self) -> u32; +} + /// Helper trait for a tiled renderer worker pub(crate) trait RenderWorker<'a, F: Function> { type Config: RenderConfig; diff --git a/fidget-raster/src/pixel.rs b/fidget-raster/src/pixel.rs index 92878dbc..d27eddc1 100644 --- a/fidget-raster/src/pixel.rs +++ b/fidget-raster/src/pixel.rs @@ -1,8 +1,7 @@ //! 2D bitmap rendering / rasterization use super::RenderHandle; use crate::{ - Image as GenericImage, RenderConfig as RenderConfigLike, RenderWorker, - Tile, TileSizesRef, + Image as GenericImage, RenderSize as _, RenderWorker, Tile, TileSizesRef, }; use fidget_core::{ eval::Function, @@ -63,13 +62,7 @@ impl Default for RenderConfig<'_> { } } -impl RenderConfigLike for RenderConfig<'_> { - fn width(&self) -> u32 { - self.image_size.width() - } - fn height(&self) -> u32 { - self.image_size.height() - } +impl crate::RenderConfig for RenderConfig<'_> { fn threads(&self) -> Option<&ThreadPool> { self.threads } @@ -78,6 +71,15 @@ impl RenderConfigLike for RenderConfig<'_> { } } +impl crate::RenderSize for RenderConfig<'_> { + fn width(&self) -> u32 { + self.image_size.width() + } + fn height(&self) -> u32 { + self.image_size.height() + } +} + impl RenderConfig<'_> { /// Render a shape in 2D using this configuration /// diff --git a/fidget-raster/src/voxel.rs b/fidget-raster/src/voxel.rs index 863eb53a..6fd373c8 100644 --- a/fidget-raster/src/voxel.rs +++ b/fidget-raster/src/voxel.rs @@ -1,8 +1,7 @@ //! 3D bitmap rendering / rasterization use super::RenderHandle; use crate::{ - Image as GenericImage, RenderConfig as RenderConfigLike, RenderWorker, - Tile, TileSizesRef, + Image as GenericImage, RenderSize as _, RenderWorker, Tile, TileSizesRef, }; use fidget_core::{ eval::Function, @@ -62,13 +61,7 @@ impl Default for RenderConfig<'_> { } } -impl RenderConfigLike for RenderConfig<'_> { - fn width(&self) -> u32 { - self.image_size.width() - } - fn height(&self) -> u32 { - self.image_size.height() - } +impl crate::RenderConfig for RenderConfig<'_> { fn threads(&self) -> Option<&ThreadPool> { self.threads } @@ -77,6 +70,15 @@ impl RenderConfigLike for RenderConfig<'_> { } } +impl crate::RenderSize for RenderConfig<'_> { + fn width(&self) -> u32 { + self.image_size.width() + } + fn height(&self) -> u32 { + self.image_size.height() + } +} + impl RenderConfig<'_> { /// Render a shape in 3D using this configuration /// diff --git a/fidget-wgpu/src/voxel.rs b/fidget-wgpu/src/voxel.rs index 7af0b305..77a8b327 100644 --- a/fidget-wgpu/src/voxel.rs +++ b/fidget-wgpu/src/voxel.rs @@ -1165,11 +1165,11 @@ pub struct Context { struct TileBuffers { /// Tiles written by the stage outputting N³ tiles - tiles: Buffer, + tiles: FlexBuffer, /// Sorted version of [`tiles`](Self::tiles) - sorted: Buffer, + sorted: FlexBuffer, /// Minimum Z height at each XY tile - zmin: Buffer, + zmin: FlexBuffer, } impl TileBuffers { @@ -1179,31 +1179,22 @@ impl TileBuffers { render_size: TileRenderSize, ) -> Result { let tile_buf_size = Self::tile_buf_size(render_size); - let tiles = Buffer::new( - device, - format!("active_tile{N}"), - tile_buf_size, - wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::INDIRECT, - ) - .map_err(|err| TileBuffersError { - buf: TileBufferName::Tiles, - err, - })?; - let sorted = Buffer::new( - device, - format!("sorted_tile{N}"), - tile_buf_size, - wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::INDIRECT, - ) - .map_err(|err| TileBuffersError { - buf: TileBufferName::Sorted, - err, - })?; - let zmin = Buffer::new( + let tiles = + FlexBuffer::new(device, format!("active_tile{N}"), tile_buf_size) + .map_err(|err| TileBuffersError { + buf: TileBufferName::Tiles, + err, + })?; + let sorted = + FlexBuffer::new(device, format!("sorted_tile{N}"), tile_buf_size) + .map_err(|err| TileBuffersError { + buf: TileBufferName::Sorted, + err, + })?; + let zmin = FlexBuffer::new( device, format!("tile{N}_zmin"), Self::zmin_buf_size(render_size), - wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, ) .map_err(|err| TileBuffersError { buf: TileBufferName::Zmin, @@ -1292,11 +1283,11 @@ impl TileBuffers { /// Root tile buffers store strata-packed tile lists struct RootTileBuffers { /// Initial output tiles - tiles: Buffer, + tiles: FlexBuffer, /// Strata-sorted output tiles - strata: Buffer, - zmin: Buffer, - zmax: Buffer, + strata: FlexBuffer, + zmin: FlexBuffer, + zmax: FlexBuffer, } impl RootTileBuffers { @@ -1309,24 +1300,20 @@ impl RootTileBuffers { const N: usize = 64; // Allocate enough words to write all of the output tiles - let tiles = Buffer::new( + let tiles = FlexBuffer::new( device, format!("tiles_out{N}"), Self::tiles_buf_size(render_size), - wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, ) .map_err(|err| RootTileBuffersError { buf: RootTileBufferName::Tiles, err, })?; - let strata = Buffer::new( + let strata = FlexBuffer::new( device, format!("strata_tile{N}"), Self::strata_buf_size(render_size), - wgpu::BufferUsages::STORAGE - | wgpu::BufferUsages::INDIRECT - | wgpu::BufferUsages::COPY_DST, ) .map_err(|err| RootTileBuffersError { buf: RootTileBufferName::Strata, @@ -1334,26 +1321,16 @@ impl RootTileBuffers { })?; let z_buf_size = Self::z_buf_size(render_size); - let zmin = Buffer::new( - device, - format!("tile{N}_zmin"), - z_buf_size, - wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - ) - .map_err(|err| RootTileBuffersError { - buf: RootTileBufferName::Zmin, - err, - })?; - let zmax = Buffer::new( - device, - format!("tile{N}_zmax"), - z_buf_size, - wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - ) - .map_err(|err| RootTileBuffersError { - buf: RootTileBufferName::Zmax, - err, - })?; + let zmin = FlexBuffer::new(device, format!("tile{N}_zmin"), z_buf_size) + .map_err(|err| RootTileBuffersError { + buf: RootTileBufferName::Zmin, + err, + })?; + let zmax = FlexBuffer::new(device, format!("tile{N}_zmax"), z_buf_size) + .map_err(|err| RootTileBuffersError { + buf: RootTileBufferName::Zmax, + err, + })?; Ok(Self { tiles, strata, @@ -1561,7 +1538,7 @@ pub struct Buffers { z_hist_buf: wgpu::Buffer, /// Map from tile to the relevant tape (as a start index) - tile_tapes: Buffer, + tile_tapes: FlexBuffer, /// Root tile Z heights (64³) tile64: RootTileBuffers, @@ -1573,16 +1550,16 @@ pub struct Buffers { tile4: TileBuffers<4>, /// Z heights for voxels - voxels: Buffer, + voxels: FlexBuffer, /// Buffer of [`GeometryPixel`] data, generated by the normal pass - geom: Buffer, + geom: FlexBuffer, /// Result buffer that can be read back from the host /// /// This is mostly image pixels (as [`GeometryPixel`] values), but also /// contains two trailing `u64` values for timestamps. - image: Buffer, + image: FlexBuffer, /// Query set for timestamps /// @@ -1920,7 +1897,7 @@ impl Buffers { } /// Handle around a growable GPU buffer which pretends to be smaller -struct Buffer { +struct FlexBuffer { /// Current active size, which may be smaller than the buffer's capacity size: u64, /// Actual GPU buffer @@ -1929,14 +1906,28 @@ struct Buffer { name: String, } -impl Buffer { +// Usage constants for FlexBuffer +const STORAGE_COPY_DST: u32 = + wgpu::BufferUsages::STORAGE.bits() | wgpu::BufferUsages::COPY_DST.bits(); +const STORAGE_INDIRECT: u32 = + wgpu::BufferUsages::STORAGE.bits() | wgpu::BufferUsages::INDIRECT.bits(); +const STORAGE_INDIRECT_COPY_DST: u32 = wgpu::BufferUsages::STORAGE.bits() + | wgpu::BufferUsages::INDIRECT.bits() + | wgpu::BufferUsages::COPY_DST.bits(); +const STORAGE_COPY_SRC_DST: u32 = wgpu::BufferUsages::STORAGE.bits() + | wgpu::BufferUsages::COPY_SRC.bits() + | wgpu::BufferUsages::COPY_DST.bits(); +const COPY_DST_MAP_READ: u32 = + wgpu::BufferUsages::COPY_DST.bits() | wgpu::BufferUsages::MAP_READ.bits(); + +impl FlexBuffer { fn new( device: &wgpu::Device, name: String, size: u64, - usage: wgpu::BufferUsages, ) -> Result { assert_eq!(size % 4, 0); + let usage = wgpu::BufferUsages::from_bits(U).unwrap(); Self::check_size(usage, size)?; let data = device.create_buffer(&wgpu::BufferDescriptor { label: Some(name.as_str()), @@ -2043,22 +2034,20 @@ impl Buffers { }); let render_size = TileRenderSize::from(image_size); - let voxels = Buffer::new( + let voxels = FlexBuffer::new( device, "voxels".to_string(), Self::voxels_buf_size(render_size), - wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, ) .map_err(|err| BuffersError { requested: image_size, buf: BufferName::Voxels, err, })?; - let tile_tapes = Buffer::new( + let tile_tapes = FlexBuffer::new( device, "tile tape".to_string(), Self::tile_tapes_buf_size(render_size), - wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, ) .map_err(|err| BuffersError { requested: image_size, @@ -2066,13 +2055,10 @@ impl Buffers { err, })?; - let geom = Buffer::new( + let geom = FlexBuffer::new( device, "geom".to_string(), Self::geom_buf_size(image_size), - wgpu::BufferUsages::STORAGE - | wgpu::BufferUsages::COPY_SRC - | wgpu::BufferUsages::COPY_DST, ) .map_err(|err| BuffersError { requested: image_size, @@ -2080,11 +2066,10 @@ impl Buffers { err, })?; - let image = Buffer::new( + let image = FlexBuffer::new( device, "image".to_string(), Self::image_buf_size(image_size), - wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, ) .map_err(|err| BuffersError { requested: image_size, From b699d418cabe5f3a010cc94c3028a49dc41c3afd Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Sun, 7 Jun 2026 09:15:49 -0400 Subject: [PATCH 2/7] Make FlexBuffer strongly typed --- fidget-wgpu/src/voxel.rs | 130 +++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 59 deletions(-) diff --git a/fidget-wgpu/src/voxel.rs b/fidget-wgpu/src/voxel.rs index 77a8b327..15a7d059 100644 --- a/fidget-wgpu/src/voxel.rs +++ b/fidget-wgpu/src/voxel.rs @@ -581,11 +581,11 @@ struct RootContext { /// Per-strata offset in the root tiles list /// /// This must be equivalent to `strata_size_bytes` in the interval root shader -fn strata_size_bytes(render_size: TileRenderSize) -> u64 { - let nx = u64::from(render_size.nx()); - let ny = u64::from(render_size.ny()); +fn strata_size_bytes(render_size: TileRenderSize) -> usize { + let nx = usize::try_from(render_size.nx()).unwrap(); + let ny = usize::try_from(render_size.ny()).unwrap(); // Snap to `min_storage_buffer_offset_alignment` - ((nx * ny + 4) * std::mem::size_of::() as u64).next_multiple_of(256) + ((nx * ny + 4) * std::mem::size_of::()).next_multiple_of(256) } impl RootContext { @@ -939,7 +939,7 @@ impl IntervalContext { reg_count: u8, compute_pass: &mut wgpu::ComputePass, ) { - let strata_bytes = buffers.strata_size_bytes(); + let strata_bytes = u64::try_from(buffers.strata_size_bytes()).unwrap(); let offset_bytes = strata * strata_bytes; let bind_group16 = buffers.bind_groups.interval16(ctx, buffers); compute_pass.set_pipeline(self.interval64_pipeline.get(reg_count)); @@ -1165,11 +1165,11 @@ pub struct Context { struct TileBuffers { /// Tiles written by the stage outputting N³ tiles - tiles: FlexBuffer, + tiles: FlexBuffer, /// Sorted version of [`tiles`](Self::tiles) - sorted: FlexBuffer, + sorted: FlexBuffer, /// Minimum Z height at each XY tile - zmin: FlexBuffer, + zmin: FlexBuffer, } impl TileBuffers { @@ -1208,19 +1208,21 @@ impl TileBuffers { }) } - fn tile_buf_size(render_size: TileRenderSize) -> u64 { - let nx = u64::from(render_size.width()) / N; - let ny = u64::from(render_size.height()) / N; - let nz = 64 / N; + fn tile_buf_size(render_size: TileRenderSize) -> usize { + let n = usize::try_from(N).unwrap(); + let nx = usize::try_from(render_size.width()).unwrap() / n; + let ny = usize::try_from(render_size.height()).unwrap() / n; + let nz = 64 / n; // wg_dispatch: [u32; 3] // count: u32, - (4 + nx * ny * nz) * std::mem::size_of::() as u64 + 4 + nx * ny * nz } - fn zmin_buf_size(render_size: TileRenderSize) -> u64 { - let nx = u64::from(render_size.width()) / N; - let ny = u64::from(render_size.height()) / N; - (nx * ny) * std::mem::size_of::() as u64 + fn zmin_buf_size(render_size: TileRenderSize) -> usize { + let n = usize::try_from(N).unwrap(); + let nx = usize::try_from(render_size.width()).unwrap() / n; + let ny = usize::try_from(render_size.height()).unwrap() / n; + nx * ny } fn grow_to_fit( @@ -1283,11 +1285,11 @@ impl TileBuffers { /// Root tile buffers store strata-packed tile lists struct RootTileBuffers { /// Initial output tiles - tiles: FlexBuffer, + tiles: FlexBuffer, /// Strata-sorted output tiles - strata: FlexBuffer, - zmin: FlexBuffer, - zmax: FlexBuffer, + strata: FlexBuffer, + zmin: FlexBuffer, + zmax: FlexBuffer, } impl RootTileBuffers { @@ -1339,25 +1341,25 @@ impl RootTileBuffers { }) } - fn tiles_buf_size(render_size: TileRenderSize) -> u64 { - let nx = u64::from(render_size.nx()); - let ny = u64::from(render_size.ny()); - let nz = u64::from(render_size.nz()); + fn tiles_buf_size(render_size: TileRenderSize) -> usize { + let nx = usize::try_from(render_size.nx()).unwrap(); + let ny = usize::try_from(render_size.ny()).unwrap(); + let nz = usize::try_from(render_size.nz()).unwrap(); // wg_dispatch: [u32; 3] (unused) // count: u32, - (4 + nx * ny * nz) * std::mem::size_of::() as u64 + 4 + nx * ny * nz } - fn strata_buf_size(render_size: TileRenderSize) -> u64 { - let nz = u64::from(render_size.nz()); + fn strata_buf_size(render_size: TileRenderSize) -> usize { + let nz = usize::try_from(render_size.nz()).unwrap(); let strata_size = strata_size_bytes(render_size); strata_size * nz } - fn z_buf_size(render_size: TileRenderSize) -> u64 { - let nx = u64::from(render_size.nx()); - let ny = u64::from(render_size.ny()); - nx * ny * std::mem::size_of::() as u64 + fn z_buf_size(render_size: TileRenderSize) -> usize { + let nx = usize::try_from(render_size.nx()).unwrap(); + let ny = usize::try_from(render_size.ny()).unwrap(); + nx * ny } /// Grows all of the buffers to fit a particular render size @@ -1538,7 +1540,7 @@ pub struct Buffers { z_hist_buf: wgpu::Buffer, /// Map from tile to the relevant tape (as a start index) - tile_tapes: FlexBuffer, + tile_tapes: FlexBuffer, /// Root tile Z heights (64³) tile64: RootTileBuffers, @@ -1550,16 +1552,16 @@ pub struct Buffers { tile4: TileBuffers<4>, /// Z heights for voxels - voxels: FlexBuffer, + voxels: FlexBuffer, /// Buffer of [`GeometryPixel`] data, generated by the normal pass - geom: FlexBuffer, + geom: FlexBuffer, /// Result buffer that can be read back from the host /// /// This is mostly image pixels (as [`GeometryPixel`] values), but also /// contains two trailing `u64` values for timestamps. - image: FlexBuffer, + image: FlexBuffer, /// Query set for timestamps /// @@ -1716,7 +1718,7 @@ impl BindGroups { } fn interval16(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { - let strata_bytes = buffers.strata_size_bytes(); + let strata_bytes = u64::try_from(buffers.strata_size_bytes()).unwrap(); self.interval16.get_or_init(|| { ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("interval16 bind group"), @@ -1897,13 +1899,15 @@ impl Buffers { } /// Handle around a growable GPU buffer which pretends to be smaller -struct FlexBuffer { +struct FlexBuffer { /// Current active size, which may be smaller than the buffer's capacity size: u64, /// Actual GPU buffer data: wgpu::Buffer, /// Buffer label (to be used when reallocating) name: String, + /// Marker for buffer data type + _t: std::marker::PhantomData, } // Usage constants for FlexBuffer @@ -1920,12 +1924,14 @@ const STORAGE_COPY_SRC_DST: u32 = wgpu::BufferUsages::STORAGE.bits() const COPY_DST_MAP_READ: u32 = wgpu::BufferUsages::COPY_DST.bits() | wgpu::BufferUsages::MAP_READ.bits(); -impl FlexBuffer { +impl FlexBuffer { fn new( device: &wgpu::Device, name: String, - size: u64, + item_count: usize, ) -> Result { + let size = + u64::try_from(item_count * std::mem::size_of::()).unwrap(); assert_eq!(size % 4, 0); let usage = wgpu::BufferUsages::from_bits(U).unwrap(); Self::check_size(usage, size)?; @@ -1935,7 +1941,12 @@ impl FlexBuffer { usage, mapped_at_creation: false, }); - Ok(Self { data, size, name }) + Ok(Self { + data, + size, + name, + _t: std::marker::PhantomData, + }) } fn check_size( @@ -1961,8 +1972,10 @@ impl FlexBuffer { fn grow_to_fit( &mut self, device: &wgpu::Device, - size: u64, + item_count: usize, ) -> Result<(), BufferSizeError> { + let size = + u64::try_from(item_count * std::mem::size_of::()).unwrap(); assert_eq!(size % 4, 0); if size > self.capacity() { let usage = self.data.usage(); @@ -2152,7 +2165,7 @@ impl Buffers { } /// Returns the size of one strata (in bytes) - fn strata_size_bytes(&self) -> u64 { + fn strata_size_bytes(&self) -> usize { strata_size_bytes(self.render_size()) } @@ -2177,29 +2190,28 @@ impl Buffers { /// | index | index | index | ... | 16² XY tiles × 4 Z positions /// | index | index | index | ... | 4² XY tiles × 16 Z positions /// ``` - fn tile_tapes_buf_size(render_size: TileRenderSize) -> u64 { - let nx = u64::from(render_size.nx()); - let ny = u64::from(render_size.ny()); - let nz = u64::from(render_size.nz()); - (nx * ny * nz + (nx * ny) * ((64u64 / 16).pow(3) + (64u64 / 4).pow(3))) - * std::mem::size_of::() as u64 + fn tile_tapes_buf_size(render_size: TileRenderSize) -> usize { + let nx = usize::try_from(render_size.nx()).unwrap(); + let ny = usize::try_from(render_size.ny()).unwrap(); + let nz = usize::try_from(render_size.nz()).unwrap(); + nx * ny * nz + + (nx * ny) * ((64usize / 16).pow(3) + (64usize / 4).pow(3)) } - fn voxels_buf_size(render_size: TileRenderSize) -> u64 { - let render_pixels = render_size.pixels(); - (render_pixels * std::mem::size_of::()) as u64 + fn voxels_buf_size(render_size: TileRenderSize) -> usize { + render_size.pixels() } /// Returns the size in bytes for the `geom` buffers - fn geom_buf_size(image_size: VoxelSize) -> u64 { - let image_pixels = - u64::from(image_size.width()) * u64::from(image_size.height()); - image_pixels * std::mem::size_of::() as u64 + fn geom_buf_size(image_size: VoxelSize) -> usize { + usize::try_from(image_size.width()).unwrap() + * usize::try_from(image_size.height()).unwrap() } - fn image_buf_size(image_size: VoxelSize) -> u64 { + fn image_buf_size(image_size: VoxelSize) -> usize { // Allocate an extra 16 bytes for timestamp queries - Self::geom_buf_size(image_size) + 16 + Self::geom_buf_size(image_size) * std::mem::size_of::() + + 16 } /// Resizes to render the target image size @@ -3004,7 +3016,7 @@ impl ResetContext { for s in 0..buffers.render_size().nz() { encoder.clear_buffer( &buffers.tile64.strata.data, - u64::from(s) * strata_size_bytes, + u64::from(s) * u64::try_from(strata_size_bytes).unwrap(), Some(16), ); } From 1783c7b271cb6d0939fa95b52e1af70f91a5bc8c Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Sun, 7 Jun 2026 09:19:13 -0400 Subject: [PATCH 3/7] Store item count instead of byte size --- fidget-wgpu/src/voxel.rs | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/fidget-wgpu/src/voxel.rs b/fidget-wgpu/src/voxel.rs index 15a7d059..8cb619e6 100644 --- a/fidget-wgpu/src/voxel.rs +++ b/fidget-wgpu/src/voxel.rs @@ -1894,14 +1894,14 @@ impl Buffers { /// the tuple when binding the buffer, and may also want to use /// [`Buffers::image_size`] (if they care about image width and height). pub fn image_storage_buffer(&self) -> (&wgpu::Buffer, u64) { - (&self.geom.data, self.geom.size) + (&self.geom.data, self.geom.size()) } } /// Handle around a growable GPU buffer which pretends to be smaller struct FlexBuffer { - /// Current active size, which may be smaller than the buffer's capacity - size: u64, + /// Current item count, which may be smaller than the buffer's capacity + item_count: usize, /// Actual GPU buffer data: wgpu::Buffer, /// Buffer label (to be used when reallocating) @@ -1943,12 +1943,17 @@ impl FlexBuffer { }); Ok(Self { data, - size, + item_count, name, _t: std::marker::PhantomData, }) } + /// Returns the active buffer size (in bytes) + fn size(&self) -> u64 { + u64::try_from(self.item_count * std::mem::size_of::()).unwrap() + } + fn check_size( usage: wgpu::BufferUsages, size: u64, @@ -1974,10 +1979,10 @@ impl FlexBuffer { device: &wgpu::Device, item_count: usize, ) -> Result<(), BufferSizeError> { - let size = - u64::try_from(item_count * std::mem::size_of::()).unwrap(); - assert_eq!(size % 4, 0); - if size > self.capacity() { + if item_count > self.item_capacity() { + let size = + u64::try_from(item_count * std::mem::size_of::()).unwrap(); + assert_eq!(size % 4, 0); let usage = self.data.usage(); Self::check_size(usage, size)?; self.data = device.create_buffer(&wgpu::BufferDescriptor { @@ -1987,18 +1992,20 @@ impl FlexBuffer { mapped_at_creation: false, }); } - self.size = size; + self.item_count = item_count; Ok(()) } /// Returns a binding resource for the active slice of the buffer fn bind_active(&self) -> wgpu::BindingResource<'_> { - self.data.slice(0..self.size).into() + self.data.slice(0..self.size()).into() } - /// Returns the active buffer size - fn size(&self) -> u64 { - self.size + /// Returns the buffer's capacity as an item count + fn item_capacity(&self) -> usize { + let c = usize::try_from(self.capacity()).unwrap(); + assert_eq!(c % std::mem::size_of::(), 0); + c / std::mem::size_of::() } /// Returns the total buffer capacity, which may be larger than its size @@ -2013,14 +2020,14 @@ impl FlexBuffer { + wgpu::WasmNotSend + 'static, ) -> wgpu::BufferSlice<'_> { - let slice = self.data.slice(0..self.size); + let slice = self.data.slice(0..self.size()); slice.map_async(wgpu::MapMode::Read, callback); slice } /// Clears the active portion of the buffer fn clear(&self, encoder: &mut wgpu::CommandEncoder) { - encoder.clear_buffer(&self.data, 0, Some(self.size)); + encoder.clear_buffer(&self.data, 0, Some(self.size())); } } From e6d32b064172d5c06d4b2a1dcb9b2d5c71d95578 Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Sun, 7 Jun 2026 09:31:30 -0400 Subject: [PATCH 4/7] Improve comments --- fidget-wgpu/src/voxel.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fidget-wgpu/src/voxel.rs b/fidget-wgpu/src/voxel.rs index 8cb619e6..8a866296 100644 --- a/fidget-wgpu/src/voxel.rs +++ b/fidget-wgpu/src/voxel.rs @@ -2001,14 +2001,16 @@ impl FlexBuffer { self.data.slice(0..self.size()).into() } - /// Returns the buffer's capacity as an item count + /// Returns the buffer's total capacity (in items) + /// + /// This may be larger than [`self.item_count`](Self::item_count) fn item_capacity(&self) -> usize { let c = usize::try_from(self.capacity()).unwrap(); assert_eq!(c % std::mem::size_of::(), 0); c / std::mem::size_of::() } - /// Returns the total buffer capacity, which may be larger than its size + /// Returns the total buffer capacity (in bytes) fn capacity(&self) -> u64 { self.data.size() } From a5b738e388f1e8dbd941723220a3962396c35ea3 Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Sun, 7 Jun 2026 09:41:14 -0400 Subject: [PATCH 5/7] Add helper macro --- fidget-wgpu/src/voxel.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/fidget-wgpu/src/voxel.rs b/fidget-wgpu/src/voxel.rs index 8a866296..3ae56c46 100644 --- a/fidget-wgpu/src/voxel.rs +++ b/fidget-wgpu/src/voxel.rs @@ -1910,19 +1910,17 @@ struct FlexBuffer { _t: std::marker::PhantomData, } -// Usage constants for FlexBuffer -const STORAGE_COPY_DST: u32 = - wgpu::BufferUsages::STORAGE.bits() | wgpu::BufferUsages::COPY_DST.bits(); -const STORAGE_INDIRECT: u32 = - wgpu::BufferUsages::STORAGE.bits() | wgpu::BufferUsages::INDIRECT.bits(); -const STORAGE_INDIRECT_COPY_DST: u32 = wgpu::BufferUsages::STORAGE.bits() - | wgpu::BufferUsages::INDIRECT.bits() - | wgpu::BufferUsages::COPY_DST.bits(); -const STORAGE_COPY_SRC_DST: u32 = wgpu::BufferUsages::STORAGE.bits() - | wgpu::BufferUsages::COPY_SRC.bits() - | wgpu::BufferUsages::COPY_DST.bits(); -const COPY_DST_MAP_READ: u32 = - wgpu::BufferUsages::COPY_DST.bits() | wgpu::BufferUsages::MAP_READ.bits(); +// Helper macro to generate usages +macro_rules! u { + ($($flag:ident),+ $(,)?) => { + $( wgpu::BufferUsages::$flag.bits() )|+ + }; +} +const STORAGE_COPY_DST: u32 = u!(STORAGE, COPY_DST); +const STORAGE_INDIRECT: u32 = u!(STORAGE, INDIRECT); +const STORAGE_INDIRECT_COPY_DST: u32 = u!(STORAGE, INDIRECT, COPY_DST); +const STORAGE_COPY_SRC_DST: u32 = u!(STORAGE, COPY_SRC, COPY_DST); +const COPY_DST_MAP_READ: u32 = u!(COPY_DST, MAP_READ); impl FlexBuffer { fn new( From 6bbb1635ca69253e4c92c2412685ba9da02a00db Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Sun, 7 Jun 2026 13:54:08 -0400 Subject: [PATCH 6/7] Move flexible buffers to crate level --- CHANGELOG.md | 3 + fidget-raster/src/lib.rs | 22 +-- fidget-wgpu/src/lib.rs | 251 +++++++++++++++++++++++++++++++ fidget-wgpu/src/voxel.rs | 310 +++++++-------------------------------- 4 files changed, 311 insertions(+), 275 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30bb51be..f37c2c0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,9 @@ Y, Z; otherwise, `Shape::bind` must be used. This removes a potential `unwrap()` during rendering, because evaluation requires all variables to be present. +- Rename `trait ImageSizeLike` to `trait RenderSize` in `fidget_raster`; remove + `width` and `height` from `trait RenderConfig` and add a `RenderConfig: + RenderSize` bound # 0.4.3 - Fixed bug in x86 interval `OR` function ([#395](https://github.com/mkeeter/fidget/pull/395)), diff --git a/fidget-raster/src/lib.rs b/fidget-raster/src/lib.rs index 410f7ff0..3a18cdc3 100644 --- a/fidget-raster/src/lib.rs +++ b/fidget-raster/src/lib.rs @@ -221,15 +221,7 @@ pub struct Image { size: S, } -/// Helper trait to make images generic across image size types -pub trait ImageSizeLike { - /// Returns the width of the region, in pixels / voxels - fn width(&self) -> u32; - /// Returns the height of the region, in pixels / voxels - fn height(&self) -> u32; -} - -impl ImageSizeLike for pixel::RenderSize { +impl RenderSize for pixel::RenderSize { fn width(&self) -> u32 { self.width() } @@ -238,7 +230,7 @@ impl ImageSizeLike for pixel::RenderSize { } } -impl ImageSizeLike for voxel::RenderSize { +impl RenderSize for voxel::RenderSize { fn width(&self) -> u32 { self.width() } @@ -247,7 +239,7 @@ impl ImageSizeLike for voxel::RenderSize { } } -impl Image { +impl Image { /// Generates an image by computing a per-pixel function /// /// This should be called on the _output_ image; the closure takes `(x, y)` @@ -288,7 +280,7 @@ impl Default for Image { } } -impl Image { +impl Image { /// Builds a new image filled with `P::default()` pub fn new(size: S) -> Self { Self { @@ -322,7 +314,7 @@ impl Image { } } -impl Image { +impl Image { /// Returns the image width pub fn width(&self) -> usize { self.size.width() as usize @@ -440,7 +432,7 @@ define_image_index!(std::ops::RangeToInclusive); define_image_index!(std::ops::RangeFull); /// Indexes an image with `(row, col)` -impl std::ops::Index<(usize, usize)> for Image { +impl std::ops::Index<(usize, usize)> for Image { type Output = P; fn index(&self, pos: (usize, usize)) -> &Self::Output { let index = self.decode_position(pos); @@ -448,7 +440,7 @@ impl std::ops::Index<(usize, usize)> for Image { } } -impl std::ops::IndexMut<(usize, usize)> for Image { +impl std::ops::IndexMut<(usize, usize)> for Image { fn index_mut(&mut self, pos: (usize, usize)) -> &mut Self::Output { let index = self.decode_position(pos); &mut self.data[index] diff --git a/fidget-wgpu/src/lib.rs b/fidget-wgpu/src/lib.rs index 15c9c977..6fd4591a 100644 --- a/fidget-wgpu/src/lib.rs +++ b/fidget-wgpu/src/lib.rs @@ -2,11 +2,14 @@ #![warn(missing_docs)] pub mod voxel; +use fidget_core::render::ImageSize; use heck::ToShoutySnakeCase; /// Re-export the [`wgpu`] module pub use wgpu; +//////////////////////////////////////////////////////////////////////////////// + /// Returns a set of constant definitions for each opcode fn opcode_constants() -> String { let mut out = String::new(); @@ -16,6 +19,8 @@ fn opcode_constants() -> String { out } +//////////////////////////////////////////////////////////////////////////////// + /// Error type for [`init`] #[derive(Debug, thiserror::Error)] pub enum InitError { @@ -52,3 +57,249 @@ pub async fn init() -> Result<(wgpu::Device, wgpu::Queue), InitError> { .await?; Ok(out) } + +//////////////////////////////////////////////////////////////////////////////// + +/// Handle around a growable GPU buffer which pretends to be smaller +struct GenericFlexBuffer { + /// Current item count, which may be smaller than the buffer's capacity + item_count: usize, + /// Actual GPU buffer + data: wgpu::Buffer, + /// Buffer label (to be used when reallocating) + name: String, + /// Marker for buffer data type + _t: std::marker::PhantomData, + /// Marker for buffer size type + _b: std::marker::PhantomData, +} + +/// Flexible buffer which can be resized with a single item count +type ArrayBuffer = GenericFlexBuffer; + +/// Flexible buffer which can be resized to fit an image size +type ImageBuffer = GenericFlexBuffer; + +/// Module containing usage constants +mod usage { + /// Helper macro to generate usage constants + macro_rules! u { + ($($flag:ident),+ $(,)?) => { + $( wgpu::BufferUsages::$flag.bits() )|+ + }; + } + pub const STORAGE_COPY_DST: u32 = u!(STORAGE, COPY_DST); + pub const STORAGE_INDIRECT: u32 = u!(STORAGE, INDIRECT); + pub const STORAGE_INDIRECT_COPY_DST: u32 = u!(STORAGE, INDIRECT, COPY_DST); + pub const STORAGE_COPY_SRC_DST: u32 = u!(STORAGE, COPY_SRC, COPY_DST); + pub const COPY_DST_MAP_READ: u32 = u!(COPY_DST, MAP_READ); +} + +trait BufferItemCount { + fn item_count(&self) -> usize; +} + +impl BufferItemCount for usize { + fn item_count(&self) -> usize { + *self + } +} + +impl BufferItemCount for ImageSize { + fn item_count(&self) -> usize { + usize::try_from(self.width()).unwrap() + * usize::try_from(self.height()).unwrap() + } +} + +impl GenericFlexBuffer { + fn new( + device: &wgpu::Device, + name: String, + item_count: B, + ) -> Result { + let item_count = item_count.item_count(); + let size = + u64::try_from(item_count * std::mem::size_of::()).unwrap(); + assert_eq!(size % 4, 0); + let usage = wgpu::BufferUsages::from_bits(U).unwrap(); + Self::check_size(usage, size)?; + let data = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(name.as_str()), + size, + usage, + mapped_at_creation: false, + }); + Ok(Self { + data, + item_count, + name, + _t: std::marker::PhantomData, + _b: std::marker::PhantomData, + }) + } + + /// Returns the active buffer size (in bytes) + fn size(&self) -> u64 { + u64::try_from(self.item_count * std::mem::size_of::()).unwrap() + } + + fn check_size( + usage: wgpu::BufferUsages, + size: u64, + ) -> Result<(), BufferSizeError> { + let buf_ty = if usage.contains(wgpu::BufferUsages::STORAGE) { + BufferType::Storage + } else if usage.contains(wgpu::BufferUsages::UNIFORM) { + BufferType::Uniform + } else { + BufferType::Generic + }; + buf_ty.check(size) + } + + /// Grows the buffer to fit a particular size in bytes + /// + /// If the buffer already fits that size, then no allocation is performed, + /// but we always update the internal `member_count` (e.g. so that + /// [`bind_active`](Self::bind_active) returns the correct subset of the + /// buffer). + fn grow_to_fit( + &mut self, + device: &wgpu::Device, + item_count: B, + ) -> Result<(), BufferSizeError> { + let item_count = item_count.item_count(); + if item_count > self.item_capacity() { + let size = + u64::try_from(item_count * std::mem::size_of::()).unwrap(); + assert_eq!(size % 4, 0); + let usage = self.data.usage(); + Self::check_size(usage, size)?; + self.data = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(self.name.as_str()), + size, + usage, + mapped_at_creation: false, + }); + } + self.item_count = item_count; + Ok(()) + } + + /// Returns a binding resource for the active slice of the buffer + fn bind_active(&self) -> wgpu::BindingResource<'_> { + self.data.slice(0..self.size()).into() + } + + /// Returns the buffer's total capacity (in items) + /// + /// This may be larger than [`self.item_count`](Self::item_count) + fn item_capacity(&self) -> usize { + let c = usize::try_from(self.capacity()).unwrap(); + assert_eq!(c % std::mem::size_of::(), 0); + c / std::mem::size_of::() + } + + /// Returns the total buffer capacity (in bytes) + fn capacity(&self) -> u64 { + self.data.size() + } + + /// Maps the active portion of the buffer for reading + fn map_async( + &self, + callback: impl FnOnce(Result<(), wgpu::BufferAsyncError>) + + wgpu::WasmNotSend + + 'static, + ) -> wgpu::BufferSlice<'_> { + let slice = self.data.slice(0..self.size()); + slice.map_async(wgpu::MapMode::Read, callback); + slice + } + + /// Clears the active portion of the buffer + fn clear(&self, encoder: &mut wgpu::CommandEncoder) { + encoder.clear_buffer(&self.data, 0, Some(self.size())); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Error handling zone! This is perhaps a bit overengineered, but it meets the +// desired behavior of function error types only containing errors that they can +// actually return. + +/// Error type when resizing a buffer beyond its limit +/// +/// We check against maximum buffer sizes (from the WebGPU spec) and return an +/// error immediately, instead of deferring the error to the point where the +/// buffer is used. +#[derive(Debug, thiserror::Error)] +pub enum BufferSizeError { + /// Buffer size is not aligned to 4 bytes + #[error("requested size {0} must be a multiple of 4 bytes")] + NotAligned(u64), + + /// Buffer size is too large for the requested buffer usage + #[error( + "requested size {requested_size} exceeds maximum {} for \ + {buffer_type} buffer", + buffer_type.max_size() + )] + TooLarge { + /// Size requested (in bytes) + requested_size: u64, + /// Buffer type (which determines the [max size](BufferType::max_size)) + buffer_type: BufferType, + }, +} + +/// Buffer type for error reporting +#[derive(Copy, Clone, Debug)] +pub enum BufferType { + /// Uniform buffer ([`wgpu::BufferUsages::UNIFORM`]) + Uniform, + /// Storage buffer ([`wgpu::BufferUsages::STORAGE`]) + Storage, + /// Other buffer type (e.g. [`wgpu::BufferUsages::MAP_READ`]) + Generic, +} + +impl std::fmt::Display for BufferType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + BufferType::Uniform => "uniform", + BufferType::Storage => "storage", + BufferType::Generic => "generic", + }; + s.fmt(f) + } +} + +impl BufferType { + /// Maximum size of this buffer type, per the WebGPU spec + pub const fn max_size(&self) -> u64 { + // These are copied from the spec, since we don't ask for anything extra + match self { + // maxUniformBufferBindingSize + BufferType::Uniform => 64 * 1024, + // maxStorageBufferBindingSize + BufferType::Storage => 128 * 1024 * 1024, + // maxBufferSize + BufferType::Generic => 256 * 1024 * 1024, + } + } + + fn check(&self, requested_size: u64) -> Result<(), BufferSizeError> { + if !requested_size.is_multiple_of(4) { + Err(BufferSizeError::NotAligned(requested_size)) + } else if requested_size > self.max_size() { + Err(BufferSizeError::TooLarge { + requested_size, + buffer_type: *self, + }) + } else { + Ok(()) + } + } +} diff --git a/fidget-wgpu/src/voxel.rs b/fidget-wgpu/src/voxel.rs index 3ae56c46..d7d13942 100644 --- a/fidget-wgpu/src/voxel.rs +++ b/fidget-wgpu/src/voxel.rs @@ -89,11 +89,14 @@ //! To reuse the image buffer within a more complex GPU pipeline (without //! copying to the host), see [`Buffers::image_storage_buffer`]. -use crate::opcode_constants; +use crate::{ + ArrayBuffer, BufferSizeError, BufferType, ImageBuffer, opcode_constants, + usage::*, +}; use fidget_bytecode::{Bytecode, ReservedRegister}; use fidget_core::{ eval::Function, - render::VoxelSize, + render::{ImageSize, VoxelSize}, shape::{MissingVar, ShapeVars}, var::Var, vm::VmShape, @@ -117,86 +120,6 @@ const NORMALS_SHADER: &str = include_str!("shaders/normals.wgsl"); const TAPE_INTERPRETER: &str = include_str!("shaders/tape_interpreter.wgsl"); const TAPE_SIMPLIFY: &str = include_str!("shaders/tape_simplify.wgsl"); -//////////////////////////////////////////////////////////////////////////////// -// Error handling zone! This is perhaps a bit overengineered, but it meets the -// desired behavior of function error types only containing errors that they can -// actually return. - -/// Buffer type for error reporting -#[derive(Copy, Clone, Debug)] -pub enum BufferType { - /// Uniform buffer ([`wgpu::BufferUsages::UNIFORM`]) - Uniform, - /// Storage buffer ([`wgpu::BufferUsages::STORAGE`]) - Storage, - /// Other buffer type (e.g. [`wgpu::BufferUsages::MAP_READ`]) - Generic, -} - -impl std::fmt::Display for BufferType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - BufferType::Uniform => "uniform", - BufferType::Storage => "storage", - BufferType::Generic => "generic", - }; - s.fmt(f) - } -} - -impl BufferType { - /// Maximum size of this buffer type, per the WebGPU spec - pub const fn max_size(&self) -> u64 { - // These are copied from the spec, since we don't ask for anything extra - match self { - // maxUniformBufferBindingSize - BufferType::Uniform => 64 * 1024, - // maxStorageBufferBindingSize - BufferType::Storage => 128 * 1024 * 1024, - // maxBufferSize - BufferType::Generic => 256 * 1024 * 1024, - } - } - - fn check(&self, requested_size: u64) -> Result<(), BufferSizeError> { - if !requested_size.is_multiple_of(4) { - Err(BufferSizeError::NotAligned(requested_size)) - } else if requested_size > self.max_size() { - Err(BufferSizeError::TooLarge { - requested_size, - buffer_type: *self, - }) - } else { - Ok(()) - } - } -} - -/// Error type when resizing a buffer beyond its limit -/// -/// We check against maximum buffer sizes (from the WebGPU spec) and return an -/// error immediately, instead of deferring the error to the point where the -/// buffer is used. -#[derive(Debug, thiserror::Error)] -pub enum BufferSizeError { - /// Buffer size is not aligned to 4 bytes - #[error("requested size {0} must be a multiple of 4 bytes")] - NotAligned(u64), - - /// Buffer size is too large for the requested buffer usage - #[error( - "requested size {requested_size} exceeds maximum {} for \ - {buffer_type} buffer", - buffer_type.max_size() - )] - TooLarge { - /// Size requested (in bytes) - requested_size: u64, - /// Buffer type (which determines the [max size](BufferType::max_size)) - buffer_type: BufferType, - }, -} - /// Error type when resizing intermediate tile buffers #[derive(Debug, thiserror::Error)] #[error("failed to resize `{buf}` tile buffer")] @@ -1165,11 +1088,11 @@ pub struct Context { struct TileBuffers { /// Tiles written by the stage outputting N³ tiles - tiles: FlexBuffer, + tiles: ArrayBuffer, /// Sorted version of [`tiles`](Self::tiles) - sorted: FlexBuffer, + sorted: ArrayBuffer, /// Minimum Z height at each XY tile - zmin: FlexBuffer, + zmin: ImageBuffer, } impl TileBuffers { @@ -1180,18 +1103,18 @@ impl TileBuffers { ) -> Result { let tile_buf_size = Self::tile_buf_size(render_size); let tiles = - FlexBuffer::new(device, format!("active_tile{N}"), tile_buf_size) + ArrayBuffer::new(device, format!("active_tile{N}"), tile_buf_size) .map_err(|err| TileBuffersError { - buf: TileBufferName::Tiles, - err, - })?; + buf: TileBufferName::Tiles, + err, + })?; let sorted = - FlexBuffer::new(device, format!("sorted_tile{N}"), tile_buf_size) + ArrayBuffer::new(device, format!("sorted_tile{N}"), tile_buf_size) .map_err(|err| TileBuffersError { - buf: TileBufferName::Sorted, - err, - })?; - let zmin = FlexBuffer::new( + buf: TileBufferName::Sorted, + err, + })?; + let zmin = ImageBuffer::new( device, format!("tile{N}_zmin"), Self::zmin_buf_size(render_size), @@ -1218,11 +1141,11 @@ impl TileBuffers { 4 + nx * ny * nz } - fn zmin_buf_size(render_size: TileRenderSize) -> usize { - let n = usize::try_from(N).unwrap(); - let nx = usize::try_from(render_size.width()).unwrap() / n; - let ny = usize::try_from(render_size.height()).unwrap() / n; - nx * ny + fn zmin_buf_size(render_size: TileRenderSize) -> ImageSize { + ImageSize::new( + render_size.width() / u32::try_from(N).unwrap(), + render_size.height() / u32::try_from(N).unwrap(), + ) } fn grow_to_fit( @@ -1285,11 +1208,11 @@ impl TileBuffers { /// Root tile buffers store strata-packed tile lists struct RootTileBuffers { /// Initial output tiles - tiles: FlexBuffer, + tiles: ArrayBuffer, /// Strata-sorted output tiles - strata: FlexBuffer, - zmin: FlexBuffer, - zmax: FlexBuffer, + strata: ArrayBuffer, + zmin: ImageBuffer, + zmax: ImageBuffer, } impl RootTileBuffers { @@ -1302,7 +1225,7 @@ impl RootTileBuffers { const N: usize = 64; // Allocate enough words to write all of the output tiles - let tiles = FlexBuffer::new( + let tiles = ArrayBuffer::new( device, format!("tiles_out{N}"), Self::tiles_buf_size(render_size), @@ -1312,7 +1235,7 @@ impl RootTileBuffers { err, })?; - let strata = FlexBuffer::new( + let strata = ArrayBuffer::new( device, format!("strata_tile{N}"), Self::strata_buf_size(render_size), @@ -1323,16 +1246,18 @@ impl RootTileBuffers { })?; let z_buf_size = Self::z_buf_size(render_size); - let zmin = FlexBuffer::new(device, format!("tile{N}_zmin"), z_buf_size) - .map_err(|err| RootTileBuffersError { - buf: RootTileBufferName::Zmin, - err, - })?; - let zmax = FlexBuffer::new(device, format!("tile{N}_zmax"), z_buf_size) - .map_err(|err| RootTileBuffersError { - buf: RootTileBufferName::Zmax, - err, - })?; + let zmin = + ImageBuffer::new(device, format!("tile{N}_zmin"), z_buf_size) + .map_err(|err| RootTileBuffersError { + buf: RootTileBufferName::Zmin, + err, + })?; + let zmax = + ImageBuffer::new(device, format!("tile{N}_zmax"), z_buf_size) + .map_err(|err| RootTileBuffersError { + buf: RootTileBufferName::Zmax, + err, + })?; Ok(Self { tiles, strata, @@ -1356,10 +1281,8 @@ impl RootTileBuffers { strata_size * nz } - fn z_buf_size(render_size: TileRenderSize) -> usize { - let nx = usize::try_from(render_size.nx()).unwrap(); - let ny = usize::try_from(render_size.ny()).unwrap(); - nx * ny + fn z_buf_size(render_size: TileRenderSize) -> ImageSize { + ImageSize::new(render_size.nx(), render_size.ny()) } /// Grows all of the buffers to fit a particular render size @@ -1540,7 +1463,7 @@ pub struct Buffers { z_hist_buf: wgpu::Buffer, /// Map from tile to the relevant tape (as a start index) - tile_tapes: FlexBuffer, + tile_tapes: ArrayBuffer, /// Root tile Z heights (64³) tile64: RootTileBuffers, @@ -1552,16 +1475,16 @@ pub struct Buffers { tile4: TileBuffers<4>, /// Z heights for voxels - voxels: FlexBuffer, + voxels: ArrayBuffer, /// Buffer of [`GeometryPixel`] data, generated by the normal pass - geom: FlexBuffer, + geom: ArrayBuffer, /// Result buffer that can be read back from the host /// /// This is mostly image pixels (as [`GeometryPixel`] values), but also /// contains two trailing `u64` values for timestamps. - image: FlexBuffer, + image: ArrayBuffer, /// Query set for timestamps /// @@ -1898,139 +1821,6 @@ impl Buffers { } } -/// Handle around a growable GPU buffer which pretends to be smaller -struct FlexBuffer { - /// Current item count, which may be smaller than the buffer's capacity - item_count: usize, - /// Actual GPU buffer - data: wgpu::Buffer, - /// Buffer label (to be used when reallocating) - name: String, - /// Marker for buffer data type - _t: std::marker::PhantomData, -} - -// Helper macro to generate usages -macro_rules! u { - ($($flag:ident),+ $(,)?) => { - $( wgpu::BufferUsages::$flag.bits() )|+ - }; -} -const STORAGE_COPY_DST: u32 = u!(STORAGE, COPY_DST); -const STORAGE_INDIRECT: u32 = u!(STORAGE, INDIRECT); -const STORAGE_INDIRECT_COPY_DST: u32 = u!(STORAGE, INDIRECT, COPY_DST); -const STORAGE_COPY_SRC_DST: u32 = u!(STORAGE, COPY_SRC, COPY_DST); -const COPY_DST_MAP_READ: u32 = u!(COPY_DST, MAP_READ); - -impl FlexBuffer { - fn new( - device: &wgpu::Device, - name: String, - item_count: usize, - ) -> Result { - let size = - u64::try_from(item_count * std::mem::size_of::()).unwrap(); - assert_eq!(size % 4, 0); - let usage = wgpu::BufferUsages::from_bits(U).unwrap(); - Self::check_size(usage, size)?; - let data = device.create_buffer(&wgpu::BufferDescriptor { - label: Some(name.as_str()), - size, - usage, - mapped_at_creation: false, - }); - Ok(Self { - data, - item_count, - name, - _t: std::marker::PhantomData, - }) - } - - /// Returns the active buffer size (in bytes) - fn size(&self) -> u64 { - u64::try_from(self.item_count * std::mem::size_of::()).unwrap() - } - - fn check_size( - usage: wgpu::BufferUsages, - size: u64, - ) -> Result<(), BufferSizeError> { - let buf_ty = if usage.contains(wgpu::BufferUsages::STORAGE) { - BufferType::Storage - } else if usage.contains(wgpu::BufferUsages::UNIFORM) { - BufferType::Uniform - } else { - BufferType::Generic - }; - buf_ty.check(size) - } - - /// Grows the buffer to fit a particular size in bytes - /// - /// If the buffer already fits that size, then no allocation is performed, - /// but we always update the internal `size` member (e.g. so that - /// [`bind_active`](Self::bind_active) returns the correct - /// subset of the buffer). - fn grow_to_fit( - &mut self, - device: &wgpu::Device, - item_count: usize, - ) -> Result<(), BufferSizeError> { - if item_count > self.item_capacity() { - let size = - u64::try_from(item_count * std::mem::size_of::()).unwrap(); - assert_eq!(size % 4, 0); - let usage = self.data.usage(); - Self::check_size(usage, size)?; - self.data = device.create_buffer(&wgpu::BufferDescriptor { - label: Some(self.name.as_str()), - size, - usage, - mapped_at_creation: false, - }); - } - self.item_count = item_count; - Ok(()) - } - - /// Returns a binding resource for the active slice of the buffer - fn bind_active(&self) -> wgpu::BindingResource<'_> { - self.data.slice(0..self.size()).into() - } - - /// Returns the buffer's total capacity (in items) - /// - /// This may be larger than [`self.item_count`](Self::item_count) - fn item_capacity(&self) -> usize { - let c = usize::try_from(self.capacity()).unwrap(); - assert_eq!(c % std::mem::size_of::(), 0); - c / std::mem::size_of::() - } - - /// Returns the total buffer capacity (in bytes) - fn capacity(&self) -> u64 { - self.data.size() - } - - /// Maps the active portion of the buffer for reading - fn map_async( - &self, - callback: impl FnOnce(Result<(), wgpu::BufferAsyncError>) - + wgpu::WasmNotSend - + 'static, - ) -> wgpu::BufferSlice<'_> { - let slice = self.data.slice(0..self.size()); - slice.map_async(wgpu::MapMode::Read, callback); - slice - } - - /// Clears the active portion of the buffer - fn clear(&self, encoder: &mut wgpu::CommandEncoder) { - encoder.clear_buffer(&self.data, 0, Some(self.size())); - } -} - impl Buffers { fn new( device: &wgpu::Device, @@ -2054,7 +1844,7 @@ impl Buffers { }); let render_size = TileRenderSize::from(image_size); - let voxels = FlexBuffer::new( + let voxels = ArrayBuffer::new( device, "voxels".to_string(), Self::voxels_buf_size(render_size), @@ -2064,7 +1854,7 @@ impl Buffers { buf: BufferName::Voxels, err, })?; - let tile_tapes = FlexBuffer::new( + let tile_tapes = ArrayBuffer::new( device, "tile tape".to_string(), Self::tile_tapes_buf_size(render_size), @@ -2075,7 +1865,7 @@ impl Buffers { err, })?; - let geom = FlexBuffer::new( + let geom = ArrayBuffer::new( device, "geom".to_string(), Self::geom_buf_size(image_size), @@ -2086,7 +1876,7 @@ impl Buffers { err, })?; - let image = FlexBuffer::new( + let image = ArrayBuffer::new( device, "image".to_string(), Self::image_buf_size(image_size), From f36abf1b77a2585cc194d100c59983af91fdc8aa Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Wed, 10 Jun 2026 12:06:35 -0400 Subject: [PATCH 7/7] copilot feedback --- fidget-wgpu/src/lib.rs | 37 ++++++++++++++++++++----------------- fidget-wgpu/src/voxel.rs | 40 +++++++++++++++++++++++++++------------- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/fidget-wgpu/src/lib.rs b/fidget-wgpu/src/lib.rs index 6fd4591a..976e0d3f 100644 --- a/fidget-wgpu/src/lib.rs +++ b/fidget-wgpu/src/lib.rs @@ -107,8 +107,10 @@ impl BufferItemCount for usize { impl BufferItemCount for ImageSize { fn item_count(&self) -> usize { - usize::try_from(self.width()).unwrap() - * usize::try_from(self.height()).unwrap() + usize::try_from(self.width()) + .unwrap() + .checked_mul(usize::try_from(self.height()).unwrap()) + .unwrap() } } @@ -119,9 +121,7 @@ impl GenericFlexBuffer { item_count: B, ) -> Result { let item_count = item_count.item_count(); - let size = - u64::try_from(item_count * std::mem::size_of::()).unwrap(); - assert_eq!(size % 4, 0); + let size = Self::calculate_buffer_size(item_count); let usage = wgpu::BufferUsages::from_bits(U).unwrap(); Self::check_size(usage, size)?; let data = device.create_buffer(&wgpu::BufferDescriptor { @@ -139,9 +139,20 @@ impl GenericFlexBuffer { }) } + /// Calculate size from buffer item count + /// + /// Size is rounded up to the nearest multiple of 4 for alignment + fn calculate_buffer_size(item_count: usize) -> u64 { + let out = u64::try_from(item_count) + .unwrap() + .checked_mul(u64::try_from(std::mem::size_of::()).unwrap()) + .unwrap(); + out.next_multiple_of(4) + } + /// Returns the active buffer size (in bytes) fn size(&self) -> u64 { - u64::try_from(self.item_count * std::mem::size_of::()).unwrap() + Self::calculate_buffer_size(self.item_count) } fn check_size( @@ -161,7 +172,7 @@ impl GenericFlexBuffer { /// Grows the buffer to fit a particular size in bytes /// /// If the buffer already fits that size, then no allocation is performed, - /// but we always update the internal `member_count` (e.g. so that + /// but we always update the internal `item_count` (e.g. so that /// [`bind_active`](Self::bind_active) returns the correct subset of the /// buffer). fn grow_to_fit( @@ -171,9 +182,7 @@ impl GenericFlexBuffer { ) -> Result<(), BufferSizeError> { let item_count = item_count.item_count(); if item_count > self.item_capacity() { - let size = - u64::try_from(item_count * std::mem::size_of::()).unwrap(); - assert_eq!(size % 4, 0); + let size = Self::calculate_buffer_size(item_count); let usage = self.data.usage(); Self::check_size(usage, size)?; self.data = device.create_buffer(&wgpu::BufferDescriptor { @@ -236,10 +245,6 @@ impl GenericFlexBuffer { /// buffer is used. #[derive(Debug, thiserror::Error)] pub enum BufferSizeError { - /// Buffer size is not aligned to 4 bytes - #[error("requested size {0} must be a multiple of 4 bytes")] - NotAligned(u64), - /// Buffer size is too large for the requested buffer usage #[error( "requested size {requested_size} exceeds maximum {} for \ @@ -291,9 +296,7 @@ impl BufferType { } fn check(&self, requested_size: u64) -> Result<(), BufferSizeError> { - if !requested_size.is_multiple_of(4) { - Err(BufferSizeError::NotAligned(requested_size)) - } else if requested_size > self.max_size() { + if requested_size > self.max_size() { Err(BufferSizeError::TooLarge { requested_size, buffer_type: *self, diff --git a/fidget-wgpu/src/voxel.rs b/fidget-wgpu/src/voxel.rs index d7d13942..606e810a 100644 --- a/fidget-wgpu/src/voxel.rs +++ b/fidget-wgpu/src/voxel.rs @@ -90,8 +90,8 @@ //! copying to the host), see [`Buffers::image_storage_buffer`]. use crate::{ - ArrayBuffer, BufferSizeError, BufferType, ImageBuffer, opcode_constants, - usage::*, + ArrayBuffer, BufferItemCount, BufferSizeError, BufferType, ImageBuffer, + opcode_constants, usage::*, }; use fidget_bytecode::{Bytecode, ReservedRegister}; use fidget_core::{ @@ -1478,7 +1478,7 @@ pub struct Buffers { voxels: ArrayBuffer, /// Buffer of [`GeometryPixel`] data, generated by the normal pass - geom: ArrayBuffer, + geom: ImageBuffer, /// Result buffer that can be read back from the host /// @@ -1865,7 +1865,7 @@ impl Buffers { err, })?; - let geom = ArrayBuffer::new( + let geom = ImageBuffer::new( device, "geom".to_string(), Self::geom_buf_size(image_size), @@ -1991,24 +1991,38 @@ impl Buffers { let nx = usize::try_from(render_size.nx()).unwrap(); let ny = usize::try_from(render_size.ny()).unwrap(); let nz = usize::try_from(render_size.nz()).unwrap(); - nx * ny * nz - + (nx * ny) * ((64usize / 16).pow(3) + (64usize / 4).pow(3)) + + // Each tile contains 16³ and 4³ subtiles + let xy_size = (64usize / 4).pow(3) + (64usize / 16).pow(3); + + // Total size computation: + // nx * ny * nz + (nx * ny * xy_size) + // => nx * ny * (nz + xy_size) + nx.checked_mul(ny) + .unwrap() + .checked_mul(nz.checked_add(xy_size).unwrap()) + .unwrap() } fn voxels_buf_size(render_size: TileRenderSize) -> usize { render_size.pixels() } - /// Returns the size in bytes for the `geom` buffers - fn geom_buf_size(image_size: VoxelSize) -> usize { - usize::try_from(image_size.width()).unwrap() - * usize::try_from(image_size.height()).unwrap() + /// Returns the image size for the `geom` buffer + fn geom_buf_size(image_size: VoxelSize) -> ImageSize { + ImageSize::new(image_size.width(), image_size.height()) } + /// Returns image buffer size (in bytes) fn image_buf_size(image_size: VoxelSize) -> usize { - // Allocate an extra 16 bytes for timestamp queries - Self::geom_buf_size(image_size) * std::mem::size_of::() - + 16 + Self::geom_buf_size(image_size) + .item_count() + // Convert from GeometryPixel item count to bytes + .checked_mul(std::mem::size_of::()) + .unwrap() + // Allocate an extra 16 bytes for timestamp queries + .checked_add(16) + .unwrap() } /// Resizes to render the target image size