diff --git a/CHANGELOG.md b/CHANGELOG.md index e9607cb2..4b31f74f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,8 @@ This section may not be all-inclusive; sorry! I _did_ warn you that - Similarly, make `ShapeColorBuffers` not store GPU objects, so it can be constructed independently through `ShapeColorBuffers::new` (and shared between threads). +- Rename the workspace buffer objects from `Buffers` to `Workspace` (e.g. + `fidget::wgpu::voxel::Buffers`) to more clearly reflect their usage. # 0.5.0 This is a large release with a bunch of small features, reorganization, and one diff --git a/demos/cli/src/main.rs b/demos/cli/src/main.rs index 112af9e1..f507e341 100644 --- a/demos/cli/src/main.rs +++ b/demos/cli/src/main.rs @@ -396,11 +396,11 @@ fn run3d_wgpu( }; let mut image = Default::default(); let start = std::time::Instant::now(); - let mut buffers = ctx.buffers(); - let mut out = gpu.read_buffer_for(buffers.output()); + let mut workspace = ctx.workspace(); + let mut out = gpu.read_buffer_for(workspace.output()); let shape = fidget::wgpu::RenderShape::new(&shape)?; for _ in 0..settings.n { - image = ctx.run(&shape, &mut buffers, &mut out, cfg)?; + image = ctx.run(&shape, &mut workspace, &mut out, cfg)?; } let _ = image; info!( @@ -410,16 +410,16 @@ fn run3d_wgpu( ); let effects = fidget::wgpu::voxel::effects::Context::new(&gpu); - let mut merge_buf = effects.merge_buffers(); - let mut ssao_buf = effects.ssao_buffers(); - let mut shade_buf = effects.shade_buffers(); + let mut merge_buf = effects.merge_workspace(); + let mut ssao_buf = effects.ssao_workspace(); + let mut shade_buf = effects.shade_workspace(); let start = std::time::Instant::now(); use fidget::wgpu::voxel::effects::MergeSettings; let out_bytes = match mode { RenderMode3D::Heightmap => { effects.submit_merge( - buffers.output(), + workspace.output(), MergeSettings { denoise: false, z_scale: zflatten, @@ -434,7 +434,7 @@ fn run3d_wgpu( } RenderMode3D::BlurredOcclusion { denoise } => { effects.submit_merge( - buffers.output(), + workspace.output(), MergeSettings { denoise, z_scale: zflatten, @@ -447,7 +447,7 @@ fn run3d_wgpu( } RenderMode3D::RawOcclusion { denoise } => { effects.submit_merge( - buffers.output(), + workspace.output(), MergeSettings { denoise, z_scale: zflatten, @@ -460,7 +460,7 @@ fn run3d_wgpu( } RenderMode3D::Shaded { denoise, ssao } => { effects.submit_merge( - buffers.output(), + workspace.output(), MergeSettings { denoise, z_scale: zflatten, @@ -688,7 +688,7 @@ fn run2d_wgpu( }; let mut image = Default::default(); let start = std::time::Instant::now(); - let mut buffers = ctx.buffers(); + let mut buffers = ctx.workspace(); let mut out = gpu.read_buffer_for(buffers.output()); let shape = fidget::wgpu::RenderShape::new(&shape)?; let mut postprocess_time = std::time::Duration::ZERO; diff --git a/fidget-wgpu/src/lib.rs b/fidget-wgpu/src/lib.rs index 5e24dc53..10ba0deb 100644 --- a/fidget-wgpu/src/lib.rs +++ b/fidget-wgpu/src/lib.rs @@ -1,4 +1,66 @@ //! Shader generation and WGPU-based image rendering +//! +//! # API design +//! Using GPUs is complicated[citation needed]. The APIs in this +//! crate try to strike a balance between ease of use and efficiency. As +//! always, feel free to open an issue or discussion if the APIs don't work for +//! you; they were codesigned along with +//! [Halfspace](https://github.com/mkeeter/halfspace), and may not yet be +//! suitable for every use case. +//! +//! ## Object types +//! All of the modules use similar patterns of objects: +//! +//! - The [`Gpu`] object is passed around to provide device and queues +//! - A `Context` object contains pipelines +//! - A `Workspace` object contains buffers used when rendering +//! - An output buffer can be read back to the CPU or passed to a subsequent +//! render pipeline +//! +//! ### Context objects +//! A context object contains GPU pipelines and allow users to dispatch work to +//! the GPU. Voxel and pixel rendering are managed by [`voxel::Context`] and +//! [`pixel::Context`] respectively. Post-processing is done by +//! [`voxel::effects::Context`] and [`pixel::effects::Context`]. +//! +//! Users are expected to create one (of each) context object per thread or +//! worker, since GPU resources can't be shared. +//! +//! Context objects have two flavors of functions. At the highest level, `run` +//! and `run_async` functions perform rendering and copy data back to the CPU +//! (e.g. [`voxel::Context::run`] and [`run_async`](voxel::Context::run_async)). +//! To simply submit work to the GPU, use a `submit` function (e.g. +//! [`voxel::Context::submit`]). +//! +//! ### Workspace objects +//! Workspace objects contain all of the buffers that are used when dispatching +//! work to the GPU. They are also per-thread (or per-worker). You may have +//! more than one per thread if you want to dispatch multiple jobs +//! simultaneously; it's your computer. +//! +//! Workspaces resize themselves automatically when used in rendering. They +//! typically have [`size()`](voxel::Workspace::size) (active bytes) and +//! [`capacity()`](voxel::Workspace::capacity) (total allocated bytes) +//! functions; users may want to check for overly large ratios and recreate +//! workspaces. +//! +//! Workspaces are stateful; after they are used in a `submit` function, they +//! will contain data in a GPU buffer. The output buffer is typically accessed +//! with the `output()` function, e.g. [`voxel::Workspace::output`]. +//! +//! ### Reading data from buffers +//! The output of a workspace is a [`FlexBuffer`](crate::buf::FlexBuffer) +//! (indeed, they are used pervasively throughout this crate). Output buffers +//! are created with `STORAGE | COPY_SRC`. Reading data back to the CPU is a +//! three-part process: +//! +//! - Create a CPU-readable buffer with [`Gpu::read_buffer_for`] +//! - Copy data with [`Gpu::copy`] +//! - Map the readable buffer with [`Gpu::map`] or [`Gpu::map_async`] +//! +//! For quick debugging, [`Gpu::read_vec`] does all of these steps. You +//! wouldn't want to use in a tight loop, since it allocates a GPU buffer on +//! each call. #![warn(missing_docs)] use fidget_bytecode::{Bytecode, ReservedRegister}; diff --git a/fidget-wgpu/src/pixel/effects/mod.rs b/fidget-wgpu/src/pixel/effects/mod.rs index 92694576..de7b3a20 100644 --- a/fidget-wgpu/src/pixel/effects/mod.rs +++ b/fidget-wgpu/src/pixel/effects/mod.rs @@ -12,9 +12,9 @@ //! image to be completed) would simply be blurry; with distance interpolation, //! it remains sharper (though not pixel-perfect). //! -//! Output is stored in the [`MergeBuffers`] object, and may be accessed with -//! [`output_distance`](MergeBuffers::output_distance) and -//! [`output_color`](MergeBuffers::output_color). +//! Output is stored in the [`MergeWorkspace`] object, and may be accessed with +//! [`output_distance`](MergeWorkspace::output_distance) and +//! [`output_color`](MergeWorkspace::output_color). //! Note that if color has not been computed, `output_color` will return `None`. use crate::{ CopyVarsError, Gpu, RegPipeline, @@ -97,7 +97,7 @@ tag!( ); /// Handle to a set of buffers used when merging images -pub struct MergeBuffers { +pub struct MergeWorkspace { config: wgpu::Buffer, distance: FlexBuffer, color: FlexBuffer, @@ -112,7 +112,7 @@ pub struct MergeBuffers { has_color: bool, } -impl MergeBuffers { +impl MergeWorkspace { /// Resets the merge buffer /// /// The next call to [`Context::submit_merge`] will clear the buffer and @@ -222,14 +222,14 @@ impl Context { /// Submits a set of merge operations to accumulate a single image /// - /// [`MergeBuffers::reset`] should be called before the first call to + /// [`MergeWorkspace::reset`] should be called before the first call to /// `submit_merge`. For the first merge after a reset, the output buffer is /// resized to fit the images; subsequent merges must be of the same size. pub fn submit_merge( &self, image: &FlexBuffer, remove_nans: bool, - buf: &mut MergeBuffers, + buf: &mut MergeWorkspace, ) -> Result<(), MergeError> { let size = image.size(); if buf.image_count > 0 { @@ -320,8 +320,8 @@ impl Context { Ok(()) } - /// Builds a new set of [`MergeBuffers`] for the given image size - pub fn merge_buffers(&self) -> MergeBuffers { + /// Builds a new set of [`MergeWorkspace`] for the given image size + pub fn merge_workspace(&self) -> MergeWorkspace { let config = self.gpu.device.create_buffer(&wgpu::BufferDescriptor { label: Some("config"), size: std::mem::size_of::() as u64, @@ -337,7 +337,7 @@ impl Context { let color = FlexBuffer::new(&self.gpu.device, "pixel merge color", 64.into()) .unwrap(); - MergeBuffers { + MergeWorkspace { config, distance, color, @@ -348,12 +348,12 @@ impl Context { /// Submits a color evaluation pass /// - /// Image size is set from the `MergeBuffers`; the transform matrix is + /// Image size is set from the `MergeWorkspace`; the transform matrix is /// provided separately (but should be the same one used for image /// evaluation). pub fn submit_color( &self, - merge: &mut MergeBuffers, + merge: &mut MergeWorkspace, settings: ColorSettings, shape: &ShapeColorBuffers, bufs: &mut ColorWorkspace, @@ -369,12 +369,12 @@ impl Context { /// Submits a color evaluation pass with auxiliary variables /// - /// Image size is set from the `MergeBuffers`; the transform matrix is + /// Image size is set from the `MergeWorkspace`; the transform matrix is /// provided separately (but should be the same one used for image /// evaluation). pub fn submit_color_with_vars( &self, - merge: &mut MergeBuffers, + merge: &mut MergeWorkspace, settings: ColorSettings, shape: &ShapeColorBuffers, bufs: &mut ColorWorkspace, @@ -501,7 +501,7 @@ impl ColorContext { fn submit( &self, - image: &mut MergeBuffers, + image: &mut MergeWorkspace, settings: ColorSettings, shape: &ShapeColorBuffers, bufs: &mut ColorWorkspace, diff --git a/fidget-wgpu/src/pixel/mod.rs b/fidget-wgpu/src/pixel/mod.rs index 887395fc..be3e9eec 100644 --- a/fidget-wgpu/src/pixel/mod.rs +++ b/fidget-wgpu/src/pixel/mod.rs @@ -188,12 +188,12 @@ impl RootContext { fn run( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, reg_count: u8, render_size: TileRenderSize, compute_pass: &mut wgpu::ComputePass, ) { - let bind_group = buffers.bind_groups.root_tiles(ctx, buffers); + let bind_group = workspace.bind_groups.root_tiles(ctx, workspace); compute_pass.set_pipeline(self.root_pipeline.get(reg_count)); compute_pass.set_bind_group(1, bind_group, &[]); @@ -265,17 +265,17 @@ impl IntervalTilesContext { fn run( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, reg_count: u8, compute_pass: &mut wgpu::ComputePass, ) { - let bind_group = buffers.bind_groups.interval_tiles(ctx, buffers); + let bind_group = workspace.bind_groups.interval_tiles(ctx, workspace); compute_pass.set_pipeline(self.tiles_pipeline.get(reg_count)); compute_pass.set_bind_group(1, bind_group, &[]); // Indirect dispatch based on previous tile output compute_pass - .dispatch_workgroups_indirect(buffers.tile64.tiles.data(), 0); + .dispatch_workgroups_indirect(workspace.tile64.tiles.data(), 0); } } @@ -340,17 +340,17 @@ impl PixelTilesContext { fn run( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, reg_count: u8, compute_pass: &mut wgpu::ComputePass, ) { - let bind_group = buffers.bind_groups.pixel_tiles(ctx, buffers); + let bind_group = workspace.bind_groups.pixel_tiles(ctx, workspace); compute_pass.set_pipeline(self.tiles_pipeline.get(reg_count)); compute_pass.set_bind_group(1, bind_group, &[]); // Indirect dispatch based on previous tile output compute_pass - .dispatch_workgroups_indirect(buffers.tile8.tiles.data(), 0); + .dispatch_workgroups_indirect(workspace.tile8.tiles.data(), 0); } } @@ -402,11 +402,11 @@ tag!(TileTapesBufferTag, u32, usize, STORAGE | COPY_DST); tag!(pub PixelBufferTag, RawDistancePixel, ImageSize, STORAGE | COPY_SRC | COPY_DST, "Tag for a on-GPU buffer storing [`RawDistancePixel`] values"); -/// Buffers for rendering +/// Workspace for rendering /// -/// This object is constructed by [`Context::buffers`] and may only be used with +/// This object is constructed by [`Context::workspace`] and may only be used with /// that particular [`Context`]. -pub struct Buffers { +pub struct Workspace { /// Image render size /// /// Note that the tile buffers below round up to the nearest root tile @@ -435,7 +435,7 @@ pub struct Buffers { bind_groups: BindGroups, } -impl Buffers { +impl Workspace { /// Builds a new set of buffers with a default size /// /// It is expected that these will be resized before being used @@ -514,9 +514,9 @@ impl Buffers { &mut self, device: &wgpu::Device, image_size: ImageSize, - ) -> Result<(), BuffersError> { + ) -> Result<(), BufferError> { let render_size = TileRenderSize::from(image_size); - let Buffers { + let Workspace { image_size: image_size_ref, tile_tapes, tile64, @@ -533,25 +533,25 @@ impl Buffers { *image_size_ref = image_size; tile_tapes .grow_to_fit(device, Self::tile_tapes_buf_size(render_size)) - .map_err(|err| BuffersError { + .map_err(|err| BufferError { buf: BufferName::TileTapes, err, })?; tile64 .grow_to_fit(device, render_size) - .map_err(|e| BuffersError { + .map_err(|e| BufferError { buf: BufferName::Tile64(e.buf), err: e.err, })?; tile8 .grow_to_fit(device, render_size) - .map_err(|e| BuffersError { + .map_err(|e| BufferError { buf: BufferName::Tile8(e.buf), err: e.err, })?; pixels .grow_to_fit(device, image_size) - .map_err(|err| BuffersError { + .map_err(|err| BufferError { buf: BufferName::Pixel, err, })?; @@ -561,7 +561,7 @@ impl Buffers { /// Returns total allocated size (in bytes) pub fn capacity(&self) -> u64 { // Destructure to make sure we take all members into account - let Buffers { + let Workspace { image_size: _, config_buf, vars_buf, @@ -582,7 +582,7 @@ impl Buffers { /// Returns total active size (in bytes) pub fn size(&self) -> u64 { // Destructure to make sure we take all members into account - let Buffers { + let Workspace { image_size: _, vars_buf, config_buf, @@ -604,17 +604,17 @@ impl Buffers { /// /// This is intended for subsequent shaders which want to use the /// [`RawDistancePixel`] image data without copying to the CPU. It requires - /// a exclusive borrow of the `Buffers` object (and then extends that + /// an exclusive borrow of the `Workspace` object (and then extends that /// lifetime) so that other callers can't simultaneously touch the buffer. pub fn output(&mut self) -> &FlexBuffer { &self.pixels } } -/// Error returned when resizing a [`Buffers`] object +/// Error returned when resizing buffers in a [`Workspace`] object #[derive(Debug, thiserror::Error)] #[error("failed to build {buf} buffer")] -pub struct BuffersError { +pub struct BufferError { /// Buffer which failed to resize pub buf: BufferName, /// Error returned by buffer resizing @@ -660,14 +660,14 @@ pub enum SubmitError { MissingVar(#[from] MissingVar), /// Error while resizing buffers #[error(transparent)] - Buffers(#[from] BuffersError), + Buffers(#[from] BufferError), } impl From for SubmitError { fn from(value: CopyVarsError) -> Self { match value { CopyVarsError::MissingVar(v) => Self::MissingVar(v), - CopyVarsError::BufferSize(err) => Self::Buffers(BuffersError { + CopyVarsError::BufferSize(err) => Self::Buffers(BufferError { buf: BufferName::Vars, err, }), @@ -686,7 +686,7 @@ struct BindGroups { } impl BindGroups { - fn common(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn common(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup { self.common.get_or_init(|| { ctx.gpu .device @@ -696,22 +696,26 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.config_buf.as_entire_binding(), + resource: workspace.config_buf.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.tile_tapes.bind_active(), + resource: workspace.tile_tapes.bind_active(), }, wgpu::BindGroupEntry { binding: 2, - resource: buffers.vars_buf.bind_active(), + resource: workspace.vars_buf.bind_active(), }, ], }) }) } - fn root_tiles(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn root_tiles( + &self, + ctx: &Context, + workspace: &Workspace, + ) -> &wgpu::BindGroup { self.root_tiles.get_or_init(|| { ctx.gpu .device @@ -721,11 +725,11 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.tile64.tiles.bind_active(), + resource: workspace.tile64.tiles.bind_active(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.tile64.values.bind_active(), + resource: workspace.tile64.values.bind_active(), }, ], }) @@ -735,7 +739,7 @@ impl BindGroups { fn interval_tiles( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, ) -> &wgpu::BindGroup { self.interval_tiles.get_or_init(|| { ctx.gpu @@ -746,15 +750,15 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.tile64.tiles.bind_active(), + resource: workspace.tile64.tiles.bind_active(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.tile8.tiles.bind_active(), + resource: workspace.tile8.tiles.bind_active(), }, wgpu::BindGroupEntry { binding: 2, - resource: buffers.tile8.values.bind_active(), + resource: workspace.tile8.values.bind_active(), }, ], }) @@ -764,7 +768,7 @@ impl BindGroups { fn pixel_tiles( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, ) -> &wgpu::BindGroup { self.pixel_tiles.get_or_init(|| { ctx.gpu @@ -775,18 +779,18 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.tile8.tiles.bind_active(), + resource: workspace.tile8.tiles.bind_active(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.pixels.bind_active(), + resource: workspace.pixels.bind_active(), }, ], }) }) } - fn merge(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn merge(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup { self.merge.get_or_init(|| { ctx.gpu .device @@ -796,15 +800,15 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.tile64.values.bind_active(), + resource: workspace.tile64.values.bind_active(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.tile8.values.bind_active(), + resource: workspace.tile8.values.bind_active(), }, wgpu::BindGroupEntry { binding: 2, - resource: buffers.pixels.bind_active(), + resource: workspace.pixels.bind_active(), }, ], }) @@ -829,7 +833,7 @@ struct TileBuffers { /// Error type when resizing root tile buffers #[derive(Debug, thiserror::Error)] #[error("failed to resize `{buf}` root tile buffer")] -pub struct TileBuffersError { +pub struct TileBufferError { /// Buffer which failed to resize pub buf: TileBufferName, /// Error returned by buffer resizing @@ -860,14 +864,14 @@ impl TileBuffers { fn new( device: &wgpu::Device, render_size: TileRenderSize, - ) -> Result { + ) -> Result { // Allocate enough words to write all of the output tiles let tiles = FlexBuffer::new( device, format!("tiles_out{N}"), Self::tiles_buf_size(render_size), ) - .map_err(|err| TileBuffersError { + .map_err(|err| TileBufferError { buf: TileBufferName::Tiles, err, })?; @@ -875,7 +879,7 @@ impl TileBuffers { let values_buf_size = Self::values_buf_size(render_size); let values = FlexBuffer::new(device, format!("tile{N}_values"), values_buf_size) - .map_err(|err| TileBuffersError { + .map_err(|err| TileBufferError { buf: TileBufferName::Values, err, })?; @@ -906,18 +910,18 @@ impl TileBuffers { &mut self, device: &wgpu::Device, render_size: TileRenderSize, - ) -> Result<(), TileBuffersError> { + ) -> Result<(), TileBufferError> { // Destructure to make sure we take all members into account let TileBuffers { tiles, values } = self; tiles .grow_to_fit(device, Self::tiles_buf_size(render_size)) - .map_err(|err| TileBuffersError { + .map_err(|err| TileBufferError { buf: TileBufferName::Tiles, err, })?; values .grow_to_fit(device, Self::values_buf_size(render_size)) - .map_err(|err| TileBuffersError { + .map_err(|err| TileBufferError { buf: TileBufferName::Values, err, })?; @@ -1001,13 +1005,13 @@ impl Context { } } - /// Builds a new [`Buffers`] object for use in rendering + /// Builds a new [`Workspace`] object for use in rendering /// /// The buffers are initialized with a dummy size and resized automatically /// when passed into any of the runner functions (e.g. [`run`](Self::run) or /// [`submit`](Self::submit)). - pub fn buffers(&self) -> Buffers { - Buffers::new(&self.gpu.device) + pub fn workspace(&self) -> Workspace { + Workspace::new(&self.gpu.device) } /// Renders the image, with a blocking wait to read pixel data from the GPU @@ -1017,11 +1021,11 @@ impl Context { pub fn run( &self, shape: &RenderShape, - buffers: &mut Buffers, + workspace: &mut Workspace, out: &mut ReadBuffer, settings: RenderConfig, ) -> Result { - self.run_with_vars(shape, &Default::default(), buffers, out, settings) + self.run_with_vars(shape, &Default::default(), workspace, out, settings) } /// Renders the image, with a blocking wait to read pixel data from the GPU @@ -1032,12 +1036,12 @@ impl Context { &self, shape: &RenderShape, vars: &ShapeVars, - buffers: &mut Buffers, + workspace: &mut Workspace, out: &mut ReadBuffer, settings: RenderConfig, ) -> Result { - self.submit_with_vars(shape, vars, buffers, &settings)?; - self.gpu.copy(buffers.output(), out); + self.submit_with_vars(shape, vars, workspace, &settings)?; + self.gpu.copy(workspace.output(), out); let image = self.gpu.map_image(out); Ok(image.image()) } @@ -1046,14 +1050,14 @@ impl Context { pub async fn run_async( &self, shape: &RenderShape, - buffers: &mut Buffers, + workspace: &mut Workspace, out: &mut ReadBuffer, settings: RenderConfig, ) -> Result { self.run_with_vars_async( shape, &Default::default(), - buffers, + workspace, out, settings, ) @@ -1065,12 +1069,12 @@ impl Context { &self, shape: &RenderShape, vars: &ShapeVars, - buffers: &mut Buffers, + workspace: &mut Workspace, out: &mut ReadBuffer, settings: RenderConfig, ) -> Result { - self.submit_with_vars(shape, vars, buffers, &settings)?; - self.gpu.copy(buffers.output(), out); + self.submit_with_vars(shape, vars, workspace, &settings)?; + self.gpu.copy(workspace.output(), out); let image = self.gpu.map_image_async(out).await; Ok(image.image()) } @@ -1078,14 +1082,14 @@ impl Context { /// Submits a single image to be rendered on the GPU /// /// The resulting image (as a buffer of [`RawDistancePixel`] data) is - /// available on the GPU in [`buffers.output()`](Buffers::output). + /// available on the GPU in [`buffers.output()`](Workspace::output). pub fn submit( &self, shape: &RenderShape, - buffers: &mut Buffers, + workspace: &mut Workspace, settings: &RenderConfig, ) -> Result<(), SubmitError> { - self.submit_with_vars(shape, &Default::default(), buffers, settings) + self.submit_with_vars(shape, &Default::default(), workspace, settings) } /// Submits a single image to be rendered on the GPU, with extra variables @@ -1095,16 +1099,16 @@ impl Context { &self, shape: &RenderShape, vars: &ShapeVars, - buffers: &mut Buffers, + workspace: &mut Workspace, settings: &RenderConfig, ) -> Result<(), SubmitError> { - buffers.set_image_size(&self.gpu.device, settings.image_size)?; - let render_size = TileRenderSize::from(buffers.image_size); + workspace.set_image_size(&self.gpu.device, settings.image_size)?; + let render_size = TileRenderSize::from(workspace.image_size); // The WebGPU config type has a mat3x3f, but that type pads each row to // 16 bytes, so we'll just use a mat4x4 for simplicity let mat = - settings.world_to_model * buffers.image_size.screen_to_world(); + settings.world_to_model * workspace.image_size.screen_to_world(); let mut mat4 = nalgebra::Matrix4x3::::identity(); mat4.fixed_view_mut::<3, 3>(0, 0).copy_from(&mat); @@ -1116,8 +1120,8 @@ impl Context { render_size: [render_size.width(), render_size.height()], tape_data_capacity: TAPE_DATA_CAPACITY.try_into().unwrap(), image_size: [ - buffers.image_size.width(), - buffers.image_size.height(), + workspace.image_size.width(), + workspace.image_size.height(), ], tape_data_offset: start_offset, z: settings.z, @@ -1132,7 +1136,7 @@ impl Context { .gpu .queue .write_buffer_with( - &buffers.config_buf, + &workspace.config_buf, 0, ((config_len + shape.bytecode.as_bytes().len()) as u64) .try_into() @@ -1150,10 +1154,10 @@ impl Context { // Copy vars (if present), then reset relevant bind groups if the buffer // size has changed. if matches!( - shape.copy_vars(&self.gpu, vars, &mut buffers.vars_buf)?, + shape.copy_vars(&self.gpu, vars, &mut workspace.vars_buf)?, CopyVarsChanged::BufferChanged ) { - buffers.bind_groups.common = Default::default(); + workspace.bind_groups.common = Default::default(); } // Create a command encoder and dispatch the compute work @@ -1162,7 +1166,7 @@ impl Context { ); // Initial buffer reset pass - self.reset_ctx.run(&mut encoder, buffers); + self.reset_ctx.run(&mut encoder, workspace); let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { @@ -1171,26 +1175,26 @@ impl Context { }); // Build the common config buffer - let common_bind_group = buffers.bind_groups.common(self, buffers); + let common_bind_group = workspace.bind_groups.common(self, workspace); compute_pass.set_bind_group(0, common_bind_group, &[]); // Populate root tiles (64x64x64, densely packed) self.root_ctx.run( self, - buffers, + workspace, shape.bytecode.reg_count(), render_size, &mut compute_pass, ); self.tiles_ctx.run( self, - buffers, + workspace, shape.bytecode.reg_count(), &mut compute_pass, ); self.pixels_ctx.run( self, - buffers, + workspace, shape.bytecode.reg_count(), &mut compute_pass, ); @@ -1198,7 +1202,7 @@ impl Context { // Merge filled tiles from large -> small self.merge_ctx.run( self, - buffers, + workspace, settings.image_size, &mut compute_pass, ); @@ -1272,11 +1276,11 @@ impl MergeContext { fn run( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, render_size: ImageSize, compute_pass: &mut wgpu::ComputePass, ) { - let bind_group = buffers.bind_groups.merge(ctx, buffers); + let bind_group = workspace.bind_groups.merge(ctx, workspace); compute_pass.set_pipeline(&self.pipeline); compute_pass.set_bind_group(1, bind_group, &[]); compute_pass.dispatch_workgroups( @@ -1291,16 +1295,16 @@ impl MergeContext { struct ResetContext; impl ResetContext { - fn run(&self, encoder: &mut wgpu::CommandEncoder, buffers: &Buffers) { + fn run(&self, encoder: &mut wgpu::CommandEncoder, workspace: &Workspace) { // Clear `count` and `wg_size` members of the tile output buffers - encoder.clear_buffer(buffers.tile64.tiles.data(), 0, Some(16)); - encoder.clear_buffer(buffers.tile8.tiles.data(), 0, Some(16)); - buffers.tile64.values.clear(encoder); - buffers.tile8.values.clear(encoder); - buffers.pixels.clear(encoder); + encoder.clear_buffer(workspace.tile64.tiles.data(), 0, Some(16)); + encoder.clear_buffer(workspace.tile8.tiles.data(), 0, Some(16)); + workspace.tile64.values.clear(encoder); + workspace.tile8.values.clear(encoder); + workspace.pixels.clear(encoder); // Clear the whole tile tape map (TODO is this needed?) - buffers.tile_tapes.clear(encoder); + workspace.tile_tapes.clear(encoder); } } @@ -1348,8 +1352,8 @@ mod test { let pixel_ctx = Context::new(&gpu); let effects_ctx = effects::Context::new(&gpu); - let mut buf = pixel_ctx.buffers(); - let mut merge_buf = effects_ctx.merge_buffers(); + let mut buf = pixel_ctx.workspace(); + let mut merge_buf = effects_ctx.merge_workspace(); // Render and accumulate each shape for (shape, _) in shapes { diff --git a/fidget-wgpu/src/voxel/effects/mod.rs b/fidget-wgpu/src/voxel/effects/mod.rs index 7a4498c7..de04e69f 100644 --- a/fidget-wgpu/src/voxel/effects/mod.rs +++ b/fidget-wgpu/src/voxel/effects/mod.rs @@ -204,13 +204,13 @@ tag!(pub MergeVoxelBufferTag, PackedVoxel, VoxelSize, STORAGE | COPY_SRC, ); /// Handle to a set of buffers used when merging images -pub struct MergeBuffers { +pub struct MergeWorkspace { config: wgpu::Buffer, out: FlexBuffer, image_count: usize, } -impl MergeBuffers { +impl MergeWorkspace { /// Returns a handle to the output buffer pub fn output(&self) -> &FlexBuffer { &self.out @@ -231,13 +231,13 @@ tag!( ); /// Handle to a set of buffers used when shading images -pub struct ShadeBuffers { +pub struct ShadeWorkspace { config: wgpu::Buffer, has_color: bool, out: FlexBuffer, } -impl ShadeBuffers { +impl ShadeWorkspace { /// Returns a reference to the output buffer pub fn output(&self) -> &FlexBuffer { &self.out @@ -473,11 +473,11 @@ impl Context { } } - /// Builds a new set of [`MergeBuffers`] + /// Builds a new set of [`MergeWorkspace`] /// /// These will be resized when first used (in /// [`submit_merge`](Self::submit_merge)) - pub fn merge_buffers(&self) -> MergeBuffers { + pub fn merge_workspace(&self) -> MergeWorkspace { let config = self.gpu.device.create_buffer(&wgpu::BufferDescriptor { label: Some("config"), size: std::mem::size_of::() as u64, @@ -486,19 +486,19 @@ impl Context { }); let out = FlexBuffer::new(&self.gpu.device, "merge output", 64.into()) .expect("64 is always a valid size"); - MergeBuffers { + MergeWorkspace { config, out, image_count: 0, } } - /// Builds a new set of [`ShadeBuffers`] + /// Builds a new set of [`ShadeWorkspace`] /// /// These will be resized when first used (in /// either [`submit_color`](Self::submit_color) or /// [`submit_shade`](Self::submit_shade)) - pub fn shade_buffers(&self) -> ShadeBuffers { + pub fn shade_workspace(&self) -> ShadeWorkspace { let config = self.gpu.device.create_buffer(&wgpu::BufferDescriptor { label: Some("shade config"), size: std::mem::size_of::() as u64, @@ -507,18 +507,18 @@ impl Context { }); let out = FlexBuffer::new(&self.gpu.device, "shade output", 64.into()) .expect("64 is always a valid size"); - ShadeBuffers { + ShadeWorkspace { config, has_color: false, out, } } - /// Builds a new set of [`SsaoBuffers`] + /// Builds a new set of [`SsaoWorkspace`] /// /// These will be resized when first used (in /// [`submit_ssao`](Self::submit_ssao)) - pub fn ssao_buffers(&self) -> SsaoBuffers { + pub fn ssao_workspace(&self) -> SsaoWorkspace { let ssao_config = self.gpu.device.create_buffer(&wgpu::BufferDescriptor { label: Some("ssao config"), @@ -545,7 +545,7 @@ impl Context { image_size, ) .expect("64 is always a valid size"); - SsaoBuffers { + SsaoWorkspace { ssao_config, blur_config, raw_occlusion, @@ -555,14 +555,14 @@ impl Context { /// Accumulates an image into a merged image buffer /// - /// [`MergeBuffers::reset`] should be called before the first call to + /// [`MergeWorkspace::reset`] should be called before the first call to /// `submit_merge`. For the first merge after a reset, the output buffer is /// resized to fit the images; subsequent merges must be of the same size. pub fn submit_merge( &self, image: &FlexBuffer, settings: MergeSettings, - buf: &mut MergeBuffers, + buf: &mut MergeWorkspace, ) -> Result<(), MergeError> { let size = image.size(); if buf.image_count == 0 { @@ -649,9 +649,9 @@ impl Context { /// The output buffer is resized to fit the incoming image pub fn submit_shade( &self, - image: &MergeBuffers, - ssao: Option<&SsaoBuffers>, - buf: &mut ShadeBuffers, + image: &MergeWorkspace, + ssao: Option<&SsaoWorkspace>, + buf: &mut ShadeWorkspace, ) -> Result<(), ShadeError> { let size = image.out.size(); if buf.has_color { @@ -748,8 +748,8 @@ impl Context { /// The output buffer is resized to fit the incoming image pub fn submit_heightmap( &self, - image: &MergeBuffers, - buf: &mut ShadeBuffers, + image: &MergeWorkspace, + buf: &mut ShadeWorkspace, ) -> Result<(), HeightmapError> { let size = image.out.size(); if buf.has_color { @@ -828,24 +828,24 @@ impl Context { /// Submits a pass to compute an SSAO buffer pub fn submit_ssao( &self, - image: &MergeBuffers, - buf: &mut SsaoBuffers, + image: &MergeWorkspace, + buf: &mut SsaoWorkspace, ) -> Result<(), SsaoError> { self.ssao_ctx.submit(image, buf, &self.gpu) } /// Submits a color evaluation pass /// - /// Image size is set from the `MergeBuffers`; the transform matrix is + /// Image size is set from the `MergeWorkspace`; the transform matrix is /// provided separately (but should be the same one used for image /// evaluation). pub fn submit_color( &self, - merge: &MergeBuffers, + merge: &MergeWorkspace, world_to_model: &nalgebra::Matrix4, shape: &ShapeColorBuffers, bufs: &mut ColorWorkspace, - out: &mut ShadeBuffers, + out: &mut ShadeWorkspace, ) -> Result<(), ColorError> { self.submit_color_with_vars( merge, @@ -859,17 +859,17 @@ impl Context { /// Submits a color evaluation pass with auxiliary variables /// - /// Image size is set from the `MergeBuffers`; the transform matrix is + /// Image size is set from the `MergeWorkspace`; the transform matrix is /// provided separately (but should be the same one used for image /// evaluation). pub fn submit_color_with_vars( &self, - merge: &MergeBuffers, + merge: &MergeWorkspace, world_to_model: &nalgebra::Matrix4, shape: &ShapeColorBuffers, bufs: &mut ColorWorkspace, vars: &ShapeVars, - out: &mut ShadeBuffers, + out: &mut ShadeWorkspace, ) -> Result<(), ColorError> { self.color_ctx.submit( merge, @@ -896,7 +896,7 @@ tag!(pub SsaoBlurredBufferTag, f32, ImageSize, STORAGE | COPY_SRC, "Tag for a blurred SSAO occlusion buffer"); /// Handle to a set of buffers used when running an SSAO pass -pub struct SsaoBuffers { +pub struct SsaoWorkspace { ssao_config: wgpu::Buffer, // TODO add a `ConfigBuffer` type? raw_occlusion: FlexBuffer, @@ -904,7 +904,7 @@ pub struct SsaoBuffers { blurred_occlusion: FlexBuffer, } -impl SsaoBuffers { +impl SsaoWorkspace { /// Returns a shared handle to the raw SSAO occlusion buffer pub fn raw_occlusion(&self) -> &FlexBuffer { &self.raw_occlusion @@ -1085,8 +1085,8 @@ impl SsaoContext { fn submit( &self, - image: &MergeBuffers, - buf: &mut SsaoBuffers, + image: &MergeWorkspace, + buf: &mut SsaoWorkspace, gpu: &Gpu, ) -> Result<(), SsaoError> { let image_size = image.out.size(); @@ -1279,12 +1279,12 @@ impl ColorContext { #[allow(clippy::too_many_arguments)] // what are ya gonna do? fn submit( &self, - image: &MergeBuffers, + image: &MergeWorkspace, world_to_model: &nalgebra::Matrix4, shape: &ShapeColorBuffers, bufs: &mut ColorWorkspace, vars: &ShapeVars, - out: &mut ShadeBuffers, + out: &mut ShadeWorkspace, gpu: &Gpu, ) -> Result<(), ColorError> { if image.image_count != shape.shape_count() { @@ -1429,8 +1429,8 @@ mod test { let size = 128; let image_size = RenderSize::from(size); - let mut buf = voxel_ctx.buffers(); - let mut merge_buf = effects_ctx.merge_buffers(); + let mut voxel_buf = voxel_ctx.workspace(); + let mut merge_buf = effects_ctx.merge_workspace(); let (x, y, z) = Tree::axes(); let sphere = @@ -1441,7 +1441,7 @@ mod test { voxel_ctx .submit( &shape, - &mut buf, + &mut voxel_buf, &crate::voxel::RenderConfig { image_size, world_to_model: nalgebra::Matrix4::identity(), @@ -1449,9 +1449,13 @@ mod test { ) .unwrap(); effects_ctx - .submit_merge(buf.output(), Default::default(), &mut merge_buf) + .submit_merge( + voxel_buf.output(), + Default::default(), + &mut merge_buf, + ) .unwrap(); - let mut ssao_buf = effects_ctx.ssao_buffers(); + let mut ssao_buf = effects_ctx.ssao_workspace(); effects_ctx.submit_ssao(&merge_buf, &mut ssao_buf).unwrap(); let ssao_out = gpu.read_vec(ssao_buf.raw_occlusion()); diff --git a/fidget-wgpu/src/voxel/mod.rs b/fidget-wgpu/src/voxel/mod.rs index 964d648f..6ea62110 100644 --- a/fidget-wgpu/src/voxel/mod.rs +++ b/fidget-wgpu/src/voxel/mod.rs @@ -53,14 +53,14 @@ //! - [`RenderShape`] contains serialized bytecode to render a particular shape. //! Best practice is to rebuild it only when a shape changes (i.e. not once //! per frame), although in practice it's pretty fast to construct. -//! - [`Buffers`] contains GPU buffers needed for rendering at a particular +//! - [`Workspace`] contains GPU buffers needed for rendering at a particular //! image size. It is primarily expensive in GPU memory, as it contains -//! several full-frame buffers. Best practice is to construct one [`Buffers`] -//! object per worker context (or per simultaneous render); it will be -//! automatically resized when used. Systems with high variability in image -//! size may want to periodically compare [`size`](Buffers::size) versus -//! [`capacity`](Buffers::capacity) and fully reallocate buffers (by -//! constructing a new `Buffers` object) if they get too out of whack. +//! several full-frame buffers. Best practice is to construct one +//! [`Workspace`] object per worker context (or per simultaneous render); it +//! will be automatically resized when used. Systems with high variability in +//! image size may want to periodically compare [`size`](Workspace::size) +//! versus [`capacity`](Workspace::capacity) and fully reallocate buffers (by +//! constructing a new `Workspace` object) if they get too out of whack. //! - [`RenderConfig`] sets the transform matrix for rendering. This is cheap //! to construct and could be built once per frame //! @@ -68,7 +68,7 @@ //! - Build a [`Context`] //! - Use [`RenderShape::new`] to convert from a //! [`VmShape`](fidget_core::vm::VmShape) to a [`RenderShape`] -//! - Use [`Context::buffers`] to get [`Buffers`] at a particular image size +//! - Use [`Context::workspace`] to get [`Workspace`] //! - Use [`Gpu::read_buffer_for(buffers.output())`](Gpu::read_buffer_for) to //! get an output buffer //! - Call [`Context::run`] or [`Context::run_async`] to get an image @@ -93,8 +93,8 @@ //! - Read image data back to the CPU //! //! Lower-level building blocks are also available: [`Context::submit`] submits -//! the render operations to the GPU, and [`Buffers::output`] returns the output -//! buffer. +//! the render operations to the GPU, and [`Workspace::output`] returns the +//! output buffer. use crate::{ CopyVarsChanged, CopyVarsError, Gpu, RegPipeline, RenderShape, @@ -228,10 +228,10 @@ impl std::fmt::Display for BufferName { } } -/// Error returned when resizing a [`Buffers`] object +/// Error returned when resizing buffers in a [`Workspace`] object #[derive(Debug, thiserror::Error)] #[error("failed to build {buf} buffer")] -pub struct BuffersError { +pub struct BufferError { /// Buffer which failed to resize pub buf: BufferName, /// Error returned by buffer resizing @@ -247,14 +247,14 @@ pub enum SubmitError { MissingVar(#[from] MissingVar), /// Error while resizing buffers #[error(transparent)] - Buffers(#[from] BuffersError), + Buffer(#[from] BufferError), } impl From for SubmitError { fn from(value: CopyVarsError) -> Self { match value { CopyVarsError::MissingVar(v) => Self::MissingVar(v), - CopyVarsError::BufferSize(err) => Self::Buffers(BuffersError { + CopyVarsError::BufferSize(err) => Self::Buffer(BufferError { buf: BufferName::Vars, err, }), @@ -504,12 +504,12 @@ impl RootContext { fn run( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, reg_count: u8, render_size: TileRenderSize, compute_pass: &mut wgpu::ComputePass, ) { - let bind_group = buffers.bind_groups.root(ctx, buffers); + let bind_group = workspace.bind_groups.root(ctx, workspace); compute_pass.set_pipeline(self.root_pipeline.get(reg_count)); compute_pass.set_bind_group(1, bind_group, &[]); @@ -582,11 +582,11 @@ impl RepackContext { fn run( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, render_size: TileRenderSize, compute_pass: &mut wgpu::ComputePass, ) { - let bind_group = buffers.bind_groups.repack(ctx, buffers); + let bind_group = workspace.bind_groups.repack(ctx, workspace); compute_pass.set_pipeline(&self.repack_pipeline); compute_pass.set_bind_group(1, bind_group, &[]); @@ -792,14 +792,15 @@ impl IntervalContext { fn run( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, strata: u64, reg_count: u8, compute_pass: &mut wgpu::ComputePass, ) { - let strata_bytes = u64::try_from(buffers.strata_size_bytes()).unwrap(); + let strata_bytes = + u64::try_from(workspace.strata_size_bytes()).unwrap(); let offset_bytes = strata * strata_bytes; - let bind_group16 = buffers.bind_groups.interval16(ctx, buffers); + let bind_group16 = workspace.bind_groups.interval16(ctx, workspace); compute_pass.set_pipeline(self.interval64_pipeline.get(reg_count)); compute_pass.set_bind_group( 1, @@ -807,27 +808,27 @@ impl IntervalContext { &[u32::try_from(offset_bytes).unwrap()], ); compute_pass.dispatch_workgroups_indirect( - buffers.tile64.strata.data(), + workspace.tile64.strata.data(), offset_bytes, ); - let bind_group_sort16 = buffers.bind_groups.sort16(ctx, buffers); + let bind_group_sort16 = workspace.bind_groups.sort16(ctx, workspace); compute_pass.set_pipeline(&self.sort16_pipeline); compute_pass.set_bind_group(1, bind_group_sort16, &[]); compute_pass - .dispatch_workgroups_indirect(buffers.tile16.tiles.data(), 0); + .dispatch_workgroups_indirect(workspace.tile16.tiles.data(), 0); - let bind_group4 = buffers.bind_groups.interval4(ctx, buffers); + let bind_group4 = workspace.bind_groups.interval4(ctx, workspace); compute_pass.set_pipeline(self.interval16_pipeline.get(reg_count)); compute_pass.set_bind_group(1, bind_group4, &[0]); compute_pass - .dispatch_workgroups_indirect(buffers.tile16.sorted.data(), 0); + .dispatch_workgroups_indirect(workspace.tile16.sorted.data(), 0); - let bind_group_sort4 = buffers.bind_groups.sort4(ctx, buffers); + let bind_group_sort4 = workspace.bind_groups.sort4(ctx, workspace); compute_pass.set_pipeline(&self.sort4_pipeline); compute_pass.set_bind_group(1, bind_group_sort4, &[]); compute_pass - .dispatch_workgroups_indirect(buffers.tile4.tiles.data(), 0); + .dispatch_workgroups_indirect(workspace.tile4.tiles.data(), 0); } } @@ -903,18 +904,18 @@ impl VoxelContext { fn run( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, reg_count: u8, compute_pass: &mut wgpu::ComputePass, ) { - let bind_group = buffers.bind_groups.voxel(ctx, buffers); + let bind_group = workspace.bind_groups.voxel(ctx, workspace); compute_pass.set_pipeline(self.voxel_pipeline.get(reg_count)); compute_pass.set_bind_group(1, bind_group, &[]); // Each workgroup is 4x4x4, i.e. covering a 4x4 splat of pixels with 4x // workers in the Z direction. compute_pass - .dispatch_workgroups_indirect(buffers.tile4.sorted.data(), 0); + .dispatch_workgroups_indirect(workspace.tile4.sorted.data(), 0); } } @@ -976,17 +977,17 @@ impl NormalsContext { fn run( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, reg_count: u8, compute_pass: &mut wgpu::ComputePass, ) { - let bind_group = buffers.bind_groups.normals(ctx, buffers); + let bind_group = workspace.bind_groups.normals(ctx, workspace); compute_pass.set_pipeline(self.normals_pipeline.get(reg_count)); compute_pass.set_bind_group(1, bind_group, &[]); compute_pass.dispatch_workgroups( - buffers.image_size.width().div_ceil(8), - buffers.image_size.height().div_ceil(8), + workspace.image_size.width().div_ceil(8), + workspace.image_size.height().div_ceil(8), 1, ); } @@ -1299,11 +1300,11 @@ tag!(pub GeomBufferTag, GeometryPixel, VoxelSize, STORAGE | COPY_SRC | COPY_DST, "Tag for a on-GPU buffer storing [`GeometryPixel`] values"); tag!(pub(crate) VarsBufferTag, f32, usize, STORAGE | COPY_DST); -/// Buffers for rendering +/// Workspace for rendering /// -/// This object is constructed by [`Context::buffers`] and may only be used with +/// This object is constructed by [`Context::workspace`] and may only be used with /// that particular [`Context`]. -pub struct Buffers { +pub struct Workspace { /// Image render size /// /// Note that the tile buffers below round up to the nearest root tile @@ -1366,7 +1367,7 @@ struct BindGroups { } impl BindGroups { - fn common(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn common(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup { self.common.get_or_init(|| { ctx.gpu .device @@ -1376,22 +1377,22 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.config_buf.as_entire_binding(), + resource: workspace.config_buf.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.tile_tapes.bind_active(), + resource: workspace.tile_tapes.bind_active(), }, wgpu::BindGroupEntry { binding: 2, - resource: buffers.vars_buf.bind_active(), + resource: workspace.vars_buf.bind_active(), }, ], }) }) } - fn clear(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn clear(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup { self.clear.get_or_init(|| { ctx.gpu .device @@ -1401,7 +1402,7 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers + resource: workspace .tile16 .tiles .data() @@ -1410,7 +1411,7 @@ impl BindGroups { }, wgpu::BindGroupEntry { binding: 1, - resource: buffers + resource: workspace .tile16 .sorted .data() @@ -1419,7 +1420,7 @@ impl BindGroups { }, wgpu::BindGroupEntry { binding: 2, - resource: buffers + resource: workspace .tile4 .tiles .data() @@ -1428,7 +1429,7 @@ impl BindGroups { }, wgpu::BindGroupEntry { binding: 3, - resource: buffers + resource: workspace .tile4 .sorted .data() @@ -1437,14 +1438,14 @@ impl BindGroups { }, wgpu::BindGroupEntry { binding: 4, - resource: buffers.z_hist_buf.as_entire_binding(), + resource: workspace.z_hist_buf.as_entire_binding(), }, ], }) }) } - fn merge(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn merge(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup { self.merge.get_or_init(|| { ctx.gpu .device @@ -1454,26 +1455,26 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.tile64.zmin.bind_active(), + resource: workspace.tile64.zmin.bind_active(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.tile16.zmin.bind_active(), + resource: workspace.tile16.zmin.bind_active(), }, wgpu::BindGroupEntry { binding: 2, - resource: buffers.tile4.zmin.bind_active(), + resource: workspace.tile4.zmin.bind_active(), }, wgpu::BindGroupEntry { binding: 3, - resource: buffers.voxels.bind_active(), + resource: workspace.voxels.bind_active(), }, ], }) }) } - fn root(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn root(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup { self.root.get_or_init(|| { ctx.gpu .device @@ -1483,18 +1484,18 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.tile64.tiles.bind_active(), + resource: workspace.tile64.tiles.bind_active(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.tile64.zmax.bind_active(), + resource: workspace.tile64.zmax.bind_active(), }, ], }) }) } - fn repack(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn repack(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup { self.repack.get_or_init(|| { ctx.gpu .device @@ -1504,23 +1505,28 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.tile64.tiles.bind_active(), + resource: workspace.tile64.tiles.bind_active(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.tile64.zmax.bind_active(), + resource: workspace.tile64.zmax.bind_active(), }, wgpu::BindGroupEntry { binding: 2, - resource: buffers.tile64.strata.bind_active(), + resource: workspace.tile64.strata.bind_active(), }, ], }) }) } - fn interval16(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { - let strata_bytes = u64::try_from(buffers.strata_size_bytes()).unwrap(); + fn interval16( + &self, + ctx: &Context, + workspace: &Workspace, + ) -> &wgpu::BindGroup { + let strata_bytes = + u64::try_from(workspace.strata_size_bytes()).unwrap(); self.interval16.get_or_init(|| { ctx.gpu .device @@ -1530,7 +1536,7 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers + resource: workspace .tile64 .strata .data() @@ -1539,41 +1545,41 @@ impl BindGroups { }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.tile64.zmin.bind_active(), + resource: workspace.tile64.zmin.bind_active(), }, wgpu::BindGroupEntry { binding: 2, - resource: buffers.tile16.tiles.bind_active(), + resource: workspace.tile16.tiles.bind_active(), }, wgpu::BindGroupEntry { binding: 3, - resource: buffers.tile16.zmin.bind_active(), + resource: workspace.tile16.zmin.bind_active(), }, wgpu::BindGroupEntry { binding: 4, - resource: buffers.z_hist_buf.slice(0..16).into(), + resource: workspace.z_hist_buf.slice(0..16).into(), }, ], }) }) } - fn sort16(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn sort16(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup { self.sort16.get_or_init(|| { Self::sort_bind_group( ctx, - &buffers.tile16, - buffers.z_hist_buf.slice(0..16).into(), + &workspace.tile16, + workspace.z_hist_buf.slice(0..16).into(), ) }) } - fn sort4(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn sort4(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup { self.sort4.get_or_init(|| { Self::sort_bind_group( ctx, - &buffers.tile4, - buffers.z_hist_buf.slice(256..320).into(), + &workspace.tile4, + workspace.z_hist_buf.slice(256..320).into(), ) }) } @@ -1605,7 +1611,11 @@ impl BindGroups { }) } - fn interval4(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn interval4( + &self, + ctx: &Context, + workspace: &Workspace, + ) -> &wgpu::BindGroup { self.interval4.get_or_init(|| { ctx.gpu .device @@ -1615,30 +1625,33 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.tile16.sorted.bind_active(), + resource: workspace.tile16.sorted.bind_active(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.tile16.zmin.bind_active(), + resource: workspace.tile16.zmin.bind_active(), }, wgpu::BindGroupEntry { binding: 2, - resource: buffers.tile4.tiles.bind_active(), + resource: workspace.tile4.tiles.bind_active(), }, wgpu::BindGroupEntry { binding: 3, - resource: buffers.tile4.zmin.bind_active(), + resource: workspace.tile4.zmin.bind_active(), }, wgpu::BindGroupEntry { binding: 4, - resource: buffers.z_hist_buf.slice(256..320).into(), + resource: workspace + .z_hist_buf + .slice(256..320) + .into(), }, ], }) }) } - fn voxel(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn voxel(&self, ctx: &Context, workspace: &Workspace) -> &wgpu::BindGroup { self.voxel.get_or_init(|| { ctx.gpu .device @@ -1648,22 +1661,26 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.tile4.sorted.bind_active(), + resource: workspace.tile4.sorted.bind_active(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.tile4.zmin.bind_active(), + resource: workspace.tile4.zmin.bind_active(), }, wgpu::BindGroupEntry { binding: 2, - resource: buffers.voxels.bind_active(), + resource: workspace.voxels.bind_active(), }, ], }) }) } - fn normals(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup { + fn normals( + &self, + ctx: &Context, + workspace: &Workspace, + ) -> &wgpu::BindGroup { self.normals.get_or_init(|| { ctx.gpu .device @@ -1673,11 +1690,11 @@ impl BindGroups { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffers.voxels.bind_active(), + resource: workspace.voxels.bind_active(), }, wgpu::BindGroupEntry { binding: 1, - resource: buffers.geom.bind_active(), + resource: workspace.geom.bind_active(), }, ], }) @@ -1685,7 +1702,7 @@ impl BindGroups { } } -impl Buffers { +impl Workspace { /// Returns the current image size pub fn image_size(&self) -> VoxelSize { self.image_size @@ -1695,7 +1712,7 @@ impl Buffers { /// /// This is intended for subsequent shaders which want to use the /// [`GeometryPixel`] image data without copying to the CPU. It requires a - /// exclusive borrow of the `Buffers` object (and then extends that + /// exclusive borrow of the `Workspace` object (and then extends that /// lifetime) so that other callers can't simultaneously touch the buffer. pub fn output(&mut self) -> &FlexBuffer { &self.geom @@ -1832,9 +1849,9 @@ impl Buffers { &mut self, device: &wgpu::Device, image_size: VoxelSize, - ) -> Result<(), BuffersError> { + ) -> Result<(), BufferError> { let render_size = TileRenderSize::from(image_size); - let Buffers { + let Workspace { image_size: image_size_ref, config_buf: _, z_hist_buf: _, @@ -1856,37 +1873,37 @@ impl Buffers { *image_size_ref = image_size; tile_tapes .grow_to_fit(device, Self::tile_tapes_buf_size(render_size)) - .map_err(|err| BuffersError { + .map_err(|err| BufferError { buf: BufferName::TileTapes, err, })?; tile64 .grow_to_fit(device, render_size) - .map_err(|e| BuffersError { + .map_err(|e| BufferError { buf: BufferName::Tile64(e.buf), err: e.err, })?; tile16 .grow_to_fit(device, render_size) - .map_err(|e| BuffersError { + .map_err(|e| BufferError { buf: BufferName::Tile16(e.buf), err: e.err, })?; tile4 .grow_to_fit(device, render_size) - .map_err(|e| BuffersError { + .map_err(|e| BufferError { buf: BufferName::Tile4(e.buf), err: e.err, })?; voxels .grow_to_fit(device, Self::voxels_buf_size(render_size)) - .map_err(|err| BuffersError { + .map_err(|err| BufferError { buf: BufferName::Voxels, err, })?; geom.grow_to_fit(device, image_size) - .map_err(|err| BuffersError { + .map_err(|err| BufferError { buf: BufferName::Geom, err, })?; @@ -1896,7 +1913,7 @@ impl Buffers { /// Returns total allocated size (in bytes) pub fn capacity(&self) -> u64 { // Destructure to make sure we take all members into account - let Buffers { + let Workspace { image_size: _, config_buf, z_hist_buf, @@ -1923,7 +1940,7 @@ impl Buffers { /// Returns total active size (in bytes) pub fn size(&self) -> u64 { // Destructure to make sure we take all members into account - let Buffers { + let Workspace { image_size: _, config_buf, vars_buf, @@ -1992,13 +2009,13 @@ impl Context { } } - /// Builds a new [`Buffers`] object for use in rendering + /// Builds a new [`Workspace`] object for use in rendering /// /// The buffers are initialized with a dummy size and resized automatically /// when passed into any of the runner functions (e.g. [`run`](Self::run) or /// [`submit`](Self::submit)). - pub fn buffers(&self) -> Buffers { - Buffers::new(&self.gpu.device) + pub fn workspace(&self) -> Workspace { + Workspace::new(&self.gpu.device) } /// Renders the image, with a blocking wait to read pixel data from the GPU @@ -2008,11 +2025,11 @@ impl Context { pub fn run( &self, shape: &RenderShape, - buffers: &mut Buffers, + workspace: &mut Workspace, out: &mut ReadBuffer, settings: RenderConfig, ) -> Result { - self.run_with_vars(shape, &Default::default(), buffers, out, settings) + self.run_with_vars(shape, &Default::default(), workspace, out, settings) } /// Renders the image, with a blocking wait to read pixel data from the GPU @@ -2023,12 +2040,12 @@ impl Context { &self, shape: &RenderShape, vars: &ShapeVars, - buffers: &mut Buffers, + workspace: &mut Workspace, out: &mut ReadBuffer, settings: RenderConfig, ) -> Result { - self.submit_with_vars(shape, vars, buffers, &settings)?; - self.gpu.copy(&buffers.geom, out); + self.submit_with_vars(shape, vars, workspace, &settings)?; + self.gpu.copy(&workspace.geom, out); let image = self.gpu.map_image(out); Ok(image.image()) } @@ -2039,14 +2056,14 @@ impl Context { pub async fn run_async( &self, shape: &RenderShape, - buffers: &mut Buffers, + workspace: &mut Workspace, out: &mut ReadBuffer, settings: RenderConfig, ) -> Result { self.run_with_vars_async( shape, &Default::default(), - buffers, + workspace, out, settings, ) @@ -2060,12 +2077,12 @@ impl Context { &self, shape: &RenderShape, vars: &ShapeVars, - buffers: &mut Buffers, + workspace: &mut Workspace, out: &mut ReadBuffer, settings: RenderConfig, ) -> Result { - self.submit_with_vars(shape, vars, buffers, &settings)?; - self.gpu.copy(&buffers.geom, out); + self.submit_with_vars(shape, vars, workspace, &settings)?; + self.gpu.copy(&workspace.geom, out); let image = self.gpu.map_image_async(out).await; Ok(image.image()) } @@ -2073,14 +2090,14 @@ impl Context { /// Submits a single image to be rendered on the GPU /// /// The resulting image (as a buffer of [`GeometryPixel`] data) is available - /// on the GPU in [`buffers.output()`](Buffers::output). + /// on the GPU in [`workspace.output()`](Workspace::output). pub fn submit( &self, shape: &RenderShape, - buffers: &mut Buffers, + workspace: &mut Workspace, settings: &RenderConfig, ) -> Result<(), SubmitError> { - self.submit_with_vars(shape, &Default::default(), buffers, settings) + self.submit_with_vars(shape, &Default::default(), workspace, settings) } /// Submits a single image to be rendered on the GPU, with extra variables @@ -2090,14 +2107,34 @@ impl Context { &self, shape: &RenderShape, vars: &ShapeVars, - buffers: &mut Buffers, + workspace: &mut Workspace, + settings: &RenderConfig, + ) -> Result<(), SubmitError> { + // Create a command encoder and dispatch the compute work + let mut encoder = self.gpu.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: None }, + ); + self.encode_with_vars(shape, vars, workspace, settings, &mut encoder)?; + self.gpu.queue.submit(Some(encoder.finish())); + Ok(()) + } + + /// Encodes a single image to be rendered on the GPU, with extra variables + /// + /// See [`submit`](Self::submit) for additional details. + pub fn encode_with_vars( + &self, + shape: &RenderShape, + vars: &ShapeVars, + workspace: &mut Workspace, settings: &RenderConfig, + encoder: &mut wgpu::CommandEncoder, ) -> Result<(), SubmitError> { - buffers.set_image_size(&self.gpu.device, settings.image_size)?; - let render_size = TileRenderSize::from(buffers.image_size); + workspace.set_image_size(&self.gpu.device, settings.image_size)?; + let render_size = TileRenderSize::from(workspace.image_size); let mat = - settings.world_to_model * buffers.image_size.screen_to_world(); + settings.world_to_model * workspace.image_size.screen_to_world(); // Divide by 2 to go from `u32` -> `TapeWord` let start_offset = u32::try_from(shape.bytecode.len()).unwrap() / 2; @@ -2111,9 +2148,9 @@ impl Context { ], tape_data_capacity: TAPE_DATA_CAPACITY.try_into().unwrap(), image_size: [ - buffers.image_size.width(), - buffers.image_size.height(), - buffers.image_size.depth(), + workspace.image_size.width(), + workspace.image_size.height(), + workspace.image_size.depth(), ], tape_data_offset: start_offset, root_tape_len: start_offset, @@ -2126,7 +2163,7 @@ impl Context { .gpu .queue .write_buffer_with( - &buffers.config_buf, + &workspace.config_buf, 0, ((config_len + shape.bytecode.as_bytes().len()) as u64) .try_into() @@ -2144,19 +2181,14 @@ impl Context { // Copy vars (if present), then reset relevant bind groups if the buffer // size has changed. if matches!( - shape.copy_vars(&self.gpu, vars, &mut buffers.vars_buf)?, + shape.copy_vars(&self.gpu, vars, &mut workspace.vars_buf)?, CopyVarsChanged::BufferChanged ) { - buffers.bind_groups.common = Default::default(); + workspace.bind_groups.common = Default::default(); } - // Create a command encoder and dispatch the compute work - let mut encoder = self.gpu.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { label: None }, - ); - // Initial buffer reset pass - self.reset_ctx.run(&mut encoder, buffers); + self.reset_ctx.run(encoder, workspace); let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { @@ -2165,53 +2197,51 @@ impl Context { }); // Build the common config buffer - let common_bind_group = buffers.bind_groups.common(self, buffers); + let common_bind_group = workspace.bind_groups.common(self, workspace); compute_pass.set_bind_group(0, common_bind_group, &[]); // Populate root tiles (64x64x64, densely packed) self.root_ctx.run( self, - buffers, + workspace, shape.bytecode.reg_count(), render_size, &mut compute_pass, ); // Repack root tiles into strata self.repack_ctx - .run(self, buffers, render_size, &mut compute_pass); + .run(self, workspace, render_size, &mut compute_pass); // Evaluate tiles in reverse-Z order by strata (64 voxels deep) let strata_count = u64::from(render_size.depth()).div_ceil(64); for strata in 0..strata_count { self.interval_ctx.run( self, - buffers, + workspace, strata, shape.bytecode.reg_count(), &mut compute_pass, ); self.voxel_ctx.run( self, - buffers, + workspace, shape.bytecode.reg_count(), &mut compute_pass, ); // Merge filled tiles from large -> small, populating the heightmap - self.merge_ctx.run(self, buffers, &mut compute_pass); + self.merge_ctx.run(self, workspace, &mut compute_pass); self.normals_ctx.run( self, - buffers, + workspace, shape.bytecode.reg_count(), &mut compute_pass, ); - self.clear_ctx.run(self, buffers, &mut compute_pass); + self.clear_ctx.run(self, workspace, &mut compute_pass); } - drop(compute_pass); // Submit the commands and wait for the GPU to complete - self.gpu.queue.submit(Some(encoder.finish())); Ok(()) } } @@ -2277,10 +2307,10 @@ impl ClearContext { fn run( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, compute_pass: &mut wgpu::ComputePass, ) { - let bind_group = buffers.bind_groups.clear(ctx, buffers); + let bind_group = workspace.bind_groups.clear(ctx, workspace); compute_pass.set_pipeline(&self.pipeline); compute_pass.set_bind_group(1, bind_group, &[]); compute_pass.dispatch_workgroups(1, 1, 1); @@ -2348,11 +2378,11 @@ impl MergeContext { fn run( &self, ctx: &Context, - buffers: &Buffers, + workspace: &Workspace, compute_pass: &mut wgpu::ComputePass, ) { - let render_size = buffers.render_size(); - let bind_group = buffers.bind_groups.merge(ctx, buffers); + let render_size = workspace.render_size(); + let bind_group = workspace.bind_groups.merge(ctx, workspace); compute_pass.set_pipeline(&self.pipeline); compute_pass.set_bind_group(1, bind_group, &[]); compute_pass.dispatch_workgroups( @@ -2366,32 +2396,32 @@ impl MergeContext { struct ResetContext; impl ResetContext { - fn run(&self, encoder: &mut wgpu::CommandEncoder, buffers: &Buffers) { + fn run(&self, encoder: &mut wgpu::CommandEncoder, workspace: &Workspace) { // Clear only the `count` member of the tile64 `tiles_out` buffer - encoder.clear_buffer(buffers.tile64.tiles.data(), 12, Some(4)); + encoder.clear_buffer(workspace.tile64.tiles.data(), 12, Some(4)); // Per-strata counters may now be at a different location in memory if // we're using the buffers for multiple renders of different sizes! To // be safe, we'll clear them here, rather than in a render pass. - let strata_size_bytes = buffers.strata_size_bytes(); - for s in 0..buffers.render_size().nz() { + let strata_size_bytes = workspace.strata_size_bytes(); + for s in 0..workspace.render_size().nz() { encoder.clear_buffer( - buffers.tile64.strata.data(), + workspace.tile64.strata.data(), u64::from(s) * u64::try_from(strata_size_bytes).unwrap(), Some(16), ); } // Clear all of the heightmaps and output maps - buffers.tile64.zmin.clear(encoder); - buffers.tile64.zmax.clear(encoder); - buffers.tile16.zmin.clear(encoder); - buffers.tile4.zmin.clear(encoder); - buffers.voxels.clear(encoder); - buffers.geom.clear(encoder); + workspace.tile64.zmin.clear(encoder); + workspace.tile64.zmax.clear(encoder); + workspace.tile16.zmin.clear(encoder); + workspace.tile4.zmin.clear(encoder); + workspace.voxels.clear(encoder); + workspace.geom.clear(encoder); // Clear the whole tile tape map (TODO is this needed?) - buffers.tile_tapes.clear(encoder); + workspace.tile_tapes.clear(encoder); // tiles / sorted counters and z_hist are reset in clear shader } @@ -2479,9 +2509,9 @@ mod test { let voxel_ctx = Context::new(&gpu); let effects_ctx = effects::Context::new(&gpu); - let mut buf = voxel_ctx.buffers(); - let mut merge_buf = effects_ctx.merge_buffers(); - let mut shade_buf = effects_ctx.shade_buffers(); + let mut buf = voxel_ctx.workspace(); + let mut merge_buf = effects_ctx.merge_workspace(); + let mut shade_buf = effects_ctx.shade_workspace(); // Render and accumulate each shape for (shape, _) in shapes { @@ -2512,7 +2542,7 @@ mod test { let mut color_workspace = effects_ctx.color_workspace(); // Compute SSAO buffer - let mut ssao_buf = effects_ctx.ssao_buffers(); + let mut ssao_buf = effects_ctx.ssao_workspace(); effects_ctx.submit_ssao(&merge_buf, &mut ssao_buf).unwrap(); // Compute per-pixel colors diff --git a/fidget/tests/voxel_render.rs b/fidget/tests/voxel_render.rs index f825b0a6..41e2aa41 100644 --- a/fidget/tests/voxel_render.rs +++ b/fidget/tests/voxel_render.rs @@ -111,7 +111,7 @@ mod wgpu { let size = 32; let image_size = RenderSize::from(size); - let mut buf = ctx.buffers(); + let mut buf = ctx.workspace(); let mut out = gpu.read_buffer_for(buf.output()); for scale in [1.0, 0.5] { for r in [0.5, 0.75] { @@ -163,7 +163,7 @@ mod wgpu { let size = 32; let image_size = RenderSize::from(size); - let mut buf = ctx.buffers(); + let mut buf = ctx.workspace(); let mut out = gpu.read_buffer_for(buf.output()); for scale in [1.0, 0.5] { for r in [0.5, 0.75] {