typed primitive buffers + macro for creation

This commit is contained in:
2025-08-21 19:37:50 -04:00
parent b7f83b58a9
commit bde929b05a
5 changed files with 190 additions and 105 deletions

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
ActiveSensors, SensorMap, UiRegion, WidgetId, Widgets, ActiveSensors, SensorMap, UiRegion, WidgetId, Widgets,
primitive::{PrimitiveData, PrimitiveInstance, Primitives}, primitive::{Primitive, Primitives},
}; };
pub struct Painter<'a, Ctx: 'static> { pub struct Painter<'a, Ctx: 'static> {
@@ -28,16 +28,8 @@ impl<'a, Ctx> Painter<'a, Ctx> {
region: UiRegion::full(), region: UiRegion::full(),
} }
} }
pub fn write<Data: PrimitiveData>(&mut self, data: Data) { pub fn write<P: Primitive>(&mut self, data: P) {
let ptr = self.primitives.data.len() as u32; self.primitives.write(data, self.region);
let region = self.region;
self.primitives
.instances
.push(PrimitiveInstance { region, ptr });
self.primitives.data.push(Data::DISCRIM);
self.primitives
.data
.extend_from_slice(bytemuck::cast_slice::<_, u32>(&[data]));
} }
pub fn draw<W>(&mut self, id: &WidgetId<W>) pub fn draw<W>(&mut self, id: &WidgetId<W>)

View File

@@ -13,16 +13,18 @@ pub struct WindowUniform {
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PrimitiveInstance { pub struct PrimitiveInstance {
pub region: UiRegion, pub region: UiRegion,
pub ptr: u32, pub binding: u32,
pub idx: u32,
} }
impl PrimitiveInstance { impl PrimitiveInstance {
const ATTRIBS: [VertexAttribute; 5] = wgpu::vertex_attr_array![ const ATTRIBS: [VertexAttribute; 6] = wgpu::vertex_attr_array![
0 => Float32x2, 0 => Float32x2,
1 => Float32x2, 1 => Float32x2,
2 => Float32x2, 2 => Float32x2,
3 => Float32x2, 3 => Float32x2,
4 => Uint32, 4 => Uint32,
5 => Uint32,
]; ];
pub fn desc() -> wgpu::VertexBufferLayout<'static> { pub fn desc() -> wgpu::VertexBufferLayout<'static> {

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
primitive::{PrimitiveInstance, Primitives}, primitive::{PrimitiveBuffers, Primitives},
render::util::ArrBuf, render::{data::PrimitiveInstance, util::ArrBuf},
}; };
use data::WindowUniform; use data::WindowUniform;
use wgpu::{ use wgpu::{
@@ -16,40 +16,34 @@ mod util;
const SHAPE_SHADER: &str = include_str!("./shader.wgsl"); const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
pub struct UIRenderNode { pub struct UIRenderNode {
bind_group_layout: BindGroupLayout, layout0: BindGroupLayout,
bind_group: BindGroup, group0: BindGroup,
primitive_layout: BindGroupLayout,
primitive_group: BindGroup,
pipeline: RenderPipeline, pipeline: RenderPipeline,
window_buffer: Buffer, window_buffer: Buffer,
instance: ArrBuf<PrimitiveInstance>, instance: ArrBuf<PrimitiveInstance>,
data: ArrBuf<u32>, primitives: PrimitiveBuffers,
} }
impl UIRenderNode { impl UIRenderNode {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) { pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
if self.instance.len() != 0 { if self.instance.len() != 0 {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.group0, &[]);
pass.set_bind_group(1, &self.primitive_group, &[]);
pass.set_vertex_buffer(0, self.instance.buffer.slice(..)); pass.set_vertex_buffer(0, self.instance.buffer.slice(..));
pass.draw(0..4, 0..self.instance.len() as u32); pass.draw(0..4, 0..self.instance.len() as u32);
} }
} }
pub fn update( pub fn update(&mut self, device: &Device, queue: &Queue, primitives: Option<&Primitives>) {
&mut self,
device: &Device,
queue: &Queue,
primitives: Option<&Primitives>,
) {
if let Some(primitives) = primitives { if let Some(primitives) = primitives {
self.instance.update(device, queue, &primitives.instances); self.instance.update(device, queue, &primitives.instances);
self.data.update(device, queue, &primitives.data); self.primitives.update(device, queue, &primitives.data);
self.bind_group = Self::bind_group( self.primitive_group =
device, Self::primitive_group(device, &self.primitive_layout, self.primitives.buffers())
&self.bind_group_layout,
&self.window_buffer,
&self.data.buffer,
)
} }
} }
@@ -69,7 +63,7 @@ impl UIRenderNode {
let window_uniform = WindowUniform::default(); let window_uniform = WindowUniform::default();
let window_buffer = device.create_buffer_init(&BufferInitDescriptor { let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("Camera Buffer"), label: Some("window"),
contents: bytemuck::cast_slice(&[window_uniform]), contents: bytemuck::cast_slice(&[window_uniform]),
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
}); });
@@ -79,15 +73,10 @@ impl UIRenderNode {
BufferUsages::VERTEX | BufferUsages::COPY_DST, BufferUsages::VERTEX | BufferUsages::COPY_DST,
"instance", "instance",
); );
let data = ArrBuf::new( let primitives = PrimitiveBuffers::new(device);
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"data",
);
let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor { let layout0 = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[BindGroupLayoutEntry {
BindGroupLayoutEntry {
binding: 0, binding: 0,
visibility: ShaderStages::VERTEX, visibility: ShaderStages::VERTEX,
ty: BindingType::Buffer { ty: BindingType::Buffer {
@@ -96,9 +85,16 @@ impl UIRenderNode {
min_binding_size: None, min_binding_size: None,
}, },
count: None, count: None,
}, }],
label: Some("window"),
});
let group0 = Self::bind_group_0(device, &layout0, &window_buffer);
let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &core::array::from_fn::<_, { PrimitiveBuffers::LEN }, _>(|i| {
BindGroupLayoutEntry { BindGroupLayoutEntry {
binding: 1, binding: i as u32,
visibility: ShaderStages::FRAGMENT, visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer { ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true }, ty: BufferBindingType::Storage { read_only: true },
@@ -106,16 +102,17 @@ impl UIRenderNode {
min_binding_size: None, min_binding_size: None,
}, },
count: None, count: None,
}, }
], }),
label: Some("camera_bind_group_layout"), label: Some("primitive"),
}); });
let bind_group = Self::bind_group(device, &bind_group_layout, &window_buffer, &data.buffer); let primitive_group =
Self::primitive_group(device, &primitive_layout, primitives.buffers());
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"), label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout], bind_group_layouts: &[&layout0, &primitive_layout],
push_constant_ranges: &[], push_constant_ranges: &[],
}); });
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
@@ -157,34 +154,44 @@ impl UIRenderNode {
}); });
Self { Self {
bind_group_layout, layout0,
bind_group, group0,
primitive_layout,
primitive_group,
pipeline, pipeline,
window_buffer, window_buffer,
instance, instance,
data, primitives,
} }
} }
pub fn bind_group( pub fn bind_group_0(
device: &Device, device: &Device,
layout: &BindGroupLayout, layout: &BindGroupLayout,
window_buffer: &Buffer, window_buffer: &Buffer,
data: &Buffer,
) -> BindGroup { ) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor { device.create_bind_group(&BindGroupDescriptor {
layout, layout,
entries: &[ entries: &[BindGroupEntry {
BindGroupEntry {
binding: 0, binding: 0,
resource: window_buffer.as_entire_binding(), resource: window_buffer.as_entire_binding(),
}, }],
BindGroupEntry { label: Some("ui window"),
binding: 1, })
resource: data.as_entire_binding(), }
},
], pub fn primitive_group(
label: Some("ui_bind_group"), device: &Device,
layout: &BindGroupLayout,
buffers: [&Buffer; PrimitiveBuffers::LEN],
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &buffers.each_ref().map(|b| BindGroupEntry {
binding: 0,
resource: b.as_entire_binding(),
}),
label: Some("ui primitives"),
}) })
} }
} }

