Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,20 @@
{
"group": "Runtimes",
"pages": [
{
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.

Can you put this after Unity?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Architecturally I think it makes sense to have core first followed by the wrappers. At least as a dev, that's how I would expect it to flow. Happy to discuss and hear your thoughts.

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.

Agreed, but it's also the most complex, the most intimidating, and the least likely for someone to use.

"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": [
Expand Down
Binary file added images/runtimes/cpp/rendercontext_layers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/runtimes/cpp/rive_pipeline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
156 changes: 156 additions & 0 deletions runtimes/cpp/asset-loading.mdx
Original file line number Diff line number Diff line change
@@ -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<FileAssetLoader> {
public:
virtual bool loadContents(FileAsset& asset,
Span<const uint8_t> 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 <filesystem>
#include <fstream>
Comment thread
Tod-Rive marked this conversation as resolved.
#include <iterator>
#include <vector>

class DiskAssetLoader : public rive::FileAssetLoader {
public:
explicit DiskAssetLoader(std::filesystem::path root)
: m_root(std::move(root)) {}

bool loadContents(rive::FileAsset& asset,
rive::Span<const uint8_t> 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<uint8_t> bytes((std::istreambuf_iterator<char>(f)), {});
rive::Span<const uint8_t> span{bytes.data(), bytes.size()};

if (auto* img = dynamic_cast<rive::ImageAsset*>(&asset)) {
img->renderImage(factory->decodeImage(span));
return true;
}
if (auto* fnt = dynamic_cast<rive::FontAsset*>(&asset)) {
fnt->font(factory->decodeFont(span));
return true;
}
if (auto* aud = dynamic_cast<rive::AudioAsset*>(&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<FileAssetLoader> loader = make_rcp<DiskAssetLoader>("assets/");

ImportResult result;
rcp<File> 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<const uint8_t>, Factory* factory) override {
auto* image = dynamic_cast<ImageAsset*>(&asset);
if (!image) return false;

rcp<ImageAsset> keepAlive = ref_rcp(image);

fetchAsync(image->cdnUuidStr(), [keepAlive, factory](std::vector<uint8_t> bytes) {
// Assumes this callback is dispatched to the render thread — see the
// warning below about decoder thread-safety.
rcp<RenderImage> ri =
factory->decodeImage({bytes.data(), bytes.size()});
keepAlive->renderImage(std::move(ri));
// The next advanceAndApply will pick up the new image.
Comment thread
Tod-Rive marked this conversation as resolved.
});
return true;
}
```

<Warning>
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.
</Warning>

## 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<FileAssetLoader> loader =
make_rcp<RelativeLocalAssetLoader>("/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.
185 changes: 185 additions & 0 deletions runtimes/cpp/data-binding.mdx
Original file line number Diff line number Diff line change
@@ -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<ViewModelInstanceRuntime> instance = vm->createDefaultInstance();
if (instance) {
artboard->bindViewModelInstance(instance->instance());
scene ->bindViewModelInstance(instance->instance());
}
Comment thread
Tod-Rive marked this conversation as resolved.
Comment thread
Tod-Rive marked this conversation as resolved.
```

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<ViewModelInstanceRuntime> instance = vm->createDefaultInstance();
// alternatives — pick one and replace the line above:
// rcp<ViewModelInstanceRuntime> instance = vm->createInstanceFromName("Hero");
// rcp<ViewModelInstanceRuntime> instance = vm->createInstanceFromIndex(0);
// rcp<ViewModelInstanceRuntime> instance = vm->createInstance(); // no editor preset; properties at type defaults

if (!instance) return;
artboard->bindViewModelInstance(instance->instance());
scene ->bindViewModelInstance(instance->instance());
```
Comment thread
Tod-Rive marked this conversation as resolved.

<Note>
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.
</Note>

## 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<ViewModelInstanceRuntime> header = card->propertyViewModel("header");
auto* title = header->propertyString("title");
Comment thread
Tod-Rive marked this conversation as resolved.
```

You can also **swap** a nested view model wholesale — useful for swapping a
list cell's data without rebuilding the artboard:

```cpp
rcp<ViewModelInstanceRuntime> 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<ViewModelInstanceRuntime> row = items->instanceAt(0);
Comment thread
Tod-Rive marked this conversation as resolved.
```

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<BindableArtboard>
Comment thread
Tod-Rive marked this conversation as resolved.
```

## 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, ... }
}
```
Loading