Skip to content

[Copilot] Add FlatBuffer runtime interpreter for Lottie animations - #583

Open
jonwis wants to merge 31 commits into
CommunityToolkit:mainfrom
jonwis:jonwis-build-windows-interpreter-sample-tool
Open

jonwis wants to merge 31 commits into
CommunityToolkit:mainfrom
jonwis:jonwis-build-windows-interpreter-sample-tool

Conversation

@jonwis

@jonwis jonwis commented Sep 1, 2026

Copy link
Copy Markdown

Summary

Adds a data-driven runtime path for Lottie animations:

  1. LottieGen -Language flatbuffer converts Lottie JSON into a compact .lcomp FlatBuffer.
  2. LottieRuntime validates the FlatBuffer and hydrates the corresponding Windows.UI.Composition visual tree.
  3. Applications either compile the interpreter directly into native code or share it as a registration-free COM DLL.

The serialized graph supports the Composition objects, animations, effects, geometries, loaded images, and custom animation controllers emitted by the existing native generators.

Motivation

Generated C++/CX and C++/WinRT implementations duplicate the code needed to construct every object, property, animation, and expression in each animation. Complex animations produce large generated translation units and linked binaries. That increases DLL size, loaded code pages, private working set, and duplicated WinRT ABI call-site code.

The FlatBuffer path keeps each animation as data. One shared interpreter hydrates any number of animations into the same Composition object graph the generated implementations produce. Applications pay the interpreter cost once, then add only the FlatBuffer payload for each additional animation. The COM form can also be shared across binaries.

Size comparison

Measured with LottieSamples/Assets/LottieLogo1.json, targeting UAP 15 on both paths. Both native binaries used /O1, /Gy, string pooling, /GL + /LTCG, /OPT:REF, /OPT:ICF, and non-incremental linking.

Payload Bytes KiB
Generated C++/WinRT DLL 198,656 194.0
FlatBuffer (LottieLogo1.lcomp) 28,120 27.5
Shared interpreter DLL 155,136 151.5
FlatBuffer + interpreter for first animation 183,256 179.0

The first interpreted animation is 15,400 bytes (7.8%) smaller than the generated DLL, including the complete interpreter. Each additional animation adds only its FlatBuffer: 28,120 bytes instead of another 198,656-byte generated implementation for this sample, an 85.8% reduction in marginal binary payload.

The generated comparison DLL exports one creation entry point to keep the complete generated graph construction path reachable. The measurement excludes application host code and dependencies common to both approaches.

Runtime and ABI

LottieRuntime:

  • verifies the LCMP identifier and FlatBuffer structure before reading data;
  • validates the buffer's required Universal API contract before hydration;
  • range-checks graph references and rejects geometry cycles;
  • uses dense index-based caches so each string and Composition node is realized once;
  • builds Direct2D path geometry without taking a Win2D dependency;
  • exposes ILottieCompositionLoader::LoadComposition(UINT32, BYTE const*, REFIID, void**) as its DLL ABI;
  • supports registration-free COM activation through LottieRuntime.manifest;
  • keeps the C++ LoadComposition(Compositor, span<byte>) API available to applications that compile the implementation directly.

The DLL exports only DllGetClassObject and DllCanUnloadNow. C++ projection types and exceptions do not cross the DLL boundary. The DLL targets Windows 10 1809 / UniversalApiContract v7; individual buffers declare any newer contract they require.

Tooling

LottieRuntime.exe <path-to-flatbuffer> [--dump|--show] provides one native test and inspection tool:

  • no option: validate and load the composition;
  • --dump: print the hydrated Composition object hierarchy;
  • --show: host and animate the resulting visual in a Win32 window.

Distribution

LottieGen.nupkg includes the complete native runtime source set under LottieRuntime/:

  • direct-link C++ implementation and header;
  • COM ABI, class factory, module definition, manifest, and native project;
  • generated FlatBuffer C++ binding;
  • pinned FlatBuffers headers and licenses;
  • integration instructions for C++/WinRT and managed applications.

C++/WinRT applications can compile LottieRuntime.cpp directly into their product. Managed applications add LottieRuntime.vcxproj to the solution and copy LottieRuntime.dll plus LottieRuntime.manifest into application outputs.

Validation

  • FlatBuffer serializer/deserializer corpus, robustness, and coverage tests: 23 passed.
  • LottieLogo1.json converted to .lcomp, activated through registration-free COM, and hydrated successfully.
  • LottieRuntime.exe validate, dump, and show modes exercised on Release x64.
  • Runtime DLL and tool compile for x64 and ARM64.
  • Runtime project extracted from LottieGen.nupkg and built independently.

(via Copilot)

Copilot AI and others added 26 commits August 31, 2026 17:49
Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
…/writer

Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
…pData graph

Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
…++ verifier

