| /* | |
| * Copyright 2026 Rive | |
| */ | |
| #include "utils/serialized_replay.hpp" | |
| #include "utils/serialize_ops.hpp" | |
| #include "rive/core/binary_reader.hpp" | |
| #include "rive/math/mat2d.hpp" | |
| #include "utils/no_op_renderer.hpp" | |
| #include <unordered_map> | |
| #include <vector> | |
| using namespace rive; | |
| namespace | |
| { | |
| // Absent ids mean a truncated or corrupt stream; callers fail instead of | |
| // dereferencing a null resource. | |
| template <typename T> | |
| T* find(std::unordered_map<uint64_t, rcp<T>>& map, uint64_t id) | |
| { | |
| auto it = map.find(id); | |
| return it != map.end() ? it->second.get() : nullptr; | |
| } | |
| } // namespace | |
| bool rive::replaySerializedCommands(Span<const uint8_t> stream, | |
| Factory* factory, | |
| Renderer* renderer, | |
| const SerializedReplayHooks& hooks) | |
| { | |
| BinaryReader reader(stream); | |
| if (reader.readByte() != 'S' || reader.readByte() != 'R' || | |
| reader.readByte() != 'I' || reader.readByte() != 'V') | |
| { | |
| return false; | |
| } | |
| if (reader.readVarUint64() != 1) | |
| { | |
| return false; | |
| } | |
| std::unordered_map<uint64_t, rcp<RenderPath>> paths; | |
| std::unordered_map<uint64_t, rcp<RenderPaint>> paints; | |
| std::unordered_map<uint64_t, rcp<RenderShader>> shaders; | |
| std::unordered_map<uint64_t, rcp<RenderImage>> images; | |
| std::unordered_map<uint64_t, rcp<RenderBuffer>> buffers; | |
| std::unordered_map<uint64_t, rcp<ImageMeshInstances>> meshInstances; | |
| // Cache-as-bitmap. A canvas is declared by makeRenderCanvas, its content | |
| // arrives inline between canvasContentBegin/End, and the composite that | |
| // samples it is an ordinary drawImage of the same id -- so a canvas image | |
| // lands in `images` alongside the decoded ones. | |
| struct CanvasSize | |
| { | |
| uint32_t width; | |
| uint32_t height; | |
| }; | |
| std::unordered_map<uint64_t, CanvasSize> canvasSizes; | |
| // Where the ops currently being read draw. The screen renderer at the | |
| // bottom, a host-supplied one for each open canvas, and the null sink for | |
| // a canvas the host declined -- its content still has to parse (later ops | |
| // reference resources declared inside it) but must not reach the screen. | |
| NoOpRenderer droppedContent; | |
| Renderer* active = renderer; | |
| // One entry per open canvas bracket. The id rides along with the renderer | |
| // it interrupted so an end can be checked against the begin that opened | |
| // it; without it the stack depth is the only thing validated, and a | |
| // mismatched end would close the wrong canvas and still report success. | |
| struct OpenCanvas | |
| { | |
| uint64_t id; | |
| Renderer* interruptedRenderer; | |
| }; | |
| std::vector<OpenCanvas> interrupted; | |
| while (!reader.reachedEnd()) | |
| { | |
| SerializeOp op = static_cast<SerializeOp>(reader.readVarUint64()); | |
| if (reader.hasError()) | |
| return false; | |
| switch (op) | |
| { | |
| case SerializeOp::makeRenderPath: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| // Geometry and fill rule arrive as later ops. | |
| paths[id] = factory->makeEmptyRenderPath(); | |
| break; | |
| } | |
| case SerializeOp::makeRenderPaint: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| paints[id] = factory->makeRenderPaint(); | |
| break; | |
| } | |
| case SerializeOp::rewind: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderPath* path = find(paths, id); | |
| if (path == nullptr) | |
| return false; | |
| path->rewind(); | |
| break; | |
| } | |
| case SerializeOp::fillRule: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderPath* path = find(paths, id); | |
| if (path == nullptr) | |
| return false; | |
| path->fillRule(static_cast<FillRule>(reader.readVarUint64())); | |
| break; | |
| } | |
| case SerializeOp::addRawPath: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderPath* path = find(paths, id); | |
| if (path == nullptr) | |
| return false; | |
| RawPath rp = deserializeRawPath(reader); | |
| path->addRawPath(rp); | |
| break; | |
| } | |
| case SerializeOp::color: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderPaint* paint = find(paints, id); | |
| if (paint == nullptr) | |
| return false; | |
| paint->color(static_cast<unsigned int>(reader.readVarUint64())); | |
| break; | |
| } | |
| case SerializeOp::style: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderPaint* paint = find(paints, id); | |
| if (paint == nullptr) | |
| return false; | |
| // The stream writes 0 for stroke and 1 for fill. | |
| bool stroked = reader.readVarUint64() == 0; | |
| paint->style(stroked ? RenderPaintStyle::stroke | |
| : RenderPaintStyle::fill); | |
| break; | |
| } | |
| case SerializeOp::thickness: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderPaint* paint = find(paints, id); | |
| if (paint == nullptr) | |
| return false; | |
| paint->thickness(reader.readFloat32()); | |
| break; | |
| } | |
| case SerializeOp::join: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderPaint* paint = find(paints, id); | |
| if (paint == nullptr) | |
| return false; | |
| paint->join(static_cast<StrokeJoin>(reader.readVarUint64())); | |
| break; | |
| } | |
| case SerializeOp::cap: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderPaint* paint = find(paints, id); | |
| if (paint == nullptr) | |
| return false; | |
| paint->cap(static_cast<StrokeCap>(reader.readVarUint64())); | |
| break; | |
| } | |
| case SerializeOp::feather: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderPaint* paint = find(paints, id); | |
| if (paint == nullptr) | |
| return false; | |
| paint->feather(reader.readFloat32()); | |
| break; | |
| } | |
| case SerializeOp::blendMode: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderPaint* paint = find(paints, id); | |
| if (paint == nullptr) | |
| return false; | |
| paint->blendMode( | |
| static_cast<BlendMode>(reader.readVarUint64())); | |
| break; | |
| } | |
| case SerializeOp::additiveness: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderPaint* paint = find(paints, id); | |
| if (paint == nullptr) | |
| return false; | |
| paint->additiveness(reader.readFloat32()); | |
| break; | |
| } | |
| case SerializeOp::shader: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| uint64_t shaderId = reader.readVarUint64(); | |
| RenderPaint* paint = find(paints, id); | |
| if (paint == nullptr) | |
| return false; | |
| // The stream writes 0 for both nullptr and shader id 0. The map | |
| // resolves it since a missing entry yields a null rcp. | |
| paint->shader(shaders[shaderId]); | |
| break; | |
| } | |
| case SerializeOp::paintModulatedImage: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| uint64_t rawImageId = reader.readVarUint64(); | |
| auto filter = static_cast<ImageFilter>(reader.readVarUint64()); | |
| auto wrapX = static_cast<ImageWrap>(reader.readVarUint64()); | |
| auto wrapY = static_cast<ImageWrap>(reader.readVarUint64()); | |
| float xx = reader.readFloat32(); | |
| float xy = reader.readFloat32(); | |
| float yx = reader.readFloat32(); | |
| float yy = reader.readFloat32(); | |
| float tx = reader.readFloat32(); | |
| float ty = reader.readFloat32(); | |
| RenderPaint* paint = find(paints, id); | |
| if (paint == nullptr) | |
| return false; | |
| // Image id is offset by one; 0 means no image. | |
| RenderImage* image = | |
| rawImageId == 0 ? nullptr : images[rawImageId - 1].get(); | |
| ImageSampler sampler; | |
| sampler.filter = filter; | |
| sampler.wrapX = wrapX; | |
| sampler.wrapY = wrapY; | |
| paint->modulatedImage(image, | |
| sampler, | |
| Mat2D(xx, xy, yx, yy, tx, ty)); | |
| break; | |
| } | |
| case SerializeOp::makeLinearGradient: | |
| case SerializeOp::makeRadialGradient: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| size_t count = static_cast<size_t>(reader.readVarUint64()); | |
| std::vector<ColorInt> colors(count); | |
| std::vector<float> stops(count); | |
| for (size_t i = 0; i < count; ++i) | |
| { | |
| colors[i] = static_cast<ColorInt>(reader.readVarUint64()); | |
| stops[i] = reader.readFloat32(); | |
| } | |
| float a = reader.readFloat32(); | |
| float b = reader.readFloat32(); | |
| float c = reader.readFloat32(); | |
| if (op == SerializeOp::makeLinearGradient) | |
| { | |
| float d = reader.readFloat32(); | |
| shaders[id] = factory->makeLinearGradient(a, | |
| b, | |
| c, | |
| d, | |
| colors.data(), | |
| stops.data(), | |
| count); | |
| } | |
| else | |
| { | |
| shaders[id] = factory->makeRadialGradient(a, | |
| b, | |
| c, | |
| colors.data(), | |
| stops.data(), | |
| count); | |
| } | |
| break; | |
| } | |
| case SerializeOp::decodeImage: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| size_t size = static_cast<size_t>(reader.readVarUint64()); | |
| Span<const uint8_t> data = reader.readBytes(size); | |
| images[id] = factory->decodeImage(data); | |
| break; | |
| } | |
| case SerializeOp::makeRenderBuffer: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| size_t size = static_cast<size_t>(reader.readVarUint64()); | |
| auto type = | |
| static_cast<RenderBufferType>(reader.readVarUint64()); | |
| auto flags = | |
| static_cast<RenderBufferFlags>(reader.readVarUint64()); | |
| buffers[id] = factory->makeRenderBuffer(type, flags, size); | |
| break; | |
| } | |
| case SerializeOp::setVertexBufferData: | |
| case SerializeOp::setIndexBufferData: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| RenderBuffer* buf = find(buffers, id); | |
| if (buf == nullptr) | |
| return false; | |
| void* mapped = buf->map(); | |
| if (op == SerializeOp::setVertexBufferData) | |
| { | |
| size_t n = buf->sizeInBytes() / sizeof(float); | |
| float* out = static_cast<float*>(mapped); | |
| for (size_t i = 0; i < n; ++i) | |
| out[i] = reader.readFloat32(); | |
| } | |
| else | |
| { | |
| size_t n = buf->sizeInBytes() / sizeof(uint16_t); | |
| uint16_t* out = static_cast<uint16_t*>(mapped); | |
| for (size_t i = 0; i < n; ++i) | |
| out[i] = static_cast<uint16_t>(reader.readVarUint64()); | |
| } | |
| buf->unmap(); | |
| break; | |
| } | |
| case SerializeOp::save: | |
| active->save(); | |
| break; | |
| case SerializeOp::restore: | |
| active->restore(); | |
| break; | |
| case SerializeOp::transform: | |
| { | |
| float m[6]; | |
| for (int i = 0; i < 6; ++i) | |
| m[i] = reader.readFloat32(); | |
| active->transform(Mat2D(m[0], m[1], m[2], m[3], m[4], m[5])); | |
| break; | |
| } | |
| case SerializeOp::modulateOpacity: | |
| active->modulateOpacity(reader.readFloat32()); | |
| break; | |
| case SerializeOp::modulateColor: | |
| { | |
| ColorInt color = (ColorInt)reader.readVarUint64(); | |
| active->modulateColor(color, reader.readVarUint64() != 0); | |
| break; | |
| } | |
| case SerializeOp::drawPath: | |
| { | |
| uint64_t pathId = reader.readVarUint64(); | |
| uint64_t paintId = reader.readVarUint64(); | |
| RenderPath* path = find(paths, pathId); | |
| RenderPaint* paint = find(paints, paintId); | |
| if (path == nullptr || paint == nullptr) | |
| return false; | |
| active->drawPath(path, paint); | |
| break; | |
| } | |
| case SerializeOp::clipPath: | |
| { | |
| uint64_t pathId = reader.readVarUint64(); | |
| RenderPath* path = find(paths, pathId); | |
| if (path == nullptr) | |
| return false; | |
| active->clipPath(path); | |
| break; | |
| } | |
| case SerializeOp::applyLayerMask: | |
| { | |
| uint64_t maskId = reader.readVarUint64(); | |
| auto mode = static_cast<LayerMaskMode>(reader.readVarUint64()); | |
| // Null when a decode failed or when the host declined the | |
| // canvas this id names. Skipping leaves the layer unmasked, | |
| // which is the same degradation the feature falls back to | |
| // everywhere else, and the rest of the frame still replays. | |
| if (RenderImage* mask = find(images, maskId)) | |
| { | |
| active->applyLayerMask(mask, | |
| ImageSampler::LinearClamp(), | |
| mode); | |
| } | |
| break; | |
| } | |
| case SerializeOp::drawImage: | |
| case SerializeOp::drawImageAdditive: | |
| { | |
| uint64_t imageId = reader.readVarUint64(); | |
| auto blend = static_cast<BlendMode>(reader.readVarUint64()); | |
| float opacity = reader.readFloat32(); | |
| // Only the additive variant carries the trailing float; the | |
| // plain op means an additiveness of 0. | |
| float additiveness = op == SerializeOp::drawImageAdditive | |
| ? reader.readFloat32() | |
| : 0.0f; | |
| // Null when a decode failed or when the host declined the | |
| // canvas this id names; either way there is nothing to | |
| // composite and the rest of the frame still replays. | |
| if (RenderImage* image = find(images, imageId)) | |
| { | |
| active->drawImage(image, | |
| ImageSampler::LinearClamp(), | |
| blend, | |
| opacity, | |
| additiveness); | |
| } | |
| break; | |
| } | |
| case SerializeOp::drawImageMesh: | |
| case SerializeOp::drawImageMeshAdditive: | |
| { | |
| RenderImage* image = find(images, reader.readVarUint64()); | |
| auto blend = static_cast<BlendMode>(reader.readVarUint64()); | |
| float opacity = reader.readFloat32(); | |
| RenderBuffer* pos = find(buffers, reader.readVarUint64()); | |
| RenderBuffer* uvs = find(buffers, reader.readVarUint64()); | |
| RenderBuffer* idx = find(buffers, reader.readVarUint64()); | |
| float additiveness = op == SerializeOp::drawImageMeshAdditive | |
| ? reader.readFloat32() | |
| : 0.0f; | |
| // Draws referring to failed or invalid objects are dropped. | |
| if (image != nullptr && pos != nullptr && uvs != nullptr && | |
| idx != nullptr) | |
| { | |
| uint32_t vertexCount = static_cast<uint32_t>( | |
| pos->sizeInBytes() / (2 * sizeof(float))); | |
| uint32_t indexCount = static_cast<uint32_t>( | |
| idx->sizeInBytes() / sizeof(uint16_t)); | |
| active->drawImageMesh(image, | |
| ImageSampler::LinearClamp(), | |
| ref_rcp(pos), | |
| ref_rcp(uvs), | |
| ref_rcp(idx), | |
| vertexCount, | |
| indexCount, | |
| blend, | |
| opacity, | |
| additiveness); | |
| } | |
| break; | |
| } | |
| case SerializeOp::makeImageMeshInstances: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| size_t count = static_cast<size_t>(reader.readVarUint64()); | |
| meshInstances[id] = factory->makeImageMeshInstances(count); | |
| break; | |
| } | |
| case SerializeOp::setImageMeshInstancesData: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| size_t count = static_cast<size_t>(reader.readVarUint64()); | |
| // An unknown id is dropped, but its payload is still read | |
| // so the stream stays in step. | |
| ImageMeshInstances* mi = find(meshInstances, id); | |
| Span<ImageMeshInstanceData> dst = | |
| mi != nullptr ? mi->edit(count) | |
| : Span<ImageMeshInstanceData>(); | |
| for (size_t i = 0; i < count; ++i) | |
| { | |
| ImageMeshInstanceData instance; | |
| float m[6]; | |
| for (int j = 0; j < 6; ++j) | |
| { | |
| m[j] = reader.readFloat32(); | |
| } | |
| instance.transform = | |
| Mat2D(m[0], m[1], m[2], m[3], m[4], m[5]); | |
| instance.uvTranslate.x = reader.readFloat32(); | |
| instance.uvTranslate.y = reader.readFloat32(); | |
| instance.uvScale.x = reader.readFloat32(); | |
| instance.uvScale.y = reader.readFloat32(); | |
| instance.opacity = reader.readFloat32(); | |
| instance.additiveness = reader.readFloat32(); | |
| if (i < dst.size()) | |
| { | |
| dst[i] = instance; | |
| } | |
| } | |
| if (mi != nullptr) | |
| { | |
| mi->endEdit(); | |
| } | |
| break; | |
| } | |
| case SerializeOp::drawImageMeshInstanced: | |
| { | |
| RenderImage* image = find(images, reader.readVarUint64()); | |
| RenderBuffer* pos = find(buffers, reader.readVarUint64()); | |
| RenderBuffer* uvs = find(buffers, reader.readVarUint64()); | |
| RenderBuffer* idx = find(buffers, reader.readVarUint64()); | |
| ImageMeshInstances* mi = | |
| find(meshInstances, reader.readVarUint64()); | |
| if (image != nullptr && pos != nullptr && uvs != nullptr && | |
| idx != nullptr && mi != nullptr) | |
| { | |
| uint32_t vertexCount = static_cast<uint32_t>( | |
| pos->sizeInBytes() / (2 * sizeof(float))); | |
| uint32_t indexCount = static_cast<uint32_t>( | |
| idx->sizeInBytes() / sizeof(uint16_t)); | |
| active->drawImageMeshInstanced(image, | |
| ImageSampler::LinearClamp(), | |
| ref_rcp(pos), | |
| ref_rcp(uvs), | |
| ref_rcp(idx), | |
| vertexCount, | |
| indexCount, | |
| ref_rcp(mi)); | |
| } | |
| break; | |
| } | |
| case SerializeOp::makeRenderCanvas: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| uint32_t w = static_cast<uint32_t>(reader.readVarUint64()); | |
| uint32_t h = static_cast<uint32_t>(reader.readVarUint64()); | |
| // No allocation here: the host mints the target when the | |
| // content actually opens, on whatever device it replays | |
| // against. | |
| canvasSizes[id] = {w, h}; | |
| break; | |
| } | |
| case SerializeOp::canvasContentBegin: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| uint32_t clearColor = | |
| static_cast<uint32_t>(reader.readVarUint64()); | |
| auto it = canvasSizes.find(id); | |
| if (it == canvasSizes.end()) | |
| { | |
| return false; // content for a canvas never declared | |
| } | |
| interrupted.push_back({id, active}); | |
| rcp<RenderImage> image; | |
| Renderer* content = | |
| hooks.onCanvasContentBegin | |
| ? hooks.onCanvasContentBegin(id, | |
| it->second.width, | |
| it->second.height, | |
| clearColor, | |
| &image) | |
| : nullptr; | |
| // A host with no offscreen target returns null. Its content is | |
| // parsed and dropped, and the composite that names this id | |
| // finds no image and draws nothing. | |
| if (content != nullptr) | |
| { | |
| images[id] = std::move(image); | |
| } | |
| else | |
| { | |
| // The raster was declined, so whatever the host may have | |
| // allocated holds no content for this bracket. Drop any | |
| // image it set, and any image a previous bracket left under | |
| // this id, so the composite draws nothing rather than stale | |
| // or uninitialized pixels. | |
| images.erase(id); | |
| } | |
| active = content != nullptr ? content : &droppedContent; | |
| break; | |
| } | |
| case SerializeOp::canvasContentEnd: | |
| { | |
| uint64_t id = reader.readVarUint64(); | |
| if (interrupted.empty()) | |
| { | |
| return false; // end without a matching begin | |
| } | |
| if (interrupted.back().id != id) | |
| { | |
| // Ending a canvas the innermost bracket never opened. | |
| // Honoring it would tell the host to close an unrelated | |
| // canvas and hand the wrong renderer back to the ops that | |
| // follow, so treat it as a malformed stream. | |
| return false; | |
| } | |
| if (hooks.onCanvasContentEnd) | |
| { | |
| hooks.onCanvasContentEnd(id); | |
| } | |
| active = interrupted.back().interruptedRenderer; | |
| interrupted.pop_back(); | |
| break; | |
| } | |
| case SerializeOp::frame: | |
| if (hooks.onFrame) | |
| hooks.onFrame(); | |
| break; | |
| case SerializeOp::frameSize: | |
| { | |
| uint32_t w = static_cast<uint32_t>(reader.readVarUint64()); | |
| uint32_t h = static_cast<uint32_t>(reader.readVarUint64()); | |
| if (hooks.onFrameSize) | |
| hooks.onFrameSize(w, h); | |
| break; | |
| } | |
| default: | |
| return false; // unknown opcode | |
| } | |
| if (reader.hasError()) | |
| return false; | |
| } | |
| // A canvas left open means the stream was cut mid-bracket. | |
| return interrupted.empty(); | |
| } |