| /* | |
| * 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" | |
| #ifdef 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::paintStrokePosition: | |
| 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: | |
| case RenderCmd::paintAdditiveness: | |
| paintSets[r.read<PaintFloatPOD>().paint]++; | |
| break; | |
| case RenderCmd::paintShader: | |
| paintSets[r.read<PaintShaderPOD>().paint]++; | |
| break; | |
| case RenderCmd::paintShaderTransform: | |
| paintSets[r.read<PaintMat2DPOD>().paint]++; | |
| break; | |
| case RenderCmd::paintModulatedImage: | |
| paintSets[r.read<PaintModulatedImagePOD>().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::clipStroke: | |
| r.read<ClipStrokePOD>(); | |
| break; | |
| case RenderCmd::resourceNewVersion: | |
| r.read<ResourceVersionPOD>(); | |
| break; | |
| case RenderCmd::drawImage: | |
| r.read<DrawImagePOD>(); | |
| break; | |
| case RenderCmd::drawImageMesh: | |
| r.read<DrawImageMeshPOD>(); | |
| break; | |
| case RenderCmd::drawImageMeshInstanced: | |
| r.read<DrawImageMeshInstancedPOD>(); | |
| break; | |
| case RenderCmd::applyLayerMask: | |
| // Neither a resource mutation nor a path draw: it scales | |
| // what the canvas already holds, so it lands in no tally. | |
| r.read<ApplyLayerMaskPOD>(); | |
| break; | |
| case RenderCmd::modulateOpacity: | |
| r.read<OpacityPOD>(); | |
| break; | |
| case RenderCmd::modulateColor: | |
| r.read<ModulateColorPOD>(); | |
| break; | |
| case RenderCmd::canvasContentBegin: | |
| r.read<CanvasContentPOD>(); | |
| break; | |
| case RenderCmd::canvasContentEnd: | |
| case RenderCmd::canvasSample: | |
| 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::makeAngularGradient: | |
| r.read<AngularGradientPOD>(); | |
| break; | |
| case RenderCmd::decodeImage: | |
| r.read<DecodeImagePOD>(); | |
| break; | |
| case RenderCmd::makeBuffer: | |
| r.read<MakeBufferPOD>(); | |
| break; | |
| case RenderCmd::bufferData: | |
| r.read<BufferDataPOD>(); | |
| break; | |
| case RenderCmd::makeImageMeshInstances: | |
| r.read<MakeImageMeshInstancesPOD>(); | |
| break; | |
| case RenderCmd::imageMeshInstancesData: | |
| r.read<ImageMeshInstancesDataPOD>(); | |
| 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::paintStrokePosition: | |
| case RenderCmd::paintBlendMode: | |
| r.read<PaintU8POD>(); | |
| break; | |
| case RenderCmd::paintColor: | |
| r.read<PaintColorPOD>(); | |
| break; | |
| case RenderCmd::paintThickness: | |
| case RenderCmd::paintFeather: | |
| case RenderCmd::paintAdditiveness: | |
| r.read<PaintFloatPOD>(); | |
| break; | |
| case RenderCmd::paintShader: | |
| r.read<PaintShaderPOD>(); | |
| break; | |
| case RenderCmd::paintShaderTransform: | |
| r.read<PaintMat2DPOD>(); | |
| break; | |
| case RenderCmd::transform: | |
| r.read<TransformPOD>(); | |
| break; | |
| case RenderCmd::drawImage: | |
| r.read<DrawImagePOD>(); | |
| break; | |
| case RenderCmd::drawImageMesh: | |
| r.read<DrawImageMeshPOD>(); | |
| break; | |
| case RenderCmd::drawImageMeshInstanced: | |
| r.read<DrawImageMeshInstancedPOD>(); | |
| break; | |
| case RenderCmd::modulateOpacity: | |
| r.read<OpacityPOD>(); | |
| break; | |
| case RenderCmd::modulateColor: | |
| r.read<ModulateColorPOD>(); | |
| 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; | |
| case RenderCmd::makeImageMeshInstances: | |
| r.read<MakeImageMeshInstancesPOD>(); | |
| break; | |
| case RenderCmd::imageMeshInstancesData: | |
| r.read<ImageMeshInstancesDataPOD>(); | |
| break; | |
| default: | |
| // Sizes come from RIVE_RENDER_CMD_TABLE, so a command this | |
| // walk does not name explicitly still advances the reader | |
| // past its payload instead of desyncing the stream. | |
| r.skip(payloadSizeOf(static_cast<RenderCmd>(type))); | |
| break; | |
| } | |
| } | |
| 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) | |
| { | |
| auto sink = goldensFrameSink(); | |
| 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, | |
| .triangulationThresholds = DeterministicTriangulationThresholds, | |
| }); | |
| 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(); | |
| 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, | |
| .triangulationThresholds = DeterministicTriangulationThresholds, | |
| }); | |
| 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, | |
| .triangulationThresholds = DeterministicTriangulationThresholds, | |
| }); | |
| 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, | |
| .triangulationThresholds = DeterministicTriangulationThresholds, | |
| }); | |
| 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 // RIVE_CANVAS | |
| #endif // TESTING |