| /* | |
| * Copyright 2026 Rive | |
| */ | |
| // Per-frame cost of real .riv files: the state machine's advanceAndApply (which | |
| // runs the artboard's advance and update passes) plus the artboard's draw. | |
| // | |
| // Run from packages/runtime/tests, e.g. | |
| // | |
| // out/release/bench --duration 10 ArtboardFrame_car_widgets_idle | |
| // | |
| // Assets load from unit_tests/assets unless RIVE_BENCH_ASSETS names another | |
| // directory. Every run() plays kFramesPerRun frames, so the time bench prints | |
| // divided by kFramesPerRun is the cost of one frame. Before each timed run the | |
| // bench restarts from a fresh instance warmed up the same way, so every sample | |
| // plays the same frames of the authored animation. Each bench also prints, | |
| // once from setup(), the work it does per frame over the same scenario: | |
| // render path rebuilds (each drops the renderer's cached triangulation), | |
| // gradients, renderer calls, allocations, and how many frames asked to keep | |
| // animating. Those counts are exact, so they show work removed even where a | |
| // timing is inside the noise. (Allocations aren't counted in Dawn builds; see | |
| // below.) A bench whose .riv can't be read or imported says so and skips, so | |
| // CI can run every bench once on a device that has no assets. | |
| // | |
| // Two renderers: | |
| // - a counting no-op renderer: the runtime's own advance/update/draw cost, | |
| // with the counters above; | |
| // - (_rive) the real RiveRenderer over a null GPU context, so the renderer's | |
| // CPU work -- tessellation, triangulation and its caches -- is included. | |
| #include "bench.hpp" | |
| #include "common/render_context_null.hpp" | |
| #include "assets/paper.riv.hpp" | |
| #include "rive/artboard.hpp" | |
| #include "rive/animation/state_machine_instance.hpp" | |
| #include "rive/constraints/scrolling/scroll_constraint.hpp" | |
| #include "rive/file.hpp" | |
| #include "rive/generated/layout/layout_component_style_base.hpp" | |
| #include "rive/layout_component.hpp" | |
| #include "rive/layout/layout_enums.hpp" | |
| #include "rive/math/math_types.hpp" | |
| #include "rive/nested_artboard.hpp" | |
| #include "rive/node.hpp" | |
| #include "rive/renderer/render_context.hpp" | |
| #include "rive/renderer/rive_renderer.hpp" | |
| #include "rive/scene.hpp" | |
| #include "rive/shapes/paint/shape_paint.hpp" | |
| #include "rive/shapes/shape.hpp" | |
| #include "rive/viewmodel/viewmodel_instance.hpp" | |
| #include "rive/viewmodel/viewmodel_instance_list.hpp" | |
| #include "rive/viewmodel/viewmodel_instance_list_item.hpp" | |
| #include "utils/no_op_renderer.hpp" | |
| #include <algorithm> | |
| #include <atomic> | |
| #include <cmath> | |
| #include <cstdio> | |
| #include <cstdlib> | |
| #include <fstream> | |
| #include <iterator> | |
| #include <memory> | |
| #include <new> | |
| #include <string> | |
| #include <vector> | |
| using namespace rive; | |
| using namespace rive::gpu; | |
| // -- Allocation counting ------------------------------------------------------ | |
| // | |
| // Replaces the global allocation functions for the whole bench binary. | |
| // Counting is off unless a bench's counter pass turns it on, so every other | |
| // bench only pays a relaxed load per allocation. Dawn builds leave this out: | |
| // they link PartitionAlloc's allocator shim, which already replaces them. | |
| #ifndef RIVE_DAWN | |
| #define ARTBOARD_FRAME_BENCH_COUNTS_ALLOCATIONS | |
| #endif | |
| namespace | |
| { | |
| std::atomic<bool> g_countAllocations{false}; | |
| std::atomic<uint64_t> g_allocations{0}; | |
| #ifdef ARTBOARD_FRAME_BENCH_COUNTS_ALLOCATIONS | |
| void* countedAllocation(size_t size) | |
| { | |
| if (g_countAllocations.load(std::memory_order_relaxed)) | |
| { | |
| g_allocations.fetch_add(1, std::memory_order_relaxed); | |
| } | |
| void* ptr = malloc(size != 0 ? size : 1); | |
| if (ptr == nullptr) | |
| { | |
| abort(); | |
| } | |
| return ptr; | |
| } | |
| #endif | |
| } // namespace | |
| #ifdef ARTBOARD_FRAME_BENCH_COUNTS_ALLOCATIONS | |
| // The array forms are replaceable in their own right, so route them here too | |
| // rather than trusting the library's defaults to call the scalar ones. | |
| void* operator new(size_t size) { return countedAllocation(size); } | |
| void* operator new[](size_t size) { return countedAllocation(size); } | |
| void operator delete(void* ptr) noexcept { free(ptr); } | |
| void operator delete[](void* ptr) noexcept { free(ptr); } | |
| void operator delete(void* ptr, size_t) noexcept { free(ptr); } | |
| void operator delete[](void* ptr, size_t) noexcept { free(ptr); } | |
| #endif | |
| namespace | |
| { | |
| // -- Work counters ------------------------------------------------------------ | |
| struct WorkCounters | |
| { | |
| uint64_t renderPathsMade = 0; | |
| uint64_t renderPathRewinds = 0; | |
| uint64_t renderPathRawAdds = 0; | |
| uint64_t gradientsMade = 0; | |
| uint64_t renderPaintsMade = 0; | |
| uint64_t saves = 0; | |
| uint64_t restores = 0; | |
| uint64_t transforms = 0; | |
| uint64_t drawPaths = 0; | |
| uint64_t clipPaths = 0; | |
| uint64_t drawImages = 0; | |
| }; | |
| // The bench process runs one bench on one thread, so a plain global is enough. | |
| WorkCounters g_counters; | |
| class CountingRenderPath : public RenderPath | |
| { | |
| public: | |
| void rewind() override { g_counters.renderPathRewinds++; } | |
| void fillRule(FillRule value) override {} | |
| void addPath(CommandPath* path, const Mat2D& transform) override {} | |
| void addRenderPath(const RenderPath* path, const Mat2D& transform) override {} | |
| void moveTo(float x, float y) override {} | |
| void lineTo(float x, float y) override {} | |
| void cubicTo(float ox, float oy, float ix, float iy, float x, float y) | |
| override | |
| {} | |
| void close() override {} | |
| void addRawPath(const RawPath& path) override | |
| { | |
| g_counters.renderPathRawAdds++; | |
| } | |
| }; | |
| class CountingRenderPaint : public RenderPaint | |
| { | |
| public: | |
| void color(unsigned int value) override {} | |
| void style(RenderPaintStyle value) override {} | |
| void thickness(float value) override {} | |
| void join(StrokeJoin value) override {} | |
| void cap(StrokeCap value) override {} | |
| void blendMode(BlendMode value) override {} | |
| void shader(rcp<RenderShader>) override {} | |
| void shaderTransform(const Mat2D&) override {} | |
| void invalidateStroke() override {} | |
| void feather(float value) override {} | |
| void modulatedImage(const RenderImage*, ImageSampler, const Mat2D&) override | |
| {} | |
| }; | |
| class CountingRenderShader : public RenderShader | |
| {}; | |
| // Mirrors utils/no_op_factory.cpp (not built into this target), counting what | |
| // the runtime asks it to make. | |
| class CountingFactory : public Factory | |
| { | |
| public: | |
| rcp<RenderBuffer> makeRenderBuffer( | |
| RenderBufferType, | |
| RenderBufferFlags, | |
| size_t) override | |
| { | |
| return nullptr; | |
| } | |
| rcp<RenderShader> makeLinearGradient( | |
| float, | |
| float, | |
| float, | |
| float, | |
| const ColorInt[], | |
| const float[], | |
| size_t) override | |
| { | |
| g_counters.gradientsMade++; | |
| return make_rcp<CountingRenderShader>(); | |
| } | |
| rcp<RenderShader> makeRadialGradient( | |
| float, | |
| float, | |
| float, | |
| const ColorInt[], | |
| const float[], | |
| size_t) override | |
| { | |
| g_counters.gradientsMade++; | |
| return make_rcp<CountingRenderShader>(); | |
| } | |
| rcp<RenderShader> makeAngularGradient(const ColorInt[], const float[], size_t) | |
| override | |
| { | |
| g_counters.gradientsMade++; | |
| return make_rcp<CountingRenderShader>(); | |
| } | |
| rcp<RenderPath> makeRenderPath(RawPath&, FillRule) override | |
| { | |
| g_counters.renderPathsMade++; | |
| return make_rcp<CountingRenderPath>(); | |
| } | |
| rcp<RenderPath> makeEmptyRenderPath() override | |
| { | |
| g_counters.renderPathsMade++; | |
| return make_rcp<CountingRenderPath>(); | |
| } | |
| rcp<RenderPaint> makeRenderPaint() override | |
| { | |
| g_counters.renderPaintsMade++; | |
| return make_rcp<CountingRenderPaint>(); | |
| } | |
| rcp<RenderImage> decodeImage(Span<const uint8_t>) override { return nullptr; } | |
| }; | |
| class CountingRenderer : public NoOpRenderer | |
| { | |
| public: | |
| void save() override { g_counters.saves++; } | |
| void restore() override { g_counters.restores++; } | |
| void transform(const Mat2D&) override { g_counters.transforms++; } | |
| void drawPath(RenderPath*, RenderPaint*) override { g_counters.drawPaths++; } | |
| void clipPath(RenderPath*) override { g_counters.clipPaths++; } | |
| void drawImage(const RenderImage*, ImageSampler, BlendMode, float) override | |
| { | |
| g_counters.drawImages++; | |
| } | |
| }; | |
| // -- Scenarios | |
| // ------------------------------------------------------------------ | |
| enum class Scenario | |
| { | |
| // The default state machine as authored. | |
| idle, | |
| // Every top-level node translates together each frame: a rigid move of the | |
| // whole scene, what scrolling or a parent animation does to its contents. | |
| move, | |
| // Only one leaf shape moves: a large, mostly idle artboard. | |
| poke, | |
| // The artboard's opacity animates: a fade over everything. | |
| fade, | |
| // Every scroll constraint sweeps its full range. | |
| scroll, | |
| // One layout's corner radius animates: a style change that only reshapes | |
| // that layout's background. | |
| round, | |
| // One layout keeps tweening: its width flips every half second and | |
| // animates to the new width over half a second. | |
| tween, | |
| // Every view model list grows to kLongListItems rows, each a default | |
| // instance of its first item's view model, then the scene idles: in a | |
| // virtualized list most rows stay unrealized, so this is what each per-row | |
| // pass costs. | |
| longList, | |
| // As longList, then the lists' scroll constraints sweep a tenth of their | |
| // content back and forth, so rows keep being realized and recycled. | |
| longListScroll, | |
| }; | |
| constexpr size_t kLongListItems = 1000; | |
| enum class RendererKind | |
| { | |
| counting, | |
| rive, | |
| }; | |
| constexpr int kWarmupFrames = 120; | |
| constexpr int kFramesPerRun = 120; | |
| constexpr float kFrameSeconds = 1.0f / 60.0f; | |
| // Reads unit_tests/assets/[name], or [name] under RIVE_BENCH_ASSETS. False when | |
| // it isn't there -- CI runs every bench once from places without the assets. | |
| bool readAsset(const char* name, std::vector<uint8_t>* bytes) | |
| { | |
| #ifdef NO_GETENV | |
| // Consoles have no environment. | |
| const char* dir = nullptr; | |
| #else | |
| const char* dir = getenv("RIVE_BENCH_ASSETS"); | |
| #endif | |
| std::string path = std::string(dir != nullptr ? dir : "unit_tests/assets") + | |
| "/" + name; | |
| std::ifstream stream(path, std::ios::binary); | |
| if (!stream) | |
| { | |
| return false; | |
| } | |
| bytes->assign( | |
| std::istreambuf_iterator<char>(stream), | |
| std::istreambuf_iterator<char>()); | |
| return true; | |
| } | |
| class ArtboardFrameBench : public Bench | |
| { | |
| public: | |
| // A null asset name selects the embedded paper.riv. | |
| ArtboardFrameBench( | |
| const char* asset, | |
| Scenario scenario, | |
| RendererKind rendererKind) | |
| : m_asset(asset) | |
| , m_scenario(scenario) | |
| , m_rendererKind(rendererKind) | |
| {} | |
| void setup() override | |
| { | |
| Factory* factory = &m_countingFactory; | |
| if (m_rendererKind == RendererKind::rive) | |
| { | |
| m_nullContext = RenderContextNULL::MakeContext(); | |
| factory = m_nullContext.get(); | |
| } | |
| if (m_asset != nullptr) | |
| { | |
| if (!readAsset(m_asset, &m_bytes)) | |
| { | |
| skip("can't open it (run from packages/runtime/tests, or " | |
| "point RIVE_BENCH_ASSETS at tests/unit_tests/assets)"); | |
| return; | |
| } | |
| m_file = File::import( | |
| Span<const uint8_t>(m_bytes.data(), m_bytes.size()), | |
| factory); | |
| } | |
| else | |
| { | |
| m_file = File::import(assets::paper_riv(), factory); | |
| } | |
| if (m_file == nullptr) | |
| { | |
| skip("it didn't import (an unfetched Git LFS pointer?)"); | |
| return; | |
| } | |
| instantiate(); | |
| if (m_rendererKind == RendererKind::rive) | |
| { | |
| m_renderer = std::make_unique<RiveRenderer>(m_nullContext.get()); | |
| auto width = std::max(1u, static_cast<uint32_t>(m_artboard->width())); | |
| auto height = std::max(1u, static_cast<uint32_t>(m_artboard->height())); | |
| m_renderTarget = m_nullContext->static_impl_cast<RenderContextNULL>() | |
| ->makeRenderTarget(width, height); | |
| } | |
| else | |
| { | |
| m_renderer = std::make_unique<CountingRenderer>(); | |
| } | |
| reportScenarioTargets(); | |
| warmUp(); | |
| printCounters(); | |
| } | |
| // Without this the authored animation keeps running from one sample to | |
| // the next, each sample covers different frames, and bench keeps | |
| // whichever happened to be cheapest. | |
| void beforeRun() override | |
| { | |
| if (m_skipped) | |
| { | |
| return; | |
| } | |
| instantiate(); | |
| warmUp(); | |
| } | |
| int run() const override | |
| { | |
| if (m_skipped) | |
| { | |
| return 0; | |
| } | |
| int keepGoing = 0; | |
| for (int i = 0; i < kFramesPerRun; ++i) | |
| { | |
| keepGoing += frame() ? 1 : 0; | |
| } | |
| return keepGoing; | |
| } | |
| private: | |
| void skip(const char* reason) | |
| { | |
| fprintf( | |
| stderr, | |
| "artboard_frame_bench: skipping %s: %s\n", | |
| m_asset != nullptr ? m_asset : "paper.riv", | |
| reason); | |
| m_skipped = true; | |
| } | |
| // A fresh instance of the default artboard and its scene, with the | |
| // view model and scene chosen the way the player does | |
| // (tests/player/player.cpp). | |
| void instantiate() | |
| { | |
| // The scene refers to the artboard, so it goes first. | |
| m_scene = nullptr; | |
| m_movers.clear(); | |
| m_scrolls.clear(); | |
| m_roundedStyle = nullptr; | |
| m_roundedLayoutDraws = false; | |
| m_tweenStyle = nullptr; | |
| m_tweenLayout = nullptr; | |
| m_tweenLayoutNested = false; | |
| m_longLists = 0; | |
| m_artboard = m_file->artboardDefault(); | |
| m_viewModelInstance = m_file->createDefaultViewModelInstance( | |
| m_artboard.get()); | |
| if (m_viewModelInstance != nullptr) | |
| { | |
| m_artboard->bindViewModelInstance(m_viewModelInstance); | |
| } | |
| std::unique_ptr<StateMachineInstance> | |
| stateMachine = m_artboard->defaultStateMachine(); | |
| if (stateMachine == nullptr && m_artboard->stateMachineCount() > 0) | |
| { | |
| stateMachine = m_artboard->stateMachineAt(0); | |
| } | |
| m_scene = std::move(stateMachine); | |
| if (m_scene == nullptr) | |
| { | |
| m_scene = m_artboard->animationAt(0); | |
| } | |
| if (m_scene != nullptr && m_viewModelInstance != nullptr) | |
| { | |
| m_scene->bindViewModelInstance(m_viewModelInstance); | |
| } | |
| collectScenarioTargets(); | |
| m_frame = 0; | |
| } | |
| void warmUp() | |
| { | |
| for (int i = 0; i < kWarmupFrames; ++i) | |
| { | |
| frame(); | |
| } | |
| } | |
| void collectScenarioTargets() | |
| { | |
| switch (m_scenario) | |
| { | |
| case Scenario::move: | |
| for (auto object : m_artboard->objects()) | |
| { | |
| if (object == nullptr || object == m_artboard.get() || | |
| !object->is<Node>() || | |
| object->as<Node>()->parent() != m_artboard.get()) | |
| { | |
| continue; | |
| } | |
| auto node = object->as<Node>(); | |
| // Files from before 7.3 let the layout own a layout's x/y, | |
| // so moving one of those would measure nothing. | |
| if (node->is<LayoutComponent>() && | |
| !node->as<LayoutComponent>()->composesLayoutOffset()) | |
| { | |
| continue; | |
| } | |
| m_movers.push_back({node, node->x(), node->y()}); | |
| } | |
| break; | |
| case Scenario::poke: | |
| for (auto shape : m_artboard->find<Shape>()) | |
| { | |
| m_movers.push_back({shape, shape->x(), shape->y()}); | |
| break; | |
| } | |
| break; | |
| case Scenario::scroll: | |
| m_scrolls = m_artboard->find<ScrollConstraint>(); | |
| break; | |
| case Scenario::round: | |
| { | |
| // Prefer a layout that draws, so its own background path | |
| // rebuilds too; otherwise take the first one with a style. | |
| // Styles are found through the object list: | |
| // LayoutComponent::style() returns a type this target can't | |
| // include. | |
| std::vector<ContainerComponent*> painted; | |
| for (auto object : m_artboard->objects()) | |
| { | |
| if (object != nullptr && object->is<ShapePaint>()) | |
| { | |
| painted.push_back(object->as<ShapePaint>()->parent()); | |
| } | |
| } | |
| for (auto object : m_artboard->objects()) | |
| { | |
| if (object == nullptr || !object->is<LayoutComponentStyleBase>()) | |
| { | |
| continue; | |
| } | |
| auto style = object->as<LayoutComponentStyleBase>(); | |
| auto layout = style->parent(); | |
| if (layout == m_artboard.get()) | |
| { | |
| continue; | |
| } | |
| bool draws = std::find(painted.begin(), painted.end(), layout) != | |
| painted.end(); | |
| if (m_roundedStyle == nullptr || draws) | |
| { | |
| m_roundedStyle = style; | |
| m_roundedLayoutDraws = draws; | |
| } | |
| if (draws) | |
| { | |
| break; | |
| } | |
| } | |
| if (m_roundedStyle != nullptr) | |
| { | |
| // One radius for all four corners. | |
| m_roundedStyle->linkCornerRadius(true); | |
| } | |
| break; | |
| } | |
| case Scenario::tween: | |
| { | |
| // The last styled layout in file order, likely a leaf (a card | |
| // or a button) rather than the page it sits in. UI files tend | |
| // to build those in nested artboards, so look in those before | |
| // the artboard itself. Styles are found through the object | |
| // list, as for round. | |
| std::vector<Artboard*> artboards; | |
| for (auto nested : m_artboard->find<NestedArtboard>()) | |
| { | |
| if (nested->artboardInstance() != nullptr) | |
| { | |
| artboards.push_back(nested->artboardInstance()); | |
| } | |
| } | |
| artboards.push_back(m_artboard.get()); | |
| for (auto artboard : artboards) | |
| { | |
| for (auto object : artboard->objects()) | |
| { | |
| if (object == nullptr || !object->is<LayoutComponentStyleBase>()) | |
| { | |
| continue; | |
| } | |
| auto layout = object->as<LayoutComponentStyleBase>()->parent(); | |
| if (layout != artboard && layout->is<LayoutComponent>()) | |
| { | |
| m_tweenStyle = object->as<LayoutComponentStyleBase>(); | |
| m_tweenLayout = layout->as<LayoutComponent>(); | |
| m_tweenLayoutNested = artboard != m_artboard.get(); | |
| } | |
| } | |
| if (m_tweenLayout != nullptr) | |
| { | |
| break; | |
| } | |
| } | |
| if (m_tweenStyle != nullptr) | |
| { | |
| // Its own linear half-second tween, and a width fixed in | |
| // points so the flips below resize it. | |
| m_tweenStyle->animationStyleType( | |
| (uint8_t)LayoutAnimationStyle::custom); | |
| m_tweenStyle->interpolationType( | |
| (uint8_t)LayoutStyleInterpolation::linear); | |
| m_tweenStyle->interpolationTime(0.5f); | |
| m_tweenStyle->layoutWidthScaleType((uint8_t)LayoutScaleType::fixed); | |
| m_tweenStyle->widthUnitsValue(1); // YGUnitPoint | |
| } | |
| break; | |
| } | |
| case Scenario::longList: | |
| growLists(); | |
| break; | |
| case Scenario::longListScroll: | |
| growLists(); | |
| m_scrolls = m_artboard->find<ScrollConstraint>(); | |
| break; | |
| case Scenario::idle: | |
| case Scenario::fade: | |
| break; | |
| } | |
| } | |
| void growLists() | |
| { | |
| if (m_viewModelInstance == nullptr) | |
| { | |
| return; | |
| } | |
| for (auto& value : m_viewModelInstance->propertyValues()) | |
| { | |
| if (!value->is<ViewModelInstanceList>()) | |
| { | |
| continue; | |
| } | |
| auto list = value->as<ViewModelInstanceList>(); | |
| const size_t count = list->listItems().size(); | |
| if (count == 0) | |
| { | |
| continue; | |
| } | |
| auto viewModel = list->listItems()[0]->viewModelInstance()->viewModel(); | |
| for (size_t i = count; i < kLongListItems; i++) | |
| { | |
| auto item = make_rcp<ViewModelInstanceListItem>(); | |
| item->viewModelInstance( | |
| m_file->createDefaultViewModelInstance(viewModel)); | |
| list->addItem(item); | |
| } | |
| m_longLists++; | |
| } | |
| } | |
| void reportScenarioTargets() const | |
| { | |
| if (m_scenario == Scenario::move || m_scenario == Scenario::poke) | |
| { | |
| fprintf( | |
| stderr, | |
| "artboard_frame_bench: moving %zu nodes\n", | |
| m_movers.size()); | |
| } | |
| if (m_scenario == Scenario::scroll && m_scrolls.empty()) | |
| { | |
| fprintf(stderr, "artboard_frame_bench: no scroll constraint\n"); | |
| } | |
| if (m_scenario == Scenario::tween) | |
| { | |
| fprintf( | |
| stderr, | |
| "artboard_frame_bench: %s\n", | |
| m_tweenLayout == nullptr ? "no styled layout" | |
| : m_tweenLayoutNested ? "tweening a layout in a nested artboard" | |
| : "tweening a layout in the artboard"); | |
| } | |
| if (m_scenario == Scenario::longList || | |
| m_scenario == Scenario::longListScroll) | |
| { | |
| fprintf( | |
| stderr, | |
| "artboard_frame_bench: %d lists grown to %zu rows\n", | |
| m_longLists, | |
| kLongListItems); | |
| } | |
| if (m_scenario == Scenario::longListScroll && m_scrolls.empty()) | |
| { | |
| fprintf(stderr, "artboard_frame_bench: no scroll constraint\n"); | |
| } | |
| if (m_scenario == Scenario::round) | |
| { | |
| fprintf( | |
| stderr, | |
| "artboard_frame_bench: %s\n", | |
| m_roundedStyle == nullptr ? "no styled layout" | |
| : m_roundedLayoutDraws ? "rounding a layout that draws" | |
| : "rounding a layout that draws nothing"); | |
| } | |
| } | |
| // Returns what advanceAndApply returned: whether the scene wants another | |
| // frame. | |
| bool frame() const | |
| { | |
| // One full period of every scenario per run, so each timed run covers | |
| // the same work. | |
| float phase = 2.0f * math::PI * (float)(m_frame % kFramesPerRun) / | |
| (float)kFramesPerRun; | |
| m_frame++; | |
| switch (m_scenario) | |
| { | |
| case Scenario::move: | |
| case Scenario::poke: | |
| for (const auto& mover : m_movers) | |
| { | |
| mover.node->x(mover.x + 20.0f * std::sin(phase)); | |
| mover.node->y(mover.y + 10.0f * std::cos(phase)); | |
| } | |
| break; | |
| case Scenario::fade: | |
| m_artboard->opacity(0.55f + 0.45f * std::cos(phase)); | |
| break; | |
| case Scenario::scroll: | |
| for (auto scroll : m_scrolls) | |
| { | |
| float t = 0.5f - 0.5f * std::cos(phase); | |
| scroll->scrollOffsetX(scroll->maxOffsetX() * t); | |
| scroll->scrollOffsetY(scroll->maxOffsetY() * t); | |
| } | |
| break; | |
| case Scenario::tween: | |
| // m_frame already counts this frame. Every 30 frames, the | |
| // length of the tween, so one is always running. | |
| if (m_tweenLayout != nullptr && (m_frame - 1) % 30 == 0) | |
| { | |
| m_tweenLayout->width((m_frame - 1) % 60 == 0 ? 60.0f : 100.0f); | |
| } | |
| break; | |
| case Scenario::round: | |
| if (m_roundedStyle != nullptr) | |
| { | |
| m_roundedStyle->cornerRadiusTL(8.0f + 8.0f * std::sin(phase)); | |
| } | |
| break; | |
| case Scenario::longListScroll: | |
| for (auto scroll : m_scrolls) | |
| { | |
| float t = 0.5f - 0.5f * std::cos(phase); | |
| if (scroll->constrainsHorizontal()) | |
| { | |
| scroll->scrollOffsetX(-0.1f * scroll->contentWidth() * t); | |
| } | |
| if (scroll->constrainsVertical()) | |
| { | |
| scroll->scrollOffsetY(-0.1f * scroll->contentHeight() * t); | |
| } | |
| } | |
| break; | |
| case Scenario::idle: | |
| case Scenario::longList: | |
| break; | |
| } | |
| bool keepGoing = false; | |
| if (m_scene != nullptr) | |
| { | |
| keepGoing = m_scene->advanceAndApply(kFrameSeconds); | |
| } | |
| else | |
| { | |
| keepGoing = m_artboard->advance(kFrameSeconds); | |
| } | |
| if (m_rendererKind == RendererKind::rive) | |
| { | |
| m_nullContext->beginFrame({ | |
| .renderTargetWidth = m_renderTarget->width(), | |
| .renderTargetHeight = m_renderTarget->height(), | |
| }); | |
| m_artboard->draw(m_renderer.get()); | |
| m_nullContext->flush({.renderTarget = m_renderTarget.get()}); | |
| } | |
| else | |
| { | |
| m_artboard->draw(m_renderer.get()); | |
| } | |
| return keepGoing; | |
| } | |
| void printCounters() | |
| { | |
| g_counters = WorkCounters(); | |
| g_allocations.store(0); | |
| g_countAllocations.store(true); | |
| int keepGoing = 0; | |
| for (int i = 0; i < kFramesPerRun; ++i) | |
| { | |
| keepGoing += frame() ? 1 : 0; | |
| } | |
| g_countAllocations.store(false); | |
| auto perFrame = [](uint64_t value) { | |
| return (double)value / (double)kFramesPerRun; | |
| }; | |
| #ifdef ARTBOARD_FRAME_BENCH_COUNTS_ALLOCATIONS | |
| printf( | |
| "[%d frames/run] per frame: allocs %.1f", | |
| kFramesPerRun, | |
| perFrame(g_allocations.load())); | |
| #else | |
| printf("[%d frames/run] per frame: allocs n/a", kFramesPerRun); | |
| #endif | |
| if (m_rendererKind == RendererKind::counting) | |
| { | |
| printf( | |
| ", path rebuilds %.1f (rewind %.1f, addRawPath %.1f), " | |
| "paths made %.1f, gradients %.1f, paints made %.1f, " | |
| "save %.1f, restore %.1f, transform %.1f, drawPath %.1f, " | |
| "clipPath %.1f, drawImage %.1f", | |
| perFrame(g_counters.renderPathRewinds), | |
| perFrame(g_counters.renderPathRewinds), | |
| perFrame(g_counters.renderPathRawAdds), | |
| perFrame(g_counters.renderPathsMade), | |
| perFrame(g_counters.gradientsMade), | |
| perFrame(g_counters.renderPaintsMade), | |
| perFrame(g_counters.saves), | |
| perFrame(g_counters.restores), | |
| perFrame(g_counters.transforms), | |
| perFrame(g_counters.drawPaths), | |
| perFrame(g_counters.clipPaths), | |
| perFrame(g_counters.drawImages)); | |
| } | |
| printf(", keep-going frames %d/%d\n", keepGoing, kFramesPerRun); | |
| fflush(stdout); | |
| } | |
| struct Mover | |
| { | |
| Node* node; | |
| float x; | |
| float y; | |
| }; | |
| const char* m_asset; | |
| Scenario m_scenario; | |
| RendererKind m_rendererKind; | |
| CountingFactory m_countingFactory; | |
| std::unique_ptr<RenderContext> m_nullContext; | |
| rcp<RenderTarget> m_renderTarget; | |
| std::vector<uint8_t> m_bytes; | |
| rcp<File> m_file; | |
| std::unique_ptr<ArtboardInstance> m_artboard; | |
| rcp<ViewModelInstance> m_viewModelInstance; | |
| std::unique_ptr<Scene> m_scene; | |
| std::unique_ptr<Renderer> m_renderer; | |
| std::vector<Mover> m_movers; | |
| std::vector<ScrollConstraint*> m_scrolls; | |
| LayoutComponentStyleBase* m_roundedStyle = nullptr; | |
| bool m_roundedLayoutDraws = false; | |
| LayoutComponentStyleBase* m_tweenStyle = nullptr; | |
| LayoutComponent* m_tweenLayout = nullptr; | |
| bool m_tweenLayoutNested = false; | |
| int m_longLists = 0; | |
| mutable uint64_t m_frame = 0; | |
| bool m_skipped = false; | |
| }; | |
| #define ARTBOARD_FRAME_BENCH(NAME, ASSET, SCENARIO, RENDERER) \ | |
| class ArtboardFrame_##NAME : public ArtboardFrameBench \ | |
| { \ | |
| public: \ | |
| ArtboardFrame_##NAME() \ | |
| : ArtboardFrameBench( \ | |
| ASSET, \ | |
| Scenario::SCENARIO, \ | |
| RendererKind::RENDERER) \ | |
| {} \ | |
| }; \ | |
| REGISTER_BENCH(ArtboardFrame_##NAME); | |
| // Layout UI. | |
| ARTBOARD_FRAME_BENCH( | |
| db_health_tracker_idle, | |
| "db_health_tracker.riv", | |
| idle, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| db_health_tracker_idle_rive, | |
| "db_health_tracker.riv", | |
| idle, | |
| rive) | |
| ARTBOARD_FRAME_BENCH( | |
| db_health_tracker_round, | |
| "db_health_tracker.riv", | |
| round, | |
| counting) | |
| ARTBOARD_FRAME_BENCH(data_viz_demo_idle, "data_viz_demo.riv", idle, counting) | |
| ARTBOARD_FRAME_BENCH(data_viz_demo_round, "data_viz_demo.riv", round, counting) | |
| ARTBOARD_FRAME_BENCH( | |
| db_health_tracker_tween, | |
| "db_health_tracker.riv", | |
| tween, | |
| counting) | |
| ARTBOARD_FRAME_BENCH(data_viz_demo_tween, "data_viz_demo.riv", tween, counting) | |
| ARTBOARD_FRAME_BENCH( | |
| layout_paint_idle, | |
| "layout/layout_paint.riv", | |
| idle, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| layout_paint_move, | |
| "layout/layout_paint.riv", | |
| move, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| layout_paint_move_rive, | |
| "layout/layout_paint.riv", | |
| move, | |
| rive) | |
| ARTBOARD_FRAME_BENCH( | |
| layout_animation_nested_idle, | |
| "layout/layout_animation_nested.riv", | |
| idle, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| layout_animation_nested_round, | |
| "layout/layout_animation_nested.riv", | |
| round, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| layout_animation_component_list_idle, | |
| "layout/layout_animation_component_list.riv", | |
| idle, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| layout_scroll_vertical_scroll, | |
| "layout/layout_scroll_vertical.riv", | |
| scroll, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| layout_scroll_vertical_scroll_rive, | |
| "layout/layout_scroll_vertical.riv", | |
| scroll, | |
| rive) | |
| ARTBOARD_FRAME_BENCH( | |
| stack_participant_idle, | |
| "layout/stack_participant.riv", | |
| idle, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| hug_participant_idle, | |
| "layout/hug_participant.riv", | |
| idle, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| scroll_participant_scroll, | |
| "layout/scroll_participant.riv", | |
| scroll, | |
| counting) | |
| // Vector animation. | |
| ARTBOARD_FRAME_BENCH(car_widgets_idle, "car_widgets_v01.riv", idle, counting) | |
| ARTBOARD_FRAME_BENCH(car_widgets_move, "car_widgets_v01.riv", move, counting) | |
| ARTBOARD_FRAME_BENCH(car_widgets_fade, "car_widgets_v01.riv", fade, counting) | |
| ARTBOARD_FRAME_BENCH(car_widgets_idle_rive, "car_widgets_v01.riv", idle, rive) | |
| ARTBOARD_FRAME_BENCH(hunter_x_idle, "hunter_x_demo.riv", idle, counting) | |
| ARTBOARD_FRAME_BENCH(hunter_x_fade, "hunter_x_demo.riv", fade, counting) | |
| ARTBOARD_FRAME_BENCH(zombie_skins_idle, "zombie_skins.riv", idle, counting) | |
| ARTBOARD_FRAME_BENCH(zombie_skins_fade, "zombie_skins.riv", fade, counting) | |
| ARTBOARD_FRAME_BENCH(trim_path_idle, "trim_path.riv", idle, counting) | |
| ARTBOARD_FRAME_BENCH(trim_path_move, "trim_path.riv", move, counting) | |
| // Nested artboards and lists. | |
| ARTBOARD_FRAME_BENCH( | |
| component_list_virtualized_long, | |
| "component_list_virtualized.riv", | |
| longList, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| component_list_virtualized_long_scroll, | |
| "component_list_virtualized.riv", | |
| longListScroll, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| planets_grid_idle, | |
| "layoutstest_8-planets-grid.riv", | |
| idle, | |
| counting) | |
| ARTBOARD_FRAME_BENCH( | |
| planets_grid_tween, | |
| "layoutstest_8-planets-grid.riv", | |
| tween, | |
| counting) | |
| ARTBOARD_FRAME_BENCH(superbowl_idle, "superbowl.riv", idle, counting) | |
| ARTBOARD_FRAME_BENCH(echo_show_idle, "echo_show_demo.riv", idle, counting) | |
| // Large, mostly idle. | |
| ARTBOARD_FRAME_BENCH(paper_poke, nullptr, poke, counting) | |
| ARTBOARD_FRAME_BENCH(paper_poke_rive, nullptr, poke, rive) | |
| ARTBOARD_FRAME_BENCH( | |
| bidirectional_binding_idle, | |
| "bidirectional_binding_source.riv", | |
| idle, | |
| counting) | |
| } // namespace |