2025Q1 Fonts Quarterly

Optimize all the things / Everybody gets font-funcs
March 31, 2025
behdad

Introduction

Runtime & memory performance improvements

Shaping speedups: AAT and OpenType

AAT speedups

OpenType speedups

Drawing VARC fonts speedups

COLRv1 speedups

Allocation-free graph traversal cycle detection: hb-decycler

Allocation-free everything

Integration with other font platforms and libraries

New font backends

New face loaders

Generic API for setting font backend or loading face

Integration with the Rust-based Fontations platform: hb-fontations

Benchmarking hb-fontations

Testing more font backends

Miscellanous

FontTools varLib.hvar module

Pre-proposal to enhance the avar table

ChatGPT 4o image creation fun time

Acknowledgements

Introduction

I spent roughly one month each on:

Runtime & memory performance improvements

Shaping speedups: AAT and OpenType

Mostly results of a Lisbon workshop with Dominik Röttsches of Chrome. The goal of the workshop was to improve Chrome rendering performance on Apple platforms. Since many Apple fonts use the AAT morx/kerx technology instead of OpenType shaping, we mostly focused on that.

We let the SpeedoMeter3 benchmark numbers guide our work. Dominik put some harness together to record all HarfBuzz calls Chrome will make to run the benchmark, so we could replay those all after each HarfBuzz change to quickly see results, instead of waiting to upload the changes to Chromium infrastructure to see the benchmark results come back an hour later. The workshop resulted in a +0.7% improvement in the SpeedoMeter3 benchmark number for Chrome, which I am told is very good.

AAT speedups

Several Apple system fonts use the state-machine model of shaping via the morx/kerx tables for substitutions (ligatures, etc) and positioning (kerning, mark attachment). We added several caches to the state-machine class-type lookup facility, which at the expense of a few kilobytes per font face, will vastly speed up shaping such fonts.

One typeface of note that was behaving particularly slow was LucidaGrande. So I spent some time investigating that one, and as a result the shaping benchmark time went down from 14.6ms to 5.9ms (~2.5x faster).

OpenType speedups

With the knowledge gained through AAT optimization work, I applied some of the same techniques to OpenType shaping, and at the expense of about 1kb memory per font face, I could speed up Roboto-Regular shaping benchmark time from 10.3ms to 9.4ms. (~9% faster)

Drawing VARC fonts speedups

VARC is the name of the table containing variable-composites glyph data, a technology I have proposed for inclusion in the font format, which is progressing through the ISO standardization process currently.

Various hot-loop optimizations and other allocation-free work (more on that below) resulted in a ~20% speedup in the HarfBuzz draw benchmark for heavy CJK variable-composites VARC fonts.

COLRv1 speedups

I was able to speed up painting of COLRv1 fonts considerably as well: from benchmark 7.85ms to 4.85ms. (~40% faster)

Part of the painting speedup came from removing all memory allocations from individual glyph paint calls. This was facilitated mainly by designing an alloc-free graph cycle detection data-structure & algorithm called hb-decycler.

Allocation-free graph traversal cycle detection: hb-decycler

While drawing or painting glyphs using HarfBuzz API, the code has to traverse graphs of objects as serialized in the font file. Such graphs can contain undesirable directed cycles. It is ideal to be able to detect such loops as early as possible and apply corrective measures.

The traditional way to do this is by maintaining a set of the currently visited objects. This was implemented in various places in HarfBuzz, using either the set-of-integers data-structure hb_set_t, or the hashmap data-structure hb_map_t. Both involve at least one memory allocation to operate. The malloc overhead was visible on benchmark numbers at roughly around 10%. So I decided to intervene.

I knew about Floyd’s tortoise & hare linked-list cycle detection algorithm. I extended that to work for DFS graph traversals instead. The Floyd algorithm is essentially applied to the stackframes of the recursive procedure calls, using only a tiny stack memory allocation, with zero heap memory allocations. This completely wiped off the cycle-detection overhead from our benchmarks. It is essentially free: after this change, if I remove the cycle-detection completely, I see no further speedup whatsoever.

I wrote that down in: HarfBuzz Study: hb-decycler. This has already been ported for use in the Fontations Rust libraries.

Allocation-free everything

I got obsessed with allocation-free operations, so I took it upon myself to apply it to all major libharfbuzz operations: shaping, drawing, and painting, across a variety of different font technologies (TrueType, CFF, variable font, color font, etc).

Our shaping code paths were already allocation-free, except for the shaping buffer itself which will grow as needed by the shaping operations (eg. one-to-many glyph substitutions). Clients can reuse a hb_buffer_t to avoid reallocation on every shape call.

There are various places in glyph outline extraction (called drawing) where memory allocations are necessary. The HarfBuzz API for drawing, like many other libraries, simply issues move-to, line-to, quadratic-to, curve-to, and close-path operation call-backs to the client. This, by itself, requires no memory allocations. And indeed, for example, non-variable CFF fonts are designed to naturally fit this kind of API without any allocations. Variable TrueType (glyf/gvar) fonts on the other hand, require loading the entire glyph outline into memory, to apply variations to them, before issuing drawing commands can begin.

Painting was mostly allocation free after the hb-decycler integration. The exception was that the code to calculate the painting bounding-box has to maintain two vectors of data.

To address all of the remaining allocations, I improvised what I call the scratchpad pattern: the draw / paint operation will use a scratchpad for all its memory allocations. The scratchpad typically contains multiple vector members. Those vectors reallocate more memory as needed. Normally though, you would throw away the scratchpad after drawing / painting one glyph. Instead, I hold onto the scratchpad by attaching it to the hb_face_t font face object. The next draw / paint operation then, will borrow the scratchpad from the face if one is available, use it, and return it. In the off-chance that multiple threads need the same scratchpad (ie. same font table operation) at the same time, only one of them will be able to use the cached scratchpad, and others will allocate and free one as needed. All major HarfBuzz clients are single-threaded, so this is the perfect tradeoff.

Integration with other font platforms and libraries

New font backends

While HarfBuzz internally implements all needed for hb_font_t font objects to perform shaping, drawing, and painting, it also allows for those to be overridden by the client. The HB-internal implementation is (confusingly) called “ot”, for historical reasons.

Clients can override such operations, called font-funcs, by providing their own callback functions to be called by HarfBuzz during shaping, drawing, and painting operations.

HarfBuzz also includes integration layers with many other font platforms and libraries. These integrations can provide their own font-funcs implementations, off-loading the operation to the adjacent library instead. This is very useful for correctness and performance testing, but there are also legitimate use-cases for them, which I won’t get into.

The hb-ft integration layer was, until recently, the only font-funcs implementation HarfBuzz provided other than ot. These functions call into the well-known FreeType Open Source library to perform the operations instead of letting HarfBuzz deal with font data itself. In this quarter, I added three more font-funcs implementations to HarfBuzz: in hb-coretext, hb-directwrite, and the new hb-fontations.

New face loaders

HarfBuzz knows how to load a font face from memory data or a file. But it also provides room for custom face loaders via the hb_face_create_for_tables API. HarfBuzz also provides ways to create hb_face_t font faces from other font-platform objects, like a FreeType FT_Face, CoreText CGFont, or DirectWrite IDWFontFace.

In the latest release however, we now provide an API to load a font face from memory data or file, using other font platforms / libraries via a uniform API. Implementations for FreeType, CoreText, and DirectWrite were added respectively to hb-ft, hb-coretext, and hb-directwrite.

One example of where non-HarfBuzz face-loading is useful is to deal with WOFF or WOFF2 webfonts. HarfBuzz does not natively support these formats. But FreeType does. Simply using the ft face loader makes those fonts work with HarfBuzz seamlessly.

Generic API for setting font backend or loading face

Finally, generic API was added for setting font-funcs on a hb_font_t object, or creating a hb_face_t object from memory data or file and asking for the backend via a name string (ot, ft, coretext, directwrite, fontations):

Since my Cairo-graphics maintainer times I always wanted a generic way to open font faces using different platforms. We never implemented that in Cairo, but here we go, they are in HarfBuzz now.

Integration with the Rust-based Fontations platform: hb-fontations

