text: Utilize shared textures

Fixes #529
This commit is contained in:
Hajime Hoshi 2018-03-03 21:53:09 +09:00
parent 7da65d64be
commit c581219bb5

View File

@ -27,7 +27,6 @@ import (
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
emath "github.com/hajimehoshi/ebiten/internal/math"
"github.com/hajimehoshi/ebiten/internal/sync" "github.com/hajimehoshi/ebiten/internal/sync"
) )
@ -40,74 +39,14 @@ func now() int64 {
return monotonicClock return monotonicClock
} }
type cacheEntry struct {
// Use pointers to avoid copying on browsers.
bounds map[rune]*fixed.Rectangle26_6
atlases map[int]*atlas
}
var (
cache = map[font.Face]*cacheEntry{}
)
type char struct {
face font.Face
rune rune
atlasG int
}
func (c *char) bounds() *fixed.Rectangle26_6 {
e := cache[c.face]
if b, ok := e.bounds[c.rune]; ok {
return b
}
b, _, _ := c.face.GlyphBounds(c.rune)
e.bounds[c.rune] = &b
return &b
}
func (c *char) size() (fixed.Int26_6, fixed.Int26_6) {
b := c.bounds()
return b.Max.X - b.Min.X, b.Max.Y - b.Min.Y
}
func (c *char) empty() bool {
x, y := c.size()
return x == 0 || y == 0
}
func (c *char) atlasGroup() int {
if c.atlasG != 0 {
return c.atlasG
}
x, y := c.size()
w, h := x.Ceil(), y.Ceil()
t := w
if t < h {
t = h
}
// Different images for small runes are inefficient.
// Let's use a same texture atlas for typical character sizes.
if t < 32 {
return 32
}
c.atlasG = emath.NextPowerOf2Int(t)
return c.atlasG
}
type glyph struct {
char char
index int
atime int64
}
func fixed26_6ToFloat64(x fixed.Int26_6) float64 { func fixed26_6ToFloat64(x fixed.Int26_6) float64 {
return float64(x) / (1 << 6) return float64(x) / (1 << 6)
} }
const (
cacheLimit = 512 // This is an arbitrary number.
)
type colorMCacheKey uint32 type colorMCacheKey uint32
type colorMCacheEntry struct { type colorMCacheEntry struct {
@ -119,7 +58,7 @@ var (
colorMCache = map[colorMCacheKey]*colorMCacheEntry{} colorMCache = map[colorMCacheKey]*colorMCacheEntry{}
) )
func (g *glyph) draw(dst *ebiten.Image, x, y fixed.Int26_6, clr color.Color) { func drawGlyph(dst *ebiten.Image, face font.Face, r rune, x, y fixed.Int26_6, clr color.Color) {
// RGBA() is in [0 - 0xffff]. Adjust them in [0 - 0xff]. // RGBA() is in [0 - 0xffff]. Adjust them in [0 - 0xff].
cr, cg, cb, ca := clr.RGBA() cr, cg, cb, ca := clr.RGBA()
cr >>= 8 cr >>= 8
@ -130,7 +69,12 @@ func (g *glyph) draw(dst *ebiten.Image, x, y fixed.Int26_6, clr color.Color) {
return return
} }
b := g.char.bounds() img := getGlyphImage(face, r)
if img == nil {
return
}
b := getGlyphBounds(face, r)
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(fixed26_6ToFloat64(x+b.Min.X), fixed26_6ToFloat64(y+b.Min.Y)) op.GeoM.Translate(fixed26_6ToFloat64(x+b.Min.X), fixed26_6ToFloat64(y+b.Min.Y))
@ -139,17 +83,18 @@ func (g *glyph) draw(dst *ebiten.Image, x, y fixed.Int26_6, clr color.Color) {
if ok { if ok {
e.atime = now() e.atime = now()
} else { } else {
if len(colorMCache) >= 256 { if len(colorMCache) > cacheLimit {
var oldest colorMCacheKey oldest := int64(math.MaxInt64)
t := int64(math.MaxInt64) oldestKey := colorMCacheKey(0)
for key, e := range colorMCache { for key, c := range colorMCache {
if e.atime < t { if c.atime < oldest {
t = g.atime oldestKey = key
oldest = key oldest = c.atime
} }
} }
delete(colorMCache, oldest) delete(colorMCache, oldestKey)
} }
cm := ebiten.ColorM{} cm := ebiten.ColorM{}
rf := float64(cr) / float64(ca) rf := float64(cr) / float64(ca)
gf := float64(cg) / float64(ca) gf := float64(cg) / float64(ca)
@ -164,164 +109,108 @@ func (g *glyph) draw(dst *ebiten.Image, x, y fixed.Int26_6, clr color.Color) {
} }
op.ColorM = e.m op.ColorM = e.m
a := cache[g.char.face].atlases[g.char.atlasGroup()] _ = dst.DrawImage(img, op)
sx, sy := a.at(g)
r := image.Rect(sx, sy, sx+a.glyphSize, sy+a.glyphSize)
op.SourceRect = &r
dst.DrawImage(a.image, op)
} }
type atlas struct { var (
// image is the back-end image to hold glyph cache. fontFaces = map[font.Face]struct{}{}
image *ebiten.Image )
// tmpImage is the temporary image as a renderer source for glyph.
tmpImage *ebiten.Image
// glyphSize is the size of one glyph in the cache.
// This value is always power of 2.
glyphSize int
runeToGlyph map[rune]*glyph
}
func (a *atlas) at(glyph *glyph) (int, int) {
if a.glyphSize != glyph.char.atlasGroup() {
panic("not reached")
}
w, _ := a.image.Size()
xnum := w / a.glyphSize
x, y := glyph.index%xnum, glyph.index/xnum
return x * a.glyphSize, y * a.glyphSize
}
func (a *atlas) maxGlyphNum() int {
w, h := a.image.Size()
xnum := w / a.glyphSize
ynum := h / a.glyphSize
return xnum * ynum
}
func (a *atlas) appendGlyph(char char, now int64) *glyph {
g := &glyph{
char: char,
atime: now,
}
if len(a.runeToGlyph) == a.maxGlyphNum() {
var oldest *glyph
t := int64(math.MaxInt64)
for _, g := range a.runeToGlyph {
if g.atime < t {
t = g.atime
oldest = g
}
}
if oldest == nil {
panic("not reached")
}
idx := oldest.index
delete(a.runeToGlyph, oldest.char.rune)
g.index = idx
} else {
g.index = len(a.runeToGlyph)
}
a.runeToGlyph[g.char.rune] = g
a.draw(g)
return g
}
func (a *atlas) draw(glyph *glyph) {
if a.tmpImage == nil {
a.tmpImage, _ = ebiten.NewImage(a.glyphSize, a.glyphSize, ebiten.FilterNearest)
}
dst := image.NewRGBA(image.Rect(0, 0, a.glyphSize, a.glyphSize))
d := font.Drawer{
Dst: dst,
Src: image.White,
Face: glyph.char.face,
}
b := glyph.char.bounds()
d.Dot = fixed.Point26_6{-b.Min.X, -b.Min.Y}
d.DrawString(string(glyph.char.rune))
a.tmpImage.ReplacePixels(dst.Pix)
op := &ebiten.DrawImageOptions{}
x, y := a.at(glyph)
op.GeoM.Translate(float64(x), float64(y))
op.CompositeMode = ebiten.CompositeModeCopy
a.image.DrawImage(a.tmpImage, op)
a.tmpImage.Clear()
}
func getGlyphFromCache(face font.Face, r rune, now int64) *glyph {
ch := char{
face: face,
rune: r,
}
at, ok := cache[face].atlases[ch.atlasGroup()]
if ok {
g, ok := at.runeToGlyph[r]
if ok {
g.atime = now
return g
}
}
if ch.empty() {
// The glyph doesn't have its size but might have valid 'advance' parameter
// when ch is e.g. space (U+0020).
return &glyph{
char: ch,
atime: now,
}
}
if at == nil {
// Don't use ebiten.MaxImageSize here.
// It's because the back-end image pixels will be restored from GPU
// whenever a new glyph is rendered on the image, and restoring cost is
// expensive if the image is big.
// The back-end image is updated a temporary image, and the temporary image is
// always cleared after used. This means that there is no clue to restore
// the back-end image without reading from GPU
// (see the package 'restorable' implementation).
//
// TODO: How about making a new function for 'flagile' image?
const size = 1024
i, _ := ebiten.NewImage(size, size, ebiten.FilterNearest)
at = &atlas{
image: i,
glyphSize: ch.atlasGroup(),
runeToGlyph: map[rune]*glyph{},
}
cache[face].atlases[ch.atlasGroup()] = at
}
return at.appendGlyph(ch, now)
}
func uniqFace(f font.Face) font.Face { func uniqFace(f font.Face) font.Face {
if _, ok := cache[f]; ok { if _, ok := fontFaces[f]; ok {
return f return f
} }
// If the (DeepEqual-ly) same font exists, // If the (DeepEqual-ly) same font exists,
// reuse this to avoid to consume a lot of cache (#498). // reuse this to avoid to consume a lot of cache (#498).
for key := range cache { for key := range fontFaces {
if reflect.DeepEqual(key, f) { if reflect.DeepEqual(key, f) {
return key return key
} }
} }
cache[f] = &cacheEntry{ fontFaces[f] = struct{}{}
bounds: map[rune]*fixed.Rectangle26_6{},
atlases: map[int]*atlas{},
}
return f return f
} }
var (
// Use pointers to avoid copying on browsers.
glyphBoundsCache = map[font.Face]map[rune]*fixed.Rectangle26_6{}
)
func getGlyphBounds(face font.Face, r rune) *fixed.Rectangle26_6 {
if _, ok := glyphBoundsCache[face]; !ok {
glyphBoundsCache[face] = map[rune]*fixed.Rectangle26_6{}
}
if b, ok := glyphBoundsCache[face][r]; ok {
return b
}
b, _, _ := face.GlyphBounds(r)
glyphBoundsCache[face][r] = &b
return &b
}
type glyphImageCacheEntry struct {
image *ebiten.Image
atime int64
}
var (
glyphImageCache = map[font.Face]map[rune]*glyphImageCacheEntry{}
emptyGlyphs = map[font.Face]map[rune]struct{}{}
)
func getGlyphImage(face font.Face, r rune) *ebiten.Image {
if _, ok := emptyGlyphs[face]; !ok {
emptyGlyphs[face] = map[rune]struct{}{}
}
if _, ok := glyphImageCache[face]; !ok {
glyphImageCache[face] = map[rune]*glyphImageCacheEntry{}
}
if _, ok := emptyGlyphs[face][r]; ok {
return nil
}
if e, ok := glyphImageCache[face][r]; ok {
e.atime = now()
return e.image
}
b := getGlyphBounds(face, r)
w, h := (b.Max.X - b.Min.X).Ceil(), (b.Max.Y - b.Min.Y).Ceil()
if w == 0 || h == 0 {
emptyGlyphs[face][r] = struct{}{}
return nil
}
if len(glyphImageCache[face]) > cacheLimit {
oldest := int64(math.MaxInt64)
oldestKey := rune(-1)
for r, e := range glyphImageCache[face] {
if e.atime < oldest {
oldestKey = r
oldest = e.atime
}
}
glyphImageCache[face][oldestKey].image.Dispose()
delete(glyphImageCache[face], oldestKey)
}
rgba := image.NewRGBA(image.Rect(0, 0, w, h))
d := font.Drawer{
Dst: rgba,
Src: image.White,
Face: face,
}
d.Dot = fixed.Point26_6{-b.Min.X, -b.Min.Y}
d.DrawString(string(r))
img, _ := ebiten.NewImageFromImage(rgba, ebiten.FilterDefault)
glyphImageCache[face][r] = &glyphImageCacheEntry{
image: img,
atime: now(),
}
return img
}
var textM sync.Mutex var textM sync.Mutex
// Draw draws a given text on a given destination image dst. // Draw draws a given text on a given destination image dst.
@ -338,23 +227,19 @@ var textM sync.Mutex
func Draw(dst *ebiten.Image, text string, face font.Face, x, y int, clr color.Color) { func Draw(dst *ebiten.Image, text string, face font.Face, x, y int, clr color.Color) {
textM.Lock() textM.Lock()
n := now()
fx := fixed.I(x) fx := fixed.I(x)
prevC := rune(-1) prevR := rune(-1)
runes := []rune(text) runes := []rune(text)
for _, c := range runes { for _, r := range runes {
if prevC >= 0 { if prevR >= 0 {
fx += face.Kern(prevC, c) fx += face.Kern(prevR, r)
} }
fa := uniqFace(face) fa := uniqFace(face)
if g := getGlyphFromCache(fa, c, n); g != nil { drawGlyph(dst, fa, r, fx, fixed.I(y), clr)
if !g.char.empty() { fx += glyphAdvance(fa, r)
g.draw(dst, fx, fixed.I(y), clr)
} prevR = r
fx += glyphAdvance(fa, c)
}
prevC = c
} }
textM.Unlock() textM.Unlock()