scene/renderer.go
Functions
func Cache
func (r *Renderer) Cache() *LayerCache {
return r.cache
}
func CacheStats
CacheStats returns the layer cache statistics.
func (r *Renderer) CacheStats() CacheStats {
if r.cache == nil {
return CacheStats{}
}
return r.cache.Stats()
}
func Close
Close releases all resources used by the renderer.
The renderer should not be used after Close is called.
func (r *Renderer) Close() {
if r.workerPool != nil {
r.workerPool.Close()
}
if r.tileGrid != nil {
r.tileGrid.Close()
}
}
func DirtyTileCount
DirtyTileCount returns the number of dirty tiles.
func (r *Renderer) DirtyTileCount() int {
if r.dirty != nil {
return r.dirty.Count()
}
return len(r.tileGrid.DirtyTiles())
}
func Height
Height returns the renderer height in pixels.
func (r *Renderer) Height() int {
return r.height
}
func MarkAllDirty
MarkAllDirty marks all tiles as needing redraw.
func (r *Renderer) MarkAllDirty() {
if r.dirty != nil {
r.dirty.MarkAll()
}
r.tileGrid.MarkAllDirty()
}
func MarkDirty
MarkDirty marks the specified rectangle as needing redraw.
Coordinates are in pixel space.
func (r *Renderer) MarkDirty(x, y, w, h int) {
if r.dirty != nil {
r.dirty.MarkRect(x, y, w, h)
}
r.tileGrid.MarkRectDirty(x, y, w, h)
}
func NewRenderer
NewRenderer creates a new scene renderer for the given dimensions.
Options can be used to configure caching, parallelism, and other settings.
func NewRenderer(width, height int, opts ...RendererOption) *Renderer {
if width <= 0 || height <= 0 {
return nil
}
workers := runtime.GOMAXPROCS(0)
r := &Renderer{
width: width,
height: height,
tileSize: parallel.TileWidth,
workers: workers,
cache: DefaultLayerCache(),
}
// Apply options
for _, opt := range opts {
opt(r)
}
// Initialize parallel infrastructure
r.tileGrid = parallel.NewTileGrid(width, height)
r.workerPool = parallel.NewWorkerPool(r.workers)
// Initialize dirty region tracking
tilesX := (width + parallel.TileWidth - 1) / parallel.TileWidth
tilesY := (height + parallel.TileHeight - 1) / parallel.TileHeight
r.dirty = parallel.NewDirtyRegion(tilesX, tilesY)
r.dirty.MarkAll() // Initially all tiles are dirty
return r
}
func Render
Render renders the entire scene to the target pixmap.
This processes all tiles regardless of dirty state.
For cancellable rendering, use RenderWithContext.
func (r *Renderer) Render(target *gg.Pixmap, scene *Scene) error {
return r.RenderWithContext(context.Background(), target, scene)
}
func RenderDirty
RenderDirty renders only the dirty regions of the scene.
This is more efficient when only parts of the scene have changed.
The dirty parameter specifies which tiles need re-rendering.
For cancellable rendering, use RenderDirtyWithContext.
func (r *Renderer) RenderDirty(target *gg.Pixmap, scene *Scene, dirty *parallel.DirtyRegion) error {
return r.RenderDirtyWithContext(context.Background(), target, scene, dirty)
}
func RenderDirtyWithContext
RenderDirtyWithContext renders only the dirty regions of the scene with cancellation support.
This is more efficient when only parts of the scene have changed.
The dirty parameter specifies which tiles need re-rendering.
The context can be used to cancel long-running renders. When canceled,
the function returns ctx.Err() and the target may contain partial results.
func (r *Renderer) RenderDirtyWithContext(ctx context.Context, target *gg.Pixmap, scene *Scene, dirty *parallel.DirtyRegion) error {
if target == nil || scene == nil {
return nil
}
// Check for cancellation at start
select {
case <-ctx.Done():
return ctx.Err()
default:
}
startTotal := time.Now()
r.statsMu.Lock()
r.stats = RenderStats{}
r.statsMu.Unlock()
// Use provided dirty region or fall back to internal tracking
dirtyRegion := dirty
if dirtyRegion == nil {
dirtyRegion = r.dirty
}
// Get the flattened encoding, image registry, and font registry.
startEncode := time.Now()
enc := scene.Encoding()
images := scene.Images()
r.fontRegistry = scene.FontRegistry()
encodeTime := time.Since(startEncode)
// Check for cancellation after encoding
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Get dirty tile coordinates
dirtyCoords := dirtyRegion.GetAndClear()
tilesDirty := len(dirtyCoords)
if tilesDirty == 0 {
return nil // Nothing to render
}
// Collect dirty tiles
tiles := make([]*parallel.Tile, 0, tilesDirty)
for _, coord := range dirtyCoords {
if tile := r.tileGrid.TileAt(coord[0], coord[1]); tile != nil {
tiles = append(tiles, tile)
}
}
// Render dirty tiles in parallel with context
startRaster := time.Now()
if err := r.renderTilesWithContext(ctx, tiles, enc, target, images); err != nil {
return err
}
rasterTime := time.Since(startRaster)
// Check for cancellation before compositing
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Composite dirty tiles to target
startComposite := time.Now()
r.compositeTiles(tiles, target)
compositeTime := time.Since(startComposite)
// Update statistics
totalTime := time.Since(startTotal)
tilesTotal := r.tileGrid.TileCount()
r.updateStats(tilesTotal, tilesDirty, len(tiles), encodeTime, rasterTime, compositeTime, totalTime)
return nil
}
func RenderWithContext
RenderWithContext renders the entire scene to the target pixmap with cancellation support.
This processes all tiles regardless of dirty state.
The context can be used to cancel long-running renders. When canceled,
the function returns ctx.Err() and the target may contain partial results.
func (r *Renderer) RenderWithContext(ctx context.Context, target *gg.Pixmap, scene *Scene) error {
if target == nil || scene == nil {
return nil
}
// Check for cancellation at start
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// GPU fast path: if a GPU accelerator is registered, render through
// GPUSceneRenderer which decodes scene commands into gg.Context GPU calls.
// The gg.Context handles GPU→CPU fallback automatically per-shape.
if gg.Accelerator() != nil {
if err := r.renderGPU(target, scene); err == nil {
return nil
}
}
startTotal := time.Now()
r.statsMu.Lock()
r.stats = RenderStats{} // Reset stats
r.statsMu.Unlock()
// Mark all tiles dirty for full render
r.dirty.MarkAll()
// Get the flattened encoding, image registry, and font registry.
startEncode := time.Now()
enc := scene.Encoding()
images := scene.Images()
r.fontRegistry = scene.FontRegistry()
encodeTime := time.Since(startEncode)
// Check for cancellation after encoding
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Get all tiles for rendering
tiles := r.tileGrid.AllTiles()
tilesTotal := len(tiles)
// Render tiles in parallel with context
startRaster := time.Now()
if err := r.renderTilesWithContext(ctx, tiles, enc, target, images); err != nil {
return err
}
rasterTime := time.Since(startRaster)
// Check for cancellation before compositing
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Composite tiles to target
startComposite := time.Now()
r.compositeTiles(tiles, target)
compositeTime := time.Since(startComposite)
// Clear dirty flags
r.dirty.Clear()
r.tileGrid.ClearDirty()
// Update statistics
totalTime := time.Since(startTotal)
r.updateStats(tilesTotal, tilesTotal, tilesTotal, encodeTime, rasterTime, compositeTime, totalTime)
return nil
}
func RenderWithDamage
RenderWithDamage uses a DamageTracker to compute the minimal dirty region
from frame-to-frame object changes, then renders only affected tiles.
This is Level 1-2 of the four-level damage pipeline (ADR-021).
Returns the damage rect (in pixels) for downstream use by ggcanvas/gogpu
(Level 3-4: GPU scissor + OS present). Returns image.Rectangle{} if
nothing changed (caller can skip GPU upload + present entirely).
On first frame, renders everything (full scene).
func (r *Renderer) RenderWithDamage(target *gg.Pixmap, scene *Scene, tracker *DamageTracker) (
damageRect image.Rectangle, err error,
) {
if target == nil || scene == nil {
return image.Rectangle{}, nil
}
objects := scene.TaggedBounds()
damage := tracker.ComputeDamage(objects)
if damage.Empty() && !tracker.IsFirstRender() {
return image.Rectangle{}, nil
}
// First render or actual damage — determine what to redraw
if tracker.IsFirstRender() {
tracker.MarkRendered()
r.dirty.MarkAll()
err = r.RenderDirty(target, scene, nil)
return scene.encoding.Bounds().ImageRect(), err
}
// Convert pixel damage rect to tile coordinates and mark dirty
tileW := r.tileSize
tileH := r.tileSize
tx0 := damage.Min.X / tileW
ty0 := damage.Min.Y / tileH
tx1 := (damage.Max.X + tileW - 1) / tileW
ty1 := (damage.Max.Y + tileH - 1) / tileH
for ty := ty0; ty < ty1; ty++ {
for tx := tx0; tx < tx1; tx++ {
r.dirty.Mark(tx, ty)
}
}
err = r.RenderDirty(target, scene, nil)
return damage, err
}
func Resize
Resize updates the renderer dimensions.
All tiles will be marked dirty after resize.
func (r *Renderer) Resize(width, height int) {
if width <= 0 || height <= 0 {
return
}
if r.width == width && r.height == height {
return
}
r.width = width
r.height = height
// Resize tile grid
r.tileGrid.Resize(width, height)
// Resize dirty region
tilesX := (width + parallel.TileWidth - 1) / parallel.TileWidth
tilesY := (height + parallel.TileHeight - 1) / parallel.TileHeight
r.dirty = parallel.NewDirtyRegion(tilesX, tilesY)
r.dirty.MarkAll()
}
func Stats
Stats returns the current render statistics.
func (r *Renderer) Stats() RenderStats {
r.statsMu.RLock()
defer r.statsMu.RUnlock()
return r.stats
}
func TileCount
TileCount returns the total number of tiles.
func (r *Renderer) TileCount() int {
return r.tileGrid.TileCount()
}
func Width
Width returns the renderer width in pixels.
func (r *Renderer) Width() int {
return r.width
}
func WithCache
WithCache sets a custom layer cache.
If nil, a default cache is created.
func WithCache(cache *LayerCache) RendererOption {
return func(r *Renderer) {
r.cache = cache
}
}
func WithCacheSize
WithCacheSize sets the layer cache size in megabytes.
Default is 64MB.
func WithCacheSize(mb int) RendererOption {
return func(r *Renderer) {
if r.cache != nil {
r.cache.SetMaxSize(mb)
}
}
}
func WithTileSize
WithTileSize sets the tile size for rendering.
This is informational only; actual tile size is fixed at 64x64.
func WithTileSize(size int) RendererOption {
return func(r *Renderer) {
r.tileSize = size
}
}
func WithWorkers
WithWorkers sets the number of worker goroutines for parallel rendering.
If n <= 0, GOMAXPROCS is used.
func WithWorkers(n int) RendererOption {
return func(r *Renderer) {
r.workers = n
}
}
func Workers
Workers returns the number of worker goroutines used for parallel rendering.
func (r *Renderer) Workers() int {
return r.workers
}
Structs
type Renderer struct
Renderer renders Scene content to a target Pixmap using parallel tile-based processing.
It integrates with TileGrid for spatial subdivision and WorkerPool for concurrent execution.
The renderer supports:
- Full scene rendering (all tiles)
- Incremental rendering (dirty tiles only)
- Layer caching for static content
- Performance statistics collection
Thread safety: Renderer methods are safe for concurrent use after initialization.
type Renderer struct {
// Tile-based rendering infrastructure
tileGrid *parallel.TileGrid
workerPool *parallel.WorkerPool
dirty *parallel.DirtyRegion
// Layer caching
cache *LayerCache
// Per-tile resource pool (SoftwareRenderer + Pixmap reuse)
pool tilePool
// Dimensions
width int
height int
// Configuration
tileSize int
workers int
// Font registry for TagText resolution (set per-render from Scene).
fontRegistry map[uint64]*text.FontSource
// Statistics
stats RenderStats
statsMu sync.RWMutex
lastFrame time.Time
}
type RenderStats struct
RenderStats contains performance statistics for a render operation.
type RenderStats struct {
// Tile statistics
TilesTotal int
TilesDirty int
TilesRendered int
// Layer statistics
LayersCached int
LayersRendered int
// Timing (durations for the last render)
TimeEncode time.Duration
TimeRaster time.Duration
TimeComposite time.Duration
TimeTotal time.Duration
// Frame timing
FrameTime time.Duration
FPS float64
}
Cache returns the layer cache.