Draw each primitive with its own pipeline, registered rather than declared

The `primitives!` macro, `PrimitiveData`, `PrimitiveVec`,
`PrimitiveBuffers`, the `Primitive` trait and the shader's dispatch
switch are gone. A primitive is now a registration: its WGSL and,
implicitly, the size of the entry that WGSL reads. Everything else --
its instance list, its free list, its buffers, its bind group and its
pipeline -- follows from that, so adding one is a `register` call and a
shader file, with nothing per-type to remember and no cross-type
dispatch to extend.

Nothing dispatches dynamically. Push, free, renumber, upload and draw
are identical for every primitive; what differs is the entry size and
the pipeline, which are data. So `InstanceList` carries a runtime
stride and its instances' data as bytes, and one concrete type serves
every primitive and the textures. Measured against a typed list it
costs 0.2ns per write, where a trait object costs 1.6ns.

Because each type has its own list, an instance's index is also its
data index: `@builtin(instance_index)` replaces the `idx` field, the
`binding` field goes with the switch, and `PrimitiveInstance` drops from
28 bytes to 20. `PrimitiveVec`'s free list merges into the instance
list's, so an instance and its data are freed by one `swap_remove`
rather than two arenas kept in step.

`shader.wgsl` becomes `shader/prelude.wgsl` plus one file per primitive.
The prelude carries the window, masks, sampled texture, vertex shader
and `masked()`, and is compiled ahead of each primitive's own source --
which is also what a caller's own primitive would be. Masks move back
into group 0 beside the window uniform, since every pipeline shares one
layout.

Within a layer, types now draw in registration order: a rect under a
glyph under a texture. Order within a layer was never meaningful --
freeing an instance swaps another into its place -- so this replaces an
accident with a defined order, and backgrounds land under their content.

Verified with a headless run per case: text over its own rect and an
image over its own rect in one layer, a masked stack clipping, the
text-layout tab, and adding three images and deleting one.
This commit is contained in:
iris committed 2026-09-13 14:07:37 -04:00
1 parent b3d3da5dab
commit 4d9839f380
15 files changed
+609 -615

No files matched your search

