perf(runtime): cheaper advance for rigs with many hidden shapes (#14255) 6dcbf68a58 * cache skinned path and layout participant lookups on shape * notify the shape of a path change once per dirt cycle * skip stroke effect updates while the paint is hidden * scan for skinned paths and layout participants in editor builds, where children change under us * keep measuring hidden effects that feed a clip * keep the shape lookup caches in the never defer flag and spare padding so shape does not grow * exempt only clip sources from the hidden effect skip, nothing else reads a hidden effect path Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head index 02f75b4..6eb0cf1 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -f09c4e58ae2128e922a323b1376d2dc75c2fd451 +6dcbf68a587a0d0a29906a9b41cba01dac2c0f8e
diff --git a/include/rive/shapes/path.hpp b/include/rive/shapes/path.hpp index 4f192b5..1741819 100644 --- a/include/rive/shapes/path.hpp +++ b/include/rive/shapes/path.hpp
@@ -41,6 +41,8 @@ Shape* m_Shape = nullptr; std::vector<PathVertex*> m_Vertices; bool m_deferredPathDirt = false; + bool m_shapeNotified = false; + void shapePathChanged(); PathFlags m_pathFlags = PathFlags::none; RawPath m_rawPath; RenderPathDeformer* deformer() const;
diff --git a/include/rive/shapes/path_composer.hpp b/include/rive/shapes/path_composer.hpp index 9b398fa..e8605ff 100644 --- a/include/rive/shapes/path_composer.hpp +++ b/include/rive/shapes/path_composer.hpp
@@ -32,6 +32,7 @@ ShapePaintPath m_worldPath; ShapePaintPath m_localClockwisePath; bool m_deferredPathDirt; + bool m_shapeNotified = false; }; } // namespace rive #endif
diff --git a/include/rive/shapes/shape.hpp b/include/rive/shapes/shape.hpp index 25da3f1..4438101 100644 --- a/include/rive/shapes/shape.hpp +++ b/include/rive/shapes/shape.hpp
@@ -34,6 +34,7 @@ float m_WorldLength = -1; bool m_WantDifferencePath = false; + bool m_hasLayoutParticipant = false; RenderPathDeformer* m_deformer = nullptr; // Scale-to-fit and the memoized intrinsic bounds live on the @@ -49,6 +50,7 @@ public: Shape(); void buildDependencies() override; + void addChild(Component* component) override; bool collapse(bool value) override; bool canDeferPathUpdate(); void addPath(Path* path);
diff --git a/src/shapes/paint/shape_paint.cpp b/src/shapes/paint/shape_paint.cpp index af8e32c..c85ee8b 100644 --- a/src/shapes/paint/shape_paint.cpp +++ b/src/shapes/paint/shape_paint.cpp
@@ -50,6 +50,13 @@ if (hasDirt(value, ComponentDirt::Path) && shapeEffects->size() > 0) { auto container = ShapePaintContainer::from(parent()); + // Hidden paints are re-invalidated when shown, so measuring now is + // wasted, unless a clip still reads the result. + if (renderOpacity() == 0 && + (container->pathFlags() & PathFlags::clipping) == PathFlags::none) + { + return; + } auto path = pickPath(container); for (auto& effect : *shapeEffects) {
diff --git a/src/shapes/path.cpp b/src/shapes/path.cpp index d249a5c..4359e1f 100644 --- a/src/shapes/path.cpp +++ b/src/shapes/path.cpp
@@ -431,19 +431,26 @@ void Path::markPathDirty(bool sendToLayout) { addDirt(ComponentDirt::Path); - if (m_Shape != nullptr) + shapePathChanged(); +} + +// Dirt accumulates until update, so the shape only needs telling once per +// cycle. +void Path::shapePathChanged() +{ + if (m_shapeNotified || m_Shape == nullptr) { - m_Shape->pathChanged(); + return; } + m_shapeNotified = true; + m_Shape->pathChanged(); } void Path::onDirty(ComponentDirt value) { - if (hasDirt(value, - ComponentDirt::WorldTransform | ComponentDirt::NSlicer) && - m_Shape != nullptr) + if (hasDirt(value, ComponentDirt::WorldTransform | ComponentDirt::NSlicer)) { - m_Shape->pathChanged(); + shapePathChanged(); } if (m_deferredPathDirt) { @@ -453,6 +460,7 @@ void Path::update(ComponentDirt value) { + m_shapeNotified = false; Super::update(value); bool pathChanged = hasDirt(value, ComponentDirt::Path);
diff --git a/src/shapes/path_composer.cpp b/src/shapes/path_composer.cpp index 4d037be..344467b 100644 --- a/src/shapes/path_composer.cpp +++ b/src/shapes/path_composer.cpp
@@ -28,17 +28,19 @@ void PathComposer::onDirty(ComponentDirt dirt) { - if (m_deferredPathDirt) + if (m_deferredPathDirt && !m_shapeNotified) { // We'd deferred the update, let's make sure the rest of our // dependencies update too. Constraints need to update too, stroke // effects, etc. + m_shapeNotified = true; m_shape->pathChanged(); } } void PathComposer::update(ComponentDirt value) { + m_shapeNotified = false; if (hasDirt(value, ComponentDirt::Path | ComponentDirt::NSlicer)) { if (m_shape->canDeferPathUpdate())
diff --git a/src/shapes/shape.cpp b/src/shapes/shape.cpp index dc9b776..d4db451 100644 --- a/src/shapes/shape.cpp +++ b/src/shapes/shape.cpp
@@ -27,6 +27,11 @@ // Make sure the path is not already in the shape. assert(std::find(m_Paths.begin(), m_Paths.end(), path) == m_Paths.end()); m_Paths.push_back(path); + if (path->is<PointsPath>() && path->as<PointsPath>()->skin() != nullptr) + { + // Bones deform a skinned path whether or not the shape is visible. + addFlags(PathFlags::neverDeferUpdate); + } invalidateIntrinsicBounds(); } @@ -38,21 +43,22 @@ bool Shape::canDeferPathUpdate() { - auto canDefer = - renderOpacity() == 0 && - !isFlagged(PathFlags::clipping | PathFlags::neverDeferUpdate); - if (canDefer) + if (renderOpacity() != 0 || + isFlagged(PathFlags::clipping | PathFlags::neverDeferUpdate)) { - // If we have a dependent Skin, don't defer the update - for (auto d : dependents()) + return false; + } +#ifdef WITH_RIVE_EDITOR + // Skins come and go under the editor, after addPath set its flag. + for (auto path : m_Paths) + { + if (path->is<PointsPath>() && path->as<PointsPath>()->skin() != nullptr) { - if (d->is<PointsPath>() && d->as<PointsPath>()->skin() != nullptr) - { - return false; - } + return false; } } - return canDefer; +#endif + return true; } void Shape::update(ComponentDirt value) @@ -627,10 +633,26 @@ } } +void Shape::addChild(Component* component) +{ + Super::addChild(component); + if (component->is<LayoutParticipant>()) + { + m_hasLayoutParticipant = true; + } +} + // The whole shape scales to fit, so place the scaled combined-bounds top-left // at the slot (the origin is irrelevant once we scale). LayoutParticipant* Shape::layoutParticipant() const { +#ifndef WITH_RIVE_EDITOR + // Children are fixed at runtime and few shapes have one to look for. + if (!m_hasLayoutParticipant) + { + return nullptr; + } +#endif for (auto* child : children()) { if (child->is<LayoutParticipant>())
diff --git a/tests/unit_tests/runtime/trim_test.cpp b/tests/unit_tests/runtime/trim_test.cpp index 5dca83b..52e13a2 100644 --- a/tests/unit_tests/runtime/trim_test.cpp +++ b/tests/unit_tests/runtime/trim_test.cpp
@@ -194,3 +194,56 @@ rive::PathVerb::cubic}; testRawPath(artboard, "mixed-shapes-100", verbs10); } + +static std::vector<rive::Vec2D> trimPoints(rive::Artboard* artboard, + const char* shapeName) +{ + auto shape = artboard->find<rive::Shape>(shapeName); + for (auto& child : shape->children()) + { + if (child->is<rive::Stroke>()) + { + auto stroke = child->as<rive::Stroke>(); + auto points = + stroke->effect()->effectPath(stroke)->rawPath()->points(); + return std::vector<rive::Vec2D>(points.begin(), points.end()); + } + } + return {}; +} + +TEST_CASE("Hidden stroke picks up path changes once shown", "[trim]") +{ + auto file = ReadRiveFile("assets/trim_path.riv"); + auto artboard = file->artboard("artboard-2")->instance(); + auto control = file->artboard("artboard-2")->instance(); + auto shape = artboard->find<rive::Shape>("clipped-rect"); + REQUIRE(shape != nullptr); + REQUIRE(shape->paths()[0]->is<rive::Rectangle>()); + auto rect = shape->paths()[0]->as<rive::Rectangle>(); + artboard->advance(0.0f); + auto before = trimPoints(artboard.get(), "clipped-rect"); + REQUIRE(!before.empty()); + + shape->opacity(0.0f); + artboard->advance(0.0f); + rect->width(rect->width() * 2); + artboard->advance(0.0f); + shape->opacity(1.0f); + artboard->advance(0.0f); + + control->find<rive::Shape>("clipped-rect") + ->paths()[0] + ->as<rive::Rectangle>() + ->width(rect->width()); + control->advance(0.0f); + auto expected = trimPoints(control.get(), "clipped-rect"); + auto after = trimPoints(artboard.get(), "clipped-rect"); + REQUIRE(after.size() == expected.size()); + REQUIRE(after != before); + for (size_t i = 0; i < after.size(); i++) + { + CHECK(after[i].x == Approx(expected[i].x)); + CHECK(after[i].y == Approx(expected[i].y)); + } +}