mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2025-01-27 19:22:49 +01:00
text/v2: refactoring: unify a cache struct
This commit is contained in:
parent
41e8d063e8
commit
d19a774316
106
text/v2/cache.go
Normal file
106
text/v2/cache.go
Normal file
@ -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
|
||||
}
|
103
text/v2/glyph.go
103
text/v2/glyph.go
@ -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
|
||||
}
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user