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

+12
View File
@@ -1,6 +1,7 @@
use std::ops::{Index, IndexMut};
use crate::{
UiRegion, WidgetId,
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
util::to_mut,
};
@@ -131,6 +132,17 @@ impl PrimitiveLayers {
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self[h.layer].free(h)
}
pub fn write_image(
&mut self,
layer: LayerId,
id: WidgetId,
texture_idx: u32,
region: UiRegion,
mask_idx: MaskIdx,
) -> PrimitiveHandle {
self[layer].write_image(layer, id, texture_idx, region, mask_idx)
}
}
impl<T: Default> Default for Layers<T> {
+99 -38
View File
@@ -1,19 +1,32 @@
use crate::{
render::TexturePrimitive,
util::{RefCounter, Vec2},
};
use crate::util::{RefCounter, Vec2};
use image::{DynamicImage, GenericImageView};
use std::{
ops::Index,
sync::mpsc::{Receiver, Sender, channel},
};
/// Which of the two things a texture slot holds. The two are drawn very
/// differently: a page is a layer of one shared array texture and never gets
/// its own bind group; a standalone image is the opposite, one texture and
/// one bind group, never a layer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextureKind {
Image,
/// The array-texture layer this page was assigned. Chosen synchronously
/// by `Textures::add_page` rather than by the renderer, because glyph
/// insertion needs it in the same call, before any GPU sync happens.
Page {
layer: u32,
},
}
#[derive(Debug, Clone)]
pub struct TextureHandle {
inner: TexturePrimitive,
slot: u32,
kind: TextureKind,
size: Vec2,
counter: RefCounter,
send: Sender<u32>,
send: Sender<(TextureKind, u32)>,
}
/// a texture manager for a ui
@@ -21,17 +34,24 @@ pub struct TextureHandle {
pub struct Textures {
free: Vec<u32>,
images: Vec<Option<DynamicImage>>,
/// Next layer to hand out to an atlas page. Pages are never freed (no
/// atlas eviction), so this only grows and `free` never holds one.
next_page_layer: u32,
updates: Vec<Update>,
send: Sender<u32>,
recv: Receiver<u32>,
send: Sender<(TextureKind, u32)>,
recv: Receiver<(TextureKind, u32)>,
}
pub enum TextureUpdate<'a> {
Push(&'a DynamicImage),
Set(u32, &'a DynamicImage),
Push(TextureKind, &'a DynamicImage),
Set(TextureKind, u32, &'a DynamicImage),
/// Overwrite a rectangle of an existing texture, rather than replacing it.
/// The glyph atlas grows a glyph at a time, and re-uploading a whole atlas
/// per glyph is megabytes of copy for a few hundred bytes of change.
/// Only ever issued against a page -- a standalone image is never patched.
Patch(u32, PatchRect, &'a DynamicImage),
Free(u32),
PushFree,
PushFree(TextureKind),
SetFree,
}
@@ -44,8 +64,8 @@ pub struct PatchRect {
}
enum Update {
Push(u32),
Set(u32),
Push(TextureKind, u32),
Set(TextureKind, u32),
Patch(u32, PatchRect),
Free(u32),
}
@@ -56,70 +76,93 @@ impl Textures {
Self {
free: Vec::new(),
images: Vec::new(),
next_page_layer: 0,
updates: Vec::new(),
send,
recv,
}
}
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
let image = image.into();
let size = image.dimensions().into();
let view_idx = self.push(image);
// 0 == default in renderer; TODO: actually create samplers here
let sampler_idx = 0;
let kind = TextureKind::Image;
let slot = self.push(kind, image);
TextureHandle {
inner: TexturePrimitive {
view_idx,
sampler_idx,
},
slot,
kind,
size,
counter: RefCounter::new(),
send: self.send.clone(),
}
}
fn push(&mut self, image: DynamicImage) -> u32 {
/// Adds a page of the shared glyph atlas array. Only `atlas.rs` should
/// call this -- everything else wants `add`.
pub fn add_page(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
let image = image.into();
let size = image.dimensions().into();
let layer = self.next_page_layer;
self.next_page_layer += 1;
let kind = TextureKind::Page { layer };
let slot = self.push(kind, image);
TextureHandle {
slot,
kind,
size,
counter: RefCounter::new(),
send: self.send.clone(),
}
}
fn push(&mut self, kind: TextureKind, image: DynamicImage) -> u32 {
if let Some(i) = self.free.pop() {
self.images[i as usize] = Some(image);
self.updates.push(Update::Set(i));
self.updates.push(Update::Set(kind, i));
i
} else {
let i = self.images.len() as u32;
self.images.push(Some(image));
self.updates.push(Update::Push(i));
self.updates.push(Update::Push(kind, i));
i
}
}
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
self.images[handle.inner.view_idx as usize]
self.images[handle.slot as usize]
.as_mut()
.expect("texture was freed while still held")
}
/// Queue an upload of just `rect`, after writing it with `image_mut`.
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
self.updates
.push(Update::Patch(handle.inner.view_idx, rect));
self.updates.push(Update::Patch(handle.slot, rect));
}
pub fn free(&mut self) {
for idx in self.recv.try_iter() {
for (kind, idx) in self.recv.try_iter() {
self.images[idx as usize] = None;
self.updates.push(Update::Free(idx));
self.free.push(idx);
// A page's slot is never reclaimed: `GlyphAtlas` never drops the
// handles it holds, and there is no eviction path for a hole in
// the middle of the array's layers. So `free` holds ordinary
// image slots only, and a page's layer would need a free list of
// its own were that to change.
if kind == TextureKind::Image {
self.free.push(idx);
}
}
}
pub fn updates(&mut self) -> impl Iterator<Item = TextureUpdate<'_>> {
self.updates.drain(..).map(|u| match u {
Update::Push(i) => self.images[i as usize]
Update::Push(kind, i) => self.images[i as usize]
.as_ref()
.map(TextureUpdate::Push)
.unwrap_or(TextureUpdate::PushFree),
Update::Set(i) => self.images[i as usize]
.map(|img| TextureUpdate::Push(kind, img))
.unwrap_or(TextureUpdate::PushFree(kind)),
Update::Set(kind, i) => self.images[i as usize]
.as_ref()
.map(|img| TextureUpdate::Set(i, img))
.map(|img| TextureUpdate::Set(kind, i, img))
.unwrap_or(TextureUpdate::SetFree),
Update::Patch(i, rect) => self.images[i as usize]
.as_ref()
@@ -131,18 +174,36 @@ impl Textures {
}
impl TextureHandle {
pub fn primitive(&self) -> TexturePrimitive {
self.inner
}
pub fn size(&self) -> Vec2 {
self.size
}
/// The bind-group index this handle draws with. Only valid for a
/// standalone image; an atlas page has no bind group of its own -- it
/// samples the shared array via `layer()` instead. Getting this wrong is
/// a caller bug (the wrong kind of handle reached the wrong draw path),
/// not a recoverable condition, so it panics rather than drawing garbage.
pub fn image_index(&self) -> u32 {
match self.kind {
TextureKind::Image => self.slot,
TextureKind::Page { .. } => panic!("image_index() called on an atlas page handle"),
}
}
/// The layer this page occupies in the shared atlas array texture.
/// Only valid for a page handle; see `image_index`'s note.
pub fn layer(&self) -> u32 {
match self.kind {
TextureKind::Page { layer } => layer,
TextureKind::Image => panic!("layer() called on a standalone image handle"),
}
}
}
impl Drop for TextureHandle {
fn drop(&mut self) {
if self.counter.drop() {
let _ = self.send.send(self.inner.view_idx);
let _ = self.send.send((self.kind, self.slot));
}
}
}
@@ -151,7 +212,7 @@ impl Index<&TextureHandle> for Textures {
type Output = DynamicImage;
fn index(&self, index: &TextureHandle) -> &Self::Output {
self.images[index.inner.view_idx as usize].as_ref().unwrap()
self.images[index.slot as usize].as_ref().unwrap()
}
}