A placed glyph's offset is whole pixels by construction -- a floored pen position plus the entry's integer bearing -- and `Painter::glyphs` was converting it, and the entry's width and height, from `f32` on every frame that drew the glyph. It is a `PxVec2` now, converted once when the text is placed, and the size is two integer shifts. Measured with `perf stat -e instructions:u`, since the difference is smaller than this machine's clock: the `many` phase went from 2,013,099,594 instructions to 1,938,264,572 over 500 frames, 3.7% less. `scroll` and `repaint` are unchanged to within noise, which is right -- they do not redraw glyphs. Checked: fmt, clippy, 105 tests, the reorder fuzzer at 300 seeds, and `tabs`, `text` and `random` byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
248 lines
7.4 KiB
Rust
248 lines
7.4 KiB
Rust
use crate::{
|
|
PatchRect, PxVec2,
|
|
util::{HashMap, Vec2},
|
|
};
|
|
use image::RgbaImage;
|
|
use swash::scale::image::{Content, Image};
|
|
|
|
/// Side of one page, and so of every layer of `render::page`'s array texture.
|
|
pub(crate) const PAGE: u32 = 1024;
|
|
|
|
/// Transparent margin kept around every glyph, so that sampling one cannot
|
|
/// pick up its neighbour along a shared edge.
|
|
const PAD: u32 = 1;
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct GlyphKey {
|
|
pub font: u64,
|
|
pub glyph: u32,
|
|
/// Font size in 1/16 px, so sizes that round to the same pixels share a
|
|
/// raster instead of filling the atlas with near-duplicates.
|
|
pub size: u32,
|
|
/// Horizontal subpixel phase, in 1/4 px.
|
|
pub subpixel: u8,
|
|
/// Hash of the variation coordinates; a variable font at two weights is two
|
|
/// different sets of pixels from one glyph id.
|
|
pub coords: u64,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub struct GlyphEntry {
|
|
pub uv_min: Vec2,
|
|
pub uv_max: Vec2,
|
|
/// Offset from the glyph's pen position to the top-left of its pixels.
|
|
pub left: i32,
|
|
pub top: i32,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub is_colored: bool,
|
|
/// Which atlas array layer this glyph is on.
|
|
pub layer: u32,
|
|
}
|
|
|
|
impl GlyphEntry {
|
|
const IS_COLORED: u32 = 1;
|
|
|
|
pub(crate) fn flags(&self) -> u32 {
|
|
if self.is_colored { Self::IS_COLORED } else { 0 }
|
|
}
|
|
}
|
|
|
|
struct Page {
|
|
image: RgbaImage,
|
|
x: u32,
|
|
y: u32,
|
|
shelf_height: u32,
|
|
}
|
|
|
|
/// A rectangle of one page the renderer has not uploaded yet.
|
|
#[derive(Clone, Copy)]
|
|
pub struct PageUpload {
|
|
pub layer: u32,
|
|
pub rect: PatchRect,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct GlyphAtlas {
|
|
pages: Vec<Page>,
|
|
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
|
/// too, so it is not re-rasterised on every layout.
|
|
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
|
uploads: Vec<PageUpload>,
|
|
}
|
|
|
|
impl GlyphAtlas {
|
|
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
|
|
self.entries.get(key).copied()
|
|
}
|
|
|
|
pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option<GlyphEntry> {
|
|
let w = image.placement.width;
|
|
let h = image.placement.height;
|
|
if w == 0 || h == 0 {
|
|
log::warn!(
|
|
"glyph {} in font {} rasterized at {w}x{h}; skipping it",
|
|
key.glyph,
|
|
key.font,
|
|
);
|
|
self.entries.insert(key, None);
|
|
return None;
|
|
}
|
|
if w > PAGE - PAD * 2 || h > PAGE - PAD * 2 {
|
|
log::warn!(
|
|
"glyph {} in font {} rasterized at {w}x{h}, too large for the {PAGE}x{PAGE} atlas; skipping it",
|
|
key.glyph,
|
|
key.font,
|
|
);
|
|
self.entries.insert(key, None);
|
|
return None;
|
|
}
|
|
|
|
let upload = self.allocate(w, h);
|
|
let PatchRect { x, y, .. } = upload.rect;
|
|
write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y);
|
|
self.uploads.push(upload);
|
|
|
|
let scale = 1.0 / PAGE as f32;
|
|
let entry = GlyphEntry {
|
|
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
|
|
uv_max: Vec2::new((x + w) as f32 * scale, (y + h) as f32 * scale),
|
|
left: image.placement.left,
|
|
top: image.placement.top,
|
|
width: w,
|
|
height: h,
|
|
is_colored: matches!(image.content, Content::Color),
|
|
layer: upload.layer,
|
|
};
|
|
self.entries.insert(key, Some(entry));
|
|
Some(entry)
|
|
}
|
|
|
|
/// Reserves room for a `w` by `h` glyph, adding a page if none has it.
|
|
fn allocate(&mut self, w: u32, h: u32) -> PageUpload {
|
|
let rect = |x, y| PatchRect {
|
|
x,
|
|
y,
|
|
width: w,
|
|
height: h,
|
|
};
|
|
if let Some((i, (x, y))) = self
|
|
.pages
|
|
.iter_mut()
|
|
.enumerate()
|
|
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
|
|
{
|
|
return PageUpload {
|
|
layer: i as u32,
|
|
rect: rect(x, y),
|
|
};
|
|
}
|
|
|
|
self.pages.push(Page {
|
|
image: RgbaImage::new(PAGE, PAGE),
|
|
x: PAD + w + PAD,
|
|
y: PAD,
|
|
shelf_height: h + PAD,
|
|
});
|
|
PageUpload {
|
|
layer: self.pages.len() as u32 - 1,
|
|
rect: rect(PAD, PAD),
|
|
}
|
|
}
|
|
|
|
/// Drains what has been written since the last call, for the renderer to
|
|
/// upload. A new page needs nothing more: wgpu leaves the rest of a fresh
|
|
/// layer transparent, which is what an atlas wants.
|
|
pub fn uploads(&mut self) -> impl Iterator<Item = (PageUpload, &RgbaImage)> {
|
|
let pages = &self.pages;
|
|
self.uploads
|
|
.drain(..)
|
|
.map(|upload| (upload, &pages[upload.layer as usize].image))
|
|
}
|
|
|
|
pub fn insert_empty(&mut self, key: GlyphKey) {
|
|
self.entries.insert(key, None);
|
|
}
|
|
|
|
pub fn page_count(&self) -> u32 {
|
|
self.pages.len() as u32
|
|
}
|
|
|
|
pub fn glyph_count(&self) -> usize {
|
|
self.entries.len()
|
|
}
|
|
}
|
|
|
|
impl Page {
|
|
fn allocate(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
|
|
let need_w = w + PAD;
|
|
let need_h = h + PAD;
|
|
if self.x + need_w > PAGE {
|
|
if need_w + PAD > PAGE || self.y + self.shelf_height + need_h > PAGE {
|
|
return None;
|
|
}
|
|
self.y += self.shelf_height;
|
|
self.x = PAD;
|
|
self.shelf_height = 0;
|
|
} else if self.y + need_h > PAGE {
|
|
return None;
|
|
}
|
|
|
|
let position = (self.x, self.y);
|
|
self.x += need_w;
|
|
self.shelf_height = self.shelf_height.max(need_h);
|
|
Some(position)
|
|
}
|
|
}
|
|
|
|
/// Mask glyphs keep coverage in alpha so their raster can be tinted at draw time.
|
|
fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
|
let width = image.placement.width as usize;
|
|
let height = image.placement.height as usize;
|
|
let page_stride = page.width() as usize * 4;
|
|
let x = x as usize * 4;
|
|
let y = y as usize;
|
|
let page = page.as_mut();
|
|
|
|
for row in 0..height {
|
|
let start = (y + row) * page_stride + x;
|
|
let target = &mut page[start..start + width * 4];
|
|
match image.content {
|
|
Content::Color => {
|
|
let start = row * width * 4;
|
|
target.copy_from_slice(&image.data[start..start + width * 4]);
|
|
}
|
|
Content::Mask => {
|
|
let start = row * width;
|
|
for (target, &alpha) in target
|
|
.as_chunks_mut::<4>()
|
|
.0
|
|
.iter_mut()
|
|
.zip(&image.data[start..start + width])
|
|
{
|
|
target.copy_from_slice(&[255, 255, 255, alpha]);
|
|
}
|
|
}
|
|
Content::SubpixelMask => {
|
|
let start = row * width * 4;
|
|
for (target, source) in target
|
|
.as_chunks_mut::<4>()
|
|
.0
|
|
.iter_mut()
|
|
.zip(image.data[start..start + width * 4].as_chunks::<4>().0)
|
|
{
|
|
target.copy_from_slice(&[255, 255, 255, source[1]]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub struct PlacedGlyph {
|
|
pub entry: GlyphEntry,
|
|
/// Whole pixels from the origin of the text to this glyph's top-left,
|
|
/// on the grid once here rather than on every frame that draws it.
|
|
pub offset: PxVec2,
|
|
}
|