Register a primitive only when it is drawn, and keep the prelude shared

Nothing seeds the registry any more, so a kind's id is decided by the first
draw and no order within a layer can be relied on even by accident. A ui that
draws no images now pays for no image pipeline, and a layer's list vector only
reaches the highest kind that layer draws.

The atlas and the sampler are still bound for every draw, but are declared by
the two shaders that read them rather than by the prelude, which is now only
what every primitive uses.

`TexturePrimitive` gets a `From<&TextureHandle>`; `Painter::texture_at` stays
because the share of the handle is what keeps the slot from being freed while
it is drawn, which a `Pod` primitive cannot hold.

Checked on the headless rig that the layers carry the ordering rather than the
ids: with a bare text drawn first, so glyph registers before rect, a stacked
label still draws over its background. Also re-ran an image alone in a layer,
now the only primitive a ui registers, and a four-layer atlas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-13 16:52:32 -04:00
1 parent 29d390da52
commit 01a9b8633d
6 files changed
+35 -40

No files matched your search

+4 -5
View File
@@ -364,9 +364,9 @@ impl UiRenderNode {
}) })
} }
/// One list's per-instance data. Per primitive with `stride` stated: a /// One list's per-instance data. Each primitive needs its own, stating its
/// `None` minimum is filled in from the first pipeline built against the /// own `stride`: a `None` minimum takes its value from the first pipeline
/// layout, so a shared one would hold every primitive to the largest. /// built against the layout, and then holds every later one to that.
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 {
@@ -383,8 +383,7 @@ impl UiRenderNode {
}) })
} }
/// The one image an instance samples. The sampler is shared, so there is /// The image one instance samples.
/// nothing else in here.
fn image_layout(device: &Device) -> BindGroupLayout { fn image_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry { entries: &[BindGroupLayoutEntry {
+13 -20
View File
@@ -1,7 +1,7 @@
use std::{any::TypeId, marker::PhantomData}; use std::{any::TypeId, marker::PhantomData};
use crate::{ use crate::{
Color, UiRegion, WidgetId, Color, TextureHandle, UiRegion, WidgetId,
render::data::{MaskIdx, PrimitiveInstance}, render::data::{MaskIdx, PrimitiveInstance},
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
@@ -15,13 +15,12 @@ pub trait Primitive: Pod + 'static {
/// Compiled after `prelude.wgsl`, which states what it declares and what /// Compiled after `prelude.wgsl`, which states what it declares and what
/// it is given. /// it is given.
const WGSL: &'static str; const WGSL: &'static str;
/// Reads the image an instance samples, for a primitive that draws one. /// Reads the image an instance uses. Each instance is then a draw of its
/// Each instance is then a draw of its own, bound for it alone. /// own, with that image bound for it alone.
const TEXTURE: Option<fn(&Self) -> u32> = None; const TEXTURE: Option<fn(&Self) -> u32> = None;
} }
/// Which registered primitive an instance is. Only `PrimitiveRegistry::kind` /// Which registered primitive an instance is.
/// 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)>,
@@ -45,6 +44,7 @@ impl<P> Clone for PrimitiveKind<P> {
impl<P> Copy for PrimitiveKind<P> {} impl<P> Copy for PrimitiveKind<P> {}
/// Every primitive a ui can draw, in the order they were first drawn. /// Every primitive a ui can draw, in the order they were first drawn.
#[derive(Default)]
pub struct PrimitiveRegistry { pub struct PrimitiveRegistry {
kinds: Vec<PrimitiveSource>, kinds: Vec<PrimitiveSource>,
ids: HashMap<TypeId, u32>, ids: HashMap<TypeId, u32>,
@@ -59,21 +59,6 @@ pub struct PrimitiveSource {
pub textured: bool, 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 {
let mut registry = Self {
kinds: Vec::new(),
ids: HashMap::default(),
};
registry.kind::<RectPrimitive>();
registry.kind::<GlyphPrimitive>();
registry.kind::<TexturePrimitive>();
registry
}
}
impl PrimitiveRegistry { impl PrimitiveRegistry {
/// Registers `P` if this is the first time it has been drawn. /// Registers `P` if this is the first time it has been drawn.
pub fn kind<P: Primitive>(&mut self) -> PrimitiveKind<P> { pub fn kind<P: Primitive>(&mut self) -> PrimitiveKind<P> {
@@ -354,3 +339,11 @@ impl Primitive for TexturePrimitive {
const WGSL: &'static str = include_str!("shader/texture.wgsl"); const WGSL: &'static str = include_str!("shader/texture.wgsl");
const TEXTURE: Option<fn(&Self) -> u32> = Some(|texture| texture.slot); const TEXTURE: Option<fn(&Self) -> u32> = Some(|texture| texture.slot);
} }
impl From<&TextureHandle> for TexturePrimitive {
fn from(handle: &TextureHandle) -> Self {
Self {
slot: handle.slot(),
}
}
}
+6
View File
@@ -1,6 +1,12 @@
// Matches `GlyphEntry::IS_COLORED`. // Matches `GlyphEntry::IS_COLORED`.
const COLORED: u32 = 1u; const COLORED: u32 = 1u;
// 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 GlyphInfo { struct GlyphInfo {
uv_min: vec2<f32>, uv_min: vec2<f32>,
uv_max: vec2<f32>, uv_max: vec2<f32>,
+5 -7
View File
@@ -1,17 +1,15 @@
// Prepended to every primitive's shader, which declares its own instance data // 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` // 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 // shading one instance of it.
// takes it at group 2 binding 0; see texture.wgsl. //
// The rest of group 0 is bound for every draw but declared by the shaders that
// read it: the glyph atlas at binding 2 and the ui's sampler at binding 3. A
// primitive that samples an image of its own takes it at group 2 binding 0.
@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.
@group(0) @binding(2)
var atlas: texture_2d_array<f32>;
@group(0) @binding(3)
var samp: sampler;
struct WindowUniform { struct WindowUniform {
dim: vec2<f32>, dim: vec2<f32>,
+2
View File
@@ -1,6 +1,8 @@
// The image this instance draws, bound for it alone. // The image this instance draws, bound for it alone.
@group(2) @binding(0) @group(2) @binding(0)
var image: texture_2d<f32>; var image: texture_2d<f32>;
@group(0) @binding(3)
var samp: sampler;
@fragment @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
+5 -8
View File
@@ -28,8 +28,7 @@ impl<'a> Painter<'a> {
self.write(kind, primitive, region); self.write(kind, primitive, region);
} }
/// For a caller with many of one primitive to write, since looking the kind /// Takes the kind, for a caller writing many of one primitive.
/// up is per type rather than per instance.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) { 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,
@@ -98,14 +97,12 @@ impl<'a> Painter<'a> {
self.texture_at(handle, self.region); self.texture_at(handle, self.region);
} }
/// A texture primitive, plus a share of the handle it names -- which a
/// `Pod` primitive cannot carry, and without which the slot could be
/// freed and reused while still drawn.
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(TexturePrimitive::from(handle), region);
TexturePrimitive {
slot: handle.slot(),
},
region,
);
} }
pub fn render_text( pub fn render_text(