Skip to content

AnimatedVisualPlayer: Port WinUI implementation#23889

Open
morning4coffe-dev wants to merge 4 commits into
feature/breakingchangesfrom
agents/animated-visual-player-port
Open

AnimatedVisualPlayer: Port WinUI implementation#23889
morning4coffe-dev wants to merge 4 commits into
feature/breakingchangesfrom
agents/animated-visual-player-port

Conversation

@morning4coffe-dev

Copy link
Copy Markdown
Member

GitHub Issue: closes #23881

PR Type:

✨ Feature

What changed? 🚀

Ports AnimatedVisualPlayer directly from Microsoft WinUI commit 3cae15f0 for the Skia renderer.

  • Aligns playback, lifecycle, fallback content, diagnostics, dynamic sources, animation optimization, and reverse playback with WinUI.
  • Implements the required Composition AnimationController playback-rate, progress, seeking, and commit behavior.
  • Migrates Uno.UI.Lottie to IAnimatedVisualSource3/IAnimatedVisual2 and Composition-driven rendering.
  • Removes the legacy player-driven animated visual path.
  • Adds AVP, Composition, and production Lottie regression coverage.

This targets feature/breakingchanges. The public Uno-only members previously added to IAnimatedVisualSource are removed in favor of the WinUI contract.

PR Checklist ✅

Validation

  • Uno.UI.Composition.Skia build
  • Uno.UI.Skia build
  • Uno.UI.Lottie.Skia build
  • Uno.UI.UnitTests build
  • AnimatedVisualPlayer runtime tests: 27/27 passed
  • Focused Composition/Lottie unit tests: 3/3 passed
  • git diff --check

@github-actions github-actions Bot added platform/android 🤖 Categorizes an issue or PR as relevant to the Android platform area/skia ✏️ Categorizes an issue or PR as relevant to Skia area/automation Categorizes an issue or PR as relevant to project automation labels Jul 27, 2026
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @morning4coffe-dev's task in 6m 58s —— View job


Review complete

  • Gather PR context and diff
  • Review core AnimatedVisualPlayer implementation (AnimatedVisualPlayer.mux.cs, AnimatedVisualPlayer.Properties.cs)
  • Review Composition layer changes (AnimationController, KeyFrameEvaluator, CompositionPropertySet, Compositor)
  • Review Lottie add-in changes (LottieVisualSourceBase, LottieVisualSource.skia.cs)
  • Review interface contracts (IAnimatedVisual2, IAnimatedVisualSource3, IDynamicAnimatedVisualSource)
  • Review test coverage (Given_AnimatedVisualPlayer.cs, unit tests)
  • Check async/cancellation discipline
  • Check cross-platform correctness
  • Check DependencyProperty patterns
  • Check code style (braces, events, generated files)
  • Post inline comments and summary

Summary

Verdict: fix-first — one blocking-thread issue; the rest are lower-severity.

Issues found

Severity File Finding
🔴 High LottieVisualSource.skia.cs:65 UI thread blocked (inline comment)
🟡 Medium KeyFrameEvaluator.cs:46 Dead ternary — both branches identical (inline comment)
🟡 Medium KeyFrameEvaluator.cs:94,104,111 O(n) LINQ scans on render hot path (see below)
🟢 Low LottieVisualSourceBase.cs null! masking genuine null return

O(n) LINQ scans in EvaluateInternal (KeyFrameEvaluator.cs:94,104,111): _keyFrames.Keys.Last(), _keyFrames.Keys.FirstOrDefault(...), and _keyFrames.Keys.LastOrDefault(...) are all O(n) linear walks over SortedDictionary.KeyCollection. _keyFrames is immutable after construction, so caching _finalKey = _keyFrames.Keys.Last() as a constructor-time field eliminates one O(n) scan per frame per animated property. The FirstOrDefault / LastOrDefault scans on the remaining two lines would benefit from a SortedList<float, T> (which supports O(log n) indexed lookups) but that's a larger refactor. At minimum, cache _finalKey.

null! return in LottieVisualSourceBase.TryCreateAnimatedVisualCore (line ~125): the method returns IAnimatedVisual2 but one branch returns null! to silence the nullable warning. The callers handle null correctly, but the return type should be IAnimatedVisual2? so the compiler can enforce null checks at every call site rather than relying on null! suppression.

