Skip to content

Add shared SVG animation runtime and host playback backends#481

Merged
wieslawsoltes merged 108 commits into
masterfrom
feature/svg-animation-runtime
Apr 7, 2026
Merged

Add shared SVG animation runtime and host playback backends#481
wieslawsoltes merged 108 commits into
masterfrom
feature/svg-animation-runtime

Conversation

@wieslawsoltes
Copy link
Copy Markdown
Owner

@wieslawsoltes wieslawsoltes commented Apr 3, 2026

PR Summary: SVG Animation Runtime, Retained Scene Graph, and Rendering Cutover

Overview

This branch started as shared SVG animation and interaction work, then grew into a larger rendering-architecture change.

It adds SVG 1.1 animation DOM support in Svg.Custom, shared pointer interaction and geometry-aware hit testing, a shared animation runtime in SKSvg, host playback backends for Avalonia and Uno, and an Avalonia retained NativeComposition path for supported scenes.

On top of that original scope, the branch introduces a retained scene-graph runtime, migrates rendering/editor/animation flows onto that retained runtime, splits the retained scene graph into its own Svg.SceneGraph project, and then splits the shared animation runtime into a new Svg.Animation project. The branch also removes several unshipped compatibility shims and moves the rendering pipeline toward the retained scene graph as the single runtime representation for the next major release.

Base branch: master
Merge-base: 076e0a22e3fe0220db951710d7799dc92b42576d

Branch diff vs master:

  • 163 files changed
  • about 25,165 insertions
  • about 624 deletions

Commit Progression After The Last PR Summary Update

The earlier PR summary stopped at the first animation/native-composition/doc pass. Since then the branch added:

  • retained scene graph foundation and mutation routing
  • retained-scene-first editor workflows
  • retained-scene rendering fallback for animation
  • removal of the retained compiler drawable bridge
  • retained hit-testing and filter/mask parity fixes
  • scene-graph major-release cutover work that removes drawable-first public rendering flows
  • Svg.SceneGraph split into a separate project
  • reflection removal from animation and scene compilation helpers
  • removal of unshipped obsolete compatibility APIs
  • Svg.Animation split into a separate project

Relevant later commits include:

  • f106e50a Implement subtree animation invalidation
  • a736db88 Prefer default animation backends
  • 20058d0a Fix animation timing and additive state
  • 9e5c315d Gate native composition to renderable roots
  • 7c3b5edf Add retained scene graph foundation
  • 9e8bccf1 Migrate editor workflows to retained nodes
  • 65c23848 Route interaction through retained scene
  • 796370f3 Use retained scenes for animation fallback
  • 6e86c57a Remove retained compiler drawable bridge
  • 41f4b71e Fix retained mask hit testing
  • 537eb2ef Fix event timing instance handling
  • 7865aab6 Prune hidden retained hit-test subtrees
  • 1cf14385 Remove drawable editor fallbacks
  • aaa16665 Migrate retained animation runtime APIs
  • 0bba357f Remove animation reflection access
  • 60767e4d Drop obsolete retained API shims
  • 5fc313de Split retained scene graph project
  • 754035ca Cut over rendering to retained scenes
  • eb990cc4 Fix retained filter parity and hit testing
  • e06ca06f Split animation runtime into Svg.Animation

Main Functional Changes

1. SVG animation DOM and shared runtime

The branch adds local SVG 1.1 animation element support in Svg.Custom and a shared animation runtime in SKSvg.

This includes:

  • set, animate, animateColor, animateTransform, and animateMotion
  • event-driven begin/end timing
  • additive and accumulate handling
  • animation clock/time control APIs
  • host playback backend resolution for Avalonia and Uno
  • animation invalidation and frame-state tracking

The runtime was later cleaned up by:

  • removing reflection-based animated attribute access in favor of explicit runtime bridges
  • fixing timing edge cases such as dotted event ids, repeat handling, spline handling, and zero-duration cases
  • moving the shared runtime into a dedicated Svg.Animation project

Key files:

  • src/Svg.Custom/Animation/*
  • src/Svg.Animation/Animation/*
  • src/Svg.Animation/SvgAnimationInvalidation.cs
  • src/Svg.Skia/SKSvg.AnimationLayers.cs
  • src/Svg.Controls.Skia.Avalonia/Svg.cs
  • src/Svg.Controls.Skia.Uno/Svg.cs

2. Shared interaction and geometry-aware hit testing

The branch adds typed pointer-events support, routed pointer dispatch, geometry-aware hit testing, and retained-scene-first hit testing.

This includes:

  • topmost-element and topmost-scene-node targeting
  • clip-path and mask-aware hit testing
  • routed tunnel/target/bubble dispatch
  • pressed-target capture behavior
  • cursor resolution
  • retained-scene hit-test pruning for hidden or suppressed subtrees

Key files:

  • src/Svg.Custom/Interaction/SvgPointerEvents.cs
  • src/Svg.Skia/Interaction/SvgInteractionDispatcher.cs
  • src/Svg.SceneGraph/SvgSceneHitTestService.cs
  • tests/Svg.Skia.UnitTests/HitTestTests.cs

3. Avalonia/Uno host playback and native composition

The host controls now expose:

  • AnimationBackend
  • AnimationFrameInterval
  • AnimationPlaybackRate
  • ActualAnimationBackend
  • fallback/capability reporting

Avalonia additionally gained a retained native-composition path for supported scenes, plus TestApp support for selecting and exercising it.

This work also includes follow-up fixes for:

  • stale/disposed picture usage
  • initial retained visual activation
  • clipping of translated retained layers
  • descendant opacity preservation
  • fallback from unsupported native-composition scenes

Key files:

  • src/Svg.Controls.Skia.Avalonia/Svg.cs
  • src/Svg.Controls.Skia.Avalonia/Composition/SvgCompositionVisualScene.cs
  • src/Svg.Controls.Skia.Uno/Svg.cs
  • samples/TestApp/Views/MainView.axaml
  • samples/TestApp/Views/MainView.axaml.cs

4. Retained scene graph foundation

The branch introduces a retained scene graph as a real runtime representation, not just a transient export format.

The retained layer now covers:

  • scene compilation from SVG DOM
  • retained node/resource indexing
  • retained rendering
  • retained mutation routing
  • retained resource ownership for clip, mask, filter, paint, and text payloads
  • retained-node hit testing
  • retained-scene-based tooling helpers

The compiler progressively moved away from drawable-bridge fallbacks and toward direct retained compilation for core shapes, structural wrappers, text, masks, filters, use, switch, and image-backed scenarios.

Key files:

  • src/Svg.SceneGraph/SvgSceneCompiler.cs
  • src/Svg.SceneGraph/SvgSceneDocument.cs
  • src/Svg.SceneGraph/SvgSceneNode.cs
  • src/Svg.SceneGraph/SvgSceneResource.cs
  • src/Svg.SceneGraph/SvgSceneRenderer.cs
  • src/Svg.SceneGraph/SvgSceneRuntime.cs

5. Major-release rendering cutover away from drawable-first public rendering APIs

Because this work targets a future major release, the branch starts removing drawable-first public rendering flows instead of preserving backward-compatible shims.

This cutover includes:

  • routing SKSvg render/model creation through retained scene compilation
  • removing SvgService.ToDrawable(...) / SvgService.ToModel(...)
  • switching Svg.Controls.Avalonia, source generation, and CLI/sample consumers to retained-scene model generation
  • shrinking drawable usage down to remaining internal compatibility seams instead of public rendering APIs

Key files:

  • src/Svg.Skia/SKSvg.Model.cs
  • src/Svg.Model/Services/SvgService.cs
  • src/Svg.Controls.Avalonia/SvgSource.cs
  • src/Svg.SourceGenerator.Skia/SvgSourceGenerator.cs
  • samples/SvgToPng/ViewModels/MainWindowViewModel.cs
  • samples/svgc/Program.cs

6. Animation performance and invalidation follow-up

The branch adds layered animation redraw and then pushes further toward retained-scene incremental behavior.

This includes:

  • static/animated layer caching
  • subtree animation invalidation
  • retained-scene-driven animation fallback rendering
  • fixes for inherited animated attributes and resource-driven invalidation
  • benchmark coverage for frame advancement

Key files:

  • src/Svg.Skia/SKSvg.AnimationLayers.cs
  • tests/Svg.Skia.Benchmarks/SvgAnimationFrameBenchmarks.cs

7. Project structure cleanup

The branch splits the new architecture into dedicated projects:

  • src/Svg.SceneGraph/Svg.SceneGraph.csproj
  • src/Svg.Animation/Svg.Animation.csproj

This keeps:

  • retained-scene runtime/compiler/resource logic in Svg.SceneGraph
  • shared animation runtime in Svg.Animation
  • SkiaSharp conversion, host integration, and SKSvg façade code in Svg.Skia

It also removes unshipped obsolete compatibility APIs and replaces reflection-based internal access with explicit runtime bridges where possible.

Documentation And Planning Artifacts

The branch adds and updates implementation docs and plans for the new architecture, including:

  • plan/svg-interaction-animation-phased-implementation.md
  • plan/svg-retained-scene-graph-rewrite-spec.md
  • plan/remaining-scene-graph-animation-work-plan.md
  • plan/scene-graph-major-release-cutover-plan.md
  • plan/animation-project-split-plan.md

User-facing docs were also refreshed earlier in the branch, including:

  • README.md
  • CHANGELOG.md
  • site/articles/guides/interaction-and-animation.md
  • package docs for Svg.Custom, Svg.Skia, Svg.Controls.Skia.Avalonia, and Svg.Controls.Skia.Uno

Validation

Branch work included repeated validation with:

  • dotnet format Svg.Skia.slnx --no-restore
  • dotnet build Svg.Skia.slnx -c Release
  • dotnet test Svg.Skia.slnx -c Release

Recent validation specifically covered:

  • successful build of the new Svg.SceneGraph split
  • successful build of the new Svg.Animation split
  • retained-scene regression coverage for background-image filters, invalid filter suppression, and hit testing
  • targeted resvg regression slices for e-feDiffuseLighting and malformed e-feConvolveMatrix

Expanded tests include work in:

  • tests/Svg.Model.UnitTests/*
  • tests/Svg.Skia.UnitTests/HitTestTests.cs
  • tests/Svg.Skia.UnitTests/SvgAnimationControllerTests.cs
  • tests/Svg.Skia.UnitTests/SvgRetainedSceneGraphTests.cs
  • tests/Svg.Skia.UnitTests/SKSvgNativeCompositionTests.cs
  • tests/Svg.Controls.Avalonia.UnitTests/SvgSourceTests.cs
  • tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgSourceTests.cs

Architectural Outcome

The branch is no longer just “animation support.”

It now delivers:

  • shared SVG animation and interaction support
  • retained native composition support in Avalonia
  • a retained scene graph runtime with direct DOM compilation
  • major-release migration away from drawable-first rendering APIs
  • separate Svg.SceneGraph and Svg.Animation projects

The remaining long-term direction is clear:

  • SvgDocument remains the DOM/source model
  • Svg.SceneGraph is the retained render/runtime representation
  • Svg.Animation owns shared animation timing/evaluation/runtime behavior
  • Svg.Skia becomes the Skia/host integration layer on top of those shared runtimes

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 982e46bb46

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Svg.Animation/Animation/SvgAnimationController.cs Outdated
Comment thread src/Svg.Animation/Animation/SvgAnimationController.cs
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a736db88a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Svg.Skia/SKSvg.AnimationLayers.cs Outdated
Comment thread src/Svg.Skia/SKSvg.NativeComposition.cs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e5c315dd9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Svg.Skia/Animation/SvgAnimationController.cs Outdated
Comment thread src/Svg.Skia/SKSvg.AnimationLayers.cs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 094d28ebf5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Svg.Model/Services/GeometryHitTestService.cs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13f80475d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1918 to +1920
else if (!string.IsNullOrWhiteSpace(binding.BaseValueString))
{
resolved.Add(binding.BaseValueString!);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Seed to-only animations with implicit base value

When values/from are absent, this path only seeds resolved from binding.BaseValueString if it is non-whitespace; for attributes like transform, the underlying base can be represented as an empty string (identity), so a valid to-only animation (for example <animateTransform type="translate" to="10 0" dur="2s"/>) ends up with a single value. TryResolveTransformValue then treats it as discrete (values.Count == 1) and applies the final transform immediately instead of interpolating from the base state.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f2bc1cf9f3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

instances.AddRange(eventTimes.Select(eventTime => new ResolvedTimingInstance(
eventTime + spec.Offset,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard event-time offset additions against TimeSpan overflow

When resolving event-based begin/end specs, eventTime + spec.Offset can throw OverflowException for large timestamps (for example, if callers seek/advance the clock near TimeSpan.MaxValue and use a positive offset like click+1s). This happens during frame-state evaluation and aborts animation processing instead of treating the timing instance as out-of-range. Wrapping this addition in overflow-safe logic (clamp or skip invalid instance) avoids hard failures on extreme but reachable runtime times.

Useful? React with 👍 / 👎.

{
var totalDuration = ComputeTotalDuration(animation, simpleDuration, explicitEnd, begin);
return totalDuration.HasValue
? begin + totalDuration.Value
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Saturate active-end calculation instead of overflowing

ComputeActiveEnd returns begin + totalDuration, but totalDuration is already allowed to saturate to TimeSpan.MaxValue in Multiply; with any positive begin (e.g., begin="1s" plus very large repeatCount), this addition overflows and throws during interval resolution. That turns a valid animation timing configuration into a runtime crash; the active-end computation should clamp/saturate at TimeSpan.MaxValue rather than relying on unchecked TimeSpan addition.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af1a8baab0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +801 to +804
if (!node.IsRenderable)
{
return true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Continue traversing children of non-renderable static nodes

RenderStaticNodeToCanvas returns immediately when !node.IsRenderable, which skips the entire subtree during animation-layer static rendering. In the main retained renderer (SvgSceneRenderer.RenderNodeToCanvas), non-renderable containers are still traversed so descendants can render (for example a visibility="hidden" group with a descendant that sets visibility="visible"). With layer caching enabled, those descendants disappear from the static layer, so animated documents can render differently from the normal retained path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 34deb36cc1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1014 to +1017
if (SvgAnimationParser.TryParseInvariantDouble(trimmed.AsSpan(), out repeatCount))
{
repeatCount = Math.Max(0d, repeatCount);
return RepeatCountMode.Finite;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject negative repeatCount values

ParseRepeatCount currently clamps any negative numeric repeatCount to 0, which makes malformed inputs like repeatCount="-1" resolve to a zero active duration and immediately force end-state behavior at begin. This is a runtime correctness issue for invalid-but-common authoring mistakes: instead of falling back to normal timing semantics, animations can jump to final state or appear to not run at all.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf7d85ca0e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1931 to +1933
var additiveBaseValue = resolved.Count > 0 ? resolved[resolved.Count - 1] : binding.BaseValueString;
if (additiveBaseValue is { } && TryAddValue(binding, additiveBaseValue, byValue, out var sumValue))
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Seed implicit base for by-only animation resolution

When values/from/to are absent, this by path uses binding.BaseValueString directly as the additive seed; for attributes whose underlying base is implicit (commonly transform identity represented as empty/null), TryAddValue(...) fails and resolved stays empty, so valid by-only animations are dropped entirely. A case like <animateTransform type="translate" by="10 0" dur="2s"/> on an element without an explicit transform never animates even though SVG timing expects interpolation from the underlying value.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant