scene/renderer.go

Functions Structs

Functions

func Cache

Cache returns the layer 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
}