View File

@@ -1,27 +1,93 @@
use crate::Color; use crate::{
Color, UiRegion,
pub use super::data::PrimitiveInstance; render::{ArrBuf, data::PrimitiveInstance},
};
use bytemuck::Pod;
use wgpu::*;
#[derive(Default)] #[derive(Default)]
pub struct Primitives { pub struct Primitives {
pub instances: Vec<PrimitiveInstance>, pub(super) instances: Vec<PrimitiveInstance>,
pub data: Vec<u32>, pub(super) data: PrimitiveData,
} }
/// NOTE: Self must have at least u32 alignment pub trait Primitive: Pod {
pub trait PrimitiveData: bytemuck::Pod { const BINDING: u32;
const DISCRIM: u32; fn vec(data: &mut PrimitiveData) -> &mut Vec<Self>;
}
macro_rules! primitives {
($($name:ident: $ty:ty => $binding:expr,)*) => {
#[derive(Default)]
pub struct PrimitiveData {
$($name: Vec<$ty>,)*
}
pub struct PrimitiveBuffers {
$($name: ArrBuf<$ty>,)*
}
impl PrimitiveBuffers {
pub fn update(&mut self, device: &Device, queue: &Queue, data: &PrimitiveData) {
$(self.$name.update(device, queue, &data.$name);)*
}
}
impl PrimitiveBuffers {
pub const LEN: usize = primitives!(@count $($name)*);
pub fn buffers(&self) -> [&Buffer; Self::LEN] {
[
$(&self.$name.buffer)*
]
}
pub fn new(device: &Device) -> Self {
Self {
$($name: ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
stringify!($name),
))*
}
}
}
$(
unsafe impl bytemuck::Pod for $ty {}
unsafe impl bytemuck::Zeroable for $ty {}
impl Primitive for $ty {
const BINDING: u32 = $binding;
fn vec(data: &mut PrimitiveData) -> &mut Vec<Self> {
&mut data.$name
}
}
)*
};
(@count $t1:tt, $($t:tt),+) => { 1 + gen!(@count $($t),+) };
(@count $t:tt) => { 1 };
}
primitives!(
rects: RoundedRectData => 0,
);
impl Primitives {
pub fn write<P: Primitive>(&mut self, data: P, region: UiRegion) {
let vec = P::vec(&mut self.data);
let i = vec.len() as u32;
self.instances.push(PrimitiveInstance {
region,
idx: i,
binding: P::BINDING,
});
vec.push(data);
}
} }
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone)]
pub struct RoundedRectData { pub struct RoundedRectData {
pub color: Color<u8>, pub color: Color<u8>,
pub radius: f32, pub radius: f32,
pub thickness: f32, pub thickness: f32,
pub inner_radius: f32, pub inner_radius: f32,
} }
impl PrimitiveData for RoundedRectData {
const DISCRIM: u32 = 0;
}

