From 48824a02b7cb7b734d842ae14b2f967e2b387f60 Mon Sep 17 00:00:00 2001 From: Benoit KUGLER Date: Thu, 16 Apr 2026 15:06:42 +0200 Subject: [PATCH 1/7] [font] attempt to speedup Cmap lookup, for #252 --- font/cmap_cache.go | 33 ++++++++++++++++++++++++++++++++- font/font.go | 11 +++++++++-- font/font_test.go | 14 ++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/font/cmap_cache.go b/font/cmap_cache.go index 02444cb..1e4a36c 100644 --- a/font/cmap_cache.go +++ b/font/cmap_cache.go @@ -11,7 +11,7 @@ package font * and the rest (low bits). * * The memory layout is the following : - * KEY = + * KEY = * VALUE = * with the constraints * KEY in [0, 2^key bits[ @@ -68,3 +68,34 @@ func (c *cache21_19_8) setUnchecked(key uint32, value uint32) { v := (uint32(key>>8) << 19) | value c[k] = v } + +// cache21_0_13 is a cache for integer keys, +// with 0 <= key < 2097152 +type cache21_0_13 [1 << 13]uint8 + +// clear should be used as init function +func (c *cache21_0_13) clear() { + for i := range c { + c[i] = ^uint8(0) + } +} + +func (c cache21_0_13) get(key uint32) bool { + k := key & ((1 << 13) - 1) + v := c[k] + return v != ^uint8(0) && v == uint8(key>>13) +} + +func (c *cache21_0_13) set(key uint32) { + if (key >> 21) != 0 { /* overflows */ + return + } + c.setUnchecked(key) +} + +// assumes key < 2097152 +func (c *cache21_0_13) setUnchecked(key uint32) { + k := key & ((1 << 13) - 1) + v := uint8(key >> 13) + c[k] = v +} diff --git a/font/font.go b/font/font.go index 43fbd1e..53069ab 100644 --- a/font/font.go +++ b/font/font.go @@ -620,8 +620,9 @@ func loadGDEF(ld *ot.Loader, axisCount int, gsub, gpos []byte) (tables.GDEF, err type Face struct { *Font - extentsCache extentsCache - cmapCache cache21_19_8 + extentsCache extentsCache + cmapCache cache21_19_8 // supported runes, mapping to GID + cmapNotSupportedCache cache21_0_13 // not supported runes coords []tables.Coord xPpem, yPpem uint16 @@ -631,6 +632,7 @@ type Face struct { func NewFace(font *Font) *Face { out := &Face{Font: font, extentsCache: make(extentsCache, font.nGlyphs)} out.cmapCache.clear() + out.cmapNotSupportedCache.clear() return out } @@ -639,12 +641,17 @@ func NewFace(font *Font) *Face { // Note that it only looks into the cmap, without taking account substitutions // nor variation selectors. func (f *Face) NominalGlyph(ch rune) (GID, bool) { + if notSupported := f.cmapNotSupportedCache.get(uint32(ch)); notSupported { + return 0, false + } if g, ok := f.cmapCache.get(uint32(ch)); ok { return GID(g), ok } g, ok := f.Cmap.Lookup(ch) if ok { f.cmapCache.set(uint32(ch), uint32(g)) + } else { + f.cmapNotSupportedCache.set(uint32(ch)) } return g, ok } diff --git a/font/font_test.go b/font/font_test.go index 322a668..39509b1 100644 --- a/font/font_test.go +++ b/font/font_test.go @@ -181,3 +181,17 @@ func TestBitmapExtents(t *testing.T) { extents, ok := face.GlyphExtents(41) tu.Assert(t, ok && extents.Width == 819.2 && extents.Height == -1433.6) } + +func BenchmarkCmap(b *testing.B) { + font := loadFont(b, "common/Roboto-BoldItalic.ttf") + face := NewFace(font) + text := []rune("襄陽曲四首/魯中都東樓醉起作-李白 刊误") + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + for _, r := range text { + _, _ = face.NominalGlyph(r) + } + } +} From f1ffe82d1548fececfefdccab3f183c4cd0c8a20 Mon Sep 17 00:00:00 2001 From: Benoit KUGLER Date: Sun, 19 Apr 2026 18:06:09 +0200 Subject: [PATCH 2/7] [font,harfbuzz] use pointers for large caches --- font/cmap_cache.go | 4 ++-- harfbuzz/caches.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/font/cmap_cache.go b/font/cmap_cache.go index 1e4a36c..c167b98 100644 --- a/font/cmap_cache.go +++ b/font/cmap_cache.go @@ -46,7 +46,7 @@ func (c *cache21_19_8) clear() { } } -func (c cache21_19_8) get(key uint32) (uint32, bool) { +func (c *cache21_19_8) get(key uint32) (uint32, bool) { k := key & ((1 << 8) - 1) v := c[k] if v == ^uint32(0) || (v>>19) != uint32(key>>8) { @@ -80,7 +80,7 @@ func (c *cache21_0_13) clear() { } } -func (c cache21_0_13) get(key uint32) bool { +func (c *cache21_0_13) get(key uint32) bool { k := key & ((1 << 13) - 1) v := c[k] return v != ^uint8(0) && v == uint8(key>>13) diff --git a/harfbuzz/caches.go b/harfbuzz/caches.go index 5f12bcb..8298d1d 100644 --- a/harfbuzz/caches.go +++ b/harfbuzz/caches.go @@ -11,7 +11,7 @@ package harfbuzz * and the rest (low bits). * * The memory layout is the following : - * KEY = + * KEY = * VALUE = * with the constraints * KEY in [0, 2^key bits[ @@ -46,7 +46,7 @@ func (c *cache15_8_7) clear() { } } -func (c cache15_8_7) get(key uint16) (uint16, bool) { +func (c *cache15_8_7) get(key uint16) (uint16, bool) { k := key & ((1 << 7) - 1) v := c[k] if v == ^uint16(0) || (v>>8) != uint16(key>>7) { @@ -80,7 +80,7 @@ func (c *cache21_3_8) clear() { } } -func (c cache21_3_8) get(key uint32) (uint16, bool) { +func (c *cache21_3_8) get(key uint32) (uint16, bool) { k := key & ((1 << 8) - 1) v := c[k] if v == ^uint16(0) || (v>>3) != uint16(key>>8) { From e10239f1d462e321f768a7b91f26650d859593ba Mon Sep 17 00:00:00 2001 From: Benoit KUGLER Date: Sun, 19 Apr 2026 18:12:58 +0200 Subject: [PATCH 3/7] extend benchmark --- font/font_test.go | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/font/font_test.go b/font/font_test.go index 39509b1..b81bbe7 100644 --- a/font/font_test.go +++ b/font/font_test.go @@ -185,13 +185,31 @@ func TestBitmapExtents(t *testing.T) { func BenchmarkCmap(b *testing.B) { font := loadFont(b, "common/Roboto-BoldItalic.ttf") face := NewFace(font) - text := []rune("襄陽曲四首/魯中都東樓醉起作-李白 刊误") + latinText := []rune("Hi this is a test with some âccents : $£8") + chineseText := []rune("襄陽曲四首/魯中都東樓醉起作-李白 刊误") + mixedText := append(latinText, chineseText...) b.ResetTimer() - for i := 0; i < b.N; i++ { - for _, r := range text { - _, _ = face.NominalGlyph(r) + b.Run("latin text", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, r := range latinText { + _, _ = face.NominalGlyph(r) + } } - } + }) + b.Run("chinese text", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, r := range latinText { + _, _ = face.NominalGlyph(r) + } + } + }) + b.Run("chinese text", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, r := range mixedText { + _, _ = face.NominalGlyph(r) + } + } + }) } From 70e415233ea11baa68c27e2294c8efe67f03835f Mon Sep 17 00:00:00 2001 From: Benoit KUGLER Date: Sun, 19 Apr 2026 18:14:36 +0200 Subject: [PATCH 4/7] fix benchmark name --- font/font_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/font/font_test.go b/font/font_test.go index b81bbe7..76a41f9 100644 --- a/font/font_test.go +++ b/font/font_test.go @@ -205,7 +205,7 @@ func BenchmarkCmap(b *testing.B) { } } }) - b.Run("chinese text", func(b *testing.B) { + b.Run("mixed text", func(b *testing.B) { for i := 0; i < b.N; i++ { for _, r := range mixedText { _, _ = face.NominalGlyph(r) From 3af7be22d66a265125e0a4143bf22dcb4422c667 Mon Sep 17 00:00:00 2001 From: Benoit KUGLER Date: Sun, 19 Apr 2026 18:25:22 +0200 Subject: [PATCH 5/7] fix benchmark --- font/font_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/font/font_test.go b/font/font_test.go index 76a41f9..40d9f33 100644 --- a/font/font_test.go +++ b/font/font_test.go @@ -200,7 +200,7 @@ func BenchmarkCmap(b *testing.B) { }) b.Run("chinese text", func(b *testing.B) { for i := 0; i < b.N; i++ { - for _, r := range latinText { + for _, r := range chineseText { _, _ = face.NominalGlyph(r) } } From e59d6aff72ca03b35cda44303827a586d185166e Mon Sep 17 00:00:00 2001 From: Benoit KUGLER Date: Sun, 19 Apr 2026 18:48:56 +0200 Subject: [PATCH 6/7] fix test --- font/renderer_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/font/renderer_test.go b/font/renderer_test.go index b77b06f..f5d03f3 100644 --- a/font/renderer_test.go +++ b/font/renderer_test.go @@ -591,7 +591,8 @@ func TestAppleBitmapGlyph(t *testing.T) { ft, err := NewFont(fonts[0]) tu.AssertNoErr(t, err) - face := Face{Font: ft, xPpem: 94, yPpem: 94} + face := NewFace(ft) + face.SetPpem(94, 94) runes := "The quick brown fox jumps over the lazy dog" for _, r := range runes { From e7c294505a4f3d0db28b15a52f612242c10bdc7f Mon Sep 17 00:00:00 2001 From: Benoit KUGLER Date: Wed, 22 Apr 2026 16:24:30 +0200 Subject: [PATCH 7/7] polish caches documentation --- font/cmap_cache.go | 89 +++++++++++++++++++++++++++------------------- harfbuzz/caches.go | 89 +++++++++++++++++++++++++++------------------- 2 files changed, 104 insertions(+), 74 deletions(-) diff --git a/font/cmap_cache.go b/font/cmap_cache.go index c167b98..c80b700 100644 --- a/font/cmap_cache.go +++ b/font/cmap_cache.go @@ -2,41 +2,32 @@ package font // Code generated by typesetting-utils/generators/cache/gen.go. DO NOT EDIT. -/* Implements caches for integers key->value functions. - * - * The cache is a fixed-size array of 8-bit, 16-bit or 32-bit integers, - * typically 256 elements. - * - * The key is split into two parts: the cache index (high bits) - * and the rest (low bits). - * - * The memory layout is the following : - * KEY = - * VALUE = - * with the constraints - * KEY in [0, 2^key bits[ - * VALUE in [0, 2^value bits[ - * - * The cache index is used to index into the array. The array - * member is an integer that is used BOTH - * to store the low bits of the key, and the value. - * - * The value is stored in the least significant bits of the integer. - * The low bits of the key are stored in the most significant bits - * of the integer. - * - * A cache hit is detected by comparing the low bits of the key - * with the high bits of the integer at the array position indexed - * by the high bits of the key. If they match, the value is extracted - * from the least significant bits of the integer and returned. - * Otherwise, a cache miss is reported. - * - * Cache operations (storage and retrieval) involve just a few - * arithmetic operations and a single memory access. - */ - -// cache21_19_8 is a cache for integer (key, value) pairs, -// with 0 <= key < 2097152 and 0 <= value < 524288 +// cache21_19_8 implements a cache for integer pairs, +// mapping a key in [0,1 << 21[ to a value in [0,1 << 19[ +// +// The memory layout is the following : +// +// KEY : <13 bits><8 bits> +// VALUE : <13 bits><19 bits> +// +// The cache index is used to index into the array. The array +// member is an integer that is used BOTH +// to store the high bits of the key, and the value. +// The value is stored in the least significant bits of the integer. +// The high bits of the key are stored in the most significant bits +// of the integer. +// +// A cache hit is detected by comparing the high bits of the key +// with the high bits of the integer at the array position indexed +// by the low bits of the key. If they match, the value is extracted +// from the least significant bits of the integer and returned. +// Otherwise, a cache miss is reported. +// +// Cache operations (storage and retrieval) involve just a few +// arithmetic operations and a single memory access. +// +// This cache works best with sparse keys (sharing the same high 13 bits), +// since it handles 1 << 8 contiguous keys without collision. type cache21_19_8 [1 << 8]uint32 // clear should be used as init function @@ -69,8 +60,32 @@ func (c *cache21_19_8) setUnchecked(key uint32, value uint32) { c[k] = v } -// cache21_0_13 is a cache for integer keys, -// with 0 <= key < 2097152 +// cache21_0_13 implements a cache for integer pairs, +// mapping a key in [0,1 << 21[ to a value in [0,1 << 0[ +// +// The memory layout is the following : +// +// KEY : <8 bits><13 bits> +// VALUE : <8 bits><0 bits> +// +// The cache index is used to index into the array. The array +// member is an integer that is used BOTH +// to store the high bits of the key, and the value. +// The value is stored in the least significant bits of the integer. +// The high bits of the key are stored in the most significant bits +// of the integer. +// +// A cache hit is detected by comparing the high bits of the key +// with the high bits of the integer at the array position indexed +// by the low bits of the key. If they match, the value is extracted +// from the least significant bits of the integer and returned. +// Otherwise, a cache miss is reported. +// +// Cache operations (storage and retrieval) involve just a few +// arithmetic operations and a single memory access. +// +// This cache works best with sparse keys (sharing the same high 8 bits), +// since it handles 1 << 13 contiguous keys without collision. type cache21_0_13 [1 << 13]uint8 // clear should be used as init function diff --git a/harfbuzz/caches.go b/harfbuzz/caches.go index 8298d1d..0703bfb 100644 --- a/harfbuzz/caches.go +++ b/harfbuzz/caches.go @@ -2,41 +2,32 @@ package harfbuzz // Code generated by typesetting-utils/generators/cache/gen.go. DO NOT EDIT. -/* Implements caches for integers key->value functions. - * - * The cache is a fixed-size array of 8-bit, 16-bit or 32-bit integers, - * typically 256 elements. - * - * The key is split into two parts: the cache index (high bits) - * and the rest (low bits). - * - * The memory layout is the following : - * KEY = - * VALUE = - * with the constraints - * KEY in [0, 2^key bits[ - * VALUE in [0, 2^value bits[ - * - * The cache index is used to index into the array. The array - * member is an integer that is used BOTH - * to store the low bits of the key, and the value. - * - * The value is stored in the least significant bits of the integer. - * The low bits of the key are stored in the most significant bits - * of the integer. - * - * A cache hit is detected by comparing the low bits of the key - * with the high bits of the integer at the array position indexed - * by the high bits of the key. If they match, the value is extracted - * from the least significant bits of the integer and returned. - * Otherwise, a cache miss is reported. - * - * Cache operations (storage and retrieval) involve just a few - * arithmetic operations and a single memory access. - */ - -// cache15_8_7 is a cache for integer (key, value) pairs, -// with 0 <= key < 32768 and 0 <= value < 256 +// cache15_8_7 implements a cache for integer pairs, +// mapping a key in [0,1 << 15[ to a value in [0,1 << 8[ +// +// The memory layout is the following : +// +// KEY : <8 bits><7 bits> +// VALUE : <8 bits><8 bits> +// +// The cache index is used to index into the array. The array +// member is an integer that is used BOTH +// to store the high bits of the key, and the value. +// The value is stored in the least significant bits of the integer. +// The high bits of the key are stored in the most significant bits +// of the integer. +// +// A cache hit is detected by comparing the high bits of the key +// with the high bits of the integer at the array position indexed +// by the low bits of the key. If they match, the value is extracted +// from the least significant bits of the integer and returned. +// Otherwise, a cache miss is reported. +// +// Cache operations (storage and retrieval) involve just a few +// arithmetic operations and a single memory access. +// +// This cache works best with sparse keys (sharing the same high 8 bits), +// since it handles 1 << 7 contiguous keys without collision. type cache15_8_7 [1 << 7]uint16 // clear should be used as init function @@ -69,8 +60,32 @@ func (c *cache15_8_7) setUnchecked(key uint16, value uint16) { c[k] = v } -// cache21_3_8 is a cache for integer (key, value) pairs, -// with 0 <= key < 2097152 and 0 <= value < 8 +// cache21_3_8 implements a cache for integer pairs, +// mapping a key in [0,1 << 21[ to a value in [0,1 << 3[ +// +// The memory layout is the following : +// +// KEY : <13 bits><8 bits> +// VALUE : <13 bits><3 bits> +// +// The cache index is used to index into the array. The array +// member is an integer that is used BOTH +// to store the high bits of the key, and the value. +// The value is stored in the least significant bits of the integer. +// The high bits of the key are stored in the most significant bits +// of the integer. +// +// A cache hit is detected by comparing the high bits of the key +// with the high bits of the integer at the array position indexed +// by the low bits of the key. If they match, the value is extracted +// from the least significant bits of the integer and returned. +// Otherwise, a cache miss is reported. +// +// Cache operations (storage and retrieval) involve just a few +// arithmetic operations and a single memory access. +// +// This cache works best with sparse keys (sharing the same high 13 bits), +// since it handles 1 << 8 contiguous keys without collision. type cache21_3_8 [1 << 8]uint16 // clear should be used as init function