primitives/repaint_boundary.go

Functions Structs

Functions

func AccessibilityActions

AccessibilityActions returns nil.

func (rb *RepaintBoundary) AccessibilityActions() []a11y.Action {
	return nil
}

func AccessibilityHint

AccessibilityHint returns an empty string.

func (rb *RepaintBoundary) AccessibilityHint() string {
	return ""
}

func AccessibilityLabel

AccessibilityLabel returns the debug label or empty string.

func (rb *RepaintBoundary) AccessibilityLabel() string {
	return rb.debugLabel
}

func AccessibilityRole

AccessibilityRole returns [a11y.RoleGenericContainer].

func (rb *RepaintBoundary) AccessibilityRole() a11y.Role {
	return a11y.RoleGenericContainer
}

func AccessibilityState

AccessibilityState returns the default state.

func (rb *RepaintBoundary) AccessibilityState() a11y.State {
	return a11y.State{
		Disabled:	!rb.IsEnabled(),
		Hidden:		!rb.IsVisible(),
	}
}

func AccessibilityValue

AccessibilityValue returns an empty string.

func (rb *RepaintBoundary) AccessibilityValue() string {
	return ""
}

func CacheHits

CacheHits returns how many times the cache was served instead of re-rendering.

func (rb *RepaintBoundary) CacheHits() int {
	return rb.cacheHits
}

func CacheKey

CacheKey returns the unique monotonic ID for this boundary.

func (rb *RepaintBoundary) CacheKey() uint64 {
	return rb.cacheKey
}

func CacheValid

CacheValid reports whether the cached scene holds valid content.

func (rb *RepaintBoundary) CacheValid() bool {
	return !rb.boundaryDirty && rb.cachedScene != nil
}

func Child

Child returns the wrapped child widget.

func (rb *RepaintBoundary) Child() widget.Widget {
	return rb.child
}

func Children

Children returns the child widget, or nil if none.

func (rb *RepaintBoundary) Children() []widget.Widget {
	if rb.child == nil {
		return nil
	}
	return []widget.Widget{rb.child}
}

func ClearBoundaryDirty

ClearBoundaryDirty resets the boundary dirty flag after the boundary

has been repainted. Called by the Window after painting dirty boundaries.

func (rb *RepaintBoundary) ClearBoundaryDirty() {
	rb.boundaryDirty = false
}

func ConsecutiveHits

ConsecutiveHits returns the number of consecutive cache hits since the

last cache miss. Used for observability and testing.

func (rb *RepaintBoundary) ConsecutiveHits() int {
	return rb.consecutiveHits
}

func DebugLabel

DebugLabel returns the diagnostic label, or empty string if none set.

func (rb *RepaintBoundary) DebugLabel() string {
	return rb.debugLabel
}

func Draw

Draw renders the child subtree, using the scene cache when possible.

 

On cache hit (boundary not dirty, cached scene exists): replays the

cached scene.Scene into the canvas via Canvas.ReplayScene — no child

re-execution. This is O(commands) via Encoding.Append or GPU dispatch.

 

On cache miss (boundary dirty or first draw): records child.Draw into

a new scene.Scene via SceneCanvas, then replays the result.

 

This is the ADR-007 retained-mode pattern: display list per boundary.

