iris: add replaceable glyph atlas buckets
This commit is contained in:
1 parent
c97df72015
commit
90dffce514
12 files changed
+401
-91
No files matched your search
@@ -550,6 +550,43 @@ mod apply_tests {
|
||||
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) {
|
||||
let mut rsc = TestRsc {
|
||||
ui: Ui::default(),
|
||||
|
||||
@@ -286,6 +286,13 @@ accepted duplication rather than threading a `Painter` into every input
|
||||
handler for one field, the same tradeoff `AndroidRenderer::content_scale`
|
||||
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
|
||||
resolution-independent, a fraction of the parent). `Span::gap` and
|
||||
`Padding`'s four sides moved from bare `f32` to `Len` so `dp(...)` works
|
||||
|
||||
+10
-3
@@ -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
|
||||
`WeakRscId` are sendable IDs rather than data pointers; clone and drop events
|
||||
cross one arena-owned standard channel, and reference counts remain in the
|
||||
arena instead of allocating one atomic counter per resource. Registering
|
||||
another family invalidates the live text entries and dirties their active owner
|
||||
widgets, so registration is also valid after the first shape. ai-app owns the
|
||||
arena instead of allocating one atomic counter per resource. The ordinary
|
||||
`register_font(family, data)` call needs no policy argument and uses the shared
|
||||
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
|
||||
the script that rebuilds it; the CSS generic names `sans-serif` and `monospace`
|
||||
continue to resolve through the platform.
|
||||
|
||||
+5
-1
@@ -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
|
||||
rebuilds every bind group that referenced the old view. Page numbers are
|
||||
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
|
||||
array. Each render layer keeps ordinary rect/glyph instances separately from
|
||||
|
||||
+178
-18
@@ -5,7 +5,7 @@ use crate::{
|
||||
use parley::{
|
||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
|
||||
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||
fontique::{Blob, FontInfoOverride},
|
||||
fontique::{Blob, Collection, CollectionOptions, FamilyId, FontInfoOverride, FontWidth},
|
||||
};
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
@@ -35,6 +35,7 @@ pub struct FontDiagnostics {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FontRegistrationError {
|
||||
AlreadyRegistered(String),
|
||||
NotRegistered(String),
|
||||
InvalidFont(String),
|
||||
}
|
||||
|
||||
@@ -44,6 +45,9 @@ impl fmt::Display for FontRegistrationError {
|
||||
Self::AlreadyRegistered(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) => {
|
||||
write!(
|
||||
f,
|
||||
@@ -56,6 +60,23 @@ impl fmt::Display 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 font_cx: FontContext,
|
||||
pub layout_cx: LayoutContext<PaintId>,
|
||||
@@ -71,8 +92,11 @@ pub struct TextData {
|
||||
/// truth would mean carrying a `Painter` (or output size) into every
|
||||
/// input handler for the sake of one field.
|
||||
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_glyph_bucket: u64,
|
||||
}
|
||||
|
||||
impl Default for TextData {
|
||||
@@ -86,7 +110,10 @@ impl Default for TextData {
|
||||
atlas: GlyphAtlas::default(),
|
||||
density: 1.0,
|
||||
registered_families: HashMap::new(),
|
||||
glyph_buckets: HashMap::new(),
|
||||
font_buckets: HashMap::new(),
|
||||
next_registered_family: 0,
|
||||
next_glyph_bucket: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,29 +195,145 @@ impl TextData {
|
||||
&mut self,
|
||||
family: impl AsRef<str>,
|
||||
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> {
|
||||
let family = family.as_ref().to_owned();
|
||||
if self.registered_families.contains_key(&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 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(
|
||||
Blob::new(Arc::new(data)),
|
||||
blob,
|
||||
Some(FontInfoOverride {
|
||||
family_name: Some(&private_name),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
if fonts.is_empty() {
|
||||
return Err(FontRegistrationError::InvalidFont(family));
|
||||
debug_assert!(!fonts.is_empty(), "validated font failed its second scan");
|
||||
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 {
|
||||
@@ -201,8 +344,8 @@ impl TextData {
|
||||
/// layout builder holds `&mut self` -- a `String` per shaped registered
|
||||
/// run, paid only when the layout is rebuilt.
|
||||
pub fn resolve_family(&self, family: &str) -> String {
|
||||
if let Some(name) = self.registered_families.get(family) {
|
||||
return name.clone();
|
||||
if let Some(font) = self.registered_families.get(family) {
|
||||
return font.private_name.clone();
|
||||
}
|
||||
family.to_owned()
|
||||
}
|
||||
@@ -304,9 +447,10 @@ impl TextData {
|
||||
};
|
||||
let coords_hash = hash_coords(coords);
|
||||
let font_id = font.data.id();
|
||||
let bucket = self.bucket_for_font(font_id);
|
||||
|
||||
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 {
|
||||
font: font_id,
|
||||
glyph: glyph.id,
|
||||
@@ -314,7 +458,7 @@ impl TextData {
|
||||
subpixel,
|
||||
coords: coords_hash,
|
||||
};
|
||||
let entry = match self.atlas.get(&key) {
|
||||
let entry = match self.atlas.get(bucket, &key) {
|
||||
Some(entry) => entry,
|
||||
None => {
|
||||
let mut scaler = self
|
||||
@@ -333,9 +477,9 @@ impl TextData {
|
||||
.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),
|
||||
Some(image) => self.atlas.insert(bucket, key, &image, textures),
|
||||
None => {
|
||||
self.atlas.insert_empty(key);
|
||||
self.atlas.insert_empty(bucket, key);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -345,7 +489,7 @@ impl TextData {
|
||||
placed.push(PlacedGlyph {
|
||||
entry,
|
||||
offset: Vec2::new(
|
||||
glyph.x.floor() + entry.left as f32,
|
||||
x + entry.left as f32,
|
||||
glyph.y.floor() - entry.top as f32,
|
||||
),
|
||||
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 SERIF: &str = "serif";
|
||||
pub const MONOSPACE: &str = "monospace";
|
||||
@@ -574,6 +723,10 @@ impl TextBuffer {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -849,6 +1002,13 @@ impl TextHandle {
|
||||
mod tests {
|
||||
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]
|
||||
fn invalid_font_data_is_reported() {
|
||||
let mut data = TextData::default();
|
||||
|
||||
@@ -46,8 +46,10 @@ pub struct Textures {
|
||||
/// drawing it and its slot is never recycled underneath one.
|
||||
shared: HashMap<SharedTextureKey, TextureHandle>,
|
||||
/// 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,
|
||||
free_page_layers: Vec<u32>,
|
||||
updates: Vec<Update>,
|
||||
}
|
||||
|
||||
@@ -87,6 +89,7 @@ impl Textures {
|
||||
kinds: Vec::new(),
|
||||
shared: HashMap::new(),
|
||||
next_page_layer: 0,
|
||||
free_page_layers: Vec::new(),
|
||||
updates: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -95,16 +98,20 @@ impl Textures {
|
||||
let image = image.into();
|
||||
let size = image.dimensions().into();
|
||||
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 {
|
||||
self.free();
|
||||
let image = image.into();
|
||||
let size = image.dimensions().into();
|
||||
let layer = self.next_page_layer;
|
||||
self.next_page_layer += 1;
|
||||
let layer = self.free_page_layers.pop().unwrap_or_else(|| {
|
||||
let layer = self.next_page_layer;
|
||||
self.next_page_layer += 1;
|
||||
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 {
|
||||
@@ -119,22 +126,10 @@ impl Textures {
|
||||
Some(self.handle(id))
|
||||
}
|
||||
|
||||
fn push(
|
||||
&mut self,
|
||||
kind: TextureKind,
|
||||
size: Vec2,
|
||||
image: DynamicImage,
|
||||
recycle: bool,
|
||||
) -> TextureHandle {
|
||||
fn push(&mut self, kind: TextureKind, size: Vec2, image: DynamicImage) -> TextureHandle {
|
||||
self.free();
|
||||
let old_capacity = self.resources.borrow().capacity();
|
||||
let id = if recycle {
|
||||
self.resources.borrow_mut().add(TextureRsc { kind, size })
|
||||
} else {
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
.add_unrecycled(TextureRsc { kind, size })
|
||||
};
|
||||
let id = self.resources.borrow_mut().add(TextureRsc { kind, size });
|
||||
let i = id.slot();
|
||||
if (i as usize) < old_capacity {
|
||||
self.images[i as usize] = Some(image);
|
||||
@@ -201,10 +196,14 @@ impl Textures {
|
||||
pub fn free(&mut self) {
|
||||
let updates = &mut self.updates;
|
||||
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();
|
||||
images[idx as usize] = None;
|
||||
updates.push(Update::Free(idx));
|
||||
if let TextureKind::Page { layer } = resource.kind {
|
||||
free_page_layers.push(layer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -321,16 +320,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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 page = textures.add_page(image(4));
|
||||
let page_slot = page.rsc.id().slot();
|
||||
drop(page);
|
||||
let first = textures.add_page(image(4));
|
||||
let second = textures.add_page(image(4));
|
||||
let first_layer = first.layer();
|
||||
let second_layer = second.layer();
|
||||
drop(first);
|
||||
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]
|
||||
|
||||
@@ -6,6 +6,7 @@ use image::RgbaImage;
|
||||
use swash::scale::image::{Content, Image};
|
||||
|
||||
pub(crate) const PAGE: u32 = 1024;
|
||||
pub(crate) const DEFAULT_GLYPH_BUCKET_ID: u64 = 0;
|
||||
|
||||
const PAD: u32 = 1;
|
||||
|
||||
@@ -42,40 +43,47 @@ struct Page {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GlyphAtlas {
|
||||
struct Bucket {
|
||||
pages: Vec<Page>,
|
||||
generation: u64,
|
||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GlyphAtlas {
|
||||
buckets: HashMap<u64, Bucket>,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
impl GlyphAtlas {
|
||||
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
|
||||
self.entries.get(key).copied()
|
||||
pub(crate) fn get(&self, bucket: u64, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
|
||||
self.buckets.get(&bucket)?.entries.get(key).copied()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
bucket: u64,
|
||||
key: GlyphKey,
|
||||
image: &Image,
|
||||
textures: &mut Textures,
|
||||
) -> Option<GlyphEntry> {
|
||||
let bucket = self.buckets.entry(bucket).or_default();
|
||||
let w = image.placement.width;
|
||||
let h = image.placement.height;
|
||||
if w == 0 || h == 0 {
|
||||
self.entries.insert(key, None);
|
||||
bucket.entries.insert(key, None);
|
||||
return None;
|
||||
}
|
||||
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
|
||||
// A single glyph larger than a page. Refusing is better than
|
||||
// silently drawing a cropped one; the caller draws nothing.
|
||||
self.entries.insert(key, None);
|
||||
bucket.entries.insert(key, None);
|
||||
return None;
|
||||
}
|
||||
|
||||
let (page_idx, x, y) = self.allocate(w, h, textures);
|
||||
let page = &self.pages[page_idx];
|
||||
let (page_idx, x, y) = bucket.allocate(w, h, textures);
|
||||
let page = &bucket.pages[page_idx];
|
||||
|
||||
let img = textures.image_mut(&page.handle);
|
||||
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
|
||||
@@ -89,7 +97,7 @@ impl GlyphAtlas {
|
||||
};
|
||||
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 entry = GlyphEntry {
|
||||
uv_min: [x as f32 * scale, y as f32 * scale],
|
||||
@@ -101,10 +109,46 @@ impl GlyphAtlas {
|
||||
is_color: matches!(image.content, Content::Color),
|
||||
layer: page.handle.layer(),
|
||||
};
|
||||
self.entries.insert(key, Some(entry));
|
||||
bucket.entries.insert(key, 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) {
|
||||
let need_w = w + PAD;
|
||||
let need_h = h + PAD;
|
||||
@@ -130,28 +174,6 @@ impl GlyphAtlas {
|
||||
});
|
||||
(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 {
|
||||
|
||||
@@ -137,8 +137,8 @@ fn corners_of(inst: PrimitiveInstance) -> Corners {
|
||||
let bot_right_abs = vec2(inst.x.end.abs, inst.y.end.abs);
|
||||
let move_delta = resolve_move(inst.move_idx);
|
||||
return Corners(
|
||||
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(top_left_rel * window.dim) + floor(top_left_abs + move_delta),
|
||||
floor(bot_right_rel * window.dim) + floor(bot_right_abs + move_delta),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ const MIN_ARRAY_LAYERS: u32 = 2;
|
||||
enum Slot {
|
||||
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`.
|
||||
/// The array layer a live page occupies. Dropping the page empties this
|
||||
/// resource slot; its array layer may then be assigned to a new page.
|
||||
Page(u32),
|
||||
}
|
||||
|
||||
@@ -41,7 +41,9 @@ pub struct GpuTextures {
|
||||
array_texture: Texture,
|
||||
array_view: TextureView,
|
||||
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,
|
||||
/// Bound in the image slot of the main draw's bind group, which has
|
||||
@@ -125,7 +127,7 @@ impl GpuTextures {
|
||||
rebuilt = true;
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -205,7 +207,7 @@ impl GpuTextures {
|
||||
self.pages_grown += 1;
|
||||
let new_capacity = self.array_capacity * 2;
|
||||
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
|
||||
.device
|
||||
.create_command_encoder(&CommandEncoderDescriptor {
|
||||
@@ -227,7 +229,7 @@ impl GpuTextures {
|
||||
Extent3d {
|
||||
width: 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()));
|
||||
@@ -368,7 +370,7 @@ impl GpuTextures {
|
||||
array_texture,
|
||||
array_view,
|
||||
array_capacity,
|
||||
page_count: 0,
|
||||
layer_high_water: 0,
|
||||
sampler,
|
||||
null_view,
|
||||
bind_group_creates: 0,
|
||||
|
||||
+43
-1
@@ -75,10 +75,52 @@ impl Ui {
|
||||
&mut self,
|
||||
family: impl AsRef<str>,
|
||||
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> {
|
||||
let owners = {
|
||||
let mut text = self.data.text.borrow_mut();
|
||||
text.register_font(family, data)?;
|
||||
update(&mut text)?;
|
||||
text.invalidate_all()
|
||||
};
|
||||
let active: Vec<WidgetId> = {
|
||||
|
||||
@@ -1006,7 +1006,7 @@ impl UiRenderState {
|
||||
let inst = self.primitives.instance(slot);
|
||||
let delta = self.resolve_move_chain(inst.move_idx, rsc);
|
||||
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 {
|
||||
top_left: corner(inst.region.top_left()),
|
||||
bot_right: corner(inst.region.bot_right()),
|
||||
|
||||
@@ -362,6 +362,33 @@ fn scrolling_moves_in_o1_without_a_redraw() {
|
||||
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]
|
||||
fn hit_testing_follows_a_scrolled_widget() {
|
||||
let mut rsc = TestRsc { ui: Ui::default() };
|
||||
|
||||
Reference in new issue
Block a user