blob: 53bc2d6a19f77031b5c5fc6a83f4014baec3e090 [file]
/*
* 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