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:
1 parent
b3d3da5dab
commit
4d9839f380
15 files changed
+608
-614
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 ®istry.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"),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
+158
-216
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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 {}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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(®ion),
|
||||
);
|
||||
@@ -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(®ion),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user