fast background
This commit is contained in:
@@ -101,6 +101,8 @@ type GGDrawer struct {
|
|||||||
|
|
||||||
// scratch is a reusable buffer for CopyShift to avoid allocations.
|
// scratch is a reusable buffer for CopyShift to avoid allocations.
|
||||||
scratch *image.RGBA
|
scratch *image.RGBA
|
||||||
|
|
||||||
|
bgCache bgTileCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save stores the current gg state and the current logical clip stack.
|
// Save stores the current gg state and the current logical clip stack.
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fogleman/gg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type benchBgTheme struct {
|
||||||
|
img image.Image
|
||||||
|
anchor BackgroundAnchorMode
|
||||||
|
tileMode BackgroundTileMode
|
||||||
|
scaleMode BackgroundScaleMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t benchBgTheme) ID() string { return "benchbg" }
|
||||||
|
func (t benchBgTheme) Name() string { return "benchbg" }
|
||||||
|
|
||||||
|
func (t benchBgTheme) BackgroundColor() color.Color { return color.RGBA{A: 255} }
|
||||||
|
func (t benchBgTheme) BackgroundImage() image.Image { return t.img }
|
||||||
|
|
||||||
|
func (t benchBgTheme) BackgroundTileMode() BackgroundTileMode { return t.tileMode }
|
||||||
|
func (t benchBgTheme) BackgroundScaleMode() BackgroundScaleMode { return t.scaleMode }
|
||||||
|
func (t benchBgTheme) BackgroundAnchorMode() BackgroundAnchorMode { return t.anchor }
|
||||||
|
|
||||||
|
func (t benchBgTheme) PointStyle() Style {
|
||||||
|
return Style{FillColor: color.RGBA{A: 255}, PointRadiusPx: 2}
|
||||||
|
}
|
||||||
|
func (t benchBgTheme) LineStyle() Style {
|
||||||
|
return Style{StrokeColor: color.RGBA{A: 255}, StrokeWidthPx: 1}
|
||||||
|
}
|
||||||
|
func (t benchBgTheme) CircleStyle() Style {
|
||||||
|
return Style{FillColor: color.RGBA{A: 255}, StrokeColor: color.RGBA{A: 255}, StrokeWidthPx: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t benchBgTheme) PointClassOverride(PointClassID) (StyleOverride, bool) {
|
||||||
|
return StyleOverride{}, false
|
||||||
|
}
|
||||||
|
func (t benchBgTheme) LineClassOverride(LineClassID) (StyleOverride, bool) {
|
||||||
|
return StyleOverride{}, false
|
||||||
|
}
|
||||||
|
func (t benchBgTheme) CircleClassOverride(CircleClassID) (StyleOverride, bool) {
|
||||||
|
return StyleOverride{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRender_IncrementalPan_NoBackground(b *testing.B) {
|
||||||
|
w := NewWorld(600, 600)
|
||||||
|
w.IndexOnViewportChange(1200, 800, 1.0)
|
||||||
|
|
||||||
|
// Some primitives to keep it realistic but not dominant.
|
||||||
|
for i := 0; i < 200; i++ {
|
||||||
|
_, _ = w.AddPoint(float64(i%600), float64((i*7)%600))
|
||||||
|
}
|
||||||
|
w.Reindex()
|
||||||
|
|
||||||
|
dc := gg.NewContext(1200, 800)
|
||||||
|
drawer := &GGDrawer{DC: dc}
|
||||||
|
|
||||||
|
params := RenderParams{
|
||||||
|
ViewportWidthPx: 1000,
|
||||||
|
ViewportHeightPx: 700,
|
||||||
|
MarginXPx: 250,
|
||||||
|
MarginYPx: 175,
|
||||||
|
CameraXWorldFp: 300 * SCALE,
|
||||||
|
CameraYWorldFp: 300 * SCALE,
|
||||||
|
CameraZoom: 1.0,
|
||||||
|
Options: &RenderOptions{
|
||||||
|
Incremental: &IncrementalPolicy{
|
||||||
|
AllowShiftOnly: false,
|
||||||
|
CoalesceUpdates: false,
|
||||||
|
MaxCatchUpAreaPx: 0,
|
||||||
|
RenderBudgetMs: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial render (commit state).
|
||||||
|
_ = w.Render(drawer, params)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
params.CameraXWorldFp += 1 * SCALE
|
||||||
|
_ = w.Render(drawer, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRender_IncrementalPan_BackgroundRepeat_WorldAnchor_ScaleNone(b *testing.B) {
|
||||||
|
benchRenderBg(b, BackgroundAnchorWorld, BackgroundTileRepeat, BackgroundScaleNone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRender_IncrementalPan_BackgroundRepeat_WorldAnchor_ScaleFit(b *testing.B) {
|
||||||
|
benchRenderBg(b, BackgroundAnchorWorld, BackgroundTileRepeat, BackgroundScaleFit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRender_IncrementalPan_BackgroundRepeat_ViewportAnchor_ScaleNone(b *testing.B) {
|
||||||
|
benchRenderBg(b, BackgroundAnchorViewport, BackgroundTileRepeat, BackgroundScaleNone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchRenderBg(b *testing.B, anchor BackgroundAnchorMode, tile BackgroundTileMode, scale BackgroundScaleMode) {
|
||||||
|
w := NewWorld(600, 600)
|
||||||
|
w.IndexOnViewportChange(1200, 800, 1.0)
|
||||||
|
|
||||||
|
for i := 0; i < 200; i++ {
|
||||||
|
_, _ = w.AddPoint(float64(i%600), float64((i*7)%600))
|
||||||
|
}
|
||||||
|
w.Reindex()
|
||||||
|
|
||||||
|
// Background tile (RGBA) — typical texture size.
|
||||||
|
bg := image.NewRGBA(image.Rect(0, 0, 96, 96))
|
||||||
|
// Make it semi-transparent so draw.Over has real work.
|
||||||
|
for y := 0; y < 96; y++ {
|
||||||
|
for x := 0; x < 96; x++ {
|
||||||
|
bg.SetRGBA(x, y, color.RGBA{R: 255, G: 255, B: 255, A: 18})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.SetTheme(benchBgTheme{img: bg, anchor: anchor, tileMode: tile, scaleMode: scale})
|
||||||
|
|
||||||
|
dc := gg.NewContext(1200, 800)
|
||||||
|
drawer := &GGDrawer{DC: dc}
|
||||||
|
|
||||||
|
params := RenderParams{
|
||||||
|
ViewportWidthPx: 1000,
|
||||||
|
ViewportHeightPx: 700,
|
||||||
|
MarginXPx: 250,
|
||||||
|
MarginYPx: 175,
|
||||||
|
CameraXWorldFp: 300 * SCALE,
|
||||||
|
CameraYWorldFp: 300 * SCALE,
|
||||||
|
CameraZoom: 1.0,
|
||||||
|
Options: &RenderOptions{
|
||||||
|
Incremental: &IncrementalPolicy{
|
||||||
|
AllowShiftOnly: false,
|
||||||
|
CoalesceUpdates: false,
|
||||||
|
MaxCatchUpAreaPx: 0,
|
||||||
|
RenderBudgetMs: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = w.Render(drawer, params)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
params.CameraXWorldFp += 1 * SCALE
|
||||||
|
_ = w.Render(drawer, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/draw"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bgTileCacheKey struct {
|
||||||
|
imgPtr uintptr
|
||||||
|
scaleMode BackgroundScaleMode
|
||||||
|
canvasW int
|
||||||
|
canvasH int
|
||||||
|
srcW int
|
||||||
|
srcH int
|
||||||
|
}
|
||||||
|
|
||||||
|
type bgTileCache struct {
|
||||||
|
key bgTileCacheKey
|
||||||
|
valid bool
|
||||||
|
scaledTile *image.RGBA
|
||||||
|
tileW int
|
||||||
|
tileH int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GGDrawer) drawBackgroundFast(w *World, params RenderParams, rect RectPx) bool {
|
||||||
|
th := w.Theme()
|
||||||
|
bgImg := th.BackgroundImage()
|
||||||
|
if bgImg == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
dst, ok := g.DC.Image().(*image.RGBA)
|
||||||
|
if !ok || dst == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasW := params.CanvasWidthPx()
|
||||||
|
canvasH := params.CanvasHeightPx()
|
||||||
|
|
||||||
|
// Clamp rect to canvas.
|
||||||
|
if rect.W <= 0 || rect.H <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if rect.X < 0 {
|
||||||
|
rect.W += rect.X
|
||||||
|
rect.X = 0
|
||||||
|
}
|
||||||
|
if rect.Y < 0 {
|
||||||
|
rect.H += rect.Y
|
||||||
|
rect.Y = 0
|
||||||
|
}
|
||||||
|
if rect.X+rect.W > canvasW {
|
||||||
|
rect.W = canvasW - rect.X
|
||||||
|
}
|
||||||
|
if rect.Y+rect.H > canvasH {
|
||||||
|
rect.H = canvasH - rect.Y
|
||||||
|
}
|
||||||
|
if rect.W <= 0 || rect.H <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
imgB := bgImg.Bounds()
|
||||||
|
srcW := imgB.Dx()
|
||||||
|
srcH := imgB.Dy()
|
||||||
|
if srcW <= 0 || srcH <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
tileMode := th.BackgroundTileMode()
|
||||||
|
anchor := th.BackgroundAnchorMode()
|
||||||
|
scaleMode := th.BackgroundScaleMode()
|
||||||
|
|
||||||
|
// Compute scaled tile size in pixels (scale depends on canvas size).
|
||||||
|
tileW, tileH := backgroundScaledSize(srcW, srcH, canvasW, canvasH, scaleMode)
|
||||||
|
if tileW <= 0 || tileH <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the tile image (possibly scaled) from cache.
|
||||||
|
tile := bgImg
|
||||||
|
if scaleMode != BackgroundScaleNone || tileW != srcW || tileH != srcH {
|
||||||
|
rgbaTile := g.getOrBuildScaledTile(bgImg, srcW, srcH, tileW, tileH, scaleMode, canvasW, canvasH)
|
||||||
|
if rgbaTile == nil {
|
||||||
|
// Fallback to slow path if we cannot scale (non-RGBA weirdness).
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tile = rgbaTile
|
||||||
|
}
|
||||||
|
|
||||||
|
offX, offY := w.backgroundAnchorOffsetPx(params, tileW, tileH, anchor)
|
||||||
|
|
||||||
|
switch tileMode {
|
||||||
|
case BackgroundTileNone:
|
||||||
|
// Draw single image centered in full canvas, then clipped by rect.
|
||||||
|
x := (canvasW-tileW)/2 + offX
|
||||||
|
y := (canvasH-tileH)/2 + offY
|
||||||
|
w.drawOneTileRGBA(dst, tile, rect, x, y)
|
||||||
|
|
||||||
|
case BackgroundTileRepeat:
|
||||||
|
originX := offX
|
||||||
|
originY := offY
|
||||||
|
|
||||||
|
startX := floorDiv(rect.X-originX, tileW)*tileW + originX
|
||||||
|
startY := floorDiv(rect.Y-originY, tileH)*tileH + originY
|
||||||
|
|
||||||
|
for yy := startY; yy < rect.Y+rect.H; yy += tileH {
|
||||||
|
for xx := startX; xx < rect.X+rect.W; xx += tileW {
|
||||||
|
w.drawOneTileRGBA(dst, tile, rect, xx, yy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Treat unknown as none.
|
||||||
|
x := (canvasW-tileW)/2 + offX
|
||||||
|
y := (canvasH-tileH)/2 + offY
|
||||||
|
w.drawOneTileRGBA(dst, tile, rect, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GGDrawer) getOrBuildScaledTile(img image.Image, srcW, srcH, dstW, dstH int, mode BackgroundScaleMode, canvasW, canvasH int) *image.RGBA {
|
||||||
|
// Identify image pointer (themes typically provide *image.RGBA).
|
||||||
|
ptr := imagePointer(img)
|
||||||
|
|
||||||
|
key := bgTileCacheKey{
|
||||||
|
imgPtr: ptr,
|
||||||
|
scaleMode: mode,
|
||||||
|
canvasW: canvasW,
|
||||||
|
canvasH: canvasH,
|
||||||
|
srcW: srcW,
|
||||||
|
srcH: srcH,
|
||||||
|
}
|
||||||
|
if g.bgCache.valid && g.bgCache.key == key && g.bgCache.scaledTile != nil &&
|
||||||
|
g.bgCache.tileW == dstW && g.bgCache.tileH == dstH {
|
||||||
|
return g.bgCache.scaledTile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale only from *image.RGBA fast; otherwise, try a generic slow path.
|
||||||
|
var scaled *image.RGBA
|
||||||
|
if srcRGBA, ok := img.(*image.RGBA); ok {
|
||||||
|
scaled = scaleNearestRGBA(srcRGBA, dstW, dstH)
|
||||||
|
} else {
|
||||||
|
scaled = scaleNearestGeneric(img, dstW, dstH)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.bgCache.key = key
|
||||||
|
g.bgCache.valid = true
|
||||||
|
g.bgCache.scaledTile = scaled
|
||||||
|
g.bgCache.tileW = dstW
|
||||||
|
g.bgCache.tileH = dstH
|
||||||
|
|
||||||
|
return scaled
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePointer(img image.Image) uintptr {
|
||||||
|
// Works well when img is a pointer type (e.g. *image.RGBA).
|
||||||
|
// If not pointer, Pointer() returns 0; cache will be less effective but still correct.
|
||||||
|
v := reflect.ValueOf(img)
|
||||||
|
if v.Kind() == reflect.Pointer || v.Kind() == reflect.UnsafePointer {
|
||||||
|
return v.Pointer()
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// scaleNearestRGBA scales src -> dst with nearest-neighbor sampling.
|
||||||
|
// This is intended for background textures; performance > quality.
|
||||||
|
func scaleNearestRGBA(src *image.RGBA, dstW, dstH int) *image.RGBA {
|
||||||
|
if dstW <= 0 || dstH <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sb := src.Bounds()
|
||||||
|
sw := sb.Dx()
|
||||||
|
sh := sb.Dy()
|
||||||
|
if sw <= 0 || sh <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
|
||||||
|
|
||||||
|
for y := 0; y < dstH; y++ {
|
||||||
|
sy := (y * sh) / dstH
|
||||||
|
srcOff := (sy+sb.Min.Y)*src.Stride + sb.Min.X*4
|
||||||
|
dstOff := y * dst.Stride
|
||||||
|
for x := 0; x < dstW; x++ {
|
||||||
|
sx := (x * sw) / dstW
|
||||||
|
si := srcOff + sx*4
|
||||||
|
di := dstOff + x*4
|
||||||
|
dst.Pix[di+0] = src.Pix[si+0]
|
||||||
|
dst.Pix[di+1] = src.Pix[si+1]
|
||||||
|
dst.Pix[di+2] = src.Pix[si+2]
|
||||||
|
dst.Pix[di+3] = src.Pix[si+3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
func scaleNearestGeneric(src image.Image, dstW, dstH int) *image.RGBA {
|
||||||
|
if dstW <= 0 || dstH <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sb := src.Bounds()
|
||||||
|
sw := sb.Dx()
|
||||||
|
sh := sb.Dy()
|
||||||
|
if sw <= 0 || sh <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
|
||||||
|
for y := 0; y < dstH; y++ {
|
||||||
|
sy := sb.Min.Y + (y*sh)/dstH
|
||||||
|
for x := 0; x < dstW; x++ {
|
||||||
|
sx := sb.Min.X + (x*sw)/dstW
|
||||||
|
dst.Set(x, y, src.At(sx, sy))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawOneTileRGBA draws tile at (x,y) into dst, but only the portion that intersects rect.
|
||||||
|
// Uses draw.Over (alpha compositing), assuming caller already cleared rect to background color.
|
||||||
|
func (w *World) drawOneTileRGBA(dst *image.RGBA, tile image.Image, rect RectPx, x, y int) {
|
||||||
|
tileB := tile.Bounds()
|
||||||
|
tw := tileB.Dx()
|
||||||
|
th := tileB.Dy()
|
||||||
|
if tw <= 0 || th <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intersection of tile rect and target rect.
|
||||||
|
tx0 := x
|
||||||
|
ty0 := y
|
||||||
|
tx1 := x + tw
|
||||||
|
ty1 := y + th
|
||||||
|
|
||||||
|
rx0 := rect.X
|
||||||
|
ry0 := rect.Y
|
||||||
|
rx1 := rect.X + rect.W
|
||||||
|
ry1 := rect.Y + rect.H
|
||||||
|
|
||||||
|
ix0 := max(tx0, rx0)
|
||||||
|
iy0 := max(ty0, ry0)
|
||||||
|
ix1 := min(tx1, rx1)
|
||||||
|
iy1 := min(ty1, ry1)
|
||||||
|
if ix0 >= ix1 || iy0 >= iy1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dstR := image.Rect(ix0, iy0, ix1, iy1)
|
||||||
|
srcPt := image.Point{X: tileB.Min.X + (ix0 - tx0), Y: tileB.Min.Y + (iy0 - ty0)}
|
||||||
|
|
||||||
|
draw.Draw(dst, dstR, tile, srcPt, draw.Over)
|
||||||
|
}
|
||||||
@@ -5,6 +5,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (w *World) drawBackground(drawer PrimitiveDrawer, params RenderParams, rect RectPx) {
|
func (w *World) drawBackground(drawer PrimitiveDrawer, params RenderParams, rect RectPx) {
|
||||||
|
if gd, ok := drawer.(*GGDrawer); ok {
|
||||||
|
if gd.drawBackgroundFast(w, params, rect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
th := w.Theme()
|
th := w.Theme()
|
||||||
bgImg := th.BackgroundImage()
|
bgImg := th.BackgroundImage()
|
||||||
if bgImg == nil {
|
if bgImg == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user