Files
MapLibreParticleLab/test/renderer/retained_underlay_cache.test.cpp
tei 4e3d27e1f3
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
feat: add particle static CPU fast path
2026-09-23 08:22:24 +09:00

464 lines
18 KiB
C++

#include <mbgl/renderer/retained_underlay_cache.hpp>
#include <mbgl/renderer/particle_fast_path.hpp>
#include <gtest/gtest.h>
#include <utility>
#include <vector>
using namespace mbgl;
namespace {
RetainedUnderlayKey key(Size size = {640, 480}, int32_t boundary = 4) {
RetainedUnderlayKey result{size, boundary, {}};
result.cameraMatrix[0] = 1.0;
result.cameraMatrix[5] = 1.0;
result.cameraMatrix[10] = 1.0;
result.cameraMatrix[15] = 1.0;
return result;
}
RetainedUnderlayRevisionState revisionState(Size size = {640, 480}, int32_t boundary = 4) {
RetainedUnderlayRevisionState result;
result.viewport = size;
result.boundary = boundary;
result.pixelRatio = 2.0f;
result.contentRevision = 1;
result.topologyRevision = 1;
result.styleLoaded = true;
result.renderTreeLoaded = true;
result.staticStable = true;
result.placementStable = true;
result.cameraMatrix = key(size, boundary).cameraMatrix;
return result;
}
ParticleFastPathGateInput fastPathInput() {
return {
.hasPreparedTree = true,
.retainedEnabled = true,
.cacheValid = true,
.revisionMatches = true,
.contentRevisionMatches = true,
.cameraMatches = true,
.viewportMatches = true,
.updateParametersMatch = true,
.styleLoaded = true,
.boundaryMatches = true,
.topologyMatches = true,
.staticStable = true,
.hasPlacementTransition = false,
.hasPendingChanges = false,
.hasNonParticleTransition = false,
.hasNonParticleRepaint = false,
.hasFadingTiles = false,
.particleNeedsRepaint = true,
.stillImageRequest = false,
.continuousMode = true,
.debugDisabled = true,
.supportedScene = true,
.annotationMutation = false,
};
}
} // namespace
TEST(RetainedDynamicBoundary, SelectsFirstCandidate) {
RetainedDynamicBoundaryTracker none;
none.consider(0, false);
none.consider(3, false);
EXPECT_FALSE(none.value());
RetainedDynamicBoundaryTracker particleOnly;
particleOnly.consider(5, true);
EXPECT_EQ(particleOnly.value(), 5);
RetainedDynamicBoundaryTracker weatherAndParticle;
weatherAndParticle.consider(7, true);
weatherAndParticle.consider(2, true);
EXPECT_EQ(weatherAndParticle.value(), 2);
RetainedDynamicBoundaryTracker particleAndWeather;
particleAndWeather.consider(2, true);
particleAndWeather.consider(7, true);
EXPECT_EQ(particleAndWeather.value(), 2);
}
TEST(RetainedLayerGroupRange, PreservesForwardReverseOriginalOrdinals) {
const std::vector<int32_t> indices{0, 1, 3, 5, 8};
std::vector<size_t> beforeForward;
std::vector<size_t> fromForward;
std::vector<size_t> beforeReverse;
std::vector<size_t> fromReverse;
for (size_t ordinal = 0; ordinal < indices.size(); ++ordinal) {
if (retainedLayerGroupInRange(indices[ordinal], 5, RetainedLayerGroupRange::Before))
beforeForward.push_back(ordinal);
else
fromForward.push_back(ordinal);
}
for (size_t ordinal = indices.size(); ordinal-- > 0;) {
if (retainedLayerGroupInRange(indices[ordinal], 5, RetainedLayerGroupRange::Before))
beforeReverse.push_back(ordinal);
else
fromReverse.push_back(ordinal);
}
EXPECT_EQ(beforeForward, (std::vector<size_t>{0, 1, 2}));
EXPECT_EQ(fromForward, (std::vector<size_t>{3, 4}));
EXPECT_EQ(beforeReverse, (std::vector<size_t>{2, 1, 0}));
EXPECT_EQ(fromReverse, (std::vector<size_t>{4, 3}));
}
TEST(RetainedUnderlayCache, BuildReuseAndInvalidation) {
RetainedUnderlayCache cache;
EXPECT_EQ(cache.evaluate(std::nullopt, 1, false), RetainedUnderlayDecision::Disabled);
EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Reuse);
EXPECT_EQ(cache.getBuildRevision(), 1u);
EXPECT_EQ(cache.evaluate(key({800, 600}), 2, true, nullptr, "viewport"), RetainedUnderlayDecision::Build);
EXPECT_EQ(cache.getLastInvalidationReason(), "viewport");
cache.markBuildSucceeded();
EXPECT_EQ(cache.evaluate(key({800, 600}, 6), 3, true, nullptr, "camera-or-boundary"),
RetainedUnderlayDecision::Build);
EXPECT_EQ(cache.getLastInvalidationReason(), "camera-or-boundary");
}
TEST(RetainedUnderlayRevision, MonotonicAcrossCameraViewportStyleAndSourceChanges) {
RetainedUnderlayRevisionTracker tracker;
RetainedUnderlayCache cache;
auto state = revisionState();
const auto currentKey = [&] {
return RetainedUnderlayKey{state.viewport, *state.boundary, state.cameraMatrix};
};
EXPECT_EQ(tracker.observe(state), 1u);
EXPECT_EQ(tracker.observe(state), 1u);
EXPECT_EQ(cache.evaluate(currentKey(), tracker.current(), true), RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
state.cameraMatrix[12] = 4.0;
EXPECT_EQ(tracker.observe(state), 2u);
EXPECT_EQ(tracker.reason(), "camera-or-boundary");
EXPECT_EQ(cache.evaluate(currentKey(), tracker.current(), true, nullptr, tracker.reason().c_str()),
RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
state.viewport = {800, 600};
EXPECT_EQ(tracker.observe(state), 3u);
EXPECT_EQ(tracker.reason(), "viewport");
EXPECT_EQ(cache.evaluate(currentKey(), tracker.current(), true, nullptr, tracker.reason().c_str()),
RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
state.pixelRatio = 3.0f;
EXPECT_EQ(tracker.observe(state), 4u);
EXPECT_EQ(tracker.reason(), "pixel-ratio");
EXPECT_EQ(cache.evaluate(currentKey(), tracker.current(), true, nullptr, tracker.reason().c_str()),
RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
++state.contentRevision;
EXPECT_EQ(tracker.observe(state, "style-mutation"), 5u);
EXPECT_EQ(tracker.reason(), "style-mutation");
EXPECT_EQ(cache.evaluate(currentKey(), tracker.current(), true, nullptr, tracker.reason().c_str()),
RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
++state.contentRevision;
EXPECT_EQ(tracker.observe(state, "layer-mutation"), 6u);
EXPECT_EQ(tracker.reason(), "layer-mutation");
EXPECT_EQ(cache.evaluate(currentKey(), tracker.current(), true, nullptr, tracker.reason().c_str()),
RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
++state.contentRevision;
EXPECT_EQ(tracker.observe(state, "source-mutation"), 7u);
EXPECT_EQ(tracker.reason(), "source-mutation");
EXPECT_EQ(cache.evaluate(currentKey(), tracker.current(), true, nullptr, tracker.reason().c_str()),
RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
++state.contentRevision;
EXPECT_EQ(tracker.observe(state, "tile-mutation"), 8u);
EXPECT_EQ(tracker.reason(), "tile-mutation");
EXPECT_EQ(cache.evaluate(currentKey(), tracker.current(), true, nullptr, tracker.reason().c_str()),
RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
++state.topologyRevision;
EXPECT_EQ(tracker.observe(state), 9u);
EXPECT_EQ(tracker.reason(), "layer-group-mutation");
EXPECT_EQ(cache.evaluate(currentKey(), tracker.current(), true, nullptr, tracker.reason().c_str()),
RetainedUnderlayDecision::Build);
}
TEST(RetainedUnderlayRevision, RepeatedFramesWithoutSemanticMutationDoNotAdvanceRevision) {
RetainedUnderlayRevisionTracker tracker;
auto state = revisionState();
const auto settledRevision = tracker.observe(state);
// Transient drawable add/remove/rebuild and equivalent stencil assignment are
// deliberately not inputs to the semantic revision state. Stable frames must
// therefore keep the revision used by the retained build.
for (int frame = 0; frame < 240; ++frame) {
EXPECT_EQ(tracker.observe(state), settledRevision);
}
++state.contentRevision;
EXPECT_GT(tracker.observe(state, "tile-mutation"), settledRevision);
EXPECT_EQ(tracker.reason(), "tile-mutation");
}
TEST(RetainedUnderlayRevision, EquivalentIntraFrameRemoveAddDoesNotAdvanceTopology) {
RetainedUnderlayTopologyTracker tracker;
const RetainedUnderlayTopologySnapshot initial{
.boundary = 32,
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
};
const auto settledRevision = tracker.observe(initial);
// The request processor may temporarily remove a group and add an equivalent
// replacement. Only the final post-processChanges snapshot is observed.
auto finalTopology = initial;
finalTopology.staticPrefix.erase(finalTopology.staticPrefix.begin());
finalTopology.staticPrefix.insert(finalTopology.staticPrefix.begin(), {3, "sea-area-fill", 1});
EXPECT_EQ(finalTopology, initial);
EXPECT_EQ(tracker.observe(finalTopology), settledRevision);
}
TEST(RetainedUnderlayRevision, PersistentTopologyChangeAdvancesOnce) {
RetainedUnderlayTopologyTracker tracker;
const RetainedUnderlayTopologySnapshot initial{
.boundary = 32,
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
};
const auto initialRevision = tracker.observe(initial);
auto changed = initial;
changed.staticPrefix.pop_back();
const auto changedRevision = tracker.observe(changed);
EXPECT_GT(changedRevision, initialRevision);
EXPECT_EQ(tracker.observe(changed), changedRevision);
}
TEST(RetainedUnderlayRevision, OrderingAndIndexChangesAdvanceTopology) {
RetainedUnderlayTopologyTracker tracker;
RetainedUnderlayTopologySnapshot topology{
.boundary = 32,
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
};
const auto initialRevision = tracker.observe(topology);
std::swap(topology.staticPrefix[0], topology.staticPrefix[1]);
const auto reorderedRevision = tracker.observe(topology);
EXPECT_GT(reorderedRevision, initialRevision);
topology.staticPrefix[0].layerIndex = 30;
const auto reindexedRevision = tracker.observe(topology);
EXPECT_GT(reindexedRevision, reorderedRevision);
EXPECT_EQ(tracker.observe(topology), reindexedRevision);
}
TEST(RetainedUnderlayRevision, MatchingBuildRevisionAllowsReuseAndMismatchRebuilds) {
RetainedUnderlayCache cache;
EXPECT_EQ(cache.evaluate(key(), 7, true), RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
EXPECT_EQ(cache.getBuildRevision(), 7u);
EXPECT_EQ(cache.evaluate(key(), 7, true), RetainedUnderlayDecision::Reuse);
EXPECT_EQ(cache.evaluate(key(), 8, true, nullptr, "tile-mutation"), RetainedUnderlayDecision::Build);
EXPECT_FALSE(cache.isValid());
EXPECT_EQ(cache.getLastInvalidationReason(), "tile-mutation");
}
TEST(RetainedUnderlayRevision, PlacementAndFadeActivityRejectsStaleCacheUntilStable) {
RetainedUnderlayRevisionTracker tracker;
RetainedUnderlayCache cache;
auto state = revisionState();
auto revision = tracker.observe(state);
EXPECT_EQ(cache.evaluate(key(), revision, true), RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
state.placementStable = false;
revision = tracker.observe(state);
EXPECT_EQ(cache.evaluate(key(), revision, false, "static-placement", tracker.reason().c_str()),
RetainedUnderlayDecision::Fallback);
EXPECT_FALSE(cache.isValid());
state.placementStable = true;
revision = tracker.observe(state);
EXPECT_EQ(cache.evaluate(key(), revision, true, nullptr, tracker.reason().c_str()),
RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
state.staticStable = false;
revision = tracker.observe(state);
EXPECT_EQ(cache.evaluate(key(), revision, false, "static-transition", tracker.reason().c_str()),
RetainedUnderlayDecision::Fallback);
EXPECT_FALSE(cache.isValid());
state.staticStable = true;
revision = tracker.observe(state);
EXPECT_EQ(cache.evaluate(key(), revision, true, nullptr, tracker.reason().c_str()),
RetainedUnderlayDecision::Build);
}
TEST(RetainedUnderlayCache, UnsupportedAndResourceFailureFallBackSafely) {
RetainedUnderlayCache cache;
EXPECT_EQ(cache.evaluate(key(), 1, false, "3d"), RetainedUnderlayDecision::Fallback);
EXPECT_EQ(cache.getLastInvalidationReason(), "3d");
EXPECT_FALSE(cache.isValid());
EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Build);
cache.markBuildFailed("resource-or-compositor");
EXPECT_FALSE(cache.isValid());
EXPECT_EQ(cache.getFallbackCount(), 2u);
}
TEST(RetainedUnderlayCache, RuntimeControlDisablesAndReenablesMapScopedCache) {
auto state = std::make_shared<RetainedUnderlayRuntimeState>();
RetainedUnderlayCache cache;
cache.synchronizeRuntimeState(state);
EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
EXPECT_TRUE(cache.isValid());
EXPECT_TRUE(state->setEnabled(false));
cache.synchronizeRuntimeState(state);
EXPECT_FALSE(cache.isValid());
EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Fallback);
auto disabled = state->snapshot();
EXPECT_FALSE(disabled.enabled);
EXPECT_EQ(disabled.build, 1u);
EXPECT_EQ(disabled.fallback, 1u);
EXPECT_EQ(disabled.invalidation, 2u); // first-frame plus the runtime disable.
EXPECT_EQ(disabled.lastReason, "runtime-disabled");
EXPECT_TRUE(state->setEnabled(true));
cache.synchronizeRuntimeState(state);
EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Build);
EXPECT_TRUE(state->snapshot().enabled);
}
TEST(ParticleFastPath, ParticleOnlyStableRevisionIsCandidate) {
const auto decision = evaluateParticleFastPath(fastPathInput());
EXPECT_TRUE(decision.candidate);
EXPECT_EQ(decision.reason, ParticleFastPathMissReason::None);
}
TEST(ParticleFastPath, RevisionCameraAndViewportMismatchReject) {
auto input = fastPathInput();
input.revisionMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::RevisionMismatch);
input = fastPathInput();
input.cameraMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::CameraChanged);
input = fastPathInput();
input.viewportMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::ViewportChanged);
}
TEST(ParticleFastPath, NonParticleTransitionAndRepaintReject) {
auto input = fastPathInput();
input.hasNonParticleTransition = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::NonParticleTransition);
input = fastPathInput();
input.hasNonParticleRepaint = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::NonParticleRepaint);
}
TEST(ParticleFastPath, SettledCurrentPlacementAllowsCandidateAfterPlacementFullFrame) {
// A full frame may have run placement and captured placementChanged=true.
// That previous-frame event is deliberately not a fast-path gate input;
// only the controller's current transition state is authoritative.
auto input = fastPathInput();
input.hasPlacementTransition = false;
EXPECT_TRUE(evaluateParticleFastPath(input).candidate);
}
TEST(ParticleFastPath, ActivePlacementFadeAndFadingTilesReject) {
auto input = fastPathInput();
input.hasPlacementTransition = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::PlacementOrFade);
input = fastPathInput();
input.hasFadingTiles = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::FadingTiles);
}
TEST(ParticleFastPath, CacheDisabledAndSemanticMutationReject) {
auto input = fastPathInput();
input.retainedEnabled = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::RetainedDisabled);
input = fastPathInput();
input.cacheValid = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::CacheInvalid);
input = fastPathInput();
input.contentRevisionMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::ContentMutation);
input = fastPathInput();
input.updateParametersMatch = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::UpdateParametersChanged);
input = fastPathInput();
input.topologyMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::TopologyChanged);
input = fastPathInput();
input.styleLoaded = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::StyleNotLoaded);
}
TEST(ParticleFastPath, PendingStillAndUnsupportedPathsReject) {
auto input = fastPathInput();
input.hasPendingChanges = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::PendingChanges);
input = fastPathInput();
input.stillImageRequest = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::StillImageRequest);
input = fastPathInput();
input.supportedScene = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::UnsupportedScene);
}
TEST(ParticleFastPath, FullFrameCanRestabilizeCandidate) {
auto input = fastPathInput();
input.contentRevisionMatches = false;
EXPECT_FALSE(evaluateParticleFastPath(input).candidate);
input.contentRevisionMatches = true;
input.revisionMatches = true;
input.cacheValid = true;
input.staticStable = true;
EXPECT_TRUE(evaluateParticleFastPath(input).candidate);
}
TEST(ParticleFastPath, PreparedTreeReplacementOwnsLifetime) {
struct TrackedTree {
TrackedTree(int value_, int& destroyed_)
: value(value_), destroyed(destroyed_) {}
~TrackedTree() { ++destroyed; }
int value;
int& destroyed;
};
int destroyed = 0;
ParticleFastPathPreparedTree<TrackedTree> slot;
slot.replace(std::make_unique<TrackedTree>(1, destroyed));
EXPECT_EQ(slot.get()->value, 1);
slot.replace(std::make_unique<TrackedTree>(2, destroyed));
EXPECT_EQ(destroyed, 1);
EXPECT_EQ(slot.get()->value, 2);
slot.reset();
EXPECT_EQ(destroyed, 2);
EXPECT_TRUE(slot.empty());
}