Skip to content

animations demo: UI thread saturated idle (rAF text mutation defeats layer-tree reuse) + hover-transition present stalls #82

Description

@codymullins

Companion to #81. Traced 2026-06-10 on the live GUI showing the animations demo (http://localhost:8088/animations/, fixture testdata/sites/animations/). The page freezes during hover transitions; the trace shows the UI thread is already saturated before any interaction.

Findings

1. Layer-tree reuse never engages on this page. app.js auto-starts a WAAPI animation, and its rAF loop writes status.textContent every frame. The text mutation bumps LayoutInvalidationVersion, so WebviewPanel.LiveTick (src/Starling.Gui/Controls/WebviewPanel.cs:1591) takes the full relayout + rebuild path every tick and sets _animationTickPresent=false — the cached-tree fast path in NativeViewportRenderer.Present (src/Starling.Gui.Core/Rendering/NativeViewportRenderer.cs:126) is skipped on every frame. Idle trace: UI thread 100% busy, 7.3s of 8.5s inside LayerTreeBuilder.Build.

2. The rebuild cost is dominated by the per-box style-override callback. WebviewPanel.AnimatedStyle runs for every box during Build (via DisplayListBuilder.EffectiveStyle, 5.1s of the 8.5s idle trace), enumerating Animation/Transition ActiveProperties per box per frame.

3. Hover transitions stack ~170ms synchronous present stalls on top. A hover state change is a non-animation present (whole-tree rebuild by design), and the 2048x512 tiles containing the hovered element re-raster. Those tiles hold cards with box-shadow: 0 18px 40px: the shadow blur is cached (BoxShadowRasterCache) but every draw runs an ImageSharp CPU bicubic resize to device scale (CloningImageProcessor) — 4.7s in a 16s hover-toggle trace. Transition start/end also flips layer promotion (TransitionEngine.ActiveProperties -> IsElementAnimatingLayerRoot), re-keying the base slice and forcing extra base-tile re-rasters.

Work items

  • Don't let a paint-only / single-layer text mutation force the full-rebuild path: scope the rebuild to the mutated layer (the recently-mutated promotion already isolates it) and keep _animationTickPresent for the rest of the tree.
  • Cheapen or cache the per-box AnimatedStyle probe during Build (e.g. an animating-element set computed once per frame instead of two enumerator probes per box).
  • Make hover presents reuse the cached tree, refreshing only the hover-affected layer.
  • Stop the per-draw CPU resize of cached shadow rasters: cache at device scale or serve shadows from a GPU texture (overlaps the GPU filter pipeline in github.com beachballs ~30s on load: CPU Gaussian blur runs per tile on the UI thread (+ hydration, shutdown-abort follow-ups) #81).
  • Damp promotion flip-flop at transition start/end (hysteresis like WasRecentlyMutated) so the base slice is not re-keyed twice per transition.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions