use crate::{
Gpu,
buf::{
ArrayBuffer, BufferItemCount, BufferSizeError, BufferType, ImageBuffer,
buffer_ro, buffer_ro_dyn, buffer_rw,
},
opcode_constants, tag,
};
use fidget_bytecode::{Bytecode, ReservedRegister};
use fidget_core::{
eval::Function,
render::{ImageSize, VoxelSize},
shape::{MissingVar, ShapeVars},
var::Var,
vm::VmShape,
};
use fidget_raster::voxel::{GeometryPixel, Image};
use std::{collections::BTreeMap, num::NonZeroU64};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
const COMMON_SHADER: &str = include_str!("shaders/common.wgsl");
const VOXEL_TILES_SHADER: &str = include_str!("shaders/voxel_tiles.wgsl");
const STACK_SHADER: &str = include_str!("shaders/stack.wgsl");
const DUMMY_STACK_SHADER: &str = include_str!("shaders/dummy_stack.wgsl");
const INTERVAL_TILES_SHADER: &str = include_str!("shaders/interval_tiles.wgsl");
const REPACK_SHADER: &str = include_str!("shaders/repack.wgsl");
const SORT_SHADER: &str = include_str!("shaders/sort.wgsl");
const INTERVAL_ROOT_SHADER: &str = include_str!("shaders/interval_root.wgsl");
const INTERVAL_OPS_SHADER: &str = include_str!("shaders/interval_ops.wgsl");
const CLEAR_SHADER: &str = include_str!("shaders/clear.wgsl");
const MERGE_SHADER: &str = include_str!("shaders/merge.wgsl");
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");
#[derive(Debug, thiserror::Error)]
#[error("failed to resize `{buf}` tile buffer")]
pub struct TileBuffersError {
pub buf: TileBufferName,
#[source]
pub err: BufferSizeError,
}
#[derive(Debug)]
#[expect(missing_docs)]
pub enum TileBufferName {
Tiles,
Sorted,
Zmin,
}
impl std::fmt::Display for TileBufferName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
TileBufferName::Tiles => "tiles",
TileBufferName::Sorted => "sorted",
TileBufferName::Zmin => "zmin",
};
s.fmt(f)
}
}
#[derive(Debug, thiserror::Error)]
#[error("failed to resize `{buf}` root tile buffer")]
pub struct RootTileBuffersError {
pub buf: RootTileBufferName,
#[source]
pub err: BufferSizeError,
}
#[derive(Debug)]
#[expect(missing_docs)]
pub enum RootTileBufferName {
Tiles,
Strata,
Zmin,
Zmax,
}
impl std::fmt::Display for RootTileBufferName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
RootTileBufferName::Tiles => "tiles",
RootTileBufferName::Strata => "strata",
RootTileBufferName::Zmin => "zmin",
RootTileBufferName::Zmax => "zmax",
};
s.fmt(f)
}
}
#[derive(Debug)]
#[expect(missing_docs)]
pub enum BufferName {
Tile64(RootTileBufferName),
Tile16(TileBufferName),
Tile4(TileBufferName),
TileTapes,
Voxels,
Heightmap,
Geom,
Image,
}
impl std::fmt::Display for BufferName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BufferName::Tile64(buf) => write!(f, "`{buf}` tile64"),
BufferName::Tile16(buf) => write!(f, "`{buf}` tile16"),
BufferName::Tile4(buf) => write!(f, "`{buf}` tile4"),
BufferName::TileTapes => write!(f, "`tile tapes`"),
BufferName::Voxels => write!(f, "`voxels`"),
BufferName::Heightmap => write!(f, "`heightmap`"),
BufferName::Geom => write!(f, "`geom`"),
BufferName::Image => write!(f, "`image`"),
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("failed to build {buf} buffer when requesting size {requested:?}")]
pub struct BuffersError {
pub requested: VoxelSize,
pub buf: BufferName,
pub err: BufferSizeError,
}
#[derive(Copy, Clone)]
pub struct RenderConfig {
pub world_to_model: nalgebra::Matrix4<f32>,
}
impl Default for RenderConfig {
fn default() -> Self {
Self {
world_to_model: nalgebra::Matrix4::identity(),
}
}
}
#[derive(Debug, IntoBytes, Immutable, FromBytes, KnownLayout)]
#[repr(C)]
struct Config {
mat: [f32; 16],
axes: [u32; 3],
tape_data_offset: u32,
render_size: [u32; 3],
tape_data_capacity: u32,
image_size: [u32; 3],
root_tape_len: u32,
}
#[derive(Copy, Clone, Debug)]
struct TileRenderSize(VoxelSize);
impl From<VoxelSize> for TileRenderSize {
fn from(image_size: VoxelSize) -> Self {
let nx = image_size.width().div_ceil(64);
let ny = image_size.height().div_ceil(64);
let nz = image_size.depth().div_ceil(64);
Self(VoxelSize::new(nx, ny, nz))
}
}
impl TileRenderSize {
fn nx(&self) -> u32 {
self.0.width()
}
fn ny(&self) -> u32 {
self.0.height()
}
fn nz(&self) -> u32 {
self.0.depth()
}
fn width(&self) -> u32 {
self.0.width() * 64
}
fn height(&self) -> u32 {
self.0.height() * 64
}
fn depth(&self) -> u32 {
self.0.depth() * 64
}
fn pixels(&self) -> usize {
self.width() as usize * self.height() as usize
}
}
const TAPE_DATA_CAPACITY: usize = 8 * 1024 * 1024;
#[repr(C)]
struct TapeWord {
op: u32,
imm: u32,
}
fn interval_root_shader(reg_count: u8) -> String {
let mut shader_code = opcode_constants();
shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
shader_code += INTERVAL_ROOT_SHADER;
shader_code += INTERVAL_OPS_SHADER;
shader_code += COMMON_SHADER;
shader_code += crate::COMMON_SHADER;
shader_code += TAPE_INTERPRETER;
shader_code += STACK_SHADER;
shader_code += TAPE_SIMPLIFY;
shader_code
}
fn repack_shader() -> String {
let mut shader_code = String::new();
shader_code += REPACK_SHADER;
shader_code += COMMON_SHADER;
shader_code += crate::COMMON_SHADER;
shader_code
}
fn sort_shader() -> String {
let mut shader_code = String::new();
shader_code += SORT_SHADER;
shader_code += COMMON_SHADER;
shader_code += crate::COMMON_SHADER;
shader_code
}
fn interval_tiles_shader(reg_count: u8) -> String {
let mut shader_code = opcode_constants();
shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
shader_code += INTERVAL_TILES_SHADER;
shader_code += INTERVAL_OPS_SHADER;
shader_code += COMMON_SHADER;
shader_code += crate::COMMON_SHADER;
shader_code += TAPE_INTERPRETER;
shader_code += STACK_SHADER;
shader_code += TAPE_SIMPLIFY;
shader_code
}
fn voxel_tiles_shader(reg_count: u8) -> String {
let mut shader_code = opcode_constants();
shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
shader_code += VOXEL_TILES_SHADER;
shader_code += COMMON_SHADER;
shader_code += crate::COMMON_SHADER;
shader_code += TAPE_INTERPRETER;
shader_code += DUMMY_STACK_SHADER;
shader_code
}
fn normals_shader(reg_count: u8) -> String {
let mut shader_code = opcode_constants();
shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
shader_code += NORMALS_SHADER;
shader_code += COMMON_SHADER;
shader_code += crate::COMMON_SHADER;
shader_code += TAPE_INTERPRETER;
shader_code += DUMMY_STACK_SHADER;
shader_code
}
fn merge_shader() -> String {
MERGE_SHADER.to_owned() + COMMON_SHADER + crate::COMMON_SHADER
}
fn clear_shader() -> String {
CLEAR_SHADER.to_owned() + COMMON_SHADER + crate::COMMON_SHADER
}
struct RegPipeline(BTreeMap<u8, wgpu::ComputePipeline>);
impl RegPipeline {
fn build<F: Fn(u8) -> wgpu::ComputePipeline>(builder: F) -> Self {
let mut out = BTreeMap::new();
for reg_count in [8, 16, 32, 64, 128, 192, 255] {
out.insert(reg_count, builder(reg_count));
}
Self(out)
}
fn get(&self, reg_count: u8) -> &wgpu::ComputePipeline {
let (r, v) = self
.0
.range(reg_count..)
.next()
.expect("bytecode tape cannot use more than 255 registers");
assert!(*r >= reg_count);
v
}
}
struct RootContext {
root_pipeline: RegPipeline,
bind_group_layout: wgpu::BindGroupLayout,
}
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();
((nx * ny + 4) * std::mem::size_of::<u32>()).next_multiple_of(256)
}
impl RootContext {
fn new(
device: &wgpu::Device,
common_bind_group_layout: &wgpu::BindGroupLayout,
vars_bind_group_layout: &wgpu::BindGroupLayout,
) -> Self {
let bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[
buffer_rw(0), buffer_rw(1), ],
});
let root_pipeline = RegPipeline::build(|reg_count| {
let shader_code = interval_root_shader(reg_count);
let pipeline_layout = device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[
Some(common_bind_group_layout),
Some(vars_bind_group_layout),
Some(&bind_group_layout),
],
immediate_size: 0u32,
},
);
let shader_module =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
});
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(&format!("interval root ({reg_count})")),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("interval_root_main"),
compilation_options: Default::default(),
cache: None,
})
});
Self {
bind_group_layout,
root_pipeline,
}
}
fn run(
&self,
ctx: &Context,
buffers: &Buffers,
reg_count: u8,
render_size: TileRenderSize,
compute_pass: &mut wgpu::ComputePass,
) {
let bind_group = buffers.bind_groups.root(ctx, buffers);
compute_pass.set_pipeline(self.root_pipeline.get(reg_count));
compute_pass.set_bind_group(2, bind_group, &[]);
let nx = render_size.nx().div_ceil(4);
let ny = render_size.ny().div_ceil(4);
let nz = render_size.nz().div_ceil(4);
compute_pass.dispatch_workgroups(nx, ny, nz);
}
}
struct RepackContext {
repack_pipeline: wgpu::ComputePipeline,
bind_group_layout: wgpu::BindGroupLayout,
}
impl RepackContext {
fn new(
device: &wgpu::Device,
common_bind_group_layout: &wgpu::BindGroupLayout,
vars_bind_group_layout: &wgpu::BindGroupLayout,
) -> Self {
let bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[
buffer_ro(0), buffer_ro(1), buffer_rw(2), ],
});
let repack_pipeline = {
let shader_code = repack_shader();
let pipeline_layout = device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[
Some(common_bind_group_layout),
Some(vars_bind_group_layout),
Some(&bind_group_layout),
],
immediate_size: 0u32,
},
);
let shader_module =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
});
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("repack"),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("repack_main"),
compilation_options: Default::default(),
cache: None,
})
};
Self {
bind_group_layout,
repack_pipeline,
}
}
fn run(
&self,
ctx: &Context,
buffers: &Buffers,
render_size: TileRenderSize,
compute_pass: &mut wgpu::ComputePass,
) {
let bind_group = buffers.bind_groups.repack(ctx, buffers);
compute_pass.set_pipeline(&self.repack_pipeline);
compute_pass.set_bind_group(2, bind_group, &[]);
let nx = render_size.nx().div_ceil(64);
let ny = render_size.ny();
let nz = render_size.nz();
compute_pass.dispatch_workgroups(nx, ny, nz);
}
}
struct IntervalContext {
interval64_pipeline: RegPipeline,
sort16_pipeline: wgpu::ComputePipeline,
interval16_pipeline: RegPipeline,
sort4_pipeline: wgpu::ComputePipeline,
interval_bind_group_layout: wgpu::BindGroupLayout,
sort_bind_group_layout: wgpu::BindGroupLayout,
}
impl IntervalContext {
fn new(
device: &wgpu::Device,
common_bind_group_layout: &wgpu::BindGroupLayout,
vars_bind_group_layout: &wgpu::BindGroupLayout,
) -> Self {
let interval_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[
buffer_ro_dyn(0), buffer_ro(1), buffer_rw(2), buffer_rw(3), buffer_rw(4), ],
});
let interval_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("interval pipeline layout"),
bind_group_layouts: &[
Some(common_bind_group_layout),
Some(vars_bind_group_layout),
Some(&interval_bind_group_layout),
],
immediate_size: 0u32,
});
let interval64_pipeline = RegPipeline::build(|reg_count| {
let shader_code = interval_tiles_shader(reg_count);
let shader_module = unsafe {
device.create_shader_module_trusted(
wgpu::ShaderModuleDescriptor {
label: Some(&format!(
"interval64 tiles shader ({reg_count})"
)),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
},
wgpu::ShaderRuntimeChecks {
bounds_checks: false,
force_loop_bounding: false,
ray_query_initialization_tracking: false,
task_shader_dispatch_tracking: false,
mesh_shader_primitive_indices_clamp: false,
},
)
};
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(&format!("interval64 ({reg_count})")),
layout: Some(&interval_pipeline_layout),
module: &shader_module,
entry_point: Some("interval_tile_main"),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &[("TILE_SIZE", 64.0), ("SUBTILE_SIZE", 16.0)],
..Default::default()
},
cache: None,
})
});
let interval16_pipeline = RegPipeline::build(|reg_count| {
let shader_code = interval_tiles_shader(reg_count);
let shader_module = unsafe {
device.create_shader_module_trusted(
wgpu::ShaderModuleDescriptor {
label: Some(&format!(
"interval16 tiles shader ({reg_count})"
)),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
},
wgpu::ShaderRuntimeChecks {
bounds_checks: false,
force_loop_bounding: false,
ray_query_initialization_tracking: false,
task_shader_dispatch_tracking: false,
mesh_shader_primitive_indices_clamp: false,
},
)
};
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(&format!("interval16 ({reg_count})")),
layout: Some(&interval_pipeline_layout),
module: &shader_module,
entry_point: Some("interval_tile_main"),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &[("TILE_SIZE", 16.0), ("SUBTILE_SIZE", 4.0)],
..Default::default()
},
cache: None,
})
});
let sort_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("sort bind group layout"),
entries: &[
buffer_ro(0), buffer_rw(1), buffer_rw(2), ],
});
let sort_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("sort pipeline layout"),
bind_group_layouts: &[
Some(common_bind_group_layout),
Some(vars_bind_group_layout),
Some(&sort_bind_group_layout),
],
immediate_size: 0u32,
});
let shader_code = sort_shader();
let shader_module = unsafe {
device.create_shader_module_trusted(
wgpu::ShaderModuleDescriptor {
label: Some("sort shader module"),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
},
wgpu::ShaderRuntimeChecks {
bounds_checks: false,
force_loop_bounding: false,
ray_query_initialization_tracking: false,
task_shader_dispatch_tracking: false,
mesh_shader_primitive_indices_clamp: false,
},
)
};
let sort16_pipeline =
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("sort16"),
layout: Some(&sort_pipeline_layout),
module: &shader_module,
entry_point: Some("sort_main"),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &[("SUBTILE_SIZE", 16.0)],
..Default::default()
},
cache: None,
});
let sort4_pipeline =
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("sort4"),
layout: Some(&sort_pipeline_layout),
module: &shader_module,
entry_point: Some("sort_main"),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &[("SUBTILE_SIZE", 4.0)],
..Default::default()
},
cache: None,
});
Self {
interval_bind_group_layout,
sort_bind_group_layout,
interval64_pipeline,
sort16_pipeline,
interval16_pipeline,
sort4_pipeline,
}
}
fn run(
&self,
ctx: &Context,
buffers: &Buffers,
strata: u64,
reg_count: u8,
compute_pass: &mut wgpu::ComputePass,
) {
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));
compute_pass.set_bind_group(
2,
bind_group16,
&[u32::try_from(offset_bytes).unwrap()],
);
compute_pass.dispatch_workgroups_indirect(
buffers.tile64.strata.data(),
offset_bytes,
);
let bind_group_sort16 = buffers.bind_groups.sort16(ctx, buffers);
compute_pass.set_pipeline(&self.sort16_pipeline);
compute_pass.set_bind_group(2, bind_group_sort16, &[]);
compute_pass
.dispatch_workgroups_indirect(buffers.tile16.tiles.data(), 0);
let bind_group4 = buffers.bind_groups.interval4(ctx, buffers);
compute_pass.set_pipeline(self.interval16_pipeline.get(reg_count));
compute_pass.set_bind_group(2, bind_group4, &[0]);
compute_pass
.dispatch_workgroups_indirect(buffers.tile16.sorted.data(), 0);
let bind_group_sort4 = buffers.bind_groups.sort4(ctx, buffers);
compute_pass.set_pipeline(&self.sort4_pipeline);
compute_pass.set_bind_group(2, bind_group_sort4, &[]);
compute_pass
.dispatch_workgroups_indirect(buffers.tile4.tiles.data(), 0);
}
}
struct VoxelContext {
bind_group_layout: wgpu::BindGroupLayout,
voxel_pipeline: RegPipeline,
}
impl VoxelContext {
fn new(
device: &wgpu::Device,
common_bind_group_layout: &wgpu::BindGroupLayout,
vars_bind_group_layout: &wgpu::BindGroupLayout,
) -> Self {
let bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("voxel bind group layout"),
entries: &[
buffer_ro(0), buffer_ro(1), buffer_rw(2), ],
});
let voxel_pipeline = RegPipeline::build(|reg_count| {
let shader_code = voxel_tiles_shader(reg_count);
let pipeline_layout = device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: Some("voxel pipeline layout"),
bind_group_layouts: &[
Some(common_bind_group_layout),
Some(vars_bind_group_layout),
Some(&bind_group_layout),
],
immediate_size: 0u32,
},
);
let shader_module = unsafe {
device.create_shader_module_trusted(
wgpu::ShaderModuleDescriptor {
label: Some("voxel shader module"),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
},
wgpu::ShaderRuntimeChecks {
bounds_checks: false,
force_loop_bounding: false,
ray_query_initialization_tracking: false,
task_shader_dispatch_tracking: false,
mesh_shader_primitive_indices_clamp: false,
},
)
};
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(&format!("voxels ({reg_count})")),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("voxel_ray_main"),
compilation_options: Default::default(),
cache: None,
})
});
Self {
bind_group_layout,
voxel_pipeline,
}
}
fn run(
&self,
ctx: &Context,
buffers: &Buffers,
reg_count: u8,
compute_pass: &mut wgpu::ComputePass,
) {
let bind_group = buffers.bind_groups.voxel(ctx, buffers);
compute_pass.set_pipeline(self.voxel_pipeline.get(reg_count));
compute_pass.set_bind_group(2, bind_group, &[]);
compute_pass
.dispatch_workgroups_indirect(buffers.tile4.sorted.data(), 0);
}
}
struct NormalsContext {
bind_group_layout: wgpu::BindGroupLayout,
normals_pipeline: RegPipeline,
}
impl NormalsContext {
fn new(
device: &wgpu::Device,
common_bind_group_layout: &wgpu::BindGroupLayout,
vars_bind_group_layout: &wgpu::BindGroupLayout,
) -> Self {
let bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("normals bind group layout"),
entries: &[
buffer_ro(0), buffer_rw(1), ],
});
let normals_pipeline = RegPipeline::build(|reg_count| {
let shader_code = normals_shader(reg_count);
let pipeline_layout = device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: Some("normals pipeline"),
bind_group_layouts: &[
Some(common_bind_group_layout),
Some(vars_bind_group_layout),
Some(&bind_group_layout),
],
immediate_size: 0u32,
},
);
let shader_module =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("normals shader module"),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
});
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(&format!("normals ({reg_count})")),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("normals_main"),
compilation_options: Default::default(),
cache: None,
})
});
Self {
bind_group_layout,
normals_pipeline,
}
}
fn run(
&self,
ctx: &Context,
buffers: &Buffers,
reg_count: u8,
compute_pass: &mut wgpu::ComputePass,
) {
let bind_group = buffers.bind_groups.normals(ctx, buffers);
compute_pass.set_pipeline(self.normals_pipeline.get(reg_count));
compute_pass.set_bind_group(2, bind_group, &[]);
compute_pass.dispatch_workgroups(
buffers.image_size.width().div_ceil(8),
buffers.image_size.height().div_ceil(8),
1,
);
}
}
pub struct Context {
gpu: Gpu,
has_timestamps: bool,
common_bind_group_layout: wgpu::BindGroupLayout,
vars_bind_group_layout: wgpu::BindGroupLayout,
root_ctx: RootContext,
repack_ctx: RepackContext,
interval_ctx: IntervalContext,
voxel_ctx: VoxelContext,
normals_ctx: NormalsContext,
merge_ctx: MergeContext,
reset_ctx: ResetContext,
clear_ctx: ClearContext,
}
tag!(TilesBufferTag, u32, STORAGE | INDIRECT);
tag!(SortedBufferTag, u32, STORAGE | INDIRECT);
tag!(ZminBufferTag, u32, STORAGE | COPY_DST);
struct TileBuffers<const N: u64> {
tiles: ArrayBuffer<TilesBufferTag>,
sorted: ArrayBuffer<SortedBufferTag>,
zmin: ImageBuffer<ZminBufferTag>,
}
impl<const N: u64> TileBuffers<N> {
fn new(
device: &wgpu::Device,
render_size: TileRenderSize,
) -> Result<Self, TileBuffersError> {
let tile_buf_size = Self::tile_buf_size(render_size);
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),
)
.map_err(|err| TileBuffersError {
buf: TileBufferName::Zmin,
err,
})?;
Ok(Self {
tiles,
sorted,
zmin,
})
}
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;
4 + nx * ny * nz
}
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(
&mut self,
device: &wgpu::Device,
render_size: TileRenderSize,
) -> Result<(), TileBuffersError> {
let TileBuffers {
tiles,
sorted,
zmin,
} = self;
let tile_buf_size = Self::tile_buf_size(render_size);
tiles.grow_to_fit(device, tile_buf_size).map_err(|err| {
TileBuffersError {
buf: TileBufferName::Tiles,
err,
}
})?;
sorted.grow_to_fit(device, tile_buf_size).map_err(|err| {
TileBuffersError {
buf: TileBufferName::Sorted,
err,
}
})?;
zmin.grow_to_fit(device, Self::zmin_buf_size(render_size))
.map_err(|err| TileBuffersError {
buf: TileBufferName::Zmin,
err,
})?;
Ok(())
}
pub fn size(&self) -> u64 {
let TileBuffers {
tiles,
sorted,
zmin,
} = self;
tiles.size_bytes() + sorted.size_bytes() + zmin.size_bytes()
}
pub fn capacity(&self) -> u64 {
let TileBuffers {
tiles,
sorted,
zmin,
} = self;
tiles.capacity() + sorted.capacity() + zmin.capacity()
}
}
tag!(RootTilesBufferTag, u32, STORAGE | COPY_DST);
tag!(RootStrataBufferTag, u8, STORAGE | INDIRECT | COPY_DST);
tag!(RootZminBufferTag, u32, STORAGE | COPY_DST);
tag!(RootZmaxBufferTag, u32, STORAGE | COPY_DST);
struct RootTileBuffers {
tiles: ArrayBuffer<RootTilesBufferTag>,
strata: ArrayBuffer<RootStrataBufferTag>,
zmin: ImageBuffer<RootZminBufferTag>,
zmax: ImageBuffer<RootZmaxBufferTag>,
}
impl RootTileBuffers {
fn new(
device: &wgpu::Device,
render_size: TileRenderSize,
) -> Result<Self, RootTileBuffersError> {
const N: usize = 64;
let tiles = ArrayBuffer::new(
device,
format!("tiles_out{N}"),
Self::tiles_buf_size(render_size),
)
.map_err(|err| RootTileBuffersError {
buf: RootTileBufferName::Tiles,
err,
})?;
let strata = ArrayBuffer::new(
device,
format!("strata_tile{N}"),
Self::strata_buf_size(render_size),
)
.map_err(|err| RootTileBuffersError {
buf: RootTileBufferName::Strata,
err,
})?;
let z_buf_size = Self::z_buf_size(render_size);
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,
zmin,
zmax,
})
}
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();
4 + nx * ny * 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) -> ImageSize {
ImageSize::new(render_size.nx(), render_size.ny())
}
fn grow_to_fit(
&mut self,
device: &wgpu::Device,
render_size: TileRenderSize,
) -> Result<(), RootTileBuffersError> {
let RootTileBuffers {
tiles,
strata,
zmin,
zmax,
} = self;
tiles
.grow_to_fit(device, Self::tiles_buf_size(render_size))
.map_err(|err| RootTileBuffersError {
buf: RootTileBufferName::Tiles,
err,
})?;
strata
.grow_to_fit(device, Self::strata_buf_size(render_size))
.map_err(|err| RootTileBuffersError {
buf: RootTileBufferName::Strata,
err,
})?;
let z_buf_size = Self::z_buf_size(render_size);
zmin.grow_to_fit(device, z_buf_size).map_err(|err| {
RootTileBuffersError {
buf: RootTileBufferName::Zmin,
err,
}
})?;
zmax.grow_to_fit(device, z_buf_size).map_err(|err| {
RootTileBuffersError {
buf: RootTileBufferName::Zmax,
err,
}
})?;
Ok(())
}
pub fn size(&self) -> u64 {
let RootTileBuffers {
tiles,
strata,
zmin,
zmax,
} = self;
tiles.size_bytes()
+ strata.size_bytes()
+ zmin.size_bytes()
+ zmax.size_bytes()
}
pub fn capacity(&self) -> u64 {
let RootTileBuffers {
tiles,
strata,
zmin,
zmax,
} = self;
tiles.capacity() + strata.capacity() + zmin.capacity() + zmax.capacity()
}
}
pub struct RenderShape {
shape: VmShape,
axes: [u32; 3],
bytecode: Bytecode,
vars: wgpu::Buffer,
vars_bind_group: std::cell::OnceCell<wgpu::BindGroup>,
}
#[derive(Debug, thiserror::Error)]
pub enum RenderShapeError {
#[error(
"shape bytecode is {0} tape words (8 bytes each), which exceeds \
buffer capacity of {TAPE_DATA_CAPACITY} tape words"
)]
TooLong(usize),
#[error(transparent)]
RegisterError(#[from] ReservedRegister),
}
impl RenderShape {
fn new(
shape: &VmShape,
device: &wgpu::Device,
) -> Result<Self, RenderShapeError> {
let bytecode = Bytecode::new(shape.inner().data())?;
if bytecode.len() / 2 > TAPE_DATA_CAPACITY {
return Err(RenderShapeError::TooLong(bytecode.len() / 2));
}
let vars = shape.inner().vars();
let axes = [Var::X, Var::Y, Var::Z]
.map(|a| vars.get(&a).map(|v| v as u32).unwrap_or(u32::MAX));
let vars = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("vars"),
size: u64::try_from(std::mem::size_of::<f32>() * vars.len())
.unwrap(),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Ok(Self {
shape: shape.clone(),
axes,
bytecode,
vars,
vars_bind_group: Default::default(),
})
}
fn vars_bind_group(&self, ctx: &Context) -> &wgpu::BindGroup {
self.vars_bind_group.get_or_init(|| {
ctx.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("vars bind group"),
layout: &ctx.vars_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: self.vars.as_entire_binding(),
}],
})
})
}
}
tag!(TileTapesBufferTag, u32, STORAGE | COPY_DST);
tag!(VoxelsBufferTag, u32, STORAGE | COPY_DST);
tag!(pub GeomBufferTag, GeometryPixel, STORAGE | COPY_SRC | COPY_DST,
"Tag for a on-GPU buffer storing [`GeometryPixel`] values");
pub struct Buffers {
image_size: VoxelSize,
config_buf: wgpu::Buffer,
z_hist_buf: wgpu::Buffer,
tile_tapes: ArrayBuffer<TileTapesBufferTag>,
tile64: RootTileBuffers,
tile16: TileBuffers<16>,
tile4: TileBuffers<4>,
voxels: ArrayBuffer<VoxelsBufferTag>,
geom: ImageBuffer<GeomBufferTag>,
timestamps: Option<wgpu::QuerySet>,
ts_buf: wgpu::Buffer,
bind_groups: BindGroups,
}
pub struct ImageReadBuffer {
image_size: VoxelSize,
buffer: ImageReadArrayBuffer,
}
impl ImageReadBuffer {
fn new(
device: &wgpu::Device,
name: String,
image_size: VoxelSize,
) -> Result<Self, BufferSizeError> {
Ok(Self {
image_size,
buffer: ImageReadArrayBuffer::new(
device,
name,
Buffers::image_buf_size(image_size),
)?,
})
}
fn grow_to_fit(
&mut self,
device: &wgpu::Device,
image_size: VoxelSize,
) -> Result<(), BufferSizeError> {
self.image_size = image_size;
self.buffer
.grow_to_fit(device, Buffers::image_buf_size(image_size))
}
}
tag!(ImageReadTag, u8, COPY_DST | MAP_READ);
type ImageReadArrayBuffer = ArrayBuffer<ImageReadTag>;
#[derive(Default)]
struct BindGroups {
common: std::cell::OnceCell<wgpu::BindGroup>,
merge: std::cell::OnceCell<wgpu::BindGroup>,
root: std::cell::OnceCell<wgpu::BindGroup>,
repack: std::cell::OnceCell<wgpu::BindGroup>,
interval16: std::cell::OnceCell<wgpu::BindGroup>,
sort16: std::cell::OnceCell<wgpu::BindGroup>,
interval4: std::cell::OnceCell<wgpu::BindGroup>,
sort4: std::cell::OnceCell<wgpu::BindGroup>,
voxel: std::cell::OnceCell<wgpu::BindGroup>,
normals: std::cell::OnceCell<wgpu::BindGroup>,
clear: std::cell::OnceCell<wgpu::BindGroup>,
}
impl BindGroups {
fn common(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
self.common.get_or_init(|| {
ctx.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("common bind group"),
layout: &ctx.common_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buffers.config_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: buffers.tile_tapes.bind_active(),
},
],
})
})
}
fn clear(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
self.clear.get_or_init(|| {
ctx.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("clear bind group"),
layout: &ctx.clear_ctx.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buffers
.tile16
.tiles
.data()
.slice(0..16)
.into(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: buffers
.tile16
.sorted
.data()
.slice(0..16)
.into(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: buffers
.tile4
.tiles
.data()
.slice(0..16)
.into(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: buffers
.tile4
.sorted
.data()
.slice(0..16)
.into(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: buffers.z_hist_buf.as_entire_binding(),
},
],
})
})
}
fn merge(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
self.merge.get_or_init(|| {
ctx.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("merge bind group"),
layout: &ctx.merge_ctx.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buffers.tile64.zmin.bind_active(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: buffers.tile16.zmin.bind_active(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: buffers.tile4.zmin.bind_active(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: buffers.voxels.bind_active(),
},
],
})
})
}
fn root(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
self.root.get_or_init(|| {
ctx.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("interval root bind group"),
layout: &ctx.root_ctx.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buffers.tile64.tiles.bind_active(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: buffers.tile64.zmax.bind_active(),
},
],
})
})
}
fn repack(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
self.repack.get_or_init(|| {
ctx.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("repack bind group"),
layout: &ctx.repack_ctx.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buffers.tile64.tiles.bind_active(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: buffers.tile64.zmax.bind_active(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: buffers.tile64.strata.bind_active(),
},
],
})
})
}
fn interval16(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
let strata_bytes = u64::try_from(buffers.strata_size_bytes()).unwrap();
self.interval16.get_or_init(|| {
ctx.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("interval16 bind group"),
layout: &ctx.interval_ctx.interval_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buffers
.tile64
.strata
.data()
.slice(0..strata_bytes) .into(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: buffers.tile64.zmin.bind_active(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: buffers.tile16.tiles.bind_active(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: buffers.tile16.zmin.bind_active(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: buffers.z_hist_buf.slice(0..16).into(),
},
],
})
})
}
fn sort16(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
self.sort16.get_or_init(|| {
Self::sort_bind_group(
ctx,
&buffers.tile16,
buffers.z_hist_buf.slice(0..16).into(),
)
})
}
fn sort4(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
self.sort4.get_or_init(|| {
Self::sort_bind_group(
ctx,
&buffers.tile4,
buffers.z_hist_buf.slice(256..320).into(),
)
})
}
fn sort_bind_group<const N: u64>(
ctx: &Context,
tile_bufs: &TileBuffers<N>,
z_hist: wgpu::BindingResource,
) -> wgpu::BindGroup {
ctx.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("sort{N} bind group")),
layout: &ctx.interval_ctx.sort_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: tile_bufs.tiles.bind_active(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: z_hist,
},
wgpu::BindGroupEntry {
binding: 2,
resource: tile_bufs.sorted.bind_active(),
},
],
})
}
fn interval4(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
self.interval4.get_or_init(|| {
ctx.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("interval4 bind group"),
layout: &ctx.interval_ctx.interval_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buffers.tile16.sorted.bind_active(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: buffers.tile16.zmin.bind_active(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: buffers.tile4.tiles.bind_active(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: buffers.tile4.zmin.bind_active(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: buffers.z_hist_buf.slice(256..320).into(),
},
],
})
})
}
fn voxel(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
self.voxel.get_or_init(|| {
ctx.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("voxel bind group"),
layout: &ctx.voxel_ctx.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buffers.tile4.sorted.bind_active(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: buffers.tile4.zmin.bind_active(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: buffers.voxels.bind_active(),
},
],
})
})
}
fn normals(&self, ctx: &Context, buffers: &Buffers) -> &wgpu::BindGroup {
self.normals.get_or_init(|| {
ctx.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("normals bind group"),
layout: &ctx.normals_ctx.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buffers.voxels.bind_active(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: buffers.geom.bind_active(),
},
],
})
})
}
}
impl Buffers {
pub fn image_size(&self) -> VoxelSize {
self.image_size
}
pub fn image_storage_buffer(&mut self) -> &ImageBuffer<GeomBufferTag> {
&self.geom
}
fn new(
device: &wgpu::Device,
image_size: VoxelSize,
has_timestamps: bool,
) -> Result<Self, BuffersError> {
static_assertions::const_assert!(
(std::mem::size_of::<Config>()
+ TAPE_DATA_CAPACITY * std::mem::size_of::<TapeWord>())
as u64
<= BufferType::Storage.max_size()
);
ImageReadArrayBuffer::check_size(Self::image_buf_size(image_size))
.map_err(|err| BuffersError {
requested: image_size,
buf: BufferName::Image,
err,
})?;
let config_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("config"),
size: (std::mem::size_of::<Config>()
+ TAPE_DATA_CAPACITY * std::mem::size_of::<TapeWord>())
as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let render_size = TileRenderSize::from(image_size);
let voxels = ArrayBuffer::new(
device,
"voxels".to_string(),
Self::voxels_buf_size(render_size),
)
.map_err(|err| BuffersError {
requested: image_size,
buf: BufferName::Voxels,
err,
})?;
let tile_tapes = ArrayBuffer::new(
device,
"tile tape".to_string(),
Self::tile_tapes_buf_size(render_size),
)
.map_err(|err| BuffersError {
requested: image_size,
buf: BufferName::TileTapes,
err,
})?;
let geom = ImageBuffer::new(
device,
"geom".to_string(),
Self::geom_buf_size(image_size),
)
.map_err(|err| BuffersError {
requested: image_size,
buf: BufferName::Geom,
err,
})?;
let ts_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ts"),
size: 2 * std::mem::size_of::<u64>() as u64,
usage: wgpu::BufferUsages::QUERY_RESOLVE
| wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let tile64 =
RootTileBuffers::new(device, render_size).map_err(|e| {
BuffersError {
requested: image_size,
buf: BufferName::Tile64(e.buf),
err: e.err,
}
})?;
let tile16 = TileBuffers::new(device, render_size).map_err(|e| {
BuffersError {
requested: image_size,
buf: BufferName::Tile16(e.buf),
err: e.err,
}
})?;
let tile4 = TileBuffers::new(device, render_size).map_err(|e| {
BuffersError {
requested: image_size,
buf: BufferName::Tile4(e.buf),
err: e.err,
}
})?;
let timestamps = if has_timestamps {
Some(device.create_query_set(&wgpu::QuerySetDescriptor {
label: Some("timestamp query set"),
ty: wgpu::QueryType::Timestamp,
count: 2,
}))
} else {
None
};
let z_hist_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("tiles_zhist"),
size: u64::try_from(
(4 * std::mem::size_of::<u32>()).next_multiple_of(256)
+ (16 * std::mem::size_of::<u32>()),
)
.unwrap(),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Ok(Self {
config_buf,
image_size,
tile_tapes,
tile64,
tile16,
tile4,
voxels,
geom,
timestamps,
z_hist_buf,
ts_buf,
bind_groups: Default::default(),
})
}
fn render_size(&self) -> TileRenderSize {
self.image_size.into()
}
fn strata_size_bytes(&self) -> usize {
strata_size_bytes(self.render_size())
}
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();
let xy_size = (64usize / 4).pow(3) + (64usize / 16).pow(3);
nx.checked_mul(ny)
.unwrap()
.checked_mul(nz.checked_add(xy_size).unwrap())
.unwrap()
}
fn voxels_buf_size(render_size: TileRenderSize) -> usize {
render_size.pixels()
}
fn geom_buf_size(image_size: VoxelSize) -> ImageSize {
ImageSize::new(image_size.width(), image_size.height())
}
fn image_buf_size(image_size: VoxelSize) -> usize {
Self::geom_buf_size(image_size)
.item_count()
.checked_mul(std::mem::size_of::<GeometryPixel>())
.unwrap()
.checked_add(16)
.unwrap()
}
fn set_image_size(
&mut self,
device: &wgpu::Device,
image_size: VoxelSize,
) -> Result<(), BuffersError> {
let render_size = TileRenderSize::from(image_size);
let Buffers {
image_size: image_size_ref,
config_buf: _,
z_hist_buf: _,
tile_tapes,
tile64,
tile16,
tile4,
voxels,
geom,
timestamps: _,
ts_buf: _,
bind_groups,
} = self;
if *image_size_ref != image_size {
*bind_groups = Default::default();
}
*image_size_ref = image_size;
tile_tapes
.grow_to_fit(device, Self::tile_tapes_buf_size(render_size))
.map_err(|err| BuffersError {
requested: image_size,
buf: BufferName::TileTapes,
err,
})?;
tile64
.grow_to_fit(device, render_size)
.map_err(|e| BuffersError {
requested: image_size,
buf: BufferName::Tile64(e.buf),
err: e.err,
})?;
tile16
.grow_to_fit(device, render_size)
.map_err(|e| BuffersError {
requested: image_size,
buf: BufferName::Tile16(e.buf),
err: e.err,
})?;
tile4
.grow_to_fit(device, render_size)
.map_err(|e| BuffersError {
requested: image_size,
buf: BufferName::Tile4(e.buf),
err: e.err,
})?;
voxels
.grow_to_fit(device, Self::voxels_buf_size(render_size))
.map_err(|err| BuffersError {
requested: image_size,
buf: BufferName::Voxels,
err,
})?;
geom.grow_to_fit(device, Self::geom_buf_size(image_size))
.map_err(|err| BuffersError {
requested: image_size,
buf: BufferName::Geom,
err,
})?;
ImageReadArrayBuffer::check_size(Self::image_buf_size(image_size))
.map_err(|err| BuffersError {
requested: image_size,
buf: BufferName::Image,
err,
})?;
Ok(())
}
pub fn capacity(&self) -> u64 {
let Buffers {
image_size: _,
config_buf,
z_hist_buf,
tile_tapes,
tile64,
tile16,
tile4,
voxels,
geom,
timestamps: _,
ts_buf,
bind_groups: _,
} = self;
config_buf.size()
+ z_hist_buf.size()
+ tile_tapes.capacity()
+ tile64.capacity()
+ tile16.capacity()
+ tile4.capacity()
+ voxels.capacity()
+ geom.capacity()
+ ts_buf.size()
}
pub fn size(&self) -> u64 {
let Buffers {
image_size: _,
config_buf,
z_hist_buf,
tile_tapes,
tile64,
tile16,
tile4,
voxels,
geom,
timestamps: _,
ts_buf,
bind_groups: _,
} = self;
config_buf.size()
+ z_hist_buf.size()
+ tile_tapes.size_bytes()
+ tile64.size()
+ tile16.size()
+ tile4.size()
+ voxels.size_bytes()
+ geom.size_bytes()
+ ts_buf.size()
}
}
impl Context {
pub fn new(gpu: &Gpu) -> Self {
let has_timestamps = gpu
.device
.features()
.contains(wgpu::Features::TIMESTAMP_QUERY);
if !has_timestamps {
log::warn!(
"WGPU device is missing `TIMESTAMP_QUERY`; \
timestamps are disabled"
);
}
let common_bind_group_layout = gpu.device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: Some("common bind group layout"),
entries: &[
buffer_rw(0), buffer_rw(1), ],
},
);
let vars_bind_group_layout = gpu.device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: Some("vars bind group layout"),
entries: &[
buffer_ro(0), ],
},
);
let root_ctx = RootContext::new(
&gpu.device,
&common_bind_group_layout,
&vars_bind_group_layout,
);
let repack_ctx = RepackContext::new(
&gpu.device,
&common_bind_group_layout,
&vars_bind_group_layout,
);
let interval_ctx = IntervalContext::new(
&gpu.device,
&common_bind_group_layout,
&vars_bind_group_layout,
);
let voxel_ctx = VoxelContext::new(
&gpu.device,
&common_bind_group_layout,
&vars_bind_group_layout,
);
let normals_ctx = NormalsContext::new(
&gpu.device,
&common_bind_group_layout,
&vars_bind_group_layout,
);
let merge_ctx = MergeContext::new(
&gpu.device,
&common_bind_group_layout,
&vars_bind_group_layout,
);
let reset_ctx = ResetContext::new();
let clear_ctx = ClearContext::new(
&gpu.device,
&common_bind_group_layout,
&vars_bind_group_layout,
);
Self {
gpu: gpu.clone(),
has_timestamps,
common_bind_group_layout,
vars_bind_group_layout,
root_ctx,
repack_ctx,
interval_ctx,
voxel_ctx,
normals_ctx,
merge_ctx,
reset_ctx,
clear_ctx,
}
}
pub fn buffers(
&self,
image_size: VoxelSize,
) -> Result<Buffers, BuffersError> {
Buffers::new(&self.gpu.device, image_size, self.has_timestamps)
}
pub fn image_buffer(&self, buffers: &Buffers) -> ImageReadBuffer {
ImageReadBuffer::new(
&self.gpu.device,
"image".to_owned(),
buffers.image_size,
)
.expect(
"buffers.image_size should always be \
a valid size for ImageReadBuffer::new",
)
}
pub fn shape(
&self,
shape: &VmShape,
) -> Result<RenderShape, RenderShapeError> {
RenderShape::new(shape, &self.gpu.device)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn run(
&self,
shape: &RenderShape,
buffers: &Buffers,
out: &mut ImageReadBuffer,
settings: RenderConfig,
) -> Result<Image, MissingVar> {
self.run_with_vars(shape, &Default::default(), buffers, out, settings)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn run_with_vars(
&self,
shape: &RenderShape,
vars: &ShapeVars<f32>,
buffers: &Buffers,
out: &mut ImageReadBuffer,
settings: RenderConfig,
) -> Result<Image, MissingVar> {
self.submit_with_vars(shape, vars, buffers, Some(out), &settings)?;
let image = self.map_image(out);
Ok(image.image())
}
#[cfg(any(target_arch = "wasm32", doc))]
pub async fn run_async(
&self,
shape: &RenderShape,
buffers: &Buffers,
out: &mut ImageReadBuffer,
settings: RenderConfig,
) -> Result<Image, MissingVar> {
self.run_with_vars_async(
shape,
&Default::default(),
buffers,
out,
settings,
)
.await
}
#[cfg(any(target_arch = "wasm32", doc))]
pub async fn run_with_vars_async(
&self,
shape: &RenderShape,
vars: &ShapeVars<f32>,
buffers: &Buffers,
out: &mut ImageReadBuffer,
settings: RenderConfig,
) -> Result<Image, MissingVar> {
self.submit_with_vars(shape, vars, buffers, Some(out), &settings)?;
let image = self.map_image_async(out).await;
Ok(image.image())
}
pub fn submit(
&self,
shape: &RenderShape,
buffers: &mut Buffers,
out: Option<&mut ImageReadBuffer>,
settings: &RenderConfig,
) -> Result<(), MissingVar> {
self.submit_with_vars(
shape,
&Default::default(),
buffers,
out,
settings,
)
}
pub fn submit_with_vars(
&self,
shape: &RenderShape,
vars: &ShapeVars<f32>,
buffers: &Buffers,
out: Option<&mut ImageReadBuffer>,
settings: &RenderConfig,
) -> Result<(), MissingVar> {
let render_size = TileRenderSize::from(buffers.image_size);
let mat =
settings.world_to_model * buffers.image_size.screen_to_world();
let start_offset = u32::try_from(shape.bytecode.len()).unwrap() / 2;
let config = Config {
mat: mat.data.as_slice().try_into().unwrap(),
axes: shape.axes,
render_size: [
render_size.width(),
render_size.height(),
render_size.depth(),
],
tape_data_capacity: TAPE_DATA_CAPACITY.try_into().unwrap(),
image_size: [
buffers.image_size.width(),
buffers.image_size.height(),
buffers.image_size.depth(),
],
tape_data_offset: start_offset,
root_tape_len: start_offset,
};
{
let config_len = std::mem::size_of_val(&config);
let mut writer = self
.gpu
.queue
.write_buffer_with(
&buffers.config_buf,
0,
((config_len + shape.bytecode.as_bytes().len()) as u64)
.try_into()
.unwrap(),
)
.unwrap();
writer
.slice(..config_len)
.copy_from_slice(config.as_bytes());
writer
.slice(config_len..)
.copy_from_slice(shape.bytecode.as_bytes());
}
if let Some(var_size) = NonZeroU64::new(shape.vars.size()) {
let mut writer = self
.gpu
.queue
.write_buffer_with(&shape.vars, 0, var_size)
.unwrap();
for (v, i) in shape.shape.inner().vars().iter() {
match v {
Var::X | Var::Y | Var::Z => (),
Var::V(vi) => {
let Some(value) = vars.get(vi) else {
return Err(MissingVar { var: vi });
};
let offset = i * std::mem::size_of::<f32>();
writer
.slice(offset..offset + 4)
.copy_from_slice(value.as_bytes());
}
}
}
}
let mut encoder = self.gpu.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor { label: None },
);
self.reset_ctx.run(&mut encoder, buffers);
let mut compute_pass =
encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: None,
timestamp_writes: buffers.timestamps.as_ref().map(
|query_set| wgpu::ComputePassTimestampWrites {
query_set,
beginning_of_pass_write_index: Some(0),
end_of_pass_write_index: Some(1),
},
),
});
let common_bind_group = buffers.bind_groups.common(self, buffers);
compute_pass.set_bind_group(0, common_bind_group, &[]);
let vars_bind_group = shape.vars_bind_group(self);
compute_pass.set_bind_group(1, vars_bind_group, &[]);
self.root_ctx.run(
self,
buffers,
shape.bytecode.reg_count(),
render_size,
&mut compute_pass,
);
self.repack_ctx
.run(self, buffers, render_size, &mut compute_pass);
let strata_count = u64::from(render_size.depth()).div_ceil(64);
for strata in 0..strata_count {
self.interval_ctx.run(
self,
buffers,
strata,
shape.bytecode.reg_count(),
&mut compute_pass,
);
self.voxel_ctx.run(
self,
buffers,
shape.bytecode.reg_count(),
&mut compute_pass,
);
self.merge_ctx.run(self, buffers, &mut compute_pass);
self.normals_ctx.run(
self,
buffers,
shape.bytecode.reg_count(),
&mut compute_pass,
);
self.clear_ctx.run(self, buffers, &mut compute_pass);
}
drop(compute_pass);
if let Some(image) = out {
image
.grow_to_fit(&self.gpu.device, buffers.image_size)
.expect(
"buffers.image_size should always be \
a valid size for ImageReadBuffer::grow_to_fit",
);
if let Some(timestamps) = &buffers.timestamps {
encoder.resolve_query_set(timestamps, 0..2, &buffers.ts_buf, 0);
encoder.copy_buffer_to_buffer(
&buffers.ts_buf,
0,
image.buffer.data(),
buffers.geom.size_bytes(), buffers.ts_buf.size(),
);
}
encoder.copy_buffer_to_buffer(
buffers.geom.data(),
0,
image.buffer.data(),
0,
buffers.geom.size_bytes(),
);
}
self.gpu.queue.submit(Some(encoder.finish()));
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn map_image<'a>(
&self,
image: &'a mut ImageReadBuffer,
) -> MappedImage<'a> {
let slice = image.buffer.map_async(|_| {});
self.gpu
.device
.poll(wgpu::PollType::wait_indefinitely())
.unwrap();
MappedImage {
image,
slice,
ns_per_tick: if self.has_timestamps {
Some(self.gpu.queue.get_timestamp_period())
} else {
None
},
}
}
#[cfg(any(target_arch = "wasm32", doc))]
pub async fn map_image_async<'a>(
&self,
image: &'a mut ImageReadBuffer,
) -> MappedImage<'a> {
let (tx, rx) = flume::bounded(0);
let slice = image.buffer.map_async(move |_| tx.send(()).unwrap());
rx.recv_async().await.unwrap();
MappedImage {
image,
slice,
ns_per_tick: if self.has_timestamps {
Some(self.gpu.queue.get_timestamp_period())
} else {
None
},
}
}
pub fn set_buffers_image_size(
&self,
buffers: &mut Buffers,
image_size: VoxelSize,
) -> Result<(), BuffersError> {
buffers.set_image_size(&self.gpu.device, image_size)
}
}
pub struct MappedImage<'a> {
image: &'a ImageReadBuffer,
slice: wgpu::BufferSlice<'a>,
ns_per_tick: Option<f32>,
}
impl Drop for MappedImage<'_> {
fn drop(&mut self) {
self.image.buffer.data().unmap();
}
}
impl MappedImage<'_> {
pub fn image(&self) -> Image {
let result = <[GeometryPixel]>::ref_from_bytes(
&self.slice.get_mapped_range()[..self.image_bytes()],
)
.unwrap()
.to_owned();
Image::build(result, self.image.image_size).unwrap()
}
pub fn time(&self) -> Option<std::time::Duration> {
self.ns_per_tick.map(|ns_per_tick| {
let slice = self.slice.get_mapped_range();
let ts =
<[u64]>::ref_from_bytes(&slice[self.image_bytes()..]).unwrap();
std::time::Duration::from_nanos(
(ts[1].saturating_sub(ts[0]) as f64 * ns_per_tick as f64)
as u64,
)
})
}
fn image_bytes(&self) -> usize {
(self.image.image_size.width() as usize)
* (self.image.image_size.height() as usize)
* std::mem::size_of::<GeometryPixel>()
}
}
struct ClearContext {
bind_group_layout: wgpu::BindGroupLayout,
pipeline: wgpu::ComputePipeline,
}
impl ClearContext {
fn new(
device: &wgpu::Device,
common_bind_group_layout: &wgpu::BindGroupLayout,
vars_bind_group_layout: &wgpu::BindGroupLayout,
) -> Self {
let bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("clear bind group layout"),
entries: &[
buffer_rw(0), buffer_rw(1), buffer_rw(2), buffer_rw(3), buffer_rw(4), ],
});
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("clear pipeline layout"),
bind_group_layouts: &[
Some(common_bind_group_layout),
Some(vars_bind_group_layout),
Some(&bind_group_layout),
],
immediate_size: 0u32,
});
let shader_code = clear_shader();
let shader_module =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("clear shader module"),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
});
let pipeline =
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("clear"),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("clear_main"),
compilation_options: Default::default(),
cache: None,
});
Self {
pipeline,
bind_group_layout,
}
}
fn run(
&self,
ctx: &Context,
buffers: &Buffers,
compute_pass: &mut wgpu::ComputePass,
) {
let bind_group = buffers.bind_groups.clear(ctx, buffers);
compute_pass.set_pipeline(&self.pipeline);
compute_pass.set_bind_group(2, bind_group, &[]);
compute_pass.dispatch_workgroups(1, 1, 1);
}
}
struct MergeContext {
bind_group_layout: wgpu::BindGroupLayout,
pipeline: wgpu::ComputePipeline,
}
impl MergeContext {
fn new(
device: &wgpu::Device,
common_bind_group_layout: &wgpu::BindGroupLayout,
vars_bind_group_layout: &wgpu::BindGroupLayout,
) -> Self {
let shader_code = merge_shader();
let bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("merge bind group layout"),
entries: &[
buffer_rw(0), buffer_rw(1), buffer_rw(2), buffer_rw(3), ],
});
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("merge pipeline layout"),
bind_group_layouts: &[
Some(common_bind_group_layout),
Some(vars_bind_group_layout),
Some(&bind_group_layout),
],
immediate_size: 0u32,
});
let shader_module =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("merge shader module"),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
});
let pipeline =
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("merge"),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("merge_main"),
compilation_options: Default::default(),
cache: None,
});
Self {
pipeline,
bind_group_layout,
}
}
fn run(
&self,
ctx: &Context,
buffers: &Buffers,
compute_pass: &mut wgpu::ComputePass,
) {
let render_size = buffers.render_size();
let bind_group = buffers.bind_groups.merge(ctx, buffers);
compute_pass.set_pipeline(&self.pipeline);
compute_pass.set_bind_group(2, bind_group, &[]);
compute_pass.dispatch_workgroups(
render_size.width().div_ceil(8),
render_size.height().div_ceil(8),
1,
);
}
}
struct ResetContext;
impl ResetContext {
fn new() -> Self {
ResetContext
}
fn run(&self, encoder: &mut wgpu::CommandEncoder, buffers: &Buffers) {
encoder.clear_buffer(buffers.tile64.tiles.data(), 12, Some(4));
let strata_size_bytes = buffers.strata_size_bytes();
for s in 0..buffers.render_size().nz() {
encoder.clear_buffer(
buffers.tile64.strata.data(),
u64::from(s) * u64::try_from(strata_size_bytes).unwrap(),
Some(16),
);
}
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);
buffers.tile_tapes.clear(encoder);
}
}
#[cfg(test)]
mod test {
use super::*;
use heck::ToShoutySnakeCase;
#[test]
fn shader_has_all_ops() {
for (op, _) in fidget_bytecode::iter_ops() {
let op = format!("OP_{}", op.to_shouty_snake_case());
assert!(
TAPE_INTERPRETER.contains(&op),
"tape interpreter is missing {op}"
);
assert!(
TAPE_SIMPLIFY.contains(&op),
"tape simplification is missing {op}"
);
}
}
#[test]
fn compile_shaders() {
for (src, desc) in [
(interval_root_shader(16), "interval root"),
(interval_tiles_shader(16), "interval tiles"),
(voxel_tiles_shader(16), "voxel tiles"),
(normals_shader(16), "normals tiles"),
(repack_shader(), "repack"),
(sort_shader(), "sort"),
(merge_shader(), "merge"),
(clear_shader(), "clear"),
] {
crate::compile_shader(&src, desc);
}
}
}