Metadata is serialized last but its strings must be present in the string
table, which is written first. Intern them up front, then freeze the table
so a missed interning site throws instead of handing out an out of range
index. Also store marker durations as a proportion of the animation and
carry the composition width and height through to the metadata.

Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
The managed counterpart of the native interpreter. It exists so that the
wire format can be round tripped and compared with the original graph,
which is a much stronger check than testing the serializer alone. Every
index is bounds checked and the buffer is verified before it is read,
because buffers are untrusted input.

Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
…d graphs

Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
…y kind

Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
- Move #include <unknwn.h> before LottieRuntime.h (which pulls in
  C++/WinRT headers), satisfying the classic-COM interop requirement.
- Use winrt::implements<> with the projected interface listed first
  for GeometrySource and Effect, so winrt::make<> returns a com_ptr
  convertible to CompositionPath / IGraphicsEffect directly.
- Fix StrokeMiterLimit cast (was uint32_t, should be float).
- Default-construct WinRT projection type caches with nullptr fill
  value; these types have no default constructor.
- Link dxguid.lib for D2D1 effect CLSIDs (Composite, GaussianBlur).
A console tool that links LottieRuntime.vcxproj, loads a serialized
Lottie composition (.lcomp) given as its command-line argument, and
prints the composition object hierarchy of the resulting visual tree:
visuals, their shapes (recursively through container shapes), fill and
stroke brushes, and gradient stops. Each line shows the runtime class
name and, where set, the Comment property, so the printed tree can be
matched back to the source Lottie layers.

Builds via project reference to LottieRuntime.vcxproj and copies
LottieRuntime.dll next to the exe as a post-build step, since C++
project references do not do this automatically.
Defines ILottieCompositionLoader (IID 3874B71B-05E6-42A9-B838-1AE7F0BA4E1C),
a classic COM interface accepting (UINT32 length, BYTE const* buffer, REFIID,
void**) that wraps LoadComposition for callers without a C++/WinRT toolchain.
Implements it as LottieCompositionLoader (CLSID 9F7B7089-F512-4BBC-81FE-4FF12B4DA159)
via winrt::implements<>, plus DllGetClassObject/DllCanUnloadNow exports and a
LottieRuntime.def to keep those names undecorated.

The class is never registered with the system: LottieRuntime.manifest is a
private-assembly manifest declaring the comClass, copied next to a consuming
exe and named directly by an ACTCTX, so CoCreateInstance resolves the CLSID
without any HKEY_CLASSES_ROOT entry.

Adds LottieRuntimeComTest, a console harness that builds an activation
context from that manifest, activates it, CoCreateInstances the loader, and
calls LoadComposition on a file argument.

Known issue: CoCreateInstance currently returns E_ACCESSDENIED (0x80070005)
even though CreateActCtx/ActivateActCtx succeed and the DLL exports
DllGetClassObject/DllCanUnloadNow correctly - the private-assembly activation
path needs further debugging before this is fully working end-to-end.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
Create the desktop DispatcherQueue lazily before activating the loader's Compositor. This keeps CoCreateInstance side-effect-free and gives Windows.UI.Composition the thread infrastructure it requires.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
Use C++/WinRT's module lock for IClassFactory::LockServer and DllCanUnloadNow so COM cannot unload the DLL while its objects remain alive.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
Build the COM test, record it under Time Travel Debugging, and collect optional SideBySide and COM event logs into one directory.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
Remove the one-off investigation helper now that the activation failure is diagnosed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
Handle omitted property-set state, expose the required effect-source interface, cache and cycle-check canvas geometries and strings, preserve sprite shadows, and load URI and stream image surfaces.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
Use the FlatBuffers headers and flatc from pinned upstream default-branch commit 5761d6e for C++. Keep C# generation on the 25.2.10 runtime available through Microsoft's configured NuGet feed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
Create custom controllers explicitly, pass them to StartAnimation, and initialize root property sets before descending into children so controller expressions can bind to root progress.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
Remove exported C++ symbols, require callers to own the DispatcherQueue, support architecture-neutral registration-free activation, load embedded image streams synchronously, and make the native project portable from its packaged source layout.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
Replace separate validation and dump executables with LottieRuntime.exe supporting validate, --dump, and --show modes. Document JSON conversion and package the complete native runtime source set, COM project, generated binding, and FlatBuffers headers in LottieGen.nupkg.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
Disable incremental linking and enable /O1, function-level linking, string pooling, LTCG, reference elimination, and COMDAT folding for Release builds.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
Keep the runtime loadable on Windows 10 1809 while rejecting compositions whose declared UniversalApiContract version is unavailable before graph hydration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 58fac82f-0489-4c57-a18d-2b7ba273faa6
@jonwis

jonwis commented Sep 2, 2026

Copy link
Copy Markdown
Author

@dotnet-policy-service agree company="Microsoft"