func (rb *RepaintBoundary) Draw(ctx widget.Context, canvas widget.Canvas) {
	if !rb.IsVisible() || rb.child == nil {
		return
	}

	// Wire the onBoundaryDirty callback on first Draw so that future
	// SetNeedsRedraw → propagateDirtyUpward → MarkBoundaryDirty
	// triggers RequestRedraw on the window. Without this, dirty flags
	// are set but the render loop never wakes up (ADR-007 fix).
	if rb.onBoundaryDirty == nil && ctx != nil {
		capturedCtx := ctx
		rb.onBoundaryDirty = func(_ *RepaintBoundary) {
			capturedCtx.InvalidateRect(rb.Bounds())
		}
	}

	bounds := rb.Bounds()
	w := int(bounds.Width())
	h := int(bounds.Height())
	if w <= 0 || h <= 0 {
		return
	}

	// Cache hit: boundary is clean and we have a cached scene.
	// Suppress damage tracking during replay — cached content hasn't changed,
	// so it must not inflate the gg-level damage list (ADR-021 false positive).
	if !rb.boundaryDirty && rb.cachedScene != nil {
		rb.recordCacheHit(ctx)
		rb.consecutiveHits++
		rb.evaluatePromotion(w, h)
		canvas.PushTransform(bounds.Min)
		if dc, ok := canvas.(widget.DamageController); ok {
			dc.SetDamageTracking(false)
		}
		canvas.ReplayScene(rb.cachedScene)
		if dc, ok := canvas.(widget.DamageController); ok {
			dc.SetDamageTracking(true)
		}
		canvas.PopTransform()
		return
	}

	// Cache miss: reset consecutive hits and demote if stable.
	rb.demoteIfStable()
	rb.consecutiveHits = 0

	// Record child drawing into a scene.
	if rb.cachedScene == nil {
		rb.cachedScene = scene.NewScene()
	}
	rb.cachedScene.Reset()

	recorder := internalRender.NewSceneCanvas(rb.cachedScene, w, h)
	rb.child.Draw(ctx, recorder)
	recorder.Close()

	// Clear redraw flags in the child subtree since we just rendered them.
	widget.ClearRedrawInTree(rb.child)

	rb.boundaryDirty = false
	rb.cacheVersion++
	rb.lastSceneVersion = rb.cacheVersion

	// Replay the freshly recorded scene into the parent canvas.
	canvas.PushTransform(bounds.Min)
	canvas.ReplayScene(rb.cachedScene)
	canvas.PopTransform()
}

func Event

Event dispatches events to the child.

func (rb *RepaintBoundary) Event(ctx widget.Context, e event.Event) bool {
	if !rb.IsVisible() || !rb.IsEnabled() {
		return false
	}

	if rb.child == nil {
		return false
	}

	// Translate mouse events to local coordinates.
	if me, ok := e.(*event.MouseEvent); ok {
		local := *me
		local.Position = me.Position.Sub(rb.Bounds().Min)
		return rb.child.Event(ctx, &local)
	}

	return rb.child.Event(ctx, e)
}

func InvalidateCache

InvalidateCache marks the cache as stale, forcing a re-record on the

next draw pass. This is called automatically when descendants are dirty;

manual invocation is rarely needed.

func (rb *RepaintBoundary) InvalidateCache() {
	rb.boundaryDirty = true
}

func IsBoundaryDirty

IsBoundaryDirty reports whether this boundary has been marked dirty by

upward propagation since the last draw pass.

func (rb *RepaintBoundary) IsBoundaryDirty() bool {
	return rb.boundaryDirty
}

func IsStable

IsStable reports whether this boundary has been promoted to stable status

by the raster cache heuristic. Stable boundaries have had consecutive

cache hits >= the promotion threshold, with sufficient complexity and area.

 

Stable boundaries are candidates for GPU texture caching (future Phase 4).

The compositor can use this to prioritize GPU texture allocation.

func (rb *RepaintBoundary) IsStable() bool {
	return rb.stable
}

func Layout

Layout delegates to the child and stores the resulting size.

func (rb *RepaintBoundary) Layout(ctx widget.Context, constraints geometry.Constraints) geometry.Size {
	if rb.child == nil {
		size := constraints.Constrain(geometry.Sz(0, 0))
		rb.SetBounds(geometry.FromPointSize(rb.Position(), size))
		return size
	}

	size := rb.child.Layout(ctx, constraints)

	// Position child at origin (no offset within boundary).
	rb.child.(interface{ SetBounds(geometry.Rect) }).SetBounds(
		geometry.FromPointSize(geometry.Pt(0, 0), size),
	)

	rb.SetBounds(geometry.FromPointSize(rb.Position(), size))

	// Invalidate cache if size changed.
	w := int(size.Width)
	h := int(size.Height)
	if w != rb.cacheWidth || h != rb.cacheHeight {
		rb.boundaryDirty = true
		rb.cacheWidth = w
		rb.cacheHeight = h
	}

	return size
}

func MarkBoundaryDirty

MarkBoundaryDirty marks this repaint boundary as needing re-rendering.

 

Called by the upward dirty propagation in [widget.WidgetBase.SetNeedsRedraw]

when a descendant widget changes. This invalidates the scene cache and

notifies the Window (via callback) to add this boundary to its dirty set.

 

Implements [widget.RepaintBoundaryMarker].

func (rb *RepaintBoundary) MarkBoundaryDirty() {
	if rb.boundaryDirty {
		return	// Already dirty — O(1) guard.
	}
	rb.boundaryDirty = true

	// Reset consecutive hits on dirty — the boundary is no longer in a
	// consecutive-cache-hit streak. Demotion from stable happens in Draw
	// (cache miss path) to keep MarkBoundaryDirty O(1).
	rb.consecutiveHits = 0

	if rb.onBoundaryDirty != nil {
		rb.onBoundaryDirty(rb)
	}
}

func NewRepaintBoundary

NewRepaintBoundary creates a RepaintBoundary that caches the rendering

of the given child widget as a scene.Scene display list.

 

If child is nil, the boundary renders nothing and reports zero size.

 

Options:

- [WithDebugLabel] — optional label for diagnostics

func NewRepaintBoundary(child widget.Widget, opts ...Option) *RepaintBoundary {
	rb := &RepaintBoundary{
		child:		child,
		cacheKey:	nextCacheKey.Add(1),
		rasterCacheCfg:	DefaultRasterCacheConfig(),
	}
	rb.SetVisible(true)
	rb.SetEnabled(true)

	for _, opt := range opts {
		opt(rb)
	}

	// ADR-028: parent chain for upward dirty propagation.
	// Flutter: RenderObject.adoptChild sets parent on each child.
	if child != nil {
		type parentSetter interface{ SetParent(widget.Widget) }
		if ps, ok := child.(parentSetter); ok {
			ps.SetParent(rb)
		}
	}

	return rb
}

func RasterCacheStats

RasterCacheStats returns a snapshot of the raster cache state for this

boundary. Used for diagnostics, benchmarks, and compositor decisions.

func (rb *RepaintBoundary) RasterCacheStats() RasterCacheStats {
	var tagCount int
	if rb.cachedScene != nil {
		tagCount = len(rb.cachedScene.Encoding().Tags())
	}

	return RasterCacheStats{
		ConsecutiveHits:	rb.consecutiveHits,
		IsStable:		rb.stable,
		TagCount:		tagCount,
		Area:			rb.cacheWidth * rb.cacheHeight,
		TotalPromotions:	rb.totalPromotions,
		TotalDemotions:		rb.totalDemotions,
	}
}

func SceneChanged

SceneChanged reports whether the boundary's scene has changed since the

given version. Used by the compositor to determine which boundaries

need re-compositing in the damage-aware pipeline (Task 3d).

 

Usage:

 

if rb.SceneChanged(lastVersion) {

// Re-composite this boundary's region.

lastVersion = rb.SceneVersion()

}

func (rb *RepaintBoundary) SceneChanged(sinceVersion uint64) bool {
	return rb.cacheVersion != sinceVersion
}

func SceneVersion

SceneVersion returns a monotonic counter that increments each time the

boundary's scene cache is re-recorded. The compositor uses this to detect

when a boundary's content has actually changed between frames.

 

When SceneVersion() == lastObservedVersion, the boundary's scene has not

changed and the compositor can skip re-compositing this region.

func (rb *RepaintBoundary) SceneVersion() uint64 {
	return rb.cacheVersion
}

func SetOnBoundaryDirty

SetOnBoundaryDirty sets the callback invoked when this boundary

transitions from clean to dirty via upward propagation.

 

Used by the Window to collect dirty boundaries into its set.

func (rb *RepaintBoundary) SetOnBoundaryDirty(fn func(rb *RepaintBoundary)) {
	rb.onBoundaryDirty = fn
}

func Unmount

Unmount releases the scene cache when the widget is removed from the tree.

