From 01a9b8633d061c51643e769821e9fa96ecb03d07 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sun, 13 Sep 2026 16:52:32 -0400 Subject: [PATCH] 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 --- core/src/render/mod.rs | 9 ++++---- core/src/render/primitive.rs | 33 ++++++++++++----------------- core/src/render/shader/glyph.wgsl | 6 ++++++ core/src/render/shader/prelude.wgsl | 12 +++++------ core/src/render/shader/texture.wgsl | 2 ++ core/src/ui/painter.rs | 13 +++++------- 6 files changed, 35 insertions(+), 40 deletions(-) diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index 12c5146..8a31049 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -364,9 +364,9 @@ impl UiRenderNode { }) } - /// 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. + /// One list's per-instance data. Each primitive needs its own, stating its + /// own `stride`: a `None` minimum takes its value from the first pipeline + /// built against the layout, and then holds every later one to that. fn data_layout(device: &Device, stride: u64) -> BindGroupLayout { device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[BindGroupLayoutEntry { @@ -383,8 +383,7 @@ impl UiRenderNode { }) } - /// The one image an instance samples. The sampler is shared, so there is - /// nothing else in here. + /// The image one instance samples. fn image_layout(device: &Device) -> BindGroupLayout { device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[BindGroupLayoutEntry { diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index 0f236da..1f43e47 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -1,7 +1,7 @@ use std::{any::TypeId, marker::PhantomData}; use crate::{ - Color, UiRegion, WidgetId, + Color, TextureHandle, UiRegion, WidgetId, render::data::{MaskIdx, PrimitiveInstance}, util::{HashMap, Vec2}, }; @@ -15,13 +15,12 @@ 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. + /// Reads the image an instance uses. Each instance is then a draw of its + /// own, with that image bound for it alone. const TEXTURE: Option u32> = None; } -/// 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. +/// Which registered primitive an instance is. pub struct PrimitiveKind

{ id: u32, _p: PhantomData, @@ -45,6 +44,7 @@ impl

Clone for PrimitiveKind

{ impl

Copy for PrimitiveKind

{} /// Every primitive a ui can draw, in the order they were first drawn. +#[derive(Default)] pub struct PrimitiveRegistry { kinds: Vec, ids: HashMap, @@ -59,21 +59,6 @@ pub struct PrimitiveSource { 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::(); - registry.kind::(); - registry.kind::(); - registry - } -} - impl PrimitiveRegistry { /// Registers `P` if this is the first time it has been drawn. pub fn kind(&mut self) -> PrimitiveKind

{ @@ -354,3 +339,11 @@ impl Primitive for TexturePrimitive { const WGSL: &'static str = include_str!("shader/texture.wgsl"); const TEXTURE: Option u32> = Some(|texture| texture.slot); } + +impl From<&TextureHandle> for TexturePrimitive { + fn from(handle: &TextureHandle) -> Self { + Self { + slot: handle.slot(), + } + } +} diff --git a/core/src/render/shader/glyph.wgsl b/core/src/render/shader/glyph.wgsl index 376b4fd..08029ff 100644 --- a/core/src/render/shader/glyph.wgsl +++ b/core/src/render/shader/glyph.wgsl @@ -1,6 +1,12 @@ // Matches `GlyphEntry::IS_COLORED`. const COLORED: u32 = 1u; +// The glyph atlas, whose array layers are its pages. +@group(0) @binding(2) +var atlas: texture_2d_array; +@group(0) @binding(3) +var samp: sampler; + struct GlyphInfo { uv_min: vec2, uv_max: vec2, diff --git a/core/src/render/shader/prelude.wgsl b/core/src/render/shader/prelude.wgsl index c967a24..013fe4b 100644 --- a/core/src/render/shader/prelude.wgsl +++ b/core/src/render/shader/prelude.wgsl @@ -1,17 +1,15 @@ // Prepended to every primitive's shader, which declares its own instance data // as `var : array` 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. +// shading one instance of it. +// +// 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) var window: WindowUniform; @group(0) @binding(1) var masks: array; -// The glyph atlas, whose array layers are its pages. -@group(0) @binding(2) -var atlas: texture_2d_array; -@group(0) @binding(3) -var samp: sampler; struct WindowUniform { dim: vec2, diff --git a/core/src/render/shader/texture.wgsl b/core/src/render/shader/texture.wgsl index e4e3cec..179d0a3 100644 --- a/core/src/render/shader/texture.wgsl +++ b/core/src/render/shader/texture.wgsl @@ -1,6 +1,8 @@ // The image this instance draws, bound for it alone. @group(2) @binding(0) var image: texture_2d; +@group(0) @binding(3) +var samp: sampler; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 11139cb..d0496b5 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -28,8 +28,7 @@ impl<'a> Painter<'a> { 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. + /// Takes the kind, for a caller writing many of one primitive. fn write(&mut self, kind: PrimitiveKind

, primitive: P, region: UiRegion) { let h = self.state.layers.write( self.layer, @@ -98,14 +97,12 @@ impl<'a> Painter<'a> { 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) { self.textures.push(handle.clone()); - self.primitive_at( - TexturePrimitive { - slot: handle.slot(), - }, - region, - ); + self.primitive_at(TexturePrimitive::from(handle), region); } pub fn render_text(