text/v2: let StdFace and GoTextFaceSource have their own glyph caches

This commit is contained in:
Hajime Hoshi 2023-11-19 23:19:24 +09:00
parent 7a08737326
commit 57ae07eb36
5 changed files with 27 additions and 83 deletions

View File

@ -39,11 +39,6 @@ func init() {
})
}
type faceCacheKey struct {
id uint64
goTextFaceSize float64
}
type glyphImageCacheKey struct {
// id is rune for StdFace, and GID for GoTextFace.
id uint32
@ -60,28 +55,22 @@ type glyphImageCacheEntry struct {
}
type glyphImageCache struct {
cache map[faceCacheKey]map[glyphImageCacheKey]*glyphImageCacheEntry
cache map[glyphImageCacheKey]*glyphImageCacheEntry
m sync.Mutex
}
var theGlyphImageCache glyphImageCache
func (g *glyphImageCache) getOrCreate(face Face, key glyphImageCacheKey, create func() *ebiten.Image) *ebiten.Image {
g.m.Lock()
defer g.m.Unlock()
e, ok := g.cache[face.faceCacheKey()][key]
e, ok := g.cache[key]
if ok {
e.atime = now()
return e.image
}
if g.cache == nil {
g.cache = map[faceCacheKey]map[glyphImageCacheKey]*glyphImageCacheEntry{}
}
faceCacheKey := face.faceCacheKey()
if g.cache[faceCacheKey] == nil {
g.cache[faceCacheKey] = map[glyphImageCacheKey]*glyphImageCacheEntry{}
g.cache = map[glyphImageCacheKey]*glyphImageCacheEntry{}
}
img := create()
@ -95,7 +84,7 @@ func (g *glyphImageCache) getOrCreate(face Face, key glyphImageCacheKey, create
// Keep this until the face is GCed.
e.atime = infTime
}
g.cache[faceCacheKey][key] = e
g.cache[key] = e
// Clean up old entries.
@ -104,26 +93,15 @@ func (g *glyphImageCache) getOrCreate(face Face, key glyphImageCacheKey, create
// Even after cleaning up the cache, the number of glyphs might still exceed the soft limit, but
// this is fine.
cacheSoftLimit := 128 * glyphVariationCount(face)
if len(g.cache[faceCacheKey]) > cacheSoftLimit {
for key, e := range g.cache[faceCacheKey] {
if len(g.cache) > cacheSoftLimit {
for key, e := range g.cache {
// 60 is an arbitrary number.
if e.atime >= now()-60 {
continue
}
delete(g.cache[faceCacheKey], key)
delete(g.cache, key)
}
}
return img
}
func (g *glyphImageCache) clear(comp func(key faceCacheKey) bool) {
g.m.Lock()
defer g.m.Unlock()
for key := range g.cache {
if comp(key) {
delete(g.cache, key)
}
}
}

View File

@ -238,14 +238,6 @@ func (g *GoTextFace) ensureFeaturesString() string {
return g.featuresString
}
// faceCacheKey implements Face.
func (g *GoTextFace) faceCacheKey() faceCacheKey {
return faceCacheKey{
id: g.Source.id,
goTextFaceSize: g.Size,
}
}
func (g *GoTextFace) outputCacheKey(text string) goTextOutputCacheKey {
return goTextOutputCacheKey{
text: text,
@ -343,7 +335,7 @@ func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Im
yoffset: subpixelOffset.Y,
variations: g.ensureVariationsString(),
}
img := theGlyphImageCache.getOrCreate(g, key, func() *ebiten.Image {
img := g.Source.getOrCreateGlyphImage(g, key, func() *ebiten.Image {
return segmentsToImage(glyph.scaledSegments, subpixelOffset, b)
})

View File

@ -17,7 +17,6 @@ package text
import (
"bytes"
"io"
"runtime"
"sync"
"github.com/go-text/typesetting/font"
@ -25,6 +24,8 @@ import (
"github.com/go-text/typesetting/opentype/api"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2"
)
type goTextOutputCacheKey struct {
@ -53,10 +54,10 @@ type goTextOutputCacheValue struct {
// GoTextFaceSource is a source of a GoTextFace. This can be shared by multiple GoTextFace objects.
type GoTextFaceSource struct {
f font.Face
id uint64
f font.Face
outputCache map[goTextOutputCacheKey]*goTextOutputCacheValue
outputCache map[goTextOutputCacheKey]*goTextOutputCacheValue
glyphImageCache map[float64]*glyphImageCache
addr *GoTextFaceSource
@ -94,11 +95,9 @@ func NewGoTextFaceSource(source io.ReadSeeker) (*GoTextFaceSource, error) {
}
s := &GoTextFaceSource{
f: f,
id: nextUniqueID(),
f: f,
}
s.addr = s
runtime.SetFinalizer(s, finalizeGoTextFaceSource)
return s, nil
}
@ -117,23 +116,14 @@ func NewGoTextFaceSourcesFromCollection(source io.ReadSeeker) ([]*GoTextFaceSour
sources := make([]*GoTextFaceSource, len(fs))
for i, f := range fs {
s := &GoTextFaceSource{
f: f,
id: nextUniqueID(),
f: f,
}
s.addr = s
runtime.SetFinalizer(s, finalizeGoTextFaceSource)
sources[i] = s
}
return sources, nil
}
func finalizeGoTextFaceSource(source *GoTextFaceSource) {
runtime.SetFinalizer(source, nil)
theGlyphImageCache.clear(func(key faceCacheKey) bool {
return key.id == source.id
})
}
func (g *GoTextFaceSource) copyCheck() {
if g.addr != g {
panic("text: illegal use of non-zero GoTextFaceSource copied by value")
@ -233,3 +223,13 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output,
func (g *GoTextFaceSource) scale(size float64) float64 {
return size / float64(g.f.Upem())
}
func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key glyphImageCacheKey, create func() *ebiten.Image) *ebiten.Image {
if g.glyphImageCache == nil {
g.glyphImageCache = map[float64]*glyphImageCache{}
}
if _, ok := g.glyphImageCache[goTextFace.Size]; !ok {
g.glyphImageCache[goTextFace.Size] = &glyphImageCache{}
}
return g.glyphImageCache[goTextFace.Size].getOrCreate(goTextFace, key, create)
}

View File

@ -16,7 +16,6 @@ package text
import (
"image"
"runtime"
"unicode/utf8"
"golang.org/x/image/font"
@ -33,7 +32,7 @@ var _ Face = (*StdFace)(nil)
type StdFace struct {
f *faceWithCache
id uint64
glyphImageCache glyphImageCache
addr *StdFace
}
@ -44,20 +43,11 @@ func NewStdFace(face font.Face) *StdFace {
f: &faceWithCache{
f: face,
},
id: nextUniqueID(),
}
s.addr = s
runtime.SetFinalizer(s, finalizeStdFace)
return s
}
func finalizeStdFace(face *StdFace) {
runtime.SetFinalizer(face, nil)
theGlyphImageCache.clear(func(key faceCacheKey) bool {
return key.id == face.id
})
}
func (s *StdFace) copyCheck() {
if s.addr != s {
panic("text: illegal use of non-zero StdFace copied by value")
@ -82,13 +72,6 @@ func (s *StdFace) UnsafeInternal() any {
return s.f.f
}
// faceCacheKey implements Face.
func (s *StdFace) faceCacheKey() faceCacheKey {
return faceCacheKey{
id: s.id,
}
}
// advance implements Face.
func (s *StdFace) advance(text string) float64 {
return fixed26_6ToFloat64(font.MeasureString(s.f, text))
@ -143,7 +126,7 @@ func (s *StdFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int
xoffset: subpixelOffset.X,
// yoffset is always the same if the rune is the same, so this doesn't have to be a key.
}
img := theGlyphImageCache.getOrCreate(s, key, func() *ebiten.Image {
img := s.glyphImageCache.getOrCreate(s, key, func() *ebiten.Image {
return s.glyphImageImpl(r, subpixelOffset, b)
})
imgX := (origin.X + b.Min.X).Floor()

View File

@ -20,7 +20,6 @@ package text
import (
"math"
"strings"
"sync/atomic"
"golang.org/x/image/math/fixed"
@ -37,8 +36,6 @@ type Face interface {
// This is unsafe since this might make internal cache states out of sync.
UnsafeInternal() any
faceCacheKey() faceCacheKey
advance(text string) float64
appendGlyphs(glyphs []Glyph, text string, indexOffset int, originX, originY float64) []Glyph
@ -262,9 +259,3 @@ func CacheGlyphs(text string, face Face) {
}
}
}
var currentUniqueID uint64
func nextUniqueID() uint64 {
return atomic.AddUint64(&currentUniqueID, 1)
}