Skip to content

Commit

Permalink
text/v2: refactoring: unify a cache struct
Browse files Browse the repository at this point in the history
  • Loading branch information
hajimehoshi committed Oct 27, 2024
1 parent 41e8d06 commit d19a774
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 150 deletions.
106 changes: 106 additions & 0 deletions text/v2/cache.go
Original file line number Diff line number Diff line change
@@ -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 changes: 0 additions & 103 deletions text/v2/glyph.go

This file was deleted.

5 changes: 3 additions & 2 deletions text/v2/gotext.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
55 changes: 16 additions & 39 deletions text/v2/gotextfacesource.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"bytes"
"io"
"slices"
"sync"

"github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/font/opentype"
Expand Down Expand Up @@ -50,7 +49,6 @@ type glyph struct {
type goTextOutputCacheValue struct {
outputs []shaping.Output
glyphs []glyph
atime int64
}

type goTextGlyphImageCacheKey struct {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -254,42 +254,19 @@ 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
}

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)
}
Expand Down
11 changes: 5 additions & 6 deletions text/v2/gox.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type goXFaceGlyphImageCacheKey struct {
type GoXFace struct {
f *faceWithCache

glyphImageCache *glyphImageCache[goXFaceGlyphImageCacheKey]
glyphImageCache *cache[goXFaceGlyphImageCacheKey, *ebiten.Image]

cachedMetrics Metrics

Expand All @@ -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
}

Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit d19a774

Please sign in to comment.