What looks good

  • WinUI commit 3cae15f0 is accurately ported — field names, reentrance comments, and lifecycle semantics faithfully follow the original.
  • Events consistently use TypedEventHandler and EventHandler — no event Action anywhere.
  • DependencyProperty registrations all use nameof(), correct owner/property types, and appropriate FrameworkPropertyMetadataOptions.
  • CompositionScopedBatch.End() defers Completed via the dispatcher queue, correctly matching WinUI's cross-thread semantics and avoiding re-entrant stack overflows.
  • RequestCommitAsync now uses a real scoped-batch fence instead of Task.CompletedTask, ensuring CompleteDestroyAnimations runs after the compositor processes pending commands.
  • CompositionPropertySet.SetValue early-return for unchanged values is correct — StopAnimation still fires unconditionally before the equality check.
  • Test class correctly gates with #if __SKIA__, uses [RunsOnUIThread] at class level, includes [TestCleanup], and exercises 27 behavioral scenarios including fallback content, dynamic source reload, reverse playback, and animation optimization modes.
  • Platform file suffixes (*.skia.cs, *.reference.cs) used correctly. Generated stubs updated to "Skipping already declared" comments — no hand-edited Generated/ implementations.
  • #nullable enable is present in all new files.

{
try
{
var animation = Task.Run(() => CreateAnimation(animationJson)).GetAwaiter().GetResult();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking the UI threadGetAwaiter().GetResult() on a Task.Run call blocks the calling thread while Skottie parses the JSON animation. This method is called from TryCreateAnimatedVisualCoreUpdateContentOnSourcePropertyChanged, all on the UI thread. For larger Lottie assets this stalls the UI for the full parse duration.

Suggested fix: push the synchronous parse off the UI thread using the same async/dispatch pattern LottieVisualSourceBase already uses for LoadAnimationAsync, or at minimum wrap TryCreate so LottieAnimatedVisual.TryCreate is called from a background thread and the result is marshalled back before TryCreateAnimatedVisual returns — though that would require making TryCreateAnimatedVisual async, which is a deeper change.

At a minimum, CreateAnimation should not be wrapped in Task.Run(...).GetAwaiter().GetResult() — if Skottie's Animation.TryCreate must remain synchronous here, call it directly without the Task.Run wrapper (which just adds overhead without any UI-thread protection) and document the blocking behaviour explicitly.

// Before (blocks UI thread via Task.Run + GetResult):
var animation = Task.Run(() => CreateAnimation(animationJson)).GetAwaiter().GetResult();

// After (call directly on UI thread; document that it blocks, or refactor to async):
var animation = CreateAnimation(animationJson);

Fix this →

_finalValue = finalValue;
_duration = duration;
_totalDuration = iterationBehavior == AnimationIterationBehavior.Forever ? TimeSpan.MaxValue : duration * iterationCount;
_iterationCount = iterationBehavior == AnimationIterationBehavior.Forever ? Math.Max(iterationCount, 1) : Math.Max(iterationCount, 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dead ternary — both branches are identical. The condition iterationBehavior == AnimationIterationBehavior.Forever controls which branch executes, but both produce exactly Math.Max(iterationCount, 1). The Forever branch presumably wanted to avoid clamping to iterationCount (since the loop is infinite, the count doesn't bound the playhead), but the value written to _iterationCount is the same regardless.

GetCurrentProgress already guards the Forever case independently (it uses WrapProgress instead of _iterationCount), so this doesn't introduce a runtime bug — but it's misleading code. Consider simplifying:

// Current (dead ternary):
_iterationCount = iterationBehavior == AnimationIterationBehavior.Forever
    ? Math.Max(iterationCount, 1)
    : Math.Max(iterationCount, 1);

// Suggested:
_iterationCount = Math.Max(iterationCount, 1);

Fix this →

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23889/wasm-skia-net9/index.html

@nventive-devops

Copy link
Copy Markdown
Contributor

The build 224209 found UI Test snapshots differences: skia-linux-screenshots: 67, skia-windows-screenshots: 102

Details
  • skia-linux-screenshots: 67 changed over 2368

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • DataTransferManager.png
    • Gamepad_Enumeration.png-dark
    • Buttons.png-dark
    • Buttons.png
    • EllipsemaskingEllipseGrid.png
    • ContextRequested.png-dark
    • ContextRequested.png
    • BitmapIcon_Foreground.png
    • DisplayInformation.png-dark
    • Focus_FocusVisual_Properties.png-dark
    • Focus_FocusVisual_Properties.png
    • Image_AnimatedWebP.png
    • Examples.png-dark
    • Examples.png
    • Image_Margin_Large.png-dark
    • ClipboardTests.png-dark
    • DoubleImageBrushInList.png
    • Gamepad_CurrentReading.png-dark
    • ElementLevelTheme.png-dark
    • ImageIconPage.png-dark
  • skia-windows-screenshots: 102 changed over 2392

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • Buttons.png
    • Examples.png
    • ExpanderColorValidationPage.png-dark
    • ButtonClippingTestsControl.png-dark
    • DisplayInformation.png-dark
    • ClipboardTests.png-dark
    • ClipboardTests.png
    • ButtonClippingTestsControl.png
    • Buttons.png-dark
    • Gamepad_CurrentReading.png-dark
    • ImageBrushInList.png-dark
    • ImageBrushInList.png
    • Image_UseTargetSize.png-dark
    • Gamepad_Enumeration.png-dark
    • DropDownButtonPage.png-dark
    • DropDownButtonPage.png
    • Image_AnimatedWebP.png-dark
    • CalendarView_Theming.png-dark
    • CalendarView_Theming.png
    • ImageIconPage.png-dark

@unodevops

Copy link
Copy Markdown
Contributor

⚠️⚠️ The build 224209 has failed on Uno.UI - CI.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @morning4coffe-dev's task in 8m 36s —— View job


Review complete

  • Gather PR context and diff
  • Check whether previous review findings were addressed
  • Review updated files for new issues
  • Post inline comments
  • Update summary

Summary

Verdict: fix-first — the blocking-thread call and the new lock inconsistency both need to land before merge; the remainder are lower-severity.

Previous inline findings — still unresolved (inline comments already exist on the PR):

Severity File Finding
🔴 High LottieVisualSource.skia.cs:65 Task.Run(...).GetAwaiter().GetResult() blocks UI thread
🟡 Medium KeyFrameEvaluator.cs:46 Dead ternary — both branches produce Math.Max(iterationCount, 1)

New findings from this review pass:

Severity File Finding
🟡 Medium LottieVisualSourceBase.cs:230 _hasPendingAnimatedVisualInvalidation / _pendingAnimatedVisualInvalidation reset outside _stateGate lock in ClearLoadState, inconsistent with every other write site — thread-safety issue (inline comment posted)
🟢 Low LottieVisualSourceBase.cs:126 null! suppression on a non-nullable return type; change to IAnimatedVisual2? (inline comment posted)

O(n) LINQ scans — pre-existing, not in diff: _keyFrames.Keys.Last() (line 94), FirstOrDefault (line 104), LastOrDefault (line 111) in EvaluateInternal run on every compositor tick per animated property. These lines predate this PR, so no inline comment can be posted, but they should be addressed: at minimum cache _finalKey = _keyFrames.Keys.Last() at construction time to eliminate one O(n) scan per frame.

What landed in the fix commit (a1f20249) and looks correct:

  • LottieVisualSourceBase async wiring (OnAnimationDataChanged / PublishLoadFailure now properly awaited; RaiseAnimatedVisualInvalidatedAsync propagated through)
  • UIElement._elementVisualCompositor correctly moved from UIElement.cs to UIElement.crossruntime.cs — avoids the field being present on native Android/iOS where it's unused
  • RequestCommitAsyncCore in Compositor uses a scoped-batch fence (correctly defers to the next dispatcher turn)
  • Given_ProgressRing test revised to use behavioural assertions instead of a pixel-diff comparison that was inherently fragile


if (animationJson is null)
{
return hasLoadFailure ? null! : CreatePendingAnimatedVisual(compositor);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

null! on a non-nullable return typeTryCreateAnimatedVisualCore is declared as returning IAnimatedVisual2 (non-nullable), but this line and line 131 both return null!, silencing the compiler warning with a null-forgiving operator. The WinUI TryCreateAnimatedVisual contract permits null, so the correct fix is to change the method's return type to IAnimatedVisual2?:

Suggested change
return hasLoadFailure ? null! : CreatePendingAnimatedVisual(compositor);
return hasLoadFailure ? null : CreatePendingAnimatedVisual(compositor);

Then update the method signature at line 106:

private IAnimatedVisual2? TryCreateAnimatedVisualCore(Compositor compositor, out object diagnostics, bool createAnimations)

and fix line 131 to drop ?? null!. With a ? return type the compiler enforces null checks at every call site instead of relying on ! suppression.

throw new NotImplementedException();
_loadRevoker.Disposable = null;
_animationDataSubscription.Disposable = null;
_hasPendingAnimatedVisualInvalidation = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lock inconsistency for _hasPendingAnimatedVisualInvalidation and _pendingAnimatedVisualInvalidation — these two fields are read and written inside lock (_stateGate) in RaiseAnimatedVisualInvalidatedAsync and reset inside lock (_stateGate) in RaiseAnimatedVisualInvalidatedCore, but here in ClearLoadState they are reset outside the lock. A background LoadAnimationAsync task can concurrently enter RaiseAnimatedVisualInvalidatedAsync, acquire the lock, observe _hasPendingAnimatedVisualInvalidation == false, and set it to true — then the UI thread runs ClearLoadState and resets it to false without the lock, leaving the coalescing flag corrupted. Wrap these two lines in lock (_stateGate) for consistency with every other write site:

Suggested change
_hasPendingAnimatedVisualInvalidation = false;
lock (_stateGate)
{
_hasPendingAnimatedVisualInvalidation = false;
_pendingAnimatedVisualInvalidation = Task.CompletedTask;
}

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23889/wasm-skia-net9/index.html

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @morning4coffe-dev's task in 4m 53s —— View job


Review complete

  • Gather PR context and diff
  • Review updated files against previous findings
  • Check new changes since last review pass (a1f20249 CI fixes, 279e8c61 security fix)
  • Post inline comments
  • Update summary

Summary

Verdict: fix-first — the 4 findings from the previous two review passes remain open. No new blocking issues were introduced by the latest two commits.

What landed in a1f20249 (CI failures fix) — all correct ✓

  • PublishLoadFailure / OnAnimationDataChanged are now properly awaited — fire-and-forget bug fixed.
  • _pendingAnimatedVisualInvalidation field initialised at declaration and reset in ClearLoadState.
  • await initialInvalidation ensures the first invalidation completes before _animationDataSubscription is assigned.
  • UIElement._elementVisualCompositor moved to UIElement.crossruntime.cs (Skia-only file) — correct platform targeting.
  • HasCompositionChildVisual in UIElement.cs + FrameworkElement.IsViewHit() delegate to it: the field defaults false, so non-Skia behaviour is unchanged (the previous return was also false).
  • Given_ProgressRing revised to behavioural assertions.

What landed in 279e8c61 (security fix) — correct ✓

  • Uri.UnescapeDataString applied before splitting segments — prevents %2F-encoded path traversal (e.g. appdata://local/..%2Fsecret).
  • Path.Combine(canonicalRoot, relativePath) + Path.GetFullPath + boundary check correctly rejects all traversal patterns verified by tracing.

Still-open inline findings (inline comments already posted)

Severity File Finding
🔴 High LottieVisualSource.skia.cs:65 Task.Run(...).GetAwaiter().GetResult() blocks UI thread
🟡 Medium LottieVisualSourceBase.cs:229 _hasPendingAnimatedVisualInvalidation / _pendingAnimatedVisualInvalidation reset outside _stateGate lock in ClearLoadState
🟡 Medium KeyFrameEvaluator.cs:46 Dead ternary — both branches identical
🟢 Low LottieVisualSourceBase.cs:125 null! suppression on non-nullable return type; change to IAnimatedVisual2?

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23889/wasm-skia-net9/index.html

@nventive-devops

Copy link
Copy Markdown
Contributor

The build 224238 found UI Test snapshots differences: skia-linux-screenshots: 67, skia-windows-screenshots: 95

Details
  • skia-linux-screenshots: 67 changed over 2368

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • BitmapIcon_Foreground.png
    • Buttons.png-dark
    • Buttons.png
    • ButtonClippingTestsControl.png-dark
    • DisplayInformation.png-dark
    • Gamepad_Enumeration.png-dark
    • DynamicFont.png
    • Gamepad_CurrentReading.png-dark
    • Focus_FocusVisual_Properties.png-dark
    • Focus_FocusVisual_Properties.png
    • ClipboardTests.png-dark
    • ContextRequested.png-dark
    • ContextRequested.png
    • Examples.png-dark
    • Examples.png
    • ImageIconPage.png-dark
    • ImageIconPage.png
    • DataTransferManager.png
    • EllipsemaskingEllipseGrid.png
    • Image_AnimatedWebP.png
  • skia-windows-screenshots: 95 changed over 2392

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • Gamepad_CurrentReading.png-dark
    • ImageBrushAlignmentXY.png
    • ImageBrushInList.png-dark
    • ImageBrushInList.png
    • CalendarView_Theming.png-dark
    • CalendarView_Theming.png
    • DisplayInformation.png-dark
    • ElementLevelTheme.png-dark
    • ElementLevelTheme.png
    • Buttons.png-dark
    • Buttons.png
    • ImageIconPage.png-dark
    • ButtonClippingTestsControl.png-dark
    • ColorPickerSample.png-dark
    • ButtonClippingTestsControl.png
    • ClipboardTests.png-dark
    • ClipboardTests.png
    • Focus_FocusVisual_Properties.png-dark
    • DropDownButtonPage.png-dark
    • DropDownButtonPage.png

@github-actions github-actions Bot added the area/build Categorizes an issue or PR as relevant to build infrastructure label Jul 27, 2026
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @morning4coffe-dev's task in 1m 38s —— View job


Review complete

  • Gather PR context and diff
  • Check whether previous review findings were addressed
  • Review latest commit (407ba68a) for new issues
  • Post inline comments
  • Update summary

Summary

Verdict: fix-first — the 4 findings from previous review passes remain open. The latest commit introduces no new issues.

Latest commit (407ba68a ci: Allow legacy animated source removals) ✓

Adds 9 entries to build/PackageDiffIgnore.xml suppressing the removal of Uno-only IAnimatedVisualSource extension methods (Update, Load, Unload, Play, Stop, Pause, Resume, SetProgress, Measure) in favour of the WinUI composition contract. The entries are correct in shape, carry accurate reason strings, and align with the feature/breakingchanges target.

Still-open findings from previous passes (inline comments already posted)

Severity File Finding
🔴 High LottieVisualSource.skia.cs:65 Task.Run(...).GetAwaiter().GetResult() blocks the UI thread during Skottie JSON parse
🟡 Medium LottieVisualSourceBase.cs:229 _hasPendingAnimatedVisualInvalidation / _pendingAnimatedVisualInvalidation reset outside _stateGate lock in ClearLoadState — inconsistent with every other write site
🟡 Medium KeyFrameEvaluator.cs:46 Dead ternary — both branches produce Math.Max(iterationCount, 1)
🟢 Low LottieVisualSourceBase.cs:125 null! suppression on non-nullable return type; change to IAnimatedVisual2?

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23889/wasm-skia-net9/index.html

@nventive-devops

Copy link
Copy Markdown
Contributor

The build 224270 found UI Test snapshots differences: skia-linux-screenshots: 67, skia-windows-screenshots: 103

Details
  • skia-linux-screenshots: 67 changed over 2368

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • ClipboardTests.png-dark
    • Examples.png-dark
    • Examples.png
    • BitmapIcon_Foreground.png
    • DisplayInformation.png-dark
    • ElementLevelTheme.png-dark
    • Buttons.png-dark
    • Buttons.png
    • Gamepad_Enumeration.png-dark
    • Image_AnimatedWebP.png
    • ContextRequested.png-dark
    • ContextRequested.png
    • DynamicFont.png
    • Focus_FocusVisual_Properties.png-dark
    • Focus_FocusVisual_Properties.png
    • EllipsemaskingEllipseGrid.png
    • ImageIconPage.png-dark
    • ImageIconPage.png
    • ButtonClippingTestsControl.png-dark
    • CalendarView_Theming.png-dark
  • skia-windows-screenshots: 103 changed over 2392

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • Gamepad_CurrentReading.png-dark
    • ColorPickerSample.png-dark
    • Focus_FocusVisual_Properties.png-dark
    • ElementLevelTheme.png-dark
    • ElementLevelTheme.png
    • Image_AnimatedWebP.png-dark
    • ButtonClippingTestsControl.png-dark
    • ButtonClippingTestsControl.png
    • DropDownButtonPage.png-dark
    • DropDownButtonPage.png
    • Examples.png
    • ExpanderColorValidationPage.png-dark
    • Gamepad_Enumeration.png-dark
    • Image_Stretch_None.png-dark
    • Image_UseTargetSize.png-dark
    • Buttons.png-dark
    • Buttons.png
    • DisplayInformation.png-dark
    • ImageBrushInList.png
    • Image_UseTargetSizeLate.png-dark

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

Labels

area/automation Categorizes an issue or PR as relevant to project automation area/build Categorizes an issue or PR as relevant to build infrastructure area/skia ✏️ Categorizes an issue or PR as relevant to Skia platform/android 🤖 Categorizes an issue or PR as relevant to the Android platform

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants