From d19a774316b41a34b2ab1882b9fdcabc008ef24e Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 27 Oct 2024 21:44:17 +0900 Subject: [PATCH] text/v2: refactoring: unify a cache struct --- text/v2/cache.go | 106 ++++++++++++++++++++++++++++++++++++ text/v2/glyph.go | 103 ----------------------------------- text/v2/gotext.go | 5 +- text/v2/gotextfacesource.go | 55 ++++++------------- text/v2/gox.go | 11 ++-- 5 files changed, 130 insertions(+), 150 deletions(-) create mode 100644 text/v2/cache.go delete mode 100644 text/v2/glyph.go diff --git a/text/v2/cache.go b/text/v2/cache.go new file mode 100644 index 000000000000..a3d218971141 --- /dev/null +++ b/text/v2/cache.go @@ -0,0 +1,106 @@ +// Copyright 2024 The Ebitengine Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package text + +import ( + "math" + "sync" + "sync/atomic" + + "github.com/hajimehoshi/ebiten/v2/internal/hook" +) + +var monotonicClock atomic.Int64 + +const infTime = math.MaxInt64 + +func init() { + hook.AppendHookOnBeforeUpdate(func() error { + monotonicClock.Add(1) + return nil + }) +} + +type cacheValue[Value any] struct { + value Value + + // atime is the last time when the value was accessed. + atime int64 +} + +type cache[Key comparable, Value any] struct { + // softLimit indicates the soft limit of the number of values in the cache. + softLimit int + + values map[Key]*cacheValue[Value] + + // atime is the last time when the cache was accessed. + atime int64 + + m sync.Mutex +} + +func newCache[Key comparable, Value any](softLimit int) *cache[Key, Value] { + return &cache[Key, Value]{ + softLimit: softLimit, + } +} + +func (c *cache[Key, Value]) getOrCreate(key Key, create func() (Value, bool)) Value { + n := monotonicClock.Load() + + c.m.Lock() + defer c.m.Unlock() + + e, ok := c.values[key] + if ok { + e.atime = n + return e.value + } + + if c.values == nil { + c.values = map[Key]*cacheValue[Value]{} + } + + ent, canExpire := create() + e = &cacheValue[Value]{ + value: ent, + atime: infTime, + } + if canExpire { + e.atime = n + } + c.values[key] = e + + // Clean up old entries. + if c.atime < n { + // If the number of values exceeds the soft limits, old values are removed. + // Even after cleaning up the cache, the number of values might still exceed the soft limit, + // but this is fine. + if len(c.values) > c.softLimit { + for key, e := range c.values { + // 60 is an arbitrary number. + if e.atime >= n-60 { + continue + } + delete(c.values, key) + } + } + } + + c.atime = n + + return e.value +} diff --git a/text/v2/glyph.go b/text/v2/glyph.go deleted file mode 100644 index 96053a033095..000000000000 --- a/text/v2/glyph.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2023 The Ebitengine Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package text - -import ( - "math" - "sync" - - "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/internal/hook" -) - -var monotonicClock int64 - -const infTime = math.MaxInt64 - -func now() int64 { - return monotonicClock -} - -func init() { - hook.AppendHookOnBeforeUpdate(func() error { - monotonicClock++ - return nil - }) -} - -type glyphImageCacheEntry struct { - image *ebiten.Image - atime int64 -} - -type glyphImageCache[Key comparable] struct { - cache map[Key]*glyphImageCacheEntry - atime int64 - glyphVariationCount int - - m sync.Mutex -} - -func (g *glyphImageCache[Key]) getOrCreate(key Key, create func() *ebiten.Image) *ebiten.Image { - g.m.Lock() - defer g.m.Unlock() - - n := now() - - e, ok := g.cache[key] - if ok { - e.atime = n - return e.image - } - - if g.cache == nil { - g.cache = map[Key]*glyphImageCacheEntry{} - } - - img := create() - e = &glyphImageCacheEntry{ - image: img, - } - if img != nil { - e.atime = n - } else { - // If the glyph image is nil, the entry doesn't have to be removed. - // Keep this until the face is GCed. - e.atime = infTime - } - g.cache[key] = e - - // Clean up old entries. - if g.atime < n { - // cacheSoftLimit indicates the soft limit of the number of glyphs in the cache. - // If the number of glyphs exceeds this soft limits, old glyphs are removed. - // Even after cleaning up the cache, the number of glyphs might still exceed the soft limit, but - // this is fine. - cacheSoftLimit := 128 * g.glyphVariationCount - 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, key) - } - } - } - - g.atime = n - - return img -} diff --git a/text/v2/gotext.go b/text/v2/gotext.go index db1b4a8df876..60ec4fceeeec 100644 --- a/text/v2/gotext.go +++ b/text/v2/gotext.go @@ -369,8 +369,9 @@ func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Im yoffset: subpixelOffset.Y, variations: g.ensureVariationsString(), } - img := g.Source.getOrCreateGlyphImage(g, key, func() *ebiten.Image { - return segmentsToImage(glyph.scaledSegments, subpixelOffset, b) + img := g.Source.getOrCreateGlyphImage(g, key, func() (*ebiten.Image, bool) { + img := segmentsToImage(glyph.scaledSegments, subpixelOffset, b) + return img, img != nil }) imgX := (origin.X + b.Min.X).Floor() diff --git a/text/v2/gotextfacesource.go b/text/v2/gotextfacesource.go index ddfbd7ead335..8003b4a22b24 100644 --- a/text/v2/gotextfacesource.go +++ b/text/v2/gotextfacesource.go @@ -18,7 +18,6 @@ import ( "bytes" "io" "slices" - "sync" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/font/opentype" @@ -50,7 +49,6 @@ type glyph struct { type goTextOutputCacheValue struct { outputs []shaping.Output glyphs []glyph - atime int64 } type goTextGlyphImageCacheKey struct { @@ -65,14 +63,12 @@ type GoTextFaceSource struct { f *font.Face metadata Metadata - outputCache map[goTextOutputCacheKey]*goTextOutputCacheValue - glyphImageCache map[float64]*glyphImageCache[goTextGlyphImageCacheKey] + outputCache *cache[goTextOutputCacheKey, goTextOutputCacheValue] + glyphImageCache map[float64]*cache[goTextGlyphImageCacheKey, *ebiten.Image] addr *GoTextFaceSource shaper shaping.HarfbuzzShaper - - m sync.Mutex } func toFontResource(source io.Reader) (font.Resource, error) { @@ -115,6 +111,7 @@ func NewGoTextFaceSource(source io.Reader) (*GoTextFaceSource, error) { } s.addr = s s.metadata = metadataFromLoader(l) + s.outputCache = newCache[goTextOutputCacheKey, goTextOutputCacheValue](512) return s, nil } @@ -171,15 +168,18 @@ func (g *GoTextFaceSource) UnsafeInternal() any { func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Output, []glyph) { g.copyCheck() - g.m.Lock() - defer g.m.Unlock() - key := face.outputCacheKey(text) - if out, ok := g.outputCache[key]; ok { - out.atime = now() - return out.outputs, out.glyphs - } + e := g.outputCache.getOrCreate(key, func() (goTextOutputCacheValue, bool) { + outputs, gs := g.shapeImpl(text, face) + return goTextOutputCacheValue{ + outputs: outputs, + glyphs: gs, + }, true + }) + return e.outputs, e.glyphs +} +func (g *GoTextFaceSource) shapeImpl(text string, face *GoTextFace) ([]shaping.Output, []glyph) { f := face.Source.f f.SetVariations(face.variations) @@ -254,27 +254,6 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Outpu }) } } - - if g.outputCache == nil { - g.outputCache = map[goTextOutputCacheKey]*goTextOutputCacheValue{} - } - g.outputCache[key] = &goTextOutputCacheValue{ - outputs: outputs, - glyphs: gs, - atime: now(), - } - - const cacheSoftLimit = 512 - if len(g.outputCache) > cacheSoftLimit { - for key, e := range g.outputCache { - // 60 is an arbitrary number. - if e.atime >= now()-60 { - continue - } - delete(g.outputCache, key) - } - } - return outputs, gs } @@ -282,14 +261,12 @@ func (g *GoTextFaceSource) scale(size float64) float64 { return size / float64(g.f.Upem()) } -func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func() *ebiten.Image) *ebiten.Image { +func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func() (*ebiten.Image, bool)) *ebiten.Image { if g.glyphImageCache == nil { - g.glyphImageCache = map[float64]*glyphImageCache[goTextGlyphImageCacheKey]{} + g.glyphImageCache = map[float64]*cache[goTextGlyphImageCacheKey, *ebiten.Image]{} } if _, ok := g.glyphImageCache[goTextFace.Size]; !ok { - g.glyphImageCache[goTextFace.Size] = &glyphImageCache[goTextGlyphImageCacheKey]{ - glyphVariationCount: glyphVariationCount(goTextFace), - } + g.glyphImageCache[goTextFace.Size] = newCache[goTextGlyphImageCacheKey, *ebiten.Image](128 * glyphVariationCount(goTextFace)) } return g.glyphImageCache[goTextFace.Size].getOrCreate(key, create) } diff --git a/text/v2/gox.go b/text/v2/gox.go index 571b21779dfc..e6c42e60f622 100644 --- a/text/v2/gox.go +++ b/text/v2/gox.go @@ -43,7 +43,7 @@ type goXFaceGlyphImageCacheKey struct { type GoXFace struct { f *faceWithCache - glyphImageCache *glyphImageCache[goXFaceGlyphImageCacheKey] + glyphImageCache *cache[goXFaceGlyphImageCacheKey, *ebiten.Image] cachedMetrics Metrics @@ -59,9 +59,7 @@ func NewGoXFace(face font.Face) *GoXFace { } // Set addr as early as possible. This is necessary for glyphVariationCount. s.addr = s - s.glyphImageCache = &glyphImageCache[goXFaceGlyphImageCacheKey]{ - glyphVariationCount: glyphVariationCount(s), - } + s.glyphImageCache = newCache[goXFaceGlyphImageCacheKey, *ebiten.Image](128 * glyphVariationCount(s)) return s } @@ -174,8 +172,9 @@ func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int rune: r, xoffset: subpixelOffset.X, } - img := s.glyphImageCache.getOrCreate(key, func() *ebiten.Image { - return s.glyphImageImpl(r, subpixelOffset, b) + img := s.glyphImageCache.getOrCreate(key, func() (*ebiten.Image, bool) { + img := s.glyphImageImpl(r, subpixelOffset, b) + return img, img != nil }) imgX := (origin.X + b.Min.X).Floor() imgY := (origin.Y + b.Min.Y).Floor()