fast background

This commit is contained in:
IliaDenisov
2026-03-08 15:58:57 +02:00
parent 1c2fc30127
commit fdcbb5d6f4
4 changed files with 411 additions and 0 deletions
+255
View File
@@ -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)
}