fix(runtime): measure a skinned path's pose winding exactly (#14480) 0b5fd6293b fix(runtime): measure a skinned path's pose winding exactly, like the editor feat(tests): choose the present mode, and report Artemis refresh (#14380) 1570bcffb4 * feat(tests): a --present mode option, and the Artemis window reports its present modes and measured refresh * query the present modes safely, log the display refresh on its own, and say only Artemis reads the present mode Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head index b699c18..53b26e1 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -961a2accfaed7de1c9b486518baf81c36748462d +0b5fd6293b7655e31839cbdb4cf6319fe898bdd8
diff --git a/include/rive/shapes/points_path.hpp b/include/rive/shapes/points_path.hpp index 33b0ac2..278c261 100644 --- a/include/rive/shapes/points_path.hpp +++ b/include/rive/shapes/points_path.hpp
@@ -21,8 +21,9 @@ private: // Winding with the bones' mirroring divided out, 0 when unknown. int m_windingReference = 0; - // Winding of the path as bound, 0 when it has no area there. - int bindWinding(); + // 1 when the vertices wind clockwise, -1 the other way, 0 when they enclose + // too little area to tell. Reads where the bones put them when [deformed]. + int measureWinding(bool deformed); }; } // namespace rive
diff --git a/src/shapes/points_path.cpp b/src/shapes/points_path.cpp index 31ae0fa..795955f 100644 --- a/src/shapes/points_path.cpp +++ b/src/shapes/points_path.cpp
@@ -57,41 +57,20 @@ void PointsPath::markSkinDirty() { Super::markPathDirty(); } -namespace -{ -// 1 when [path] winds clockwise, -1 when it winds the other way, 0 when it has -// too little area to tell. -int measureWinding(const RawPath& path) -{ - AABB bounds = path.bounds(); - float area = path.computeCoarseArea(bounds.center()); - float extent = std::max(bounds.width(), bounds.height()); - // The area sums products of the path's extent, so that is the scale of - // its rounding. A collapsed path has no winding worth caching. - if (std::abs(area) <= 1e-5f * extent * extent) - { - return 0; - } - return area < 0 ? -1 : 1; -} -} // namespace - -// With every bone at its bind transform the tendons' inverse binds cancel, -// leaving each point at the bind transform applied to it. That map is affine, -// so it scales the local area by its determinant. -int PointsPath::bindWinding() +int PointsPath::measureWinding(bool deformed) { const auto& points = m_Vertices; size_t count = points.size(); - const Mat2D& bind = skin()->bindTransform(); - int orientation = - Skin::orientation(bind.xx(), bind.xy(), bind.yx(), bind.yy()); - if (count < 2 || orientation == 0) + if (count < 2) { return 0; } + auto at = [deformed](PathVertex* vertex) { + return deformed ? vertex->renderTranslation() + : Vec2D(vertex->x(), vertex->y()); + }; // Measured from the first point, so an open path closes on it for free. - Vec2D origin(points[0]->x(), points[0]->y()); + Vec2D origin = at(points[0]); Vec2D p0(0, 0); float area = 0, minX = 0, minY = 0, maxX = 0, maxY = 0; size_t segments = isPathClosed() ? count : count - 1; @@ -99,13 +78,18 @@ { auto from = points[i]; auto to = points[(i + 1) % count]; - Vec2D p3 = Vec2D(to->x(), to->y()) - origin; - Vec2D p1 = from->is<CubicVertex>() - ? from->as<CubicVertex>()->outPoint() - origin - : p0; - Vec2D p2 = to->is<CubicVertex>() - ? to->as<CubicVertex>()->inPoint() - origin - : p3; + Vec2D p3 = at(to) - origin; + Vec2D p1 = p0, p2 = p3; + if (from->is<CubicVertex>()) + { + auto cubic = from->as<CubicVertex>(); + p1 = (deformed ? cubic->renderOut() : cubic->outPoint()) - origin; + } + if (to->is<CubicVertex>()) + { + auto cubic = to->as<CubicVertex>(); + p2 = (deformed ? cubic->renderIn() : cubic->inPoint()) - origin; + } // Exact area of a cubic, a line being one with its handles on its ends. area += 6 * Vec2D::cross(p0, p1) + 3 * Vec2D::cross(p0, p2) + Vec2D::cross(p0, p3) + 3 * Vec2D::cross(p1, p2) + @@ -121,12 +105,13 @@ } area /= 20; float extent = std::max(maxX - minX, maxY - minY); - // Same tolerance as measureWinding. + // The area sums products of the path's extent, so that is the scale of + // its rounding. A collapsed path has no winding worth caching. if (std::abs(area) <= 1e-5f * extent * extent) { return 0; } - return (area < 0 ? -1 : 1) * orientation; + return area < 0 ? -1 : 1; } // Taken from the path as bound, then follows the bones' mirroring. Bones that @@ -141,7 +126,13 @@ } if (m_windingReference == 0) { - m_windingReference = bindWinding(); + // With every bone at its bind transform the inverse binds cancel, + // leaving the local points under an affine map that only flips their + // winding when it mirrors. + const Mat2D& bind = skin()->bindTransform(); + m_windingReference = + measureWinding(false) * + Skin::orientation(bind.xx(), bind.xy(), bind.yx(), bind.yy()); } int sign = skin()->windingSign(); if (sign != 0 && m_windingReference != 0) @@ -150,7 +141,7 @@ } // Mixed or collapsed bones leave no mirroring to follow, and a path bound // with no area has no reference: measure the pose itself. - int measured = measureWinding(rawPath()); + int measured = measureWinding(true); if (measured == 0) { return authored;
diff --git a/tests/common/testing_window.hpp b/tests/common/testing_window.hpp index fd0387b..3810eff 100644 --- a/tests/common/testing_window.hpp +++ b/tests/common/testing_window.hpp
@@ -138,6 +138,19 @@ ANGLERenderer::vk; #endif std::string gpuNameFilter; + + // How a window presents: fifo waits for vblank, relaxed tears when + // late, mailbox and immediate run uncapped. Only the Artemis window + // reads this today, and it falls back to fifo when the surface lacks + // the mode. + enum class PresentMode + { + fifo, + fifoRelaxed, + mailbox, + immediate, + }; + PresentMode presentMode = PresentMode::fifo; }; enum class Visibility
diff --git a/tests/player/player.cpp b/tests/player/player.cpp index 65f5243..bab6b9a 100644 --- a/tests/player/player.cpp +++ b/tests/player/player.cpp
@@ -7,6 +7,7 @@ #include "player.hpp" +#include <optional> #include <sstream> #include "common/test_harness.hpp" #include "common/testing_window.hpp" @@ -279,6 +280,8 @@ FrameRunner::LaunchOptions& options) { bool onlyUbershaders = false; + // Applied after parsing, since --backend resets the backend params. + std::optional<TestingWindow::BackendParams::PresentMode> presentMode; for (int i = 1; i < argc; ++i) { @@ -331,6 +334,35 @@ : strcmp(fit, "fill") == 0 ? rive::Fit::fill : rive::Fit::contain; } + else if (strcmp(argv[i], "--present") == 0 && i + 1 < argc) + { + using PM = TestingWindow::BackendParams::PresentMode; + const char* mode = argv[++i]; + if (strcmp(mode, "fifo") == 0) + { + presentMode = PM::fifo; + } + else if (strcmp(mode, "relaxed") == 0) + { + presentMode = PM::fifoRelaxed; + } + else if (strcmp(mode, "mailbox") == 0) + { + presentMode = PM::mailbox; + } + else if (strcmp(mode, "immediate") == 0) + { + presentMode = PM::immediate; + } + else + { + fprintf(stderr, + "--present must be fifo, relaxed, mailbox or " + "immediate, not %s\n", + mode); + abort(); + } + } else if (strcmp(argv[i], "--threaded") == 0) { m_threaded = true; @@ -370,6 +402,10 @@ } } + if (presentMode) + { + options.backendParams.presentMode = *presentMode; + } if (onlyUbershaders) { options.backendParams.shaderCompilationMode =
diff --git a/tests/unit_tests/runtime/skinned_winding_test.cpp b/tests/unit_tests/runtime/skinned_winding_test.cpp index 9b6aa02..ba7baca 100644 --- a/tests/unit_tests/runtime/skinned_winding_test.cpp +++ b/tests/unit_tests/runtime/skinned_winding_test.cpp
@@ -355,6 +355,21 @@ } } +// Folding the lens and mirroring only its top bone leaves the pose to be +// measured, at a size where a pixel tolerance would only see the chords. +TEST_CASE("a small curved path measures its pose from its handles", + "[skinwinding]") +{ + for (bool clockwise : {true, false}) + { + QuadRig rig(clockwise, 8.0f, true, 4.0f); + rig.scale(rig.top, 1.0f, -1.0f); + rig.artboard.advance(0.0f); + CHECK(rig.skin->windingSign() == 0); + CHECK(rig.path->winding() == (clockwise ? -1 : 1)); + } +} + TEST_CASE("a real rig stays clockwise when its bones mirror", "[skinwinding]") { auto file = ReadRiveFile("assets/zombie_skins.riv");