improved caching and added font context cache
This commit is contained in:
parent
9d1e5b306a
commit
7faf3cdcc6
6 changed files with 114 additions and 115 deletions
|
@ -3,7 +3,6 @@ package goglbackend
|
|||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tfriedel6/canvas/backend/backendbase"
|
||||
|
@ -39,12 +38,6 @@ func (b *GoGLBackend) LoadImage(src image.Image) (backendbase.Image, error) {
|
|||
}
|
||||
img.b = b
|
||||
|
||||
runtime.SetFinalizer(img, func(img *Image) {
|
||||
b.glChan <- func() {
|
||||
gl.DeleteTextures(1, &img.tex)
|
||||
}
|
||||
})
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ package xmobilebackend
|
|||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tfriedel6/canvas/backend/backendbase"
|
||||
|
@ -39,12 +38,6 @@ func (b *XMobileBackend) LoadImage(src image.Image) (backendbase.Image, error) {
|
|||
}
|
||||
img.b = b
|
||||
|
||||
runtime.SetFinalizer(img, func(img *Image) {
|
||||
b.glChan <- func() {
|
||||
b.glctx.DeleteTexture(img.tex)
|
||||
}
|
||||
})
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
|
|
78
canvas.go
78
canvas.go
|
@ -5,11 +5,13 @@ package canvas
|
|||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"sort"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/tfriedel6/canvas/backend/backendbase"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
//go:generate go run make_shaders.go
|
||||
|
@ -27,7 +29,7 @@ type Canvas struct {
|
|||
|
||||
images map[interface{}]*Image
|
||||
fonts map[interface{}]*Font
|
||||
fontCtxs map[fontKey]*frContext
|
||||
fontCtxs map[fontKey]*frCache
|
||||
|
||||
shadowBuf [][2]float64
|
||||
}
|
||||
|
@ -37,7 +39,7 @@ type drawState struct {
|
|||
fill drawStyle
|
||||
stroke drawStyle
|
||||
font *Font
|
||||
fontSize float64
|
||||
fontSize fixed.Int26_6
|
||||
fontMetrics font.Metrics
|
||||
textAlign textAlign
|
||||
textBaseline textBaseline
|
||||
|
@ -127,7 +129,7 @@ var Performance = struct {
|
|||
// CacheSize is only approximate
|
||||
CacheSize int
|
||||
}{
|
||||
CacheSize: 16_000_000,
|
||||
CacheSize: 128_000_000,
|
||||
}
|
||||
|
||||
// New creates a new canvas with the given viewport coordinates.
|
||||
|
@ -140,6 +142,7 @@ func New(backend backendbase.Backend) *Canvas {
|
|||
stateStack: make([]drawState, 0, 20),
|
||||
images: make(map[interface{}]*Image),
|
||||
fonts: make(map[interface{}]*Font),
|
||||
fontCtxs: make(map[fontKey]*frCache),
|
||||
}
|
||||
cv.state.lineWidth = 1
|
||||
cv.state.lineAlpha = 1
|
||||
|
@ -287,31 +290,12 @@ func (cv *Canvas) SetLineWidth(width float64) {
|
|||
// with the LoadFont function, a filename for a font to load (which will be
|
||||
// cached), or nil, in which case the first loaded font will be used
|
||||
func (cv *Canvas) SetFont(src interface{}, size float64) {
|
||||
cv.state.fontSize = fixed.Int26_6(math.Round(size * 64))
|
||||
if src == nil {
|
||||
cv.state.font = defaultFont
|
||||
} else {
|
||||
cv.state.font = cv.getFont(src)
|
||||
// switch v := src.(type) {
|
||||
// case *Font:
|
||||
// cv.state.font = v
|
||||
// case *truetype.Font:
|
||||
// cv.state.font = &Font{font: v}
|
||||
// case string:
|
||||
// if f, ok := fonts[v]; ok {
|
||||
// cv.state.font = f
|
||||
// } else {
|
||||
// f, err := cv.LoadFont(v)
|
||||
// if err != nil {
|
||||
// fmt.Fprintf(os.Stderr, "Error loading font %s: %v\n", v, err)
|
||||
// fonts[v] = nil
|
||||
// } else {
|
||||
// fonts[v] = f
|
||||
// cv.state.font = f
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
cv.state.fontSize = size
|
||||
|
||||
fontFace := truetype.NewFace(cv.state.font.font, &truetype.Options{Size: size})
|
||||
cv.state.fontMetrics = fontFace.Metrics()
|
||||
|
@ -487,28 +471,42 @@ func (cv *Canvas) IsPointInStroke(x, y float64) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (cv *Canvas) reduceCache(keepSize int) {
|
||||
func (cv *Canvas) reduceCache(keepSize, rec int) {
|
||||
if rec > 100 {
|
||||
return
|
||||
}
|
||||
|
||||
var total int
|
||||
for _, img := range cv.images {
|
||||
oldest := time.Now()
|
||||
var oldestFontKey fontKey
|
||||
var oldestImageKey interface{}
|
||||
for src, img := range cv.images {
|
||||
w, h := img.img.Size()
|
||||
total += w * h * 4
|
||||
if img.lastUsed.Before(oldest) {
|
||||
oldest = img.lastUsed
|
||||
oldestImageKey = src
|
||||
}
|
||||
}
|
||||
for key, frctx := range cv.fontCtxs {
|
||||
total += frctx.ctx.cacheSize()
|
||||
if frctx.lastUsed.Before(oldest) {
|
||||
oldest = frctx.lastUsed
|
||||
oldestFontKey = key
|
||||
oldestImageKey = nil
|
||||
}
|
||||
}
|
||||
if total <= keepSize {
|
||||
return
|
||||
}
|
||||
list := make([]*Image, 0, len(cv.images))
|
||||
for _, img := range cv.images {
|
||||
list = append(list, img)
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
return list[i].lastUsed.Before(list[j].lastUsed)
|
||||
})
|
||||
pos := 0
|
||||
for total > keepSize {
|
||||
img := list[pos]
|
||||
pos++
|
||||
delete(cv.images, img.src)
|
||||
w, h := img.img.Size()
|
||||
total -= w * h * 4
|
||||
|
||||
if oldestImageKey != nil {
|
||||
cv.images[oldestImageKey].Delete()
|
||||
delete(cv.images, oldestImageKey)
|
||||
} else {
|
||||
cv.fontCtxs[oldestFontKey].ctx = nil
|
||||
delete(cv.fontCtxs, oldestFontKey)
|
||||
}
|
||||
|
||||
cv.reduceCache(keepSize, rec+1)
|
||||
}
|
||||
|
|
55
freetype.go
55
freetype.go
|
@ -44,10 +44,8 @@ type frContext struct {
|
|||
glyphBuf truetype.GlyphBuf
|
||||
// dst and src are the destination and source images for drawing.
|
||||
dst draw.Image
|
||||
// fontSize and dpi are used to calculate scale. scale is the number of
|
||||
// 26.6 fixed point units in 1 em. hinting is the hinting policy.
|
||||
fontSize, dpi float64
|
||||
scale fixed.Int26_6
|
||||
|
||||
fontSize fixed.Int26_6
|
||||
hinting font.Hinting
|
||||
// cache is the glyph cache.
|
||||
cache [nGlyphs * nXFractions * nYFractions]cacheEntry
|
||||
|
@ -132,7 +130,7 @@ func (c *frContext) drawContour(ps []truetype.Point, dx, dy fixed.Int26_6) {
|
|||
// The 26.6 fixed point arguments fx and fy must be in the range [0, 1).
|
||||
func (c *frContext) rasterize(glyph truetype.Index, fx, fy fixed.Int26_6) (fixed.Int26_6, *image.Alpha, image.Point, error) {
|
||||
|
||||
if err := c.glyphBuf.Load(c.f, c.scale, glyph, c.hinting); err != nil {
|
||||
if err := c.glyphBuf.Load(c.f, c.fontSize, glyph, c.hinting); err != nil {
|
||||
return 0, nil, image.Point{}, err
|
||||
}
|
||||
// Calculate the integer-pixel bounds for the glyph.
|
||||
|
@ -190,14 +188,14 @@ func (c *frContext) glyph(glyph truetype.Index, p fixed.Point26_6) (fixed.Int26_
|
|||
}
|
||||
|
||||
func (c *frContext) glyphAdvance(glyph truetype.Index) (fixed.Int26_6, error) {
|
||||
if err := c.glyphBuf.Load(c.f, c.scale, glyph, c.hinting); err != nil {
|
||||
if err := c.glyphBuf.Load(c.f, c.fontSize, glyph, c.hinting); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return c.glyphBuf.AdvanceWidth, nil
|
||||
}
|
||||
|
||||
func (c *frContext) glyphMeasure(glyph truetype.Index, p fixed.Point26_6) (fixed.Int26_6, image.Rectangle, error) {
|
||||
if err := c.glyphBuf.Load(c.f, c.scale, glyph, c.hinting); err != nil {
|
||||
if err := c.glyphBuf.Load(c.f, c.fontSize, glyph, c.hinting); err != nil {
|
||||
return 0, image.Rectangle{}, err
|
||||
}
|
||||
|
||||
|
@ -221,15 +219,12 @@ func (c *frContext) glyphBounds(glyph truetype.Index, p fixed.Point26_6) (image.
|
|||
|
||||
const maxInt = int(^uint(0) >> 1)
|
||||
|
||||
// recalc recalculates scale and bounds values from the font size, screen
|
||||
// resolution and font metrics, and invalidates the glyph cache.
|
||||
func (c *frContext) recalc() {
|
||||
c.scale = fixed.Int26_6(c.fontSize * c.dpi * (64.0 / 72.0))
|
||||
if c.f == nil {
|
||||
c.r.SetBounds(0, 0)
|
||||
} else {
|
||||
// Set the rasterizer's bounds to be big enough to handle the largest glyph.
|
||||
b := c.f.Bounds(c.scale)
|
||||
b := c.f.Bounds(c.fontSize)
|
||||
xmin := +int(b.Min.X) >> 6
|
||||
ymin := -int(b.Max.Y) >> 6
|
||||
xmax := +int(b.Max.X+63) >> 6
|
||||
|
@ -241,16 +236,7 @@ func (c *frContext) recalc() {
|
|||
}
|
||||
}
|
||||
|
||||
// SetDPI sets the screen resolution in dots per inch.
|
||||
func (c *frContext) setDPI(dpi float64) {
|
||||
if c.dpi == dpi {
|
||||
return
|
||||
}
|
||||
c.dpi = dpi
|
||||
c.recalc()
|
||||
}
|
||||
|
||||
// SetFont sets the font used to draw text.
|
||||
// setFont sets the font used to draw text.
|
||||
func (c *frContext) setFont(f *truetype.Font) {
|
||||
if c.f == f {
|
||||
return
|
||||
|
@ -259,8 +245,8 @@ func (c *frContext) setFont(f *truetype.Font) {
|
|||
c.recalc()
|
||||
}
|
||||
|
||||
// SetFontSize sets the font size in points (as in "a 12 point font").
|
||||
func (c *frContext) setFontSize(fontSize float64) {
|
||||
// setFontSize sets the font size in points (as in "a 12 point font").
|
||||
func (c *frContext) setFontSize(fontSize fixed.Int26_6) {
|
||||
if c.fontSize == fontSize {
|
||||
return
|
||||
}
|
||||
|
@ -268,7 +254,7 @@ func (c *frContext) setFontSize(fontSize float64) {
|
|||
c.recalc()
|
||||
}
|
||||
|
||||
// SetHinting sets the hinting policy.
|
||||
// setHinting sets the hinting policy.
|
||||
func (c *frContext) setHinting(hinting font.Hinting) {
|
||||
c.hinting = hinting
|
||||
for i := range c.cache {
|
||||
|
@ -276,19 +262,32 @@ func (c *frContext) setHinting(hinting font.Hinting) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetDst sets the destination image for draw operations.
|
||||
// setDst sets the destination image for draw operations.
|
||||
func (c *frContext) setDst(dst draw.Image) {
|
||||
c.dst = dst
|
||||
}
|
||||
|
||||
func (c *frContext) cacheSize() int {
|
||||
if c.f == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
b := c.f.Bounds(c.fontSize)
|
||||
xmin := +int(b.Min.X) >> 6
|
||||
ymin := -int(b.Max.Y) >> 6
|
||||
xmax := +int(b.Max.X+63) >> 6
|
||||
ymax := -int(b.Min.Y-63) >> 6
|
||||
w := xmax - xmin
|
||||
h := ymax - ymin
|
||||
return w * h * len(c.cache)
|
||||
}
|
||||
|
||||
// TODO(nigeltao): implement Context.SetGamma.
|
||||
|
||||
// NewContext creates a new Context.
|
||||
func newFRContext() *frContext {
|
||||
return &frContext{
|
||||
r: raster.NewRasterizer(0, 0),
|
||||
fontSize: 12,
|
||||
dpi: 72,
|
||||
scale: 12 << 6,
|
||||
fontSize: fixed.I(12),
|
||||
}
|
||||
}
|
||||
|
|
27
images.go
27
images.go
|
@ -26,16 +26,22 @@ type Image struct {
|
|||
// string. If you want the canvas package to load the image, make sure you
|
||||
// import the required format packages
|
||||
func (cv *Canvas) LoadImage(src interface{}) (*Image, error) {
|
||||
var reload *Image
|
||||
if img, ok := src.(*Image); ok {
|
||||
if img.deleted {
|
||||
reload = img
|
||||
src = img.src
|
||||
} else {
|
||||
img.lastUsed = time.Now()
|
||||
return img, nil
|
||||
}
|
||||
} else if _, ok := src.([]byte); !ok {
|
||||
if img, ok := cv.images[src]; ok {
|
||||
img.lastUsed = time.Now()
|
||||
return img, nil
|
||||
}
|
||||
}
|
||||
cv.reduceCache(Performance.CacheSize)
|
||||
cv.reduceCache(Performance.CacheSize, 0)
|
||||
var srcImg image.Image
|
||||
switch v := src.(type) {
|
||||
case image.Image:
|
||||
|
@ -66,6 +72,10 @@ func (cv *Canvas) LoadImage(src interface{}) (*Image, error) {
|
|||
return nil, err
|
||||
}
|
||||
cvimg := &Image{cv: cv, img: backendImg, lastUsed: time.Now(), src: src}
|
||||
if reload != nil {
|
||||
*reload = *cvimg
|
||||
return reload, nil
|
||||
}
|
||||
if _, ok := src.([]byte); !ok {
|
||||
cv.images[src] = cvimg
|
||||
}
|
||||
|
@ -101,8 +111,6 @@ func (cv *Canvas) getImage(src interface{}) *Image {
|
|||
default:
|
||||
fmt.Fprintf(os.Stderr, "Failed to load image: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
cv.images[src] = img
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
@ -116,12 +124,14 @@ func (img *Image) Height() int { return img.img.Height() }
|
|||
// Size returns the width and height of the image
|
||||
func (img *Image) Size() (int, int) { return img.img.Size() }
|
||||
|
||||
// Delete deletes the image from memory. Any draw calls with a deleted image
|
||||
// will not do anything
|
||||
// Delete deletes the image from memory
|
||||
func (img *Image) Delete() {
|
||||
img.img.Delete()
|
||||
img.img = nil
|
||||
if img == nil || img.deleted {
|
||||
return
|
||||
}
|
||||
img.deleted = true
|
||||
img.img.Delete()
|
||||
delete(img.cv.images, img.src)
|
||||
}
|
||||
|
||||
// Replace replaces the image with the new one
|
||||
|
@ -147,8 +157,7 @@ func (img *Image) Replace(src interface{}) error {
|
|||
// source coordinates
|
||||
func (cv *Canvas) DrawImage(image interface{}, coords ...float64) {
|
||||
img := cv.getImage(image)
|
||||
|
||||
if img == nil || img.deleted {
|
||||
if img == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
49
text.go
49
text.go
|
@ -7,6 +7,7 @@ import (
|
|||
"image/draw"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang/freetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
|
@ -14,8 +15,6 @@ import (
|
|||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
var fontRenderingContext = newFRContext()
|
||||
|
||||
// Font is a loaded font that can be passed to the
|
||||
// SetFont method
|
||||
type Font struct {
|
||||
|
@ -27,9 +26,9 @@ type fontKey struct {
|
|||
size fixed.Int26_6
|
||||
}
|
||||
|
||||
type fontCache struct {
|
||||
font *Font
|
||||
frctx *frContext
|
||||
type frCache struct {
|
||||
ctx *frContext
|
||||
lastUsed time.Time
|
||||
}
|
||||
|
||||
var zeroes [alphaTexSize]byte
|
||||
|
@ -92,20 +91,32 @@ func (cv *Canvas) getFont(src interface{}) *Font {
|
|||
return f
|
||||
}
|
||||
|
||||
// func (cv *Canvas) getFRContext(src interface{}, size float64) *Font {
|
||||
func (cv *Canvas) getFRContext(font *Font, size fixed.Int26_6) *frContext {
|
||||
k := fontKey{font: font, size: size}
|
||||
if frctx, ok := cv.fontCtxs[k]; ok {
|
||||
frctx.lastUsed = time.Now()
|
||||
return frctx.ctx
|
||||
}
|
||||
|
||||
// }
|
||||
cv.reduceCache(Performance.CacheSize, 0)
|
||||
frctx := newFRContext()
|
||||
frctx.fontSize = k.size
|
||||
frctx.f = k.font.font
|
||||
frctx.recalc()
|
||||
|
||||
cv.fontCtxs[k] = &frCache{ctx: frctx, lastUsed: time.Now()}
|
||||
|
||||
return frctx
|
||||
}
|
||||
|
||||
// FillText draws the given string at the given coordinates
|
||||
// using the currently set font and font height
|
||||
func (cv *Canvas) FillText(str string, x, y float64) {
|
||||
if cv.state.font == nil {
|
||||
if cv.state.font.font == nil {
|
||||
return
|
||||
}
|
||||
|
||||
frc := fontRenderingContext
|
||||
frc.setFont(cv.state.font.font)
|
||||
frc.setFontSize(cv.state.fontSize)
|
||||
frc := cv.getFRContext(cv.state.font, cv.state.fontSize)
|
||||
fnt := cv.state.font.font
|
||||
|
||||
curX := x
|
||||
|
@ -127,7 +138,7 @@ func (cv *Canvas) FillText(str string, x, y float64) {
|
|||
continue
|
||||
}
|
||||
if hasPrev {
|
||||
kern := fnt.Kern(frc.scale, prev, idx)
|
||||
kern := fnt.Kern(frc.fontSize, prev, idx)
|
||||
if frc.hinting != font.HintingNone {
|
||||
kern = (kern + 32) &^ 63
|
||||
}
|
||||
|
@ -207,7 +218,7 @@ func (cv *Canvas) FillText(str string, x, y float64) {
|
|||
continue
|
||||
}
|
||||
if hasPrev {
|
||||
kern := fnt.Kern(frc.scale, prev, idx)
|
||||
kern := fnt.Kern(frc.fontSize, prev, idx)
|
||||
if frc.hinting != font.HintingNone {
|
||||
kern = (kern + 32) &^ 63
|
||||
}
|
||||
|
@ -267,9 +278,7 @@ func (cv *Canvas) StrokeText(str string, x, y float64) {
|
|||
return
|
||||
}
|
||||
|
||||
frc := fontRenderingContext
|
||||
frc.setFont(cv.state.font.font)
|
||||
frc.setFontSize(float64(cv.state.fontSize))
|
||||
frc := cv.getFRContext(cv.state.font, cv.state.fontSize)
|
||||
fnt := cv.state.font.font
|
||||
|
||||
prevPath := cv.path
|
||||
|
@ -296,7 +305,7 @@ func (cv *Canvas) StrokeText(str string, x, y float64) {
|
|||
|
||||
func (cv *Canvas) runePath(rn rune, pos vec) {
|
||||
gb := &truetype.GlyphBuf{}
|
||||
gb.Load(cv.state.font.font, fixed.Int26_6(cv.state.fontSize*64), cv.state.font.font.Index(rn), font.HintingNone)
|
||||
gb.Load(cv.state.font.font, cv.state.fontSize, cv.state.font.font.Index(rn), font.HintingNone)
|
||||
|
||||
from := 0
|
||||
for _, to := range gb.Ends {
|
||||
|
@ -376,9 +385,7 @@ func (cv *Canvas) MeasureText(str string) TextMetrics {
|
|||
return TextMetrics{}
|
||||
}
|
||||
|
||||
frc := fontRenderingContext
|
||||
frc.setFont(cv.state.font.font)
|
||||
frc.setFontSize(float64(cv.state.fontSize))
|
||||
frc := cv.getFRContext(cv.state.font, cv.state.fontSize)
|
||||
fnt := cv.state.font.font
|
||||
|
||||
var p fixed.Point26_6
|
||||
|
@ -394,7 +401,7 @@ func (cv *Canvas) MeasureText(str string) TextMetrics {
|
|||
continue
|
||||
}
|
||||
if hasPrev {
|
||||
kern := fnt.Kern(frc.scale, prev, idx)
|
||||
kern := fnt.Kern(frc.fontSize, prev, idx)
|
||||
if frc.hinting != font.HintingNone {
|
||||
kern = (kern + 32) &^ 63
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue