Files
galaxy-game/client/coalesce.go
T
2026-03-07 00:29:06 +03:00

166 lines
4.6 KiB
Go

package client
/*
Fyne-friendly latest-wins coalescing for canvas.NewRaster(draw func(w,h int) image.Image).
How to use (integration sketch):
type editor struct {
w *world.World
drawer world.PrimitiveDrawer // wraps gg.Context over a backing *image.RGBA
raster *canvas.Raster
co *client.RasterCoalescer[world.RenderParams]
}
func (e *editor) initCoalescer() {
exec := client.FyneMainThreadExecutor{} // uses fyne.CurrentApp().Driver().RunOnMain
e.co = client.NewRasterCoalescer(exec, e.raster, func(wPx, hPx int, p world.RenderParams) image.Image {
// your existing draw pipeline:
// 1) ensure viewport/margins inside p are consistent with wPx/hPx if needed
// 2) call e.w.Render(e.drawer, p)
// 3) get image from gg.Context, crop margins, return
_ = e.w.Render(e.drawer, p)
return e.drawerImageCropped() // your code
})
}
// Call from input handlers (pan/zoom/etc). Can be from any goroutine.
func (e *editor) RefreshUI(p world.RenderParams) {
e.co.Request(p) // schedules raster.Refresh() on UI thread
}
// Raster draw callback:
func (e *editor) draw(wPx, hPx int) image.Image {
return e.co.Draw(wPx, hPx)
}
Key property:
- draw() renders at most once per invocation (never loops).
- if new requests arrived while drawing, we schedule exactly one extra Refresh.
*/
import (
"image"
"sync"
)
// UIExecutor posts a function to run on the UI/main thread.
type UIExecutor interface {
Post(fn func())
}
// Refresher is the minimal interface we need from fyne.CanvasObject / Raster.
type Refresher interface {
Refresh()
}
// RasterRenderer renders the latest params and returns an image.
// Must be called on the UI thread (inside draw callback).
type RasterRenderer[P any] func(wPx, hPx int, params P) image.Image
// RasterCoalescer implements latest-wins coalescing for raster rendering.
// It is designed specifically for toolkits like fyne where the system calls draw(w,h)
// and expects a returned image.
type RasterCoalescer[P any] struct {
exec UIExecutor
refresher Refresher
renderer RasterRenderer[P]
mu sync.Mutex
// inDraw == true while Draw() is running on UI thread.
inDraw bool
// refreshQueued == true when we have already posted a Refresh() that has not yet
// resulted in a Draw() call (or is expected to call Draw soon).
refreshQueued bool
// pending == true when new params arrived while inDraw==true.
// Draw() will schedule exactly one follow-up Refresh after it returns.
pending bool
latest P
have bool
}
// NewRasterCoalescer creates a new coalescer.
// - exec.Post must run fn on UI thread.
// - refresher.Refresh will trigger the framework to call draw(w,h).
func NewRasterCoalescer[P any](exec UIExecutor, refresher Refresher, renderer RasterRenderer[P]) *RasterCoalescer[P] {
if exec == nil {
panic("RasterCoalescer: nil executor")
}
if refresher == nil {
panic("RasterCoalescer: nil refresher")
}
if renderer == nil {
panic("RasterCoalescer: nil renderer")
}
return &RasterCoalescer[P]{exec: exec, refresher: refresher, renderer: renderer}
}
// Request stores the latest params and schedules exactly one refresh (latest-wins).
// Can be called from any goroutine.
func (c *RasterCoalescer[P]) Request(params P) {
c.mu.Lock()
c.latest = params
c.have = true
// If we are currently inside Draw(), don't schedule refresh immediately.
// Just mark pending; Draw() will schedule one follow-up refresh after it returns.
if c.inDraw {
c.pending = true
c.mu.Unlock()
return
}
// Not drawing. Schedule at most one refresh until the next Draw() happens.
if c.refreshQueued {
c.mu.Unlock()
return
}
c.refreshQueued = true
c.mu.Unlock()
c.exec.Post(c.refresher.Refresh)
}
// Draw must be called from the raster draw callback on the UI thread.
// It renders exactly once with the latest snapshot.
// If more requests arrived while drawing, it schedules exactly one extra refresh.
func (c *RasterCoalescer[P]) Draw(wPx, hPx int) image.Image {
c.mu.Lock()
// A Draw call corresponds to a previously scheduled refresh being serviced.
c.refreshQueued = false
if !c.have {
c.mu.Unlock()
return image.NewRGBA(image.Rect(0, 0, wPx, hPx))
}
c.inDraw = true
c.pending = false
params := c.latest
c.mu.Unlock()
img := c.renderer(wPx, hPx, params)
c.mu.Lock()
needAnother := c.pending
c.pending = false
c.inDraw = false
// If we need another frame, schedule exactly one refresh (if not already queued).
if needAnother && !c.refreshQueued {
c.refreshQueued = true
c.mu.Unlock()
c.exec.Post(c.refresher.Refresh)
return img
}
c.mu.Unlock()
return img
}