Fontations is the name of a set of Rust crates to implement a full font platform in fully safe Rust code. It is primarily developed by a team of Google developers, to replace glyph drawing and painting in Chrome and Android, from FreeType’s unsafe C codebase, to a safe Rust replacement. The crate closest in functionality to FreeType is called Skrifa, while lower level crates exist, like font-types, read-fonts, and write-fonts.

The hb-fontations font-funcs are complete and fully functional: providing shaping needs, drawing glyphs, and painting color-glyphs. This feature can be enabled by passing -Dfontations=enabled to the meson command-line when configuring a HarfBuzz build.

Benchmarking hb-fontations

Speed & memory consumption of the Fontations Rust font platform were assessed via the hb-fontations integration layer and using the HarfBuzz benchmark suite and a variety of performance monitoring tools. These measurements already resulted in major optimization work in the involved Fontations crates (Skrifa / read-fonts).

Testing more font backends

With so many font-funcs implementations in the HarfBuzz codebase now, I tweaked the regression test suite to run shaping using all the available font-funcs implementations in a build. Some tests fail with some backends, and we have tailored those tests to be run only on backends they will pass, such that the test results stay green despite the known limitations.

The HarfBuzz benchmarking suite has been ported to run across all shapers and font backends available. Instructions were added on how to test the hb-directwrite font-funcs on Linux / macOS.

Finally, two new environment variables are recognized by HarfBuzz now, namely HB_FONT_FUNCS and HB_FACE_LOADER. For shaping, there has always been HB_SHAPER_LIST. These three variables can change the default implementation of their respective aspects of HarfBuzz’s operation to use an implementation other than the default ot. The hb-view and hb-shape command-line tools allow controlling those aspects using --font-funcs, --face-loader, and --shapers.

Miscellanous

FontTools varLib.hvar module

The HVAR table in a variable font is essentially a cache of horizontal glyph advance widths. A variable font will perform functionally correctly without it, but with (vastly) slowed-down shaping. For that reason, the Glyphs app authors see it unnecessary to generate an HVAR table in the exported fonts.

To work around the Glyphs lack of this export feature, some designers have turned to surgically adding an HVAR table to their fonts in post-processing. The HVAR will be generated by exporting a font binary using fontmake with the Glyphs font source, then the TTX or binary HVAR table appended to the font exported natively via Glyphs app. This is a tedious and completely unnecessary burden.

While I cannot convince Georg to implement HVAR export in Glyphs, I could help desperate designers by creating a tool to automate the HVAR generation and addition to binary fonts. That is exactly what the new fonttools varLib.hvar command does. This will be in the next FontTools release. [merged PR]

See here for some follow-up discussion.

Pre-proposal to enhance the avar table

While working with some esoteric fonts, I ended up coming up with a list of ideas for enhancing the capabilities of the avar table in allowing for more distortions of the variable font design space in post-production. I have not written a formal proposal yet, but posted the ideas for discussion on the boring-expansion repo.

ChatGPT 4o image creation fun time

I love creating images with ChatGPT 4o. All pictures of me on my redesigned (also with ChatGPT help) website are retouched by AI.

There has been an obsessive thought I had a few years ago, for redesigning the logo for the FontTools project. Here is the current logo, designed by the project’s founder:

With ChatGPT help I created a few sketches. For the life of me I could not get ChatGPT to get left and right right. But here’s the best I got out of her:

I put it up on a poll on typo.social. The numbers speak for themselves (vast majority prefer the existing logo). FontTools creator said it best:

You've proposed this idea many times. I'm not opposed to a new logo, but I also don't see an immediate need. The old logo may not be perfect, but it mostly just works.

To me, the idea of using pictures of tools in the shape of letters is too simplistic, and I don't think a first year design student would get away with it.

Aside from that, even with a flipped F, I don't think it'll be recognizable enough as an F and a T.

I'll let others be grumpy on my behalf for the use of ChatGPT.

I will stick to writing code, not designing logos!

Acknowledgements

I like to thank Google Fonts for financially supporting my work. I am grateful for the help of the following people: HarfBuzz maintainer Khaled Hosny, Chrome developer Dominik Röttsches, Google Fonts: Dave Crossland, Rod Sheeter, Chad Brokaw, Raph Levien, and Cosimo Lupo.