Draw the glyph atlas as an array texture and images with their own bind groups

The renderer bound every texture through one
`binding_array<texture_2d<f32>>` indexed per primitive. That needs
`VK_EXT_descriptor_indexing`, which a real share of Android GPUs do not
have, so the shape did not run there at all.

Split the two things being bound, since they want opposite treatment:

- Glyph atlas pages become layers of one `texture_2d_array`. A glyph
  primitive carries a layer rather than a view/sampler index pair, and a
  layer index is an ordinary sampling operand -- no extension. Growing the
  atlas recreates the array with headroom and copies the old layers across
  GPU-side.
- A standalone image gets its own texture and its own bind group, and
  draws in its own call. It no longer needs a per-instance entry in
  `PrimitiveData` at all: the bind group has already picked the texture.

`Primitives` therefore keeps images in a list of their own, with
`PrimitiveChange::is_image` saying which list a renumbering belongs to --
the two have independent index spaces, so `(layer, inst_idx)` alone would
collide between them.

Verified on this machine's real GPU (Venus onto an RX 7900 XT, confirmed
by the loaded ICD rather than assumed): the `tabs` example renders
byte-identical screenshots before and after, both for a text-and-rect tab
and for one holding a standalone image.
This commit is contained in:
iris committed 2026-09-13 03:58:12 -04:00
1 parent 0f6a28b4dd
commit bafaa1db6d
10 files changed
+794 -255

No files matched your search