Copilot AI and others added 2 commits September 2, 2026 16:17
Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com>
Split runtime schema dependencies from tool-side serialization, avoid copying byte-array buffers, and process keyframes in one pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 408cb9b9-d2b1-4a7a-bc3e-f6ab25b8793f
…preter

Add a managed FlatBuffer interpreter alongside the native one
@aborziak-ms

Copy link
Copy Markdown
Collaborator

@jonwis I left a few inline comments, but wanted to share some higher-level thoughts as well:

  • I'd suggest scoping this PR to the .lcomp format + LottieGen output + a small native C++ runtime. For native, a simple .h/.cpp drop-in that takes the app's Compositor and works with AnimatedVisualPlayer should be enough. I don't think we necessarily need the COM DLL here, since C++ apps can just compile the drop-in. The C# interpreter would mostly duplicate Instantiator, which the managed side (Lottie-Windows package) can reuse.
  • I can pick up the managed side separately and integrate lcomp into Lottie-Windows UWP and WinUI packages. The .lcomp graph is basically what Loader already builds from JSON, so we can load it through the existing LottieVisualSource path and get AnimatedVisualPlayer, images, WinUI 3 etc. for free.
  • Since the format would be shared by both, it'd be good to settle a few things now: include all UAP versions (like codegen does), have a versioning for schema changes, and keep the generated types internal.
  • For FlatBuffers, maybe just check in include/flatbuffers + LICENSE inside the native runtime folder instead of a full submodule? It's the only thing that needs the headers, it keeps the drop-in self-contained.

return false;
}

// NOTE: this only writes the latest version of a multi-version translation.

@aborziak-ms aborziak-ms Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Lower-UAP translations are silently dropped here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'll see what it takes to do that. I was hoping we'd be able to drop support for older UAPs and focus on WASDK version levels instead.

return E_ILLEGAL_METHOD_CALL;
}

m_compositor = Compositor();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The loader creates its own Compositor, so the returned visual can't be attached to the host's tree. I think we should accept compositor parameter from the caller, e.g. LoadComposition(IUnknown* compositor, UINT32, BYTE const*, REFIID, void**)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sure. Will add the new parameter here and in the IDL, docs, etc.

//
// Throws winrt::hresult_error with E_INVALIDARG if the buffer is not a well formed
// composition, and E_NOTIMPL if it requires a feature this build does not have.
winrt::Windows::UI::Composition::Visual LoadComposition(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Right now this only returns back a raw Visual, so it doesn't match what other generated code does. Should we wrap it in an IAnimatedVisualSource that exposes the same stuff the generated class does (Duration, Markers, FrameCount, FrameToProgress, SetColorProperty, etc.)? Then TryCreateAnimatedVisual(compositor) would return the visual with Size/Duration.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hm. That's a XAML type? Why not stay within the Composition scope so somebody can use it without XAML?

@@ -0,0 +1,108 @@
# LottieRuntime (managed)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think the managed side should just live in Lottie-Windows. Then there's one C# library that handles both JSON and .lcomp, and .lcomp can reuse the existing pipeline (CompositionDeserializer → Instantiator → AnimatedVisualPlayer) instead of a separate C# interpreter. I'd keep LottieRuntime as the small native runtime for C++ only. Otherwise, I'm worried we end up with too many pieces doing the same thing but a little differently.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

OK. I'll extend the existing C# library with this support instead and remove this one.

auto collection = container.Children();
for (uint32_t i = 0; i != children->size(); ++i)
{
auto child = GetVisual(children->Get(i));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We might want to cap recursion depth here (and in GetShape/BuildGeometry). A relatively small malformed or malicious .lcomp could blow the stack.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Seems reasonable. What cap depth seems good? I'll start with 256 maybe?

@jonwis

jonwis commented Oct 2, 2026

Copy link
Copy Markdown
Author
  • I don't think we necessarily need the COM DLL here, since C++ apps can just compile the drop-in. The C# interpreter would mostly duplicate Instantiator, which the managed side (Lottie-Windows package) can reuse.

OK, will narrow to just the producer and C++ consumer code, and update the C# instantiator. In Windows, we'll want to have a shared copy of the instantiator. What do you think about "a pretty OK sample" that shows using this in a common way? Or do we just assume the bots will figure out how to have one copy per process?

  • pick up the managed side separately and integrate lcomp into Lottie-Windows UWP and WinUI packages.

That'd be great! Is there anything needed to get this ready for a C++/WinRT consumer of WASDK XAML/Composition? Does Lottie-Windows have a C++/WinRT package?

  • it'd be good to settle a few things now: include all UAP versions (like codegen does), have a versioning for schema changes, and keep the generated types internal.

See note on supported UAP versions. At some point we should include "supported WASDK versions" and migrate people to using the Microsoft.UI types. I'll make the changes you suggest.

include/flatbuffers + LICENSE inside the native runtime

I'll have to see what that looks like; IIRC I had to tinker a bit to get flatc.exe working correctly.

Thanks for the comments!

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants