perf(runtime, performance): preserve path geometry on rigid transform (#14315) 4c824e6f05
* perf(runtime): keep a shape's local path across a rigid move

A shape's local path is each path's geometry mapped by inverseWorld *
pathTransform -- the path's transform relative to the shape. That survives any
rigid move: an ancestor translating, rotating or scaling moves the shape and
its paths together, so the local path the composer rebuilds is the one it
already holds. It rebuilt anyway, because a world transform change routes
through Path::onDirty -> Shape::pathChanged() as plain Path dirt.

The copy was never the expensive part. Rebuilding rewinds the ShapePaintPath,
which rewinds the RenderPath, which bumps a monotonic mutation id -- so the
cached inner-fan triangulation is dropped and rebuilt for geometry that did not
change. Worse, building one is charged against TriangulationController's frame
budget, and once that is spent every remaining path in the frame falls back to
midpoint fans; cache hits cost nothing.

So snapshot what the local path is built from -- each path's geometry version
and its transform relative to the shape -- and skip the rebuild when none of it
moved. Measured on a rigidly moving artboard: car_widgets 20.8 -> 15.1 us/frame
in the update pass (-27%, the same for rotation as for translation), zombie
skins 104 -> 90 us/frame, and re-triangulating those files' paths costs 41 and
140 us/frame respectively, which the retained cache now avoids entirely.

The comparison cannot be exact. Recomposing inverseWorld * pathTransform after
a move is an A^-1 * A round trip, so it lands a rounding step from the matrix
the buffer was built with -- under translation only in the translation, under
rotation in the linear part too. The tolerance is derived from the terms that
produce the value, and is compared against the last built transform rather than
the last frame, so the error is bounded rather than accumulating: at most a few
ULPs of the coordinates involved, measured at 3.7e-04 artboard units across
twelve real files. A sweep from 1 to 1024 ULPs moves the skip rate by under 3
points, so the constant is not load bearing.

Skinned paths rebuild their raw geometry every frame (the skin bakes world
space bone transforms into the vertices), so they keep rebuilding; making skins
shape relative would extend this to them.

Note this changes the silver command streams, which record the rewind and
addRawPath calls that no longer happen. 69 of them need rebaselining; nothing
else in the suite moves.

* test(runtime): render a tarnished silver next to its baseline

A silver fails on any byte difference in the recorded command stream, which is
stricter than "the frame changed": dropping a redundant rewind and addRawPath
pair moves those bytes without moving a pixel. Until now the only thing a
failure told you was the size of the stream, so a rebaseline came down to
trust.

Everything needed to do better was already in the binary. The SRIV stream
records at the Factory and Renderer level and carries its image bytes and mesh
buffers inline, so replaySerializedCommands can replay it with no .riv file or
fonts; unit_tests already links rive_cg_renderer and tools_common, so it can
replay into a CoreGraphics bitmap and write PNGs. Both sides go through that
same CPU renderer, so the comparison is exact even where the rasterizer is
approximate -- there is no golden to disagree with.

Hashes the frames first and only keeps pixels for the frames that differ (a
2370x1680 stream is 16MB a frame), then writes the baseline, the tarnished
render, a map of the differing pixels and a padded close-up of the region they
fall in, because the evidence is usually a handful of pixels on an
anti-aliased edge.

Hidden behind a dot tag, and deliberately run against the built binary rather
than through test.sh, which wipes silvers/tarnished on startup:

    ./out/release/unit_tests "[.silverdiff]"

* rebased tests

* fix(runtime): address review on the rigid transform path skip

- Do not run the snapshot for a shape with no local path. A clip source or a
  world stroke was paying for an inverse, a scratch vector and a per path
  compare whose result the world branch never reads. A shape that gains a
  local flag later still arrives here with no snapshot, which reads as
  changed, so it still builds.

- Release a CGRenderer before the context it draws into. ~CGRenderer restores
  a graphics state on its context, so releasing the context first left that
  restore reading through freed memory -- on the frame size change and again
  on the size probe.

- Fail when a tarnished silver has no readable baseline. It only counted the
  file as unreadable and carried on, so a run could report success having
  validated nothing, which is the one thing a rebaseline check must not do.

- clang-format (19.1.7, matching CI).

Drops silvers/super_ultra_mega_rig_bound_v002.sriv, which was picked up by the
rebaseline rather than authored: it is the only added silver in the set, and
its .riv lives outside the repo.

Co-authored-by: hernan <hernan@rive.app>
78 files changed
tree: 764e887484ffbabfa3a08e7bc345a44efca5a2d9
  1. .github/
  2. .vscode/
  3. build/
  4. cg_renderer/
  5. decoders/
  6. dependencies/
  7. dev/
  8. include/
  9. renderer/
  10. rivinfo/
  11. scripting/
  12. skia/
  13. src/
  14. tests/
  15. utils/
  16. .dockerignore
  17. .gitattributes
  18. .gitignore
  19. .lua-format
  20. .rive_head
  21. .version
  22. Dockerfile
  23. Doxyfile
  24. dummy.cpp
  25. LICENSE
  26. premake5_v2.lua
  27. README.md
  28. runtime.natvis
README.md

Build Status Discord badge Twitter handle

rive-runtime

Rive hero image

Rive's C++ runtime — the lowest-level Rive runtime. Loads .riv files, advances state machines and animations, and draws via the abstract Renderer interface. The built-in GPU renderer (RiveRenderer) has RenderContextImpl backends for Metal, Vulkan, D3D11, D3D12, and OpenGL/WebGL.

Rive's Apple, Android, Flutter, Unity, Unreal, and web runtimes all wrap this library.

Features:

  • Loading artboards and their contents from .riv files.
  • Querying state machines from artboards.
  • Mutating the artboard hierarchy (the same mechanism used by state machines) and efficiently solving those changes via Artboard::advance.
  • A state-of-the-art vector renderer for Metal, Vulkan, D3D12, D3D11, and OpenGL/WebGL.
  • An abstract Renderer interface for hooking up an external vector renderer.

Prerequisites

  • A C++17 toolchain:
    • macOS: clang from Xcode Command Line Tools (xcode-select --install).
    • Linux: clang from your distro (e.g. apt install clang).
    • Windows: Visual Studio 2022 with the C++ Clang Compiler for Windows and MSBuild support for LLVM (clang-cl) toolset individual components.
  • git — the build script clones a pinned premake5 on first run. On Windows, install Git for Windows and during setup pick “Use Git and optional Unix tools from the Command Prompt” so that sh.exe ends up on PATH.
  • Platform SDK for the renderer you're targeting:
    • macOS / iOS: Xcode.
    • Windows: Windows SDK (for D3D).
    • Linux: a Vulkan or OpenGL development environment.
    • Vulkan: the Vulkan SDK.

Build

The build is driven by premake5 wrapped by a helper script:

  • build/build_rive.sh — macOS / Linux (bash) / Windows (MinGW).
  • build/build_rive.ps1 — a PowerShell convenience wrapper for Windows; it shells out to the bash script, so a bash environment (Git for Windows / MinGW, see Prerequisites) must still be on PATH.

The helper self-installs the pinned premake version on first run and dispatches to the right build system for your platform (gmake2 on macOS/Linux, MSBuild on Windows). It must be run from a directory that contains a premake5.lua — typically tests/, which builds the core library, the GPU renderer, and the player sample app.

macOS / Linux

git clone https://github.com/rive-app/rive-runtime.git
cd rive-runtime/tests
../build/build_rive.sh release

Windows

git clone https://github.com/rive-app/rive-runtime.git
cd rive-runtime\tests
..\build\build_rive.ps1 release

Common variants

(On Windows, substitute build_rive.ps1 for build_rive.sh.)

  • build_rive.sh (no args) — debug build for the host.
  • build_rive.sh release clean — clean then build release.
  • build_rive.sh ninja release — use Ninja instead of make/MSBuild.
  • build_rive.sh ios release — cross-compile for iOS.
  • build_rive.sh android release — cross-compile for Android (defaults to arm64).
  • build_rive.sh ninja release wasm — cross-compile for WebAssembly.
  • build_rive.sh --toolset=msc release (Windows) — build with MSVC's cl.exe instead of clang-cl. The build supports both toolchains; the lua warning suppressions cover MSVC too.

See the comment header at the top of build/build_rive.sh for the complete flag reference.

Build outputs

Artifacts land in out/<config>/ relative to the directory you built from (typically tests/). The config directory encodes any OS/arch flags you passed:

  • out/release, out/debug — host build.
  • out/ios_release, out/android_arm64_release, out/wasm_release — cross-compile builds.
ArtifactDescription
librive.a / rive.libCore runtime library.
librive_pls_renderer.a / rive_pls_renderer.libGPU renderer.
player / player.exeSample app — loads and renders a .riv file.
out/<config>/goldens, out/<config>/gms, out/<config>/benchTest harness binaries (regression, golden-image, benchmarking).
librive_decoders.a, librive_harfbuzz.a, …Supporting libraries.

Testing

The runtime's primary form of testing is golden testing — rendering known scenes and diffing the output against checked-in reference images via the goldens and gms test harness binaries (built into out/<config>/goldens and out/<config>/gms). See tests/ for how to run and rebaseline goldens.

Unit tests are secondary and use the Catch2 framework. From the repo root:

cd tests/unit_tests
./test.sh

Unit tests live in tests/unit_tests/runtime/ (core runtime) and tests/unit_tests/renderer/ (renderer). To add a test, create an xxx_test.cpp file in the appropriate directory — the harness picks it up automatically.

Code formatting

rive-runtime uses clang-format.

  • macOS: brew install clang-format.
  • Windows: already installed if you have the prerequisites — VS 2022's C++ Clang Compiler component ships clang-format.exe alongside clang-cl.exe at C:\Program Files\Microsoft Visual Studio\2022\<edition>\VC\Tools\Llvm\x64\bin\. You can also install LLVM standalone from llvm.org or via winget install LLVM.LLVM.
  • Linux: install via your distro's package manager (e.g. apt install clang-format).

Memory checks (macOS only)

To run the tests under macOS's built-in leaks tool:

cd tests/unit_tests
./test.sh memory

This wraps the test binary with leaks --atExit, which ships with macOS — no install required. The memory flag is ignored on Linux and Windows.

Disassembly explorer (macOS / Linux)

To inspect generated assembly per cpp file, install the Disassembly Explorer VSCode extension. A disassemble task is provided in .vscode/tasks.json. The underlying gen assembly task invokes clang++ directly, so it needs clang++ on PATH — works out of the box on macOS (Xcode CLI tools) and most Linux distros, but not on a default Windows + VS install (which provides clang-cl.exe, not clang++.exe).

Reach the task from Tasks: Run Task, or bind a key in keybindings.json:

[
    {
        "key": "cmd+d",
        "command": "workbench.action.tasks.runTask",
        "args": "disassemble"
    }
]