Draw each primitive with its own pipeline, registered rather than declared

The `primitives!` macro, `PrimitiveData`, `PrimitiveVec`,
`PrimitiveBuffers`, the `Primitive` trait and the shader's dispatch
switch are gone. A primitive is now a registration: its WGSL and,
implicitly, the size of the entry that WGSL reads. Everything else --
its instance list, its free list, its buffers, its bind group and its
pipeline -- follows from that, so adding one is a `register` call and a
shader file, with nothing per-type to remember and no cross-type
dispatch to extend.

Nothing dispatches dynamically. Push, free, renumber, upload and draw
are identical for every primitive; what differs is the entry size and
the pipeline, which are data. So `InstanceList` carries a runtime
stride and its instances' data as bytes, and one concrete type serves
every primitive and the textures. Measured against a typed list it
costs 0.2ns per write, where a trait object costs 1.6ns.

Because each type has its own list, an instance's index is also its
data index: `@builtin(instance_index)` replaces the `idx` field, the
`binding` field goes with the switch, and `PrimitiveInstance` drops from
28 bytes to 20. `PrimitiveVec`'s free list merges into the instance
list's, so an instance and its data are freed by one `swap_remove`
rather than two arenas kept in step.

`shader.wgsl` becomes `shader/prelude.wgsl` plus one file per primitive.
The prelude carries the window, masks, sampled texture, vertex shader
and `masked()`, and is compiled ahead of each primitive's own source --
which is also what a caller's own primitive would be. Masks move back
into group 0 beside the window uniform, since every pipeline shares one
layout.

Within a layer, types now draw in registration order: a rect under a
glyph under a texture. Order within a layer was never meaningful --
freeing an instance swaps another into its place -- so this replaces an
accident with a defined order, and backgrounds land under their content.

Verified with a headless run per case: text over its own rect and an
image over its own rect in one layer, a masked stack clipping, the
text-layout tab, and adding three images and deleting one.
This commit is contained in:
iris committed 2026-09-13 14:07:37 -04:00
1 parent b3d3da5dab
commit 4d9839f380
15 files changed
+614 -620

No files matched your search

+18 -15
View File
@@ -1,7 +1,9 @@
use bytemuck::Pod;
use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
render::{GLYPH, GlyphPrimitive, Mask, MaskIdx, PrimitiveHandle, PrimitiveKind},
util::Vec2,
};
@@ -20,16 +22,11 @@ pub struct Painter<'a> {
}
impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let h = self.state.layers.write(
self.layer,
PrimitiveInst {
id: self.id,
primitive,
region,
mask_idx: self.mask,
},
);
fn primitive_at<P: Pod>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
let h = self
.state
.layers
.write(self.layer, kind, self.id, primitive, region, self.mask);
self.push_primitive(h);
}
@@ -42,12 +39,17 @@ impl<'a> Painter<'a> {
}
/// Writes a primitive to be rendered
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
self.primitive_at(primitive, self.region)
pub fn primitive<P: Pod>(&mut self, kind: PrimitiveKind<P>, primitive: P) {
self.primitive_at(kind, primitive, self.region)
}
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
self.primitive_at(primitive, region.within(&self.region));
pub fn primitive_within<P: Pod>(
&mut self,
kind: PrimitiveKind<P>,
primitive: P,
region: UiRegion,
) {
self.primitive_at(kind, primitive, region.within(&self.region));
}
pub fn set_mask(&mut self, region: UiRegion) {
@@ -115,6 +117,7 @@ impl<'a> Painter<'a> {
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,
GlyphPrimitive {
uv_min: glyph.entry.uv_min,
uv_max: glyph.entry.uv_max,