Skip to content

glifo: Always draw non-hinted glyphs at upem size#1629

Merged
LaurenzV merged 6 commits into
mainfrom
laurenz/outline_opt
May 22, 2026
Merged

glifo: Always draw non-hinted glyphs at upem size#1629
LaurenzV merged 6 commits into
mainfrom
laurenz/outline_opt

Conversation

@LaurenzV
Copy link
Copy Markdown
Collaborator

@LaurenzV LaurenzV commented May 8, 2026

Problem

Right now, we always draw glyph outlines using skrifa at the given font size we are currently rendering. This causes two problems:

  1. It is expensive, because when the user zooms we keep redrawing the glyph outlines at increasingly larger font sizes, meaning that we don't get any cache hits.
  2. It also causes cache thrashing, since we try to cache the same glyph outlines at all kinds of different sizes.

Solution

The insight is that in the common case of having an unhinted and not-stroked glyph, it should be totally fine to just always outline the glyph at upem size and then scale it to the appropriate font size with a simple transform. I believe internally skrifa does something slightly different to keep compatibility with freetype, but in general this shouldn't cause any problems apart from small pixel differences (@dfrg can you confirm?).

Performance

When continuously zooming on our vello text scene, I get the following:

Before:
image

After:
image

As you can see, the skrifa outlining part completely disappears because each glyph is only outlined once and everything can therefore stay in the cache. The glyph cache maintenance part also goes away since there is no cache thrashing anymore.

@LaurenzV LaurenzV requested a review from grebmeg May 8, 2026 06:55
@laurenz-canva laurenz-canva force-pushed the laurenz/outline_opt branch from 4d699a7 to 513b096 Compare May 8, 2026 07:08
@LaurenzV
Copy link
Copy Markdown
Collaborator Author

LaurenzV commented May 8, 2026

Hmm, it is a bit unfortunate that basically all tests had to be regenerated. I would really be curious to hear from @dfrg first if he sees any issue with doing this.

@dfrg
Copy link
Copy Markdown
Collaborator

dfrg commented May 8, 2026

No issues at all. In fact, I intended to implement this optimization for unhinted glyphs in vello classic but never got around to it.

There are some older fonts where hinting is required for correct rasterization so I would keep that in mind if the aim is production quality. See googlefonts/fontations#1303

edit: I suppose “no issues” was a bit strong. More like “one minor issue…”

@LaurenzV
Copy link
Copy Markdown
Collaborator Author

LaurenzV commented May 8, 2026

Thanks a lot for confirming! Right, I remember that, though the issue is orthogonal to this PR. This could be fixed in a follow-up if we want to.

Copy link
Copy Markdown
Collaborator

@grebmeg grebmeg left a comment

Choose a reason for hiding this comment

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

It’s a nice improvement, @LaurenzV! Well done! 👏 Thanks for highlighting the areas to focus on in the screenshot 🙏 How much improvement are we seeing in the numbers here?

Comment thread glifo/src/glyph.rs Outdated
buffer: f32,
) -> impl Iterator<Item = Rect> + 'c {
let font_ref = self.prepared_run.font.as_skrifa();
let upem: f32 = font_ref.head().map(|h| h.units_per_em()).unwrap().into();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

upem and the cache-size decision are pure functions of the prepared run, but they're computed independently in draw_glyphs and here. Storing the resolved cache_size (and ideally upem) on PreparedGlyphRun during prepare_glyph_run would prevent these two consumers from drifting and remove the redundant font_ref.head() lookup.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Storing the cache_size on PreparedGlyphRun is not quite possible because at that point we don't know yet whether we are filling or stroking (unless we precompute both, which seems a bit ugly to me too 😄).

Will precompute the upem though, as you suggested.

Comment thread glifo/src/renderer.rs Outdated
Comment thread glifo/src/glyph.rs Outdated
Comment thread glifo/src/glyph.rs Outdated
Comment thread glifo/src/glyph.rs
self.prepared_run.font.data.id(),
self.prepared_run.font.index,
draw_props.font_size,
scale_props.cache_size,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If you pass upem here as well, I think you can communicate the intent a bit more clearly when building draw_settings. Essentially, you could write something like the following:

let size_param = if scale_props.cache_size == upem {
    Size::unscaled()
} else {
    Size::new(scale_props.cache_size)
};
DrawSettings::unhinted(size_param, var_key.0)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hmm I've not changed this for now because we already have soo many arguments in that function. Feel free to let me know if you feel strongly about it and then I'll do it in a follow-up.

@LaurenzV
Copy link
Copy Markdown
Collaborator Author

LaurenzV commented May 20, 2026

How much improvement are we seeing in the numbers here?

I don't remember the numbers, but seems like ~20-25%? It's definitely a big improvement though!

Also thanks for the comments, will try to address them soon!

@laurenz-canva laurenz-canva force-pushed the laurenz/outline_opt branch from c502a94 to dfb833f Compare May 22, 2026 19:30
@LaurenzV LaurenzV added this pull request to the merge queue May 22, 2026
Merged via the queue into main with commit 92d98d4 May 22, 2026
17 checks passed
@LaurenzV LaurenzV deleted the laurenz/outline_opt branch May 22, 2026 20:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants