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:
1 parent
0f6a28b4dd
commit
bafaa1db6d
10 files changed
+794
-255
No files matched your search
+161
-27
@@ -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> {
|
||||
|
||||
Reference in new issue
Block a user