+8 -4
View File
@@ -2,7 +2,7 @@ use std::ops::{Index, IndexMut};
use crate::{
UiRegion, WidgetId,
render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
render::{LayerDraws, MaskIdx, PrimitiveHandle, PrimitiveKind},
util::to_mut,
};
@@ -121,12 +121,16 @@ impl<T: Default> Layers<T> {
}
impl DrawLayers {
pub fn write<P: Primitive>(
pub fn write<P: bytemuck::Pod>(
&mut self,
layer: LayerId,
info: PrimitiveInst<P>,
kind: PrimitiveKind<P>,
id: WidgetId,
primitive: P,
region: UiRegion,
mask_idx: MaskIdx,
) -> PrimitiveHandle {
self[layer].write(layer, info)
self[layer].write(layer, kind, id, primitive, region, mask_idx)
}
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
+1 -5
View File
@@ -12,20 +12,16 @@ pub struct WindowUniform {
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PrimitiveInstance {
pub region: UiRegion,
pub binding: u32,
pub idx: u32,
pub mask_idx: MaskIdx,
}
impl PrimitiveInstance {
const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![
const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
0 => Float32x2,
1 => Float32x2,
2 => Float32x2,
3 => Float32x2,
4 => Uint32,
5 => Uint32,
6 => Uint32,
];
pub fn desc() -> VertexBufferLayout<'static> {
+233 -171
View File
@@ -25,16 +25,22 @@ pub use atlas::*;
pub use data::{Mask, MaskIdx};
pub use primitive::*;
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
const TEXTURE_SHADER: &str = include_str!("./shader/texture.wgsl");
pub struct UiRenderNode {
uniform_group: BindGroup,
primitive_layout: BindGroupLayout,
device: Device,
shared_layout: BindGroupLayout,
shared_group: BindGroup,
data_layout: BindGroupLayout,
texture_layout: BindGroupLayout,
mask_layout: BindGroupLayout,
mask_group: BindGroup,
pipeline_layout: PipelineLayout,
format: TextureFormat,
pipeline: RenderPipeline,
/// One per registered primitive, in id order. The texture pipeline is
/// apart because a texture binds group 2 per instance, not per draw.
pipelines: Vec<RenderPipeline>,
texture_pipeline: RenderPipeline,
layers: HashMap<usize, RenderLayer>,
active: Vec<usize>,
@@ -46,30 +52,41 @@ pub struct UiRenderNode {
}
struct RenderLayer {
instance: ArrBuf<PrimitiveInstance>,
primitives: PrimitiveBuffers,
primitive_group: BindGroup,
texture_instance: ArrBuf<PrimitiveInstance>,
/// Which texture each entry of `texture_instance` samples, in the same
/// order. Not in the vertex buffer because it names a bind group.
/// One per registered primitive, in id order, matching `LayerDraws`.
primitives: Vec<ListBuffers>,
textures: ListBuffers,
/// Which texture each entry of `textures` samples, in the same order.
texture_slots: Vec<u32>,
}
/// One list's vertex buffer and the data its shader reads at group 1.
struct ListBuffers {
instance: ArrBuf<PrimitiveInstance>,
data: ArrBuf<u8>,
group: Option<BindGroup>,
}
impl UiRenderNode {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.uniform_group, &[]);
pass.set_bind_group(3, &self.mask_group, &[]);
pass.set_bind_group(0, &self.shared_group, &[]);
for i in &self.active {
let layer = &self.layers[i];
pass.set_bind_group(1, &layer.primitive_group, &[]);
if layer.instance.len() > 0 {
// Types run in registration order, so a rect is under a glyph is
// under a texture. Ordering beyond that is what `Layers` is for --
// freeing an instance swaps another into its place.
pass.set_bind_group(2, self.pages.group(), &[]);
pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
pass.draw(0..4, 0..layer.instance.len() as u32);
for (id, list) in layer.primitives.iter().enumerate() {
let (Some(group), true) = (&list.group, list.instance.len() > 0) else {
continue;
};
pass.set_pipeline(&self.pipelines[id]);
pass.set_bind_group(1, group, &[]);
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
pass.draw(0..4, 0..list.instance.len() as u32);
}
if !layer.texture_slots.is_empty() {
pass.set_vertex_buffer(0, layer.texture_instance.buffer.slice(..));
pass.set_pipeline(&self.texture_pipeline);
pass.set_vertex_buffer(0, layer.textures.instance.buffer.slice(..));
for (i, &slot) in layer.texture_slots.iter().enumerate() {
let Some(group) = self.textures.group(slot) else {
continue;
@@ -101,52 +118,40 @@ impl UiRenderNode {
}
}
}
let rlayer = self.layers.entry(i).or_insert_with(|| {
let primitives = PrimitiveBuffers::new(device);
let primitive_group =
Self::primitive_group(device, &self.primitive_layout, primitives.buffers());
RenderLayer {
instance: ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"instance",
),
primitives,
primitive_group,
texture_instance: ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"texture instance",
),
texture_slots: Vec::new(),
}
});
let rlayer = self
.layers
.entry(i)
.or_insert_with(|| RenderLayer::new(device));
if draws.updated {
rlayer
.instance
.update(device, queue, draws.primitives.instances());
rlayer
.primitives
.update(device, queue, draws.primitives.data());
rlayer.primitive_group = Self::primitive_group(
device,
&self.primitive_layout,
rlayer.primitives.buffers(),
);
.resize_with(draws.primitives().len(), || ListBuffers::new(device));
for (list, draws) in rlayer.primitives.iter_mut().zip(draws.primitives()) {
list.update(device, queue, &self.data_layout, draws);
}
rlayer
.texture_instance
.update(device, queue, draws.textures.instances());
.textures
.update(device, queue, &self.data_layout, &draws.textures);
rlayer.texture_slots.clear();
// Read rather than cast: the payload is a byte vec, so it
// carries no alignment a `u32` slice could borrow.
let slots = draws.textures.data().as_chunks::<4>().0;
rlayer
.texture_slots
.extend(draws.textures.instances().iter().map(|inst| inst.idx));
.extend(slots.iter().copied().map(u32::from_ne_bytes));
draws.updated = false;
}
}
self.build_pipelines(&ui.primitives);
if ui.masks.changed {
ui.masks.changed = false;
if self.masks.update(device, queue, &ui.masks[..]) {
self.mask_group = Self::mask_group(device, &self.mask_layout, &self.masks);
self.shared_group = Self::shared_group(
device,
&self.shared_layout,
&self.window_buffer,
&self.masks,
);
}
}
self.pages
@@ -165,11 +170,6 @@ impl UiRenderNode {
}
pub fn new(device: &Device, queue: &Queue, config: &SurfaceConfiguration) -> Self {
let shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("UI Shape Shader"),
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
});
let window_uniform = WindowUniform {
width: config.width as f32,
height: config.height as f32,
@@ -180,74 +180,95 @@ impl UiRenderNode {
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
});
let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("window"),
});
let uniform_group = Self::bind_group_0(device, &uniform_layout, &window_buffer);
let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &PrimitiveBuffers::BINDINGS.map(|binding| BindGroupLayoutEntry {
binding,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}),
label: Some("primitive"),
});
let shared_layout = Self::shared_layout(device);
let data_layout = Self::data_layout(device);
let texture_layout = Self::texture_layout(device);
let masks = ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks",
);
let texture_layout = Self::texture_layout(device);
let mask_layout = Self::mask_layout(device);
let mask_group = Self::mask_group(device, &mask_layout, &masks);
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
let sampler = default_sampler(device);
let pages = GpuPages::new(device, queue, &texture_layout, &sampler);
let textures = GpuTextures::new(device, queue);
// One layout for every pipeline, so a primitive that does not sample
// or read a buffer binds what is there instead of needing its own.
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[
&uniform_layout,
&primitive_layout,
&texture_layout,
&mask_layout,
],
label: Some("ui"),
bind_group_layouts: &[&shared_layout, &data_layout, &texture_layout],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("UI Shape Pipeline"),
layout: Some(&pipeline_layout),
let texture_pipeline = Self::pipeline(
device,
&pipeline_layout,
config.format,
TEXTURE_SHADER,
"texture",
);
Self {
device: device.clone(),
shared_layout,
shared_group,
data_layout,
texture_layout,
pipeline_layout,
format: config.format,
pipelines: Vec::new(),
texture_pipeline,
window_buffer,
layers: HashMap::default(),
active: Vec::new(),
sampler,
pages,
textures,
masks,
}
}
/// Compiles a pipeline for every primitive registered since the last call.
/// Sources only ever arrive at the end, so an id keeps its pipeline.
fn build_pipelines(&mut self, registry: &PrimitiveRegistry) {
for source in &registry.sources()[self.pipelines.len()..] {
self.pipelines.push(Self::pipeline(
&self.device,
&self.pipeline_layout,
self.format,
source.wgsl,
source.label,
));
}
}
fn pipeline(
device: &Device,
layout: &PipelineLayout,
format: TextureFormat,
wgsl: &str,
label: &str,
) -> RenderPipeline {
let module = device.create_shader_module(ShaderModuleDescriptor {
label: Some(label),
source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()),
});
device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some(label),
layout: Some(layout),
vertex: VertexState {
module: &shader,
module: &module,
entry_point: Some("vs_main"),
buffers: &[PrimitiveInstance::desc()],
compilation_options: Default::default(),
},
fragment: Some(FragmentState {
module: &shader,
module: &module,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format: config.format,
format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
@@ -270,52 +291,74 @@ impl UiRenderNode {
},
multiview_mask: None,
cache: None,
});
Self {
uniform_group,
primitive_layout,
texture_layout,
mask_layout,
mask_group,
pipeline,
window_buffer,
layers: HashMap::default(),
active: Vec::new(),
sampler,
pages,
textures,
masks,
}
}
fn bind_group_0(
device: &Device,
layout: &BindGroupLayout,
window_buffer: &Buffer,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[BindGroupEntry {
binding: 0,
resource: window_buffer.as_entire_binding(),
}],
label: Some("ui window"),
})
}
fn primitive_group(
/// Group 0: what every draw in the ui shares.
fn shared_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: Some("ui shared"),
})
}
fn shared_group(
device: &Device,
layout: &BindGroupLayout,
buffers: [(u32, &Buffer); PrimitiveBuffers::LEN],
window: &Buffer,
masks: &ArrBuf<Mask>,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &buffers.map(|(binding, buf)| BindGroupEntry {
binding,
resource: buf.as_entire_binding(),
}),
label: Some("ui primitives"),
entries: &[
BindGroupEntry {
binding: 0,
resource: window.as_entire_binding(),
},
BindGroupEntry {
binding: 1,
resource: masks.buffer.as_entire_binding(),
},
],
label: Some("ui shared"),
})
}
/// Group 1: one list's per-instance data, whatever its shader reads it as.
fn data_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("ui primitive data"),
})
}
@@ -345,36 +388,55 @@ impl UiRenderNode {
})
}
/// Group 3, apart from the textures so that resizing the masks buffer
/// leaves every texture group intact.
fn mask_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("ui masks"),
})
}
fn mask_group(device: &Device, layout: &BindGroupLayout, masks: &ArrBuf<Mask>) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[BindGroupEntry {
binding: 0,
resource: masks.buffer.as_entire_binding(),
}],
label: Some("ui masks"),
})
}
pub fn texture_count(&self) -> usize {
self.textures.count()
}
}
impl RenderLayer {
fn new(device: &Device) -> Self {
Self {
primitives: Vec::new(),
textures: ListBuffers::new(device),
texture_slots: Vec::new(),
}
}
}
impl ListBuffers {
fn new(device: &Device) -> Self {
Self {
instance: ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"instance",
),
data: ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"primitive data",
),
group: None,
}
}
fn update(
&mut self,
device: &Device,
queue: &Queue,
layout: &BindGroupLayout,
list: &InstanceList,
) {
self.instance.update(device, queue, list.instances());
if self.data.update(device, queue, list.data()) || self.group.is_none() {
self.group = Some(device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[BindGroupEntry {
binding: 0,
resource: self.data.buffer.as_entire_binding(),
}],
label: Some("ui primitive data"),
}));
}
}
}
+159 -217
View File
@@ -1,68 +1,143 @@
use std::ops::{Deref, DerefMut};
use std::marker::PhantomData;
use crate::{
Color, UiRegion, WidgetId,
render::{
ArrBuf,
data::{MaskIdx, PrimitiveInstance},
},
render::data::{MaskIdx, PrimitiveInstance},
util::Vec2,
};
use bytemuck::Pod;
use wgpu::*;
/// Everything one layer draws. A texture binds its own group 2, so it draws on
/// its own and cannot join the instanced draw the primitives share.
pub struct LayerDraws {
pub primitives: Primitives,
pub textures: InstanceList,
pub updated: bool,
/// Which registered primitive an instance is, and so which list it lives in
/// and which pipeline draws it. The type ties a `write` to what its shader reads.
pub struct PrimitiveKind<P> {
id: u32,
_p: PhantomData<fn(P)>,
}
impl Default for LayerDraws {
fn default() -> Self {
impl<P> PrimitiveKind<P> {
const fn new(id: u32) -> Self {
Self {
primitives: Default::default(),
textures: Default::default(),
updated: true,
}
id,
_p: PhantomData,
}
}
/// Which of a layer's two lists an instance is in. They index independently,
/// so a handle or a renumbering naming only a position would be ambiguous.
pub fn id(&self) -> u32 {
self.id
}
}
impl<P> Clone for PrimitiveKind<P> {
fn clone(&self) -> Self {
*self
}
}
impl<P> Copy for PrimitiveKind<P> {}
pub const RECT: PrimitiveKind<RectPrimitive> = PrimitiveKind::new(0);
pub const GLYPH: PrimitiveKind<GlyphPrimitive> = PrimitiveKind::new(1);
/// Every primitive a ui can draw, in id order. Registering one is all the
/// wiring it needs: its list, buffers, free list and pipeline follow, and
/// nothing here knows its type.
///
/// A source is compiled after `prelude.wgsl` and supplies its data at
/// `@group(1) @binding(0)` and an `fs_main` shading one instance.
pub struct PrimitiveRegistry {
kinds: Vec<PrimitiveSource>,
}
pub struct PrimitiveSource {
pub wgsl: &'static str,
pub label: &'static str,
}
impl Default for PrimitiveRegistry {
fn default() -> Self {
let mut kinds = Self { kinds: Vec::new() };
assert_eq!(
kinds
.register::<RectPrimitive>(include_str!("shader/rect.wgsl"), "rect")
.id(),
RECT.id()
);
assert_eq!(
kinds
.register::<GlyphPrimitive>(include_str!("shader/glyph.wgsl"), "glyph")
.id(),
GLYPH.id()
);
kinds
}
}
impl PrimitiveRegistry {
pub fn register<P: Pod>(
&mut self,
wgsl: &'static str,
label: &'static str,
) -> PrimitiveKind<P> {
self.kinds.push(PrimitiveSource { wgsl, label });
PrimitiveKind::new(self.kinds.len() as u32 - 1)
}
pub fn sources(&self) -> &[PrimitiveSource] {
&self.kinds
}
}
/// Which of a layer's lists an instance is in. They index independently, so a
/// handle or a renumbering naming only a position would be ambiguous.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstanceKind {
Primitive,
Primitive(u32),
Texture,
}
/// A texture instance's `idx` names the texture to bind rather than a group-1
/// entry, so it is not in `primitives!` and has no buffer of its own.
pub const TEXTURE_BINDING: u32 = 2;
/// Instances, the widget each belongs to, and the slots waiting to be reused.
/// Instances, the widget each belongs to, the slots waiting to be reused, and
/// `stride` bytes of data per instance at the same index.
///
/// That data is the struct a primitive's shader reads, or the slot a texture
/// binds. Keeping both here is what holds them in step through a
/// `swap_remove`.
#[derive(Default)]
pub struct InstanceList {
instances: Vec<PrimitiveInstance>,
assoc: Vec<WidgetId>,
free: Vec<usize>,
data: Vec<u8>,
stride: usize,
}
impl InstanceList {
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
pub fn instances(&self) -> &[PrimitiveInstance] {
&self.instances
}
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance) -> usize {
pub fn data(&self) -> &[u8] {
&self.data
}
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> usize {
if self.instances.is_empty() {
self.stride = data.len();
}
debug_assert_eq!(
data.len(),
self.stride,
"primitive written to the wrong list"
);
if let Some(i) = self.free.pop() {
self.instances[i] = inst;
self.assoc[i] = id;
self.data[i * self.stride..][..self.stride].copy_from_slice(data);
i
} else {
let i = self.instances.len();
self.instances.push(inst);
self.assoc.push(id);
self.data.extend_from_slice(data);
i
}
}
@@ -76,156 +151,75 @@ impl InstanceList {
self.free.sort_by(|a, b| b.cmp(a));
let instances = &mut self.instances;
let assoc = &mut self.assoc;
let data = &mut self.data;
let stride = self.stride;
self.free.drain(..).filter_map(move |i| {
instances.swap_remove(i);
assoc.swap_remove(i);
if i == instances.len() {
let last = instances.len();
data.copy_within(last * stride..(last + 1) * stride, i * stride);
data.truncate(last * stride);
if i == last {
return None;
}
let id = assoc[i];
let old = instances.len();
Some(PrimitiveChange {
id,
kind,
old,
old: last,
new: i,
})
})
}
}
pub trait Primitive: Pod {
const BINDING: u32;
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>;
/// Everything one layer draws. A texture binds its own group 2, so it draws on
/// its own and cannot join the instanced draw a primitive gets.
pub struct LayerDraws {
primitives: Vec<InstanceList>,
pub textures: InstanceList,
pub updated: bool,
}
macro_rules! primitives {
($($name:ident: $ty:ty => $binding:expr,)*) => {
#[derive(Default)]
pub struct PrimitiveData {
$(pub(crate) $name: PrimitiveVec<$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 const BINDINGS: [u32; Self::LEN] = [$(<$ty>::BINDING,)*];
pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
[
$((<$ty>::BINDING, &self.$name.buffer),)*
]
}
pub fn new(device: &Device) -> Self {
impl Default for LayerDraws {
fn default() -> Self {
Self {
$($name: ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
stringify!($name),
),)*
primitives: Vec::new(),
textures: InstanceList::default(),
updated: true,
}
}
}
impl PrimitiveData {
pub fn clear(&mut self) {
$(self.$name.clear();)*
}
pub fn free(&mut self, binding: u32, idx: usize) {
match binding {
$(<$ty>::BINDING => self.$name.free(idx),)*
_ => unreachable!()
}
}
}
$(
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 PrimitiveVec<Self> {
&mut data.$name
}
}
)*
};
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t)+) };
(@count $t:tt) => { 1 };
}
pub struct PrimitiveInst<P> {
pub id: WidgetId,
pub primitive: P,
pub region: UiRegion,
pub mask_idx: MaskIdx,
}
/// The instanced half of a layer: a list, plus the group-1 data it reads.
#[derive(Default)]
pub struct Primitives {
list: InstanceList,
data: PrimitiveData,
}
impl Primitives {
fn write<P: Primitive>(
&mut self,
PrimitiveInst {
id,
primitive,
region,
mask_idx,
}: PrimitiveInst<P>,
) -> usize {
let idx = P::vec(&mut self.data).add(primitive) as u32;
self.list.push(
id,
PrimitiveInstance {
region,
idx,
mask_idx,
binding: P::BINDING,
},
)
}
fn free(&mut self, i: usize) -> MaskIdx {
// The instance says where its group-1 entry is, so the handle need not.
let inst = self.list.instances[i];
self.data.free(inst.binding, inst.idx as usize);
self.list.free(i)
}
pub fn data(&self) -> &PrimitiveData {
&self.data
}
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
&self.list.instances
}
}
impl LayerDraws {
pub fn write<P: Primitive>(&mut self, layer: usize, inst: PrimitiveInst<P>) -> PrimitiveHandle {
pub fn write<P: Pod>(
&mut self,
layer: usize,
kind: PrimitiveKind<P>,
id: WidgetId,
primitive: P,
region: UiRegion,
mask_idx: MaskIdx,
) -> PrimitiveHandle {
self.updated = true;
if self.primitives.len() <= kind.id as usize {
self.primitives
.resize_with(kind.id as usize + 1, Default::default);
}
let inst_idx = self.primitives[kind.id as usize].push(
id,
PrimitiveInstance { region, mask_idx },
bytemuck::bytes_of(&primitive),
);
PrimitiveHandle {
layer,
kind: InstanceKind::Primitive,
inst_idx: self.primitives.write(inst),
kind: InstanceKind::Primitive(kind.id),
inst_idx,
}
}
/// Writes an instance that samples the texture in slot `texture` instead of
/// reading a group-1 buffer.
/// Writes an instance that samples the texture in slot `texture`, which is
/// bound for it alone rather than read from a buffer.
pub fn write_texture(
&mut self,
layer: usize,
@@ -240,16 +234,16 @@ impl LayerDraws {
kind: InstanceKind::Texture,
inst_idx: self.textures.push(
id,
PrimitiveInstance {
region,
idx: texture,
mask_idx,
binding: TEXTURE_BINDING,
},
PrimitiveInstance { region, mask_idx },
bytemuck::bytes_of(&texture),
),
}
}
pub fn primitives(&self) -> &[InstanceList] {
&self.primitives
}
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
let Self {
primitives,
@@ -257,17 +251,15 @@ impl LayerDraws {
..
} = self;
primitives
.list
.apply_free(InstanceKind::Primitive)
.iter_mut()
.enumerate()
.flat_map(|(i, list)| list.apply_free(InstanceKind::Primitive(i as u32)))
.chain(textures.apply_free(InstanceKind::Texture))
}
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self.updated = true;
match h.kind {
InstanceKind::Primitive => self.primitives.free(h.inst_idx),
InstanceKind::Texture => self.textures.free(h.inst_idx),
}
self.list_mut(h.kind).free(h.inst_idx)
}
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
@@ -277,7 +269,7 @@ impl LayerDraws {
fn list_mut(&mut self, kind: InstanceKind) -> &mut InstanceList {
match kind {
InstanceKind::Primitive => &mut self.primitives.list,
InstanceKind::Primitive(i) => &mut self.primitives[i as usize],
InstanceKind::Texture => &mut self.textures,
}
}
@@ -297,11 +289,6 @@ pub struct PrimitiveHandle {
pub inst_idx: usize,
}
primitives!(
rects: RectPrimitive => 0,
glyphs: GlyphPrimitive => 1,
);
#[repr(C)]
#[derive(Copy, Clone)]
pub struct RectPrimitive {
@@ -311,6 +298,9 @@ pub struct RectPrimitive {
pub inner_radius: f32,
}
unsafe impl bytemuck::Pod for RectPrimitive {}
unsafe impl bytemuck::Zeroable for RectPrimitive {}
impl RectPrimitive {
pub fn color(color: Color<u8>) -> Self {
Self {
@@ -335,53 +325,5 @@ pub struct GlyphPrimitive {
pub flags: u32,
}
pub struct PrimitiveVec<T> {
vec: Vec<T>,
free: Vec<usize>,
}
impl<T> PrimitiveVec<T> {
pub fn new() -> Self {
Self {
vec: Vec::new(),
free: Vec::new(),
}
}
pub fn add(&mut self, t: T) -> usize {
if let Some(i) = self.free.pop() {
self.vec[i] = t;
i
} else {
let i = self.vec.len();
self.vec.push(t);
i
}
}
pub fn free(&mut self, i: usize) {
self.free.push(i);
}
pub fn clear(&mut self) {
self.free.clear();
self.vec.clear();
}
}
impl<T> Default for PrimitiveVec<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Deref for PrimitiveVec<T> {
type Target = Vec<T>;
fn deref(&self) -> &Self::Target {
&self.vec
}
}
impl<T> DerefMut for PrimitiveVec<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.vec
}
}
unsafe impl bytemuck::Pod for GlyphPrimitive {}
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
-198
View File
@@ -1,198 +0,0 @@
const RECT: u32 = 0u;
const GLYPH: u32 = 1u;
// No group 1 entry: a texture instance's idx names the texture bound in group 2.
const TEXTURE: u32 = 2u;
@group(0) @binding(0)
var<uniform> window: WindowUniform;
@group(1) @binding(RECT)
var<storage> rects: array<Rect>;
@group(1) @binding(GLYPH)
var<storage> glyphs: array<GlyphInfo>;
struct Rect {
color: u32,
radius: f32,
thickness: f32,
inner_radius: f32,
}
struct GlyphInfo {
uv_min: vec2<f32>,
uv_max: vec2<f32>,
layer: u32,
color: u32,
flags: u32,
}
struct Mask {
x: UiSpan,
y: UiSpan,
}
struct UiSpan {
start: UiScalar,
end: UiScalar,
}
struct UiScalar {
rel: f32,
abs: f32,
}
struct UiVec2 {
rel: vec2<f32>,
abs: vec2<f32>,
}
// What this draw samples: the glyph atlas, whose layers are its pages, or one
// standalone image as an array of one.
@group(2) @binding(0)
var tex: texture_2d_array<f32>;
@group(2) @binding(1)
var samp: sampler;
@group(3) @binding(0)
var<storage> masks: array<Mask>;
struct WindowUniform {
dim: vec2<f32>,
};
struct InstanceInput {
@location(0) x_start: vec2<f32>,
@location(1) x_end: vec2<f32>,
@location(2) y_start: vec2<f32>,
@location(3) y_end: vec2<f32>,
@location(4) binding: u32,
@location(5) idx: u32,
@location(6) mask_idx: u32,
}
struct VertexOutput {
@location(0) top_left: vec2<f32>,
@location(1) bot_right: vec2<f32>,
@location(2) uv: vec2<f32>,
@location(3) binding: u32,
@location(4) idx: u32,
@location(5) mask_idx: u32,
@builtin(position) clip_position: vec4<f32>,
};
struct Region {
pos: vec2<f32>,
uv: vec2<f32>,
top_left: vec2<f32>,
bot_right: vec2<f32>,
}
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
in: InstanceInput,
) -> VertexOutput {
var out: VertexOutput;
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
let size = bot_right - top_left;
let uv = vec2<f32>(
f32(vi % 2u),
f32(vi / 2u)
);
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
out.uv = uv;
out.binding = in.binding;
out.idx = in.idx;
out.top_left = top_left;
out.bot_right = bot_right;
out.mask_idx = in.mask_idx;
return out;
}
@fragment
fn fs_main(
in: VertexOutput
) -> @location(0) vec4<f32> {
let pos = in.clip_position.xy;
let region = Region(pos, in.uv, in.top_left, in.bot_right);
let i = in.idx;
var color: vec4<f32>;
switch in.binding {
case RECT: {
color = draw_rounded_rect(region, rects[i]);
}
case TEXTURE: {
color = draw_texture(region);
}
case GLYPH: {
color = draw_glyph(region, glyphs[i]);
}
default: {
color = vec4(1.0, 0.0, 1.0, 1.0);
}
}
if in.mask_idx != 4294967295u {
let mask = masks[in.mask_idx];
let tl = UiVec2(vec2(mask.x.start.rel, mask.y.start.rel), vec2(mask.x.start.abs, mask.y.start.abs));
let br = UiVec2(vec2(mask.x.end.rel, mask.y.end.rel), vec2(mask.x.end.abs, mask.y.end.abs));
let top_left = floor(tl.rel * window.dim) + floor(tl.abs);
let bot_right = floor(br.rel * window.dim) + floor(br.abs);
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
color *= 0.0;
}
}
return color;
}
fn draw_texture(region: Region) -> vec4<f32> {
return textureSample(tex, samp, region.uv, 0);
}
fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
let uv = mix(g.uv_min, g.uv_max, region.uv);
let texel = textureSample(tex, samp, uv, i32(g.layer));
if (g.flags & 1u) != 0u {
return texel;
}
var color = unpack4x8unorm(g.color);
color.a *= texel.a;
return color;
}
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
var color = unpack4x8unorm(rect.color);
let edge = 0.5;
let size = region.bot_right - region.top_left;
let corner = size / 2.0;
let center = region.top_left + corner;
let dist = distance_from_rect(region.pos, center, corner, rect.radius);
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
if rect.thickness > 0.0 {
let dist2 = distance_from_rect(region.pos, center, corner - rect.thickness, rect.inner_radius);
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
}
return color;
}
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
// vec from center to pixel
let p = pixel_pos - rect_center;
// vec from inner rect corner to pixel
let q = abs(p) - (rect_corner - radius);
return length(max(q, vec2(0.0))) - radius;
}
+24
View File
@@ -0,0 +1,24 @@
struct GlyphInfo {
uv_min: vec2<f32>,
uv_max: vec2<f32>,
// Which layer of the atlas array this glyph's page is.
layer: u32,
color: u32,
flags: u32,
}
@group(1) @binding(0)
var<storage> glyphs: array<GlyphInfo>;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let g = glyphs[in.idx];
let uv = mix(g.uv_min, g.uv_max, in.uv);
let texel = textureSample(tex, samp, uv, i32(g.layer));
if (g.flags & 1u) != 0u {
return masked(in, texel);
}
var color = unpack4x8unorm(g.color);
color.a *= texel.a;
return masked(in, color);
}
+102
View File
@@ -0,0 +1,102 @@
// Prepended to every primitive's shader, which supplies only its own data
// struct at group 1 and an `fs_main` that shades one instance.
@group(0) @binding(0)
var<uniform> window: WindowUniform;
@group(0) @binding(1)
var<storage> masks: array<Mask>;
// The texture this draw samples: the glyph atlas, whose layers are its pages,
// or one standalone image as an array of one.
@group(2) @binding(0)
var tex: texture_2d_array<f32>;
@group(2) @binding(1)
var samp: sampler;
struct WindowUniform {
dim: vec2<f32>,
};
struct Mask {
x: UiSpan,
y: UiSpan,
}
struct UiSpan {
start: UiScalar,
end: UiScalar,
}
struct UiScalar {
rel: f32,
abs: f32,
}
struct InstanceInput {
@location(0) x_start: vec2<f32>,
@location(1) x_end: vec2<f32>,
@location(2) y_start: vec2<f32>,
@location(3) y_end: vec2<f32>,
@location(4) mask_idx: u32,
}
struct VertexOutput {
@location(0) top_left: vec2<f32>,
@location(1) bot_right: vec2<f32>,
@location(2) uv: vec2<f32>,
@location(3) mask_idx: u32,
// The instance's own index, which is also where its data sits in group 1.
@location(4) @interpolate(flat) idx: u32,
@builtin(position) clip_position: vec4<f32>,
};
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
@builtin(instance_index) ii: u32,
in: InstanceInput,
) -> VertexOutput {
var out: VertexOutput;
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
let size = bot_right - top_left;
let uv = vec2<f32>(
f32(vi % 2u),
f32(vi / 2u)
);
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
out.uv = uv;
out.top_left = top_left;
out.bot_right = bot_right;
out.mask_idx = in.mask_idx;
out.idx = ii;
return out;
}
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
if in.mask_idx == 4294967295u {
return color;
}
let mask = masks[in.mask_idx];
let tl = vec2(mask.x.start.rel, mask.y.start.rel);
let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
let br = vec2(mask.x.end.rel, mask.y.end.rel);
let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
let top_left = floor(tl * window.dim) + floor(tl_abs);
let bot_right = floor(br * window.dim) + floor(br_abs);
let pos = in.clip_position.xy;
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
return color * 0.0;
}
return color;
}
+39
View File
@@ -0,0 +1,39 @@
struct Rect {
color: u32,
radius: f32,
thickness: f32,
inner_radius: f32,
}
@group(1) @binding(0)
var<storage> rects: array<Rect>;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let rect = rects[in.idx];
var color = unpack4x8unorm(rect.color);
let edge = 0.5;
let size = in.bot_right - in.top_left;
let corner = size / 2.0;
let center = in.top_left + corner;
let pos = in.clip_position.xy;
let dist = distance_from_rect(pos, center, corner, rect.radius);
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
if rect.thickness > 0.0 {
let dist2 = distance_from_rect(pos, center, corner - rect.thickness, rect.inner_radius);
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
}
return masked(in, color);
}
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
// vec from center to pixel
let p = pixel_pos - rect_center;
// vec from inner rect corner to pixel
let q = abs(p) - (rect_corner - radius);
return length(max(q, vec2(0.0))) - radius;
}
+7
View File
@@ -0,0 +1,7 @@
// No group 1: the texture is bound at group 2 for this instance alone, so
// there is nothing per-instance left to look up.
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return masked(in, textureSample(tex, samp, in.uv, 0));
}
+5 -1
View File
@@ -39,7 +39,11 @@ impl<T: Pod> ArrBuf<T> {
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) {
size = size.max(std::mem::size_of::<T>() as u64);
// An empty storage buffer is still bound, and a binding has to be
// non-empty and a multiple of four however small `T` is.
size = size
.max(std::mem::size_of::<T>() as u64)
.next_multiple_of(4);
}
device.create_buffer(&BufferDescriptor {
label: Some(label),
+5 -1
View File
@@ -1,4 +1,6 @@
use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena};
use crate::{
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
};
mod active;
mod cache;
@@ -14,6 +16,8 @@ pub use size::*;
#[derive(Default)]
pub struct UiData {
pub widgets: Widgets,
/// Every primitive this ui can draw.
pub primitives: PrimitiveRegistry,
pub textures: Textures,
pub text: TextData,
pub masks: TrackedArena<Mask, u32>,
+18 -15
View File
@@ -1,7 +1,9 @@
use bytemuck::Pod;
use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
render::{GLYPH, GlyphPrimitive, Mask, MaskIdx, PrimitiveHandle, PrimitiveKind},
util::Vec2,
};
@@ -20,16 +22,11 @@ pub struct Painter<'a> {
}
impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let h = self.state.layers.write(
self.layer,
PrimitiveInst {
id: self.id,
primitive,
region,
mask_idx: self.mask,
},
);
fn primitive_at<P: Pod>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
let h = self
.state
.layers
.write(self.layer, kind, self.id, primitive, region, self.mask);
self.push_primitive(h);
}
@@ -42,12 +39,17 @@ impl<'a> Painter<'a> {
}
/// Writes a primitive to be rendered
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
self.primitive_at(primitive, self.region)
pub fn primitive<P: Pod>(&mut self, kind: PrimitiveKind<P>, primitive: P) {
self.primitive_at(kind, primitive, self.region)
}
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
self.primitive_at(primitive, region.within(&self.region));
pub fn primitive_within<P: Pod>(
&mut self,
kind: PrimitiveKind<P>,
primitive: P,
region: UiRegion,
) {
self.primitive_at(kind, primitive, region.within(&self.region));
}
pub fn set_mask(&mut self, region: UiRegion) {
@@ -115,6 +117,7 @@ impl<'a> Painter<'a> {
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
self.primitive_at(
GLYPH,
GlyphPrimitive {
uv_min: glyph.entry.uv_min,
uv_max: glyph.entry.uv_max,
+1 -1
View File
@@ -245,7 +245,7 @@ impl UiRenderState {
pub fn debug_layers(&self) {
for ((idx, depth), draws) in self.layers.iter_depth() {
let indent = " ".repeat(depth * 2);
let primitives = draws.primitives.instances().len();
let primitives: usize = draws.primitives().iter().map(|l| l.instances().len()).sum();
let textures = draws.textures.instances().len();
println!("{indent}{idx}: {primitives} primitives, {textures} textures");
}
+5 -2
View File
@@ -29,12 +29,15 @@ impl Rect {
impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) {
painter.primitive(RectPrimitive {
painter.primitive(
RECT,
RectPrimitive {
color: self.color,
radius: self.radius,
thickness: self.thickness,
inner_radius: self.inner_radius,
});
},
);
}
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
+2
View File
@@ -73,6 +73,7 @@ impl Widget for TextEdit {
let size = vec2(rect.width() as f32, rect.height() as f32);
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
painter.primitive_within(
RECT,
RectPrimitive::color(Color::SKY),
size.align(Align::TOP_LEFT).offset(top_left).within(&region),
);
@@ -82,6 +83,7 @@ impl Widget for TextEdit {
let size = vec2(caret.width() as f32, caret.height() as f32);
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
painter.primitive_within(
RECT,
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(top_left).within(&region),
);