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
+268 -333

No files matched your search

+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"),