Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore glyph hinting support #544

Merged
merged 1 commit into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ clippy.semicolon_if_nothing_returned = "warn"
[workspace.dependencies]
vello_encoding = { version = "0.1.0", path = "crates/encoding" }
bytemuck = { version = "1.15.0", features = ["derive"] }
skrifa = "0.16.0"
skrifa = "0.19.0"
peniko = "0.1.0"
futures-intrusive = "0.5.0"
raw-window-handle = "0.6.0"
Expand Down
130 changes: 125 additions & 5 deletions crates/encoding/src/glyph_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use super::{Encoding, StreamOffsets};

use peniko::kurbo::{BezPath, Shape};
use peniko::{Fill, Style};
use skrifa::instance::NormalizedCoord;
use skrifa::outline::OutlinePen;
use skrifa::instance::{NormalizedCoord, Size};
use skrifa::outline::{HintingInstance, HintingMode, LcdLayout, OutlineGlyphFormat, OutlinePen};
use skrifa::{GlyphId, OutlineGlyphCollection};

#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)]
Expand All @@ -24,12 +24,14 @@ pub struct GlyphKey {
pub struct GlyphCache {
pub encoding: Encoding,
glyphs: HashMap<GlyphKey, CachedRange>,
hinting: HintCache,
}

impl GlyphCache {
pub fn clear(&mut self) {
self.encoding.reset();
self.glyphs.clear();
// No need to clear the hinting cache
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Is it because it's already bounded in size?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. In reality, none of this is retained right now so it’s mostly irrelevant. This module will need some substantial rework when we do rasterized glyph caching.

}

pub fn get_or_insert(
Expand All @@ -43,6 +45,7 @@ impl GlyphCache {
let size = skrifa::instance::Size::new(font_size);
let is_var = !coords.is_empty();
let encoding_cache = &mut self.encoding;
let hinting_cache = &mut self.hinting;
let mut encode_glyph = || {
let start = encoding_cache.stream_offsets();
let fill = match style {
Expand All @@ -55,9 +58,18 @@ impl GlyphCache {
encoding_cache.encode_fill_style(fill);
let mut path = encoding_cache.encode_path(true);
let outline = outlines.get(GlyphId::new(key.glyph_id as u16))?;
// FIXME: Re-add hinting when skrifa supports it
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥

// Tracking issue <https://github.com/googlefonts/fontations/issues/620>
let draw_settings = skrifa::outline::DrawSettings::unhinted(size, coords);
use skrifa::outline::DrawSettings;
let draw_settings = if key.hint {
if let Some(hint_instance) =
hinting_cache.get(&HintKey::new(outlines, &key, font_size, coords))
{
DrawSettings::hinted(hint_instance, false)
} else {
DrawSettings::unhinted(size, coords)
}
} else {
DrawSettings::unhinted(size, coords)
};
match style {
Style::Fill(_) => {
outline.draw(draw_settings, &mut path).ok()?;
Expand Down Expand Up @@ -145,3 +157,111 @@ impl OutlinePen for BezPathPen {
self.0.close_path();
}
}

/// We keep this small to enable a simple LRU cache with a linear
/// search. Regenerating hinting data is low to medium cost so it's fine
/// to redo it occassionally.
const MAX_CACHED_HINT_INSTANCES: usize = 8;

pub struct HintKey<'a> {
font_id: u64,
font_index: u32,
outlines: &'a OutlineGlyphCollection<'a>,
size: Size,
coords: &'a [NormalizedCoord],
}

impl<'a> HintKey<'a> {
fn new(
outlines: &'a OutlineGlyphCollection<'a>,
glyph_key: &GlyphKey,
size: f32,
coords: &'a [NormalizedCoord],
) -> Self {
Self {
font_id: glyph_key.font_id,
font_index: glyph_key.font_index,
outlines,
size: Size::new(size),
coords,
}
}

fn instance(&self) -> Option<HintingInstance> {
HintingInstance::new(self.outlines, self.size, self.coords, HINTING_MODE).ok()
}
}

const HINTING_MODE: HintingMode = HintingMode::Smooth {
lcd_subpixel: Some(LcdLayout::Horizontal),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this will need to be wired through at some point?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s an open question. The skrifa hinter supports a lot of modes because we have a requirement of matching FreeType. There’s an argument to be made that vello should always just use a modern hinting mode but that’s open to discussion. Ultimately, most fonts these days have hints generated by ttfautohint which ignores these things. Only the MS ClearType fonts really take advantage of them and we can potentially choose a sane default for those.

preserve_linear_metrics: true,
};

#[derive(Default)]
struct HintCache {
// Split caches for glyf/cff because the instance type can reuse
// internal memory when reconfigured for the same format.
glyf_entries: Vec<HintEntry>,
cff_entries: Vec<HintEntry>,
serial: u64,
}

impl HintCache {
fn get(&mut self, key: &HintKey) -> Option<&HintingInstance> {
let entries = match key.outlines.format()? {
OutlineGlyphFormat::Glyf => &mut self.glyf_entries,
OutlineGlyphFormat::Cff | OutlineGlyphFormat::Cff2 => &mut self.cff_entries,
};
let (entry_ix, is_current) = find_hint_entry(entries, key)?;
let entry = entries.get_mut(entry_ix)?;
self.serial += 1;
entry.serial = self.serial;
if !is_current {
entry.font_id = key.font_id;
entry.font_index = key.font_index;
entry
.instance
.reconfigure(key.outlines, key.size, key.coords, HINTING_MODE)
.ok()?;
}
Some(&entry.instance)
}
}

struct HintEntry {
font_id: u64,
font_index: u32,
instance: HintingInstance,
serial: u64,
}

fn find_hint_entry(entries: &mut Vec<HintEntry>, key: &HintKey) -> Option<(usize, bool)> {
let mut found_serial = u64::MAX;
let mut found_index = 0;
for (ix, entry) in entries.iter().enumerate() {
if entry.font_id == key.font_id
&& entry.font_index == key.font_index
&& entry.instance.size() == key.size
&& entry.instance.location().coords() == key.coords
{
return Some((ix, true));
}
if entry.serial < found_serial {
found_serial = entry.serial;
found_index = ix;
}
}
if entries.len() < MAX_CACHED_HINT_INSTANCES {
let instance = key.instance()?;
let ix = entries.len();
entries.push(HintEntry {
font_id: key.font_id,
font_index: key.font_index,
instance,
serial: 0,
});
Some((ix, true))
} else {
Some((found_index, false))
}
}
12 changes: 9 additions & 3 deletions crates/encoding/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ impl Resolver {
index,
glyphs: _,
transform,
scale,
} = patch
{
let run = &resources.glyph_runs[*index];
Expand All @@ -338,7 +339,7 @@ impl Resolver {
let xform = *transform
* Transform {
matrix: [1.0, 0.0, 0.0, -1.0],
translation: [glyph.x, glyph.y],
translation: [glyph.x * scale, glyph.y * scale],
}
* glyph_transform;
data.extend_from_slice(bytemuck::bytes_of(&xform));
Expand All @@ -348,7 +349,7 @@ impl Resolver {
let xform = *transform
* Transform {
matrix: [1.0, 0.0, 0.0, -1.0],
translation: [glyph.x, glyph.y],
translation: [glyph.x * scale, glyph.y * scale],
};
data.extend_from_slice(bytemuck::bytes_of(&xform));
}
Expand Down Expand Up @@ -429,6 +430,7 @@ impl Resolver {
let mut hint = run.hint;
let mut font_size = run.font_size;
let mut transform = run.transform;
let mut scale = 1.0;
if hint {
// If hinting was requested and our transform matrix is just a uniform
// scale, then adjust our font size and cancel out the matrix. Otherwise,
Expand All @@ -437,7 +439,8 @@ impl Resolver {
&& transform.matrix[1] == 0.0
&& transform.matrix[2] == 0.0
{
font_size *= transform.matrix[0];
scale = transform.matrix[0];
font_size *= scale;
transform.matrix = [1.0, 0.0, 0.0, 1.0];
} else {
hint = false;
Expand Down Expand Up @@ -468,6 +471,7 @@ impl Resolver {
index: *index,
glyphs: glyph_start..glyph_end,
transform,
scale,
});
}
Patch::Image {
Expand Down Expand Up @@ -569,6 +573,8 @@ enum ResolvedPatch {
glyphs: Range<usize>,
/// Global transform.
transform: Transform,
/// Additional scale factor to apply to translation.
scale: f32,
},
Image {
/// Index of pending image element.
Expand Down
1 change: 1 addition & 0 deletions examples/scenes/src/simple_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ impl SimpleText {
.glyph_transform(glyph_transform)
.normalized_coords(var_loc.coords())
.brush(brush)
.hint(false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect us to want to test this?
Is it because SimpleText is also used for the headless example?

I see in Zulip that you've mentioned that text should be on a pixel baseline, which we don't enforce in our current examples.

I still would imagine that hinting could be productively used in e.g. the stats displays?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is disabled for the test scenes because we have some animated text which doesn’t work well with hinting (it snaps to pixels as it animates causing annoying jitter) and everything currently uses the same code path. We can definitely change this but I wanted to get the functionality in for xilem where hinting is more important.

.draw(
style,
text.chars().filter_map(|ch| {
Expand Down