Compare commits

..
17 changed files with 416 additions and 984 deletions

No files matched your search

Generated
-1
View File
@@ -1189,7 +1189,6 @@ dependencies = [
"bytemuck",
"fxhash",
"image",
"log",
"parley",
"swash",
"wgpu",
-1
View File
@@ -35,7 +35,6 @@ image = "0.25.6"
parley = "0.11.1"
swash = "0.2.10"
fxhash = "0.2.1"
log = "0.4.29"
arboard = "3.6.1"
iris-core = { path = "core" }
iris-macro = { path = "macro" }
+25 -1
View File
@@ -1,6 +1,19 @@
images
settings (sampler)
consider typed TextureHandle<T> variants for distinct texture uses
text
figure out ways to speed up / what costs the most
resizing (per frame) is really slow (assuming painter isn't griefing)
j is weird / fix x offset
masks r just made to bare minimum work
scaling
could be just a simple scaling factor that multiplies abs
and need to ensure text uses raw abs and not scaled abs
naming? (pt, px)
want to keep (drawn) regions using px? or should I add another field to UiScalar/Vec
field could be best solution so redrawing stuff isn't needed & you can specify both as user
WidgetRef<W> or smth instead of Id
enum that's either an Id or an actual concrete instance of W
@@ -11,6 +24,17 @@ WidgetRef<W> or smth instead of Id
maybe introduce InnerWidget trait to allow for editors to expose & modify inner type
maybe could also store a parent widget and keep using InnerWidget trait? unsure if possible
really weird limitation:
I don't think you can currently remove an element from a parent and put it in a child of the same parent
because it removes the unused children after the entire parent redraw
but the child gets drawn during that, so it will think the child is still active !!!
or something like that idk, maybe I need a special enum for parent that includes a undecided state where it may or may not get redrawn by the parent
or just do ref counting and ensure all drawn things == 1 afterwards (seems like best way)
ok so I'm removing the limit for now
don't forget I'm streaming
tags
vecs for each widget type?
POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..??
-1
View File
@@ -10,4 +10,3 @@ image = { workspace = true }
parley = { workspace = true }
swash = { workspace = true }
fxhash = { workspace = true }
log = { workspace = true }
-12
View File
@@ -1,7 +1,6 @@
use std::ops::{Index, IndexMut};
use crate::{
UiRegion, WidgetId,
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
util::to_mut,
};
@@ -132,17 +131,6 @@ impl PrimitiveLayers {
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self[h.layer].free(h)
}
pub fn write_image(
&mut self,
layer: LayerId,
id: WidgetId,
texture_idx: u32,
region: UiRegion,
mask_idx: MaskIdx,
) -> PrimitiveHandle {
self[layer].write_image(layer, id, texture_idx, region, mask_idx)
}
}
impl<T: Default> Default for Layers<T> {
+54 -95
View File
@@ -1,12 +1,8 @@
use crate::{
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor,
util::Vec2,
};
use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2};
use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
};
use std::hash::{DefaultHasher, Hash, Hasher};
use swash::{
FontRef,
scale::{Render, ScaleContext, Source, StrikeWith},
@@ -14,18 +10,18 @@ use swash::{
};
pub struct TextData {
pub font_ctx: FontContext,
pub layout_ctx: LayoutContext<UiColor>,
scale_ctx: ScaleContext,
pub font_cx: FontContext,
pub layout_cx: LayoutContext<UiColor>,
scale_cx: ScaleContext,
pub atlas: GlyphAtlas,
}
impl Default for TextData {
fn default() -> Self {
Self {
font_ctx: FontContext::new(),
layout_ctx: LayoutContext::new(),
scale_ctx: ScaleContext::new(),
font_cx: FontContext::new(),
layout_cx: LayoutContext::new(),
scale_cx: ScaleContext::new(),
atlas: GlyphAtlas::default(),
}
}
@@ -81,13 +77,7 @@ impl Default for TextAttrs {
pub struct TextBuffer {
text: String,
layout: Layout<UiColor>,
layout_key: Option<LayoutKey>,
}
#[derive(PartialEq)]
struct LayoutKey {
attrs: TextAttrs,
max_width: Option<f32>,
shaped: Option<(TextAttrs, Option<f32>)>,
}
impl TextBuffer {
@@ -95,7 +85,7 @@ impl TextBuffer {
Self {
text: text.into(),
layout: Layout::new(),
layout_key: None,
shaped: None,
}
}
@@ -119,13 +109,13 @@ impl TextBuffer {
let text = text.into();
if text != self.text {
self.text = text;
self.layout_key = None;
self.shaped = None;
}
}
/// Invalidates the layout and returns the underlying string for editing.
pub fn edit(&mut self) -> &mut String {
self.layout_key = None;
self.shaped = None;
&mut self.text
}
@@ -134,16 +124,12 @@ impl TextBuffer {
}
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
let layout_key = LayoutKey {
attrs: attrs.clone(),
max_width: width,
};
if self.layout_key.as_ref() == Some(&layout_key) {
if self.shaped.as_ref() == Some(&(attrs.clone(), width)) {
return;
}
let mut builder = data
.layout_ctx
.ranged_builder(&mut data.font_ctx, &self.text, 1.0, true);
.layout_cx
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
builder.push_default(StyleProperty::FontFamily(attrs.family.family()));
builder.push_default(StyleProperty::FontSize(attrs.font_size));
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
@@ -154,7 +140,7 @@ impl TextBuffer {
self.layout.break_all_lines(width);
self.layout
.align(Alignment::Start, AlignmentOptions::default());
self.layout_key = Some(layout_key);
self.shaped = Some((attrs.clone(), width));
}
}
@@ -174,6 +160,8 @@ impl TextData {
continue;
};
let coords_hash = hash_coords(coords);
// `font.data.id()` rather than the pointer, so the same font
// loaded twice is still one set of entries.
let font_id = font.data.id();
for glyph in run.positioned_glyphs() {
@@ -181,23 +169,38 @@ impl TextData {
let key = GlyphKey {
font: font_id,
glyph: glyph.id,
size: glyph_size_key(font_size),
size: (font_size * 16.0).round() as u32,
subpixel,
coords: coords_hash,
};
let Some(entry) = self.glyph_entry(
GlyphRaster {
key,
font: font_ref,
font_size,
coords,
subpixel,
glyph_id: glyph.id,
},
textures,
) else {
continue;
let entry = match self.atlas.get(&key) {
Some(entry) => entry,
None => {
let mut scaler = self
.scale_cx
.builder(font_ref)
.size(font_size)
.hint(true)
.normalized_coords(coords)
.build();
let image = Render::new(&[
Source::ColorOutline(0),
Source::ColorBitmap(StrikeWith::BestFit),
Source::Outline,
])
.format(Format::Alpha)
.offset(Vector::new(subpixel as f32 / 4.0, 0.0))
.render(&mut scaler, glyph.id as u16);
match image {
Some(image) => self.atlas.insert(key, &image, textures),
None => {
self.atlas.insert_empty(key);
None
}
}
}
};
let Some(entry) = entry else { continue };
placed.push(PlacedGlyph {
entry,
offset: Vec2::new(
@@ -210,64 +213,20 @@ impl TextData {
}
placed
}
fn glyph_entry(
&mut self,
glyph: GlyphRaster<'_>,
textures: &mut Textures,
) -> Option<GlyphEntry> {
if let Some(entry) = self.atlas.get(&glyph.key) {
return entry;
}
let mut scaler = self
.scale_ctx
.builder(glyph.font)
.size(glyph.font_size)
.hint(true)
.normalized_coords(glyph.coords)
.build();
let image = Render::new(&[
Source::ColorOutline(0),
Source::ColorBitmap(StrikeWith::BestFit),
Source::Outline,
])
.format(Format::Alpha)
.offset(Vector::new(glyph.subpixel as f32 / 4.0, 0.0))
.render(&mut scaler, glyph.glyph_id as u16);
if let Some(image) = image {
self.atlas.insert(glyph.key, &image, textures)
} else {
self.atlas.insert_empty(glyph.key);
None
}
}
}
struct GlyphRaster<'a> {
key: GlyphKey,
font: FontRef<'a>,
font_size: f32,
coords: &'a [i16],
subpixel: u8,
glyph_id: u32,
}
fn hash_coords(coords: &[i16]) -> u64 {
let mut hasher = DefaultHasher::new();
coords.hash(&mut hasher);
hasher.finish()
}
const GLYPH_SIZE_STEPS_PER_PIXEL: f32 = 16.0;
fn glyph_size_key(font_size: f32) -> u32 {
(font_size * GLYPH_SIZE_STEPS_PER_PIXEL).round() as u32
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for c in coords {
h ^= *c as u16 as u64;
h = h.wrapping_mul(0x1000_0000_01b3);
}
h
}
#[derive(Clone)]
pub struct RenderedText {
pub glyphs: Vec<PlacedGlyph>,
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
pub size: Vec2,
pub color: UiColor,
}
@@ -283,7 +242,7 @@ impl TextData {
buffer.shape(self, attrs, width);
let glyphs = self.place(buffer, textures);
RenderedText {
glyphs,
glyphs: std::sync::Arc::new(glyphs),
size: buffer.size(),
color: attrs.color,
}
+37 -98
View File
@@ -1,32 +1,19 @@
use crate::util::{RefCounter, Vec2};
use crate::{
render::TexturePrimitive,
util::{RefCounter, Vec2},
};
use image::{DynamicImage, GenericImageView};
use std::{
ops::Index,
sync::mpsc::{Receiver, Sender, channel},
};
/// Which of the two things a texture slot holds. The two are drawn very
/// differently: a page is a layer of one shared array texture and never gets
/// its own bind group; a standalone image is the opposite, one texture and
/// one bind group, never a layer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextureKind {
Image,
/// The array-texture layer this page was assigned. Chosen synchronously
/// by `Textures::add_page` rather than by the renderer, because glyph
/// insertion needs it in the same call, before any GPU sync happens.
Page {
layer: u32,
},
}
#[derive(Debug, Clone)]
pub struct TextureHandle {
slot: u32,
kind: TextureKind,
inner: TexturePrimitive,
size: Vec2,
counter: RefCounter,
send: Sender<(TextureKind, u32)>,
send: Sender<u32>,
}
/// a texture manager for a ui
@@ -34,24 +21,17 @@ 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),
PushFree,
SetFree,
}
@@ -64,8 +44,8 @@ pub struct PatchRect {
}
enum Update {
Push(TextureKind, u32),
Set(TextureKind, u32),
Push(u32),
Set(u32),
Patch(u32, PatchRect),
Free(u32),
}
@@ -76,93 +56,70 @@ 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);
let view_idx = self.push(image);
// 0 == default in renderer; TODO: actually create samplers here
let sampler_idx = 0;
TextureHandle {
slot,
kind,
inner: TexturePrimitive {
view_idx,
sampler_idx,
},
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
}
}
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
self.images[handle.slot as usize]
self.images[handle.inner.view_idx as usize]
.as_mut()
.expect("texture was freed while still held")
}
/// Queue an upload of just `rect`, after writing it with `image_mut`.
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
self.updates.push(Update::Patch(handle.slot, rect));
self.updates
.push(Update::Patch(handle.inner.view_idx, rect));
}
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 +131,18 @@ impl Textures {
}
impl TextureHandle {
pub fn primitive(&self) -> TexturePrimitive {
self.inner
}
pub fn size(&self) -> Vec2 {
self.size
}
/// The bind-group index this handle draws with. Only valid for a
/// standalone image; an atlas page has no bind group of its own -- it
/// samples the shared array via `layer()` instead. Getting this wrong is
/// a caller bug (the wrong kind of handle reached the wrong draw path),
/// not a recoverable condition, so it panics rather than drawing garbage.
pub fn image_index(&self) -> u32 {
match self.kind {
TextureKind::Image => self.slot,
TextureKind::Page { .. } => panic!("image_index() called on an atlas page handle"),
}
}
/// The layer this page occupies in the shared atlas array texture.
/// Only valid for a page handle; see `image_index`'s note.
pub fn layer(&self) -> u32 {
match self.kind {
TextureKind::Page { layer } => layer,
TextureKind::Image => panic!("layer() called on a standalone image handle"),
}
}
}
impl Drop for TextureHandle {
fn drop(&mut self) {
if self.counter.drop() {
let _ = self.send.send((self.kind, self.slot));
let _ = self.send.send(self.inner.view_idx);
}
}
}
@@ -212,7 +151,7 @@ impl Index<&TextureHandle> for Textures {
type Output = DynamicImage;
fn index(&self, index: &TextureHandle) -> &Self::Output {
self.images[index.slot as usize].as_ref().unwrap()
self.images[index.inner.view_idx as usize].as_ref().unwrap()
}
}
+55 -91
View File
@@ -5,12 +5,7 @@ use crate::{
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.
pub(crate) const PAGE: u32 = 1024;
const PAGE: u32 = 1024;
/// Transparent margin kept around every glyph, so that sampling one cannot
/// pick up its neighbour along a shared edge.
@@ -32,24 +27,16 @@ pub struct GlyphKey {
#[derive(Clone, Copy)]
pub struct GlyphEntry {
pub uv_min: Vec2,
pub uv_max: Vec2,
pub uv_min: [f32; 2],
pub uv_max: [f32; 2],
/// Offset from the glyph's pen position to the top-left of its pixels.
pub left: i32,
pub top: i32,
pub width: u32,
pub height: u32,
pub is_colored: bool,
/// The atlas array layer this glyph's page occupies.
pub layer: u32,
}
impl GlyphEntry {
const IS_COLORED: u32 = 1;
pub(crate) fn flags(&self) -> u32 {
if self.is_colored { Self::IS_COLORED } else { 0 }
}
pub is_color: bool,
pub view_idx: u32,
pub sampler_idx: u32,
}
struct Page {
@@ -81,20 +68,10 @@ impl GlyphAtlas {
let w = image.placement.width;
let h = image.placement.height;
if w == 0 || h == 0 {
log::warn!(
"glyph {} in font {} rasterized at {w}x{h}; skipping it",
key.glyph,
key.font,
);
self.entries.insert(key, None);
return None;
}
if w > PAGE - PAD * 2 || h > PAGE - PAD * 2 {
log::warn!(
"glyph {} in font {} rasterized at {w}x{h}, too large for the {PAGE}x{PAGE} atlas; skipping it",
key.glyph,
key.font,
);
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
self.entries.insert(key, None);
return None;
}
@@ -118,30 +95,37 @@ impl GlyphAtlas {
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),
uv_max: Vec2::new((x + w) as f32 * scale, (y + h) as f32 * scale),
uv_min: [x as f32 * scale, y as f32 * scale],
uv_max: [(x + w) as f32 * scale, (y + h) as f32 * scale],
left: image.placement.left,
top: image.placement.top,
width: w,
height: h,
is_colored: matches!(image.content, Content::Color),
layer: page.handle.layer(),
is_color: matches!(image.content, Content::Color),
view_idx: page.handle.primitive().view_idx,
sampler_idx: page.handle.primitive().sampler_idx,
};
self.entries.insert(key, Some(entry));
Some(entry)
}
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, 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)))
{
let need_w = w + PAD;
let need_h = h + PAD;
if let Some(i) = self.pages.iter().position(|p| fits(p, need_w, need_h)) {
let page = &mut self.pages[i];
if page.x + need_w > PAGE {
page.y += page.shelf_height;
page.x = PAD;
page.shelf_height = 0;
}
let (x, y) = (page.x, page.y);
page.x += need_w;
page.shelf_height = page.shelf_height.max(need_h);
return (i, x, y);
}
let handle = textures.add_page(RgbaImage::new(PAGE, PAGE));
let handle = textures.add(RgbaImage::new(PAGE, PAGE));
self.pages.push(Page {
handle,
x: PAD + w + PAD,
@@ -164,65 +148,45 @@ impl GlyphAtlas {
}
}
impl Page {
fn allocate(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
let need_w = w + PAD;
let need_h = h + PAD;
if self.x + need_w > PAGE {
if need_w + PAD > PAGE || self.y + self.shelf_height + need_h > PAGE {
return None;
}
self.y += self.shelf_height;
self.x = PAD;
self.shelf_height = 0;
} else if self.y + need_h > PAGE {
return None;
}
let position = (self.x, self.y);
self.x += need_w;
self.shelf_height = self.shelf_height.max(need_h);
Some(position)
}
fn fits(page: &Page, need_w: u32, need_h: u32) -> bool {
(page.x + need_w <= PAGE && page.y + need_h <= PAGE)
|| (need_w + PAD <= PAGE && page.y + page.shelf_height + need_h <= PAGE)
}
/// Mask glyphs keep coverage in alpha so their raster can be tinted at draw time.
fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
let width = image.placement.width as usize;
let height = image.placement.height as usize;
let page_stride = page.width() as usize * 4;
let x = x as usize * 4;
let y = y as usize;
let page = page.as_mut();
for row in 0..height {
let start = (y + row) * page_stride + x;
let target = &mut page[start..start + width * 4];
let w = image.placement.width;
let h = image.placement.height;
match image.content {
Content::Color => {
let start = row * width * 4;
target.copy_from_slice(&image.data[start..start + width * 4]);
}
Content::Mask => {
let start = row * width;
for (target, &alpha) in target
.as_chunks_mut::<4>()
.0
.iter_mut()
.zip(&image.data[start..start + width])
{
target.copy_from_slice(&[255, 255, 255, alpha]);
for row in 0..h {
for col in 0..w {
let a = image.data[(row * w + col) as usize];
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
}
}
}
Content::Color => {
for row in 0..h {
for col in 0..w {
let i = ((row * w + col) * 4) as usize;
let px = [
image.data[i],
image.data[i + 1],
image.data[i + 2],
image.data[i + 3],
];
page.put_pixel(x + col, y + row, image::Rgba(px));
}
}
}
Content::SubpixelMask => {
let start = row * width * 4;
for (target, source) in target
.as_chunks_mut::<4>()
.0
.iter_mut()
.zip(image.data[start..start + width * 4].as_chunks::<4>().0)
{
target.copy_from_slice(&[255, 255, 255, source[1]]);
// Preserve readable output if the rasterizer returns a subpixel mask.
for row in 0..h {
for col in 0..w {
let i = ((row * w + col) * 4) as usize;
let a = image.data[i + 1];
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
}
}
}
+53 -92
View File
@@ -1,3 +1,5 @@
use std::num::NonZero;
use crate::{
UiData, UiRenderState,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
@@ -40,45 +42,22 @@ 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>,
}
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(2, &self.rsc_group, &[]);
for i in &self.active {
let layer = &self.layers[i];
if layer.instance.len() == 0 && layer.image_instance.len() == 0 {
if layer.instance.len() == 0 {
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);
}
}
}
}
pub fn update(
@@ -94,15 +73,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 +92,6 @@ impl UiRenderNode {
),
primitives,
primitive_group,
image_instance: ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"image instance",
),
image_tex_indices: Vec::new(),
}
});
if primitives.updated {
@@ -139,30 +104,17 @@ 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();
primitives.updated = false;
}
}
let masks_resized = if ui.masks.changed {
let mut changed = false;
changed |= self.textures.update(&mut ui.textures);
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.masks.update(device, queue, &ui.masks[..]);
changed = true;
}
if changed {
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks);
}
}
@@ -176,7 +128,12 @@ impl UiRenderNode {
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
}
pub fn new(device: &Device, queue: &Queue, config: &SurfaceConfiguration) -> Self {
pub fn new(
device: &Device,
queue: &Queue,
config: &SurfaceConfiguration,
limits: UiLimits,
) -> Self {
let shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("UI Shape Shader"),
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
@@ -209,8 +166,9 @@ impl UiRenderNode {
let uniform_group = Self::bind_group_0(device, &uniform_layout, &window_buffer);
let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &PrimitiveBuffers::BINDINGS.map(|binding| BindGroupLayoutEntry {
binding,
entries: &core::array::from_fn::<_, { PrimitiveBuffers::LEN }, _>(|i| {
BindGroupLayoutEntry {
binding: i as u32,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
@@ -218,6 +176,7 @@ impl UiRenderNode {
min_binding_size: None,
},
count: None,
}
}),
label: Some("primitive"),
});
@@ -229,7 +188,7 @@ impl UiRenderNode {
"ui masks",
);
let rsc_layout = Self::rsc_layout(device);
let rsc_layout = Self::rsc_layout(device, &limits);
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
@@ -319,12 +278,7 @@ 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 {
fn rsc_layout(device: &Device, limits: &UiLimits) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
@@ -332,30 +286,20 @@ impl UiRenderNode {
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2Array,
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: None,
count: Some(NonZero::new(limits.max_textures).unwrap()),
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: None,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: Some(NonZero::new(limits.max_samplers).unwrap()),
},
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: None,
},
BindGroupLayoutEntry {
binding: 3,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
@@ -368,8 +312,6 @@ impl UiRenderNode {
})
}
/// 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,
@@ -381,18 +323,14 @@ impl UiRenderNode {
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(tex_manager.array_view()),
resource: BindingResource::TextureViewArray(&tex_manager.views()),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::TextureView(tex_manager.null_view()),
resource: BindingResource::SamplerArray(&tex_manager.samplers()),
},
BindGroupEntry {
binding: 2,
resource: BindingResource::Sampler(tex_manager.sampler()),
},
BindGroupEntry {
binding: 3,
resource: masks.buffer.as_entire_binding(),
},
],
@@ -404,3 +342,26 @@ impl UiRenderNode {
self.textures.view_count()
}
}
pub struct UiLimits {
max_textures: u32,
max_samplers: u32,
}
impl Default for UiLimits {
fn default() -> Self {
Self {
max_textures: 100000,
max_samplers: 1000,
}
}
}
impl UiLimits {
pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 {
self.max_textures + self.max_samplers
}
pub fn max_binding_array_sampler_elements_per_shader_stage(&self) -> u32 {
self.max_samplers
}
}
+23 -154
View File
@@ -6,7 +6,6 @@ use crate::{
ArrBuf,
data::{MaskIdx, PrimitiveInstance},
},
util::Vec2,
};
use bytemuck::Pod;
use wgpu::*;
@@ -16,17 +15,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,21 +25,11 @@ impl Default for Primitives {
assoc: Default::default(),
data: Default::default(),
free: Vec::new(),
images: Default::default(),
image_assoc: Default::default(),
image_free: Vec::new(),
updated: true,
}
}
}
/// The `binding` tag `Painter` writes on an image instance. Distinct from any
/// `Primitive::BINDING` because images have no `PrimitiveData` entry to key
/// one from -- a bind group already selects the texture -- so this only ever
/// has to match the shader's `TEXTURE` constant and flag "this instance lives
/// in `Primitives::images`, not `Primitives::instances`" to the code below.
pub const IMAGE_BINDING: u32 = 1;
pub trait Primitive: Pod {
const BINDING: u32;
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>;
@@ -76,14 +54,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] {
[
$((<$ty>::BINDING, &self.$name.buffer),)*
@@ -167,104 +137,27 @@ impl Primitives {
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,
}
}
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);
self.free.push(h.inst_idx);
self.instances[h.inst_idx].mask_idx
}
}
pub fn data(&self) -> &PrimitiveData {
&self.data
@@ -276,21 +169,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,
}
@@ -316,6 +200,7 @@ impl PrimitiveHandle {
primitives!(
rects: RectPrimitive => 0,
textures: TexturePrimitive => 1,
glyphs: GlyphPrimitive => 2,
);
@@ -339,42 +224,26 @@ 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.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct TexturePrimitive {
pub view_idx: u32,
pub sampler_idx: u32,
}
#[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.
pub layer: u32,
pub uv_min: [f32; 2],
pub uv_max: [f32; 2],
pub view_idx: u32,
pub sampler_idx: 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 const IS_COLOR: u32 = 1;
}
pub struct PrimitiveVec<T> {
+16 -22
View File
@@ -1,7 +1,4 @@
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;
@@ -9,6 +6,8 @@ const GLYPH: u32 = 2u;
var<uniform> window: WindowUniform;
@group(1) @binding(RECT)
var<storage> rects: array<Rect>;
@group(1) @binding(TEXTURE)
var<storage> textures: array<TextureInfo>;
@group(1) @binding(GLYPH)
var<storage> glyphs: array<GlyphInfo>;
@@ -19,12 +18,16 @@ struct Rect {
inner_radius: f32,
}
struct TextureInfo {
view_idx: u32,
sampler_idx: u32,
}
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.
layer: u32,
view_idx: u32,
sampler_idx: u32,
color: u32,
flags: u32,
}
@@ -49,21 +52,11 @@ 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.
@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 views: binding_array<texture_2d<f32>>;
@group(2) @binding(1)
var image_texture: texture_2d<f32>;
var samplers: binding_array<sampler>;
@group(2) @binding(2)
var samp: sampler;
@group(2) @binding(3)
var<storage> masks: array<Mask>;
struct WindowUniform {
@@ -142,7 +135,7 @@ fn fs_main(
color = draw_rounded_rect(region, rects[i]);
}
case TEXTURE: {
color = draw_texture(region);
color = draw_texture(region, textures[i]);
}
case GLYPH: {
color = draw_glyph(region, glyphs[i]);
@@ -165,13 +158,14 @@ fn fs_main(
return color;
}
fn draw_texture(region: Region) -> vec4<f32> {
return textureSample(image_texture, samp, region.uv);
// TODO: this seems really inefficient (per frag indexing)?
fn draw_texture(region: Region, info: TextureInfo) -> vec4<f32> {
return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv);
}
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 texel = textureSample(views[g.view_idx], samplers[g.sampler_idx], uv);
if (g.flags & 1u) != 0u {
return texel;
}
+74 -342
View File
@@ -1,163 +1,78 @@
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.
pub struct GpuTextures {
device: Device,
queue: Queue,
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.
/// Parallel to `views`; patches require textures rather than views.
textures: Vec<Option<Texture>>,
views: Vec<TextureView>,
view_count: usize,
samplers: Vec<Sampler>,
null_view: TextureView,
no_views: Vec<TextureView>,
}
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 update(&mut self, textures: &mut Textures) -> bool {
let mut changed = false;
for update in textures.updates() {
match update {
TextureUpdate::Push(kind, image) => {
rebuild_main |= self.push(kind, image, rsc_layout, masks);
TextureUpdate::Push(image) => {
self.push(image);
changed = true;
}
TextureUpdate::Set(kind, i, image) => {
rebuild_main |= self.set(kind, i, image, rsc_layout, masks);
TextureUpdate::Set(i, image) => {
self.set(i, image);
changed = true;
}
// 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::SetFree => {}
TextureUpdate::Free(i) => self.free(i),
TextureUpdate::PushFree(_kind) => self.slots.push(Slot::Empty),
TextureUpdate::Patch(i, rect, image) => {
// Patching contents leaves the binding array unchanged.
self.patch(i, rect, image);
}
TextureUpdate::SetFree => {
self.view_count += 1;
changed = true;
}
rebuild_main
TextureUpdate::Free(i) => {
self.free(i);
changed = true;
}
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::PushFree => {
self.push_free();
changed = true;
}
}
}
changed
}
fn set(&mut self, i: u32, image: &DynamicImage) {
self.view_count += 1;
let (texture, view) = self.create(image);
self.textures[i as usize] = Some(texture);
self.views[i as usize] = view;
}
fn free(&mut self, i: u32) {
if let Some(slot) = self.slots.get_mut(i as usize) {
*slot = Slot::Empty;
self.view_count -= 1;
self.textures[i as usize] = None;
self.views[i as usize] = self.null_view.clone();
}
// A page's layer is not reclaimed here either -- see `Slot::Page`.
fn push(&mut self, image: &DynamicImage) {
self.view_count += 1;
let (texture, view) = self.create(image);
self.textures.push(Some(texture));
self.views.push(view);
}
fn push_free(&mut self) {
self.view_count += 1;
self.textures.push(None);
self.views.push(self.null_view.clone());
}
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
let Some(&Slot::Page(layer)) = self.slots.get(i as usize) else {
let Some(texture) = &self.textures[i as usize] else {
return;
};
if rect.width == 0 || rect.height == 0 {
@@ -169,12 +84,12 @@ impl GpuTextures {
.to_image();
self.queue.write_texture(
TexelCopyTextureInfo {
texture: &self.array_texture,
texture,
mip_level: 0,
origin: Origin3d {
x: rect.x,
y: rect.y,
z: layer,
z: 0,
},
aspect: TextureAspect::All,
},
@@ -192,105 +107,13 @@ impl GpuTextures {
);
}
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(
&self,
image: &DynamicImage,
rsc_layout: &BindGroupLayout,
masks: &ArrBuf<Mask>,
) -> ImageGpu {
let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions();
fn create(&self, image: &DynamicImage) -> (Texture, TextureView) {
let image = image.to_rgba8();
let (width, height) = image.dimensions();
let texture = self.device.create_texture_with_data(
&self.queue,
&TextureDescriptor {
label: Some("image"),
label: None,
size: Extent3d {
width,
height,
@@ -304,134 +127,43 @@ impl GpuTextures {
view_formats: &[],
},
wgt::TextureDataOrder::MipMajor,
rgba.as_bytes(),
image.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 }
}
/// 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(
device: &Device,
rsc_layout: &BindGroupLayout,
array_view: &TextureView,
image_view: &TextureView,
sampler: &Sampler,
masks: &ArrBuf<Mask>,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout: rsc_layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(array_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"),
})
}
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: &[],
})
(texture, view)
}
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),
..Default::default()
});
Self {
device: device.clone(),
queue: queue.clone(),
slots: Vec::new(),
array_texture,
array_view,
array_capacity,
page_count: 0,
sampler,
textures: Vec::new(),
views: Vec::new(),
samplers: vec![default_sampler(device)],
no_views: vec![null_view.clone()],
null_view,
view_count: 0,
}
}
pub fn array_view(&self) -> &TextureView {
&self.array_view
pub fn views(&self) -> Vec<&TextureView> {
if self.views.is_empty() {
&self.no_views
} else {
&self.views
}
.iter()
.by_ref()
.collect()
}
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 samplers(&self) -> Vec<&Sampler> {
self.samplers.iter().by_ref().collect()
}
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})"),
}
self.view_count
}
}
+2 -7
View File
@@ -21,18 +21,13 @@ 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.
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
let resized = self.len != data.len();
if resized {
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) {
if self.len != data.len() {
self.len = data.len();
self.buffer =
Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label);
}
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
resized
}
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64;
+18 -24
View File
@@ -77,31 +77,17 @@ 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.primitive_at(handle.primitive(), 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.primitive(handle.primitive());
}
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
.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);
self.primitive_at(handle.primitive(), region);
}
pub fn render_text(
@@ -115,6 +101,13 @@ impl<'a> Painter<'a> {
}
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
let flags_for = |is_color| {
if is_color {
GlyphPrimitive::IS_COLOR
} else {
0
}
};
for glyph in text.glyphs.iter() {
let mut region = origin;
region.x.end = region.x.start;
@@ -123,13 +116,14 @@ 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,
view_idx: glyph.entry.view_idx,
sampler_idx: glyph.entry.sampler_idx,
color: text.color,
flags: flags_for(glyph.entry.is_color),
},
region,
);
}
+11 -8
View File
@@ -1,4 +1,4 @@
use iris_core::{UiData, UiRenderNode, UiRenderState};
use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState};
use pollster::FutureExt;
use std::sync::Arc;
use wgpu::*;
@@ -83,15 +83,18 @@ 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.
let ui_limits = UiLimits::default();
let (device, queue) = adapter
.request_device(&DeviceDescriptor {
required_features: Features::TEXTURE_BINDING_ARRAY
| Features::PARTIALLY_BOUND_BINDING_ARRAY
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
required_limits: Limits {
max_binding_array_elements_per_shader_stage: ui_limits
.max_binding_array_elements_per_shader_stage(),
max_binding_array_sampler_elements_per_shader_stage: ui_limits
.max_binding_array_sampler_elements_per_shader_stage(),
max_buffer_size: 1 << 30,
..Default::default()
},
@@ -123,7 +126,7 @@ impl UiRenderer {
let encoder = Self::create_encoder(&device);
let ui = UiRenderNode::new(&device, &queue, &config);
let ui = UiRenderNode::new(&device, &queue, &config, ui_limits);
Self {
surface,
+26 -16
View File
@@ -111,7 +111,7 @@ impl<'a> TextEditCtx<'a> {
self.text.view.buf.layout()
}
fn clamp_selection_to_layout(&mut self) {
fn refresh(&mut self) {
if let Some(sel) = self.text.selection {
let layout = self.layout();
self.text.selection = Some(sel.refresh(layout));
@@ -119,8 +119,8 @@ impl<'a> TextEditCtx<'a> {
}
pub fn take(&mut self) -> String {
let text = std::mem::take(self.text.view.buf.edit());
self.text.selection = None;
let text = self.text.view.buf.text().to_string();
self.set("");
text
}
@@ -219,8 +219,9 @@ impl<'a> TextEditCtx<'a> {
if end == 0 {
return;
}
let start = {
let layout = self.layout();
let start = if word {
if word {
sel.focus().previous_logical_word(layout).index()
} else {
let Some(cluster) = sel.focus().logical_clusters(layout)[0] else {
@@ -235,6 +236,7 @@ impl<'a> TextEditCtx<'a> {
.next_back()
.map_or(range.start, |(start, _)| start)
}
}
};
self.delete_range(start, end);
}
@@ -250,8 +252,9 @@ impl<'a> TextEditCtx<'a> {
if start >= self.text.view.buf.text().len() {
return;
}
let end = {
let layout = self.layout();
let end = if word {
if word {
sel.focus().next_logical_word(layout).index()
} else {
let clusters = sel.focus().logical_clusters(layout);
@@ -259,11 +262,17 @@ impl<'a> TextEditCtx<'a> {
return;
};
cluster.text_range().end
}
};
self.delete_range(start, end);
}
fn delete_range(&mut self, start: usize, end: usize) {
let len = self.text.view.buf.text().len();
let (start, end) = (start.min(end).min(len), start.max(end).min(len));
if start == end {
return;
}
self.text.view.buf.edit().replace_range(start..end, "");
self.text.view.buf.changed = true;
self.set_caret(start);
@@ -285,31 +294,32 @@ impl<'a> TextEditCtx<'a> {
let prev_sel = self.text.selection;
let prev_hit = self.text.double_hit;
let outcome = {
let layout = self.layout();
let (selection, double_hit) = if drag {
let Some(selection) = prev_sel else {
return;
};
(selection.extend_to_point(layout, pos.x, pos.y), prev_hit)
if drag {
prev_sel.map(|sel| (Some(sel.extend_to_point(layout, pos.x, pos.y)), prev_hit))
} else {
let hit = Selection::from_point(layout, pos.x, pos.y);
let index = hit.focus().index();
// Successive clicks at one index select the word, then the line.
if recent && prev_hit == Some(index) {
(Selection::line_from_point(layout, pos.x, pos.y), None)
Some(if recent && prev_hit == Some(index) {
(Some(Selection::line_from_point(layout, pos.x, pos.y)), None)
} else if recent && prev_sel.map(|s| s.focus().index()) == Some(index) {
(
Selection::word_from_point(layout, pos.x, pos.y),
Some(Selection::word_from_point(layout, pos.x, pos.y)),
Some(index),
)
} else {
(hit, None)
(Some(hit), None)
})
}
};
self.text.selection = Some(selection);
if let Some((selection, double_hit)) = outcome {
self.text.selection = selection;
self.text.double_hit = double_hit;
}
}
pub fn deselect(&mut self) {
self.text.selection = None;
@@ -324,7 +334,7 @@ impl<'a> TextEditCtx<'a> {
if let Some((old, selection)) = self.text.history.pop() {
self.set(&old);
self.text.selection = selection;
self.clamp_selection_to_layout();
self.refresh();
}
} else if self.text.view.buf.text() != old.0 {
self.text.history.push(old);
+22 -19
View File
@@ -23,7 +23,7 @@ pub struct TextView {
}
impl TextView {
fn is_empty(&self) -> bool {
fn is_blank(&self) -> bool {
self.buf.is_empty()
}
@@ -52,25 +52,31 @@ impl TextView {
.align(self.align)
}
fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText {
fn render(&mut self, ctx: &mut SizeCtx) -> RenderedText {
let width = if self.attrs.wrap {
Some(ctx.px_size().x)
} else {
None
};
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
if width == self.width
&& let Some(tex) = &self.tex
&& !self.attrs.changed
&& !self.buf.changed
{
return tex.clone();
}
self.width = width;
self.tex = Some(ctx.draw_text(&mut self.buf, &self.attrs, width));
let tex = ctx.draw_text(&mut self.buf, &self.attrs, width);
self.tex = Some(tex.clone());
self.attrs.changed = false;
self.buf.changed = false;
}
self.tex.as_ref().unwrap()
tex
}
pub fn tex(&self) -> Option<&RenderedText> {
self.tex.as_ref()
}
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
if self.is_empty()
if self.is_blank()
&& let Some(hint) = &self.hint
{
ctx.width(hint)
@@ -79,7 +85,7 @@ impl TextView {
}
}
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if self.is_empty()
if self.is_blank()
&& let Some(hint) = &self.hint
{
ctx.height(hint)
@@ -88,19 +94,16 @@ impl TextView {
}
}
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
let align = self.align;
if self.is_empty() && self.hint.is_some() {
let region = self.render(&mut painter.size_ctx()).size.align(align);
if let Some(hint) = &self.hint {
painter.widget(hint);
}
return region;
}
let tex = self.render(&mut painter.size_ctx());
let region = tex.size.align(align);
let region = tex.size.align(self.align);
if self.is_blank()
&& let Some(hint) = &self.hint
{
painter.widget(hint);
} else {
let within = region.within(&painter.region());
painter.glyphs(tex, within);
painter.glyphs(&tex, within);
}
region
}