diff --git a/text/shape/lru.go b/text/shape/lru.go new file mode 100644 index 00000000..2f7f429f --- /dev/null +++ b/text/shape/lru.go @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package shape + +import ( + "gioui.org/op" + "gioui.org/text" + "golang.org/x/image/font/sfnt" + "golang.org/x/image/math/fixed" +) + +type layoutCache struct { + m map[layoutKey]*layout + head, tail *layout +} + +type pathCache struct { + m map[pathKey]*path + head, tail *path +} + +type layout struct { + next, prev *layout + key layoutKey + layout *text.Layout +} + +type path struct { + next, prev *path + key pathKey + val op.MacroOp +} + +type layoutKey struct { + f *sfnt.Font + ppem fixed.Int26_6 + str string + opts text.LayoutOptions +} + +type pathKey struct { + f *sfnt.Font + ppem fixed.Int26_6 + str string +} + +const maxSize = 1000 + +func (l *layoutCache) Get(k layoutKey) (*text.Layout, bool) { + if lt, ok := l.m[k]; ok { + l.remove(lt) + l.insert(lt) + return lt.layout, true + } + return nil, false +} + +func (l *layoutCache) Put(k layoutKey, lt *text.Layout) { + if l.m == nil { + l.m = make(map[layoutKey]*layout) + l.head = new(layout) + l.tail = new(layout) + l.head.prev = l.tail + l.tail.next = l.head + } + val := &layout{key: k, layout: lt} + l.m[k] = val + l.insert(val) + if len(l.m) > maxSize { + oldest := l.tail.next + l.remove(oldest) + delete(l.m, oldest.key) + } +} + +func (l *layoutCache) remove(lt *layout) { + lt.next.prev = lt.prev + lt.prev.next = lt.next +} + +func (l *layoutCache) insert(lt *layout) { + lt.next = l.head + lt.prev = l.head.prev + lt.prev.next = lt + lt.next.prev = lt +} + +func (c *pathCache) Get(k pathKey) (op.MacroOp, bool) { + if v, ok := c.m[k]; ok { + c.remove(v) + c.insert(v) + return v.val, true + } + return op.MacroOp{}, false +} + +func (c *pathCache) Put(k pathKey, v op.MacroOp) { + if c.m == nil { + c.m = make(map[pathKey]*path) + c.head = new(path) + c.tail = new(path) + c.head.prev = c.tail + c.tail.next = c.head + } + val := &path{key: k, val: v} + c.m[k] = val + c.insert(val) + if len(c.m) > maxSize { + oldest := c.tail.next + c.remove(oldest) + delete(c.m, oldest.key) + } +} + +func (c *pathCache) remove(v *path) { + v.next.prev = v.prev + v.prev.next = v.next +} + +func (c *pathCache) insert(v *path) { + v.next = c.head + v.prev = c.head.prev + v.prev.next = v + v.next.prev = v +} diff --git a/text/shape/lru_test.go b/text/shape/lru_test.go new file mode 100644 index 00000000..fa72ea4c --- /dev/null +++ b/text/shape/lru_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package shape + +import ( + "strconv" + "testing" + + "gioui.org/op" +) + +func TestLayoutLRU(t *testing.T) { + c := new(layoutCache) + put := func(i int) { + c.Put(layoutKey{str: strconv.Itoa(i)}, nil) + } + get := func(i int) bool { + _, ok := c.Get(layoutKey{str: strconv.Itoa(i)}) + return ok + } + testLRU(t, put, get) +} + +func TestuPathLRU(t *testing.T) { + c := new(pathCache) + put := func(i int) { + c.Put(pathKey{str: strconv.Itoa(i)}, op.MacroOp{}) + } + get := func(i int) bool { + _, ok := c.Get(pathKey{str: strconv.Itoa(i)}) + return ok + } + testLRU(t, put, get) +} + +func testLRU(t *testing.T, put func(i int), get func(i int) bool) { + for i := 0; i < maxSize; i++ { + put(i) + } + for i := 0; i < maxSize; i++ { + if !get(i) { + t.Fatalf("key %d was evicted", i) + } + } + put(maxSize) + for i := 1; i < maxSize+1; i++ { + if !get(i) { + t.Fatalf("key %d was evicted", i) + } + } + if i := 0; get(i) { + t.Fatalf("key %d was not evicted", i) + } +} diff --git a/text/shape/measure.go b/text/shape/measure.go index e6ae257f..cb073c83 100644 --- a/text/shape/measure.go +++ b/text/shape/measure.go @@ -27,52 +27,8 @@ type Family struct { Italic *sfnt.Font Bold *sfnt.Font - layoutCache map[layoutKey]cachedLayout - pathCache map[pathKey]cachedPath -} - -type cachedLayout struct { - active bool - layout *text.Layout -} - -type cachedPath struct { - active bool - path op.MacroOp -} - -type layoutKey struct { - f *sfnt.Font - ppem fixed.Int26_6 - str string - opts text.LayoutOptions -} - -type pathKey struct { - f *sfnt.Font - ppem fixed.Int26_6 - str string -} - -// Reset the cache, discarding any layouts or paths that -// haven't been used since the last call to Reset. -func (f *Family) Reset() { - for pk, p := range f.pathCache { - if !p.active { - delete(f.pathCache, pk) - continue - } - p.active = false - f.pathCache[pk] = p - } - for lk, l := range f.layoutCache { - if !l.active { - delete(f.layoutCache, lk) - continue - } - l.active = false - f.layoutCache[lk] = l - } + layoutCache layoutCache + pathCache pathCache } // for returns a font for the given face. @@ -90,16 +46,7 @@ func (f *Family) fontFor(face text.Face) *sfnt.Font { return font } -func (f *Family) init() { - if f.pathCache != nil { - return - } - f.pathCache = make(map[pathKey]cachedPath) - f.layoutCache = make(map[layoutKey]cachedLayout) -} - func (f *Family) Layout(face text.Face, size float32, str string, opts text.LayoutOptions) *text.Layout { - f.init() fnt := f.fontFor(face) ppem := fixed.Int26_6(size * 64) lk := layoutKey{ @@ -108,18 +55,15 @@ func (f *Family) Layout(face text.Face, size float32, str string, opts text.Layo str: str, opts: opts, } - if l, ok := f.layoutCache[lk]; ok { - l.active = true - f.layoutCache[lk] = l - return l.layout + if l, ok := f.layoutCache.Get(lk); ok { + return l } l := layoutText(ppem, str, &opentype{Font: fnt, Hinting: font.HintingFull}, opts) - f.layoutCache[lk] = cachedLayout{active: true, layout: l} + f.layoutCache.Put(lk, l) return l } func (f *Family) Shape(face text.Face, size float32, str text.String) op.MacroOp { - f.init() fnt := f.fontFor(face) ppem := fixed.Int26_6(size * 64) pk := pathKey{ @@ -127,13 +71,11 @@ func (f *Family) Shape(face text.Face, size float32, str text.String) op.MacroOp ppem: ppem, str: str.String, } - if p, ok := f.pathCache[pk]; ok { - p.active = true - f.pathCache[pk] = p - return p.path + if p, ok := f.pathCache.Get(pk); ok { + return p } p := textPath(ppem, &opentype{Font: fnt, Hinting: font.HintingFull}, str) - f.pathCache[pk] = cachedPath{active: true, path: p} + f.pathCache.Put(pk, p) return p }