blob: d08c87ef6a7710810119f76ea323bcbc411fda92 [file]
/*
* Copyright 2022 Rive
*/
// Env gated diagnostics and the RIVE_GOLDENS_BENCH benchmark, split out of
// goldens.cpp.
// Don't compile this file as part of the "tests" project.
#ifndef TESTING
#include "goldens_shared.hpp"
#if defined(WITH_RIVE_SCRIPTING) && defined(RIVE_CANVAS)
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <unordered_map>
// Tallies per resource mutations and draws in one recorded 2D frame to spot
// redundant rebuilds.
static void analyze_frame_redundancy(const rive::cmd::RenderCommandBuffer& cmd)
{
using namespace rive::cmd;
RenderCommandReader r(cmd.commandBytes(), cmd.blobBytes());
std::unordered_map<RenderHandle, int> rewinds, addRaw, draws, paintSets;
std::unordered_map<RenderHandle, uint32_t> lastColor;
int colorSets = 0, colorSameValueRepeat = 0;
size_t geomBytes = 0;
uint8_t type;
while (r.next(type))
{
switch (static_cast<RenderCmd>(type))
{
case RenderCmd::pathRewind:
rewinds[r.read<ResIdPOD>().id]++;
break;
case RenderCmd::pathFillRule:
r.read<PathFillRulePOD>();
break;
case RenderCmd::pathAddRawPath:
{
auto c = r.read<PathRawPOD>();
addRaw[c.path]++;
geomBytes += c.verbCount * sizeof(rive::PathVerb) +
c.pointCount * sizeof(rive::Vec2D);
break;
}
case RenderCmd::pathAddRenderPath:
r.read<PathAddPathPOD>();
break;
case RenderCmd::paintStyle:
case RenderCmd::paintJoin:
case RenderCmd::paintCap:
case RenderCmd::paintBlendMode:
paintSets[r.read<PaintU8POD>().paint]++;
break;
case RenderCmd::paintColor:
{
auto c = r.read<PaintColorPOD>();
paintSets[c.paint]++;
colorSets++;
auto it = lastColor.find(c.paint);
if (it != lastColor.end() && it->second == c.color)
colorSameValueRepeat++;
lastColor[c.paint] = c.color;
break;
}
case RenderCmd::paintThickness:
case RenderCmd::paintFeather:
paintSets[r.read<PaintFloatPOD>().paint]++;
break;
case RenderCmd::paintShader:
paintSets[r.read<PaintShaderPOD>().paint]++;
break;
case RenderCmd::paintInvalidateStroke:
r.read<ResIdPOD>();
break;
case RenderCmd::save:
case RenderCmd::restore:
case RenderCmd::makeEmptyPath:
case RenderCmd::makePaint:
break;
case RenderCmd::transform:
r.read<TransformPOD>();
break;
case RenderCmd::drawPath:
draws[r.read<DrawPathPOD>().path]++;
break;
case RenderCmd::clipPath:
r.read<ClipPathPOD>();
break;
case RenderCmd::resourceNewVersion:
r.read<ResourceVersionPOD>();
break;
case RenderCmd::drawImage:
r.read<DrawImagePOD>();
break;
case RenderCmd::drawImageMesh:
r.read<DrawImageMeshPOD>();
break;
case RenderCmd::modulateOpacity:
r.read<OpacityPOD>();
break;
case RenderCmd::canvasContentBegin:
r.read<CanvasContentPOD>();
break;
case RenderCmd::canvasContentEnd:
r.read<ResIdPOD>();
break;
case RenderCmd::makePath:
r.read<MakePathPOD>();
break;
case RenderCmd::makeLinearGradient:
r.read<LinearGradientPOD>();
break;
case RenderCmd::makeRadialGradient:
r.read<RadialGradientPOD>();
break;
case RenderCmd::decodeImage:
r.read<DecodeImagePOD>();
break;
case RenderCmd::makeBuffer:
r.read<MakeBufferPOD>();
break;
case RenderCmd::bufferData:
r.read<BufferDataPOD>();
break;
case RenderCmd::destroyResource:
r.read<DestroyResourcePOD>();
break;
}
}
auto sum = [](const std::unordered_map<RenderHandle, int>& m) {
int t = 0;
for (auto& kv : m)
t += kv.second;
return t;
};
auto multi = [](const std::unordered_map<RenderHandle, int>& m) {
int t = 0, mx = 0;
for (auto& kv : m)
{
if (kv.second > 1)
t++;
mx = std::max(mx, kv.second);
}
return std::pair<int, int>(t, mx);
};
int totalRewind = sum(rewinds), totalAdd = sum(addRaw),
totalDraw = sum(draws), totalPaint = sum(paintSets);
auto rw = multi(rewinds);
auto ad = multi(addRaw);
auto pt = multi(paintSets);
printf("\n-- frame redundancy analysis (one clean frame) --\n");
printf(" paths: %zu distinct rewound, %d total rewinds "
"(%d rewound >1x, max %dx)\n",
rewinds.size(),
totalRewind,
rw.first,
rw.second);
printf(" paths: %zu distinct addRawPath, %d total adds "
"(%d added >1x, max %dx), geom %.1f KB\n",
addRaw.size(),
totalAdd,
ad.first,
ad.second,
geomBytes / 1024.0);
printf(" paints: %zu distinct, %d total property sets "
"(%d set >1x, max %dx)\n",
paintSets.size(),
totalPaint,
pt.first,
pt.second);
printf(" paint color sets: %d total, %d set to the SAME value again "
"(redundant)\n",
colorSets,
colorSameValueRepeat);
printf(" draws: %zu distinct paths drawn, %d total drawPath\n",
draws.size(),
totalDraw);
if (rewinds.size() > 0)
printf(" => rebuild ratio: %.2f rewinds/path, %.2f adds/path "
"(1.0 = each built once; >1 = redundant rebuilds)\n",
double(totalRewind) / rewinds.size(),
addRaw.empty() ? 0.0 : double(totalAdd) / addRaw.size());
}
// Counts drawPath commands that resolve against the resident table versus ones
// skipped, to tell missing resources apart from other replay bugs.
static void diagnose_replay_coverage(const rive::cmd::RenderCommandBuffer& cmd,
const rive::cmd::ResourceTable& t)
{
using namespace rive::cmd;
RenderCommandReader r(cmd.commandBytes(), cmd.blobBytes());
int total = 0, resolved = 0, pNull = 0, pOOR = 0, ptNull = 0, ptOOR = 0;
RenderHandle maxPath = 0, maxPaint = 0;
uint8_t type;
while (r.next(type))
{
switch (static_cast<RenderCmd>(type))
{
case RenderCmd::drawPath:
{
auto c = r.read<DrawPathPOD>();
total++;
maxPath = std::max(maxPath, c.path);
maxPaint = std::max(maxPaint, c.paint);
bool pOk = t.paths.get(c.path) != nullptr;
bool ptOk = t.paints.get(c.paint) != nullptr;
if (c.path >= t.paths.objects.size())
pOOR++;
else if (!pOk)
pNull++;
if (c.paint >= t.paints.objects.size())
ptOOR++;
else if (!ptOk)
ptNull++;
if (pOk && ptOk)
resolved++;
break;
}
case RenderCmd::pathRewind:
case RenderCmd::clipPath:
case RenderCmd::paintInvalidateStroke:
case RenderCmd::canvasContentEnd:
r.read<ResIdPOD>();
break;
case RenderCmd::pathFillRule:
r.read<PathFillRulePOD>();
break;
case RenderCmd::pathAddRawPath:
r.read<PathRawPOD>();
break;
case RenderCmd::pathAddRenderPath:
r.read<PathAddPathPOD>();
break;
case RenderCmd::paintStyle:
case RenderCmd::paintJoin:
case RenderCmd::paintCap:
case RenderCmd::paintBlendMode:
r.read<PaintU8POD>();
break;
case RenderCmd::paintColor:
r.read<PaintColorPOD>();
break;
case RenderCmd::paintThickness:
case RenderCmd::paintFeather:
r.read<PaintFloatPOD>();
break;
case RenderCmd::paintShader:
r.read<PaintShaderPOD>();
break;
case RenderCmd::transform:
r.read<TransformPOD>();
break;
case RenderCmd::drawImage:
r.read<DrawImagePOD>();
break;
case RenderCmd::drawImageMesh:
r.read<DrawImageMeshPOD>();
break;
case RenderCmd::modulateOpacity:
r.read<OpacityPOD>();
break;
case RenderCmd::canvasContentBegin:
r.read<CanvasContentPOD>();
break;
case RenderCmd::makePath:
r.read<MakePathPOD>();
break;
case RenderCmd::makeLinearGradient:
r.read<LinearGradientPOD>();
break;
case RenderCmd::makeRadialGradient:
r.read<RadialGradientPOD>();
break;
case RenderCmd::decodeImage:
r.read<DecodeImagePOD>();
break;
case RenderCmd::makeBuffer:
r.read<MakeBufferPOD>();
break;
case RenderCmd::bufferData:
r.read<BufferDataPOD>();
break;
default:
break; // no payload
}
}
printf(
"\n-- replay coverage diagnosis (clean frame vs resident table) --\n");
printf(" table: %zu paths, %zu paints\n",
t.paths.objects.size(),
t.paints.objects.size());
printf(" drawPath: %d total, %d resolved, skipped path[null %d, OOR %d] "
"paint[null %d, OOR %d]\n",
total,
resolved,
pNull,
pOOR,
ptNull,
ptOOR);
printf(" max referenced: path id %u, paint id %u\n", maxPath, maxPaint);
}
// Drains one recorded frame through the caller owned replayer. Leaves the
// screen frame open for the caller to present via endFrame.
static void replay_deferred_frame(rive::cmd::DeferredReplayer& replayer,
rive::cmd::DeferredSession* session)
{
GoldensFrameSink sink;
replayer.replayFrame(*session, sink);
}
void run_benchmark(const std::vector<uint8_t>& bytes,
const char* artboardName,
const char* stateMachineName,
int iters)
{
using clock = std::chrono::steady_clock;
auto us = [](clock::duration d) {
return std::chrono::duration<double, std::micro>(d).count();
};
auto* win = TestingWindow::Get();
const int cellSize = 256;
const rive::AABB cell(0, 0, cellSize, cellSize);
const float dt = 1.0f / 60.0f;
const int kWarmup = 8;
auto drawInto = [&](rive::Renderer* r, rive::Scene* s, rive::Artboard* a) {
r->save();
r->align(rive::Fit::cover, rive::Alignment::center, cell, s->bounds());
a->drawInternal(r);
r->restore();
};
// Immediate: full main thread frame.
RIVLoader imm(bytes,
artboardName,
stateMachineName,
RIVLoader::DeferMode::Immediate);
auto* immScene = imm.stateMachine();
auto* immArt = imm.artboard();
immScene->advanceAndApply(0.0f);
auto immFrame = [&]() {
immScene->advanceAndApply(dt);
auto r = win->beginFrame({.clearColor = 0xffffffff});
drawInto(r.get(), immScene, immArt);
win->endFrame();
};
for (int i = 0; i < kWarmup; ++i)
immFrame();
auto t0 = clock::now();
for (int i = 0; i < iters; ++i)
immFrame();
double immUs = us(clock::now() - t0) / iters;
// Deferred record: no GPU submission.
RIVLoader def(bytes,
artboardName,
stateMachineName,
RIVLoader::DeferMode::Deferred);
auto* session = def.deferredSession();
auto* defScene = def.stateMachine();
auto* defArt = def.artboard();
defScene->advanceAndApply(0.0f);
auto recFrame = [&]() {
defScene->advanceAndApply(dt);
session->recordOreReplayMarker();
auto r = session->makeScreenRenderer();
drawInto(r.get(), defScene, defArt);
};
// The 2D stream accumulates because recFrame never resets. Resources are
// created on the first frame only, so later deltas are draws only.
auto bytes2D = [&]() -> size_t {
return session->commandBuffer().commandBytes().size() +
session->commandBuffer().blobBytes().size();
};
auto streamBytes = [&]() -> size_t {
return bytes2D() + session->oreContext().streamBytes().total();
};
recFrame(); // first frame includes one time resource creation
auto coldOre = session->oreContext().streamBytes();
size_t cold2D = bytes2D();
size_t coldBytes = cold2D + coldOre.total();
for (int i = 1; i < kWarmup; ++i)
recFrame();
size_t before = streamBytes();
size_t before2D = bytes2D();
auto t1 = clock::now();
for (int i = 0; i < iters; ++i)
recFrame();
double recUs = us(clock::now() - t1) / iters;
double perFrameBytes = double(streamBytes() - before) / iters;
double perFrame2D = double(bytes2D() - before2D) / iters;
// RIVE_GOLDENS_ORE_HISTO prints an Ore opcode histogram for one clean
// frame to diagnose per frame resource churn.
if (goldens_getenv("RIVE_GOLDENS_ORE_HISTO"))
{
session->resetFrame();
recFrame();
using rive::ore::cmd::CommandType;
static const char* kNames[] = {"beginRenderPass", "setPipeline",
"setVertexBuffer", "setIndexBuffer",
"setBindGroup", "setViewport",
"setScissorRect", "setStencilRef",
"setBlendColor", "draw",
"drawIndexed", "finish",
"makeBuffer", "makeTexture",
"makeSampler", "makeShaderModule",
"makeBGLayout", "makeTextureView",
"makePipeline", "makeBindGroup",
"bufferUpdate", "textureUpload",
"destroyResource"};
int counts[64] = {};
auto& cb = session->oreContext().stream();
rive::ore::cmd::OreCommandReader rd(cb.commandBytes(), cb.blobBytes());
CommandType t;
while (rd.next(t))
{
uint8_t v = static_cast<uint8_t>(t);
if (v < 64)
{
counts[v]++;
}
rive::ore::cmd::skipOreCommand(t, rd);
}
printf("\n-- one steady frame, Ore opcode histogram --\n");
for (size_t i = 0; i < sizeof(kNames) / sizeof(kNames[0]); ++i)
{
if (counts[i] != 0)
{
printf(" %-16s : %d\n", kNames[i], counts[i]);
}
}
}
// Deferred replay: one recorded frame replayed repeatedly, cold and
// steady, to isolate the amortizable resource creation cost.
RIVLoader rep(bytes,
artboardName,
stateMachineName,
RIVLoader::DeferMode::Deferred);
auto* repSession = rep.deferredSession();
auto* repScene = rep.stateMachine();
auto* repArt = rep.artboard();
repScene->advanceAndApply(0.0f);
for (int i = 0; i < kWarmup; ++i)
repScene->advanceAndApply(dt);
repSession->recordOreReplayMarker();
{
auto r = repSession->makeScreenRenderer();
drawInto(r.get(), repScene, repArt);
}
const int kReplays = 30;
// Cold: a fresh replayer each frame recreates every resource.
for (int i = 0; i < 3; ++i)
{
rive::cmd::DeferredReplayer cold;
replay_deferred_frame(cold, repSession);
win->endFrame();
}
auto t2 = clock::now();
for (int i = 0; i < kReplays; ++i)
{
rive::cmd::DeferredReplayer cold;
replay_deferred_frame(cold, repSession);
win->endFrame();
}
double coldUs = us(clock::now() - t2) / kReplays;
// Steady: Ore makes are idempotent so Ore resources stay resident. 2D
// makes overwrite rather than skip, so 2D resources are recreated.
rive::cmd::DeferredReplayer steady;
for (int i = 0; i < 3; ++i)
{
replay_deferred_frame(steady, repSession);
win->endFrame();
}
auto t3 = clock::now();
for (int i = 0; i < kReplays; ++i)
{
replay_deferred_frame(steady, repSession);
win->endFrame();
}
double steadyUs = us(clock::now() - t3) / kReplays;
// Phase breakdown for pure 2D scenes. Replay runs on a clean single frame
// against a primed resident table so it reflects one real frame.
bool pure2D = repSession->oreContext().streamBytes().commands == 0;
double mImmAdv = 0, mDefAdv = 0, mImmRen = 0, mDefRec = 0;
double immAdv = 0, immCpu = 0, immGpu = 0;
double repAdv = 0, repRecDraw = 0, repCpu = 0, repGpu = 0;
if (pure2D)
{
for (int i = 0; i < kReplays + 3; ++i)
{
auto a = clock::now();
immScene->advanceAndApply(dt);
rive::Artboard::incFrameId();
auto b = clock::now();
auto r = win->beginFrame({.clearColor = 0xffffffff});
drawInto(r.get(), immScene, immArt);
auto c = clock::now();
win->endFrame();
auto d = clock::now();
if (i >= 3)
{
immAdv += us(b - a);
immCpu += us(c - b);
immGpu += us(d - c);
}
}
immAdv /= kReplays;
immCpu /= kReplays;
immGpu /= kReplays;
// Prime the resident table with one full replay, then measure clean
// single frames against it.
rive::cmd::ResourceTable t2;
rive::cmd::replayRenderCommands(win->factory(),
nullptr,
repSession->commandBuffer(),
t2);
for (int i = 0; i < kReplays + 3; ++i)
{
repSession->resetFrame();
auto a = clock::now();
repScene->advanceAndApply(dt);
rive::Artboard::incFrameId();
auto a2 = clock::now();
{
auto rr = repSession->makeScreenRenderer();
drawInto(rr.get(), repScene, repArt);
}
auto b = clock::now();
// Consumer replay against the resident table.
auto screen = win->beginFrame({.clearColor = 0xffffffff});
rive::cmd::replayRenderCommands(win->factory(),
screen.get(),
repSession->commandBuffer(),
t2);
auto c = clock::now();
bool last = (i == kReplays + 2);
std::vector<uint8_t> px;
win->endFrame(last && goldens_getenv("RIVE_GOLDENS_BENCH_DUMP")
? &px
: nullptr);
auto d = clock::now();
if (last && goldens_getenv("RIVE_GOLDENS_BENCH_DUMP"))
dumpPixelsAsPng("bench_consumer",
win->width(),
win->height(),
std::move(px));
if (i >= 3)
{
repAdv += us(a2 - a);
repRecDraw += us(b - a2);
repCpu += us(c - b);
repGpu += us(d - c);
}
}
analyze_frame_redundancy(repSession->commandBuffer());
diagnose_replay_coverage(repSession->commandBuffer(), t2);
repAdv /= kReplays;
repRecDraw /= kReplays;
repCpu /= kReplays;
repGpu /= kReplays;
// Two fresh artboards advanced in lockstep so advance is compared at
// the same animation state, isolating the serializer overhead.
RIVLoader immM(bytes,
artboardName,
stateMachineName,
RIVLoader::DeferMode::Immediate);
RIVLoader defM(bytes,
artboardName,
stateMachineName,
RIVLoader::DeferMode::Deferred);
auto* immMs = immM.stateMachine();
auto* immMa = immM.artboard();
auto* defMs = defM.stateMachine();
auto* defMa = defM.artboard();
auto* defMsess = defM.deferredSession();
immMs->advanceAndApply(0.0f);
defMs->advanceAndApply(0.0f);
for (int i = 0; i < kReplays + 5; ++i)
{
defMsess->resetFrame();
auto t0 = clock::now();
immMs->advanceAndApply(dt);
auto t1 = clock::now();
defMs->advanceAndApply(dt); // same frame, plus serialize
auto t2 = clock::now();
rive::Artboard::incFrameId();
auto rim = win->beginFrame({.clearColor = 0xffffffff});
auto t3 = clock::now();
drawInto(rim.get(), immMs, immMa);
auto t4 = clock::now();
win->endFrame();
auto t5 = clock::now();
{
auto rr = defMsess->makeScreenRenderer();
drawInto(rr.get(), defMs, defMa); // records instead of drawing
}
auto t6 = clock::now();
if (i >= 5)
{
mImmAdv += us(t1 - t0);
mDefAdv += us(t2 - t1);
mImmRen += us(t4 - t3);
mDefRec += us(t6 - t5);
}
}
mImmAdv /= kReplays;
mDefAdv /= kReplays;
mImmRen /= kReplays;
mDefRec /= kReplays;
}
printf("\n=== deferred-rendering benchmark (%d iters @ 60fps) ===\n",
iters);
printf("scene resolution: %dx%d, 1 cell\n", cellSize, cellSize);
printf("\n-- per-frame timing (microseconds) --\n");
printf(" immediate (main thread, record + GPU submit) : %9.1f us\n",
immUs);
printf(" deferred RECORD only (main thread) : %9.1f us "
"(%.2fx immediate)\n",
recUs,
recUs / immUs);
printf(" deferred REPLAY cold (recreate every frame): %9.1f us "
"(%.2fx immediate)\n",
coldUs,
coldUs / immUs);
printf(" deferred REPLAY steady (Ore resident) : %9.1f us "
"(%.2fx immediate) [MEASURED]\n",
steadyUs,
steadyUs / immUs);
printf(" Ore shader/pipeline recompile saved/frame : %9.1f us\n",
coldUs - steadyUs);
printf("\n-- serialized stream size --\n");
printf(" 2D ordered stream, first frame (creates+draws): %9zu B "
"(%.1f KB)\n",
cold2D,
cold2D / 1024.0);
printf(" Ore ordered stream, first frame (creates+passes): %9zu B "
"(%.1f KB)\n",
coldOre.total(),
coldOre.total() / 1024.0);
printf(" steady per-frame (crosses every frame) : %9.0f B "
"(%.2f KB)\n",
perFrameBytes,
perFrameBytes / 1024.0);
printf(" 2D draws (steady, creates amortized) : %9.0f B\n",
perFrame2D);
if (pure2D)
{
printf("\n-- phase breakdown (pure-2D, single clean frame, us) --\n");
printf(" IMMEDIATE (all on the main thread):\n");
printf(" advance (anim / IK / skin / databind) : %8.1f us\n",
immAdv);
printf(" render CPU (issue draw calls) : %8.1f us\n",
immCpu);
printf(" flush + present (feed the GPU) : %8.1f us\n",
immGpu);
printf(" total : %8.1f us\n",
immAdv + immCpu + immGpu);
printf(" DEFERRED:\n");
printf(" PRODUCER (main): advance (+serialize) : %8.1f us "
"(immediate advance was %.1f)\n",
repAdv,
immAdv);
printf(" PRODUCER (main): record draw commands : %8.1f us "
"(immediate draw-CPU was %.1f)\n",
repRecDraw,
immCpu);
printf(" PRODUCER total (main thread) : %8.1f us\n",
repAdv + repRecDraw);
printf(" CONSUMER (render): replay CPU : %8.1f us "
"(parse + apply + draw calls)\n",
repCpu);
printf(" CONSUMER (render): flush + present : %8.1f us\n",
repGpu);
printf(" consumer total (render thread) : %8.1f us\n",
repCpu + repGpu);
printf(" deltas:\n");
printf(" GPU feed: replay vs immediate : %+8.1f us "
"(should be ~0 — identical work)\n",
repGpu - immGpu);
printf(" parse/apply tax: replayCPU - immCPU : %+8.1f us\n",
repCpu - immCpu);
printf(" advance moved off render thread : %8.1f us\n",
immAdv);
printf(
"\n-- matched-frame serializer cost (same animation state) --\n");
printf(" advance: immediate %.1f vs deferred %.1f "
"=> serializer-in-advance %+.1f us\n",
mImmAdv,
mDefAdv,
mDefAdv - mImmAdv);
printf(" draws : immediate issue %.1f vs deferred record %.1f "
"=> %+.1f us\n",
mImmRen,
mDefRec,
mDefRec - mImmRen);
printf(" total serializer overhead vs immediate: %+.1f us/frame\n",
(mDefAdv - mImmAdv) + (mDefRec - mImmRen));
}
printf("=======================================================\n\n");
}
#endif // WITH_RIVE_SCRIPTING && RIVE_CANVAS
#endif // TESTING