func (rb *RepaintBoundary) Unmount() {
	rb.cachedScene = nil
	rb.cacheVersion = 0
	rb.cacheWidth = 0
	rb.cacheHeight = 0
	rb.cacheHits = 0
	rb.boundaryDirty = false
	// Reset raster cache state (Task 3a).
	rb.consecutiveHits = 0
	rb.stable = false
	rb.lastSceneVersion = 0
}

func WithDebugLabel

WithDebugLabel sets an optional label for diagnostics and logging.

func WithDebugLabel(label string) Option {
	return func(rb *RepaintBoundary) {
		rb.debugLabel = label
	}
}

Structs

type RepaintBoundary struct

RepaintBoundary is a display widget that caches its child subtree as a

scene.Scene display list. When the child subtree is clean (no dirty

widgets), the cached display list is replayed into the parent canvas

instead of re-executing Draw on every descendant.

 

This is the Flutter RepaintBoundary / PictureLayer pattern: an explicit

opt-in boundary that isolates expensive subtrees from the rest of the

render tree. Display lists are portable across all gg backends (Vulkan,

DX12, Metal, GLES, Software, future WASM/WebGPU).

 

Cache lifecycle:

- The cached scene is allocated on first draw (lazy).

- The cache is invalidated when any descendant calls SetNeedsRedraw,

which propagates UP to the boundary (ADR-007 upward propagation),

or when the widget size changes.

- The cache is freed on Unmount.

 

RepaintBoundary implements [widget.Widget] and [a11y.Accessible].

 

Example:

 

expensive := primitives.Box(

primitives.Text("Complex chart..."),

).Padding(16)

 

cached := primitives.NewRepaintBoundary(expensive)

type RepaintBoundary struct {
	widget.WidgetBase

	child	widget.Widget

	// cacheKey is a unique monotonic ID for this boundary, used as the key
	// into the Window's dirty boundary set. Assigned once at creation time.
	cacheKey	uint64

	// cachedScene holds the recorded display list (scene.Scene) for the
	// child subtree. On cache hit, this is replayed into the parent canvas
	// via Canvas.ReplayScene — no child.Draw() re-execution needed.
	cachedScene	*scene.Scene

	// cacheVersion is a monotonic counter incremented each time the cache
	// is refreshed. Used for observability and diagnostics.
	cacheVersion	uint64
	// cacheWidth and cacheHeight track the cache dimensions to detect
	// size changes that require re-recording.
	cacheWidth	int
	cacheHeight	int

	// debugLabel is an optional identifier for diagnostics.
	debugLabel	string

	// cacheHits tracks how many times the cache was used (for stats).
	cacheHits	int

	// boundaryDirty indicates that a descendant has changed and this
	// boundary's cache needs to be re-recorded. Set by upward dirty
	// propagation (ADR-007) via [MarkBoundaryDirty].
	boundaryDirty	bool

	// onBoundaryDirty is a callback invoked when this boundary transitions
	// from clean to dirty. Used by the Window to collect dirty boundaries
	// into its dirtyBoundaries set (ADR-007, Task 1e).
	onBoundaryDirty	func(rb *RepaintBoundary)

	// --- RasterCache (ADR-007 Phase 3, Task 3a) ---

	// rasterCacheCfg holds the heuristic parameters for stability tracking.
	// Initialized to DefaultRasterCacheConfig in NewRepaintBoundary.
	rasterCacheCfg	RasterCacheConfig

	// consecutiveHits tracks the number of consecutive cache hits since
	// the last cache miss. Used by the raster cache heuristic to decide
	// when to promote the boundary to stable status.
	consecutiveHits	int

	// stable indicates that this boundary has been promoted by the raster
	// cache heuristic. Stable boundaries rarely change and are candidates
	// for GPU texture caching (future Phase 4).
	stable	bool

	// totalPromotions counts cumulative promotions for diagnostics.
	totalPromotions	int

	// totalDemotions counts cumulative demotions for diagnostics.
	totalDemotions	int

	// lastSceneVersion tracks the scene version from the last Draw call.
	// Used by the damage-aware compositor (Task 3d) to detect when a
	// boundary's scene content has actually changed between frames.
	lastSceneVersion	uint64
}