Files
MapLibreParticleLab/test/renderer/retained_underlay_cache.test.cpp

313 lines
13 KiB
C++

#include <mbgl/renderer/retained_underlay_cache.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;
}
} // 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);
}