Take a primitive's kind from its type, and keep images out of the rest

A `Primitive` now carries its own WGSL, and `PrimitiveRegistry` keys ids by
`TypeId`, so `Painter::primitive` takes only the value and the `RECT`,
`GLYPH` and `TEXTURE` constants are gone. Registering is what a first draw
does; the built-ins are seeded up front so first-draw order cannot decide
anything about them.

What a primitive samples is no longer something every registration states.
The glyph atlas and the one sampler moved into the shared group, which is
where a mask texture would go too, so a rect's pipeline has no texture in
its layout at all. Only a primitive whose type sets `TEXTURE` gets an image
group, and that is also what records the slot at write time -- so nothing
reads a `u32` back out of the instance payload.

Verified on the headless rig: the tabs example, two images added at runtime,
an image alone in a layer, and text spanning a four-layer atlas after the
array grew twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-13 16:14:53 -04:00
1 parent 7b318e3271
commit 29d390da52
11 files changed
+244 -309

No files matched your search

+84 -103
View File
@@ -30,7 +30,7 @@ const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
pub struct UiRenderNode { pub struct UiRenderNode {
shared_layout: BindGroupLayout, shared_layout: BindGroupLayout,
shared_group: BindGroup, shared_group: BindGroup,
texture_layout: BindGroupLayout, image_layout: BindGroupLayout,
format: TextureFormat, format: TextureFormat,
/// One per registered primitive, in id order. /// One per registered primitive, in id order.
@@ -46,26 +46,22 @@ pub struct UiRenderNode {
} }
struct RenderLayer { struct RenderLayer {
/// One per registered primitive, in id order, matching `LayerDraws` -- /// One per registered primitive, `None` where this layer draws none.
/// `None` where this layer draws none, so a primitive nobody uses costs no
/// buffers per layer.
primitives: Vec<Option<ListBuffers>>, primitives: Vec<Option<ListBuffers>>,
} }
/// What draws one registered primitive. The group 1 layout is its own rather /// What draws one registered primitive.
/// than shared, so its entry size is the minimum only for it.
struct PrimitivePipeline { struct PrimitivePipeline {
data_layout: BindGroupLayout, data_layout: BindGroupLayout,
pipeline: RenderPipeline, pipeline: RenderPipeline,
texture: PrimitiveTexture,
} }
/// One list's vertex buffer and the data its shader reads at group 1. /// One list's vertex buffer and the data its shader reads.
struct ListBuffers { struct ListBuffers {
instance: ArrBuf<PrimitiveInstance>, instance: ArrBuf<PrimitiveInstance>,
data: ArrBuf<u8>, data: ArrBuf<u8>,
group: Option<BindGroup>, group: Option<BindGroup>,
/// For a `PerInstance` primitive, the texture each instance binds. /// The image each instance binds. Empty unless the primitive is textured.
slots: Vec<u32>, slots: Vec<u32>,
} }
@@ -74,41 +70,28 @@ impl UiRenderNode {
pass.set_bind_group(0, &self.shared_group, &[]); pass.set_bind_group(0, &self.shared_group, &[]);
for i in &self.active { for i in &self.active {
let layer = &self.layers[i]; let layer = &self.layers[i];
// 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.
for (id, list) in layer.primitives.iter().enumerate() { for (id, list) in layer.primitives.iter().enumerate() {
let Some(list) = list else { let Some(list) = list else { continue };
continue; let Some(group) = &list.group else { continue };
}; pass.set_pipeline(&self.primitives[id].pipeline);
let Some(group) = &list.group else { // After the pipeline: a change drops the groups from where two
continue; // pipeline layouts differ, and each primitive has its own.
};
let primitive = &self.primitives[id];
pass.set_pipeline(&primitive.pipeline);
// Both groups after the pipeline: each primitive has its own
// pipeline layout, and a change drops the groups from where
// the two layouts differ.
pass.set_bind_group(1, group, &[]); pass.set_bind_group(1, group, &[]);
pass.set_vertex_buffer(0, list.instance.buffer.slice(..)); pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
match primitive.texture { if list.slots.is_empty() {
PrimitiveTexture::Atlas => {
pass.set_bind_group(2, self.pages.group(), &[]);
pass.draw(0..4, 0..list.instance.len() as u32); pass.draw(0..4, 0..list.instance.len() as u32);
continue;
} }
PrimitiveTexture::PerInstance => {
for (i, &slot) in list.slots.iter().enumerate() { for (i, &slot) in list.slots.iter().enumerate() {
let Some(texture) = self.textures.group(slot) else { let Some(image) = self.textures.group(slot) else {
continue; continue;
}; };
pass.set_bind_group(2, texture, &[]); pass.set_bind_group(2, image, &[]);
pass.draw(0..4, i as u32..i as u32 + 1); pass.draw(0..4, i as u32..i as u32 + 1);
} }
} }
} }
} }
}
}
pub fn update( pub fn update(
&mut self, &mut self,
@@ -117,9 +100,7 @@ impl UiRenderNode {
ui: &mut UiData, ui: &mut UiData,
ui_render: &mut UiRenderState, ui_render: &mut UiRenderState,
) { ) {
// Before the layers: a list is given the bind group layout of the // Before the layers: each list is given its pipeline's data layout.
// pipeline that will draw it, so every registered primitive needs one
// by the time a layer is reached.
self.build_pipelines(device, &ui.primitives); self.build_pipelines(device, &ui.primitives);
self.active.clear(); self.active.clear();
for (i, draws) in ui_render.layers.iter_mut() { for (i, draws) in ui_render.layers.iter_mut() {
@@ -150,35 +131,30 @@ impl UiRenderNode {
}; };
// Indexed, not zipped: a missing pipeline should say so // Indexed, not zipped: a missing pipeline should say so
// rather than quietly leave the list unbuilt. // rather than quietly leave the list unbuilt.
let primitive = &self.primitives[id]; let layout = &self.primitives[id].data_layout;
buffers buffers
.get_or_insert_with(|| ListBuffers::new(device)) .get_or_insert_with(|| ListBuffers::new(device))
.update( .update(device, queue, layout, list);
device,
queue,
primitive.texture,
&primitive.data_layout,
list,
);
} }
draws.updated = false; draws.updated = false;
} }
} }
let mut shared_stale = self.pages.update(&mut ui.text.atlas);
if ui.masks.changed { if ui.masks.changed {
ui.masks.changed = false; ui.masks.changed = false;
if self.masks.update(device, queue, &ui.masks[..]) { shared_stale |= self.masks.update(device, queue, &ui.masks[..]);
}
if shared_stale {
self.shared_group = Self::shared_group( self.shared_group = Self::shared_group(
device, device,
&self.shared_layout, &self.shared_layout,
&self.window_buffer, &self.window_buffer,
&self.masks, &self.masks,
&self.pages,
&self.sampler,
); );
} }
} self.textures.update(&mut ui.textures, &self.image_layout);
self.pages
.update(&mut ui.text.atlas, &self.texture_layout, &self.sampler);
self.textures
.update(&mut ui.textures, &self.texture_layout, &self.sampler);
} }
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) { pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
@@ -202,23 +178,30 @@ impl UiRenderNode {
}); });
let shared_layout = Self::shared_layout(device); let shared_layout = Self::shared_layout(device);
let texture_layout = Self::texture_layout(device); let image_layout = Self::image_layout(device);
let masks = ArrBuf::new( let masks = ArrBuf::new(
device, device,
BufferUsages::STORAGE | BufferUsages::COPY_DST, BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks", "ui masks",
); );
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
let sampler = default_sampler(device); let sampler = default_sampler(device);
let pages = GpuPages::new(device, queue, &texture_layout, &sampler); let pages = GpuPages::new(device, queue);
let textures = GpuTextures::new(device, queue); let textures = GpuTextures::new(device, queue);
let shared_group = Self::shared_group(
device,
&shared_layout,
&window_buffer,
&masks,
&pages,
&sampler,
);
Self { Self {
shared_layout, shared_layout,
shared_group, shared_group,
texture_layout, image_layout,
format: config.format, format: config.format,
primitives: Vec::new(), primitives: Vec::new(),
window_buffer, window_buffer,
@@ -236,34 +219,23 @@ impl UiRenderNode {
fn build_pipelines(&mut self, device: &Device, registry: &PrimitiveRegistry) { fn build_pipelines(&mut self, device: &Device, registry: &PrimitiveRegistry) {
for source in &registry.sources()[self.primitives.len()..] { for source in &registry.sources()[self.primitives.len()..] {
let data_layout = Self::data_layout(device, source.stride); let data_layout = Self::data_layout(device, source.stride);
let layout = Self::pipeline_layout( let mut groups = vec![&self.shared_layout, &data_layout];
device, if source.textured {
&self.shared_layout, groups.push(&self.image_layout);
&data_layout, }
&self.texture_layout, let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
); label: Some(source.label),
bind_group_layouts: &groups,
immediate_size: 0,
});
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label); let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
self.primitives.push(PrimitivePipeline { self.primitives.push(PrimitivePipeline {
data_layout, data_layout,
pipeline, pipeline,
texture: source.texture,
}); });
} }
} }
fn pipeline_layout(
device: &Device,
shared: &BindGroupLayout,
data: &BindGroupLayout,
texture: &BindGroupLayout,
) -> PipelineLayout {
device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("ui"),
bind_group_layouts: &[shared, data, texture],
immediate_size: 0,
})
}
fn pipeline( fn pipeline(
device: &Device, device: &Device,
layout: &PipelineLayout, layout: &PipelineLayout,
@@ -314,7 +286,8 @@ impl UiRenderNode {
}) })
} }
/// Group 0: what every draw in the ui shares. /// What every draw in the ui is given, whether or not its shader reads it:
/// the window, the masks, the glyph atlas and the one sampler.
fn shared_layout(device: &Device) -> BindGroupLayout { fn shared_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[
@@ -338,6 +311,22 @@ impl UiRenderNode {
}, },
count: None, count: None,
}, },
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2Array,
multisampled: false,
},
count: None,
},
BindGroupLayoutEntry {
binding: 3,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: None,
},
], ],
label: Some("ui shared"), label: Some("ui shared"),
}) })
@@ -348,6 +337,8 @@ impl UiRenderNode {
layout: &BindGroupLayout, layout: &BindGroupLayout,
window: &Buffer, window: &Buffer,
masks: &ArrBuf<Mask>, masks: &ArrBuf<Mask>,
pages: &GpuPages,
sampler: &Sampler,
) -> BindGroup { ) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor { device.create_bind_group(&BindGroupDescriptor {
layout, layout,
@@ -360,16 +351,22 @@ impl UiRenderNode {
binding: 1, binding: 1,
resource: masks.buffer.as_entire_binding(), resource: masks.buffer.as_entire_binding(),
}, },
BindGroupEntry {
binding: 2,
resource: BindingResource::TextureView(pages.view()),
},
BindGroupEntry {
binding: 3,
resource: BindingResource::Sampler(sampler),
},
], ],
label: Some("ui shared"), label: Some("ui shared"),
}) })
} }
/// Group 1: one list's per-instance data, whatever its shader reads it as. /// One list's per-instance data. Per primitive with `stride` stated: a
/// /// `None` minimum is filled in from the first pipeline built against the
/// One per primitive with `stride` stated, because a `None` minimum is /// layout, so a shared one would hold every primitive to the largest.
/// filled in from the first pipeline built against the layout -- sharing
/// one would hold every primitive to the largest.
fn data_layout(device: &Device, stride: u64) -> BindGroupLayout { fn data_layout(device: &Device, stride: u64) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry { entries: &[BindGroupLayoutEntry {
@@ -386,29 +383,21 @@ impl UiRenderNode {
}) })
} }
/// Group 2: the texture this draw samples, and the sampler. No `count` on /// The one image an instance samples. The sampler is shared, so there is
/// either entry -- plain Vulkan 1.0 / GLES sampling is all this needs. /// nothing else in here.
fn texture_layout(device: &Device) -> BindGroupLayout { fn image_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[BindGroupLayoutEntry {
BindGroupLayoutEntry {
binding: 0, binding: 0,
visibility: ShaderStages::FRAGMENT, visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture { ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false }, sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2Array, view_dimension: TextureViewDimension::D2,
multisampled: false, multisampled: false,
}, },
count: None, count: None,
}, }],
BindGroupLayoutEntry { label: Some("ui image"),
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: None,
},
],
label: Some("ui texture"),
}) })
} }
@@ -447,22 +436,14 @@ impl ListBuffers {
&mut self, &mut self,
device: &Device, device: &Device,
queue: &Queue, queue: &Queue,
texture: PrimitiveTexture,
layout: &BindGroupLayout, layout: &BindGroupLayout,
list: &InstanceList, list: &InstanceList,
) { ) {
if texture == PrimitiveTexture::PerInstance {
self.slots.clear(); self.slots.clear();
// Read rather than cast: the payload is a byte vec, so it carries self.slots.extend_from_slice(list.slots());
// no alignment a `u32` slice could borrow.
let slots = list.data().as_chunks::<4>().0;
self.slots
.extend(slots.iter().copied().map(u32::from_ne_bytes));
}
self.instance.update(device, queue, list.instances()); self.instance.update(device, queue, list.instances());
let resized = self.data.update(device, queue, list.data()); let resized = self.data.update(device, queue, list.data());
// An empty list has no buffer big enough for one entry, and nothing // An empty list has no buffer big enough to bind, and nothing to draw.
// draws it, so it has no bind group either.
if list.instances().is_empty() { if list.instances().is_empty() {
self.group = None; self.group = None;
} else if resized || self.group.is_none() { } else if resized || self.group.is_none() {
+21 -21
View File
@@ -2,10 +2,7 @@ use wgpu::*;
use crate::GlyphAtlas; use crate::GlyphAtlas;
use super::{ use super::{atlas::PAGE, texture::write_region};
atlas::PAGE,
texture::{array_view, texture_group, write_region},
};
/// The glyph atlas on the GPU: one array texture whose layers are the pages /// The glyph atlas on the GPU: one array texture whose layers are the pages
/// `GlyphAtlas` packs. /// `GlyphAtlas` packs.
@@ -17,29 +14,25 @@ pub struct GpuPages {
device: Device, device: Device,
queue: Queue, queue: Queue,
texture: Texture, texture: Texture,
group: BindGroup, view: TextureView,
} }
impl GpuPages { impl GpuPages {
pub fn new( pub fn new(device: &Device, queue: &Queue) -> Self {
device: &Device,
queue: &Queue,
layout: &BindGroupLayout,
sampler: &Sampler,
) -> Self {
let texture = create_array(device, 1); let texture = create_array(device, 1);
let group = texture_group(device, layout, &array_view(&texture), sampler);
Self { Self {
device: device.clone(), device: device.clone(),
queue: queue.clone(), queue: queue.clone(),
view: array_view(&texture),
texture, texture,
group,
} }
} }
pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) { /// Returns whether the array was replaced, which stales the view.
if atlas.page_count() > self.texture.depth_or_array_layers() { pub fn update(&mut self, atlas: &mut GlyphAtlas) -> bool {
self.grow(atlas.page_count(), layout, sampler); let grew = atlas.page_count() > self.texture.depth_or_array_layers();
if grew {
self.grow(atlas.page_count());
} }
for (upload, page) in atlas.uploads() { for (upload, page) in atlas.uploads() {
let dst = TexelCopyTextureInfo { let dst = TexelCopyTextureInfo {
@@ -54,15 +47,15 @@ impl GpuPages {
}; };
write_region(&self.queue, dst, page, upload.rect); write_region(&self.queue, dst, page, upload.rect);
} }
grew
} }
pub fn group(&self) -> &BindGroup { pub fn view(&self) -> &TextureView {
&self.group &self.view
} }
/// Doubles until `needed` fits and copies the old layers across GPU side. /// Doubles until `needed` fits and copies the old layers across GPU side.
/// The new view invalidates the old group, so that is rebuilt here. fn grow(&mut self, needed: u32) {
fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) {
let old = self.texture.depth_or_array_layers(); let old = self.texture.depth_or_array_layers();
let mut layers = old; let mut layers = old;
while layers < needed { while layers < needed {
@@ -84,11 +77,18 @@ impl GpuPages {
}, },
); );
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
self.group = texture_group(&self.device, layout, &array_view(&texture), sampler); self.view = array_view(&texture);
self.texture = texture; self.texture = texture;
} }
} }
fn array_view(texture: &Texture) -> TextureView {
texture.create_view(&TextureViewDescriptor {
dimension: Some(TextureViewDimension::D2Array),
..Default::default()
})
}
fn create_array(device: &Device, layers: u32) -> Texture { fn create_array(device: &Device, layers: u32) -> Texture {
device.create_texture(&TextureDescriptor { device.create_texture(&TextureDescriptor {
label: Some("glyph atlas"), label: Some("glyph atlas"),
+86 -92
View File
@@ -1,38 +1,39 @@
use std::marker::PhantomData; use std::{any::TypeId, marker::PhantomData};
use crate::{ use crate::{
Color, UiRegion, WidgetId, Color, UiRegion, WidgetId,
render::data::{MaskIdx, PrimitiveInstance}, render::data::{MaskIdx, PrimitiveInstance},
util::Vec2, util::{HashMap, Vec2},
}; };
use bytemuck::Pod; use bytemuck::Pod;
/// One instance of a registered primitive, laid out as the struct that /// One instance of a primitive, laid out as the struct its shader reads.
/// primitive's shader reads at `@group(1) @binding(0)`.
/// ///
/// A `PrimitiveKind<P>` is only minted by `register::<P>`, and `write` takes /// The type carries its own shader, so drawing one is all the wiring it needs:
/// the kind and the value together, so holding one is the proof that `P` has a /// its list, free list, buffers and pipeline follow from being registered.
/// list of its own to go in and a write needs no check. pub trait Primitive: Pod + 'static {
pub trait Primitive: Pod {} /// Compiled after `prelude.wgsl`, which states what it declares and what
/// it is given.
const WGSL: &'static str;
/// Reads the image an instance samples, for a primitive that draws one.
/// Each instance is then a draw of its own, bound for it alone.
const TEXTURE: Option<fn(&Self) -> u32> = None;
}
/// Which registered primitive an instance is, and so which list it lives in /// Which registered primitive an instance is. Only `PrimitiveRegistry::kind`
/// and which pipeline draws it. The type ties a `write` to what its shader reads. /// mints one, so holding it is the proof that `P` has a list to go in.
pub struct PrimitiveKind<P> { pub struct PrimitiveKind<P> {
id: u32, id: u32,
_p: PhantomData<fn(P)>, _p: PhantomData<fn(P)>,
} }
impl<P> PrimitiveKind<P> { impl<P> PrimitiveKind<P> {
const fn new(id: u32) -> Self { fn new(id: u32) -> Self {
Self { Self {
id, id,
_p: PhantomData, _p: PhantomData,
} }
} }
pub fn id(&self) -> u32 {
self.id
}
} }
impl<P> Clone for PrimitiveKind<P> { impl<P> Clone for PrimitiveKind<P> {
@@ -43,80 +44,50 @@ impl<P> Clone for PrimitiveKind<P> {
impl<P> Copy for PrimitiveKind<P> {} impl<P> Copy for PrimitiveKind<P> {}
pub const RECT: PrimitiveKind<RectPrimitive> = PrimitiveKind::new(0); /// Every primitive a ui can draw, in the order they were first drawn.
pub const GLYPH: PrimitiveKind<GlyphPrimitive> = PrimitiveKind::new(1);
pub const TEXTURE: PrimitiveKind<TexturePrimitive> = PrimitiveKind::new(2);
/// 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.
///
/// Order is draw order within a layer, so a primitive registered later is
/// drawn over one registered earlier.
pub struct PrimitiveRegistry { pub struct PrimitiveRegistry {
kinds: Vec<PrimitiveSource>, kinds: Vec<PrimitiveSource>,
ids: HashMap<TypeId, u32>,
} }
pub struct PrimitiveSource { pub struct PrimitiveSource {
pub wgsl: &'static str, pub wgsl: &'static str,
pub label: &'static str, pub label: &'static str,
/// Size of one instance's entry, which the renderer states as the group 1 /// Size of one instance's entry, stated as the data binding's minimum.
/// binding's minimum rather than leaving it to be inferred.
pub stride: u64, pub stride: u64,
pub texture: PrimitiveTexture, /// Whether an instance binds a texture of its own, from `P::TEXTURE`.
} pub textured: bool,
/// What a primitive samples at group 2, which is the whole of why some of them
/// cannot share one instanced draw.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrimitiveTexture {
/// The shared glyph atlas, bound once for the layer.
Atlas,
/// The `Textures` slot in the first four bytes of its own data, bound for
/// that instance alone -- so one draw call each.
PerInstance,
} }
impl Default for PrimitiveRegistry { impl Default for PrimitiveRegistry {
/// The built-ins are registered up front rather than on first use, so that
/// what a ui happens to draw first cannot decide anything about them.
fn default() -> Self { fn default() -> Self {
use PrimitiveTexture::*; let mut registry = Self {
let mut registry = Self { kinds: Vec::new() }; kinds: Vec::new(),
let rect = ids: HashMap::default(),
registry.register::<RectPrimitive>(include_str!("shader/rect.wgsl"), "rect", Atlas); };
let glyph = registry.kind::<RectPrimitive>();
registry.register::<GlyphPrimitive>(include_str!("shader/glyph.wgsl"), "glyph", Atlas); registry.kind::<GlyphPrimitive>();
let texture = registry.register::<TexturePrimitive>( registry.kind::<TexturePrimitive>();
include_str!("shader/texture.wgsl"),
"texture",
PerInstance,
);
// The built-ins have constant ids so a widget can name one without the
// registry; registering them first is what makes those constants true.
assert_eq!(
(rect.id(), glyph.id(), texture.id()),
(RECT.id(), GLYPH.id(), TEXTURE.id())
);
registry registry
} }
} }
impl PrimitiveRegistry { impl PrimitiveRegistry {
pub fn register<P: Primitive>( /// Registers `P` if this is the first time it has been drawn.
&mut self, pub fn kind<P: Primitive>(&mut self) -> PrimitiveKind<P> {
wgsl: &'static str, let Self { kinds, ids } = self;
label: &'static str, let id = *ids.entry(TypeId::of::<P>()).or_insert_with(|| {
texture: PrimitiveTexture, kinds.push(PrimitiveSource {
) -> PrimitiveKind<P> { wgsl: P::WGSL,
self.kinds.push(PrimitiveSource { label: std::any::type_name::<P>(),
wgsl,
label,
stride: size_of::<P>() as u64, stride: size_of::<P>() as u64,
texture, textured: P::TEXTURE.is_some(),
}); });
PrimitiveKind::new(self.kinds.len() as u32 - 1) kinds.len() as u32 - 1
});
PrimitiveKind::new(id)
} }
pub fn sources(&self) -> &[PrimitiveSource] { pub fn sources(&self) -> &[PrimitiveSource] {
@@ -124,18 +95,18 @@ impl PrimitiveRegistry {
} }
} }
/// One registered primitive's instances in one layer: the instances, the /// One registered primitive's instances in one layer. Everything per-instance
/// widget each belongs to, the slots waiting to be reused, and `stride` bytes /// rides here, so it stays in step through a `swap_remove`.
/// of that primitive's data per instance at the same index.
///
/// `stride` comes from the type the list was made for, so a write is never
/// checked against it. The data rides here rather than beside the list so the
/// two stay in step through a `swap_remove`.
pub struct InstanceList { pub struct InstanceList {
instances: Vec<PrimitiveInstance>, instances: Vec<PrimitiveInstance>,
/// The widget each instance belongs to, for renumbering its handles.
assoc: Vec<WidgetId>, assoc: Vec<WidgetId>,
free: Vec<usize>, free: Vec<usize>,
/// `stride` bytes of the primitive's own data per instance.
data: Vec<u8>, data: Vec<u8>,
/// The image each instance samples, from `P::TEXTURE`. Empty without it.
slots: Vec<u32>,
/// From the type the list was made for, so a write is never checked.
stride: usize, stride: usize,
} }
@@ -146,6 +117,7 @@ impl InstanceList {
assoc: Vec::new(), assoc: Vec::new(),
free: Vec::new(), free: Vec::new(),
data: Vec::new(), data: Vec::new(),
slots: Vec::new(),
stride: size_of::<P>(), stride: size_of::<P>(),
} }
} }
@@ -158,17 +130,31 @@ impl InstanceList {
&self.data &self.data
} }
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> usize { pub fn slots(&self) -> &[u32] {
&self.slots
}
fn push(
&mut self,
id: WidgetId,
inst: PrimitiveInstance,
data: &[u8],
slot: Option<u32>,
) -> usize {
if let Some(i) = self.free.pop() { if let Some(i) = self.free.pop() {
self.instances[i] = inst; self.instances[i] = inst;
self.assoc[i] = id; self.assoc[i] = id;
self.data[i * self.stride..][..self.stride].copy_from_slice(data); self.data[i * self.stride..][..self.stride].copy_from_slice(data);
if let Some(slot) = slot {
self.slots[i] = slot;
}
i i
} else { } else {
let i = self.instances.len(); let i = self.instances.len();
self.instances.push(inst); self.instances.push(inst);
self.assoc.push(id); self.assoc.push(id);
self.data.extend_from_slice(data); self.data.extend_from_slice(data);
self.slots.extend(slot);
i i
} }
} }
@@ -183,10 +169,14 @@ impl InstanceList {
let instances = &mut self.instances; let instances = &mut self.instances;
let assoc = &mut self.assoc; let assoc = &mut self.assoc;
let data = &mut self.data; let data = &mut self.data;
let slots = &mut self.slots;
let stride = self.stride; let stride = self.stride;
self.free.drain(..).filter_map(move |i| { self.free.drain(..).filter_map(move |i| {
instances.swap_remove(i); instances.swap_remove(i);
assoc.swap_remove(i); assoc.swap_remove(i);
if !slots.is_empty() {
slots.swap_remove(i);
}
let last = instances.len(); let last = instances.len();
data.copy_within(last * stride..(last + 1) * stride, i * stride); data.copy_within(last * stride..(last + 1) * stride, i * stride);
data.truncate(last * stride); data.truncate(last * stride);
@@ -204,9 +194,7 @@ impl InstanceList {
} }
} }
/// Everything one layer draws, one list per registered primitive. They index /// Everything one layer draws, one list per registered primitive.
/// independently, so a handle or a renumbering naming only a position would be
/// ambiguous between them.
pub struct LayerDraws { pub struct LayerDraws {
/// `None` until this layer draws that primitive, because only the write /// `None` until this layer draws that primitive, because only the write
/// knows the type the list is for. /// knows the type the list is for.
@@ -247,6 +235,7 @@ impl LayerDraws {
id, id,
PrimitiveInstance { region, mask_idx }, PrimitiveInstance { region, mask_idx },
bytemuck::bytes_of(&primitive), bytemuck::bytes_of(&primitive),
P::TEXTURE.map(|slot| slot(&primitive)),
); );
PrimitiveHandle { PrimitiveHandle {
layer, layer,
@@ -309,7 +298,7 @@ pub struct PrimitiveHandle {
} }
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RectPrimitive { pub struct RectPrimitive {
pub color: Color<u8>, pub color: Color<u8>,
pub radius: f32, pub radius: f32,
@@ -317,9 +306,9 @@ pub struct RectPrimitive {
pub inner_radius: f32, pub inner_radius: f32,
} }
unsafe impl bytemuck::Pod for RectPrimitive {} impl Primitive for RectPrimitive {
unsafe impl bytemuck::Zeroable for RectPrimitive {} const WGSL: &'static str = include_str!("shader/rect.wgsl");
impl Primitive for RectPrimitive {} }
impl RectPrimitive { impl RectPrimitive {
pub fn color(color: Color<u8>) -> Self { pub fn color(color: Color<u8>) -> Self {
@@ -345,18 +334,23 @@ pub struct GlyphPrimitive {
pub flags: u32, pub flags: u32,
} }
// Manual rather than derived: the align(8) leaves four bytes of padding, which
// is how WGSL lays the struct out.
unsafe impl bytemuck::Pod for GlyphPrimitive {} unsafe impl bytemuck::Pod for GlyphPrimitive {}
unsafe impl bytemuck::Zeroable for GlyphPrimitive {} unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
impl Primitive for GlyphPrimitive {} impl Primitive for GlyphPrimitive {
const WGSL: &'static str = include_str!("shader/glyph.wgsl");
}
/// One drawn image. Its shader reads nothing: the slot names the texture bound /// One drawn image. Its shader reads nothing per instance; the slot names the
/// for this instance alone, which is what `PrimitiveTexture::PerInstance` does. /// texture to bind for it.
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TexturePrimitive { pub struct TexturePrimitive {
pub slot: u32, pub slot: u32,
} }
unsafe impl bytemuck::Pod for TexturePrimitive {} impl Primitive for TexturePrimitive {
unsafe impl bytemuck::Zeroable for TexturePrimitive {} const WGSL: &'static str = include_str!("shader/texture.wgsl");
impl Primitive for TexturePrimitive {} const TEXTURE: Option<fn(&Self) -> u32> = Some(|texture| texture.slot);
}
+5 -2
View File
@@ -1,3 +1,6 @@
// Matches `GlyphEntry::IS_COLORED`.
const COLORED: u32 = 1u;
struct GlyphInfo { struct GlyphInfo {
uv_min: vec2<f32>, uv_min: vec2<f32>,
uv_max: vec2<f32>, uv_max: vec2<f32>,
@@ -14,8 +17,8 @@ var<storage> glyphs: array<GlyphInfo>;
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let g = glyphs[in.idx]; let g = glyphs[in.idx];
let uv = mix(g.uv_min, g.uv_max, in.uv); let uv = mix(g.uv_min, g.uv_max, in.uv);
let texel = textureSample(tex, samp, uv, i32(g.layer)); let texel = textureSample(atlas, samp, uv, i32(g.layer));
if (g.flags & 1u) != 0u { if (g.flags & COLORED) != 0u {
return masked(in, texel); return masked(in, texel);
} }
var color = unpack4x8unorm(g.color); var color = unpack4x8unorm(g.color);
+8 -9
View File
@@ -1,16 +1,16 @@
// Prepended to every primitive's shader, which supplies only its own data // Prepended to every primitive's shader, which declares its own instance data
// struct at group 1 and an `fs_main` that shades one instance. // as `var<storage> <name>: array<T>` at group 1 binding 0, and an `fs_main`
// shading one instance of it. A primitive that samples an image of its own
// takes it at group 2 binding 0; see texture.wgsl.
@group(0) @binding(0) @group(0) @binding(0)
var<uniform> window: WindowUniform; var<uniform> window: WindowUniform;
@group(0) @binding(1) @group(0) @binding(1)
var<storage> masks: array<Mask>; var<storage> masks: array<Mask>;
// The glyph atlas, whose array layers are its pages.
// The texture this draw samples: the glyph atlas, whose layers are its pages, @group(0) @binding(2)
// or one standalone image as an array of one. var atlas: texture_2d_array<f32>;
@group(2) @binding(0) @group(0) @binding(3)
var tex: texture_2d_array<f32>;
@group(2) @binding(1)
var samp: sampler; var samp: sampler;
struct WindowUniform { struct WindowUniform {
@@ -45,7 +45,6 @@ struct VertexOutput {
@location(1) bot_right: vec2<f32>, @location(1) bot_right: vec2<f32>,
@location(2) uv: vec2<f32>, @location(2) uv: vec2<f32>,
@location(3) @interpolate(flat) mask_idx: u32, @location(3) @interpolate(flat) mask_idx: u32,
// The instance's own index, which is also where its data sits in group 1.
@location(4) @interpolate(flat) idx: u32, @location(4) @interpolate(flat) idx: u32,
@builtin(position) clip_position: vec4<f32>, @builtin(position) clip_position: vec4<f32>,
}; };
+4 -3
View File
@@ -1,7 +1,8 @@
// No group 1: the texture is bound at group 2 for this instance alone, so // The image this instance draws, bound for it alone.
// there is nothing per-instance left to look up. @group(2) @binding(0)
var image: texture_2d<f32>;
@fragment @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return masked(in, textureSample(tex, samp, in.uv, 0)); return masked(in, textureSample(image, samp, in.uv));
} }
+15 -47
View File
@@ -3,7 +3,7 @@ use wgpu::{util::DeviceExt, *};
use crate::{PatchRect, TextureUpdate, Textures}; use crate::{PatchRect, TextureUpdate, Textures};
/// The standalone images a ui draws, each its own texture and group 2 -- /// The standalone images a ui draws, each its own texture and bind group --
/// unlike the glyph atlas in `super::page`, which is one array they share. /// unlike the glyph atlas in `super::page`, which is one array they share.
pub struct GpuTextures { pub struct GpuTextures {
device: Device, device: Device,
@@ -26,15 +26,15 @@ impl GpuTextures {
} }
} }
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) { pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout) {
for update in textures.updates() { for update in textures.updates() {
match update { match update {
TextureUpdate::Push(image) => { TextureUpdate::Push(image) => {
let image = self.create(image, layout, sampler); let image = self.create(image, layout);
self.slots.push(Some(image)); self.slots.push(Some(image));
} }
TextureUpdate::Set(i, image) => { TextureUpdate::Set(i, image) => {
let image = self.create(image, layout, sampler); let image = self.create(image, layout);
self.slots[i as usize] = Some(image); self.slots[i as usize] = Some(image);
} }
TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image), TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
@@ -45,8 +45,6 @@ impl GpuTextures {
} }
} }
/// `None` once freed. A drawn instance holds a `TextureHandle`, so its
/// slot outlives it.
pub fn group(&self, slot: u32) -> Option<&BindGroup> { pub fn group(&self, slot: u32) -> Option<&BindGroup> {
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group) self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
} }
@@ -55,12 +53,7 @@ impl GpuTextures {
self.slots.iter().flatten().count() self.slots.iter().flatten().count()
} }
fn create( fn create(&self, image: &DynamicImage, layout: &BindGroupLayout) -> ImageGpu {
&self,
image: &DynamicImage,
layout: &BindGroupLayout,
sampler: &Sampler,
) -> ImageGpu {
let rgba = image.to_rgba8(); let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions(); let (width, height) = rgba.dimensions();
let texture = self.device.create_texture_with_data( let texture = self.device.create_texture_with_data(
@@ -82,7 +75,16 @@ impl GpuTextures {
wgt::TextureDataOrder::MipMajor, wgt::TextureDataOrder::MipMajor,
rgba.as_bytes(), rgba.as_bytes(),
); );
let group = texture_group(&self.device, layout, &array_view(&texture), sampler); let group = self.device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(
&texture.create_view(&TextureViewDescriptor::default()),
),
}],
label: Some("ui image"),
});
ImageGpu { texture, group } ImageGpu { texture, group }
} }
@@ -114,8 +116,6 @@ impl GpuTextures {
} }
} }
/// Uploads `rect` of `src` without copying it out first: `write_texture` takes
/// a row stride, so a region can be addressed where it already is.
pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) { pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) {
if rect.width == 0 || rect.height == 0 { if rect.width == 0 || rect.height == 0 {
return; return;
@@ -137,38 +137,6 @@ pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, r
); );
} }
/// One array texture and a sampler, so the atlas and a standalone image share a
/// layout and the shader samples whichever is bound -- an image being a
/// single-layer texture viewed as an array of one.
pub fn texture_group(
device: &Device,
layout: &BindGroupLayout,
view: &TextureView,
sampler: &Sampler,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(sampler),
},
],
label: Some("ui texture"),
})
}
pub fn array_view(texture: &Texture) -> TextureView {
texture.create_view(&TextureViewDescriptor {
dimension: Some(TextureViewDimension::D2Array),
..Default::default()
})
}
pub fn default_sampler(device: &Device) -> Sampler { pub fn default_sampler(device: &Device) -> Sampler {
device.create_sampler(&SamplerDescriptor::default()) device.create_sampler(&SamplerDescriptor::default())
} }
+2 -5
View File
@@ -39,11 +39,8 @@ impl<T: Pod> ArrBuf<T> {
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer { fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64; let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) { if usage.contains(BufferUsages::STORAGE) {
// An empty storage buffer is still bound, and a binding has to be // A binding cannot be empty or under the layout's minimum.
// non-empty and a multiple of four however small `T` is. size = size.max(std::mem::size_of::<T>() as u64);
size = size
.max(std::mem::size_of::<T>() as u64)
.next_multiple_of(4);
} }
device.create_buffer(&BufferDescriptor { device.create_buffer(&BufferDescriptor {
label: Some(label), label: Some(label),
+17 -20
View File
@@ -2,8 +2,8 @@ use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData, Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{ render::{
GLYPH, GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
PrimitiveKind, TEXTURE, TexturePrimitive, TexturePrimitive,
}, },
util::Vec2, util::Vec2,
}; };
@@ -23,12 +23,14 @@ pub struct Painter<'a> {
} }
impl<'a> Painter<'a> { impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>( fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
&mut self, let kind = self.rsc.ui_mut().primitives.kind::<P>();
kind: PrimitiveKind<P>, self.write(kind, primitive, region);
primitive: P, }
region: UiRegion,
) { /// For a caller with many of one primitive to write, since looking the kind
/// up is per type rather than per instance.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
let h = self.state.layers.write( let h = self.state.layers.write(
self.layer, self.layer,
PrimitiveInst { PrimitiveInst {
@@ -51,17 +53,12 @@ impl<'a> Painter<'a> {
} }
/// Writes a primitive to be rendered /// Writes a primitive to be rendered
pub fn primitive<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P) { pub fn primitive<P: Primitive>(&mut self, primitive: P) {
self.primitive_at(kind, primitive, self.region) self.primitive_at(primitive, self.region)
} }
pub fn primitive_within<P: Primitive>( pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
&mut self, self.primitive_at(primitive, region.within(&self.region));
kind: PrimitiveKind<P>,
primitive: P,
region: UiRegion,
) {
self.primitive_at(kind, primitive, region.within(&self.region));
} }
pub fn set_mask(&mut self, region: UiRegion) { pub fn set_mask(&mut self, region: UiRegion) {
@@ -104,7 +101,6 @@ impl<'a> Painter<'a> {
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) { pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
self.textures.push(handle.clone()); self.textures.push(handle.clone());
self.primitive_at( self.primitive_at(
TEXTURE,
TexturePrimitive { TexturePrimitive {
slot: handle.slot(), slot: handle.slot(),
}, },
@@ -123,6 +119,7 @@ impl<'a> Painter<'a> {
} }
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() { for glyph in text.glyphs.iter() {
let mut region = origin; let mut region = origin;
region.x.end = region.x.start; region.x.end = region.x.start;
@@ -130,8 +127,8 @@ impl<'a> Painter<'a> {
let mut region = region.offset(UiVec2::abs(glyph.offset)); let mut region = region.offset(UiVec2::abs(glyph.offset));
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32); 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); region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
self.primitive_at( self.write(
GLYPH, kind,
GlyphPrimitive { GlyphPrimitive {
uv_min: glyph.entry.uv_min, uv_min: glyph.entry.uv_min,
uv_max: glyph.entry.uv_max, uv_max: glyph.entry.uv_max,
+2 -5
View File
@@ -29,15 +29,12 @@ impl Rect {
impl Widget for Rect { impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
painter.primitive( painter.primitive(RectPrimitive {
RECT,
RectPrimitive {
color: self.color, color: self.color,
radius: self.radius, radius: self.radius,
thickness: self.thickness, thickness: self.thickness,
inner_radius: self.inner_radius, inner_radius: self.inner_radius,
}, });
);
} }
fn desired_width(&mut self, _: &mut SizeCtx) -> Len { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
-2
View File
@@ -73,7 +73,6 @@ impl Widget for TextEdit {
let size = vec2(rect.width() as f32, rect.height() as f32); let size = vec2(rect.width() as f32, rect.height() as f32);
let top_left = vec2(rect.x0 as f32, rect.y0 as f32); let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
painter.primitive_within( painter.primitive_within(
RECT,
RectPrimitive::color(Color::SKY), RectPrimitive::color(Color::SKY),
size.align(Align::TOP_LEFT).offset(top_left).within(&region), size.align(Align::TOP_LEFT).offset(top_left).within(&region),
); );
@@ -83,7 +82,6 @@ impl Widget for TextEdit {
let size = vec2(caret.width() as f32, caret.height() as f32); let size = vec2(caret.width() as f32, caret.height() as f32);
let top_left = vec2(caret.x0 as f32, caret.y0 as f32); let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
painter.primitive_within( painter.primitive_within(
RECT,
RectPrimitive::color(Color::WHITE), RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(top_left).within(&region), size.align(Align::TOP_LEFT).offset(top_left).within(&region),
); );