Replaces the bindless `binding_array<texture_2d<f32>>` the renderer bound every texture through. That array needs `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack, so the old shape did not run there at all. The two things being bound want opposite treatment, so they are now split: - **Glyph atlas pages become layers of one `texture_2d_array`.** A glyph primitive carries a `layer` instead of a view/sampler index pair. A layer index is an ordinary sampling operand, so this needs nothing beyond plain Vulkan 1.0 / GLES. Growing the atlas recreates the array with headroom and `copy_texture_to_texture`s the old layers across, no readback. - **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`: the bind group has already picked the texture. `Primitives` keeps images in a list of their own as a result, with `PrimitiveChange::is_image` naming which list a renumbering belongs to -- the two have independent index spaces, so `(layer, inst_idx)` alone would collide between them. Two notes on judgement calls, since this slice was rebuilt on top of `main` rather than transplanted: - The source version renamed `GlyphEntry::is_colored` to `is_color` and added a second `IS_COLOR` flag constant beside the existing `GlyphEntry::IS_COLORED`. Both dropped: #10's naming and its `flags()` are kept, and UVs stay `Vec2` rather than going back to `[f32; 2]`. - `ImageGpu` no longer holds the `Texture` behind its view, which removes an `#[allow(dead_code)]`. A `TextureView` keeps its own reference to the texture, checked by rendering rather than assumed -- see below. ### Verification ``` cargo fmt --all --check cargo clippy --workspace --all-targets --locked -- -D warnings cargo test --workspace --locked ``` All clean; the 4 text-edit tests pass. The only clippy output is the pre-existing future-incompatibility notice about `naga`/`wgpu`/`winit`. Because this is a rendering change, it was also run for real rather than only compiled. The `tabs` example was rendered on this machine's GPU -- Venus onto an RX 7900 XT, confirmed from the loaded ICD (`libvulkan_virtio.so` on `/dev/dri/renderD128`) rather than assumed, since a failed Vulkan init here silently falls back to llvmpipe and would make the screenshots meaningless. Screenshots before and after the change are **byte-identical** (same md5) in two scenes: the default tab, which exercises text (the atlas path) and rects, and the image tab with a standalone image pushed at startup, which exercises the per-image bind group. The image-tab scene needed a temporary local edit to the example to push the image without a click; that edit is not part of this branch. The same comparison, re-run after dropping the `Texture` field, is still byte-identical -- which is the check that the view alone keeps it alive. --------- Co-authored-by: iris <2+iris@noreply.localhost> Reviewed-on: iris/iris#11 Reviewed-by: iris <2+iris@noreply.localhost> Co-authored-by: AIris <4+iris-ai@noreply.localhost>
162 lines
4.3 KiB
Rust
162 lines
4.3 KiB
Rust
use crate::util::{RefCounter, Vec2};
|
|
use image::{DynamicImage, GenericImageView};
|
|
use std::{
|
|
ops::Index,
|
|
sync::mpsc::{Receiver, Sender, channel},
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct TextureHandle {
|
|
slot: u32,
|
|
size: Vec2,
|
|
counter: RefCounter,
|
|
send: Sender<u32>,
|
|
}
|
|
|
|
/// a texture manager for a ui
|
|
/// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped
|
|
pub struct Textures {
|
|
free: Vec<u32>,
|
|
images: Vec<Option<DynamicImage>>,
|
|
updates: Vec<Update>,
|
|
send: Sender<u32>,
|
|
recv: Receiver<u32>,
|
|
}
|
|
|
|
pub enum TextureUpdate<'a> {
|
|
Push(&'a DynamicImage),
|
|
Set(u32, &'a DynamicImage),
|
|
Patch(u32, PatchRect, &'a DynamicImage),
|
|
Free(u32),
|
|
/// Added and freed before the renderer drained either update. It still has
|
|
/// to push a slot to stay lined up with `images`; `Free` then empties it.
|
|
PushFree,
|
|
SetFree,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct PatchRect {
|
|
pub x: u32,
|
|
pub y: u32,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
}
|
|
|
|
enum Update {
|
|
Push(u32),
|
|
Set(u32),
|
|
Patch(u32, PatchRect),
|
|
Free(u32),
|
|
}
|
|
|
|
impl Textures {
|
|
pub fn new() -> Self {
|
|
let (send, recv) = channel();
|
|
Self {
|
|
free: Vec::new(),
|
|
images: Vec::new(),
|
|
updates: Vec::new(),
|
|
send,
|
|
recv,
|
|
}
|
|
}
|
|
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
|
let image = image.into();
|
|
let size = image.dimensions().into();
|
|
TextureHandle {
|
|
slot: self.push(image),
|
|
size,
|
|
counter: RefCounter::new(),
|
|
send: self.send.clone(),
|
|
}
|
|
}
|
|
|
|
fn push(&mut self, image: DynamicImage) -> u32 {
|
|
if let Some(i) = self.free.pop() {
|
|
self.images[i as usize] = Some(image);
|
|
self.updates.push(Update::Set(i));
|
|
i
|
|
} else {
|
|
let i = self.images.len() as u32;
|
|
self.images.push(Some(image));
|
|
self.updates.push(Update::Push(i));
|
|
i
|
|
}
|
|
}
|
|
|
|
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
|
|
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.slot, rect));
|
|
}
|
|
|
|
/// How many textures are live, which is what a ui can ask; the renderer's
|
|
/// copies follow from the updates it drains.
|
|
pub fn count(&self) -> usize {
|
|
self.images.iter().flatten().count()
|
|
}
|
|
|
|
pub fn free(&mut self) {
|
|
for idx in self.recv.try_iter() {
|
|
self.images[idx as usize] = None;
|
|
self.updates.push(Update::Free(idx));
|
|
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]
|
|
.as_ref()
|
|
.map(TextureUpdate::Push)
|
|
.unwrap_or(TextureUpdate::PushFree),
|
|
Update::Set(i) => self.images[i as usize]
|
|
.as_ref()
|
|
.map(|img| TextureUpdate::Set(i, img))
|
|
.unwrap_or(TextureUpdate::SetFree),
|
|
Update::Patch(i, rect) => self.images[i as usize]
|
|
.as_ref()
|
|
.map(|img| TextureUpdate::Patch(i, rect, img))
|
|
.unwrap_or(TextureUpdate::SetFree),
|
|
Update::Free(i) => TextureUpdate::Free(i),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl TextureHandle {
|
|
/// Index into `Textures`, and into the renderer's parallel slots.
|
|
pub fn slot(&self) -> u32 {
|
|
self.slot
|
|
}
|
|
pub fn size(&self) -> Vec2 {
|
|
self.size
|
|
}
|
|
}
|
|
|
|
impl Drop for TextureHandle {
|
|
fn drop(&mut self) {
|
|
if self.counter.drop() {
|
|
let _ = self.send.send(self.slot);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Index<&TextureHandle> for Textures {
|
|
type Output = DynamicImage;
|
|
|
|
fn index(&self, index: &TextureHandle) -> &Self::Output {
|
|
self.images[index.slot as usize].as_ref().unwrap()
|
|
}
|
|
}
|
|
|
|
impl Default for Textures {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|