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 {
shared_layout: BindGroupLayout,
shared_group: BindGroup,
texture_layout: BindGroupLayout,
image_layout: BindGroupLayout,
format: TextureFormat,
/// One per registered primitive, in id order.
@@ -46,26 +46,22 @@ pub struct UiRenderNode {
}
struct RenderLayer {
/// One per registered primitive, in id order, matching `LayerDraws` --
/// `None` where this layer draws none, so a primitive nobody uses costs no
/// buffers per layer.
/// One per registered primitive, `None` where this layer draws none.
primitives: Vec<Option<ListBuffers>>,
}
/// What draws one registered primitive. The group 1 layout is its own rather
/// than shared, so its entry size is the minimum only for it.
/// What draws one registered primitive.
struct PrimitivePipeline {
data_layout: BindGroupLayout,
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 {
instance: ArrBuf<PrimitiveInstance>,
data: ArrBuf<u8>,
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>,
}
@@ -74,41 +70,28 @@ impl UiRenderNode {
pass.set_bind_group(0, &self.shared_group, &[]);
for i in &self.active {
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() {
let Some(list) = list else {
continue;
};
let Some(group) = &list.group else {
continue;
};
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.
let Some(list) = list else { continue };
let Some(group) = &list.group else { continue };
pass.set_pipeline(&self.primitives[id].pipeline);
// After the pipeline: a change drops the groups from where two
// pipeline layouts differ, and each primitive has its own.
pass.set_bind_group(1, group, &[]);
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
match primitive.texture {
PrimitiveTexture::Atlas => {
pass.set_bind_group(2, self.pages.group(), &[]);
if list.slots.is_empty() {
pass.draw(0..4, 0..list.instance.len() as u32);
continue;
}
PrimitiveTexture::PerInstance => {
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;
};
pass.set_bind_group(2, texture, &[]);
pass.set_bind_group(2, image, &[]);
pass.draw(0..4, i as u32..i as u32 + 1);
}
}
}
}
}
}
pub fn update(
&mut self,
@@ -117,9 +100,7 @@ impl UiRenderNode {
ui: &mut UiData,
ui_render: &mut UiRenderState,
) {
// Before the layers: a list is given the bind group layout of the
// pipeline that will draw it, so every registered primitive needs one
// by the time a layer is reached.
// Before the layers: each list is given its pipeline's data layout.
self.build_pipelines(device, &ui.primitives);
self.active.clear();
for (i, draws) in ui_render.layers.iter_mut() {
@@ -150,35 +131,30 @@ impl UiRenderNode {
};
// Indexed, not zipped: a missing pipeline should say so
// rather than quietly leave the list unbuilt.
let primitive = &self.primitives[id];
let layout = &self.primitives[id].data_layout;
buffers
.get_or_insert_with(|| ListBuffers::new(device))
.update(
device,
queue,
primitive.texture,
&primitive.data_layout,
list,
);
.update(device, queue, layout, list);
}
draws.updated = false;
}
}
let mut shared_stale = self.pages.update(&mut ui.text.atlas);
if ui.masks.changed {
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(
device,
&self.shared_layout,
&self.window_buffer,
&self.masks,
&self.pages,
&self.sampler,
);
}
}
self.pages
.update(&mut ui.text.atlas, &self.texture_layout, &self.sampler);
self.textures
.update(&mut ui.textures, &self.texture_layout, &self.sampler);
self.textures.update(&mut ui.textures, &self.image_layout);
}
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 texture_layout = Self::texture_layout(device);
let image_layout = Self::image_layout(device);
let masks = ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui 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 pages = GpuPages::new(device, queue);
let textures = GpuTextures::new(device, queue);
let shared_group = Self::shared_group(
device,
&shared_layout,
&window_buffer,
&masks,
&pages,
&sampler,
);
Self {
shared_layout,
shared_group,
texture_layout,
image_layout,
format: config.format,
primitives: Vec::new(),
window_buffer,
@@ -236,34 +219,23 @@ impl UiRenderNode {
fn build_pipelines(&mut self, device: &Device, registry: &PrimitiveRegistry) {
for source in &registry.sources()[self.primitives.len()..] {
let data_layout = Self::data_layout(device, source.stride);
let layout = Self::pipeline_layout(
device,
&self.shared_layout,
&data_layout,
&self.texture_layout,
);
let mut groups = vec![&self.shared_layout, &data_layout];
if source.textured {
groups.push(&self.image_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);
self.primitives.push(PrimitivePipeline {
data_layout,
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(
device: &Device,
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 {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
@@ -338,6 +311,22 @@ impl UiRenderNode {
},
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"),
})
@@ -348,6 +337,8 @@ impl UiRenderNode {
layout: &BindGroupLayout,
window: &Buffer,
masks: &ArrBuf<Mask>,
pages: &GpuPages,
sampler: &Sampler,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
@@ -360,16 +351,22 @@ impl UiRenderNode {
binding: 1,
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"),
})
}
/// Group 1: one list's per-instance data, whatever its shader reads it as.
///
/// One per primitive with `stride` stated, because a `None` minimum is
/// filled in from the first pipeline built against the layout -- sharing
/// one would hold every primitive to the largest.
/// One list's per-instance data. Per primitive with `stride` stated: a
/// `None` minimum is filled in from the first pipeline built against the
/// layout, so a shared one would hold every primitive to the largest.
fn data_layout(device: &Device, stride: u64) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
@@ -386,29 +383,21 @@ impl UiRenderNode {
})
}
/// Group 2: the texture this draw samples, and the sampler. No `count` on
/// either entry -- plain Vulkan 1.0 / GLES sampling is all this needs.
fn texture_layout(device: &Device) -> BindGroupLayout {
/// The one image an instance samples. The sampler is shared, so there is
/// nothing else in here.
fn image_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2Array,
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: None,
},
],
label: Some("ui texture"),
}],
label: Some("ui image"),
})
}
@@ -447,22 +436,14 @@ impl ListBuffers {
&mut self,
device: &Device,
queue: &Queue,
texture: PrimitiveTexture,
layout: &BindGroupLayout,
list: &InstanceList,
) {
if texture == PrimitiveTexture::PerInstance {
self.slots.clear();
// Read rather than cast: the payload is a byte vec, so it carries
// 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.slots.extend_from_slice(list.slots());
self.instance.update(device, queue, list.instances());
let resized = self.data.update(device, queue, list.data());
// An empty list has no buffer big enough for one entry, and nothing
// draws it, so it has no bind group either.
// An empty list has no buffer big enough to bind, and nothing to draw.
if list.instances().is_empty() {
self.group = None;
} else if resized || self.group.is_none() {
+21 -21
View File
@@ -2,10 +2,7 @@ use wgpu::*;
use crate::GlyphAtlas;
use super::{
atlas::PAGE,
texture::{array_view, texture_group, write_region},
};
use super::{atlas::PAGE, texture::write_region};
/// The glyph atlas on the GPU: one array texture whose layers are the pages
/// `GlyphAtlas` packs.
@@ -17,29 +14,25 @@ pub struct GpuPages {
device: Device,
queue: Queue,
texture: Texture,
group: BindGroup,
view: TextureView,
}
impl GpuPages {
pub fn new(
device: &Device,
queue: &Queue,
layout: &BindGroupLayout,
sampler: &Sampler,
) -> Self {
pub fn new(device: &Device, queue: &Queue) -> Self {
let texture = create_array(device, 1);
let group = texture_group(device, layout, &array_view(&texture), sampler);
Self {
device: device.clone(),
queue: queue.clone(),
view: array_view(&texture),
texture,
group,
}
}
pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) {
if atlas.page_count() > self.texture.depth_or_array_layers() {
self.grow(atlas.page_count(), layout, sampler);
/// Returns whether the array was replaced, which stales the view.
pub fn update(&mut self, atlas: &mut GlyphAtlas) -> bool {
let grew = atlas.page_count() > self.texture.depth_or_array_layers();
if grew {
self.grow(atlas.page_count());
}
for (upload, page) in atlas.uploads() {
let dst = TexelCopyTextureInfo {
@@ -54,15 +47,15 @@ impl GpuPages {
};
write_region(&self.queue, dst, page, upload.rect);
}
grew
}
pub fn group(&self) -> &BindGroup {
&self.group
pub fn view(&self) -> &TextureView {
&self.view
}
/// 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, layout: &BindGroupLayout, sampler: &Sampler) {
fn grow(&mut self, needed: u32) {
let old = self.texture.depth_or_array_layers();
let mut layers = old;
while layers < needed {
@@ -84,11 +77,18 @@ impl GpuPages {
},
);
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;
}
}
fn array_view(texture: &Texture) -> TextureView {
texture.create_view(&TextureViewDescriptor {
dimension: Some(TextureViewDimension::D2Array),
..Default::default()
})
}
fn create_array(device: &Device, layers: u32) -> Texture {
device.create_texture(&TextureDescriptor {
label: Some("glyph atlas"),
+86 -92
View File
@@ -1,38 +1,39 @@
use std::marker::PhantomData;
use std::{any::TypeId, marker::PhantomData};
use crate::{
Color, UiRegion, WidgetId,
render::data::{MaskIdx, PrimitiveInstance},
util::Vec2,
util::{HashMap, Vec2},
};
use bytemuck::Pod;
/// One instance of a registered primitive, laid out as the struct that
/// primitive's shader reads at `@group(1) @binding(0)`.
/// One instance of a primitive, laid out as the struct its shader reads.
///
/// A `PrimitiveKind<P>` is only minted by `register::<P>`, and `write` takes
/// the kind and the value together, so holding one is the proof that `P` has a
/// list of its own to go in and a write needs no check.
pub trait Primitive: Pod {}
/// The type carries its own shader, so drawing one is all the wiring it needs:
/// its list, free list, buffers and pipeline follow from being registered.
pub trait Primitive: Pod + 'static {
/// 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
/// and which pipeline draws it. The type ties a `write` to what its shader reads.
/// Which registered primitive an instance is. Only `PrimitiveRegistry::kind`
/// mints one, so holding it is the proof that `P` has a list to go in.
pub struct PrimitiveKind<P> {
id: u32,
_p: PhantomData<fn(P)>,
}
impl<P> PrimitiveKind<P> {
const fn new(id: u32) -> Self {
fn new(id: u32) -> Self {
Self {
id,
_p: PhantomData,
}
}
pub fn id(&self) -> u32 {
self.id
}
}
impl<P> Clone for PrimitiveKind<P> {
@@ -43,80 +44,50 @@ impl<P> Clone for PrimitiveKind<P> {
impl<P> Copy for PrimitiveKind<P> {}
pub const RECT: PrimitiveKind<RectPrimitive> = PrimitiveKind::new(0);
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.
/// Every primitive a ui can draw, in the order they were first drawn.
pub struct PrimitiveRegistry {
kinds: Vec<PrimitiveSource>,
ids: HashMap<TypeId, u32>,
}
pub struct PrimitiveSource {
pub wgsl: &'static str,
pub label: &'static str,
/// Size of one instance's entry, which the renderer states as the group 1
/// binding's minimum rather than leaving it to be inferred.
/// Size of one instance's entry, stated as the data binding's minimum.
pub stride: u64,
pub texture: PrimitiveTexture,
}
/// 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,
/// Whether an instance binds a texture of its own, from `P::TEXTURE`.
pub textured: bool,
}
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 {
use PrimitiveTexture::*;
let mut registry = Self { kinds: Vec::new() };
let rect =
registry.register::<RectPrimitive>(include_str!("shader/rect.wgsl"), "rect", Atlas);
let glyph =
registry.register::<GlyphPrimitive>(include_str!("shader/glyph.wgsl"), "glyph", Atlas);
let texture = registry.register::<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())
);
let mut registry = Self {
kinds: Vec::new(),
ids: HashMap::default(),
};
registry.kind::<RectPrimitive>();
registry.kind::<GlyphPrimitive>();
registry.kind::<TexturePrimitive>();
registry
}
}
impl PrimitiveRegistry {
pub fn register<P: Primitive>(
&mut self,
wgsl: &'static str,
label: &'static str,
texture: PrimitiveTexture,
) -> PrimitiveKind<P> {
self.kinds.push(PrimitiveSource {
wgsl,
label,
/// Registers `P` if this is the first time it has been drawn.
pub fn kind<P: Primitive>(&mut self) -> PrimitiveKind<P> {
let Self { kinds, ids } = self;
let id = *ids.entry(TypeId::of::<P>()).or_insert_with(|| {
kinds.push(PrimitiveSource {
wgsl: P::WGSL,
label: std::any::type_name::<P>(),
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] {
@@ -124,18 +95,18 @@ impl PrimitiveRegistry {
}
}
/// One registered primitive's instances in one layer: the instances, the
/// widget each belongs to, the slots waiting to be reused, and `stride` bytes
/// 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`.
/// One registered primitive's instances in one layer. Everything per-instance
/// rides here, so it stays in step through a `swap_remove`.
pub struct InstanceList {
instances: Vec<PrimitiveInstance>,
/// The widget each instance belongs to, for renumbering its handles.
assoc: Vec<WidgetId>,
free: Vec<usize>,
/// `stride` bytes of the primitive's own data per instance.
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,
}
@@ -146,6 +117,7 @@ impl InstanceList {
assoc: Vec::new(),
free: Vec::new(),
data: Vec::new(),
slots: Vec::new(),
stride: size_of::<P>(),
}
}
@@ -158,17 +130,31 @@ impl InstanceList {
&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() {
self.instances[i] = inst;
self.assoc[i] = id;
self.data[i * self.stride..][..self.stride].copy_from_slice(data);
if let Some(slot) = slot {
self.slots[i] = slot;
}
i
} else {
let i = self.instances.len();
self.instances.push(inst);
self.assoc.push(id);
self.data.extend_from_slice(data);
self.slots.extend(slot);
i
}
}
@@ -183,10 +169,14 @@ impl InstanceList {
let instances = &mut self.instances;
let assoc = &mut self.assoc;
let data = &mut self.data;
let slots = &mut self.slots;
let stride = self.stride;
self.free.drain(..).filter_map(move |i| {
instances.swap_remove(i);
assoc.swap_remove(i);
if !slots.is_empty() {
slots.swap_remove(i);
}
let last = instances.len();
data.copy_within(last * stride..(last + 1) * stride, i * stride);
data.truncate(last * stride);
@@ -204,9 +194,7 @@ impl InstanceList {
}
}
/// Everything one layer draws, one list per registered primitive. They index
/// independently, so a handle or a renumbering naming only a position would be
/// ambiguous between them.
/// Everything one layer draws, one list per registered primitive.
pub struct LayerDraws {
/// `None` until this layer draws that primitive, because only the write
/// knows the type the list is for.
@@ -247,6 +235,7 @@ impl LayerDraws {
id,
PrimitiveInstance { region, mask_idx },
bytemuck::bytes_of(&primitive),
P::TEXTURE.map(|slot| slot(&primitive)),
);
PrimitiveHandle {
layer,
@@ -309,7 +298,7 @@ pub struct PrimitiveHandle {
}
#[repr(C)]
#[derive(Copy, Clone)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RectPrimitive {
pub color: Color<u8>,
pub radius: f32,
@@ -317,9 +306,9 @@ pub struct RectPrimitive {
pub inner_radius: f32,
}
unsafe impl bytemuck::Pod for RectPrimitive {}
unsafe impl bytemuck::Zeroable for RectPrimitive {}
impl Primitive for RectPrimitive {}
impl Primitive for RectPrimitive {
const WGSL: &'static str = include_str!("shader/rect.wgsl");
}
impl RectPrimitive {
pub fn color(color: Color<u8>) -> Self {
@@ -345,18 +334,23 @@ pub struct GlyphPrimitive {
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::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
/// for this instance alone, which is what `PrimitiveTexture::PerInstance` does.
/// One drawn image. Its shader reads nothing per instance; the slot names the
/// texture to bind for it.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TexturePrimitive {
pub slot: u32,
}
unsafe impl bytemuck::Pod for TexturePrimitive {}
unsafe impl bytemuck::Zeroable for TexturePrimitive {}
impl Primitive for TexturePrimitive {}
impl Primitive for TexturePrimitive {
const WGSL: &'static str = include_str!("shader/texture.wgsl");
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 {
uv_min: vec2<f32>,
uv_max: vec2<f32>,
@@ -14,8 +17,8 @@ var<storage> glyphs: array<GlyphInfo>;
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 {
let texel = textureSample(atlas, samp, uv, i32(g.layer));
if (g.flags & COLORED) != 0u {
return masked(in, texel);
}
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
// struct at group 1 and an `fs_main` that shades one instance.
// Prepended to every primitive's shader, which declares its own instance data
// 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)
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)
// The glyph atlas, whose array layers are its pages.
@group(0) @binding(2)
var atlas: texture_2d_array<f32>;
@group(0) @binding(3)
var samp: sampler;
struct WindowUniform {
@@ -45,7 +45,6 @@ struct VertexOutput {
@location(1) bot_right: vec2<f32>,
@location(2) uv: vec2<f32>,
@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,
@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
// there is nothing per-instance left to look up.
// The image this instance draws, bound for it alone.
@group(2) @binding(0)
var image: texture_2d<f32>;
@fragment
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};
/// 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.
pub struct GpuTextures {
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() {
match update {
TextureUpdate::Push(image) => {
let image = self.create(image, layout, sampler);
let image = self.create(image, layout);
self.slots.push(Some(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);
}
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> {
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
}
@@ -55,12 +53,7 @@ impl GpuTextures {
self.slots.iter().flatten().count()
}
fn create(
&self,
image: &DynamicImage,
layout: &BindGroupLayout,
sampler: &Sampler,
) -> ImageGpu {
fn create(&self, image: &DynamicImage, layout: &BindGroupLayout) -> ImageGpu {
let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions();
let texture = self.device.create_texture_with_data(
@@ -82,7 +75,16 @@ impl GpuTextures {
wgt::TextureDataOrder::MipMajor,
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 }
}
@@ -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) {
if rect.width == 0 || rect.height == 0 {
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 {
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 {
let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) {
// 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);
// A binding cannot be empty or under the layout's minimum.
size = size.max(std::mem::size_of::<T>() as u64);
}
device.create_buffer(&BufferDescriptor {
label: Some(label),
+17 -20
View File
@@ -2,8 +2,8 @@ use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{
GLYPH, GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst,
PrimitiveKind, TEXTURE, TexturePrimitive,
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
},
util::Vec2,
};
@@ -23,12 +23,14 @@ pub struct Painter<'a> {
}
impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(
&mut self,
kind: PrimitiveKind<P>,
primitive: P,
region: UiRegion,
) {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region);
}
/// 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(
self.layer,
PrimitiveInst {
@@ -51,17 +53,12 @@ impl<'a> Painter<'a> {
}
/// Writes a primitive to be rendered
pub fn primitive<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P) {
self.primitive_at(kind, primitive, self.region)
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
self.primitive_at(primitive, self.region)
}
pub fn primitive_within<P: Primitive>(
&mut self,
kind: PrimitiveKind<P>,
primitive: P,
region: UiRegion,
) {
self.primitive_at(kind, primitive, region.within(&self.region));
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
self.primitive_at(primitive, region.within(&self.region));
}
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) {
self.textures.push(handle.clone());
self.primitive_at(
TEXTURE,
TexturePrimitive {
slot: handle.slot(),
},
@@ -123,6 +119,7 @@ impl<'a> Painter<'a> {
}
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() {
let mut region = origin;
region.x.end = region.x.start;
@@ -130,8 +127,8 @@ impl<'a> Painter<'a> {
let mut region = region.offset(UiVec2::abs(glyph.offset));
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,
self.write(
kind,
GlyphPrimitive {
uv_min: glyph.entry.uv_min,
uv_max: glyph.entry.uv_max,
+2 -5
View File
@@ -29,15 +29,12 @@ impl Rect {
impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) {
painter.primitive(
RECT,
RectPrimitive {
painter.primitive(RectPrimitive {
color: self.color,
radius: self.radius,
thickness: self.thickness,
inner_radius: self.inner_radius,
},
);
});
}
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
-2
View File
@@ -73,7 +73,6 @@ impl Widget for TextEdit {
let size = vec2(rect.width() as f32, rect.height() as f32);
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
painter.primitive_within(
RECT,
RectPrimitive::color(Color::SKY),
size.align(Align::TOP_LEFT).offset(top_left).within(&region),
);
@@ -83,7 +82,6 @@ impl Widget for TextEdit {
let size = vec2(caret.width() as f32, caret.height() as f32);
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
painter.primitive_within(
RECT,
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(top_left).within(&region),
);