iris: add replaceable glyph atlas buckets

This commit is contained in:
iris committed 2026-09-12 18:10:55 -04:00
1 parent 1be6cc2248
commit 2b9d8c49a0
8 files changed
+342 -87

No files matched your search

+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();
+28 -26
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.next_page_layer; let layer = self.free_page_layers.pop().unwrap_or_else(|| {
self.next_page_layer += 1; let layer = self.next_page_layer;
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() };