blob: 8f91e5d4204c5addddaea71348b60bbdc2bf399b [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& meshInstances = table.imageMeshInstances;
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::clipStroke:
case RenderCmd::drawImage:
case RenderCmd::drawImageMesh:
case RenderCmd::drawImageMeshInstanced:
case RenderCmd::applyLayerMask:
case RenderCmd::modulateOpacity:
case RenderCmd::modulateColor:
case RenderCmd::canvasContentBegin:
case RenderCmd::canvasContentEnd:
case RenderCmd::canvasSample:
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;
// A restore past these would pop the renderer's base state, whoever
// recorded it.
std::unordered_map<const Renderer*, uint32_t> callSaves;
auto& openSaves = hooks.openSaves != nullptr ? *hooks.openSaves : callSaves;
auto savesOf = [&](const Renderer* r) {
return r != nullptr ? &openSaves[r] : nullptr;
};
uint32_t* curSaves = savesOf(cur);
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::makeAngularGradient:
{
auto c = reader.read<AngularGradientPOD>();
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->makeAngularGradient(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::makeImageMeshInstances:
{
auto c = reader.read<MakeImageMeshInstancesPOD>();
meshInstances.set(
c.id,
factory->makeImageMeshInstances(c.count),
c.generation);
break;
}
case RenderCmd::imageMeshInstancesData:
{
auto c = reader.read<ImageMeshInstancesDataPOD>();
Span<const uint8_t> src = reader.blobAt(
c.blobOffset,
c.count * static_cast<uint32_t>(sizeof(ImageMeshInstanceData)));
if (auto* mi = meshInstances.get(c.id))
{
if (src.size() == c.count * sizeof(ImageMeshInstanceData))
{
Span<ImageMeshInstanceData> dst = mi->edit(c.count);
std::memcpy(dst.data(), src.data(), src.size_bytes());
mi->endEdit();
}
}
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->strokePosition(
static_cast<StrokePosition>(sh.strokePosition));
fresh->feather(sh.feather);
fresh->additiveness(sh.additiveness);
fresh->blendMode(static_cast<BlendMode>(sh.blendMode));
fresh->shaderTransform(sh.transform);
if (sh.shader != kInvalidRenderHandle)
{
fresh->shader(shaders.shared(sh.shader));
}
if (sh.image != kInvalidRenderHandle)
{
RenderImage* im = (sh.image & kCanvasHandleFlag)
? (hooks.canvasImage
? hooks.canvasImage(
sh.image & kCanvasHandleMask)
: nullptr)
: images.get(sh.image);
if (im != nullptr && !(sh.image & kCanvasHandleFlag) &&
hooks.prepImage)
{
im = hooks.prepImage(im);
table.markPaintImagePending(c.id, im == nullptr);
}
fresh->modulatedImage(
im,
ImageSampler{
static_cast<ImageWrap>(sh.imageWrapX),
static_cast<ImageWrap>(sh.imageWrapY),
static_cast<ImageFilter>(sh.imageFilter)},
Mat2D(
sh.imageXX,
sh.imageXY,
sh.imageYX,
sh.imageYY,
sh.imageTX,
sh.imageTY));
}
}
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;
}
case ResourceKind::imageMeshInstances:
{
meshInstances.newVersion(
c.id,
c.version,
factory->makeImageMeshInstances(0));
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::paintStrokePosition:
{
auto c = reader.read<PaintU8POD>();
if (auto* pt = paint(c.paint))
{
pt->strokePosition(static_cast<StrokePosition>(c.value));
table.paintShadows[c.paint].strokePosition = 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::paintAdditiveness:
{
auto c = reader.read<PaintFloatPOD>();
if (auto* pt = paint(c.paint))
{
pt->additiveness(c.value);
table.paintShadows[c.paint].additiveness = 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::paintShaderTransform:
{
auto c = reader.read<PaintMat2DPOD>();
if (auto* pt = paint(c.paint))
{
Mat2D transform(c.xx, c.xy, c.yx, c.yy, c.tx, c.ty);
pt->shaderTransform(transform);
table.paintShadows[c.paint].transform = transform;
}
break;
}
case RenderCmd::paintModulatedImage:
{
auto c = reader.read<PaintModulatedImagePOD>();
if (auto* pt = paint(c.paint))
{
RenderImage* im = c.image == kInvalidRenderHandle ? nullptr
: (c.image & kCanvasHandleFlag)
? (hooks.canvasImage
? hooks.canvasImage(
c.image & kCanvasHandleMask)
: nullptr)
: image(c.image);
bool pendingImage = false;
if (im != nullptr && !(c.image & kCanvasHandleFlag) &&
hooks.prepImage)
{
im = hooks.prepImage(im);
pendingImage = im == nullptr;
}
table.markPaintImagePending(c.paint, pendingImage);
pt->modulatedImage(
im,
sampler(c.wrapX, c.wrapY, c.filter),
Mat2D(c.xx, c.xy, c.yx, c.yy, c.tx, c.ty));
PaintShadow& sh = table.paintShadows[c.paint];
sh.image = c.image;
sh.imageWrapX = c.wrapX;
sh.imageWrapY = c.wrapY;
sh.imageFilter = c.filter;
sh.imageXX = c.xx;
sh.imageXY = c.xy;
sh.imageYX = c.yx;
sh.imageYY = c.yy;
sh.imageTX = c.tx;
sh.imageTY = c.ty;
}
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();
++*curSaves;
}
break;
case RenderCmd::restore:
if (cur && *curSaves > 0)
{
--*curSaves;
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::clipStroke:
{
auto c = reader.read<ClipStrokePOD>();
if (cur)
{
if (auto* p = paths.get(c.path, c.version))
cur->clipStroke(
p,
{
.thickness = c.thickness,
.join = StrokeJoin(c.join),
.cap = StrokeCap(c.cap),
.position = StrokePosition(c.position),
});
}
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,
c.additiveness);
}
else if (cur != nullptr && hooks.stats != nullptr)
{
hooks.stats->droppedDraws = hooks.stats->droppedDraws + 1;
replay_detail::logDroppedDraw(type, c.image, 0);
}
break;
}
case RenderCmd::applyLayerMask:
{
auto c = reader.read<ApplyLayerMaskPOD>();
// The mask is nearly always a canvas image (layer masking
// rasterizes its coverage into one), so the canvas hook is the
// expected path here rather than the exception.
RenderImage* im = (c.mask & kCanvasHandleFlag)
? (hooks.canvasImage
? hooks.canvasImage(
c.mask & kCanvasHandleMask)
: nullptr)
: image(c.mask);
if (cur && im)
{
cur->applyLayerMask(
im,
sampler(c.wrapX, c.wrapY, c.filter),
static_cast<LayerMaskMode>(c.mode));
}
else if (cur != nullptr && hooks.stats != nullptr)
{
hooks.stats->droppedDraws = hooks.stats->droppedDraws + 1;
replay_detail::logDroppedDraw(type, c.mask, 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,
c.additiveness);
}
else if (cur != nullptr && hooks.stats != nullptr)
{
hooks.stats->droppedDraws = hooks.stats->droppedDraws + 1;
replay_detail::logDroppedDraw(type, c.image, 0);
}
break;
}
case RenderCmd::drawImageMeshInstanced:
{
auto c = reader.read<DrawImageMeshInstancedPOD>();
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);
rcp<ImageMeshInstances> mi = meshInstances.shared(
c.instances,
c.instancesVersion);
if (cur && im && vb && uv && ib && mi)
{
cur->drawImageMeshInstanced(
im,
sampler(c.wrapX, c.wrapY, c.filter),
vb,
uv,
ib,
c.vertexCount,
c.indexCount,
std::move(mi));
}
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::modulateColor:
{
auto c = reader.read<ModulateColorPOD>();
if (cur)
{
cur->modulateColor(c.color, c.replace != 0);
}
break;
}
case RenderCmd::canvasContentBegin:
{
auto c = reader.read<CanvasContentPOD>();
cur = hooks.beginCanvasContent ? hooks.beginCanvasContent(
c.canvasId & kCanvasHandleMask,
c.clearColor)
: nullptr;
curSaves = savesOf(cur);
break;
}
case RenderCmd::canvasContentEnd:
{
reader.read<ResIdPOD>(); // advance past the canvas id
cur = renderer; // back to the screen; null drops draws
curSaves = savesOf(cur);
break;
}
case RenderCmd::canvasSample:
reader.read<ResIdPOD>(); // read by the canvas schedule only
break;
}
}
if (reader.overrun())
{
fprintf(
stderr,
"rive replay ABORT: payload overrun at byte %zu of %zu\n",
reader.position(),
commands.size());
assert(false);
}
}
void retryPendingPaintImages(ResourceTable& table, const ReplayHooks& hooks)
{
auto& pending = table.pendingPaintImages;
if (pending.empty() || !hooks.prepImage)
{
return;
}
for (size_t i = 0; i < pending.size();)
{
RenderHandle id = pending[i];
RenderPaint* pt = table.paints.get(id);
const PaintShadow* sh = id < table.paintShadows.size()
? &table.paintShadows[id]
: nullptr;
// The paint or its image went away, or the image is no longer a
// decoded one: nothing left to ready.
RenderImage* im = pt != nullptr && sh != nullptr &&
sh->image != kInvalidRenderHandle &&
!(sh->image & kCanvasHandleFlag)
? table.images.get(sh->image)
: nullptr;
if (im == nullptr)
{
pending.erase(pending.begin() + i);
continue;
}
RenderImage* ready = hooks.prepImage(im);
if (ready == nullptr)
{
i++;
continue;
}
pt->modulatedImage(
ready,
ImageSampler{
static_cast<ImageWrap>(sh->imageWrapX),
static_cast<ImageWrap>(sh->imageWrapY),
static_cast<ImageFilter>(sh->imageFilter)},
Mat2D(
sh->imageXX,
sh->imageXY,
sh->imageYX,
sh->imageYY,
sh->imageTX,
sh->imageTY));
pending.erase(pending.begin() + i);
}
}
} // namespace rive::cmd