+161 -27
View File
@@ -16,6 +16,17 @@ pub struct Primitives {
assoc: Vec<WidgetId>,
data: PrimitiveData,
free: Vec<usize>,
/// Standalone images, kept apart from `instances` because each one draws
/// with its own bind group rather than sharing the layer's one instanced
/// draw. `idx` on each `PrimitiveInstance` here is the texture's slot in
/// `Textures`/`GpuTextures`, not an index into `data`: a bind group has
/// already picked the texture, so there is nothing left to look up
/// per-instance and no per-image entry in `data` at all.
images: Vec<PrimitiveInstance>,
image_assoc: Vec<WidgetId>,
image_free: Vec<usize>,
pub updated: bool,
}
@@ -26,11 +37,21 @@ impl Default for Primitives {
assoc: Default::default(),
data: Default::default(),
free: Vec::new(),
images: Default::default(),
image_assoc: Default::default(),
image_free: Vec::new(),
updated: true,
}
}
}
/// The `binding` tag `Painter` writes on an image instance. Distinct from any
/// `Primitive::BINDING` because images have no `PrimitiveData` entry to key
/// one from -- a bind group already selects the texture -- so this only ever
/// has to match the shader's `TEXTURE` constant and flag "this instance lives
/// in `Primitives::images`, not `Primitives::instances`" to the code below.
pub const IMAGE_BINDING: u32 = 1;
pub trait Primitive: Pod {
const BINDING: u32;
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>;
@@ -55,6 +76,14 @@ macro_rules! primitives {
impl PrimitiveBuffers {
pub const LEN: usize = primitives!(@count $($name)*);
/// The group-1 binding number each primitive's storage buffer
/// sits at, in declaration order. Not `0..LEN`: a primitive's
/// `BINDING` also tags its instances for the shader's dispatch
/// switch, and a removed primitive (as `TEXTURE` was, once
/// images stopped needing a per-instance storage entry) can
/// leave a gap, so the pipeline layout has to ask for these
/// exact numbers rather than assuming they are contiguous.
pub const BINDINGS: [u32; Self::LEN] = [$(<$ty>::BINDING,)*];
pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
[
$((<$ty>::BINDING, &self.$name.buffer),)*
@@ -138,26 +167,103 @@ impl Primitives {
PrimitiveHandle::new::<P>(layer, inst_i, i)
}
/// returns (old index, new index)
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
self.free.sort_by(|a, b| b.cmp(a));
self.free.drain(..).filter_map(|i| {
self.instances.swap_remove(i);
self.assoc.swap_remove(i);
if i == self.instances.len() {
return None;
}
let id = self.assoc[i];
let old = self.instances.len();
Some(PrimitiveChange { id, old, new: i })
})
/// Writes an image instance directly -- there is no `Primitive` impl for
/// it to go through `write`, since it has nowhere in `PrimitiveData` to
/// put a per-instance entry. `texture_idx` is the slot the bind group at
/// draw time is chosen from, carried in the otherwise-unused `idx` field.
pub fn write_image(
&mut self,
layer: usize,
id: WidgetId,
texture_idx: u32,
region: UiRegion,
mask_idx: MaskIdx,
) -> PrimitiveHandle {
self.updated = true;
let inst = PrimitiveInstance {
region,
idx: texture_idx,
mask_idx,
binding: IMAGE_BINDING,
};
let inst_i = if let Some(i) = self.image_free.pop() {
self.images[i] = inst;
self.image_assoc[i] = id;
i
} else {
let i = self.images.len();
self.images.push(inst);
self.image_assoc.push(id);
i
};
PrimitiveHandle {
layer,
inst_idx: inst_i,
data_idx: 0,
binding: IMAGE_BINDING,
}
}
pub fn image_instances(&self) -> &Vec<PrimitiveInstance> {
&self.images
}
/// returns (old index, new index) for both lists this layer keeps --
/// `PrimitiveChange::is_image` says which, since the two have separate
/// index spaces and `old`/`new` alone would collide between them.
///
/// Both lists free with `swap_remove`, so a layer's draw order was
/// already undefined before images existed: nothing here may assume one
/// primitive stays adjacent to another once anything in the layer has
/// been freed.
pub fn apply_free(&mut self) -> Vec<PrimitiveChange> {
let mut changes =
Self::apply_free_list(&mut self.free, &mut self.instances, &mut self.assoc, false);
changes.extend(Self::apply_free_list(
&mut self.image_free,
&mut self.images,
&mut self.image_assoc,
true,
));
changes
}
fn apply_free_list(
free: &mut Vec<usize>,
instances: &mut Vec<PrimitiveInstance>,
assoc: &mut Vec<WidgetId>,
is_image: bool,
) -> Vec<PrimitiveChange> {
free.sort_by(|a, b| b.cmp(a));
free.drain(..)
.filter_map(|i| {
instances.swap_remove(i);
assoc.swap_remove(i);
if i == instances.len() {
return None;
}
let id = assoc[i];
let old = instances.len();
Some(PrimitiveChange {
id,
is_image,
old,
new: i,
})
})
.collect()
}
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self.updated = true;
self.data.free(h.binding, h.data_idx);
self.free.push(h.inst_idx);
self.instances[h.inst_idx].mask_idx
if h.binding == IMAGE_BINDING {
self.image_free.push(h.inst_idx);
self.images[h.inst_idx].mask_idx
} else {
self.data.free(h.binding, h.data_idx);
self.free.push(h.inst_idx);
self.instances[h.inst_idx].mask_idx
}
}
pub fn data(&self) -> &PrimitiveData {
@@ -170,12 +276,21 @@ impl Primitives {
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
self.updated = true;
&mut self.instances[h.inst_idx].region
if h.binding == IMAGE_BINDING {
&mut self.images[h.inst_idx].region
} else {
&mut self.instances[h.inst_idx].region
}
}
}
pub struct PrimitiveChange {
pub id: WidgetId,
/// Which of `Primitives::instances`/`Primitives::images` this change
/// belongs to -- their `old`/`new` indices are independent, so a
/// consumer matching only on `(layer, inst_idx)` could apply an image's
/// renumbering to a rect's handle that happens to share the same index.
pub is_image: bool,
pub old: usize,
pub new: usize,
}
@@ -201,7 +316,6 @@ impl PrimitiveHandle {
primitives!(
rects: RectPrimitive => 0,
textures: TexturePrimitive => 1,
glyphs: GlyphPrimitive => 2,
);
@@ -225,22 +339,42 @@ impl RectPrimitive {
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct TexturePrimitive {
pub view_idx: u32,
pub sampler_idx: u32,
}
/// One glyph, drawn as a sub-rectangle of the glyph atlas array.
///
/// `color` is the text colour and is multiplied by the atlas's alpha for an
/// ordinary mask glyph; a colour glyph (emoji) carries its own colour and
/// takes the atlas texel unchanged, which is what `GlyphEntry::IS_COLORED`
/// selects.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive {
pub uv_min: Vec2,
pub uv_max: Vec2,
pub view_idx: u32,
pub sampler_idx: u32,
/// Layer of the shared atlas array texture this glyph's page occupies --
/// not a bind-group or view index, since a page never gets one of its own.
pub layer: u32,
pub color: Color<u8>,
pub flags: u32,
/// Pads this struct's Rust size to match WGSL's storage-buffer layout for
/// `GlyphInfo`: two `vec2<f32>` members give the struct an 8-byte
/// alignment, which rounds the WGSL size up to 32 bytes even though the
/// fields above only total 28. `bytemuck` does not check this for us.
_pad: u32,
}
impl GlyphPrimitive {
/// The only constructor, since `_pad` is private: callers outside this
/// module cannot write the struct literal.
pub fn new(uv_min: Vec2, uv_max: Vec2, layer: u32, color: Color<u8>, flags: u32) -> Self {
Self {
uv_min,
uv_max,
layer,
color,
flags,
_pad: 0,
}
}
}
pub struct PrimitiveVec<T> {