mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-07 18:35:34 +00:00
text/shape: remove Family.Reset by introducing LRU caches
It was easy to forget Family.Reset, and the per-frame caching strategy is probably too aggressive. Use a static size for the caches and evict according to a least recently used policy. Reset is then no longer required, and we can delete it. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-66
@@ -27,52 +27,8 @@ type Family struct {
|
|||||||
Italic *sfnt.Font
|
Italic *sfnt.Font
|
||||||
Bold *sfnt.Font
|
Bold *sfnt.Font
|
||||||
|
|
||||||
layoutCache map[layoutKey]cachedLayout
|
layoutCache layoutCache
|
||||||
pathCache map[pathKey]cachedPath
|
pathCache pathCache
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// for returns a font for the given face.
|
// for returns a font for the given face.
|
||||||
@@ -90,16 +46,7 @@ func (f *Family) fontFor(face text.Face) *sfnt.Font {
|
|||||||
return 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 {
|
func (f *Family) Layout(face text.Face, size float32, str string, opts text.LayoutOptions) *text.Layout {
|
||||||
f.init()
|
|
||||||
fnt := f.fontFor(face)
|
fnt := f.fontFor(face)
|
||||||
ppem := fixed.Int26_6(size * 64)
|
ppem := fixed.Int26_6(size * 64)
|
||||||
lk := layoutKey{
|
lk := layoutKey{
|
||||||
@@ -108,18 +55,15 @@ func (f *Family) Layout(face text.Face, size float32, str string, opts text.Layo
|
|||||||
str: str,
|
str: str,
|
||||||
opts: opts,
|
opts: opts,
|
||||||
}
|
}
|
||||||
if l, ok := f.layoutCache[lk]; ok {
|
if l, ok := f.layoutCache.Get(lk); ok {
|
||||||
l.active = true
|
return l
|
||||||
f.layoutCache[lk] = l
|
|
||||||
return l.layout
|
|
||||||
}
|
}
|
||||||
l := layoutText(ppem, str, &opentype{Font: fnt, Hinting: font.HintingFull}, opts)
|
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
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Family) Shape(face text.Face, size float32, str text.String) op.MacroOp {
|
func (f *Family) Shape(face text.Face, size float32, str text.String) op.MacroOp {
|
||||||
f.init()
|
|
||||||
fnt := f.fontFor(face)
|
fnt := f.fontFor(face)
|
||||||
ppem := fixed.Int26_6(size * 64)
|
ppem := fixed.Int26_6(size * 64)
|
||||||
pk := pathKey{
|
pk := pathKey{
|
||||||
@@ -127,13 +71,11 @@ func (f *Family) Shape(face text.Face, size float32, str text.String) op.MacroOp
|
|||||||
ppem: ppem,
|
ppem: ppem,
|
||||||
str: str.String,
|
str: str.String,
|
||||||
}
|
}
|
||||||
if p, ok := f.pathCache[pk]; ok {
|
if p, ok := f.pathCache.Get(pk); ok {
|
||||||
p.active = true
|
return p
|
||||||
f.pathCache[pk] = p
|
|
||||||
return p.path
|
|
||||||
}
|
}
|
||||||
p := textPath(ppem, &opentype{Font: fnt, Hinting: font.HintingFull}, str)
|
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
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user