Skip to content

Commit

Permalink
improved caching and added font context cache
Browse files Browse the repository at this point in the history
  • Loading branch information
tfriedel6 committed Mar 21, 2020
1 parent 9d1e5b3 commit 7faf3cd
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 115 deletions.
7 changes: 0 additions & 7 deletions backend/goglbackend/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package goglbackend
import (
"errors"
"image"
"runtime"
"unsafe"

"github.com/tfriedel6/canvas/backend/backendbase"
Expand Down Expand Up @@ -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
}

Expand Down
7 changes: 0 additions & 7 deletions backend/xmobilebackend/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package xmobilebackend
import (
"errors"
"image"
"runtime"
"unsafe"

"github.com/tfriedel6/canvas/backend/backendbase"
Expand Down Expand Up @@ -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
}

Expand Down
78 changes: 38 additions & 40 deletions canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
57 changes: 28 additions & 29 deletions freetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,9 @@ 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
hinting font.Hinting

fontSize fixed.Int26_6
hinting font.Hinting
// cache is the glyph cache.
cache [nGlyphs * nXFractions * nYFractions]cacheEntry
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -259,36 +245,49 @@ 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
}
c.fontSize = fontSize
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 {
c.cache[i] = cacheEntry{}
}
}

// 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),
}
}
31 changes: 20 additions & 11 deletions images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
img.lastUsed = time.Now()
return img, nil
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:
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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
}

Expand Down
Loading

0 comments on commit 7faf3cd

Please sign in to comment.