View File

@@ -1,7 +1,9 @@
const RECT: u32 = 0;
@group(0) @binding(0) @group(0) @binding(0)
var<uniform> window: WindowUniform; var<uniform> window: WindowUniform;
@group(0) @binding(1) @group(1) @binding(RECT)
var<storage> data: array<u32>; var<storage> rects: array<RoundedRect>;
struct WindowUniform { struct WindowUniform {
dim: vec2<f32>, dim: vec2<f32>,
@@ -12,7 +14,8 @@ struct InstanceInput {
@location(1) top_left_offset: vec2<f32>, @location(1) top_left_offset: vec2<f32>,
@location(2) bottom_right_anchor: vec2<f32>, @location(2) bottom_right_anchor: vec2<f32>,
@location(3) bottom_right_offset: vec2<f32>, @location(3) bottom_right_offset: vec2<f32>,
@location(4) pointer: u32, @location(4) binding: u32,
@location(5) idx: u32,
} }
struct RoundedRect { struct RoundedRect {
@@ -23,9 +26,10 @@ struct RoundedRect {
} }
struct VertexOutput { struct VertexOutput {
@location(0) pointer: u32, @location(0) top_left: vec2<f32>,
@location(1) top_left: vec2<f32>, @location(1) bot_right: vec2<f32>,
@location(2) bot_right: vec2<f32>, @location(2) binding: u32,
@location(3) idx: u32,
@builtin(position) clip_position: vec4<f32>, @builtin(position) clip_position: vec4<f32>,
}; };
@@ -52,7 +56,8 @@ fn vs_main(
) * size; ) * size;
pos = pos / window.dim * 2.0 - 1.0; pos = pos / window.dim * 2.0 - 1.0;
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0); out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
out.pointer = in.pointer; out.binding = in.binding;
out.idx = in.idx;
out.top_left = top_left; out.top_left = top_left;
out.bot_right = bot_right; out.bot_right = bot_right;
@@ -64,25 +69,20 @@ fn fs_main(
in: VertexOutput in: VertexOutput
) -> @location(0) vec4<f32> { ) -> @location(0) vec4<f32> {
let pos = in.clip_position.xy; let pos = in.clip_position.xy;
let ty = data[in.pointer];
let dp = in.pointer + 1u;
let region = Region(pos, in.top_left, in.bot_right); let region = Region(pos, in.top_left, in.bot_right);
switch ty { let i = in.idx;
case 0u: { switch in.binding {
return draw_rounded_rect(region, RoundedRect( case RECT: {
data[dp + 0u], return draw_rounded_rect(region, rects[i]);
bitcast<f32>(data[dp + 1u]),
bitcast<f32>(data[dp + 2u]),
bitcast<f32>(data[dp + 3u]),
));
}
default: {}
} }
default: {
return vec4(1.0, 0.0, 1.0, 1.0); return vec4(1.0, 0.0, 1.0, 1.0);
} }
}
}
fn draw_rounded_rect(region: Region, rect: RoundedRect) -> vec4<f32> { fn draw_rounded_rect(region: Region, rect: RoundedRect) -> vec4<f32> {
var color = unpack4x8unorm(rect.color); var color = read_color(rect.color);
let edge = 0.5; let edge = 0.5;
@@ -108,3 +108,21 @@ fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner:
let q = abs(p) - (rect_corner - radius); let q = abs(p) - (rect_corner - radius);
return length(max(q, vec2<f32>(0.0, 0.0))) - radius; return length(max(q, vec2<f32>(0.0, 0.0))) - radius;
} }
fn read_color(c: u32) -> vec4<f32> {
let color = unpack4x8unorm(c);
return vec4(
srgb_to_linear(color.r),
srgb_to_linear(color.g),
srgb_to_linear(color.b),
color.a,
);
}
fn srgb_to_linear(c: f32) -> f32 {
if c <= 0.04045 {
return c / 12.92;
} else {
return pow((c + 0.055) / 1.055, 2.4);
}
}