iris: add replaceable glyph atlas buckets

This commit is contained in:
iris committed 2026-09-12 18:10:55 -04:00
1 parent c97df72015
commit 90dffce514
12 files changed
+399 -89

No files matched your search

+37
View File
@@ -550,6 +550,43 @@ mod apply_tests {
assert!(!rsc.widgets().has_updates()); assert!(!rsc.widgets().has_updates());
} }
#[test]
fn replacing_a_font_only_clears_its_glyph_bucket() {
let mut rsc = TestRsc {
ui: Ui::default(),
events: EventManager::default(),
};
rsc.ui.resize((800.0, 600.0));
rsc.ui
.register_font_in("first-icons", "first", ICON_FONT)
.unwrap();
rsc.ui
.register_font_in("second-icons", "second", ICON_FONT)
.unwrap();
let first = wtext(icon::OPEN)
.family("first-icons")
.add_strong(&mut rsc)
.any();
let second = wtext(icon::OPEN)
.family("second-icons")
.add_strong(&mut rsc)
.any();
rsc.draw(&first);
rsc.draw(&second);
let glyphs_before = rsc.ui.text.borrow().atlas.glyph_count();
let pages_before = rsc.ui.text.borrow().atlas.page_count();
assert_eq!((glyphs_before, pages_before), (2, 2));
rsc.ui.replace_font("first-icons", ICON_FONT).unwrap();
assert_eq!(rsc.ui.text.borrow().atlas.glyph_count(), 1);
assert_eq!(rsc.ui.text.borrow().atlas.page_count(), 1);
rsc.draw(&first);
assert_eq!(rsc.ui.text.borrow().atlas.glyph_count(), 2);
assert_eq!(rsc.ui.text.borrow().atlas.page_count(), 2);
}
fn cost_of_one_delta(paragraphs: usize) -> (u64, u64) { fn cost_of_one_delta(paragraphs: usize) -> (u64, u64) {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: Ui::default(), ui: Ui::default(),
+7
View File
@@ -286,6 +286,13 @@ accepted duplication rather than threading a `Painter` into every input
handler for one field, the same tradeoff `AndroidRenderer::content_scale` handler for one field, the same tradeoff `AndroidRenderer::content_scale`
already makes for the Diagnostics page. already makes for the Diagnostics page.
Glyph masks are cached at four horizontal quarter-pixel phases. Their final
quad edges snap to physical pixels after retained move offsets are applied;
the CPU mask geometry uses the same calculation as the shader. In particular,
a fractional scroll offset therefore moves text and other primitives in whole
physical-pixel steps instead of resampling the atlas vertically with the
nearest sampler.
**What did not change**: `rel`/`rest` are unaffected (already **What did not change**: `rel`/`rest` are unaffected (already
resolution-independent, a fraction of the parent). `Span::gap` and resolution-independent, a fraction of the parent). `Span::gap` and
`Padding`'s four sides moved from bare `f32` to `Len` so `dp(...)` works `Padding`'s four sides moved from bare `f32` to `Len` so `dp(...)` works
+10 -3
View File
@@ -823,9 +823,16 @@ a text widget's `TextHandle` wraps the UI-local `RscHandle` into it. The same
generic arena owns texture and managed-paint lifetimes. Its `StrongRscId` and generic arena owns texture and managed-paint lifetimes. Its `StrongRscId` and
`WeakRscId` are sendable IDs rather than data pointers; clone and drop events `WeakRscId` are sendable IDs rather than data pointers; clone and drop events
cross one arena-owned standard channel, and reference counts remain in the cross one arena-owned standard channel, and reference counts remain in the
arena instead of allocating one atomic counter per resource. Registering arena instead of allocating one atomic counter per resource. The ordinary
another family invalidates the live text entries and dirties their active owner `register_font(family, data)` call needs no policy argument and uses the shared
widgets, so registration is also valid after the first shape. ai-app owns the default glyph-atlas bucket; `register_font_in` opts a reloadable or sparse font
into a named bucket. `replace_font` unregisters the old faces, releases only
that bucket's pages, and invalidates the live text entries so their active owner
widgets reshape automatically. Freed array layers are reused without moving
live pages. Glyph rasterisation retains four horizontal quarter-pixel phases,
while final primitive edges (including retained scroll moves) snap to physical
pixels; this favours low-DPI text stability and keeps vertical scrolling from
resampling atlas masks. ai-app owns the
`ai-app-icons` family, its Nerd Fonts subset, its codepoints, its license and `ai-app-icons` family, its Nerd Fonts subset, its codepoints, its license and
the script that rebuilds it; the CSS generic names `sans-serif` and `monospace` the script that rebuilds it; the CSS generic names `sans-serif` and `monospace`
continue to resolve through the platform. continue to resolve through the platform.
+5 -1
View File
@@ -18,7 +18,11 @@ Glyph atlas pages are layers of one `texture_2d_array`. `GpuTextures` doubles
the array when it runs out of layers, copies the old layers on the GPU, and the array when it runs out of layers, copies the old layers on the GPU, and
rebuilds every bind group that referenced the old view. Page numbers are rebuilds every bind group that referenced the old view. Page numbers are
assigned synchronously by `Textures::add_page` because glyph insertion needs assigned synchronously by `Textures::add_page` because glyph insertion needs
the layer before the renderer processes queued texture updates. the layer before the renderer processes queued texture updates. Atlas maps and
pages are grouped into named buckets: ordinary font registration uses one
shared default bucket, while callers can isolate fonts they expect to replace.
Replacing a font drops its bucket. The resource slots and array layers from
those pages are reused without compacting or changing any live page number.
Standalone images each own a bind group and are not placed in the glyph Standalone images each own a bind group and are not placed in the glyph
array. Each render layer keeps ordinary rect/glyph instances separately from array. Each render layer keeps ordinary rect/glyph instances separately from
+178 -18
View File
@@ -5,7 +5,7 @@ use crate::{
use parley::{ use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily, Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
fontique::{Blob, FontInfoOverride}, fontique::{Blob, Collection, CollectionOptions, FamilyId, FontInfoOverride, FontWidth},
}; };
use std::{ use std::{
cell::{Ref, RefCell, RefMut}, cell::{Ref, RefCell, RefMut},
@@ -35,6 +35,7 @@ pub struct FontDiagnostics {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum FontRegistrationError { pub enum FontRegistrationError {
AlreadyRegistered(String), AlreadyRegistered(String),
NotRegistered(String),
InvalidFont(String), InvalidFont(String),
} }
@@ -44,6 +45,9 @@ impl fmt::Display for FontRegistrationError {
Self::AlreadyRegistered(family) => { Self::AlreadyRegistered(family) => {
write!(f, "font data is already registered for {family:?}") write!(f, "font data is already registered for {family:?}")
} }
Self::NotRegistered(family) => {
write!(f, "no font data is registered for {family:?}")
}
Self::InvalidFont(family) => { Self::InvalidFont(family) => {
write!( write!(
f, f,
@@ -56,6 +60,23 @@ impl fmt::Display for FontRegistrationError {
impl std::error::Error for FontRegistrationError {} impl std::error::Error for FontRegistrationError {}
/// The bucket used by [`crate::Ui::register_font`] when none is specified.
pub const DEFAULT_GLYPH_BUCKET: &str = "default";
struct RegisteredFace {
family: FamilyId,
width: FontWidth,
style: FontStyle,
weight: FontWeight,
}
struct RegisteredFont {
private_name: String,
bucket: u64,
font_id: u64,
faces: Vec<RegisteredFace>,
}
pub struct TextData { pub struct TextData {
pub font_cx: FontContext, pub font_cx: FontContext,
pub layout_cx: LayoutContext<PaintId>, pub layout_cx: LayoutContext<PaintId>,
@@ -71,8 +92,11 @@ pub struct TextData {
/// truth would mean carrying a `Painter` (or output size) into every /// truth would mean carrying a `Painter` (or output size) into every
/// input handler for the sake of one field. /// input handler for the sake of one field.
pub density: f32, pub density: f32,
registered_families: HashMap<String, String>, registered_families: HashMap<String, RegisteredFont>,
glyph_buckets: HashMap<String, u64>,
font_buckets: HashMap<u64, u64>,
next_registered_family: u64, next_registered_family: u64,
next_glyph_bucket: u64,
} }
impl Default for TextData { impl Default for TextData {
@@ -86,7 +110,10 @@ impl Default for TextData {
atlas: GlyphAtlas::default(), atlas: GlyphAtlas::default(),
density: 1.0, density: 1.0,
registered_families: HashMap::new(), registered_families: HashMap::new(),
glyph_buckets: HashMap::new(),
font_buckets: HashMap::new(),
next_registered_family: 0, next_registered_family: 0,
next_glyph_bucket: 1,
} }
} }
} }
@@ -168,29 +195,145 @@ impl TextData {
&mut self, &mut self,
family: impl AsRef<str>, family: impl AsRef<str>,
data: impl AsRef<[u8]> + Send + Sync + 'static, data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), FontRegistrationError> {
self.register_font_in(family, DEFAULT_GLYPH_BUCKET, data)
}
pub(crate) fn register_font_in(
&mut self,
family: impl AsRef<str>,
bucket: impl AsRef<str>,
data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), FontRegistrationError> { ) -> Result<(), FontRegistrationError> {
let family = family.as_ref().to_owned(); let family = family.as_ref().to_owned();
if self.registered_families.contains_key(&family) { if self.registered_families.contains_key(&family) {
return Err(FontRegistrationError::AlreadyRegistered(family)); return Err(FontRegistrationError::AlreadyRegistered(family));
} }
// Parley selects fonts by family rather than by a face handle. Give
// application data a private family name so selecting it cannot find
// a system font that happens to carry the same embedded metadata.
let private_name = format!("__iris_registered_font_{}__", self.next_registered_family); let private_name = format!("__iris_registered_font_{}__", self.next_registered_family);
let blob = Blob::new(Arc::new(data));
Self::validate_font(&family, blob.clone())?;
let bucket = self.glyph_bucket(bucket.as_ref());
let registered = self.add_font(private_name, bucket, blob);
self.next_registered_family += 1;
self.font_buckets.insert(registered.font_id, bucket);
self.registered_families.insert(family, registered);
Ok(())
}
pub(crate) fn replace_font(
&mut self,
family: impl AsRef<str>,
data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), FontRegistrationError> {
let family = family.as_ref();
let Some(bucket) = self.registered_families.get(family).map(|font| font.bucket) else {
return Err(FontRegistrationError::NotRegistered(family.to_owned()));
};
self.replace_font_with(family, bucket, data)
}
pub(crate) fn replace_font_in(
&mut self,
family: impl AsRef<str>,
bucket: impl AsRef<str>,
data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), FontRegistrationError> {
let family = family.as_ref();
if !self.registered_families.contains_key(family) {
return Err(FontRegistrationError::NotRegistered(family.to_owned()));
}
let bucket = self.glyph_bucket(bucket.as_ref());
self.replace_font_with(family, bucket, data)
}
fn replace_font_with(
&mut self,
family: &str,
bucket: u64,
data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), FontRegistrationError> {
let blob = Blob::new(Arc::new(data));
Self::validate_font(family, blob.clone())?;
let old = self.registered_families.remove(family).unwrap();
for face in old.faces {
self.font_cx.collection.unregister_font(
face.family,
face.width,
face.style,
face.weight,
);
}
self.font_buckets.remove(&old.font_id);
self.atlas.clear_bucket(old.bucket);
let registered = self.add_font(old.private_name, bucket, blob);
self.font_buckets.insert(registered.font_id, bucket);
self.registered_families
.insert(family.to_owned(), registered);
Ok(())
}
fn validate_font(family: &str, blob: Blob<u8>) -> Result<(), FontRegistrationError> {
let mut collection = Collection::new(CollectionOptions {
shared: false,
system_fonts: false,
});
let fonts = collection.register_fonts(blob, None);
if fonts.is_empty() {
Err(FontRegistrationError::InvalidFont(family.to_owned()))
} else {
Ok(())
}
}
fn add_font(&mut self, private_name: String, bucket: u64, blob: Blob<u8>) -> RegisteredFont {
let font_id = blob.id();
let fonts = self.font_cx.collection.register_fonts( let fonts = self.font_cx.collection.register_fonts(
Blob::new(Arc::new(data)), blob,
Some(FontInfoOverride { Some(FontInfoOverride {
family_name: Some(&private_name), family_name: Some(&private_name),
..Default::default() ..Default::default()
}), }),
); );
if fonts.is_empty() { debug_assert!(!fonts.is_empty(), "validated font failed its second scan");
return Err(FontRegistrationError::InvalidFont(family)); let faces = fonts
.into_iter()
.flat_map(|(family, fonts)| {
fonts.into_iter().map(move |font| RegisteredFace {
family,
width: font.width(),
style: font.style(),
weight: font.weight(),
})
})
.collect();
RegisteredFont {
private_name,
bucket,
font_id,
faces,
} }
self.next_registered_family += 1; }
self.registered_families.insert(family, private_name);
Ok(()) fn glyph_bucket(&mut self, name: &str) -> u64 {
if name == DEFAULT_GLYPH_BUCKET {
return crate::render::DEFAULT_GLYPH_BUCKET_ID;
}
if let Some(bucket) = self.glyph_buckets.get(name) {
return *bucket;
}
let bucket = self.next_glyph_bucket;
self.next_glyph_bucket += 1;
self.glyph_buckets.insert(name.to_owned(), bucket);
bucket
}
fn bucket_for_font(&self, font: u64) -> u64 {
self.font_buckets
.get(&font)
.copied()
.unwrap_or(crate::render::DEFAULT_GLYPH_BUCKET_ID)
} }
pub(crate) fn is_font_registered(&self, family: impl AsRef<str>) -> bool { pub(crate) fn is_font_registered(&self, family: impl AsRef<str>) -> bool {
@@ -201,8 +344,8 @@ impl TextData {
/// layout builder holds `&mut self` -- a `String` per shaped registered /// layout builder holds `&mut self` -- a `String` per shaped registered
/// run, paid only when the layout is rebuilt. /// run, paid only when the layout is rebuilt.
pub fn resolve_family(&self, family: &str) -> String { pub fn resolve_family(&self, family: &str) -> String {
if let Some(name) = self.registered_families.get(family) { if let Some(font) = self.registered_families.get(family) {
return name.clone(); return font.private_name.clone();
} }
family.to_owned() family.to_owned()
} }
@@ -304,9 +447,10 @@ impl TextData {
}; };
let coords_hash = hash_coords(coords); let coords_hash = hash_coords(coords);
let font_id = font.data.id(); let font_id = font.data.id();
let bucket = self.bucket_for_font(font_id);
for glyph in run.positioned_glyphs() { for glyph in run.positioned_glyphs() {
let subpixel = ((glyph.x.fract() * 4.0).round() as i32).rem_euclid(4) as u8; let (x, subpixel) = horizontal_phase(glyph.x);
let key = GlyphKey { let key = GlyphKey {
font: font_id, font: font_id,
glyph: glyph.id, glyph: glyph.id,
@@ -314,7 +458,7 @@ impl TextData {
subpixel, subpixel,
coords: coords_hash, coords: coords_hash,
}; };
let entry = match self.atlas.get(&key) { let entry = match self.atlas.get(bucket, &key) {
Some(entry) => entry, Some(entry) => entry,
None => { None => {
let mut scaler = self let mut scaler = self
@@ -333,9 +477,9 @@ impl TextData {
.offset(Vector::new(subpixel as f32 / 4.0, 0.0)) .offset(Vector::new(subpixel as f32 / 4.0, 0.0))
.render(&mut scaler, glyph.id as u16); .render(&mut scaler, glyph.id as u16);
match image { match image {
Some(image) => self.atlas.insert(key, &image, textures), Some(image) => self.atlas.insert(bucket, key, &image, textures),
None => { None => {
self.atlas.insert_empty(key); self.atlas.insert_empty(bucket, key);
None None
} }
} }
@@ -345,7 +489,7 @@ impl TextData {
placed.push(PlacedGlyph { placed.push(PlacedGlyph {
entry, entry,
offset: Vec2::new( offset: Vec2::new(
glyph.x.floor() + entry.left as f32, x + entry.left as f32,
glyph.y.floor() - entry.top as f32, glyph.y.floor() - entry.top as f32,
), ),
paint: run_color.slot(), paint: run_color.slot(),
@@ -376,6 +520,11 @@ impl TextData {
} }
} }
fn horizontal_phase(x: f32) -> (f32, u8) {
let quarters = (x * 4.0).round() as i32;
(quarters.div_euclid(4) as f32, quarters.rem_euclid(4) as u8)
}
pub const SANS_SERIF: &str = "sans-serif"; pub const SANS_SERIF: &str = "sans-serif";
pub const SERIF: &str = "serif"; pub const SERIF: &str = "serif";
pub const MONOSPACE: &str = "monospace"; pub const MONOSPACE: &str = "monospace";
@@ -574,6 +723,10 @@ impl TextBuffer {
} }
fn invalidate(&mut self) { fn invalidate(&mut self) {
// A layout owns the font blobs it was shaped with. Drop it now so
// replacing a registered font does not retain the old bytes until an
// off-screen text resource happens to be shaped again.
self.layout = Layout::new();
self.shaped = None; self.shaped = None;
} }
} }
@@ -849,6 +1002,13 @@ impl TextHandle {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn a_horizontal_phase_carries_across_pixel_boundaries() {
assert_eq!(horizontal_phase(10.20), (10.0, 1));
assert_eq!(horizontal_phase(10.90), (11.0, 0));
assert_eq!(horizontal_phase(-0.20), (-1.0, 3));
}
#[test] #[test]
fn invalid_font_data_is_reported() { fn invalid_font_data_is_reported() {
let mut data = TextData::default(); let mut data = TextData::default();
+26 -24
View File
@@ -46,8 +46,10 @@ pub struct Textures {
/// drawing it and its slot is never recycled underneath one. /// drawing it and its slot is never recycled underneath one.
shared: HashMap<SharedTextureKey, TextureHandle>, shared: HashMap<SharedTextureKey, TextureHandle>,
/// Next layer to hand out to an atlas page. Page layers and resource slots /// Next layer to hand out to an atlas page. Page layers and resource slots
/// are never reused, even if an explicit atlas clear drops their handles. /// are separate identities: released page layers are reused without moving
/// any still-live page.
next_page_layer: u32, next_page_layer: u32,
free_page_layers: Vec<u32>,
updates: Vec<Update>, updates: Vec<Update>,
} }
@@ -87,6 +89,7 @@ impl Textures {
kinds: Vec::new(), kinds: Vec::new(),
shared: HashMap::new(), shared: HashMap::new(),
next_page_layer: 0, next_page_layer: 0,
free_page_layers: Vec::new(),
updates: Vec::new(), updates: Vec::new(),
} }
} }
@@ -95,16 +98,20 @@ impl Textures {
let image = image.into(); let image = image.into();
let size = image.dimensions().into(); let size = image.dimensions().into();
let kind = TextureKind::Image; let kind = TextureKind::Image;
self.push(kind, size, image, true) self.push(kind, size, image)
} }
pub fn add_page(&mut self, image: impl Into<DynamicImage>) -> TextureHandle { pub fn add_page(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
self.free();
let image = image.into(); let image = image.into();
let size = image.dimensions().into(); let size = image.dimensions().into();
let layer = self.free_page_layers.pop().unwrap_or_else(|| {
let layer = self.next_page_layer; let layer = self.next_page_layer;
self.next_page_layer += 1; self.next_page_layer += 1;
layer
});
let kind = TextureKind::Page { layer }; let kind = TextureKind::Page { layer };
self.push(kind, size, image, false) self.push(kind, size, image)
} }
pub fn handle(&self, id: StrongRscId<TextureRsc>) -> TextureHandle { pub fn handle(&self, id: StrongRscId<TextureRsc>) -> TextureHandle {
@@ -119,22 +126,10 @@ impl Textures {
Some(self.handle(id)) Some(self.handle(id))
} }
fn push( fn push(&mut self, kind: TextureKind, size: Vec2, image: DynamicImage) -> TextureHandle {
&mut self,
kind: TextureKind,
size: Vec2,
image: DynamicImage,
recycle: bool,
) -> TextureHandle {
self.free(); self.free();
let old_capacity = self.resources.borrow().capacity(); let old_capacity = self.resources.borrow().capacity();
let id = if recycle { let id = self.resources.borrow_mut().add(TextureRsc { kind, size });
self.resources.borrow_mut().add(TextureRsc { kind, size })
} else {
self.resources
.borrow_mut()
.add_unrecycled(TextureRsc { kind, size })
};
let i = id.slot(); let i = id.slot();
if (i as usize) < old_capacity { if (i as usize) < old_capacity {
self.images[i as usize] = Some(image); self.images[i as usize] = Some(image);
@@ -201,10 +196,14 @@ impl Textures {
pub fn free(&mut self) { pub fn free(&mut self) {
let updates = &mut self.updates; let updates = &mut self.updates;
let images = &mut self.images; let images = &mut self.images;
self.resources.borrow_mut().apply(|id, _| { let free_page_layers = &mut self.free_page_layers;
self.resources.borrow_mut().apply(|id, resource| {
let idx = id.slot(); let idx = id.slot();
images[idx as usize] = None; images[idx as usize] = None;
updates.push(Update::Free(idx)); updates.push(Update::Free(idx));
if let TextureKind::Page { layer } = resource.kind {
free_page_layers.push(layer);
}
}); });
} }
@@ -321,16 +320,19 @@ mod tests {
} }
#[test] #[test]
fn a_released_atlas_page_slot_is_not_reused_by_an_image() { fn a_released_atlas_page_layer_is_reused_without_moving_live_pages() {
let mut textures = Textures::new(); let mut textures = Textures::new();
let page = textures.add_page(image(4)); let first = textures.add_page(image(4));
let page_slot = page.rsc.id().slot(); let second = textures.add_page(image(4));
drop(page); let first_layer = first.layer();
let second_layer = second.layer();
drop(first);
textures.free(); textures.free();
let plain = textures.add(image(4)); let replacement = textures.add_page(image(4));
assert_ne!(plain.image_index(), page_slot); assert_eq!(replacement.layer(), first_layer);
assert_eq!(second.layer(), second_layer);
} }
#[test] #[test]
+54 -32
View File
@@ -6,6 +6,7 @@ use image::RgbaImage;
use swash::scale::image::{Content, Image}; use swash::scale::image::{Content, Image};
pub(crate) const PAGE: u32 = 1024; pub(crate) const PAGE: u32 = 1024;
pub(crate) const DEFAULT_GLYPH_BUCKET_ID: u64 = 0;
const PAD: u32 = 1; const PAD: u32 = 1;
@@ -42,40 +43,47 @@ struct Page {
} }
#[derive(Default)] #[derive(Default)]
pub struct GlyphAtlas { struct Bucket {
pages: Vec<Page>, pages: Vec<Page>,
generation: u64,
entries: HashMap<GlyphKey, Option<GlyphEntry>>, entries: HashMap<GlyphKey, Option<GlyphEntry>>,
} }
#[derive(Default)]
pub struct GlyphAtlas {
buckets: HashMap<u64, Bucket>,
generation: u64,
}
impl GlyphAtlas { impl GlyphAtlas {
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> { pub(crate) fn get(&self, bucket: u64, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
self.entries.get(key).copied() self.buckets.get(&bucket)?.entries.get(key).copied()
} }
/// Rasterised pixels in, a place in the atlas out. `None` means the glyph /// Rasterised pixels in, a place in the atlas out. `None` means the glyph
/// has no pixels, which is a normal answer rather than a failure. /// has no pixels, which is a normal answer rather than a failure.
pub fn insert( pub fn insert(
&mut self, &mut self,
bucket: u64,
key: GlyphKey, key: GlyphKey,
image: &Image, image: &Image,
textures: &mut Textures, textures: &mut Textures,
) -> Option<GlyphEntry> { ) -> Option<GlyphEntry> {
let bucket = self.buckets.entry(bucket).or_default();
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 {
self.entries.insert(key, None); bucket.entries.insert(key, None);
return None; return None;
} }
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE { if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
// A single glyph larger than a page. Refusing is better than // A single glyph larger than a page. Refusing is better than
// silently drawing a cropped one; the caller draws nothing. // silently drawing a cropped one; the caller draws nothing.
self.entries.insert(key, None); bucket.entries.insert(key, None);
return None; return None;
} }
let (page_idx, x, y) = self.allocate(w, h, textures); let (page_idx, x, y) = bucket.allocate(w, h, textures);
let page = &self.pages[page_idx]; let page = &bucket.pages[page_idx];
let img = textures.image_mut(&page.handle); let img = textures.image_mut(&page.handle);
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8"); let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
@@ -89,7 +97,7 @@ impl GlyphAtlas {
}; };
textures.patch(&page.handle, rect); textures.patch(&page.handle, rect);
let page = &self.pages[page_idx]; let page = &bucket.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: [x as f32 * scale, y as f32 * scale], uv_min: [x as f32 * scale, y as f32 * scale],
@@ -101,10 +109,46 @@ impl GlyphAtlas {
is_color: matches!(image.content, Content::Color), is_color: matches!(image.content, Content::Color),
layer: page.handle.layer(), layer: page.handle.layer(),
}; };
self.entries.insert(key, Some(entry)); bucket.entries.insert(key, Some(entry));
Some(entry) Some(entry)
} }
pub(crate) fn insert_empty(&mut self, bucket: u64, key: GlyphKey) {
self.buckets
.entry(bucket)
.or_default()
.entries
.insert(key, None);
}
pub(crate) fn clear_bucket(&mut self, bucket: u64) {
if self.buckets.remove(&bucket).is_some() {
self.generation += 1;
}
}
pub fn generation(&self) -> u64 {
self.generation
}
pub fn page_count(&self) -> usize {
self.buckets.values().map(|bucket| bucket.pages.len()).sum()
}
pub fn glyph_count(&self) -> usize {
self.buckets
.values()
.map(|bucket| bucket.entries.len())
.sum()
}
pub fn clear(&mut self) {
self.buckets.clear();
self.generation += 1;
}
}
impl Bucket {
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) { fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
let need_w = w + PAD; let need_w = w + PAD;
let need_h = h + PAD; let need_h = h + PAD;
@@ -130,28 +174,6 @@ impl GlyphAtlas {
}); });
(self.pages.len() - 1, PAD, PAD) (self.pages.len() - 1, PAD, PAD)
} }
pub fn insert_empty(&mut self, key: GlyphKey) {
self.entries.insert(key, None);
}
pub fn generation(&self) -> u64 {
self.generation
}
pub fn page_count(&self) -> usize {
self.pages.len()
}
pub fn glyph_count(&self) -> usize {
self.entries.len()
}
pub fn clear(&mut self) {
self.pages.clear();
self.entries.clear();
self.generation += 1;
}
} }
fn fits(page: &Page, need_w: u32, need_h: u32) -> bool { fn fits(page: &Page, need_w: u32, need_h: u32) -> bool {
+2 -2
View File
@@ -137,8 +137,8 @@ fn corners_of(inst: PrimitiveInstance) -> Corners {
let bot_right_abs = vec2(inst.x.end.abs, inst.y.end.abs); let bot_right_abs = vec2(inst.x.end.abs, inst.y.end.abs);
let move_delta = resolve_move(inst.move_idx); let move_delta = resolve_move(inst.move_idx);
return Corners( return Corners(
floor(top_left_rel * window.dim) + floor(top_left_abs) + move_delta, floor(top_left_rel * window.dim) + floor(top_left_abs + move_delta),
floor(bot_right_rel * window.dim) + floor(bot_right_abs) + move_delta, floor(bot_right_rel * window.dim) + floor(bot_right_abs + move_delta),
); );
} }
+9 -7
View File
@@ -12,8 +12,8 @@ const MIN_ARRAY_LAYERS: u32 = 2;
enum Slot { enum Slot {
Empty, Empty,
Image(ImageGpu), Image(ImageGpu),
/// The array layer a page occupies. Pages are never freed (see /// The array layer a live page occupies. Dropping the page empties this
/// `Textures::free`), so this is the only variant that outlives a `Free`. /// resource slot; its array layer may then be assigned to a new page.
Page(u32), Page(u32),
} }
@@ -41,7 +41,9 @@ pub struct GpuTextures {
array_texture: Texture, array_texture: Texture,
array_view: TextureView, array_view: TextureView,
array_capacity: u32, array_capacity: u32,
page_count: u32, /// One past the highest layer ever populated. Holes below this remain in
/// place when the array grows, while `Textures` can reuse their numbers.
layer_high_water: u32,
sampler: Sampler, sampler: Sampler,
/// Bound in the image slot of the main draw's bind group, which has /// Bound in the image slot of the main draw's bind group, which has
@@ -125,7 +127,7 @@ impl GpuTextures {
rebuilt = true; rebuilt = true;
} }
self.write_full_layer(layer, image); self.write_full_layer(layer, image);
self.page_count = self.page_count.max(layer + 1); self.layer_high_water = self.layer_high_water.max(layer + 1);
(Slot::Page(layer), rebuilt) (Slot::Page(layer), rebuilt)
} }
} }
@@ -205,7 +207,7 @@ impl GpuTextures {
self.pages_grown += 1; self.pages_grown += 1;
let new_capacity = self.array_capacity * 2; let new_capacity = self.array_capacity * 2;
let new_texture = Self::create_array_texture(&self.device, new_capacity); let new_texture = Self::create_array_texture(&self.device, new_capacity);
if self.page_count > 0 { if self.layer_high_water > 0 {
let mut encoder = self let mut encoder = self
.device .device
.create_command_encoder(&CommandEncoderDescriptor { .create_command_encoder(&CommandEncoderDescriptor {
@@ -227,7 +229,7 @@ impl GpuTextures {
Extent3d { Extent3d {
width: PAGE, width: PAGE,
height: PAGE, height: PAGE,
depth_or_array_layers: self.page_count, depth_or_array_layers: self.layer_high_water,
}, },
); );
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
@@ -368,7 +370,7 @@ impl GpuTextures {
array_texture, array_texture,
array_view, array_view,
array_capacity, array_capacity,
page_count: 0, layer_high_water: 0,
sampler, sampler,
null_view, null_view,
bind_group_creates: 0, bind_group_creates: 0,
+43 -1
View File
@@ -75,10 +75,52 @@ impl Ui {
&mut self, &mut self,
family: impl AsRef<str>, family: impl AsRef<str>,
data: impl AsRef<[u8]> + Send + Sync + 'static, data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), crate::FontRegistrationError> {
self.update_fonts(|text| text.register_font(family, data))
}
/// Register application-owned font data in a named glyph-atlas bucket.
/// Fonts registered without this method share the default bucket.
#[track_caller]
pub fn register_font_in(
&mut self,
family: impl AsRef<str>,
bucket: impl AsRef<str>,
data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), crate::FontRegistrationError> {
self.update_fonts(|text| text.register_font_in(family, bucket, data))
}
/// Replace an application's registered font while retaining its atlas
/// bucket. Text is reshaped and the bucket's old glyph pages are released.
#[track_caller]
pub fn replace_font(
&mut self,
family: impl AsRef<str>,
data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), crate::FontRegistrationError> {
self.update_fonts(|text| text.replace_font(family, data))
}
/// Replace an application's registered font and assign the replacement to
/// a named glyph-atlas bucket.
#[track_caller]
pub fn replace_font_in(
&mut self,
family: impl AsRef<str>,
bucket: impl AsRef<str>,
data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), crate::FontRegistrationError> {
self.update_fonts(|text| text.replace_font_in(family, bucket, data))
}
fn update_fonts(
&mut self,
update: impl FnOnce(&mut TextResources) -> Result<(), crate::FontRegistrationError>,
) -> Result<(), crate::FontRegistrationError> { ) -> Result<(), crate::FontRegistrationError> {
let owners = { let owners = {
let mut text = self.data.text.borrow_mut(); let mut text = self.data.text.borrow_mut();
text.register_font(family, data)?; update(&mut text)?;
text.invalidate_all() text.invalidate_all()
}; };
let active: Vec<WidgetId> = { let active: Vec<WidgetId> = {
+1 -1
View File
@@ -1006,7 +1006,7 @@ impl UiRenderState {
let inst = self.primitives.instance(slot); let inst = self.primitives.instance(slot);
let delta = self.resolve_move_chain(inst.move_idx, rsc); let delta = self.resolve_move_chain(inst.move_idx, rsc);
let size = self.output_size; let size = self.output_size;
let corner = |c: UiVec2| (c.get_rel() * size).floor() + c.get_abs().floor() + delta; let corner = |c: UiVec2| (c.get_rel() * size).floor() + (c.get_abs() + delta).floor();
PixelRegion { PixelRegion {
top_left: corner(inst.region.top_left()), top_left: corner(inst.region.top_left()),
bot_right: corner(inst.region.bot_right()), bot_right: corner(inst.region.bot_right()),
+27
View File
@@ -362,6 +362,33 @@ fn scrolling_moves_in_o1_without_a_redraw() {
assert_eq!(moves, 1, "the scrolled subtree should move in one write"); assert_eq!(moves, 1, "the scrolled subtree should move in one write");
} }
#[test]
fn fractional_scrolling_keeps_primitive_edges_on_physical_pixels() {
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, rects) = scrolled_rects(&mut rsc, 20);
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-37.25);
render.update(&root, &mut rsc);
let slot = render
.first_primitive(rects[2].id())
.expect("the scrolled rect has a primitive");
let corners = render.primitive_corners(slot, &rsc);
for edge in [
corners.top_left.x,
corners.top_left.y,
corners.bot_right.x,
corners.bot_right.y,
] {
assert_eq!(edge.fract(), 0.0, "fractional rendered edge: {corners:?}");
}
}
#[test] #[test]
fn hit_testing_follows_a_scrolled_widget() { fn hit_testing_follows_a_scrolled_widget() {
let mut rsc = TestRsc { ui: Ui::default() }; let mut rsc = TestRsc { ui: Ui::default() };