feat: add retained underlay revision invalidation

This commit is contained in:
tei
2026-09-22 21:01:01 +09:00
parent 07a17b4c70
commit bcec6ce5e0
11 changed files with 817 additions and 24 deletions

View File

@@ -2,6 +2,7 @@
#include <gtest/gtest.h>
#include <utility>
#include <vector>
using namespace mbgl;
@@ -15,6 +16,21 @@ RetainedUnderlayKey key(Size size = {640, 480}, int32_t boundary = 4) {
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) {
@@ -64,25 +80,206 @@ TEST(RetainedLayerGroupRange, PreservesForwardReverseOriginalOrdinals) {
TEST(RetainedUnderlayCache, BuildReuseAndInvalidation) {
RetainedUnderlayCache cache;
EXPECT_EQ(cache.evaluate(std::nullopt, false), RetainedUnderlayDecision::Disabled);
EXPECT_EQ(cache.evaluate(key(), true), RetainedUnderlayDecision::Build);
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(), true), RetainedUnderlayDecision::Reuse);
EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Reuse);
EXPECT_EQ(cache.getBuildRevision(), 1u);
EXPECT_EQ(cache.evaluate(key({800, 600}), true), RetainedUnderlayDecision::Build);
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), true), RetainedUnderlayDecision::Build);
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(), false, "3d"), RetainedUnderlayDecision::Fallback);
EXPECT_EQ(cache.evaluate(key(), 1, false, "3d"), RetainedUnderlayDecision::Fallback);
EXPECT_EQ(cache.getLastInvalidationReason(), "3d");
EXPECT_FALSE(cache.isValid());
EXPECT_EQ(cache.evaluate(key(), true), RetainedUnderlayDecision::Build);
EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Build);
cache.markBuildFailed("resource-or-compositor");
EXPECT_FALSE(cache.isValid());
EXPECT_EQ(cache.getFallbackCount(), 2u);
@@ -93,14 +290,14 @@ TEST(RetainedUnderlayCache, RuntimeControlDisablesAndReenablesMapScopedCache) {
RetainedUnderlayCache cache;
cache.synchronizeRuntimeState(state);
EXPECT_EQ(cache.evaluate(key(), true), RetainedUnderlayDecision::Build);
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(), true), RetainedUnderlayDecision::Fallback);
EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Fallback);
auto disabled = state->snapshot();
EXPECT_FALSE(disabled.enabled);
EXPECT_EQ(disabled.build, 1u);
@@ -110,6 +307,6 @@ TEST(RetainedUnderlayCache, RuntimeControlDisablesAndReenablesMapScopedCache) {
EXPECT_TRUE(state->setEnabled(true));
cache.synchronizeRuntimeState(state);
EXPECT_EQ(cache.evaluate(key(), true), RetainedUnderlayDecision::Build);
EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Build);
EXPECT_TRUE(state->snapshot().enabled);
}