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 baf66980..3a18cdc3 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; @@ -215,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() } @@ -232,7 +230,7 @@ impl ImageSizeLike for pixel::RenderSize { } } -impl ImageSizeLike for voxel::RenderSize { +impl RenderSize for voxel::RenderSize { fn width(&self) -> u32 { self.width() } @@ -241,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)` @@ -282,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 { @@ -316,7 +314,7 @@ impl Image { } } -impl Image { +impl Image { /// Returns the image width pub fn width(&self) -> usize { self.size.width() as usize @@ -434,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); @@ -442,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-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/lib.rs b/fidget-wgpu/src/lib.rs index 15c9c977..976e0d3f 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,252 @@ 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() + .checked_mul(usize::try_from(self.height()).unwrap()) + .unwrap() + } +} + +impl GenericFlexBuffer { + fn new( + device: &wgpu::Device, + name: String, + item_count: B, + ) -> Result { + let item_count = item_count.item_count(); + 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 { + 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, + }) + } + + /// 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 { + Self::calculate_buffer_size(self.item_count) + } + + 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 `item_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 = Self::calculate_buffer_size(item_count); + 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 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 > 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 7af0b305..606e810a 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, BufferItemCount, 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")] @@ -581,11 +504,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 +862,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 +1088,11 @@ pub struct Context { struct TileBuffers { /// Tiles written by the stage outputting N³ tiles - tiles: Buffer, + tiles: ArrayBuffer, /// Sorted version of [`tiles`](Self::tiles) - sorted: Buffer, + sorted: ArrayBuffer, /// Minimum Z height at each XY tile - zmin: Buffer, + zmin: ImageBuffer, } impl TileBuffers { @@ -1179,31 +1102,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 = + ArrayBuffer::new(device, format!("active_tile{N}"), tile_buf_size) + .map_err(|err| TileBuffersError { + buf: TileBufferName::Tiles, + err, + })?; + let sorted = + ArrayBuffer::new(device, format!("sorted_tile{N}"), tile_buf_size) + .map_err(|err| TileBuffersError { + buf: TileBufferName::Sorted, + err, + })?; + let zmin = ImageBuffer::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, @@ -1217,19 +1131,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) -> ImageSize { + ImageSize::new( + render_size.width() / u32::try_from(N).unwrap(), + render_size.height() / u32::try_from(N).unwrap(), + ) } fn grow_to_fit( @@ -1292,11 +1208,11 @@ impl TileBuffers { /// Root tile buffers store strata-packed tile lists struct RootTileBuffers { /// Initial output tiles - tiles: Buffer, + tiles: ArrayBuffer, /// Strata-sorted output tiles - strata: Buffer, - zmin: Buffer, - zmax: Buffer, + strata: ArrayBuffer, + zmin: ImageBuffer, + zmax: ImageBuffer, } impl RootTileBuffers { @@ -1309,24 +1225,20 @@ impl RootTileBuffers { const N: usize = 64; // Allocate enough words to write all of the output tiles - let tiles = Buffer::new( + let tiles = ArrayBuffer::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 = ArrayBuffer::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 +1246,18 @@ 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 = + 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, @@ -1362,25 +1266,23 @@ 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) -> ImageSize { + ImageSize::new(render_size.nx(), render_size.ny()) } /// Grows all of the buffers to fit a particular render size @@ -1561,7 +1463,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: ArrayBuffer, /// Root tile Z heights (64³) tile64: RootTileBuffers, @@ -1573,16 +1475,16 @@ pub struct Buffers { tile4: TileBuffers<4>, /// Z heights for voxels - voxels: Buffer, + voxels: ArrayBuffer, /// Buffer of [`GeometryPixel`] data, generated by the normal pass - geom: Buffer, + geom: ImageBuffer, /// 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: ArrayBuffer, /// Query set for timestamps /// @@ -1739,7 +1641,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"), @@ -1915,108 +1817,7 @@ 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) - } -} - -/// Handle around a growable GPU buffer which pretends to be smaller -struct Buffer { - /// 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, -} - -impl Buffer { - fn new( - device: &wgpu::Device, - name: String, - size: u64, - usage: wgpu::BufferUsages, - ) -> Result { - assert_eq!(size % 4, 0); - 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, size, name }) - } - - 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, - size: u64, - ) -> Result<(), BufferSizeError> { - assert_eq!(size % 4, 0); - if size > self.capacity() { - 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.size = size; - 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 active buffer size - fn size(&self) -> u64 { - self.size - } - - /// Returns the total buffer capacity, which may be larger than its size - 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)); + (&self.geom.data, self.geom.size()) } } @@ -2043,22 +1844,20 @@ impl Buffers { }); let render_size = TileRenderSize::from(image_size); - let voxels = Buffer::new( + let voxels = ArrayBuffer::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 = ArrayBuffer::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 +1865,10 @@ impl Buffers { err, })?; - let geom = Buffer::new( + let geom = ImageBuffer::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 +1876,10 @@ impl Buffers { err, })?; - let image = Buffer::new( + let image = ArrayBuffer::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, @@ -2167,7 +1962,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()) } @@ -2192,29 +1987,42 @@ 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(); + + // 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) -> 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 + /// Returns the image size for the `geom` buffer + fn geom_buf_size(image_size: VoxelSize) -> ImageSize { + ImageSize::new(image_size.width(), image_size.height()) } - fn image_buf_size(image_size: VoxelSize) -> u64 { - // Allocate an extra 16 bytes for timestamp queries - Self::geom_buf_size(image_size) + 16 + /// Returns image buffer size (in bytes) + fn image_buf_size(image_size: VoxelSize) -> usize { + 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 @@ -3019,7 +2827,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), ); }