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 { type glyphImageCacheKey struct {
// id is rune for StdFace, and GID for GoTextFace. // id is rune for StdFace, and GID for GoTextFace.
id uint32 id uint32
@ -60,28 +55,22 @@ type glyphImageCacheEntry struct {
} }
type glyphImageCache struct { type glyphImageCache struct {
cache map[faceCacheKey]map[glyphImageCacheKey]*glyphImageCacheEntry cache map[glyphImageCacheKey]*glyphImageCacheEntry
m sync.Mutex m sync.Mutex
} }
var theGlyphImageCache glyphImageCache
func (g *glyphImageCache) getOrCreate(face Face, key glyphImageCacheKey, create func() *ebiten.Image) *ebiten.Image { func (g *glyphImageCache) getOrCreate(face Face, key glyphImageCacheKey, create func() *ebiten.Image) *ebiten.Image {
g.m.Lock() g.m.Lock()
defer g.m.Unlock() defer g.m.Unlock()
e, ok := g.cache[face.faceCacheKey()][key] e, ok := g.cache[key]
if ok { if ok {
e.atime = now() e.atime = now()
return e.image return e.image
} }
if g.cache == nil { if g.cache == nil {
g.cache = map[faceCacheKey]map[glyphImageCacheKey]*glyphImageCacheEntry{} g.cache = map[glyphImageCacheKey]*glyphImageCacheEntry{}
}
faceCacheKey := face.faceCacheKey()
if g.cache[faceCacheKey] == nil {
g.cache[faceCacheKey] = map[glyphImageCacheKey]*glyphImageCacheEntry{}
} }
img := create() img := create()
@ -95,7 +84,7 @@ func (g *glyphImageCache) getOrCreate(face Face, key glyphImageCacheKey, create
// Keep this until the face is GCed. // Keep this until the face is GCed.
e.atime = infTime e.atime = infTime
} }
g.cache[faceCacheKey][key] = e g.cache[key] = e
// Clean up old entries. // 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 // Even after cleaning up the cache, the number of glyphs might still exceed the soft limit, but
// this is fine. // this is fine.
cacheSoftLimit := 128 * glyphVariationCount(face) cacheSoftLimit := 128 * glyphVariationCount(face)
if len(g.cache[faceCacheKey]) > cacheSoftLimit { if len(g.cache) > cacheSoftLimit {
for key, e := range g.cache[faceCacheKey] { for key, e := range g.cache {
// 60 is an arbitrary number. // 60 is an arbitrary number.
if e.atime >= now()-60 { if e.atime >= now()-60 {
continue continue
} }
delete(g.cache[faceCacheKey], key) delete(g.cache, key)
} }
} }
return img 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 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 { func (g *GoTextFace) outputCacheKey(text string) goTextOutputCacheKey {
return goTextOutputCacheKey{ return goTextOutputCacheKey{
text: text, text: text,
@ -343,7 +335,7 @@ func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Im
yoffset: subpixelOffset.Y, yoffset: subpixelOffset.Y,
variations: g.ensureVariationsString(), 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) return segmentsToImage(glyph.scaledSegments, subpixelOffset, b)
}) })

View File

@ -17,7 +17,6 @@ package text
import ( import (
"bytes" "bytes"
"io" "io"
"runtime"
"sync" "sync"
"github.com/go-text/typesetting/font" "github.com/go-text/typesetting/font"
@ -25,6 +24,8 @@ import (
"github.com/go-text/typesetting/opentype/api" "github.com/go-text/typesetting/opentype/api"
"github.com/go-text/typesetting/shaping" "github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2"
) )
type goTextOutputCacheKey struct { type goTextOutputCacheKey struct {
@ -54,9 +55,9 @@ type goTextOutputCacheValue struct {
// GoTextFaceSource is a source of a GoTextFace. This can be shared by multiple GoTextFace objects. // GoTextFaceSource is a source of a GoTextFace. This can be shared by multiple GoTextFace objects.
type GoTextFaceSource struct { type GoTextFaceSource struct {
f font.Face f font.Face
id uint64
outputCache map[goTextOutputCacheKey]*goTextOutputCacheValue outputCache map[goTextOutputCacheKey]*goTextOutputCacheValue
glyphImageCache map[float64]*glyphImageCache
addr *GoTextFaceSource addr *GoTextFaceSource
@ -95,10 +96,8 @@ func NewGoTextFaceSource(source io.ReadSeeker) (*GoTextFaceSource, error) {
s := &GoTextFaceSource{ s := &GoTextFaceSource{
f: f, f: f,
id: nextUniqueID(),
} }
s.addr = s s.addr = s
runtime.SetFinalizer(s, finalizeGoTextFaceSource)
return s, nil return s, nil
} }
@ -118,22 +117,13 @@ func NewGoTextFaceSourcesFromCollection(source io.ReadSeeker) ([]*GoTextFaceSour
for i, f := range fs { for i, f := range fs {
s := &GoTextFaceSource{ s := &GoTextFaceSource{
f: f, f: f,
id: nextUniqueID(),
} }
s.addr = s s.addr = s
runtime.SetFinalizer(s, finalizeGoTextFaceSource)
sources[i] = s sources[i] = s
} }
return sources, nil 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() { func (g *GoTextFaceSource) copyCheck() {
if g.addr != g { if g.addr != g {
panic("text: illegal use of non-zero GoTextFaceSource copied by value") 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 { func (g *GoTextFaceSource) scale(size float64) float64 {
return size / float64(g.f.Upem()) 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 ( import (
"image" "image"
"runtime"
"unicode/utf8" "unicode/utf8"
"golang.org/x/image/font" "golang.org/x/image/font"
@ -33,7 +32,7 @@ var _ Face = (*StdFace)(nil)
type StdFace struct { type StdFace struct {
f *faceWithCache f *faceWithCache
id uint64 glyphImageCache glyphImageCache
addr *StdFace addr *StdFace
} }
@ -44,20 +43,11 @@ func NewStdFace(face font.Face) *StdFace {
f: &faceWithCache{ f: &faceWithCache{
f: face, f: face,
}, },
id: nextUniqueID(),
} }
s.addr = s s.addr = s
runtime.SetFinalizer(s, finalizeStdFace)
return s 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() { func (s *StdFace) copyCheck() {
if s.addr != s { if s.addr != s {
panic("text: illegal use of non-zero StdFace copied by value") panic("text: illegal use of non-zero StdFace copied by value")
@ -82,13 +72,6 @@ func (s *StdFace) UnsafeInternal() any {
return s.f.f return s.f.f
} }
// faceCacheKey implements Face.
func (s *StdFace) faceCacheKey() faceCacheKey {
return faceCacheKey{
id: s.id,
}
}
// advance implements Face. // advance implements Face.
func (s *StdFace) advance(text string) float64 { func (s *StdFace) advance(text string) float64 {
return fixed26_6ToFloat64(font.MeasureString(s.f, text)) 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, xoffset: subpixelOffset.X,
// yoffset is always the same if the rune is the same, so this doesn't have to be a key. // 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) return s.glyphImageImpl(r, subpixelOffset, b)
}) })
imgX := (origin.X + b.Min.X).Floor() imgX := (origin.X + b.Min.X).Floor()

View File

@ -20,7 +20,6 @@ package text
import ( import (
"math" "math"
"strings" "strings"
"sync/atomic"
"golang.org/x/image/math/fixed" "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. // This is unsafe since this might make internal cache states out of sync.
UnsafeInternal() any UnsafeInternal() any
faceCacheKey() faceCacheKey
advance(text string) float64 advance(text string) float64
appendGlyphs(glyphs []Glyph, text string, indexOffset int, originX, originY float64) []Glyph 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)
}