Separate atlas pages from textures, and draw both through one instance list
Rework of the review on #11. Pages and standalone images were one `Textures` manager separated by a `TextureKind` tag, and images were a second instance list beside `Primitives::instances`. The tag forced `image_index()`/`layer()` to panic on the wrong kind of handle, and the second list forced an `is_image` branch through `free`, `region_mut`, `apply_free` and `PrimitiveChange`. Pages are now their own thing. `GlyphAtlas` owns its page images outright and hands the renderer dirty rectangles; `GpuPages` owns the array texture they upload to. `Textures` is standalone images only, so `TextureHandle` has one kind, `slot()` cannot be wrong, and nothing needs a free list that skips pages. `GlyphAtlas::insert` no longer takes a `Textures`, which drops that parameter from `TextData::render` and `SizeCtx` too. Images go back through the one instance list. A texture instance is an ordinary `PrimitiveInstance` whose `idx` names a texture rather than a group-1 entry, which `PrimitiveHandle::data_idx: Option` records. `RenderLayer::plan_draws` batches the layer's instances into runs sharing a bind group, so a ui with no images still plans a single draw, and an image draws in instance order rather than on top of its layer. Group 2 is now one `texture_2d_array` and a sampler, bound per run: the atlas for rects and glyphs, or one image viewed as an array of one. That removes the second texture binding and the 1x1 null view that had to fill it. Masks move to group 3, so resizing that buffer no longer stales every texture bind group, and `GpuTextures` no longer reports whether the caller must rebuild one. `GlyphPrimitive` drops its manual pad: the WGSL struct now declares the uvs as scalars, which matches the Rust layout exactly. `#[repr(C, align(8))]` would have left real padding bytes, which `bytemuck::Pod` forbids. Verified with a headless run of the `tabs` example and of a scratch example mixing images, rects, glyphs and a mask in one layer; 52 glyphs at size 300 grew the atlas array from 1 to 4 layers with every earlier page still sampling correctly.
This commit is contained in:
1 parent
bafaa1db6d
commit
0106257be0
15 files changed
+501
-889
No files matched your search
@@ -133,15 +133,15 @@ impl PrimitiveLayers {
|
||||
self[h.layer].free(h)
|
||||
}
|
||||
|
||||
pub fn write_image(
|
||||
pub fn write_texture(
|
||||
&mut self,
|
||||
layer: LayerId,
|
||||
id: WidgetId,
|
||||
texture_idx: u32,
|
||||
texture: u32,
|
||||
region: UiRegion,
|
||||
mask_idx: MaskIdx,
|
||||
) -> PrimitiveHandle {
|
||||
self[layer].write_image(layer, id, texture_idx, region, mask_idx)
|
||||
self[layer].write_texture(layer, id, texture, region, mask_idx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::{
|
||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor,
|
||||
util::Vec2,
|
||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
|
||||
};
|
||||
use parley::{
|
||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
||||
@@ -159,7 +158,7 @@ impl TextBuffer {
|
||||
}
|
||||
|
||||
impl TextData {
|
||||
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
|
||||
pub fn place(&mut self, buffer: &TextBuffer) -> Vec<PlacedGlyph> {
|
||||
let mut placed = Vec::new();
|
||||
for line in buffer.layout.lines() {
|
||||
for item in line.items() {
|
||||
@@ -185,17 +184,14 @@ impl TextData {
|
||||
subpixel,
|
||||
coords: coords_hash,
|
||||
};
|
||||
let Some(entry) = self.glyph_entry(
|
||||
GlyphRaster {
|
||||
let Some(entry) = self.glyph_entry(GlyphRaster {
|
||||
key,
|
||||
font: font_ref,
|
||||
font_size,
|
||||
coords,
|
||||
subpixel,
|
||||
glyph_id: glyph.id,
|
||||
},
|
||||
textures,
|
||||
) else {
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
placed.push(PlacedGlyph {
|
||||
@@ -211,11 +207,7 @@ impl TextData {
|
||||
placed
|
||||
}
|
||||
|
||||
fn glyph_entry(
|
||||
&mut self,
|
||||
glyph: GlyphRaster<'_>,
|
||||
textures: &mut Textures,
|
||||
) -> Option<GlyphEntry> {
|
||||
fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option<GlyphEntry> {
|
||||
if let Some(entry) = self.atlas.get(&glyph.key) {
|
||||
return entry;
|
||||
}
|
||||
@@ -237,7 +229,7 @@ impl TextData {
|
||||
.render(&mut scaler, glyph.glyph_id as u16);
|
||||
|
||||
if let Some(image) = image {
|
||||
self.atlas.insert(glyph.key, &image, textures)
|
||||
self.atlas.insert(glyph.key, &image)
|
||||
} else {
|
||||
self.atlas.insert_empty(glyph.key);
|
||||
None
|
||||
@@ -278,10 +270,9 @@ impl TextData {
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
textures: &mut Textures,
|
||||
) -> RenderedText {
|
||||
buffer.shape(self, attrs, width);
|
||||
let glyphs = self.place(buffer, textures);
|
||||
let glyphs = self.place(buffer);
|
||||
RenderedText {
|
||||
glyphs,
|
||||
size: buffer.size(),
|
||||
|
||||
@@ -5,28 +5,12 @@ use std::{
|
||||
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 {
|
||||
slot: u32,
|
||||
kind: TextureKind,
|
||||
size: Vec2,
|
||||
counter: RefCounter,
|
||||
send: Sender<(TextureKind, u32)>,
|
||||
send: Sender<u32>,
|
||||
}
|
||||
|
||||
/// a texture manager for a ui
|
||||
@@ -34,24 +18,19 @@ 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<(TextureKind, u32)>,
|
||||
recv: Receiver<(TextureKind, u32)>,
|
||||
send: Sender<u32>,
|
||||
recv: Receiver<u32>,
|
||||
}
|
||||
|
||||
pub enum TextureUpdate<'a> {
|
||||
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.
|
||||
Push(&'a DynamicImage),
|
||||
Set(u32, &'a DynamicImage),
|
||||
Patch(u32, PatchRect, &'a DynamicImage),
|
||||
Free(u32),
|
||||
PushFree(TextureKind),
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -64,8 +43,8 @@ pub struct PatchRect {
|
||||
}
|
||||
|
||||
enum Update {
|
||||
Push(TextureKind, u32),
|
||||
Set(TextureKind, u32),
|
||||
Push(u32),
|
||||
Set(u32),
|
||||
Patch(u32, PatchRect),
|
||||
Free(u32),
|
||||
}
|
||||
@@ -76,54 +55,31 @@ 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 kind = TextureKind::Image;
|
||||
let slot = self.push(kind, image);
|
||||
TextureHandle {
|
||||
slot,
|
||||
kind,
|
||||
slot: self.push(image),
|
||||
size,
|
||||
counter: RefCounter::new(),
|
||||
send: self.send.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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(kind, i));
|
||||
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(kind, i));
|
||||
self.updates.push(Update::Push(i));
|
||||
i
|
||||
}
|
||||
}
|
||||
@@ -140,29 +96,22 @@ impl Textures {
|
||||
}
|
||||
|
||||
pub fn free(&mut self) {
|
||||
for (kind, idx) in self.recv.try_iter() {
|
||||
for idx in self.recv.try_iter() {
|
||||
self.images[idx as usize] = None;
|
||||
self.updates.push(Update::Free(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(kind, i) => self.images[i as usize]
|
||||
Update::Push(i) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Push(kind, img))
|
||||
.unwrap_or(TextureUpdate::PushFree(kind)),
|
||||
Update::Set(kind, i) => self.images[i as usize]
|
||||
.map(TextureUpdate::Push)
|
||||
.unwrap_or(TextureUpdate::PushFree),
|
||||
Update::Set(i) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Set(kind, i, img))
|
||||
.map(|img| TextureUpdate::Set(i, img))
|
||||
.unwrap_or(TextureUpdate::SetFree),
|
||||
Update::Patch(i, rect) => self.images[i as usize]
|
||||
.as_ref()
|
||||
@@ -174,36 +123,19 @@ impl Textures {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// 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.kind, self.slot));
|
||||
let _ = self.send.send(self.slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-34
@@ -1,15 +1,11 @@
|
||||
use crate::{
|
||||
PatchRect, TextureHandle, Textures,
|
||||
PatchRect,
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use image::RgbaImage;
|
||||
use swash::scale::image::{Content, Image};
|
||||
|
||||
/// Side of one atlas page, in pixels. 1024 is 4 MB at RGBA8 -- enough for a
|
||||
/// few thousand glyphs at UI sizes, and small enough that a page nobody fills
|
||||
/// is not a big waste. Also the fixed width/height of every layer of the
|
||||
/// shared array texture in `render::texture` -- `pub(crate)` so that module
|
||||
/// can size it without a second constant to keep in sync.
|
||||
/// Side of one page, and so of every layer of `render::page`'s array texture.
|
||||
pub(crate) const PAGE: u32 = 1024;
|
||||
|
||||
/// Transparent margin kept around every glyph, so that sampling one cannot
|
||||
@@ -40,7 +36,7 @@ pub struct GlyphEntry {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub is_colored: bool,
|
||||
/// The atlas array layer this glyph's page occupies.
|
||||
/// Which atlas array layer this glyph is on.
|
||||
pub layer: u32,
|
||||
}
|
||||
|
||||
@@ -53,18 +49,26 @@ impl GlyphEntry {
|
||||
}
|
||||
|
||||
struct Page {
|
||||
handle: TextureHandle,
|
||||
image: RgbaImage,
|
||||
x: u32,
|
||||
y: u32,
|
||||
shelf_height: u32,
|
||||
}
|
||||
|
||||
/// A rectangle of one page the renderer has not uploaded yet.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PageUpload {
|
||||
pub layer: u32,
|
||||
pub rect: PatchRect,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GlyphAtlas {
|
||||
pages: Vec<Page>,
|
||||
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
||||
/// too, so it is not re-rasterised on every layout.
|
||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||
uploads: Vec<PageUpload>,
|
||||
}
|
||||
|
||||
impl GlyphAtlas {
|
||||
@@ -72,12 +76,7 @@ impl GlyphAtlas {
|
||||
self.entries.get(key).copied()
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
key: GlyphKey,
|
||||
image: &Image,
|
||||
textures: &mut Textures,
|
||||
) -> Option<GlyphEntry> {
|
||||
pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option<GlyphEntry> {
|
||||
let w = image.placement.width;
|
||||
let h = image.placement.height;
|
||||
if w == 0 || h == 0 {
|
||||
@@ -99,23 +98,18 @@ impl GlyphAtlas {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (page_idx, x, y) = self.allocate(w, h, textures);
|
||||
let page = &self.pages[page_idx];
|
||||
|
||||
let img = textures.image_mut(&page.handle);
|
||||
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
|
||||
write_glyph(rgba, image, x, y);
|
||||
|
||||
let handle = page.handle.clone();
|
||||
let rect = PatchRect {
|
||||
let (layer, x, y) = self.allocate(w, h);
|
||||
write_glyph(&mut self.pages[layer as usize].image, image, x, y);
|
||||
self.uploads.push(PageUpload {
|
||||
layer,
|
||||
rect: PatchRect {
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
};
|
||||
textures.patch(&handle, rect);
|
||||
},
|
||||
});
|
||||
|
||||
let page = &self.pages[page_idx];
|
||||
let scale = 1.0 / PAGE as f32;
|
||||
let entry = GlyphEntry {
|
||||
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
|
||||
@@ -125,38 +119,47 @@ impl GlyphAtlas {
|
||||
width: w,
|
||||
height: h,
|
||||
is_colored: matches!(image.content, Content::Color),
|
||||
layer: page.handle.layer(),
|
||||
layer,
|
||||
};
|
||||
self.entries.insert(key, Some(entry));
|
||||
Some(entry)
|
||||
}
|
||||
|
||||
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
|
||||
fn allocate(&mut self, w: u32, h: u32) -> (u32, u32, u32) {
|
||||
if let Some((i, (x, y))) = self
|
||||
.pages
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
|
||||
{
|
||||
return (i, x, y);
|
||||
return (i as u32, x, y);
|
||||
}
|
||||
|
||||
let handle = textures.add_page(RgbaImage::new(PAGE, PAGE));
|
||||
self.pages.push(Page {
|
||||
handle,
|
||||
image: RgbaImage::new(PAGE, PAGE),
|
||||
x: PAD + w + PAD,
|
||||
y: PAD,
|
||||
shelf_height: h + PAD,
|
||||
});
|
||||
(self.pages.len() - 1, PAD, PAD)
|
||||
(self.pages.len() as u32 - 1, PAD, PAD)
|
||||
}
|
||||
|
||||
/// Drains what has been written since the last call, for the renderer to
|
||||
/// upload. Nothing else is needed for a new page: wgpu leaves the rest of
|
||||
/// a fresh layer transparent, which is what an atlas wants.
|
||||
pub fn uploads(&mut self) -> impl Iterator<Item = (PageUpload, &RgbaImage)> {
|
||||
let pages = &self.pages;
|
||||
self.uploads
|
||||
.drain(..)
|
||||
.map(|upload| (upload, &pages[upload.layer as usize].image))
|
||||
}
|
||||
|
||||
pub fn insert_empty(&mut self, key: GlyphKey) {
|
||||
self.entries.insert(key, None);
|
||||
}
|
||||
|
||||
pub fn page_count(&self) -> usize {
|
||||
self.pages.len()
|
||||
pub fn page_count(&self) -> u32 {
|
||||
self.pages.len() as u32
|
||||
}
|
||||
|
||||
pub fn glyph_count(&self) -> usize {
|
||||
|
||||
+108
-119
@@ -1,6 +1,13 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::{
|
||||
UiData, UiRenderState,
|
||||
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
|
||||
render::{
|
||||
data::PrimitiveInstance,
|
||||
page::GpuPages,
|
||||
texture::{GpuTextures, default_sampler},
|
||||
util::ArrBuf,
|
||||
},
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use data::WindowUniform;
|
||||
@@ -11,6 +18,7 @@ use wgpu::{
|
||||
|
||||
mod atlas;
|
||||
mod data;
|
||||
mod page;
|
||||
mod primitive;
|
||||
mod texture;
|
||||
mod util;
|
||||
@@ -24,14 +32,17 @@ const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
||||
pub struct UiRenderNode {
|
||||
uniform_group: BindGroup,
|
||||
primitive_layout: BindGroupLayout,
|
||||
rsc_layout: BindGroupLayout,
|
||||
rsc_group: BindGroup,
|
||||
texture_layout: BindGroupLayout,
|
||||
mask_layout: BindGroupLayout,
|
||||
mask_group: BindGroup,
|
||||
|
||||
pipeline: RenderPipeline,
|
||||
|
||||
layers: HashMap<usize, RenderLayer>,
|
||||
active: Vec<usize>,
|
||||
window_buffer: Buffer,
|
||||
sampler: Sampler,
|
||||
pages: GpuPages,
|
||||
textures: GpuTextures,
|
||||
masks: ArrBuf<Mask>,
|
||||
}
|
||||
@@ -40,43 +51,38 @@ struct RenderLayer {
|
||||
instance: ArrBuf<PrimitiveInstance>,
|
||||
primitives: PrimitiveBuffers,
|
||||
primitive_group: BindGroup,
|
||||
/// A standalone image's instances, kept apart from `instance` because
|
||||
/// each one draws with its own bind group -- see `UiRenderNode::draw`.
|
||||
image_instance: ArrBuf<PrimitiveInstance>,
|
||||
/// The texture slot each entry of `image_instance` draws with, in the
|
||||
/// same order, refreshed alongside it. Not stored in the vertex buffer
|
||||
/// itself because it names a bind group, not shader data.
|
||||
image_tex_indices: Vec<u32>,
|
||||
draws: Vec<Draw>,
|
||||
}
|
||||
|
||||
/// A run of consecutive instances sharing one group 2: `texture` names a
|
||||
/// standalone image, or `None` is the glyph atlas rects and glyphs sample.
|
||||
struct Draw {
|
||||
texture: Option<u32>,
|
||||
instances: Range<u32>,
|
||||
}
|
||||
|
||||
impl UiRenderNode {
|
||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_group, &[]);
|
||||
pass.set_bind_group(3, &self.mask_group, &[]);
|
||||
for i in &self.active {
|
||||
let layer = &self.layers[i];
|
||||
if layer.instance.len() == 0 && layer.image_instance.len() == 0 {
|
||||
if layer.draws.is_empty() {
|
||||
continue;
|
||||
}
|
||||
pass.set_bind_group(1, &layer.primitive_group, &[]);
|
||||
if layer.instance.len() > 0 {
|
||||
pass.set_bind_group(2, &self.rsc_group, &[]);
|
||||
pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
|
||||
pass.draw(0..4, 0..layer.instance.len() as u32);
|
||||
}
|
||||
// Images draw after this layer's rects and glyphs, one draw call
|
||||
// each with its own bind group. That draws every image "on top"
|
||||
// within the layer, which loses nothing that currently exists:
|
||||
// `Primitives::apply_free` frees with `swap_remove`, so a layer's
|
||||
// draw order was already undefined before images had their own
|
||||
// list -- nothing before this relied on interleaving a rect
|
||||
// between two images at a particular position.
|
||||
if layer.image_instance.len() > 0 {
|
||||
pass.set_vertex_buffer(0, layer.image_instance.buffer.slice(..));
|
||||
for (k, &tex_idx) in layer.image_tex_indices.iter().enumerate() {
|
||||
pass.set_bind_group(2, self.textures.image_bind_group(tex_idx), &[]);
|
||||
pass.draw(0..4, k as u32..k as u32 + 1);
|
||||
}
|
||||
for draw in &layer.draws {
|
||||
let group = match draw.texture {
|
||||
Some(slot) => match self.textures.group(slot) {
|
||||
Some(group) => group,
|
||||
None => continue,
|
||||
},
|
||||
None => self.pages.group(),
|
||||
};
|
||||
pass.set_bind_group(2, group, &[]);
|
||||
pass.draw(0..4, draw.instances.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,15 +100,7 @@ impl UiRenderNode {
|
||||
for change in primitives.apply_free() {
|
||||
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
||||
for h in &mut inst.primitives {
|
||||
// `is_image` disambiguates: `instances` and `images`
|
||||
// are separate lists with independent indices, so
|
||||
// without it a rect's renumbering could be applied to
|
||||
// an image handle that happened to share the same
|
||||
// (layer, inst_idx).
|
||||
if h.layer == i
|
||||
&& h.inst_idx == change.old
|
||||
&& (h.binding == IMAGE_BINDING) == change.is_image
|
||||
{
|
||||
if h.layer == i && h.inst_idx == change.old {
|
||||
h.inst_idx = change.new;
|
||||
break;
|
||||
}
|
||||
@@ -121,12 +119,7 @@ impl UiRenderNode {
|
||||
),
|
||||
primitives,
|
||||
primitive_group,
|
||||
image_instance: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
||||
"image instance",
|
||||
),
|
||||
image_tex_indices: Vec::new(),
|
||||
draws: Vec::new(),
|
||||
}
|
||||
});
|
||||
if primitives.updated {
|
||||
@@ -139,33 +132,21 @@ impl UiRenderNode {
|
||||
&self.primitive_layout,
|
||||
rlayer.primitives.buffers(),
|
||||
);
|
||||
rlayer
|
||||
.image_instance
|
||||
.update(device, queue, primitives.image_instances());
|
||||
rlayer.image_tex_indices = primitives
|
||||
.image_instances()
|
||||
.iter()
|
||||
.map(|inst| inst.idx)
|
||||
.collect();
|
||||
rlayer.plan_draws(primitives.instances());
|
||||
primitives.updated = false;
|
||||
}
|
||||
}
|
||||
let masks_resized = if ui.masks.changed {
|
||||
if ui.masks.changed {
|
||||
ui.masks.changed = false;
|
||||
self.masks.update(device, queue, &ui.masks[..])
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let rebuild_main = self.textures.update(
|
||||
&mut ui.textures,
|
||||
&self.rsc_layout,
|
||||
&self.masks,
|
||||
masks_resized,
|
||||
);
|
||||
if rebuild_main {
|
||||
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks);
|
||||
if self.masks.update(device, queue, &ui.masks[..]) {
|
||||
self.mask_group = Self::mask_group(device, &self.mask_layout, &self.masks);
|
||||
}
|
||||
}
|
||||
self.pages
|
||||
.update(&mut ui.text.atlas, &self.texture_layout, &self.sampler);
|
||||
self.textures
|
||||
.update(&mut ui.textures, &self.texture_layout, &self.sampler);
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
||||
let size = size.into();
|
||||
@@ -222,19 +203,28 @@ impl UiRenderNode {
|
||||
label: Some("primitive"),
|
||||
});
|
||||
|
||||
let tex_manager = GpuTextures::new(device, queue);
|
||||
let masks = ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui masks",
|
||||
);
|
||||
|
||||
let rsc_layout = Self::rsc_layout(device);
|
||||
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
|
||||
let texture_layout = Self::texture_layout(device);
|
||||
let mask_layout = Self::mask_layout(device);
|
||||
let mask_group = Self::mask_group(device, &mask_layout, &masks);
|
||||
|
||||
let sampler = default_sampler(device);
|
||||
let pages = GpuPages::new(device, queue, &texture_layout, &sampler);
|
||||
let textures = GpuTextures::new(device, queue);
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some("UI Shape Pipeline Layout"),
|
||||
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
|
||||
bind_group_layouts: &[
|
||||
&uniform_layout,
|
||||
&primitive_layout,
|
||||
&texture_layout,
|
||||
&mask_layout,
|
||||
],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
@@ -278,13 +268,16 @@ impl UiRenderNode {
|
||||
Self {
|
||||
uniform_group,
|
||||
primitive_layout,
|
||||
rsc_layout,
|
||||
rsc_group,
|
||||
texture_layout,
|
||||
mask_layout,
|
||||
mask_group,
|
||||
pipeline,
|
||||
window_buffer,
|
||||
layers: HashMap::default(),
|
||||
active: Vec::new(),
|
||||
textures: tex_manager,
|
||||
sampler,
|
||||
pages,
|
||||
textures,
|
||||
masks,
|
||||
}
|
||||
}
|
||||
@@ -319,12 +312,10 @@ impl UiRenderNode {
|
||||
})
|
||||
}
|
||||
|
||||
/// Group 2: the shared atlas array, one standalone-image slot (a null
|
||||
/// view for the main draw, a real one for each image's own bind group --
|
||||
/// see `GpuTextures`), one sampler and the masks buffer. No `count` on
|
||||
/// any entry: this needs nothing beyond plain Vulkan 1.0 / GLES
|
||||
/// sampling, unlike the `binding_array` layout it replaced.
|
||||
fn rsc_layout(device: &Device) -> BindGroupLayout {
|
||||
/// Group 2: the texture a run of instances samples, and the sampler. No
|
||||
/// `count` on either entry -- plain Vulkan 1.0 / GLES sampling, unlike the
|
||||
/// `binding_array` layout this replaced.
|
||||
fn texture_layout(device: &Device) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
BindGroupLayoutEntry {
|
||||
@@ -340,21 +331,20 @@ impl UiRenderNode {
|
||||
BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Texture {
|
||||
sample_type: TextureSampleType::Float { filterable: false },
|
||||
view_dimension: TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 3,
|
||||
],
|
||||
label: Some("ui texture"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Group 3, apart from the textures so that resizing the masks buffer
|
||||
/// leaves every texture group intact.
|
||||
fn mask_layout(device: &Device) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
@@ -362,45 +352,44 @@ impl UiRenderNode {
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("ui rsc"),
|
||||
}],
|
||||
label: Some("ui masks"),
|
||||
})
|
||||
}
|
||||
|
||||
/// The main group: rects and glyphs never sample the image slot, so it
|
||||
/// gets a 1x1 null view rather than any live standalone image's.
|
||||
fn rsc_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
tex_manager: &GpuTextures,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> BindGroup {
|
||||
fn mask_group(device: &Device, layout: &BindGroupLayout, masks: &ArrBuf<Mask>) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
entries: &[BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::TextureView(tex_manager.array_view()),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: BindingResource::TextureView(tex_manager.null_view()),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: BindingResource::Sampler(tex_manager.sampler()),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: masks.buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui rsc"),
|
||||
}],
|
||||
label: Some("ui masks"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn view_count(&self) -> usize {
|
||||
self.textures.view_count()
|
||||
pub fn texture_count(&self) -> usize {
|
||||
self.textures.count()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderLayer {
|
||||
/// Only a texture instance breaks a run, so a ui without images plans one
|
||||
/// draw however many rects and glyphs it has.
|
||||
fn plan_draws(&mut self, instances: &[PrimitiveInstance]) {
|
||||
self.draws.clear();
|
||||
for (i, inst) in instances.iter().enumerate() {
|
||||
let i = i as u32;
|
||||
let texture = (inst.binding == TEXTURE_BINDING).then_some(inst.idx);
|
||||
match self.draws.last_mut() {
|
||||
Some(draw) if draw.texture.is_none() && texture.is_none() => {
|
||||
draw.instances.end = i + 1;
|
||||
}
|
||||
_ => self.draws.push(Draw {
|
||||
texture,
|
||||
instances: i..i + 1,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use image::EncodableLayout;
|
||||
use wgpu::*;
|
||||
|
||||
use crate::GlyphAtlas;
|
||||
|
||||
use super::{
|
||||
atlas::PAGE,
|
||||
texture::{array_view, texture_group},
|
||||
};
|
||||
|
||||
/// The glyph atlas on the GPU: one array texture whose layers are the pages
|
||||
/// `GlyphAtlas` packs.
|
||||
///
|
||||
/// An array rather than a `binding_array<texture_2d<f32>>` because a layer
|
||||
/// index is ordinary Vulkan 1.0 / GLES sampling, while a binding array needs
|
||||
/// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack.
|
||||
pub struct GpuPages {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
texture: Texture,
|
||||
group: BindGroup,
|
||||
}
|
||||
|
||||
impl GpuPages {
|
||||
pub fn new(
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
layout: &BindGroupLayout,
|
||||
sampler: &Sampler,
|
||||
) -> Self {
|
||||
let texture = create_array(device, 1);
|
||||
let group = texture_group(device, layout, &array_view(&texture), sampler);
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
texture,
|
||||
group,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
if atlas.page_count() > self.texture.depth_or_array_layers() {
|
||||
self.grow(atlas.page_count(), layout, sampler);
|
||||
}
|
||||
for (upload, page) in atlas.uploads() {
|
||||
let rect = upload.rect;
|
||||
// `write_texture` wants tightly packed rows; the page is wider.
|
||||
let sub =
|
||||
image::imageops::crop_imm(page, rect.x, rect.y, rect.width, rect.height).to_image();
|
||||
self.queue.write_texture(
|
||||
TexelCopyTextureInfo {
|
||||
texture: &self.texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
z: upload.layer,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
sub.as_bytes(),
|
||||
TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(rect.width * 4),
|
||||
rows_per_image: Some(rect.height),
|
||||
},
|
||||
Extent3d {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group(&self) -> &BindGroup {
|
||||
&self.group
|
||||
}
|
||||
|
||||
/// Doubles until `needed` fits and copies the old layers across GPU side.
|
||||
/// The new view invalidates the old group, so that is rebuilt here.
|
||||
fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
let old = self.texture.depth_or_array_layers();
|
||||
let mut layers = old;
|
||||
while layers < needed {
|
||||
layers *= 2;
|
||||
}
|
||||
let texture = create_array(&self.device, layers);
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("atlas grow"),
|
||||
});
|
||||
encoder.copy_texture_to_texture(
|
||||
self.texture.as_image_copy(),
|
||||
texture.as_image_copy(),
|
||||
Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: old,
|
||||
},
|
||||
);
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.group = texture_group(&self.device, layout, &array_view(&texture), sampler);
|
||||
self.texture = texture;
|
||||
}
|
||||
}
|
||||
|
||||
fn create_array(device: &Device, layers: u32) -> Texture {
|
||||
device.create_texture(&TextureDescriptor {
|
||||
label: Some("glyph atlas"),
|
||||
size: Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: layers,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
+67
-170
@@ -16,17 +16,6 @@ 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,
|
||||
}
|
||||
|
||||
@@ -37,20 +26,14 @@ 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;
|
||||
/// Not in `primitives!` and with no group-1 buffer: a texture instance's `idx`
|
||||
/// names the texture to bind, so there is nothing to look up per-instance.
|
||||
pub const TEXTURE_BINDING: u32 = 2;
|
||||
|
||||
pub trait Primitive: Pod {
|
||||
const BINDING: u32;
|
||||
@@ -76,13 +59,6 @@ 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] {
|
||||
[
|
||||
@@ -145,16 +121,51 @@ impl Primitives {
|
||||
mask_idx,
|
||||
}: PrimitiveInst<P>,
|
||||
) -> PrimitiveHandle {
|
||||
self.updated = true;
|
||||
let vec = P::vec(&mut self.data);
|
||||
let i = vec.add(primitive);
|
||||
let inst = PrimitiveInstance {
|
||||
let data_idx = P::vec(&mut self.data).add(primitive);
|
||||
self.push(
|
||||
layer,
|
||||
id,
|
||||
PrimitiveInstance {
|
||||
region,
|
||||
idx: i as u32,
|
||||
idx: data_idx as u32,
|
||||
mask_idx,
|
||||
binding: P::BINDING,
|
||||
};
|
||||
let inst_i = if let Some(i) = self.free.pop() {
|
||||
},
|
||||
Some(data_idx),
|
||||
)
|
||||
}
|
||||
|
||||
/// Writes an instance that samples `texture` instead of a group-1 buffer.
|
||||
pub fn write_texture(
|
||||
&mut self,
|
||||
layer: usize,
|
||||
id: WidgetId,
|
||||
texture: u32,
|
||||
region: UiRegion,
|
||||
mask_idx: MaskIdx,
|
||||
) -> PrimitiveHandle {
|
||||
self.push(
|
||||
layer,
|
||||
id,
|
||||
PrimitiveInstance {
|
||||
region,
|
||||
idx: texture,
|
||||
mask_idx,
|
||||
binding: TEXTURE_BINDING,
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn push(
|
||||
&mut self,
|
||||
layer: usize,
|
||||
id: WidgetId,
|
||||
inst: PrimitiveInstance,
|
||||
data_idx: Option<usize>,
|
||||
) -> PrimitiveHandle {
|
||||
self.updated = true;
|
||||
let inst_idx = if let Some(i) = self.free.pop() {
|
||||
self.instances[i] = inst;
|
||||
self.assoc[i] = id;
|
||||
i
|
||||
@@ -164,107 +175,37 @@ impl Primitives {
|
||||
self.assoc.push(id);
|
||||
i
|
||||
};
|
||||
PrimitiveHandle::new::<P>(layer, inst_i, 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,
|
||||
inst_idx,
|
||||
data_idx,
|
||||
binding: inst.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() {
|
||||
/// 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 = assoc[i];
|
||||
let old = instances.len();
|
||||
Some(PrimitiveChange {
|
||||
id,
|
||||
is_image,
|
||||
old,
|
||||
new: i,
|
||||
let id = self.assoc[i];
|
||||
let old = self.instances.len();
|
||||
Some(PrimitiveChange { id, old, new: i })
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
||||
self.updated = true;
|
||||
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);
|
||||
if let Some(i) = h.data_idx {
|
||||
self.data.free(h.binding, i);
|
||||
}
|
||||
self.free.push(h.inst_idx);
|
||||
self.instances[h.inst_idx].mask_idx
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &PrimitiveData {
|
||||
&self.data
|
||||
@@ -276,21 +217,12 @@ impl Primitives {
|
||||
|
||||
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
|
||||
self.updated = true;
|
||||
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,
|
||||
}
|
||||
@@ -299,24 +231,14 @@ pub struct PrimitiveChange {
|
||||
pub struct PrimitiveHandle {
|
||||
pub layer: usize,
|
||||
pub inst_idx: usize,
|
||||
pub data_idx: usize,
|
||||
/// `None` for a texture instance, which has no group-1 entry to free.
|
||||
pub data_idx: Option<usize>,
|
||||
pub binding: u32,
|
||||
}
|
||||
|
||||
impl PrimitiveHandle {
|
||||
fn new<P: Primitive>(layer: usize, inst_idx: usize, data_idx: usize) -> Self {
|
||||
Self {
|
||||
layer,
|
||||
inst_idx,
|
||||
data_idx,
|
||||
binding: P::BINDING,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
primitives!(
|
||||
rects: RectPrimitive => 0,
|
||||
glyphs: GlyphPrimitive => 2,
|
||||
glyphs: GlyphPrimitive => 1,
|
||||
);
|
||||
|
||||
#[repr(C)]
|
||||
@@ -339,42 +261,17 @@ impl RectPrimitive {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
|
||||
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct GlyphPrimitive {
|
||||
pub uv_min: Vec2,
|
||||
pub uv_max: Vec2,
|
||||
/// 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.
|
||||
/// Which atlas array layer this glyph is on.
|
||||
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> {
|
||||
|
||||
+18
-24
@@ -1,9 +1,7 @@
|
||||
const RECT: u32 = 0u;
|
||||
// TEXTURE has no entry in group 1: a standalone image draws with its own
|
||||
// bind group (see UiRenderNode::draw), so there is nothing per-instance left
|
||||
// to look up here -- the bind group already picked the texture.
|
||||
const TEXTURE: u32 = 1u;
|
||||
const GLYPH: u32 = 2u;
|
||||
const GLYPH: u32 = 1u;
|
||||
// No group 1 entry: a texture instance's idx names the texture bound in group 2.
|
||||
const TEXTURE: u32 = 2u;
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> window: WindowUniform;
|
||||
@@ -19,11 +17,13 @@ struct Rect {
|
||||
inner_radius: f32,
|
||||
}
|
||||
|
||||
// Scalars rather than two vec2<f32>: a vec2 member would align the struct to
|
||||
// 8 and pad it to 32 bytes, which GlyphPrimitive has no field to fill.
|
||||
struct GlyphInfo {
|
||||
uv_min: vec2<f32>,
|
||||
uv_max: vec2<f32>,
|
||||
// Layer of the shared atlas array texture, not a view or bind-group
|
||||
// index -- a page never gets its own bind group.
|
||||
uv_min_x: f32,
|
||||
uv_min_y: f32,
|
||||
uv_max_x: f32,
|
||||
uv_max_y: f32,
|
||||
layer: u32,
|
||||
color: u32,
|
||||
flags: u32,
|
||||
@@ -49,21 +49,13 @@ struct UiVec2 {
|
||||
abs: vec2<f32>,
|
||||
}
|
||||
|
||||
// The shared glyph atlas: every page is one layer. Growing it recreates this
|
||||
// texture with headroom and copies the old layers across -- see
|
||||
// GpuTextures::grow_array -- rather than the binding_array<texture_2d<f32>>
|
||||
// this replaced, which needed VK_EXT_descriptor_indexing and does not survive
|
||||
// a real share of Android GPUs.
|
||||
// Whatever this run of instances samples: the glyph atlas, whose layers are
|
||||
// its pages, or one standalone image as an array of one.
|
||||
@group(2) @binding(0)
|
||||
var atlas: texture_2d_array<f32>;
|
||||
// One standalone image's texture. The main draw (rects and glyphs) binds a
|
||||
// 1x1 null texture here, since neither samples it; each image draw call
|
||||
// binds its own -- see UiRenderNode::draw.
|
||||
var tex: texture_2d_array<f32>;
|
||||
@group(2) @binding(1)
|
||||
var image_texture: texture_2d<f32>;
|
||||
@group(2) @binding(2)
|
||||
var samp: sampler;
|
||||
@group(2) @binding(3)
|
||||
@group(3) @binding(0)
|
||||
var<storage> masks: array<Mask>;
|
||||
|
||||
struct WindowUniform {
|
||||
@@ -166,12 +158,14 @@ fn fs_main(
|
||||
}
|
||||
|
||||
fn draw_texture(region: Region) -> vec4<f32> {
|
||||
return textureSample(image_texture, samp, region.uv);
|
||||
return textureSample(tex, samp, region.uv, 0);
|
||||
}
|
||||
|
||||
fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
|
||||
let uv = mix(g.uv_min, g.uv_max, region.uv);
|
||||
let texel = textureSample(atlas, samp, uv, i32(g.layer));
|
||||
let uv_min = vec2(g.uv_min_x, g.uv_min_y);
|
||||
let uv_max = vec2(g.uv_max_x, g.uv_max_y);
|
||||
let uv = mix(uv_min, uv_max, region.uv);
|
||||
let texel = textureSample(tex, samp, uv, i32(g.layer));
|
||||
if (g.flags & 1u) != 0u {
|
||||
return texel;
|
||||
}
|
||||
|
||||
+84
-383
@@ -1,289 +1,65 @@
|
||||
use image::{DynamicImage, EncodableLayout, GenericImageView};
|
||||
use wgpu::{util::DeviceExt, *};
|
||||
|
||||
use crate::{Mask, PatchRect, TextureKind, TextureUpdate, Textures, render::util::ArrBuf};
|
||||
use crate::{PatchRect, TextureUpdate, Textures};
|
||||
|
||||
use super::atlas::PAGE;
|
||||
|
||||
/// What one texture slot is, GPU-side. Parallel to `Textures`' own slot
|
||||
/// numbering (`TextureKind`'s `Image`/`Page`), so a slot's index means the
|
||||
/// same thing on both sides without a second map to keep in sync.
|
||||
enum Slot {
|
||||
/// A slot that was freed, or pushed and freed within the same batch
|
||||
/// before ever reaching here.
|
||||
Empty,
|
||||
Image(ImageGpu),
|
||||
/// The array layer a page occupies. Pages are never freed (see
|
||||
/// `Textures::free`), so this is the only variant that outlives a `Free`.
|
||||
Page(u32),
|
||||
}
|
||||
|
||||
struct ImageGpu {
|
||||
/// Kept because a masks or atlas-array rebuild has to build a new bind
|
||||
/// group from it. The `Texture` it came from is not kept: a `TextureView`
|
||||
/// holds its own reference to that, so the image survives without one.
|
||||
view: TextureView,
|
||||
bind_group: BindGroup,
|
||||
}
|
||||
|
||||
/// Owns the two kinds of texture iris draws:
|
||||
///
|
||||
/// - **The glyph atlas**, one `texture_2d_array` whose layers are pages
|
||||
/// (`Slot::Page`), grown by recreating the array with headroom and
|
||||
/// `copy_texture_to_texture`-ing the old layers across. No feature beyond
|
||||
/// Vulkan 1.0/GLES sampling is needed for this -- a layer index is an
|
||||
/// ordinary sampling operand.
|
||||
/// - **Standalone images** (`Slot::Image`), each its own `Texture` and
|
||||
/// `BindGroup`, drawn one `draw()` call at a time with that bind group
|
||||
/// bound -- see `UiRenderNode::draw`.
|
||||
///
|
||||
/// This replaced one giant `binding_array<texture_2d<f32>>`, which needed
|
||||
/// `VK_EXT_descriptor_indexing` -- an extension a real share of Android GPUs
|
||||
/// lack, so the old shape did not run there at all.
|
||||
/// The standalone images a ui draws, each its own texture and group 2 --
|
||||
/// unlike the glyph atlas in `super::page`, which is one array they share.
|
||||
pub struct GpuTextures {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
slots: Vec<Option<ImageGpu>>,
|
||||
}
|
||||
|
||||
slots: Vec<Slot>,
|
||||
|
||||
array_texture: Texture,
|
||||
array_view: TextureView,
|
||||
array_capacity: u32,
|
||||
/// Layers actually written. Only grows -- see `Slot::Page`.
|
||||
page_count: u32,
|
||||
|
||||
sampler: Sampler,
|
||||
/// Bound in the image slot of the main draw's bind group, which has
|
||||
/// nothing of its own to put there: rects and glyphs never sample it,
|
||||
/// but the layout requires something bound regardless.
|
||||
null_view: TextureView,
|
||||
struct ImageGpu {
|
||||
/// Kept for `patch`, which needs the texture rather than the view.
|
||||
texture: Texture,
|
||||
group: BindGroup,
|
||||
}
|
||||
|
||||
impl GpuTextures {
|
||||
/// Applies queued `Textures` updates, then reports whether the *main*
|
||||
/// bind group (the one rects and glyphs draw with) needs rebuilding --
|
||||
/// true when the atlas array was recreated (its view identity changed)
|
||||
/// or the masks buffer was, since both are bound there. Pushing or
|
||||
/// freeing a standalone image never touches that group: it built or drops
|
||||
/// its own.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
textures: &mut Textures,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
masks_resized: bool,
|
||||
) -> bool {
|
||||
let mut rebuild_main = masks_resized;
|
||||
if masks_resized {
|
||||
// The masks buffer just moved, so every bind group holding a
|
||||
// reference to it -- one per live standalone image -- is stale.
|
||||
self.rebuild_image_bind_groups(rsc_layout, masks);
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
slots: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
for update in textures.updates() {
|
||||
match update {
|
||||
TextureUpdate::Push(kind, image) => {
|
||||
rebuild_main |= self.push(kind, image, rsc_layout, masks);
|
||||
TextureUpdate::Push(image) => {
|
||||
let image = self.create(image, layout, sampler);
|
||||
self.slots.push(Some(image));
|
||||
}
|
||||
TextureUpdate::Set(kind, i, image) => {
|
||||
rebuild_main |= self.set(kind, i, image, rsc_layout, masks);
|
||||
TextureUpdate::Set(i, image) => {
|
||||
let image = self.create(image, layout, sampler);
|
||||
self.slots[i as usize] = Some(image);
|
||||
}
|
||||
// A patch changes texture contents, not which layer or bind
|
||||
// group exists, so it never asks for a rebuild -- rebuilding
|
||||
// per glyph is exactly the cost this exists to avoid.
|
||||
TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
|
||||
TextureUpdate::PushFree => self.slots.push(None),
|
||||
TextureUpdate::SetFree => {}
|
||||
TextureUpdate::Free(i) => self.free(i),
|
||||
TextureUpdate::PushFree(_kind) => self.slots.push(Slot::Empty),
|
||||
}
|
||||
}
|
||||
rebuild_main
|
||||
}
|
||||
|
||||
fn push(
|
||||
&mut self,
|
||||
kind: TextureKind,
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> bool {
|
||||
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks);
|
||||
self.slots.push(slot);
|
||||
rebuilt
|
||||
}
|
||||
|
||||
fn set(
|
||||
&mut self,
|
||||
kind: TextureKind,
|
||||
i: u32,
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> bool {
|
||||
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks);
|
||||
self.slots[i as usize] = slot;
|
||||
rebuilt
|
||||
}
|
||||
|
||||
fn make_slot(
|
||||
&mut self,
|
||||
kind: TextureKind,
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> (Slot, bool) {
|
||||
match kind {
|
||||
TextureKind::Image => {
|
||||
let gpu = self.create_image(image, rsc_layout, masks);
|
||||
(Slot::Image(gpu), false)
|
||||
}
|
||||
TextureKind::Page { layer } => {
|
||||
let mut rebuilt = false;
|
||||
if layer >= self.array_capacity {
|
||||
self.grow_array(rsc_layout, masks);
|
||||
rebuilt = true;
|
||||
}
|
||||
self.write_full_layer(layer, image);
|
||||
self.page_count = self.page_count.max(layer + 1);
|
||||
(Slot::Page(layer), rebuilt)
|
||||
TextureUpdate::Free(i) => self.slots[i as usize] = None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn free(&mut self, i: u32) {
|
||||
if let Some(slot) = self.slots.get_mut(i as usize) {
|
||||
*slot = Slot::Empty;
|
||||
}
|
||||
// A page's layer is not reclaimed here either -- see `Slot::Page`.
|
||||
/// `None` once freed. A drawn instance holds a `TextureHandle`, so its
|
||||
/// slot outlives it.
|
||||
pub fn group(&self, slot: u32) -> Option<&BindGroup> {
|
||||
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
|
||||
}
|
||||
|
||||
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
||||
let Some(&Slot::Page(layer)) = self.slots.get(i as usize) else {
|
||||
return;
|
||||
};
|
||||
if rect.width == 0 || rect.height == 0 {
|
||||
return;
|
||||
}
|
||||
// `write_texture` requires tightly packed rows, unlike the atlas image.
|
||||
let sub = image
|
||||
.view(rect.x, rect.y, rect.width, rect.height)
|
||||
.to_image();
|
||||
self.queue.write_texture(
|
||||
TexelCopyTextureInfo {
|
||||
texture: &self.array_texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
z: layer,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
sub.as_bytes(),
|
||||
TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(rect.width * 4),
|
||||
rows_per_image: Some(rect.height),
|
||||
},
|
||||
Extent3d {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
pub fn count(&self) -> usize {
|
||||
self.slots.iter().flatten().count()
|
||||
}
|
||||
|
||||
fn write_full_layer(&self, layer: u32, image: &DynamicImage) {
|
||||
// Every page is created as exactly PAGE x PAGE (`GlyphAtlas::allocate`),
|
||||
// so this is always a whole-layer write, never a crop.
|
||||
let rgba = image.to_rgba8();
|
||||
self.queue.write_texture(
|
||||
TexelCopyTextureInfo {
|
||||
texture: &self.array_texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: layer,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
rgba.as_bytes(),
|
||||
TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(PAGE * 4),
|
||||
rows_per_image: Some(PAGE),
|
||||
},
|
||||
Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Doubles the array's layer capacity (headroom, so this is rare) and
|
||||
/// copies the old layers across GPU-side -- no readback. Recreates the
|
||||
/// array's view, which invalidates every bind group that referenced it,
|
||||
/// so this also rebuilds all of them before returning.
|
||||
fn grow_array(&mut self, rsc_layout: &BindGroupLayout, masks: &ArrBuf<Mask>) {
|
||||
let new_capacity = self.array_capacity * 2;
|
||||
let new_texture = Self::create_array_texture(&self.device, new_capacity);
|
||||
if self.page_count > 0 {
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("atlas array grow"),
|
||||
});
|
||||
encoder.copy_texture_to_texture(
|
||||
TexelCopyTextureInfo {
|
||||
texture: &self.array_texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d::ZERO,
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
TexelCopyTextureInfo {
|
||||
texture: &new_texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d::ZERO,
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: self.page_count,
|
||||
},
|
||||
);
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
}
|
||||
self.array_texture = new_texture;
|
||||
self.array_view = self.array_texture.create_view(&TextureViewDescriptor {
|
||||
dimension: Some(TextureViewDimension::D2Array),
|
||||
..Default::default()
|
||||
});
|
||||
self.array_capacity = new_capacity;
|
||||
self.rebuild_image_bind_groups(rsc_layout, masks);
|
||||
}
|
||||
|
||||
fn rebuild_image_bind_groups(&mut self, rsc_layout: &BindGroupLayout, masks: &ArrBuf<Mask>) {
|
||||
for slot in &mut self.slots {
|
||||
if let Slot::Image(gpu) = slot {
|
||||
gpu.bind_group = Self::make_image_bind_group(
|
||||
&self.device,
|
||||
rsc_layout,
|
||||
&self.array_view,
|
||||
&gpu.view,
|
||||
&self.sampler,
|
||||
masks,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_image(
|
||||
fn create(
|
||||
&self,
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
layout: &BindGroupLayout,
|
||||
sampler: &Sampler,
|
||||
) -> ImageGpu {
|
||||
let rgba = image.to_rgba8();
|
||||
let (width, height) = rgba.dimensions();
|
||||
@@ -306,152 +82,77 @@ impl GpuTextures {
|
||||
wgt::TextureDataOrder::MipMajor,
|
||||
rgba.as_bytes(),
|
||||
);
|
||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
||||
let bind_group = Self::make_image_bind_group(
|
||||
&self.device,
|
||||
rsc_layout,
|
||||
&self.array_view,
|
||||
&view,
|
||||
&self.sampler,
|
||||
masks,
|
||||
);
|
||||
ImageGpu { view, bind_group }
|
||||
let group = texture_group(&self.device, layout, &array_view(&texture), sampler);
|
||||
ImageGpu { texture, group }
|
||||
}
|
||||
|
||||
/// Builds group 2 for one standalone image: the shared atlas array, this
|
||||
/// image's own view, the shared sampler, and the shared masks buffer --
|
||||
/// the same layout the main draw uses with a null view in the image slot.
|
||||
fn make_image_bind_group(
|
||||
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
||||
let Some(Some(slot)) = self.slots.get(i as usize) else {
|
||||
return;
|
||||
};
|
||||
if rect.width == 0 || rect.height == 0 {
|
||||
return;
|
||||
}
|
||||
// `write_texture` requires tightly packed rows, unlike the source image.
|
||||
let sub = image
|
||||
.view(rect.x, rect.y, rect.width, rect.height)
|
||||
.to_image();
|
||||
self.queue.write_texture(
|
||||
TexelCopyTextureInfo {
|
||||
texture: &slot.texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
z: 0,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
sub.as_bytes(),
|
||||
TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(rect.width * 4),
|
||||
rows_per_image: Some(rect.height),
|
||||
},
|
||||
Extent3d {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// One array texture and one sampler, so the atlas and a standalone image bind
|
||||
/// the same layout and the shader samples whichever is bound. An image is a
|
||||
/// single-layer texture viewed as an array of one.
|
||||
pub fn texture_group(
|
||||
device: &Device,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
array_view: &TextureView,
|
||||
image_view: &TextureView,
|
||||
layout: &BindGroupLayout,
|
||||
view: &TextureView,
|
||||
sampler: &Sampler,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout: rsc_layout,
|
||||
layout,
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::TextureView(array_view),
|
||||
resource: BindingResource::TextureView(view),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: BindingResource::TextureView(image_view),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: BindingResource::Sampler(sampler),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: masks.buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui rsc image"),
|
||||
label: Some("ui texture"),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_array_texture(device: &Device, capacity: u32) -> Texture {
|
||||
device.create_texture(&TextureDescriptor {
|
||||
label: Some("glyph atlas array"),
|
||||
size: Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: capacity,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING
|
||||
| TextureUsages::COPY_DST
|
||||
| TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
let sampler = default_sampler(device);
|
||||
let null_view = null_texture_view(device);
|
||||
let array_capacity = 1;
|
||||
let array_texture = Self::create_array_texture(device, array_capacity);
|
||||
let array_view = array_texture.create_view(&TextureViewDescriptor {
|
||||
pub fn array_view(texture: &Texture) -> TextureView {
|
||||
texture.create_view(&TextureViewDescriptor {
|
||||
dimension: Some(TextureViewDimension::D2Array),
|
||||
..Default::default()
|
||||
});
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
slots: Vec::new(),
|
||||
array_texture,
|
||||
array_view,
|
||||
array_capacity,
|
||||
page_count: 0,
|
||||
sampler,
|
||||
null_view,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn array_view(&self) -> &TextureView {
|
||||
&self.array_view
|
||||
}
|
||||
|
||||
pub fn null_view(&self) -> &TextureView {
|
||||
&self.null_view
|
||||
}
|
||||
|
||||
pub fn sampler(&self) -> &Sampler {
|
||||
&self.sampler
|
||||
}
|
||||
|
||||
/// The bind group a standalone image draws with. Panics if `idx` names an
|
||||
/// atlas page or a freed slot instead -- either is a caller bug (the
|
||||
/// wrong kind of instance reached this draw path), not a condition to
|
||||
/// recover from.
|
||||
pub fn image_bind_group(&self, idx: u32) -> &BindGroup {
|
||||
match self.slots.get(idx as usize) {
|
||||
Some(Slot::Image(gpu)) => &gpu.bind_group,
|
||||
other => panic!("texture slot {idx} is not a live standalone image: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_count(&self) -> usize {
|
||||
self.slots
|
||||
.iter()
|
||||
.filter(|s| !matches!(s, Slot::Empty))
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Slot {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Slot::Empty => write!(f, "Empty"),
|
||||
Slot::Image(_) => write!(f, "Image"),
|
||||
Slot::Page(layer) => write!(f, "Page(layer={layer})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn null_texture_view(device: &Device) -> TextureView {
|
||||
device
|
||||
.create_texture(&TextureDescriptor {
|
||||
label: Some("null"),
|
||||
size: Extent3d {
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
})
|
||||
.create_view(&TextureViewDescriptor::default())
|
||||
}
|
||||
|
||||
pub fn default_sampler(device: &Device) -> Sampler {
|
||||
|
||||
@@ -21,9 +21,8 @@ impl<T: Pod> ArrBuf<T> {
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
/// Returns whether the underlying `Buffer` was recreated -- a caller that
|
||||
/// cached a `BindGroup` referencing it (as `GpuTextures` does for the
|
||||
/// masks buffer) needs to know to rebuild that too.
|
||||
/// Returns whether the `Buffer` was recreated, which stales any cached
|
||||
/// `BindGroup` holding it.
|
||||
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
|
||||
let resized = self.len != data.len();
|
||||
if resized {
|
||||
@@ -46,8 +45,4 @@ impl<T: Pod> ArrBuf<T> {
|
||||
usage,
|
||||
})
|
||||
}
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
}
|
||||
+18
-26
@@ -30,6 +30,10 @@ impl<'a> Painter<'a> {
|
||||
mask_idx: self.mask,
|
||||
},
|
||||
);
|
||||
self.push_primitive(h);
|
||||
}
|
||||
|
||||
fn push_primitive(&mut self, h: PrimitiveHandle) {
|
||||
if self.mask != MaskIdx::NONE {
|
||||
// TODO: I have no clue if this works at all :joy:
|
||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||
@@ -76,32 +80,20 @@ impl<'a> Painter<'a> {
|
||||
}
|
||||
|
||||
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
self.textures.push(handle.clone());
|
||||
self.write_image(handle.image_index(), region.within(&self.region));
|
||||
self.texture_at(handle, region.within(&self.region));
|
||||
}
|
||||
|
||||
pub fn texture(&mut self, handle: &TextureHandle) {
|
||||
self.textures.push(handle.clone());
|
||||
self.write_image(handle.image_index(), self.region);
|
||||
self.texture_at(handle, self.region);
|
||||
}
|
||||
|
||||
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
self.textures.push(handle.clone());
|
||||
self.write_image(handle.image_index(), region);
|
||||
}
|
||||
|
||||
/// A standalone image draws with its own bind group rather than sharing
|
||||
/// the layer's one instanced draw, so it goes through
|
||||
/// `Primitives::write_image` instead of `primitive_at`/`Primitive::vec`.
|
||||
fn write_image(&mut self, texture_idx: u32, region: UiRegion) {
|
||||
let h = self
|
||||
.state
|
||||
let h =
|
||||
self.state
|
||||
.layers
|
||||
.write_image(self.layer, self.id, texture_idx, region, self.mask);
|
||||
if self.mask != MaskIdx::NONE {
|
||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||
}
|
||||
self.primitives.push(h);
|
||||
.write_texture(self.layer, self.id, handle.slot(), region, self.mask);
|
||||
self.push_primitive(h);
|
||||
}
|
||||
|
||||
pub fn render_text(
|
||||
@@ -111,7 +103,7 @@ impl<'a> Painter<'a> {
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
let ui = self.rsc.ui_mut();
|
||||
ui.text.render(buffer, attrs, width, &mut ui.textures)
|
||||
ui.text.render(buffer, attrs, width)
|
||||
}
|
||||
|
||||
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
||||
@@ -123,13 +115,13 @@ 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(
|
||||
GlyphPrimitive::new(
|
||||
glyph.entry.uv_min,
|
||||
glyph.entry.uv_max,
|
||||
glyph.entry.layer,
|
||||
text.color,
|
||||
glyph.entry.flags(),
|
||||
),
|
||||
GlyphPrimitive {
|
||||
uv_min: glyph.entry.uv_min,
|
||||
uv_max: glyph.entry.uv_max,
|
||||
layer: glyph.entry.layer,
|
||||
color: text.color,
|
||||
flags: glyph.entry.flags(),
|
||||
},
|
||||
region,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -308,7 +308,6 @@ impl UiRenderState {
|
||||
source,
|
||||
cache: &mut self.cache,
|
||||
text: &mut ui.text,
|
||||
textures: &mut ui.textures,
|
||||
widgets: &ui.widgets,
|
||||
outer,
|
||||
output_size: self.output_size,
|
||||
|
||||
+3
-5
@@ -1,11 +1,10 @@
|
||||
use crate::{
|
||||
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
|
||||
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
||||
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
|
||||
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
||||
};
|
||||
|
||||
pub struct SizeCtx<'a> {
|
||||
pub text: &'a mut TextData,
|
||||
pub textures: &'a mut Textures,
|
||||
pub(super) source: WidgetId,
|
||||
pub(super) widgets: &'a Widgets,
|
||||
pub(super) cache: &'a mut Cache,
|
||||
@@ -33,7 +32,6 @@ impl SizeCtx<'_> {
|
||||
.get_dyn_dynamic(id)
|
||||
.desired_len::<A>(&mut SizeCtx {
|
||||
text: self.text,
|
||||
textures: self.textures,
|
||||
source: self.source,
|
||||
widgets: self.widgets,
|
||||
cache: self.cache,
|
||||
@@ -82,7 +80,7 @@ impl SizeCtx<'_> {
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
self.text.render(buffer, attrs, width, self.textures)
|
||||
self.text.render(buffer, attrs, width)
|
||||
}
|
||||
|
||||
pub fn label(&self, id: WidgetId) -> &String {
|
||||
|
||||
@@ -212,10 +212,10 @@ impl DefaultAppState for Client {
|
||||
render: &mut UiRenderState,
|
||||
) {
|
||||
let new = format!(
|
||||
"widgets: {}\nactive: {}\nviews: {}",
|
||||
"widgets: {}\nactive: {}\ntextures: {}",
|
||||
rsc.widgets().len(),
|
||||
render.active_widgets(),
|
||||
self.ui_state.renderer.ui.view_count(),
|
||||
self.ui_state.renderer.ui.texture_count(),
|
||||
);
|
||||
if new != *rsc.widgets()[self.info].content {
|
||||
*rsc.widgets_mut()[self.info].content = new;
|
||||
|
||||
@@ -83,12 +83,9 @@ impl UiRenderer {
|
||||
.block_on()
|
||||
.expect("Could not get adapter!");
|
||||
|
||||
// No features beyond what wgpu asks for by default, and no
|
||||
// binding-array limits: the atlas is one texture_2d_array and a
|
||||
// standalone image is its own ordinary bind group, neither of which
|
||||
// needs descriptor indexing. The binding array this replaced asked
|
||||
// for VK_EXT_descriptor_indexing unconditionally and so did not run
|
||||
// on a real share of Android GPUs.
|
||||
// No binding-array features or limits: the atlas is one
|
||||
// texture_2d_array and an image its own bind group, so nothing here
|
||||
// needs VK_EXT_descriptor_indexing the way the old layout did.
|
||||
let (device, queue) = adapter
|
||||
.request_device(&DeviceDescriptor {
|
||||
required_limits: Limits {
|
||||
|
||||
Reference in new issue
Block a user