| /* |
| * Copyright 2026 Rive |
| */ |
| |
| #include "rive/renderer/cmd/deferred_render_factory.hpp" |
| #include "rive/renderer/cmd/render_replay.hpp" |
| |
| // Out of line home for the large deferred stream bodies, so every TU that |
| // touches the deferred headers does not recompile them. |
| namespace rive::cmd |
| { |
| |
| bool sniffImageSize(Span<const uint8_t> b, int& w, int& h) |
| { |
| const uint8_t* d = b.data(); |
| size_t n = b.size(); |
| auto be32 = [&](size_t i) { |
| return (d[i] << 24) | (d[i + 1] << 16) | (d[i + 2] << 8) | d[i + 3]; |
| }; |
| // PNG: 8-byte sig, then IHDR with width/height as big-endian u32 at 16/20. |
| if (n >= 24 && d[0] == 0x89 && d[1] == 'P' && d[2] == 'N' && d[3] == 'G') |
| { |
| w = static_cast<int>(be32(16)); |
| h = static_cast<int>(be32(20)); |
| return true; |
| } |
| // GIF: "GIF87a"/"GIF89a", then width/height little-endian u16 at 6/8. |
| if (n >= 10 && d[0] == 'G' && d[1] == 'I' && d[2] == 'F') |
| { |
| w = d[6] | (d[7] << 8); |
| h = d[8] | (d[9] << 8); |
| return true; |
| } |
| // WEBP: RIFF....WEBP; VP8 / VP8L / VP8X carry dims at known offsets. |
| if (n >= 30 && d[0] == 'R' && d[1] == 'I' && d[2] == 'F' && d[3] == 'F' && |
| d[8] == 'W' && d[9] == 'E' && d[10] == 'B' && d[11] == 'P') |
| { |
| if (d[12] == 'V' && d[13] == 'P' && d[14] == '8' && d[15] == ' ') |
| { |
| w = (d[26] | (d[27] << 8)) & 0x3fff; |
| h = (d[28] | (d[29] << 8)) & 0x3fff; |
| return true; |
| } |
| if (d[12] == 'V' && d[13] == 'P' && d[14] == '8' && d[15] == 'L') |
| { |
| uint32_t bits = |
| d[21] | (d[22] << 8) | (d[23] << 16) | (d[24] << 24); |
| w = static_cast<int>((bits & 0x3fff) + 1); |
| h = static_cast<int>(((bits >> 14) & 0x3fff) + 1); |
| return true; |
| } |
| if (d[12] == 'V' && d[13] == 'P' && d[14] == '8' && d[15] == 'X') |
| { |
| w = (d[24] | (d[25] << 8) | (d[26] << 16)) + 1; |
| h = (d[27] | (d[28] << 8) | (d[29] << 16)) + 1; |
| return true; |
| } |
| } |
| // JPEG: walk markers to an SOF (0xC0..0xCF, excluding non-SOF) for dims. |
| if (n >= 4 && d[0] == 0xff && d[1] == 0xd8) |
| { |
| size_t i = 2; |
| while (i + 9 < n) |
| { |
| if (d[i] != 0xff) |
| { |
| i++; |
| continue; |
| } |
| uint8_t marker = d[i + 1]; |
| // SOF0..SOF15 carry dims; skip DHT(C4)/DAA(C8)/DAC(CC) which don't. |
| if (marker >= 0xc0 && marker <= 0xcf && marker != 0xc4 && |
| marker != 0xc8 && marker != 0xcc) |
| { |
| h = (d[i + 5] << 8) | d[i + 6]; |
| w = (d[i + 7] << 8) | d[i + 8]; |
| return true; |
| } |
| uint32_t seg = (d[i + 2] << 8) | d[i + 3]; |
| i += 2 + seg; |
| } |
| } |
| return false; |
| } |
| |
| void replayRenderCommands(Factory* factory, |
| Renderer* renderer, |
| Span<const uint8_t> commands, |
| Span<const uint8_t> blobs, |
| ResourceTable& table, |
| const ReplayHooks& hooks) |
| { |
| auto& paths = table.paths; |
| auto& paints = table.paints; |
| auto& shaders = table.shaders; |
| auto& images = table.images; |
| auto& buffers = table.buffers; |
| |
| auto filterAllows = [](ReplayFilter f, RenderCmd c) { |
| if (f == ReplayFilter::all) |
| { |
| return true; |
| } |
| switch (c) |
| { |
| case RenderCmd::save: |
| case RenderCmd::restore: |
| case RenderCmd::transform: |
| case RenderCmd::drawPath: |
| case RenderCmd::clipPath: |
| case RenderCmd::drawImage: |
| case RenderCmd::drawImageMesh: |
| case RenderCmd::modulateOpacity: |
| case RenderCmd::canvasContentBegin: |
| case RenderCmd::canvasContentEnd: |
| return f == ReplayFilter::draws; |
| case RenderCmd::destroyResource: |
| return f == ReplayFilter::destroys; |
| default: |
| return f == ReplayFilter::resources; |
| } |
| }; |
| |
| RenderCommandReader reader(commands, blobs); |
| auto path = [&](RenderHandle h) -> RenderPath* { return paths.get(h); }; |
| auto paint = [&](RenderHandle h) -> RenderPaint* { return paints.get(h); }; |
| auto image = [&](RenderHandle h) -> RenderImage* { return images.get(h); }; |
| auto sampler = [](uint8_t wx, uint8_t wy, uint8_t f) { |
| return ImageSampler{static_cast<ImageWrap>(wx), |
| static_cast<ImageWrap>(wy), |
| static_cast<ImageFilter>(f)}; |
| }; |
| |
| // Draws route into cur: the screen by default, or the active canvas |
| // between content brackets. Null drops the draw. |
| Renderer* cur = renderer; |
| |
| uint8_t type; |
| uint8_t prevType = 255; |
| size_t prevPos = 0; |
| while (reader.next(type)) |
| { |
| if (type > static_cast<uint8_t>(RenderCmd::lastRenderCmd)) |
| { |
| // Unknown opcode: its payload was not consumed, so every later |
| // read would desync. Stop. |
| fprintf(stderr, |
| "rive replay ABORT: opcode %u at byte %zu of %zu, last " |
| "good opcode %u at byte %zu\n", |
| type, |
| reader.position() - 1, |
| commands.size(), |
| prevType, |
| prevPos); |
| assert(false); |
| break; |
| } |
| prevType = type; |
| prevPos = reader.position() - 1; |
| const auto cmd = static_cast<RenderCmd>(type); |
| if (!filterAllows(hooks.filter, cmd)) |
| { |
| reader.skip(payloadSizeOf(cmd)); |
| continue; |
| } |
| switch (cmd) |
| { |
| case RenderCmd::makePath: |
| { |
| auto c = reader.read<MakePathPOD>(); |
| RawPath raw = rebuildRawPath( |
| reader.blobAt(c.blobOffset, |
| c.verbCount * |
| static_cast<uint32_t>(sizeof(PathVerb))), |
| reader.blobAt(c.pointsOffset, |
| c.pointCount * |
| static_cast<uint32_t>(sizeof(Vec2D)))); |
| paths.set( |
| c.id, |
| factory->makeRenderPath(raw, |
| static_cast<FillRule>(c.fillRule)), |
| c.generation); |
| if (c.id >= table.pathFillRules.size()) |
| { |
| table.pathFillRules.resize(c.id + 1); |
| } |
| table.pathFillRules[c.id] = c.fillRule; |
| break; |
| } |
| case RenderCmd::makeEmptyPath: |
| { |
| auto c = reader.read<MakeIdPOD>(); |
| paths.set(c.id, factory->makeEmptyRenderPath(), c.generation); |
| if (c.id >= table.pathFillRules.size()) |
| { |
| table.pathFillRules.resize(c.id + 1); |
| } |
| table.pathFillRules[c.id] = 0; |
| break; |
| } |
| case RenderCmd::makePaint: |
| { |
| auto c = reader.read<MakeIdPOD>(); |
| paints.set(c.id, factory->makeRenderPaint(), c.generation); |
| if (c.id >= table.paintShadows.size()) |
| { |
| table.paintShadows.resize(c.id + 1); |
| } |
| table.paintShadows[c.id] = PaintShadow{}; |
| break; |
| } |
| case RenderCmd::makeLinearGradient: |
| { |
| auto c = reader.read<LinearGradientPOD>(); |
| const ColorInt* colors = reinterpret_cast<const ColorInt*>( |
| reader |
| .blobAt(c.blobOffset, |
| c.count * |
| static_cast<uint32_t>(sizeof(ColorInt))) |
| .data()); |
| const float* stops = reinterpret_cast<const float*>( |
| reader |
| .blobAt(c.stopsOffset, |
| c.count * static_cast<uint32_t>(sizeof(float))) |
| .data()); |
| if (colors == nullptr || stops == nullptr) |
| { |
| break; // blob out of range (corrupt stream) |
| } |
| shaders.set(c.id, |
| factory->makeLinearGradient(c.sx, |
| c.sy, |
| c.ex, |
| c.ey, |
| colors, |
| stops, |
| c.count), |
| c.generation); |
| break; |
| } |
| case RenderCmd::makeRadialGradient: |
| { |
| auto c = reader.read<RadialGradientPOD>(); |
| const ColorInt* colors = reinterpret_cast<const ColorInt*>( |
| reader |
| .blobAt(c.blobOffset, |
| c.count * |
| static_cast<uint32_t>(sizeof(ColorInt))) |
| .data()); |
| const float* stops = reinterpret_cast<const float*>( |
| reader |
| .blobAt(c.stopsOffset, |
| c.count * static_cast<uint32_t>(sizeof(float))) |
| .data()); |
| if (colors == nullptr || stops == nullptr) |
| { |
| break; // blob out of range (corrupt stream) |
| } |
| shaders.set(c.id, |
| factory->makeRadialGradient(c.cx, |
| c.cy, |
| c.radius, |
| colors, |
| stops, |
| c.count), |
| c.generation); |
| break; |
| } |
| case RenderCmd::decodeImage: |
| { |
| auto c = reader.read<DecodeImagePOD>(); |
| images.set(c.id, |
| factory->decodeImage( |
| reader.blobAt(c.blobOffset, c.byteCount)), |
| c.generation); |
| break; |
| } |
| case RenderCmd::makeBuffer: |
| { |
| auto c = reader.read<MakeBufferPOD>(); |
| buffers.set(c.id, |
| factory->makeRenderBuffer( |
| static_cast<RenderBufferType>(c.bufferType), |
| static_cast<RenderBufferFlags>(c.flags), |
| c.sizeInBytes), |
| c.generation); |
| if (c.id >= table.bufferShadows.size()) |
| { |
| table.bufferShadows.resize(c.id + 1); |
| } |
| table.bufferShadows[c.id] = {static_cast<uint8_t>(c.bufferType), |
| static_cast<uint16_t>(c.flags), |
| c.sizeInBytes}; |
| break; |
| } |
| case RenderCmd::bufferData: |
| { |
| auto c = reader.read<BufferDataPOD>(); |
| Span<const uint8_t> src = reader.blobAt(c.blobOffset, c.size); |
| if (auto* b = buffers.get(c.buffer)) |
| { |
| if (src.size() == c.size) |
| { |
| void* dst = b->map(); |
| if (dst) |
| { |
| std::memcpy(dst, src.data(), c.size); |
| } |
| b->unmap(); |
| } |
| } |
| break; |
| } |
| case RenderCmd::destroyResource: |
| { |
| auto c = reader.read<DestroyResourcePOD>(); |
| table.destroy(static_cast<ResourceKind>(c.kind), |
| c.id, |
| c.generation); |
| break; |
| } |
| case RenderCmd::resourceNewVersion: |
| { |
| // A drawn resource was mutated again: alias the outgoing |
| // version for the draws that pinned it and continue on a |
| // fresh object carrying the shadowed state. |
| auto c = reader.read<ResourceVersionPOD>(); |
| switch (static_cast<ResourceKind>(c.kind)) |
| { |
| case ResourceKind::paint: |
| { |
| auto fresh = factory->makeRenderPaint(); |
| if (fresh != nullptr && |
| c.id < table.paintShadows.size()) |
| { |
| const PaintShadow& sh = table.paintShadows[c.id]; |
| fresh->style( |
| static_cast<RenderPaintStyle>(sh.style)); |
| fresh->color(sh.color); |
| fresh->thickness(sh.thickness); |
| fresh->join(static_cast<StrokeJoin>(sh.join)); |
| fresh->cap(static_cast<StrokeCap>(sh.cap)); |
| fresh->feather(sh.feather); |
| fresh->blendMode( |
| static_cast<BlendMode>(sh.blendMode)); |
| if (sh.shader != kInvalidRenderHandle) |
| { |
| fresh->shader(shaders.shared(sh.shader)); |
| } |
| } |
| paints.newVersion(c.id, c.version, std::move(fresh)); |
| break; |
| } |
| case ResourceKind::path: |
| { |
| // Seed from the outgoing version so a non rewind |
| // mutation appends onto prior geometry; a rewind bump |
| // clears the seed via its own recorded command. |
| auto fresh = factory->makeEmptyRenderPath(); |
| if (fresh != nullptr) |
| { |
| if (auto* outgoing = paths.get(c.id)) |
| { |
| fresh->addRenderPath(outgoing, Mat2D()); |
| } |
| if (c.id < table.pathFillRules.size()) |
| { |
| fresh->fillRule(static_cast<FillRule>( |
| table.pathFillRules[c.id])); |
| } |
| } |
| paths.newVersion(c.id, c.version, std::move(fresh)); |
| break; |
| } |
| case ResourceKind::buffer: |
| { |
| rcp<RenderBuffer> fresh; |
| if (c.id < table.bufferShadows.size()) |
| { |
| const BufferShadow& sh = table.bufferShadows[c.id]; |
| fresh = factory->makeRenderBuffer( |
| static_cast<RenderBufferType>(sh.type), |
| static_cast<RenderBufferFlags>(sh.flags), |
| sh.size); |
| } |
| buffers.newVersion(c.id, c.version, std::move(fresh)); |
| break; |
| } |
| default: |
| break; // shaders and images never mutate |
| } |
| break; |
| } |
| |
| case RenderCmd::pathRewind: |
| { |
| auto c = reader.read<ResIdPOD>(); |
| if (auto* p = path(c.id)) |
| { |
| p->rewind(); |
| } |
| break; |
| } |
| case RenderCmd::pathFillRule: |
| { |
| auto c = reader.read<PathFillRulePOD>(); |
| if (auto* p = path(c.path)) |
| { |
| p->fillRule(static_cast<FillRule>(c.fillRule)); |
| table.pathFillRules[c.path] = c.fillRule; |
| } |
| break; |
| } |
| case RenderCmd::pathAddRawPath: |
| { |
| auto c = reader.read<PathRawPOD>(); |
| RawPath raw = rebuildRawPath( |
| reader.blobAt(c.blobOffset, |
| c.verbCount * |
| static_cast<uint32_t>(sizeof(PathVerb))), |
| reader.blobAt(c.pointsOffset, |
| c.pointCount * |
| static_cast<uint32_t>(sizeof(Vec2D)))); |
| if (auto* p = path(c.path)) |
| { |
| p->addRawPath(raw); |
| } |
| break; |
| } |
| case RenderCmd::pathAddRenderPath: |
| { |
| auto c = reader.read<PathAddPathPOD>(); |
| RenderPath* src = paths.get(c.src); |
| if (auto* p = path(c.path)) |
| { |
| if (src) |
| p->addRenderPath( |
| src, |
| Mat2D(c.xx, c.xy, c.yx, c.yy, c.tx, c.ty)); |
| } |
| break; |
| } |
| |
| case RenderCmd::paintStyle: |
| { |
| auto c = reader.read<PaintU8POD>(); |
| if (auto* pt = paint(c.paint)) |
| { |
| pt->style(static_cast<RenderPaintStyle>(c.value)); |
| table.paintShadows[c.paint].style = c.value; |
| } |
| break; |
| } |
| case RenderCmd::paintColor: |
| { |
| auto c = reader.read<PaintColorPOD>(); |
| if (auto* pt = paint(c.paint)) |
| { |
| pt->color(c.color); |
| table.paintShadows[c.paint].color = c.color; |
| } |
| break; |
| } |
| case RenderCmd::paintThickness: |
| { |
| auto c = reader.read<PaintFloatPOD>(); |
| if (auto* pt = paint(c.paint)) |
| { |
| pt->thickness(c.value); |
| table.paintShadows[c.paint].thickness = c.value; |
| } |
| break; |
| } |
| case RenderCmd::paintJoin: |
| { |
| auto c = reader.read<PaintU8POD>(); |
| if (auto* pt = paint(c.paint)) |
| { |
| pt->join(static_cast<StrokeJoin>(c.value)); |
| table.paintShadows[c.paint].join = c.value; |
| } |
| break; |
| } |
| case RenderCmd::paintCap: |
| { |
| auto c = reader.read<PaintU8POD>(); |
| if (auto* pt = paint(c.paint)) |
| { |
| pt->cap(static_cast<StrokeCap>(c.value)); |
| table.paintShadows[c.paint].cap = c.value; |
| } |
| break; |
| } |
| case RenderCmd::paintFeather: |
| { |
| auto c = reader.read<PaintFloatPOD>(); |
| if (auto* pt = paint(c.paint)) |
| { |
| pt->feather(c.value); |
| table.paintShadows[c.paint].feather = c.value; |
| } |
| break; |
| } |
| case RenderCmd::paintBlendMode: |
| { |
| auto c = reader.read<PaintU8POD>(); |
| if (auto* pt = paint(c.paint)) |
| { |
| pt->blendMode(static_cast<BlendMode>(c.value)); |
| table.paintShadows[c.paint].blendMode = c.value; |
| } |
| break; |
| } |
| case RenderCmd::paintShader: |
| { |
| auto c = reader.read<PaintShaderPOD>(); |
| if (auto* pt = paint(c.paint)) |
| { |
| pt->shader(shaders.shared(c.shader)); |
| table.paintShadows[c.paint].shader = c.shader; |
| } |
| break; |
| } |
| case RenderCmd::paintInvalidateStroke: |
| { |
| auto c = reader.read<ResIdPOD>(); |
| if (auto* pt = paint(c.id)) |
| { |
| pt->invalidateStroke(); |
| } |
| break; |
| } |
| |
| case RenderCmd::save: |
| if (cur) |
| { |
| cur->save(); |
| } |
| break; |
| case RenderCmd::restore: |
| if (cur) |
| { |
| cur->restore(); |
| } |
| break; |
| case RenderCmd::transform: |
| { |
| auto c = reader.read<TransformPOD>(); |
| if (cur) |
| { |
| cur->transform(Mat2D(c.xx, c.xy, c.yx, c.yy, c.tx, c.ty)); |
| } |
| break; |
| } |
| case RenderCmd::drawPath: |
| { |
| auto c = reader.read<DrawPathPOD>(); |
| RenderPath* p = paths.get(c.path, c.pathVersion); |
| RenderPaint* pt = paints.get(c.paint, c.paintVersion); |
| if (cur && p && pt) |
| { |
| cur->drawPath(p, pt); |
| } |
| else if (cur != nullptr && hooks.stats != nullptr) |
| { |
| hooks.stats->droppedDraws = hooks.stats->droppedDraws + 1; |
| replay_detail::logDroppedDraw(type, c.path, c.paint); |
| } |
| break; |
| } |
| case RenderCmd::clipPath: |
| { |
| auto c = reader.read<ClipPathPOD>(); |
| if (cur) |
| { |
| if (auto* p = paths.get(c.path, c.version)) |
| cur->clipPath(p); |
| } |
| break; |
| } |
| case RenderCmd::drawImage: |
| { |
| auto c = reader.read<DrawImagePOD>(); |
| RenderImage* im = |
| (c.image & kCanvasHandleFlag) |
| ? (hooks.canvasImage |
| ? hooks.canvasImage(c.image & kCanvasHandleMask) |
| : nullptr) |
| : image(c.image); |
| if (cur && im) |
| { |
| cur->drawImage(im, |
| sampler(c.wrapX, c.wrapY, c.filter), |
| static_cast<BlendMode>(c.blendMode), |
| c.opacity); |
| } |
| else if (cur != nullptr && hooks.stats != nullptr) |
| { |
| hooks.stats->droppedDraws = hooks.stats->droppedDraws + 1; |
| replay_detail::logDroppedDraw(type, c.image, 0); |
| } |
| break; |
| } |
| case RenderCmd::drawImageMesh: |
| { |
| auto c = reader.read<DrawImageMeshPOD>(); |
| RenderImage* im = |
| (c.image & kCanvasHandleFlag) |
| ? (hooks.canvasImage |
| ? hooks.canvasImage(c.image & kCanvasHandleMask) |
| : nullptr) |
| : image(c.image); |
| rcp<RenderBuffer> vb = |
| buffers.shared(c.vertices, c.vertexVersion); |
| rcp<RenderBuffer> uv = buffers.shared(c.uvCoords, c.uvVersion); |
| rcp<RenderBuffer> ib = |
| buffers.shared(c.indices, c.indexVersion); |
| if (cur && im && vb && uv && ib) |
| { |
| cur->drawImageMesh(im, |
| sampler(c.wrapX, c.wrapY, c.filter), |
| vb, |
| uv, |
| ib, |
| c.vertexCount, |
| c.indexCount, |
| static_cast<BlendMode>(c.blendMode), |
| c.opacity); |
| } |
| else if (cur != nullptr && hooks.stats != nullptr) |
| { |
| hooks.stats->droppedDraws = hooks.stats->droppedDraws + 1; |
| replay_detail::logDroppedDraw(type, c.image, 0); |
| } |
| break; |
| } |
| case RenderCmd::modulateOpacity: |
| { |
| auto c = reader.read<OpacityPOD>(); |
| if (cur) |
| { |
| cur->modulateOpacity(c.opacity); |
| } |
| break; |
| } |
| |
| case RenderCmd::canvasContentBegin: |
| { |
| auto c = reader.read<CanvasContentPOD>(); |
| cur = hooks.beginCanvasContent |
| ? hooks.beginCanvasContent(c.canvasId & |
| kCanvasHandleMask, |
| c.clearColor) |
| : nullptr; |
| break; |
| } |
| case RenderCmd::canvasContentEnd: |
| { |
| reader.read<ResIdPOD>(); // advance past the canvas id |
| cur = renderer; // back to the screen; null drops draws |
| break; |
| } |
| } |
| } |
| if (reader.overrun()) |
| { |
| fprintf(stderr, |
| "rive replay ABORT: payload overrun at byte %zu of %zu\n", |
| reader.position(), |
| commands.size()); |
| assert(false); |
| } |
| } |
| |
| } // namespace rive::cmd |