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
#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
#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>
#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
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:
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.
});
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:
#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.