use bytemuck::{Pod, Zeroable};
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub(crate) struct TriInstance {
pub v0: [f32; 2],
pub v1: [f32; 2],
pub v2: [f32; 2],
pub color0: u32,
pub color1: u32,
pub color2: u32,
pub _pad0: f32,
pub clip_rect: [f32; 4],
}
const _: () = assert!(
std::mem::size_of::<TriInstance>() == 56,
"TriInstance must stay 56 bytes — WGSL struct in shaders.rs mirrors this layout"
);
pub(crate) fn tri_instance_layout() -> wgpu::VertexBufferLayout<'static> {
use wgpu::VertexFormat::*;
static ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { shader_location: 0, format: Float32x2, offset: 0 },
wgpu::VertexAttribute { shader_location: 1, format: Float32x2, offset: 8 },
wgpu::VertexAttribute { shader_location: 2, format: Float32x2, offset: 16 },
wgpu::VertexAttribute { shader_location: 3, format: Uint32, offset: 24 },
wgpu::VertexAttribute { shader_location: 4, format: Uint32, offset: 28 },
wgpu::VertexAttribute { shader_location: 5, format: Uint32, offset: 32 },
wgpu::VertexAttribute { shader_location: 6, format: Float32, offset: 36 },
wgpu::VertexAttribute { shader_location: 7, format: Float32x4, offset: 40 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<TriInstance>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
const INITIAL_CAPACITY: usize = 1024;
pub(crate) const MAX_GEOMETRY_BUFFER_BYTES: usize = 512 * 1024 * 1024;
#[derive(Debug, Clone, Copy)]
pub(crate) struct GeometryCapacityError {
pub(crate) stage: &'static str,
pub(crate) buffer: &'static str,
pub(crate) requested_instances: usize,
pub(crate) requested_bytes: usize,
pub(crate) current_capacity_instances: usize,
pub(crate) instance_size: usize,
pub(crate) max_bytes: usize,
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct GeometryUploadProfile {
pub(crate) upload_bytes: usize,
pub(crate) capacity_before_instances: usize,
pub(crate) capacity_after_instances: usize,
pub(crate) growth_bytes: usize,
pub(crate) reserve_us: u128,
pub(crate) upload_us: u128,
}
fn bounded_capacity_with_limit(
current: usize,
needed: usize,
initial_capacity: usize,
max_bytes: usize,
buffer: &'static str,
) -> Result<usize, GeometryCapacityError> {
let instance_size = std::mem::size_of::<TriInstance>();
let max_instances = max_bytes / instance_size;
if needed > max_instances {
return Err(GeometryCapacityError {
stage: "gpu_buffer_reserve",
buffer,
requested_instances: needed,
requested_bytes: needed.checked_mul(instance_size).unwrap_or(usize::MAX),
current_capacity_instances: current,
instance_size,
max_bytes,
});
}
if needed <= current {
return Ok(current);
}
Ok(current
.max(initial_capacity)
.checked_mul(2)
.unwrap_or(max_instances)
.max(needed)
.min(max_instances))
}
pub(crate) fn bounded_geometry_capacity(
current: usize,
needed: usize,
initial_capacity: usize,
buffer: &'static str,
) -> Result<usize, GeometryCapacityError> {
bounded_capacity_with_limit(
current,
needed,
initial_capacity,
MAX_GEOMETRY_BUFFER_BYTES,
buffer,
)
}
fn make_instance_buffer(device: &wgpu::Device, capacity: usize) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("uzor_urx_wgpu.native_path_buffer"),
size: (capacity * std::mem::size_of::<TriInstance>()) as wgpu::BufferAddress,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
pub(crate) struct PathPipeline {
pipeline_off: wgpu::RenderPipeline,
pipeline_test: wgpu::RenderPipeline,
buffer: wgpu::Buffer,
capacity: usize,
}
impl PathPipeline {
pub(crate) fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
sample_count: u32,
uniform_bgl: &wgpu::BindGroupLayout,
) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("uzor_urx_wgpu.path_shader_native"),
source: wgpu::ShaderSource::Wgsl(crate::shaders::PATH_SHADER_NATIVE.into()),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("uzor_urx_wgpu.path_pipeline_layout_native"),
bind_group_layouts: &[Some(uniform_bgl)],
immediate_size: 0,
});
let vertex_buffers = [tri_instance_layout()];
let color_targets = [Some(wgpu::ColorTargetState {
format,
blend: Some(crate::pipelines::quad::premultiplied_blend_state()),
write_mask: wgpu::ColorWrites::ALL,
})];
let (pipeline_off, pipeline_test) =
crate::pipelines::build_off_test_pair(device, |depth_stencil| wgpu::RenderPipelineDescriptor {
label: Some("uzor_urx_wgpu.path_pipeline_native"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &vertex_buffers,
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &color_targets,
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil,
multisample: wgpu::MultisampleState { count: sample_count, ..Default::default() },
multiview_mask: None,
cache: None,
});
let buffer = make_instance_buffer(device, INITIAL_CAPACITY);
Self { pipeline_off, pipeline_test, buffer, capacity: INITIAL_CAPACITY }
}
pub(crate) fn upload(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
data: &[TriInstance],
) -> Result<GeometryUploadProfile, GeometryCapacityError> {
if data.is_empty() {
return Ok(GeometryUploadProfile {
capacity_before_instances: self.capacity,
capacity_after_instances: self.capacity,
..GeometryUploadProfile::default()
});
}
let needed = data.len();
let profile = crate::profile::enabled();
crate::profile::stage(
"gpu_buffer_reserve_begin",
format_args!(
"buffer=path requested_instances={} requested_bytes={} current_capacity_instances={}",
needed,
needed.saturating_mul(std::mem::size_of::<TriInstance>()),
self.capacity,
),
);
let capacity_before = self.capacity;
let reserve_t0 = profile.then(std::time::Instant::now);
let capacity = bounded_geometry_capacity(self.capacity, needed, INITIAL_CAPACITY, "path")?;
if capacity != self.capacity {
self.buffer = make_instance_buffer(device, capacity);
self.capacity = capacity;
}
let reserve_us = reserve_t0.map_or(0, |started| started.elapsed().as_micros());
let upload_bytes = needed.saturating_mul(std::mem::size_of::<TriInstance>());
crate::profile::stage(
"gpu_buffer_upload_begin",
format_args!(
"buffer=path upload_bytes={} capacity_instances={}",
upload_bytes,
self.capacity,
),
);
let upload_t0 = profile.then(std::time::Instant::now);
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
let upload_us = upload_t0.map_or(0, |started| started.elapsed().as_micros());
Ok(GeometryUploadProfile {
upload_bytes,
capacity_before_instances: capacity_before,
capacity_after_instances: self.capacity,
growth_bytes: self
.capacity
.saturating_sub(capacity_before)
.saturating_mul(std::mem::size_of::<TriInstance>()),
reserve_us,
upload_us,
})
}
pub(crate) fn bind(&self, pass: &mut wgpu::RenderPass<'_>, stencil_ref: Option<u32>) {
match stencil_ref {
None => pass.set_pipeline(&self.pipeline_off),
Some(r) => {
pass.set_pipeline(&self.pipeline_test);
pass.set_stencil_reference(r);
}
}
pass.set_vertex_buffer(0, self.buffer.slice(..));
}
pub(crate) fn draw_range(&self, pass: &mut wgpu::RenderPass<'_>, start: u32, count: u32) {
pass.draw(0..3, start..(start + count));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tri_instance_is_56_bytes() {
assert_eq!(std::mem::size_of::<TriInstance>(), 56);
}
#[test]
fn bounded_growth_rejects_before_a_large_buffer_allocation() {
let eight_instances = 8 * std::mem::size_of::<TriInstance>();
assert_eq!(bounded_capacity_with_limit(4, 5, 4, eight_instances, "test").unwrap(), 8);
let error = bounded_capacity_with_limit(8, 9, 4, eight_instances, "test")
.expect_err("capacity above the byte limit must fail before create_buffer");
assert_eq!(error.stage, "gpu_buffer_reserve");
assert_eq!(error.buffer, "test");
assert_eq!(error.requested_instances, 9);
assert_eq!(error.requested_bytes, 9 * std::mem::size_of::<TriInstance>());
assert_eq!(error.current_capacity_instances, 8);
assert_eq!(error.max_bytes, eight_instances);
}
}