diff --git a/docs.json b/docs.json index 23cd2131..d253c691 100644 --- a/docs.json +++ b/docs.json @@ -495,6 +495,20 @@ { "group": "Runtimes", "pages": [ + { + "group": "C++", + "pages": [ + "runtimes/cpp/overview", + "runtimes/cpp/getting-started", + "runtimes/cpp/file-and-artboard", + "runtimes/cpp/state-machines", + "runtimes/cpp/data-binding", + "runtimes/cpp/asset-loading", + "runtimes/cpp/rendering-loop", + "runtimes/cpp/renderers", + "runtimes/cpp/external-renderer" + ] + }, { "group": "Web (JS)", "pages": [ diff --git a/images/runtimes/cpp/rendercontext_layers.png b/images/runtimes/cpp/rendercontext_layers.png new file mode 100644 index 00000000..35da09a8 Binary files /dev/null and b/images/runtimes/cpp/rendercontext_layers.png differ diff --git a/images/runtimes/cpp/rive_pipeline.png b/images/runtimes/cpp/rive_pipeline.png new file mode 100644 index 00000000..66d108d6 Binary files /dev/null and b/images/runtimes/cpp/rive_pipeline.png differ diff --git a/runtimes/cpp/asset-loading.mdx b/runtimes/cpp/asset-loading.mdx new file mode 100644 index 00000000..bde73ac0 --- /dev/null +++ b/runtimes/cpp/asset-loading.mdx @@ -0,0 +1,156 @@ +--- +title: "Asset Loading" +description: "Resolve out-of-band images, fonts, and audio." +--- + +A `.riv` file can either embed asset bytes (images, fonts, audio, scripts) **in-band** +or reference them by CDN UUID and let the runtime fetch them. Implement +`FileAssetLoader` to control the second path — resolving from disk, the +network, or your own asset pipeline. + +## The `FileAssetLoader` interface + +```cpp +#include "rive/file_asset_loader.hpp" +#include "rive/assets/file_asset.hpp" + +class FileAssetLoader : public RefCnt { +public: + virtual bool loadContents(FileAsset& asset, + Span inBandBytes, + Factory* factory) = 0; +}; +``` + +`loadContents` is called once per asset during `File::import`. You return: + +- `true` — you handled it. Either you populated `asset` synchronously, or + you kicked off async work and will populate it later. +- `false` — fall back to the in-band bytes (if any). + +`asset` arrives typed: cast it to the concrete subclass to populate it. + +| Asset type | Class | How to populate | +| ---------- | ----- | --------------- | +| Image | `ImageAsset` | `asset.renderImage(factory->decodeImage(bytes))` | +| Font | `FontAsset` | `asset.font(factory->decodeFont(bytes))` | +| Audio | `AudioAsset` | `asset.audioSource(factory->decodeAudio(bytes))` | + +## Sync example: load by file name + +```cpp +#include "rive/file_asset_loader.hpp" +#include "rive/assets/image_asset.hpp" +#include "rive/assets/font_asset.hpp" +#include "rive/assets/audio_asset.hpp" + +#include +#include +#include +#include + +class DiskAssetLoader : public rive::FileAssetLoader { +public: + explicit DiskAssetLoader(std::filesystem::path root) + : m_root(std::move(root)) {} + + bool loadContents(rive::FileAsset& asset, + rive::Span inBandBytes, + rive::Factory* factory) override + { + // Prefer in-band bytes when present. + if (inBandBytes.size() > 0) return false; + + auto path = m_root / asset.uniqueFilename(); + std::ifstream f(path, std::ios::binary); + if (!f) return false; + std::vector bytes((std::istreambuf_iterator(f)), {}); + rive::Span span{bytes.data(), bytes.size()}; + + if (auto* img = dynamic_cast(&asset)) { + img->renderImage(factory->decodeImage(span)); + return true; + } + if (auto* fnt = dynamic_cast(&asset)) { + fnt->font(factory->decodeFont(span)); + return true; + } + if (auto* aud = dynamic_cast(&asset)) { + aud->audioSource(factory->decodeAudio(span)); + return true; + } + return false; + } + +private: + std::filesystem::path m_root; +}; +``` + +`asset.uniqueFilename()` is the editor-assigned name with extension; use +`asset.cdnUuidStr()` if you index by CDN UUID instead. + +## Wiring it up + +```cpp +rcp loader = make_rcp("assets/"); + +ImportResult result; +rcp file = File::import(bytes, factory, &result, loader); +``` + +The loader is reference-counted — `File` keeps it alive for the file's +lifetime so async loads can complete after `import` returns. + +## Async loading + +For async (HTTP, decoder thread, etc), return `true` from `loadContents` +**without** calling `renderImage` / `font` / `audioSource`, then populate the +asset later from any thread: + +```cpp +bool loadContents(FileAsset& asset, Span, Factory* factory) override { + auto* image = dynamic_cast(&asset); + if (!image) return false; + + rcp keepAlive = ref_rcp(image); + + fetchAsync(image->cdnUuidStr(), [keepAlive, factory](std::vector bytes) { + // Assumes this callback is dispatched to the render thread — see the + // warning below about decoder thread-safety. + rcp ri = + factory->decodeImage({bytes.data(), bytes.size()}); + keepAlive->renderImage(std::move(ri)); + // The next advanceAndApply will pick up the new image. + }); + return true; +} +``` + + + Decoders (`Factory::decodeImage`, `Factory::decodeFont`, + `Factory::decodeAudio`) are **not guaranteed thread-safe** for every + backend. If you decode off-thread, decode into a CPU-side representation + and finalize the GPU upload back on the render thread, or use a + thread-safe `Factory` if your backend supports it. + + +## Built-in loaders + +For simple cases the runtime ships a relative-path loader you can extend: + +```cpp +#include "rive/relative_local_asset_loader.hpp" + +rcp loader = + make_rcp("/path/to/assets"); +``` + +It loads files by `uniqueFilename()` from a directory on disk — handy for +samples and tooling. + +## When in-band bytes already cover everything + +If your `.riv` file embeds all of its assets, you can skip the loader +entirely. `inBandBytes` will be non-empty for those assets and the runtime +will decode them with the `Factory` you provided. diff --git a/runtimes/cpp/data-binding.mdx b/runtimes/cpp/data-binding.mdx new file mode 100644 index 00000000..cc63cd05 --- /dev/null +++ b/runtimes/cpp/data-binding.mdx @@ -0,0 +1,185 @@ +--- +title: "Data Binding" +description: "Drive Rive ViewModels from C++." +--- + +Rive's **View Models** expose strongly-typed properties +(numbers, strings, colors, booleans, enums, triggers, lists, nested view +models, image and artboard references) that an artboard binds to. From C++ +you instance a view model, mutate properties, and the bound visuals update +on the next `advanceAndApply`. + +## Concepts + +- `ViewModelRuntime` — schema for a view model defined in the editor. Lives + in the `File`. +- `ViewModelInstanceRuntime` — typed wrapper around an instance of that + schema. Exposes the property API (`propertyNumber`, `propertyString`, etc). + You own it via `rcp<>`. +- `ViewModelInstance` — the underlying bindable instance. `Artboard` and + `Scene` bind to this type. Get one from a `ViewModelInstanceRuntime` with + `.instance()`. +- `ViewModelInstance*Runtime` — typed handles for individual properties + (`…NumberRuntime`, `…StringRuntime`, etc). + +## Creating an instance + +The simplest path is to ask the file for the artboard's default view model, +then create a default instance from it: + +```cpp +#include "rive/file.hpp" +#include "rive/viewmodel/runtime/viewmodel_runtime.hpp" + +ViewModelRuntime* vm = file->defaultArtboardViewModel(artboard.get()); +if (!vm) return; // artboard has no default view model + +rcp instance = vm->createDefaultInstance(); +if (instance) { + artboard->bindViewModelInstance(instance->instance()); + scene ->bindViewModelInstance(instance->instance()); +} +``` + +For full control, look up a specific view model schema by index or name and +create instances from it: + +```cpp +ViewModelRuntime* vm = file->viewModelByName("Card"); +if (!vm) return; // no view model with that name + +size_t propCount = vm->propertyCount(); +size_t instCount = vm->instanceCount(); + +rcp instance = vm->createDefaultInstance(); +// alternatives — pick one and replace the line above: +// rcp instance = vm->createInstanceFromName("Hero"); +// rcp instance = vm->createInstanceFromIndex(0); +// rcp instance = vm->createInstance(); // no editor preset; properties at type defaults + +if (!instance) return; +artboard->bindViewModelInstance(instance->instance()); +scene ->bindViewModelInstance(instance->instance()); +``` + + + Bind the same `ViewModelInstance` to **both** the artboard and the scene. + The artboard binding drives layout-affecting properties; the scene binding + drives state-machine inputs and listener conditions. + + +## Reading & writing properties + +All accessors are path-based — `/`-separated for nested view models. + +```cpp +auto* card = instance.get(); + +// Number +if (auto* score = card->propertyNumber("score")) { + score->value(42.0f); + float v = score->value(); +} + +// String +if (auto* title = card->propertyString("title")) { + title->value("Hello"); +} + +// Boolean +if (auto* on = card->propertyBoolean("isOpen")) { + on->value(true); +} + +// Color (ARGB packed) +if (auto* col = card->propertyColor("accent")) { + col->value(0xFFE53935); +} + +// Trigger (edge event) +if (auto* fire = card->propertyTrigger("fire")) { + fire->trigger(); +} + +// Enum (by string label) +if (auto* mood = card->propertyEnum("mood")) { + mood->value("happy"); +} +``` + +### Nested view models + +```cpp +// Option 1: deep path string. +auto* headerTitle = card->propertyString("header/title"); + +// Option 2: walk the tree. +rcp header = card->propertyViewModel("header"); +auto* title = header->propertyString("title"); +``` + +You can also **swap** a nested view model wholesale — useful for swapping a +list cell's data without rebuilding the artboard: + +```cpp +rcp newHeader = vm->createDefaultInstance(); +card->replaceViewModel("header", newHeader.get()); +``` + +### Lists + +```cpp +auto* items = card->propertyList("items"); + +// Append, insert, remove, replace, swap, count. +items->addInstance(rowInstance.get()); +items->addInstanceAt(rowInstance.get(), 0); +items->removeInstanceAt(2); +items->swap(0, 1); +size_t n = items->size(); + +rcp row = items->instanceAt(0); +``` + +List items are themselves `ViewModelInstanceRuntime`s — same property API as +above. + +### Image & artboard properties + +```cpp +auto* image = card->propertyImage("avatar"); +image->value(decodedRenderImage.get()); // RenderImage* + +auto* artboardRef = card->propertyArtboard("badge"); +artboardRef->value(file->bindableArtboardNamed("Badge")); // rcp +``` + +## Lifecycle + +- A `ViewModelInstanceRuntime` is a thin wrapper over a `ViewModelInstance`. + Hold the `rcp<>` for as long as anything binds to it. +- Property handles (`ViewModelInstanceNumberRuntime*`, etc) are owned by + the parent instance. Cache the pointer — it stays valid for the + instance's lifetime. +- After mutating properties, the next `scene->advanceAndApply(dt)` propagates + the changes through data binds and into rendering. + +## When properties don't exist + +Every `propertyX(name)` getter returns `nullptr` if the name doesn't +resolve, so you can probe an instance safely: + +```cpp +if (auto* p = instance->propertyNumber("optional")) { + p->value(1.0f); +} +``` + +For introspection, walk the schema: + +```cpp +for (const PropertyData& p : instance->properties()) { + // p.name, p.type ∈ { number, string, boolean, color, enum, trigger, + // list, viewModel, image, artboard, ... } +} +``` diff --git a/runtimes/cpp/external-renderer.mdx b/runtimes/cpp/external-renderer.mdx new file mode 100644 index 00000000..c1b398cd --- /dev/null +++ b/runtimes/cpp/external-renderer.mdx @@ -0,0 +1,172 @@ +--- +title: "External Renderer" +description: "Plug Rive into your own GPU backend or 2D engine." +--- + +`RiveRenderer` and `rive::gpu::RenderContext` are one possible backend. The +core runtime is renderer-agnostic — anything you pass to `Scene::draw` only +has to implement two interfaces: + +- **`rive::Renderer`** — receives draw / clip / save / restore commands. +- **`rive::Factory`** — creates `RenderPath`, `RenderPaint`, `RenderImage`, + `RenderShader`, `RenderBuffer`, `Font`, `AudioSource` from raw bytes or + parameters during file import. + +Implement both and Rive will route everything through them. + +## When you'd want this + +- You already have a 2D vector engine (Skia, Direct2D, custom) and want + Rive to render through it. +- You're targeting a platform Rive doesn't ship a backend for. +- You want CPU-side hit-testing or analytics — render into a no-op + `Renderer` that records command counts. + +If you only need Rive on Windows / macOS / iOS / Linux / Web / Android, +prefer the [first-party renderers](/runtimes/cpp/renderers) — they're +faster and feature-complete. + +## The `Renderer` interface + +```cpp +class Renderer { +public: + virtual void save() = 0; + virtual void restore() = 0; + virtual void transform(const Mat2D&) = 0; + + virtual void drawPath(RenderPath*, RenderPaint*) = 0; + virtual void clipPath(RenderPath*) = 0; + + virtual void drawImage(const RenderImage*, + ImageSampler, BlendMode, + float opacity) = 0; + virtual void drawImageMesh(const RenderImage*, ImageSampler, + rcp verts_f32, + rcp uvs_f32, + rcp indices_u16, + uint32_t vertexCount, + uint32_t indexCount, + BlendMode, float opacity) = 0; + + virtual void modulateOpacity(float opacity) = 0; +}; +``` + +A few constraints worth knowing up front: + +- `save` / `restore` form a stack and must capture: the current transform, + clip stack, and the modulated opacity. +- `clipPath` adds to the current clip — never replaces it. +- `modulateOpacity` is multiplicative; `0.5` then `0.2` ⇒ `0.1` effective + opacity until the next `restore`. +- `drawImageMesh` indices are 16-bit; vertex / UV buffers are + tightly-packed `float` pairs. + +## The `Factory` interface + +```cpp +class Factory { +public: + virtual rcp makeRenderBuffer( + RenderBufferType, RenderBufferFlags, size_t sizeInBytes) = 0; + + virtual rcp makeLinearGradient( + float sx, float sy, float ex, float ey, + const ColorInt colors[], const float stops[], size_t count) = 0; + + virtual rcp makeRadialGradient( + float cx, float cy, float radius, + const ColorInt colors[], const float stops[], size_t count) = 0; + + virtual rcp makeRenderPath(RawPath&, FillRule) = 0; + virtual rcp makeEmptyRenderPath() = 0; + virtual rcp makeRenderPaint() = 0; + + virtual rcp decodeImage(Span) = 0; + rcp decodeFont (Span); // non-virtual helper + rcp decodeAudio(Span); // non-virtual helper +}; +``` + +Things to keep in mind: + +- Gradient `colors[]` are packed ARGB ints; `stops[]` are normalized 0..1. +- `makeRenderPath(RawPath&, FillRule)` may **steal** the path's storage — + treat the input as moved-from after the call. +- `decodeImage` is called with raw PNG / JPEG / WebP bytes, etc. Decode in + whatever pixel format your renderer prefers and wrap the result in a + subclass of `RenderImage`. +- `decodeFont` / `decodeAudio` are non-virtual helpers that fan out to + Rive's built-in HarfBuzz / miniaudio paths. They are not subclass + override points; use the actual virtual `Factory` extension points for + custom font shaping or audio integration. + +## RenderPath / RenderPaint subclasses + +Each is just a typed bag of state for your backend to consume: + +```cpp +class MyPath : public RenderPath { +public: + void rewind() override { /* clear */ } + void moveTo(float x, float y) override { /* … */ } + void lineTo(float x, float y) override { /* … */ } + void cubicTo(float ox, float oy, + float ix, float iy, + float x, float y) override { /* … */ } + void close() override { /* … */ } + void addRenderPath(RenderPath*, const Mat2D&) override { /* … */ } + void addRawPath(const RawPath&) override { /* … */ } +}; + +class MyPaint : public RenderPaint { +public: + void style(RenderPaintStyle) override; + void color(unsigned int) override; + void thickness(float) override; + void join(StrokeJoin) override; + void cap(StrokeCap) override; + void blendMode(BlendMode) override; + void shader(rcp) override; + void invalidateStroke() override; +}; +``` + +(Method signatures match `rive/command_path.hpp` (for `CommandPath`) and +`rive/renderer.hpp` (for `RenderPath` and `RenderPaint`) — read those for +the full set.) + +## Wiring it up + +```cpp +class MyFactory : public rive::Factory { /* ... */ }; + +MyFactory factory; +rcp file = File::import(bytes, &factory); + +auto artboard = file->artboardDefault(); +auto scene = artboard->defaultScene(); + +MyRenderer renderer; +scene->advanceAndApply(dt); +scene->draw(&renderer); +``` + +There's no `RenderContext` in this path — your `Renderer` is responsible +for making the GPU calls (or buffering them, or counting them, or whatever +you want). + +## Existing implementations to crib from + +All paths below are in the [rive-runtime](https://github.com/rive-app/rive-runtime) repo. + +| Project | Uses | Where | +| ------- | ---- | ----- | +| `rive::gpu::RenderContext` | Pixel-local-storage GPU renderer | `renderer/src/` | +| `SkiaFactory` / `SkiaRenderer` | Skia | `skia/renderer/` | +| `CGFactory` / `CGRenderer` | CoreGraphics | `cg_renderer/` | +| `SokolFactory` | Sokol (tessellation) | `tess/src/sokol/` | + +The Skia and CoreGraphics implementations are the most direct templates for +a "forward to an existing 2D engine" `Renderer`. diff --git a/runtimes/cpp/file-and-artboard.mdx b/runtimes/cpp/file-and-artboard.mdx new file mode 100644 index 00000000..81744c51 --- /dev/null +++ b/runtimes/cpp/file-and-artboard.mdx @@ -0,0 +1,156 @@ +--- +title: "File & Artboard" +description: "Importing .riv data and instancing artboards." +--- + +A Rive `.riv` file is a binary container of **artboards**, **animations**, +**state machines**, **view models**, and **assets**. The C++ API gives you +read-only access to that container (`File`, `Artboard`) and mutable instances +you can advance and draw (`ArtboardInstance`, `Scene`). + +## Importing a file + +```cpp +#include "rive/file.hpp" + +ImportResult result; +rcp file = File::import( + Span{bytes.data(), bytes.size()}, + factory, // a Factory* — usually your RenderContext + &result, // optional + assetLoader); // optional rcp + +switch (result) { + case ImportResult::success: break; + case ImportResult::unsupportedVersion: /* runtime is older than file */ break; + case ImportResult::malformed: /* bad bytes */ break; +} +``` + +`File` is reference-counted (`rcp`). It owns the parsed object graph and +the assets imported in-band. Keep it alive for as long as any artboard +instance derived from it is alive. + + + The `Factory*` you pass in becomes load-bearing for every `RenderPath`, + `RenderPaint`, `RenderImage`, font, and audio source decoded from the file. + When using Rive's GPU renderer, this is your `RenderContext`. When using a + custom renderer, it is your `Factory` implementation. + + +## Determinism + +```cpp +File::deterministicMode = true; +``` + +A static flag that forces a fixed RNG seed and timestamp-driven scrolling for +all subsequent loads. Useful for golden-image tests and frame-by-frame +captures. + +## Querying artboards + +```cpp +size_t count = file->artboardCount(); +std::string name = file->artboardNameAt(0); + +Artboard* byName = file->artboard("Hero"); // by name +Artboard* byIndex = file->artboard(size_t{0}); // by index +Artboard* first = file->artboard(); // file's first artboard +``` + +`Artboard*` returned by these accessors is **read-only metadata** — do not +advance or draw it. To play an artboard, create an instance. + +## Instancing + +```cpp +// Most common: copy of the file's default artboard. +std::unique_ptr ab = file->artboardDefault(); + +// Or by name / index: +auto namedAb = file->artboardNamed("Hero"); +auto indexedAb = file->artboardAt(2); +``` + +Each call returns a **fresh, independent copy**. You can have many instances of +the same artboard playing at once — each with its own state machine, its own +data bindings, and its own layout. + +```cpp +size_t anims = ab->animationCount(); +size_t states = ab->stateMachineCount(); +std::string animName = ab->animationNameAt(0); +std::string smName = ab->stateMachineNameAt(0); + +// Designer-marked default state machine, if any. +int defaultIdx = ab->defaultStateMachineIndex(); // -1 if none +``` + +## Picking a `Scene` + +`Scene` is the abstract base of anything you can advance + draw. Two concrete +flavors ship with the runtime: + +- `StateMachineInstance` — interactive, responds to pointer events. +- `LinearAnimationInstance` — a single timeline, no input. + +```cpp +#include "rive/scene.hpp" + +std::unique_ptr scene; + +// Preferred: a state machine. +if (auto sm = ab->defaultStateMachine()) { + scene = std::move(sm); +} else if (ab->stateMachineCount() > 0) { + scene = ab->stateMachineAt(0); +} else { + // Falls back to the first animation — no input handling. + scene = ab->defaultScene(); +} +``` + +`defaultScene()` is the one-call shortcut — it returns the default state +machine, then the first state machine, then the first animation, then nullptr. + +## Sizing & layout + +Artboards have an intrinsic size set in the editor, plus an optional layout +mode (Yoga-driven). For non-layout fits the artboard stays at its intrinsic +size; for `Fit::layout` you drive width and height yourself: + +```cpp +#include "rive/layout.hpp" + +if (fit == Fit::layout) { + ab->width(static_cast(windowWidth)); + ab->height(static_cast(windowHeight)); +} else { + ab->resetSize(); // back to intrinsic +} + +// Re-evaluate layout before the next draw. +scene->advanceAndApply(0.f); +``` + +`Artboard::bounds()` returns the current rect — feed it to `computeAlignment` +to map artboard-space into your viewport. + +## File lifetime + +Reference-counted ownership keeps lifetimes simple: + +```cpp +rcp file = File::import(...); +auto ab = file->artboardDefault(); // unique_ptr +auto sm = ab->defaultStateMachine(); // unique_ptr + +// Tear down in reverse order: +sm.reset(); +ab.reset(); +file = nullptr; // last rcp drops the File +``` + +Always destroy Rive objects **before** tearing down your `RenderContext` — +they hold references to GPU resources owned by the context. diff --git a/runtimes/cpp/getting-started.mdx b/runtimes/cpp/getting-started.mdx new file mode 100644 index 00000000..46f12ad0 --- /dev/null +++ b/runtimes/cpp/getting-started.mdx @@ -0,0 +1,190 @@ +--- +title: "Getting Started" +description: "Build rive-cpp and render a .riv file." +--- + +This guide walks you through cloning the runtime, compiling it, and getting a +`.riv` file on screen with a real GPU backend. + +## Prerequisites + +- A recent **clang** or **MSVC** that supports C++17. +- **git** — the build script will clone and bootstrap `premake5` itself. +- Platform SDK for your renderer of choice (Windows SDK for D3D, Xcode for + Metal, Vulkan SDK for Vulkan, etc). + + + Rive uses clang [vector + builtins](https://reviews.llvm.org/D111529). When building with clang, use the + latest version available — older toolchains may fail to compile the renderer. + + +## 1. Clone and build the runtime + +```bash +git clone https://github.com/rive-app/rive-runtime.git +cd rive-runtime/renderer +``` + +The runtime ships a build helper at `build/build_rive.sh` (with a +PowerShell wrapper `build_rive.ps1` for Windows). It installs the pinned +`premake5` version on first run and dispatches to the right build system +for your platform (gmake2 on macOS/Linux, MSBuild on Windows, etc). + + +```bash macOS / Linux +../build/build_rive.sh release +``` + +```powershell Windows +..\build\build_rive.ps1 release +``` + + +Common variants: +- `build_rive.sh` (no args) — debug build for the host. +- `build_rive.sh release clean` — clean rebuild. +- `build_rive.sh ninja release` — use Ninja instead of make. +- `build_rive.sh ios release` / `build_rive.sh android release` — + cross-compile. + +Build artifacts land in `out/release/` (or `out/debug/`). You'll link +against `librive.a` (or `rive.lib` on Windows), plus the per-backend +renderer libraries like `librive_pls_renderer.a`. + +## 2. Add headers to your project + +The public include roots are: + +``` +rive-runtime/include # rive-cpp core +rive-runtime/renderer/include # GPU renderer (only if you use rive::gpu) +``` + +A minimal CMake snippet: + +```cmake +target_include_directories(my_app PRIVATE + ${RIVE}/include + ${RIVE}/renderer/include +) + +target_link_libraries(my_app PRIVATE + rive + rive_pls_renderer + # plus your backend, e.g. d3d11, dxgi on Windows +) +``` + +## 3. Load a `.riv` file + +```cpp +#include "rive/file.hpp" +#include +#include +#include + +using namespace rive; + +std::vector readFile(const char* path) { + std::ifstream in(path, std::ios::binary); + return {std::istreambuf_iterator(in), {}}; +} + +// `factory` is a Factory* — usually your RenderContext (which inherits Factory). +auto bytes = readFile("hero.riv"); + +ImportResult result; +rcp file = File::import(bytes, factory, &result); +if (!file || result != ImportResult::success) { + // Bad file or unsupported version. + return; +} +``` + +## 4. Pick an artboard and a scene + +```cpp +#include "rive/artboard.hpp" +#include "rive/scene.hpp" + +std::unique_ptr artboard = file->artboardDefault(); + +// Prefer a state machine — it's the only Scene that handles pointer events. +std::unique_ptr scene = artboard->defaultStateMachine(); +if (!scene) { + scene = artboard->defaultScene(); // falls back to first animation +} +``` + +## 5. Advance and draw + +A render loop has three phases each frame: **advance**, **draw**, **flush**. + +```cpp +#include "rive/renderer/rive_renderer.hpp" +#include "rive/renderer/render_context.hpp" + +void renderFrame(float dt) { + scene->advanceAndApply(dt); + + RenderContext::FrameDescriptor frame{}; + frame.renderTargetWidth = windowWidth; + frame.renderTargetHeight = windowHeight; + frame.clearColor = 0xff202020; // ARGB + renderContext->beginFrame(frame); + + RiveRenderer renderer(renderContext.get()); + renderer.save(); + renderer.align(Fit::contain, + Alignment::center, + AABB(0, 0, windowWidth, windowHeight), + artboard->bounds()); + scene->draw(&renderer); + renderer.restore(); + + RenderContext::FlushResources flush{}; + flush.renderTarget = renderTarget.get(); + renderContext->flush(flush); +} +``` + + + Use a **fixed timestep** for `advanceAndApply` (e.g. 1/120s) and accumulate + real elapsed time. State machines are deterministic at fixed steps, which + makes playback reproducible across machines and frame rates. See + [Rendering Loop](/runtimes/cpp/rendering-loop). + + +## 6. Forward input (optional) + +If you used a state machine, route pointer events back through the scene so +listeners and hit-testing work: + +```cpp +#include "rive/renderer.hpp" + +Mat2D align = computeAlignment(Fit::contain, + Alignment::center, + AABB(0, 0, w, h), + artboard->bounds()); + +Vec2D toArtboard(int x, int y) { + return align.invertOrIdentity() * Vec2D{(float)x, (float)y}; +} + +scene->pointerMove(toArtboard(mouseX, mouseY)); +scene->pointerDown(toArtboard(mouseX, mouseY)); +scene->pointerUp(toArtboard(mouseX, mouseY)); +``` + +## What's next + + + + Wire up your platform's GPU backend. + + + Read & write inputs, listen for events, control transitions. + + diff --git a/runtimes/cpp/overview.mdx b/runtimes/cpp/overview.mdx new file mode 100644 index 00000000..f120da78 --- /dev/null +++ b/runtimes/cpp/overview.mdx @@ -0,0 +1,76 @@ +--- +title: "C++ Runtime" +description: "Load, advance, and render Rive content from C++." +sidebarTitle: "Overview" +--- + +The Rive C++ runtime (`rive-cpp`) is the lowest-level Rive runtime. It loads +`.riv` files, advances state machines and animations, and draws into any +[Renderer](/runtimes/cpp/external-renderer) — including Rive's own GPU +[Renderer](/runtimes/cpp/renderers) (Metal, Vulkan, D3D11, D3D12, OpenGL/WebGL). + +Higher level Rive runtimes (Apple, Android, Flutter, Unity, Unreal) wrap this +library. + +Use the C++ runtime when you are: + +- Embedding Rive in a C++ application or game engine. +- Targeting a platform without a first-party runtime. +- Plugging Rive into your own renderer or render graph. + + + + Build the runtime and put a `.riv` file on screen in under 100 lines of code. + + + Set up the GPU backend for your platform — D3D11, D3D12, Metal, Vulkan, or GL. + + + Import `.riv` files, query artboards, and create `ArtboardInstance`s. + + + Advance scenes, forward pointer events, and read inputs. + + + Drive a Rive `ViewModel` from your application state. + + + Resolve out-of-band images, fonts, and audio with `FileAssetLoader`. + + + `beginFrame` / `flush` — what runs each frame and what it costs. + + + Implement `Renderer` and `Factory` to use your own GPU backend. + + + +## Architecture at a glance + +![Rive runtime rendering pipeline: your application owns a File (.riv data) and a RenderContext (D3D/Metal/Vulkan/...); File produces an ArtboardInstance, which produces a Scene (state machine or linear animation), which feeds the RiveRenderer along with the RenderContext to produce pixels.](/images/runtimes/cpp/rive_pipeline.png) + +The split is deliberate: **`File` / `Artboard` / `Scene` know nothing about the +GPU**, and `Renderer` / `RenderContext` know nothing about Rive content. You can +use either half independently. + +## Supported platforms & APIs + +| Backend | Headers | Class | +| ------- | -------------------------------------------------- | ---------------------- | +| D3D11 | `rive/renderer/d3d11/render_context_d3d_impl.hpp` | `RenderContextD3DImpl` | +| D3D12 | `rive/renderer/d3d12/render_context_d3d12_impl.hpp`| `RenderContextD3D12Impl` | +| Metal | `rive/renderer/metal/render_context_metal_impl.h` | `RenderContextMetalImpl` | +| Vulkan | `rive/renderer/vulkan/render_context_vulkan_impl.hpp` | `RenderContextVulkanImpl` | +| OpenGL / WebGL | `rive/renderer/gl/render_context_gl_impl.hpp` | `RenderContextGLImpl` | + + + The C++ runtime is also the foundation for Rive's [iOS/macOS](/runtimes/apple), + [Android](/runtimes/android), and [Flutter](/runtimes/flutter) runtimes — those + platforms ship a thin wrapper, not a separate engine. + + +## Source & license + +- GitHub: [rive-app/rive-runtime](https://github.com/rive-app/rive-runtime) +- License: MIT +- Build system: [premake5](https://premake.github.io/) diff --git a/runtimes/cpp/renderers.mdx b/runtimes/cpp/renderers.mdx new file mode 100644 index 00000000..3d4adc7f --- /dev/null +++ b/runtimes/cpp/renderers.mdx @@ -0,0 +1,211 @@ +--- +title: "Renderers" +description: "Set up the GPU backend for your platform." +--- + +`rive::gpu::RenderContext` is the API-agnostic frontend to Rive's GPU +renderer. You create one of the per-backend `*Impl::MakeContext` factories, +hand it to your render loop, and the rest of the C++ API stays identical. + +![RenderContext layering: your code drives RenderContext via beginFrame/flush; RenderContext delegates to a per-backend RenderContextImpl (D3D11, D3D12, Metal, Vulkan, GL), which in turn talks to the underlying GPU API (DXGI, IOSurface, VkSwapchain, ...).](/images/runtimes/cpp/rendercontext_layers.png) + +`RenderContext` also implements `rive::Factory`, so the same object you pass to +`File::import` is the one that owns your GPU resources. + + + + Header: `rive/renderer/d3d11/render_context_d3d_impl.hpp` + + ```cpp + #include "rive/renderer/d3d11/render_context_d3d_impl.hpp" + + using namespace rive::gpu; + + ComPtr device; + ComPtr context; + // ... create device + context with D3D_FEATURE_LEVEL_11_1 ... + + D3DContextOptions options; + options.isIntel = adapterDesc.VendorId == 0x163C || + adapterDesc.VendorId == 0x8086 || + adapterDesc.VendorId == 0x8087; + + std::unique_ptr rc = + RenderContextD3DImpl::MakeContext(device, context, options); + + auto* impl = rc->static_impl_cast(); + rcp target = impl->makeRenderTarget(width, height); + + // Each frame: bind the current backbuffer to the render target. + target->setTargetTexture(backbufferTexture); + ``` + + Swap-chain notes: + - Set `BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_UNORDERED_ACCESS`. + - Use `DXGI_FORMAT_R8G8B8A8_UNORM` (or the `_SRGB` variant); other formats + may force the renderer onto an offscreen path. + - Set `options.isIntel = true` on Intel HD Graphics — works around a driver + bug in pixel-local-storage emulation. + + For a working reference, see Rive's + [`tests/player`](https://github.com/rive-app/rive-runtime/tree/main/tests/player) + sample app and the D3D-specific wiring in + [`tests/common/offscreen_rendertarget_d3d.cpp`](https://github.com/rive-app/rive-runtime/blob/main/tests/common/offscreen_rendertarget_d3d.cpp). + + + + Header: `rive/renderer/d3d12/render_context_d3d12_impl.hpp` + + ```cpp + #include "rive/renderer/d3d12/render_context_d3d12_impl.hpp" + + using namespace rive::gpu; + + ComPtr device; + ComPtr initList; // recording state + + D3DContextOptions options; + std::unique_ptr rc = + RenderContextD3D12Impl::MakeContext(device, initList.Get(), options); + + auto* impl = rc->static_impl_cast(); + rcp target = impl->makeRenderTarget(width, height); + ``` + + Each frame, populate `RenderContext::FlushResources::externalCommandBuffer` + with the `ID3D12GraphicsCommandList` that should record Rive's draws, and + bump `currentFrameNumber` / `safeFrameNumber` so the renderer can recycle + buffers safely against your fence values. + + + The init command list passed to `MakeContext` must be in the **recording** + state. The renderer uses it to upload static buffers. Submit it (and wait + for the GPU to finish) before issuing any frames. + + + + + Header: `rive/renderer/metal/render_context_metal_impl.h` + + ```objc + #import "rive/renderer/metal/render_context_metal_impl.h" + + using namespace rive::gpu; + + id device = MTLCreateSystemDefaultDevice(); + + RenderContextMetalImpl::ContextOptions opts; + std::unique_ptr rc = + RenderContextMetalImpl::MakeContext(device, opts); + ``` + + Pass an `id` per frame via + `FlushResources::externalCommandBuffer`. Rive does **not** present — you + own the `CAMetalLayer` and the `present` call. + + + + Header: `rive/renderer/vulkan/render_context_vulkan_impl.hpp` + + ```cpp + #include "rive/renderer/vulkan/render_context_vulkan_impl.hpp" + + using namespace rive::gpu; + + VulkanFeatures features; + features.independentBlend = true; // detected from device features + // ... fill out from VkPhysicalDeviceFeatures ... + + std::unique_ptr rc = + RenderContextVulkanImpl::MakeContext( + instance, + physicalDevice, + device, + features, + vkGetInstanceProcAddr); + ``` + + Each frame: + - Set `FlushResources::externalCommandBuffer` to a recording + `VkCommandBuffer`. + - Set `FlushResources::currentFrameNumber` to the frame index you'll signal, + and `safeFrameNumber` to the most recent frame whose fence you've waited on. + + + Vulkan is the only backend that supports `FrameDescriptor::virtualTileWidth` + / `virtualTileHeight` (frame splitting), useful for letting other + workloads pre-empt Rive on tiled GPUs. + + + + + Header: `rive/renderer/gl/render_context_gl_impl.hpp` + + ```cpp + #include "rive/renderer/gl/render_context_gl_impl.hpp" + #include "rive/renderer/gl/render_target_gl.hpp" + + using namespace rive::gpu; + + RenderContextGLImpl::ContextOptions opts; + // opts.disableFragmentShaderInterlock = true; // if your driver lies + + std::unique_ptr rc = RenderContextGLImpl::MakeContext(opts); + ``` + + Construct a render target that wraps either an external texture or an + existing FBO — the doc graph stays the same, only the binding differs: + + ```cpp + // Render into a texture you own. + rcp target = + make_rcp(width, height); + target->setTargetTexture(externalTextureID); // GLuint, you keep ownership + + // Or, render into an existing FBO you own. + rcp target = + make_rcp( + width, height, fboId, sampleCount); + ``` + + Prefer `TextureRenderTargetGL` when you can — `FramebufferRenderTargetGL` + often has to render to an offscreen texture and blit back, because the + external FBO is usually not readable. + + Capability tiers Rive will auto-select between: + - **Pixel Local Storage** (best — Apple GPUs, modern mobile). + - **Fragment Shader Interlock** (NVIDIA, Intel via `GL_INTEL_…`). + - **R/W Texture / EXT_shader_pixel_local_storage** fallbacks. + - **MSAA** path on truly minimal drivers. + + For WebGL 2: the same header builds against Emscripten — `MakeContext` + will pick the WebGL-compatible PLS implementation automatically. + + + +## Picking modes per frame + +`RenderContext::FrameDescriptor` carries flags that change the rendering +algorithm: + +| Field | Effect | +| ----- | ------ | +| `loadAction` | `clear` (default) / `preserveRenderTarget` / `dontCare`. | +| `clearColor` | ARGB; only used when `loadAction == clear`. | +| `msaaSampleCount` | Nonzero forces MSAA mode and disables PLS. | +| `disableRasterOrdering` | Forces the atomic path even where rasterizer ordering is supported. | +| `ditherMode` | `none` or `interleavedGradientNoise` (default). | + +Most apps leave these at defaults. Override only when you see a specific issue +on a specific GPU. + +## Choosing a backend + +| Platform | Recommendation | +| -------- | -------------- | +| Windows desktop | **D3D11** if you only need 11-class hardware, **D3D12** for newer engines. | +| macOS / iOS | **Metal**. | +| Android | **Vulkan** on supported devices, **GLES** as fallback. | +| Linux desktop | **Vulkan**. | +| Web | **WebGL 2** via `RenderContextGLImpl` (build with Emscripten). | +| Game console | Talk to Rive — separate runtimes ship for PS5, Xbox, and Switch. | diff --git a/runtimes/cpp/rendering-loop.mdx b/runtimes/cpp/rendering-loop.mdx new file mode 100644 index 00000000..83342c22 --- /dev/null +++ b/runtimes/cpp/rendering-loop.mdx @@ -0,0 +1,165 @@ +--- +title: "Rendering Loop" +description: "What happens between beginFrame and flush — and how to drive it." +--- + +Each frame goes through three phases: + +1. **Advance.** `scene->advanceAndApply(dt)` runs the simulation. +2. **Record.** `RiveRenderer` records draw commands into the active `RenderContext`. +3. **Submit.** `renderContext->flush(...)` builds the GPU work and lets the + backend submit it. + +```cpp +// 1. Advance (simulation). +scene->advanceAndApply(dt); + +// 2. Record draws. +RenderContext::FrameDescriptor frame{}; +frame.renderTargetWidth = w; +frame.renderTargetHeight = h; +frame.clearColor = 0xff202020; +renderContext->beginFrame(frame); + +RiveRenderer renderer(renderContext.get()); +renderer.save(); +renderer.align(Fit::contain, Alignment::center, + AABB(0, 0, w, h), artboard->bounds()); +scene->draw(&renderer); +renderer.restore(); + +// 3. Submit. +RenderContext::FlushResources flush{}; +flush.renderTarget = renderTarget.get(); +renderContext->flush(flush); +``` + +## `FrameDescriptor` + +Configures the upcoming frame. Values are reset every `beginFrame`. + +| Field | Default | Purpose | +| ----- | ------- | ------- | +| `renderTargetWidth` / `renderTargetHeight` | 0 | Must match your render target. | +| `loadAction` | `clear` | `clear` / `preserveRenderTarget` / `dontCare`. | +| `clearColor` | 0 | ARGB; only used with `loadAction == clear`. | +| `msaaSampleCount` | 0 | Nonzero forces MSAA mode. | +| `disableRasterOrdering` | false | Forces atomic mode even when raster-ordering is supported. | +| `ditherMode` | `interleavedGradientNoise` | `none` to disable. | +| `virtualTileWidth` / `virtualTileHeight` | 0 | Vulkan-only frame tiling for pre-emption. | + +## `FlushResources` + +Carries everything the backend needs to submit the recorded work. + +| Field | Use | +| ----- | --- | +| `renderTarget` | The `RenderTarget*` you bound this frame's backbuffer to. | +| `externalCommandBuffer` | Backend command buffer — `VkCommandBuffer` (Vulkan), `id` (Metal), `WGPUCommandEncoder` (WebGPU). Unused on D3D11 / GL. | +| `currentFrameNumber` | Monotonic frame ID for resource lifetime tracking. | +| `safeFrameNumber` | Most recent frame whose GPU work has retired (i.e. fence has signaled). Resources last used on or before this frame can be recycled. | + +On D3D12, Vulkan, and Metal you must update `currentFrameNumber` and +`safeFrameNumber` every frame against your fence values. Otherwise the +renderer can't safely recycle staging buffers and you'll either see +overwrites mid-flight or unbounded memory growth. + +## Fixed-timestep accumulator + +State machines and animations are deterministic at fixed `dt`s. Wrap your +real-elapsed-time delta in an accumulator so playback is reproducible across +frame rates: + +```cpp +constexpr float kFixedSimDt = 1.f / 120.f; +constexpr float kMaxFrameDt = 0.25f; // cap after stalls + +void tick(float realDt) { + if (realDt > kMaxFrameDt) realDt = kMaxFrameDt; + accumulator += realDt; + while (accumulator >= kFixedSimDt) { + scene->advanceAndApply(kFixedSimDt); + accumulator -= kFixedSimDt; + } + drawFrame(); +} +``` + +The cap (`kMaxFrameDt`) prevents catch-up storms after the app gets paused +in a debugger or backgrounded by the OS. + +## Resize handling + +Three things have to happen on a resize: + +1. Resize your swap-chain / framebuffer. +2. Re-create the `RenderTarget`. +3. Either feed the new size into the artboard (for `Fit::layout`) or call + `resetSize()`, then run `advanceAndApply(0)` so layout solves before the + next draw. + +```cpp +void onResize(uint32_t w, uint32_t h) { + swapChain->ResizeBuffers(0, w, h, DXGI_FORMAT_UNKNOWN, 0); + + auto* impl = renderContext->static_impl_cast(); + renderTarget = impl->makeRenderTarget(w, h); + + if (fit == Fit::layout) { + artboard->width(static_cast(w)); + artboard->height(static_cast(h)); + } else { + artboard->resetSize(); + } + scene->advanceAndApply(0.f); +} +``` + +## Aligning artboard to viewport + +Use `computeAlignment` (or `Renderer::align`) to map artboard-space into the +viewport. Stash the matrix — you'll need its inverse to convert pointer +coordinates back into artboard-space. + +```cpp +#include "rive/renderer.hpp" + +Mat2D align = computeAlignment( + Fit::contain, Alignment::center, + AABB(0, 0, w, h), + artboard->bounds()); + +renderer.save(); +renderer.transform(align); +scene->draw(&renderer); +renderer.restore(); + +// Later, on input: +Vec2D pt = align.invertOrIdentity() * Vec2D{(float)mx, (float)my}; +scene->pointerMove(pt); +``` + +## When to skip a frame + +`advanceAndApply` returns `true` if the scene has more work; `false` if +everything has settled. For animations that have looped to rest, you can +skip both the simulation step and the draw call, dropping CPU/GPU usage to +zero between user inputs. + +Pointer events and external view-model writes can wake the scene back up — +re-draw at least once after each. + +## Tear-down + +```cpp +scene.reset(); +artboard.reset(); +file = nullptr; +renderTarget = nullptr; +renderContext.reset(); // last +``` + +Always destroy Rive objects **before** the `RenderContext` and the +underlying GPU device. Rive objects hold reference-counted GPU resources; +killing the device first leaves them with dangling handles and crashes on +release. diff --git a/runtimes/cpp/state-machines.mdx b/runtimes/cpp/state-machines.mdx new file mode 100644 index 00000000..ec216b58 --- /dev/null +++ b/runtimes/cpp/state-machines.mdx @@ -0,0 +1,129 @@ +--- +title: "State Machines" +description: "Advance scenes and forward pointer events." +--- + +`Scene` is the unit of playback. Whether you're running a state machine or a +linear animation, the loop is the same: **advance** the scene, then **draw** +it. + +```cpp +class Scene { +public: + virtual bool advanceAndApply(float elapsedSeconds) = 0; + void draw(Renderer*); + + virtual HitResult pointerDown(Vec2D, int pointerId = 0); + virtual HitResult pointerMove(Vec2D, float timeStamp = 0, int pointerId = 0); + virtual HitResult pointerUp(Vec2D, int pointerId = 0); + virtual HitResult pointerExit(Vec2D, int pointerId = 0); +}; +``` + +To drive a state machine — set values, fire triggers, react to changes — +use **Data Binding**. See [Data Binding](/runtimes/cpp/data-binding). + +## Advancing + +`advanceAndApply(dt)` runs solvers (state machine, animations, layout, data +bindings) for `dt` seconds and applies the results to the artboard's +component graph. Call it before every draw: + +```cpp +scene->advanceAndApply(deltaSeconds); +scene->draw(&renderer); +``` + +A return value of `true` means the scene is still animating and the next +frame should re-draw. `false` means everything has settled. + + + Use a **fixed timestep** accumulator. State machines and animations are + numerically deterministic at fixed steps, which keeps playback identical + across frame rates. See [Rendering Loop](/runtimes/cpp/rendering-loop). + + +### Forcing a layout pass + +Pass `dt = 0` to re-solve layout without advancing time. Useful right after +resizing the window or changing the artboard's `width()` / `height()`: + +```cpp +artboard->width(newWidth); +artboard->height(newHeight); +scene->advanceAndApply(0.f); +``` + +## Pointer events + +A state machine listens for pointer events on `Listener` components placed +in the editor. Map your window-space coordinates into **artboard-local** +space before forwarding: + +```cpp +#include "rive/renderer.hpp" + +Mat2D align = computeAlignment( + Fit::contain, + Alignment::center, + AABB(0, 0, windowWidth, windowHeight), + artboard->bounds()); + +Vec2D toArtboard(int x, int y) { + return align.invertOrIdentity() * Vec2D{(float)x, (float)y}; +} + +scene->pointerMove(toArtboard(mx, my)); +scene->pointerDown(toArtboard(mx, my)); +scene->pointerUp(toArtboard(mx, my)); +scene->pointerExit(toArtboard(mx, my)); +``` + +`HitResult` returns one of three values, which tells you how to route the +same event to other UI behind Rive: + +- `none` — the event passed through Rive without firing a listener. + Forward it to whatever UI is behind. +- `hit` — a listener fired, but the shape it's on is transparent. Rive + isn't blocking the event; forward it as well. +- `hitOpaque` — a listener fired and the shape is opaque. Rive consumed + the event; don't forward. + +`LinearAnimationInstance` inherits the pointer methods but ignores them. +Only state machines have listeners. + +## Reading state changes + +After each `advanceAndApply` you can introspect what happened on a +`StateMachineInstance`. If you only have a `Scene*`, use a checked downcast: + +```cpp +if (auto* sm = dynamic_cast(scene.get())) { + for (size_t i = 0, n = sm->stateChangedCount(); i < n; ++i) { + const LayerState* s = sm->stateChangedByIndex(i); + // s->name(), s->is(), etc. + } +} +``` + +This is the hook you use to react in C++ to states defined in the `.riv` +file — log analytics, play a sound, fire a callback into your engine. + +## Running Linear Animations + +If you don't want a state machine, get a `LinearAnimationInstance`: + +```cpp +#include "rive/animation/linear_animation_instance.hpp" + +auto anim = artboard->animationNamed("Idle"); +anim->loopValue(static_cast(Loop::loop)); +anim->advanceAndApply(dt); +artboard->draw(&renderer); // animations draw via the artboard +``` + +`LinearAnimationInstance` is also a `Scene`, so you can handle it +polymorphically anywhere you work with a `Scene*` and call +`advanceAndApply(...)`. Rendering still happens through the artboard, as shown +above, because the animation instance applies its pose to that artboard before +it is drawn.