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
+503
-891
No files matched your search
@@ -133,15 +133,15 @@ impl PrimitiveLayers {
|
|||||||
self[h.layer].free(h)
|
self[h.layer].free(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_image(
|
pub fn write_texture(
|
||||||
&mut self,
|
&mut self,
|
||||||
layer: LayerId,
|
layer: LayerId,
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
texture_idx: u32,
|
texture: u32,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
mask_idx: MaskIdx,
|
mask_idx: MaskIdx,
|
||||||
) -> PrimitiveHandle {
|
) -> 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::{
|
use crate::{
|
||||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor,
|
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
|
||||||
util::Vec2,
|
|
||||||
};
|
};
|
||||||
use parley::{
|
use parley::{
|
||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
||||||
@@ -159,7 +158,7 @@ impl TextBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TextData {
|
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();
|
let mut placed = Vec::new();
|
||||||
for line in buffer.layout.lines() {
|
for line in buffer.layout.lines() {
|
||||||
for item in line.items() {
|
for item in line.items() {
|
||||||
@@ -185,17 +184,14 @@ impl TextData {
|
|||||||
subpixel,
|
subpixel,
|
||||||
coords: coords_hash,
|
coords: coords_hash,
|
||||||
};
|
};
|
||||||
let Some(entry) = self.glyph_entry(
|
let Some(entry) = self.glyph_entry(GlyphRaster {
|
||||||
GlyphRaster {
|
|
||||||
key,
|
key,
|
||||||
font: font_ref,
|
font: font_ref,
|
||||||
font_size,
|
font_size,
|
||||||
coords,
|
coords,
|
||||||
subpixel,
|
subpixel,
|
||||||
glyph_id: glyph.id,
|
glyph_id: glyph.id,
|
||||||
},
|
}) else {
|
||||||
textures,
|
|
||||||
) else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
placed.push(PlacedGlyph {
|
placed.push(PlacedGlyph {
|
||||||
@@ -211,11 +207,7 @@ impl TextData {
|
|||||||
placed
|
placed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn glyph_entry(
|
fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option<GlyphEntry> {
|
||||||
&mut self,
|
|
||||||
glyph: GlyphRaster<'_>,
|
|
||||||
textures: &mut Textures,
|
|
||||||
) -> Option<GlyphEntry> {
|
|
||||||
if let Some(entry) = self.atlas.get(&glyph.key) {
|
if let Some(entry) = self.atlas.get(&glyph.key) {
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
@@ -237,7 +229,7 @@ impl TextData {
|
|||||||
.render(&mut scaler, glyph.glyph_id as u16);
|
.render(&mut scaler, glyph.glyph_id as u16);
|
||||||
|
|
||||||
if let Some(image) = image {
|
if let Some(image) = image {
|
||||||
self.atlas.insert(glyph.key, &image, textures)
|
self.atlas.insert(glyph.key, &image)
|
||||||
} else {
|
} else {
|
||||||
self.atlas.insert_empty(glyph.key);
|
self.atlas.insert_empty(glyph.key);
|
||||||
None
|
None
|
||||||
@@ -278,10 +270,9 @@ impl TextData {
|
|||||||
buffer: &mut TextBuffer,
|
buffer: &mut TextBuffer,
|
||||||
attrs: &TextAttrs,
|
attrs: &TextAttrs,
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
textures: &mut Textures,
|
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
buffer.shape(self, attrs, width);
|
buffer.shape(self, attrs, width);
|
||||||
let glyphs = self.place(buffer, textures);
|
let glyphs = self.place(buffer);
|
||||||
RenderedText {
|
RenderedText {
|
||||||
glyphs,
|
glyphs,
|
||||||
size: buffer.size(),
|
size: buffer.size(),
|
||||||
|
|||||||
@@ -5,28 +5,12 @@ use std::{
|
|||||||
sync::mpsc::{Receiver, Sender, channel},
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TextureHandle {
|
pub struct TextureHandle {
|
||||||
slot: u32,
|
slot: u32,
|
||||||
kind: TextureKind,
|
|
||||||
size: Vec2,
|
size: Vec2,
|
||||||
counter: RefCounter,
|
counter: RefCounter,
|
||||||
send: Sender<(TextureKind, u32)>,
|
send: Sender<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// a texture manager for a ui
|
/// a texture manager for a ui
|
||||||
@@ -34,24 +18,19 @@ pub struct TextureHandle {
|
|||||||
pub struct Textures {
|
pub struct Textures {
|
||||||
free: Vec<u32>,
|
free: Vec<u32>,
|
||||||
images: Vec<Option<DynamicImage>>,
|
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>,
|
updates: Vec<Update>,
|
||||||
send: Sender<(TextureKind, u32)>,
|
send: Sender<u32>,
|
||||||
recv: Receiver<(TextureKind, u32)>,
|
recv: Receiver<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum TextureUpdate<'a> {
|
pub enum TextureUpdate<'a> {
|
||||||
Push(TextureKind, &'a DynamicImage),
|
Push(&'a DynamicImage),
|
||||||
Set(TextureKind, u32, &'a DynamicImage),
|
Set(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),
|
Patch(u32, PatchRect, &'a DynamicImage),
|
||||||
Free(u32),
|
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,
|
SetFree,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +43,8 @@ pub struct PatchRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Update {
|
enum Update {
|
||||||
Push(TextureKind, u32),
|
Push(u32),
|
||||||
Set(TextureKind, u32),
|
Set(u32),
|
||||||
Patch(u32, PatchRect),
|
Patch(u32, PatchRect),
|
||||||
Free(u32),
|
Free(u32),
|
||||||
}
|
}
|
||||||
@@ -76,54 +55,31 @@ impl Textures {
|
|||||||
Self {
|
Self {
|
||||||
free: Vec::new(),
|
free: Vec::new(),
|
||||||
images: Vec::new(),
|
images: Vec::new(),
|
||||||
next_page_layer: 0,
|
|
||||||
updates: Vec::new(),
|
updates: Vec::new(),
|
||||||
send,
|
send,
|
||||||
recv,
|
recv,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
||||||
let image = image.into();
|
let image = image.into();
|
||||||
let size = image.dimensions().into();
|
let size = image.dimensions().into();
|
||||||
let kind = TextureKind::Image;
|
|
||||||
let slot = self.push(kind, image);
|
|
||||||
TextureHandle {
|
TextureHandle {
|
||||||
slot,
|
slot: self.push(image),
|
||||||
kind,
|
|
||||||
size,
|
size,
|
||||||
counter: RefCounter::new(),
|
counter: RefCounter::new(),
|
||||||
send: self.send.clone(),
|
send: self.send.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a page of the shared glyph atlas array. Only `atlas.rs` should
|
fn push(&mut self, image: DynamicImage) -> u32 {
|
||||||
/// 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() {
|
if let Some(i) = self.free.pop() {
|
||||||
self.images[i as usize] = Some(image);
|
self.images[i as usize] = Some(image);
|
||||||
self.updates.push(Update::Set(kind, i));
|
self.updates.push(Update::Set(i));
|
||||||
i
|
i
|
||||||
} else {
|
} else {
|
||||||
let i = self.images.len() as u32;
|
let i = self.images.len() as u32;
|
||||||
self.images.push(Some(image));
|
self.images.push(Some(image));
|
||||||
self.updates.push(Update::Push(kind, i));
|
self.updates.push(Update::Push(i));
|
||||||
i
|
i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,29 +96,22 @@ impl Textures {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn free(&mut self) {
|
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.images[idx as usize] = None;
|
||||||
self.updates.push(Update::Free(idx));
|
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);
|
self.free.push(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn updates(&mut self) -> impl Iterator<Item = TextureUpdate<'_>> {
|
pub fn updates(&mut self) -> impl Iterator<Item = TextureUpdate<'_>> {
|
||||||
self.updates.drain(..).map(|u| match u {
|
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()
|
.as_ref()
|
||||||
.map(|img| TextureUpdate::Push(kind, img))
|
.map(TextureUpdate::Push)
|
||||||
.unwrap_or(TextureUpdate::PushFree(kind)),
|
.unwrap_or(TextureUpdate::PushFree),
|
||||||
Update::Set(kind, i) => self.images[i as usize]
|
Update::Set(i) => self.images[i as usize]
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|img| TextureUpdate::Set(kind, i, img))
|
.map(|img| TextureUpdate::Set(i, img))
|
||||||
.unwrap_or(TextureUpdate::SetFree),
|
.unwrap_or(TextureUpdate::SetFree),
|
||||||
Update::Patch(i, rect) => self.images[i as usize]
|
Update::Patch(i, rect) => self.images[i as usize]
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -174,36 +123,19 @@ impl Textures {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TextureHandle {
|
impl TextureHandle {
|
||||||
|
/// Index into `Textures`, and into the renderer's parallel slots.
|
||||||
|
pub fn slot(&self) -> u32 {
|
||||||
|
self.slot
|
||||||
|
}
|
||||||
pub fn size(&self) -> Vec2 {
|
pub fn size(&self) -> Vec2 {
|
||||||
self.size
|
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 {
|
impl Drop for TextureHandle {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if self.counter.drop() {
|
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::{
|
use crate::{
|
||||||
PatchRect, TextureHandle, Textures,
|
PatchRect,
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
use swash::scale::image::{Content, Image};
|
use swash::scale::image::{Content, Image};
|
||||||
|
|
||||||
/// Side of one atlas page, in pixels. 1024 is 4 MB at RGBA8 -- enough for a
|
/// Side of one page, and so of every layer of `render::page`'s array texture.
|
||||||
/// 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.
|
|
||||||
pub(crate) const PAGE: u32 = 1024;
|
pub(crate) const PAGE: u32 = 1024;
|
||||||
|
|
||||||
/// Transparent margin kept around every glyph, so that sampling one cannot
|
/// Transparent margin kept around every glyph, so that sampling one cannot
|
||||||
@@ -40,7 +36,7 @@ pub struct GlyphEntry {
|
|||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub is_colored: bool,
|
pub is_colored: bool,
|
||||||
/// The atlas array layer this glyph's page occupies.
|
/// Which atlas array layer this glyph is on.
|
||||||
pub layer: u32,
|
pub layer: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,18 +49,26 @@ impl GlyphEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Page {
|
struct Page {
|
||||||
handle: TextureHandle,
|
image: RgbaImage,
|
||||||
x: u32,
|
x: u32,
|
||||||
y: u32,
|
y: u32,
|
||||||
shelf_height: 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)]
|
#[derive(Default)]
|
||||||
pub struct GlyphAtlas {
|
pub struct GlyphAtlas {
|
||||||
pages: Vec<Page>,
|
pages: Vec<Page>,
|
||||||
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
||||||
/// too, so it is not re-rasterised on every layout.
|
/// too, so it is not re-rasterised on every layout.
|
||||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||||
|
uploads: Vec<PageUpload>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GlyphAtlas {
|
impl GlyphAtlas {
|
||||||
@@ -72,12 +76,7 @@ impl GlyphAtlas {
|
|||||||
self.entries.get(key).copied()
|
self.entries.get(key).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(
|
pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option<GlyphEntry> {
|
||||||
&mut self,
|
|
||||||
key: GlyphKey,
|
|
||||||
image: &Image,
|
|
||||||
textures: &mut Textures,
|
|
||||||
) -> Option<GlyphEntry> {
|
|
||||||
let w = image.placement.width;
|
let w = image.placement.width;
|
||||||
let h = image.placement.height;
|
let h = image.placement.height;
|
||||||
if w == 0 || h == 0 {
|
if w == 0 || h == 0 {
|
||||||
@@ -99,23 +98,18 @@ impl GlyphAtlas {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (page_idx, x, y) = self.allocate(w, h, textures);
|
let (layer, x, y) = self.allocate(w, h);
|
||||||
let page = &self.pages[page_idx];
|
write_glyph(&mut self.pages[layer as usize].image, image, x, y);
|
||||||
|
self.uploads.push(PageUpload {
|
||||||
let img = textures.image_mut(&page.handle);
|
layer,
|
||||||
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
|
rect: PatchRect {
|
||||||
write_glyph(rgba, image, x, y);
|
|
||||||
|
|
||||||
let handle = page.handle.clone();
|
|
||||||
let rect = PatchRect {
|
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width: w,
|
width: w,
|
||||||
height: h,
|
height: h,
|
||||||
};
|
},
|
||||||
textures.patch(&handle, rect);
|
});
|
||||||
|
|
||||||
let page = &self.pages[page_idx];
|
|
||||||
let scale = 1.0 / PAGE as f32;
|
let scale = 1.0 / PAGE as f32;
|
||||||
let entry = GlyphEntry {
|
let entry = GlyphEntry {
|
||||||
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
|
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
|
||||||
@@ -125,38 +119,47 @@ impl GlyphAtlas {
|
|||||||
width: w,
|
width: w,
|
||||||
height: h,
|
height: h,
|
||||||
is_colored: matches!(image.content, Content::Color),
|
is_colored: matches!(image.content, Content::Color),
|
||||||
layer: page.handle.layer(),
|
layer,
|
||||||
};
|
};
|
||||||
self.entries.insert(key, Some(entry));
|
self.entries.insert(key, Some(entry));
|
||||||
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
|
if let Some((i, (x, y))) = self
|
||||||
.pages
|
.pages
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
|
.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 {
|
self.pages.push(Page {
|
||||||
handle,
|
image: RgbaImage::new(PAGE, PAGE),
|
||||||
x: PAD + w + PAD,
|
x: PAD + w + PAD,
|
||||||
y: PAD,
|
y: PAD,
|
||||||
shelf_height: h + 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) {
|
pub fn insert_empty(&mut self, key: GlyphKey) {
|
||||||
self.entries.insert(key, None);
|
self.entries.insert(key, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page_count(&self) -> usize {
|
pub fn page_count(&self) -> u32 {
|
||||||
self.pages.len()
|
self.pages.len() as u32
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn glyph_count(&self) -> usize {
|
pub fn glyph_count(&self) -> usize {
|
||||||
|
|||||||
+108
-119
@@ -1,6 +1,13 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
UiData, UiRenderState,
|
UiData, UiRenderState,
|
||||||
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
|
render::{
|
||||||
|
data::PrimitiveInstance,
|
||||||
|
page::GpuPages,
|
||||||
|
texture::{GpuTextures, default_sampler},
|
||||||
|
util::ArrBuf,
|
||||||
|
},
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
use data::WindowUniform;
|
use data::WindowUniform;
|
||||||
@@ -11,6 +18,7 @@ use wgpu::{
|
|||||||
|
|
||||||
mod atlas;
|
mod atlas;
|
||||||
mod data;
|
mod data;
|
||||||
|
mod page;
|
||||||
mod primitive;
|
mod primitive;
|
||||||
mod texture;
|
mod texture;
|
||||||
mod util;
|
mod util;
|
||||||
@@ -24,14 +32,17 @@ const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
|||||||
pub struct UiRenderNode {
|
pub struct UiRenderNode {
|
||||||
uniform_group: BindGroup,
|
uniform_group: BindGroup,
|
||||||
primitive_layout: BindGroupLayout,
|
primitive_layout: BindGroupLayout,
|
||||||
rsc_layout: BindGroupLayout,
|
texture_layout: BindGroupLayout,
|
||||||
rsc_group: BindGroup,
|
mask_layout: BindGroupLayout,
|
||||||
|
mask_group: BindGroup,
|
||||||
|
|
||||||
pipeline: RenderPipeline,
|
pipeline: RenderPipeline,
|
||||||
|
|
||||||
layers: HashMap<usize, RenderLayer>,
|
layers: HashMap<usize, RenderLayer>,
|
||||||
active: Vec<usize>,
|
active: Vec<usize>,
|
||||||
window_buffer: Buffer,
|
window_buffer: Buffer,
|
||||||
|
sampler: Sampler,
|
||||||
|
pages: GpuPages,
|
||||||
textures: GpuTextures,
|
textures: GpuTextures,
|
||||||
masks: ArrBuf<Mask>,
|
masks: ArrBuf<Mask>,
|
||||||
}
|
}
|
||||||
@@ -40,43 +51,38 @@ struct RenderLayer {
|
|||||||
instance: ArrBuf<PrimitiveInstance>,
|
instance: ArrBuf<PrimitiveInstance>,
|
||||||
primitives: PrimitiveBuffers,
|
primitives: PrimitiveBuffers,
|
||||||
primitive_group: BindGroup,
|
primitive_group: BindGroup,
|
||||||
/// A standalone image's instances, kept apart from `instance` because
|
draws: Vec<Draw>,
|
||||||
/// 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
|
/// A run of consecutive instances sharing one group 2: `texture` names a
|
||||||
/// same order, refreshed alongside it. Not stored in the vertex buffer
|
/// standalone image, or `None` is the glyph atlas rects and glyphs sample.
|
||||||
/// itself because it names a bind group, not shader data.
|
struct Draw {
|
||||||
image_tex_indices: Vec<u32>,
|
texture: Option<u32>,
|
||||||
|
instances: Range<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiRenderNode {
|
impl UiRenderNode {
|
||||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||||
pass.set_pipeline(&self.pipeline);
|
pass.set_pipeline(&self.pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_group, &[]);
|
pass.set_bind_group(0, &self.uniform_group, &[]);
|
||||||
|
pass.set_bind_group(3, &self.mask_group, &[]);
|
||||||
for i in &self.active {
|
for i in &self.active {
|
||||||
let layer = &self.layers[i];
|
let layer = &self.layers[i];
|
||||||
if layer.instance.len() == 0 && layer.image_instance.len() == 0 {
|
if layer.draws.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
pass.set_bind_group(1, &layer.primitive_group, &[]);
|
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.set_vertex_buffer(0, layer.instance.buffer.slice(..));
|
||||||
pass.draw(0..4, 0..layer.instance.len() as u32);
|
for draw in &layer.draws {
|
||||||
}
|
let group = match draw.texture {
|
||||||
// Images draw after this layer's rects and glyphs, one draw call
|
Some(slot) => match self.textures.group(slot) {
|
||||||
// each with its own bind group. That draws every image "on top"
|
Some(group) => group,
|
||||||
// within the layer, which loses nothing that currently exists:
|
None => continue,
|
||||||
// `Primitives::apply_free` frees with `swap_remove`, so a layer's
|
},
|
||||||
// draw order was already undefined before images had their own
|
None => self.pages.group(),
|
||||||
// list -- nothing before this relied on interleaving a rect
|
};
|
||||||
// between two images at a particular position.
|
pass.set_bind_group(2, group, &[]);
|
||||||
if layer.image_instance.len() > 0 {
|
pass.draw(0..4, draw.instances.clone());
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,15 +100,7 @@ impl UiRenderNode {
|
|||||||
for change in primitives.apply_free() {
|
for change in primitives.apply_free() {
|
||||||
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
||||||
for h in &mut inst.primitives {
|
for h in &mut inst.primitives {
|
||||||
// `is_image` disambiguates: `instances` and `images`
|
if h.layer == i && h.inst_idx == change.old {
|
||||||
// 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
|
|
||||||
{
|
|
||||||
h.inst_idx = change.new;
|
h.inst_idx = change.new;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -121,12 +119,7 @@ impl UiRenderNode {
|
|||||||
),
|
),
|
||||||
primitives,
|
primitives,
|
||||||
primitive_group,
|
primitive_group,
|
||||||
image_instance: ArrBuf::new(
|
draws: Vec::new(),
|
||||||
device,
|
|
||||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
|
||||||
"image instance",
|
|
||||||
),
|
|
||||||
image_tex_indices: Vec::new(),
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if primitives.updated {
|
if primitives.updated {
|
||||||
@@ -139,33 +132,21 @@ impl UiRenderNode {
|
|||||||
&self.primitive_layout,
|
&self.primitive_layout,
|
||||||
rlayer.primitives.buffers(),
|
rlayer.primitives.buffers(),
|
||||||
);
|
);
|
||||||
rlayer
|
rlayer.plan_draws(primitives.instances());
|
||||||
.image_instance
|
|
||||||
.update(device, queue, primitives.image_instances());
|
|
||||||
rlayer.image_tex_indices = primitives
|
|
||||||
.image_instances()
|
|
||||||
.iter()
|
|
||||||
.map(|inst| inst.idx)
|
|
||||||
.collect();
|
|
||||||
primitives.updated = false;
|
primitives.updated = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let masks_resized = if ui.masks.changed {
|
if ui.masks.changed {
|
||||||
ui.masks.changed = false;
|
ui.masks.changed = false;
|
||||||
self.masks.update(device, queue, &ui.masks[..])
|
if self.masks.update(device, queue, &ui.masks[..]) {
|
||||||
} else {
|
self.mask_group = Self::mask_group(device, &self.mask_layout, &self.masks);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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) {
|
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
||||||
let size = size.into();
|
let size = size.into();
|
||||||
@@ -222,19 +203,28 @@ impl UiRenderNode {
|
|||||||
label: Some("primitive"),
|
label: Some("primitive"),
|
||||||
});
|
});
|
||||||
|
|
||||||
let tex_manager = GpuTextures::new(device, queue);
|
|
||||||
let masks = ArrBuf::new(
|
let masks = ArrBuf::new(
|
||||||
device,
|
device,
|
||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
"ui masks",
|
"ui masks",
|
||||||
);
|
);
|
||||||
|
|
||||||
let rsc_layout = Self::rsc_layout(device);
|
let texture_layout = Self::texture_layout(device);
|
||||||
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
|
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 {
|
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||||
label: Some("UI Shape Pipeline Layout"),
|
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,
|
immediate_size: 0,
|
||||||
});
|
});
|
||||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||||
@@ -278,13 +268,16 @@ impl UiRenderNode {
|
|||||||
Self {
|
Self {
|
||||||
uniform_group,
|
uniform_group,
|
||||||
primitive_layout,
|
primitive_layout,
|
||||||
rsc_layout,
|
texture_layout,
|
||||||
rsc_group,
|
mask_layout,
|
||||||
|
mask_group,
|
||||||
pipeline,
|
pipeline,
|
||||||
window_buffer,
|
window_buffer,
|
||||||
layers: HashMap::default(),
|
layers: HashMap::default(),
|
||||||
active: Vec::new(),
|
active: Vec::new(),
|
||||||
textures: tex_manager,
|
sampler,
|
||||||
|
pages,
|
||||||
|
textures,
|
||||||
masks,
|
masks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,12 +312,10 @@ impl UiRenderNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Group 2: the shared atlas array, one standalone-image slot (a null
|
/// Group 2: the texture a run of instances samples, and the sampler. No
|
||||||
/// view for the main draw, a real one for each image's own bind group --
|
/// `count` on either entry -- plain Vulkan 1.0 / GLES sampling, unlike the
|
||||||
/// see `GpuTextures`), one sampler and the masks buffer. No `count` on
|
/// `binding_array` layout this replaced.
|
||||||
/// any entry: this needs nothing beyond plain Vulkan 1.0 / GLES
|
fn texture_layout(device: &Device) -> BindGroupLayout {
|
||||||
/// sampling, unlike the `binding_array` layout it replaced.
|
|
||||||
fn rsc_layout(device: &Device) -> BindGroupLayout {
|
|
||||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
entries: &[
|
entries: &[
|
||||||
BindGroupLayoutEntry {
|
BindGroupLayoutEntry {
|
||||||
@@ -340,21 +331,20 @@ impl UiRenderNode {
|
|||||||
BindGroupLayoutEntry {
|
BindGroupLayoutEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
visibility: ShaderStages::FRAGMENT,
|
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),
|
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
||||||
count: None,
|
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,
|
visibility: ShaderStages::FRAGMENT,
|
||||||
ty: BindingType::Buffer {
|
ty: BindingType::Buffer {
|
||||||
ty: BufferBindingType::Storage { read_only: true },
|
ty: BufferBindingType::Storage { read_only: true },
|
||||||
@@ -362,45 +352,44 @@ impl UiRenderNode {
|
|||||||
min_binding_size: None,
|
min_binding_size: None,
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
}],
|
||||||
],
|
label: Some("ui masks"),
|
||||||
label: Some("ui rsc"),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main group: rects and glyphs never sample the image slot, so it
|
fn mask_group(device: &Device, layout: &BindGroupLayout, masks: &ArrBuf<Mask>) -> BindGroup {
|
||||||
/// 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 {
|
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
layout,
|
layout,
|
||||||
entries: &[
|
entries: &[BindGroupEntry {
|
||||||
BindGroupEntry {
|
|
||||||
binding: 0,
|
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(),
|
resource: masks.buffer.as_entire_binding(),
|
||||||
},
|
}],
|
||||||
],
|
label: Some("ui masks"),
|
||||||
label: Some("ui rsc"),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view_count(&self) -> usize {
|
pub fn texture_count(&self) -> usize {
|
||||||
self.textures.view_count()
|
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>,
|
assoc: Vec<WidgetId>,
|
||||||
data: PrimitiveData,
|
data: PrimitiveData,
|
||||||
free: Vec<usize>,
|
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,
|
pub updated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,20 +26,14 @@ impl Default for Primitives {
|
|||||||
assoc: Default::default(),
|
assoc: Default::default(),
|
||||||
data: Default::default(),
|
data: Default::default(),
|
||||||
free: Vec::new(),
|
free: Vec::new(),
|
||||||
images: Default::default(),
|
|
||||||
image_assoc: Default::default(),
|
|
||||||
image_free: Vec::new(),
|
|
||||||
updated: true,
|
updated: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `binding` tag `Painter` writes on an image instance. Distinct from any
|
/// Not in `primitives!` and with no group-1 buffer: a texture instance's `idx`
|
||||||
/// `Primitive::BINDING` because images have no `PrimitiveData` entry to key
|
/// names the texture to bind, so there is nothing to look up per-instance.
|
||||||
/// one from -- a bind group already selects the texture -- so this only ever
|
pub const TEXTURE_BINDING: u32 = 2;
|
||||||
/// 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 {
|
pub trait Primitive: Pod {
|
||||||
const BINDING: u32;
|
const BINDING: u32;
|
||||||
@@ -76,13 +59,6 @@ macro_rules! primitives {
|
|||||||
|
|
||||||
impl PrimitiveBuffers {
|
impl PrimitiveBuffers {
|
||||||
pub const LEN: usize = primitives!(@count $($name)*);
|
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 const BINDINGS: [u32; Self::LEN] = [$(<$ty>::BINDING,)*];
|
||||||
pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
|
pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
|
||||||
[
|
[
|
||||||
@@ -145,16 +121,51 @@ impl Primitives {
|
|||||||
mask_idx,
|
mask_idx,
|
||||||
}: PrimitiveInst<P>,
|
}: PrimitiveInst<P>,
|
||||||
) -> PrimitiveHandle {
|
) -> PrimitiveHandle {
|
||||||
self.updated = true;
|
let data_idx = P::vec(&mut self.data).add(primitive);
|
||||||
let vec = P::vec(&mut self.data);
|
self.push(
|
||||||
let i = vec.add(primitive);
|
layer,
|
||||||
let inst = PrimitiveInstance {
|
id,
|
||||||
|
PrimitiveInstance {
|
||||||
region,
|
region,
|
||||||
idx: i as u32,
|
idx: data_idx as u32,
|
||||||
mask_idx,
|
mask_idx,
|
||||||
binding: P::BINDING,
|
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.instances[i] = inst;
|
||||||
self.assoc[i] = id;
|
self.assoc[i] = id;
|
||||||
i
|
i
|
||||||
@@ -164,107 +175,37 @@ impl Primitives {
|
|||||||
self.assoc.push(id);
|
self.assoc.push(id);
|
||||||
i
|
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 {
|
PrimitiveHandle {
|
||||||
layer,
|
layer,
|
||||||
inst_idx: inst_i,
|
inst_idx,
|
||||||
data_idx: 0,
|
data_idx,
|
||||||
binding: IMAGE_BINDING,
|
binding: inst.binding,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn image_instances(&self) -> &Vec<PrimitiveInstance> {
|
/// returns (old index, new index)
|
||||||
&self.images
|
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| {
|
||||||
/// returns (old index, new index) for both lists this layer keeps --
|
self.instances.swap_remove(i);
|
||||||
/// `PrimitiveChange::is_image` says which, since the two have separate
|
self.assoc.swap_remove(i);
|
||||||
/// index spaces and `old`/`new` alone would collide between them.
|
if i == self.instances.len() {
|
||||||
///
|
|
||||||
/// 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;
|
return None;
|
||||||
}
|
}
|
||||||
let id = assoc[i];
|
let id = self.assoc[i];
|
||||||
let old = instances.len();
|
let old = self.instances.len();
|
||||||
Some(PrimitiveChange {
|
Some(PrimitiveChange { id, old, new: i })
|
||||||
id,
|
|
||||||
is_image,
|
|
||||||
old,
|
|
||||||
new: i,
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
||||||
self.updated = true;
|
self.updated = true;
|
||||||
if h.binding == IMAGE_BINDING {
|
if let Some(i) = h.data_idx {
|
||||||
self.image_free.push(h.inst_idx);
|
self.data.free(h.binding, i);
|
||||||
self.images[h.inst_idx].mask_idx
|
}
|
||||||
} else {
|
|
||||||
self.data.free(h.binding, h.data_idx);
|
|
||||||
self.free.push(h.inst_idx);
|
self.free.push(h.inst_idx);
|
||||||
self.instances[h.inst_idx].mask_idx
|
self.instances[h.inst_idx].mask_idx
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn data(&self) -> &PrimitiveData {
|
pub fn data(&self) -> &PrimitiveData {
|
||||||
&self.data
|
&self.data
|
||||||
@@ -276,21 +217,12 @@ impl Primitives {
|
|||||||
|
|
||||||
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
|
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
|
||||||
self.updated = true;
|
self.updated = true;
|
||||||
if h.binding == IMAGE_BINDING {
|
|
||||||
&mut self.images[h.inst_idx].region
|
|
||||||
} else {
|
|
||||||
&mut self.instances[h.inst_idx].region
|
&mut self.instances[h.inst_idx].region
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PrimitiveChange {
|
pub struct PrimitiveChange {
|
||||||
pub id: WidgetId,
|
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 old: usize,
|
||||||
pub new: usize,
|
pub new: usize,
|
||||||
}
|
}
|
||||||
@@ -299,24 +231,14 @@ pub struct PrimitiveChange {
|
|||||||
pub struct PrimitiveHandle {
|
pub struct PrimitiveHandle {
|
||||||
pub layer: usize,
|
pub layer: usize,
|
||||||
pub inst_idx: 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,
|
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!(
|
primitives!(
|
||||||
rects: RectPrimitive => 0,
|
rects: RectPrimitive => 0,
|
||||||
glyphs: GlyphPrimitive => 2,
|
glyphs: GlyphPrimitive => 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -339,42 +261,17 @@ impl RectPrimitive {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One glyph, drawn as a sub-rectangle of the glyph atlas array.
|
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
|
||||||
///
|
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
|
||||||
/// `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)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct GlyphPrimitive {
|
pub struct GlyphPrimitive {
|
||||||
pub uv_min: Vec2,
|
pub uv_min: Vec2,
|
||||||
pub uv_max: Vec2,
|
pub uv_max: Vec2,
|
||||||
/// Layer of the shared atlas array texture this glyph's page occupies --
|
/// Which atlas array layer this glyph is on.
|
||||||
/// not a bind-group or view index, since a page never gets one of its own.
|
|
||||||
pub layer: u32,
|
pub layer: u32,
|
||||||
pub color: Color<u8>,
|
pub color: Color<u8>,
|
||||||
pub flags: u32,
|
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> {
|
pub struct PrimitiveVec<T> {
|
||||||
|
|||||||
+18
-24
@@ -1,9 +1,7 @@
|
|||||||
const RECT: u32 = 0u;
|
const RECT: u32 = 0u;
|
||||||
// TEXTURE has no entry in group 1: a standalone image draws with its own
|
const GLYPH: u32 = 1u;
|
||||||
// bind group (see UiRenderNode::draw), so there is nothing per-instance left
|
// No group 1 entry: a texture instance's idx names the texture bound in group 2.
|
||||||
// to look up here -- the bind group already picked the texture.
|
const TEXTURE: u32 = 2u;
|
||||||
const TEXTURE: u32 = 1u;
|
|
||||||
const GLYPH: u32 = 2u;
|
|
||||||
|
|
||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<uniform> window: WindowUniform;
|
var<uniform> window: WindowUniform;
|
||||||
@@ -19,11 +17,13 @@ struct Rect {
|
|||||||
inner_radius: f32,
|
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 {
|
struct GlyphInfo {
|
||||||
uv_min: vec2<f32>,
|
uv_min_x: f32,
|
||||||
uv_max: vec2<f32>,
|
uv_min_y: f32,
|
||||||
// Layer of the shared atlas array texture, not a view or bind-group
|
uv_max_x: f32,
|
||||||
// index -- a page never gets its own bind group.
|
uv_max_y: f32,
|
||||||
layer: u32,
|
layer: u32,
|
||||||
color: u32,
|
color: u32,
|
||||||
flags: u32,
|
flags: u32,
|
||||||
@@ -49,21 +49,13 @@ struct UiVec2 {
|
|||||||
abs: vec2<f32>,
|
abs: vec2<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The shared glyph atlas: every page is one layer. Growing it recreates this
|
// Whatever this run of instances samples: the glyph atlas, whose layers are
|
||||||
// texture with headroom and copies the old layers across -- see
|
// its pages, or one standalone image as an array of one.
|
||||||
// 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.
|
|
||||||
@group(2) @binding(0)
|
@group(2) @binding(0)
|
||||||
var atlas: texture_2d_array<f32>;
|
var tex: 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.
|
|
||||||
@group(2) @binding(1)
|
@group(2) @binding(1)
|
||||||
var image_texture: texture_2d<f32>;
|
|
||||||
@group(2) @binding(2)
|
|
||||||
var samp: sampler;
|
var samp: sampler;
|
||||||
@group(2) @binding(3)
|
@group(3) @binding(0)
|
||||||
var<storage> masks: array<Mask>;
|
var<storage> masks: array<Mask>;
|
||||||
|
|
||||||
struct WindowUniform {
|
struct WindowUniform {
|
||||||
@@ -166,12 +158,14 @@ fn fs_main(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_texture(region: Region) -> vec4<f32> {
|
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> {
|
fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
|
||||||
let uv = mix(g.uv_min, g.uv_max, region.uv);
|
let uv_min = vec2(g.uv_min_x, g.uv_min_y);
|
||||||
let texel = textureSample(atlas, samp, uv, i32(g.layer));
|
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 {
|
if (g.flags & 1u) != 0u {
|
||||||
return texel;
|
return texel;
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-385
@@ -1,289 +1,65 @@
|
|||||||
use image::{DynamicImage, EncodableLayout, GenericImageView};
|
use image::{DynamicImage, EncodableLayout, GenericImageView};
|
||||||
use wgpu::{util::DeviceExt, *};
|
use wgpu::{util::DeviceExt, *};
|
||||||
|
|
||||||
use crate::{Mask, PatchRect, TextureKind, TextureUpdate, Textures, render::util::ArrBuf};
|
use crate::{PatchRect, TextureUpdate, Textures};
|
||||||
|
|
||||||
use super::atlas::PAGE;
|
/// 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.
|
||||||
/// 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.
|
|
||||||
pub struct GpuTextures {
|
pub struct GpuTextures {
|
||||||
device: Device,
|
device: Device,
|
||||||
queue: Queue,
|
queue: Queue,
|
||||||
|
slots: Vec<Option<ImageGpu>>,
|
||||||
|
}
|
||||||
|
|
||||||
slots: Vec<Slot>,
|
struct ImageGpu {
|
||||||
|
/// Kept for `patch`, which needs the texture rather than the view.
|
||||||
array_texture: Texture,
|
texture: Texture,
|
||||||
array_view: TextureView,
|
group: BindGroup,
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpuTextures {
|
impl GpuTextures {
|
||||||
/// Applies queued `Textures` updates, then reports whether the *main*
|
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||||
/// bind group (the one rects and glyphs draw with) needs rebuilding --
|
Self {
|
||||||
/// true when the atlas array was recreated (its view identity changed)
|
device: device.clone(),
|
||||||
/// or the masks buffer was, since both are bound there. Pushing or
|
queue: queue.clone(),
|
||||||
/// freeing a standalone image never touches that group: it built or drops
|
slots: Vec::new(),
|
||||||
/// 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 update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||||
for update in textures.updates() {
|
for update in textures.updates() {
|
||||||
match update {
|
match update {
|
||||||
TextureUpdate::Push(kind, image) => {
|
TextureUpdate::Push(image) => {
|
||||||
rebuild_main |= self.push(kind, image, rsc_layout, masks);
|
let image = self.create(image, layout, sampler);
|
||||||
|
self.slots.push(Some(image));
|
||||||
}
|
}
|
||||||
TextureUpdate::Set(kind, i, image) => {
|
TextureUpdate::Set(i, image) => {
|
||||||
rebuild_main |= self.set(kind, i, image, rsc_layout, masks);
|
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::Patch(i, rect, image) => self.patch(i, rect, image),
|
||||||
|
TextureUpdate::PushFree => self.slots.push(None),
|
||||||
TextureUpdate::SetFree => {}
|
TextureUpdate::SetFree => {}
|
||||||
TextureUpdate::Free(i) => self.free(i),
|
TextureUpdate::Free(i) => self.slots[i as usize] = None,
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn free(&mut self, i: u32) {
|
/// `None` once freed. A drawn instance holds a `TextureHandle`, so its
|
||||||
if let Some(slot) = self.slots.get_mut(i as usize) {
|
/// slot outlives it.
|
||||||
*slot = Slot::Empty;
|
pub fn group(&self, slot: u32) -> Option<&BindGroup> {
|
||||||
}
|
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
|
||||||
// A page's layer is not reclaimed here either -- see `Slot::Page`.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
pub fn count(&self) -> usize {
|
||||||
let Some(&Slot::Page(layer)) = self.slots.get(i as usize) else {
|
self.slots.iter().flatten().count()
|
||||||
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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_full_layer(&self, layer: u32, image: &DynamicImage) {
|
fn create(
|
||||||
// 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(
|
|
||||||
&self,
|
&self,
|
||||||
image: &DynamicImage,
|
image: &DynamicImage,
|
||||||
rsc_layout: &BindGroupLayout,
|
layout: &BindGroupLayout,
|
||||||
masks: &ArrBuf<Mask>,
|
sampler: &Sampler,
|
||||||
) -> ImageGpu {
|
) -> ImageGpu {
|
||||||
let rgba = image.to_rgba8();
|
let rgba = image.to_rgba8();
|
||||||
let (width, height) = rgba.dimensions();
|
let (width, height) = rgba.dimensions();
|
||||||
@@ -306,152 +82,77 @@ impl GpuTextures {
|
|||||||
wgt::TextureDataOrder::MipMajor,
|
wgt::TextureDataOrder::MipMajor,
|
||||||
rgba.as_bytes(),
|
rgba.as_bytes(),
|
||||||
);
|
);
|
||||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
let group = texture_group(&self.device, layout, &array_view(&texture), sampler);
|
||||||
let bind_group = Self::make_image_bind_group(
|
ImageGpu { texture, group }
|
||||||
&self.device,
|
|
||||||
rsc_layout,
|
|
||||||
&self.array_view,
|
|
||||||
&view,
|
|
||||||
&self.sampler,
|
|
||||||
masks,
|
|
||||||
);
|
|
||||||
ImageGpu { view, bind_group }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds group 2 for one standalone image: the shared atlas array, this
|
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
||||||
/// image's own view, the shared sampler, and the shared masks buffer --
|
let Some(Some(slot)) = self.slots.get(i as usize) else {
|
||||||
/// the same layout the main draw uses with a null view in the image slot.
|
return;
|
||||||
fn make_image_bind_group(
|
};
|
||||||
|
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,
|
device: &Device,
|
||||||
rsc_layout: &BindGroupLayout,
|
layout: &BindGroupLayout,
|
||||||
array_view: &TextureView,
|
view: &TextureView,
|
||||||
image_view: &TextureView,
|
|
||||||
sampler: &Sampler,
|
sampler: &Sampler,
|
||||||
masks: &ArrBuf<Mask>,
|
) -> BindGroup {
|
||||||
) -> BindGroup {
|
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
layout: rsc_layout,
|
layout,
|
||||||
entries: &[
|
entries: &[
|
||||||
BindGroupEntry {
|
BindGroupEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
resource: BindingResource::TextureView(array_view),
|
resource: BindingResource::TextureView(view),
|
||||||
},
|
},
|
||||||
BindGroupEntry {
|
BindGroupEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
resource: BindingResource::TextureView(image_view),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 2,
|
|
||||||
resource: BindingResource::Sampler(sampler),
|
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 {
|
pub fn array_view(texture: &Texture) -> TextureView {
|
||||||
device.create_texture(&TextureDescriptor {
|
texture.create_view(&TextureViewDescriptor {
|
||||||
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 {
|
|
||||||
dimension: Some(TextureViewDimension::D2Array),
|
dimension: Some(TextureViewDimension::D2Array),
|
||||||
..Default::default()
|
..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 {
|
pub fn default_sampler(device: &Device) -> Sampler {
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ impl<T: Pod> ArrBuf<T> {
|
|||||||
_pd: PhantomData,
|
_pd: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Returns whether the underlying `Buffer` was recreated -- a caller that
|
/// Returns whether the `Buffer` was recreated, which stales any cached
|
||||||
/// cached a `BindGroup` referencing it (as `GpuTextures` does for the
|
/// `BindGroup` holding it.
|
||||||
/// masks buffer) needs to know to rebuild that too.
|
|
||||||
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
|
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
|
||||||
let resized = self.len != data.len();
|
let resized = self.len != data.len();
|
||||||
if resized {
|
if resized {
|
||||||
@@ -46,8 +45,4 @@ impl<T: Pod> ArrBuf<T> {
|
|||||||
usage,
|
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,
|
mask_idx: self.mask,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
self.push_primitive(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_primitive(&mut self, h: PrimitiveHandle) {
|
||||||
if self.mask != MaskIdx::NONE {
|
if self.mask != MaskIdx::NONE {
|
||||||
// TODO: I have no clue if this works at all :joy:
|
// TODO: I have no clue if this works at all :joy:
|
||||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
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) {
|
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||||
self.textures.push(handle.clone());
|
self.texture_at(handle, region.within(&self.region));
|
||||||
self.write_image(handle.image_index(), region.within(&self.region));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn texture(&mut self, handle: &TextureHandle) {
|
pub fn texture(&mut self, handle: &TextureHandle) {
|
||||||
self.textures.push(handle.clone());
|
self.texture_at(handle, self.region);
|
||||||
self.write_image(handle.image_index(), self.region);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
|
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||||
self.textures.push(handle.clone());
|
self.textures.push(handle.clone());
|
||||||
self.write_image(handle.image_index(), region);
|
let h =
|
||||||
}
|
self.state
|
||||||
|
|
||||||
/// 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
|
|
||||||
.layers
|
.layers
|
||||||
.write_image(self.layer, self.id, texture_idx, region, self.mask);
|
.write_texture(self.layer, self.id, handle.slot(), region, self.mask);
|
||||||
if self.mask != MaskIdx::NONE {
|
self.push_primitive(h);
|
||||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
|
||||||
}
|
|
||||||
self.primitives.push(h);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_text(
|
pub fn render_text(
|
||||||
@@ -111,7 +103,7 @@ impl<'a> Painter<'a> {
|
|||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
let ui = self.rsc.ui_mut();
|
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) {
|
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.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);
|
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
|
||||||
self.primitive_at(
|
self.primitive_at(
|
||||||
GlyphPrimitive::new(
|
GlyphPrimitive {
|
||||||
glyph.entry.uv_min,
|
uv_min: glyph.entry.uv_min,
|
||||||
glyph.entry.uv_max,
|
uv_max: glyph.entry.uv_max,
|
||||||
glyph.entry.layer,
|
layer: glyph.entry.layer,
|
||||||
text.color,
|
color: text.color,
|
||||||
glyph.entry.flags(),
|
flags: glyph.entry.flags(),
|
||||||
),
|
},
|
||||||
region,
|
region,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,7 +308,6 @@ impl UiRenderState {
|
|||||||
source,
|
source,
|
||||||
cache: &mut self.cache,
|
cache: &mut self.cache,
|
||||||
text: &mut ui.text,
|
text: &mut ui.text,
|
||||||
textures: &mut ui.textures,
|
|
||||||
widgets: &ui.widgets,
|
widgets: &ui.widgets,
|
||||||
outer,
|
outer,
|
||||||
output_size: self.output_size,
|
output_size: self.output_size,
|
||||||
|
|||||||
+3
-5
@@ -1,11 +1,10 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
|
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
|
||||||
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct SizeCtx<'a> {
|
pub struct SizeCtx<'a> {
|
||||||
pub text: &'a mut TextData,
|
pub text: &'a mut TextData,
|
||||||
pub textures: &'a mut Textures,
|
|
||||||
pub(super) source: WidgetId,
|
pub(super) source: WidgetId,
|
||||||
pub(super) widgets: &'a Widgets,
|
pub(super) widgets: &'a Widgets,
|
||||||
pub(super) cache: &'a mut Cache,
|
pub(super) cache: &'a mut Cache,
|
||||||
@@ -33,7 +32,6 @@ impl SizeCtx<'_> {
|
|||||||
.get_dyn_dynamic(id)
|
.get_dyn_dynamic(id)
|
||||||
.desired_len::<A>(&mut SizeCtx {
|
.desired_len::<A>(&mut SizeCtx {
|
||||||
text: self.text,
|
text: self.text,
|
||||||
textures: self.textures,
|
|
||||||
source: self.source,
|
source: self.source,
|
||||||
widgets: self.widgets,
|
widgets: self.widgets,
|
||||||
cache: self.cache,
|
cache: self.cache,
|
||||||
@@ -82,7 +80,7 @@ impl SizeCtx<'_> {
|
|||||||
attrs: &TextAttrs,
|
attrs: &TextAttrs,
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
self.text.render(buffer, attrs, width, self.textures)
|
self.text.render(buffer, attrs, width)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn label(&self, id: WidgetId) -> &String {
|
pub fn label(&self, id: WidgetId) -> &String {
|
||||||
|
|||||||
@@ -212,10 +212,10 @@ impl DefaultAppState for Client {
|
|||||||
render: &mut UiRenderState,
|
render: &mut UiRenderState,
|
||||||
) {
|
) {
|
||||||
let new = format!(
|
let new = format!(
|
||||||
"widgets: {}\nactive: {}\nviews: {}",
|
"widgets: {}\nactive: {}\ntextures: {}",
|
||||||
rsc.widgets().len(),
|
rsc.widgets().len(),
|
||||||
render.active_widgets(),
|
render.active_widgets(),
|
||||||
self.ui_state.renderer.ui.view_count(),
|
self.ui_state.renderer.ui.texture_count(),
|
||||||
);
|
);
|
||||||
if new != *rsc.widgets()[self.info].content {
|
if new != *rsc.widgets()[self.info].content {
|
||||||
*rsc.widgets_mut()[self.info].content = new;
|
*rsc.widgets_mut()[self.info].content = new;
|
||||||
|
|||||||
@@ -83,12 +83,9 @@ impl UiRenderer {
|
|||||||
.block_on()
|
.block_on()
|
||||||
.expect("Could not get adapter!");
|
.expect("Could not get adapter!");
|
||||||
|
|
||||||
// No features beyond what wgpu asks for by default, and no
|
// No binding-array features or limits: the atlas is one
|
||||||
// binding-array limits: the atlas is one texture_2d_array and a
|
// texture_2d_array and an image its own bind group, so nothing here
|
||||||
// standalone image is its own ordinary bind group, neither of which
|
// needs VK_EXT_descriptor_indexing the way the old layout did.
|
||||||
// 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.
|
|
||||||
let (device, queue) = adapter
|
let (device, queue) = adapter
|
||||||
.request_device(&DeviceDescriptor {
|
.request_device(&DeviceDescriptor {
|
||||||
required_limits: Limits {
|
required_limits: Limits {
|
||||||
|
|||||||
Reference in new issue
Block a user