638 lines
26 KiB
C++
638 lines
26 KiB
C++
#include <mbgl/renderer/retained_underlay_cache.hpp>
|
|
#include <mbgl/renderer/particle_fast_path.hpp>
|
|
#include <mbgl/renderer/continuous_layer_range.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, 0, 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,
|
|
.unsupportedContinuousLayer = false,
|
|
.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(RetainedUnderlayCache, RuntimeValidationDiagnosticsExposeRangeResourcesAndFastPath) {
|
|
RetainedUnderlayRuntimeState state;
|
|
ASSERT_TRUE(state.setContinuousLayerRange("weather", "particle"));
|
|
const auto config = state.continuousLayerRangeConfig();
|
|
state.recordRangeResolution(
|
|
{.configured = true,
|
|
.valid = true,
|
|
.startLayerId = "weather",
|
|
.endLayerId = "particle",
|
|
.startIndex = 2,
|
|
.endIndex = 3,
|
|
.configRevision = config.revision,
|
|
.reason = "explicit-range"},
|
|
true,
|
|
true);
|
|
state.recordRangeCacheState(false, RetainedRangeCacheState::Reuse);
|
|
state.recordRangeInvalidationReason(false, RetainedUnderlayReason::Viewport);
|
|
state.recordRangeTextureCreate(false);
|
|
state.recordRangeCompositeCreate(false);
|
|
state.recordRangeResourceRelease(false);
|
|
state.recordFastPathEligibility(true);
|
|
state.recordFastPathDecision(true, ParticleFastPathMissReason::None);
|
|
state.recordFullCpuFrame();
|
|
|
|
const auto snapshot = state.snapshot();
|
|
EXPECT_TRUE(snapshot.rangeValid);
|
|
EXPECT_EQ(snapshot.continuousBegin, 2);
|
|
EXPECT_EQ(snapshot.continuousEndExclusive, 4);
|
|
EXPECT_EQ(snapshot.bottomCacheState, "reuse");
|
|
EXPECT_EQ(snapshot.bottomLastInvalidationReason, "viewport");
|
|
EXPECT_EQ(snapshot.bottomTextureCreate, 1u);
|
|
EXPECT_EQ(snapshot.bottomCompositeCreate, 1u);
|
|
EXPECT_EQ(snapshot.bottomResourceRelease, 1u);
|
|
EXPECT_TRUE(snapshot.fastPathEligible);
|
|
EXPECT_FALSE(snapshot.fastPathActive);
|
|
EXPECT_EQ(snapshot.fastPathHit, 1u);
|
|
EXPECT_EQ(snapshot.fastPathMiss, 0u);
|
|
EXPECT_EQ(snapshot.fullCpuFrame, 1u);
|
|
}
|
|
|
|
TEST(RetainedUnderlayRevision, MonotonicAcrossCameraViewportStyleAndSourceChanges) {
|
|
RetainedUnderlayRevisionTracker tracker;
|
|
RetainedUnderlayCache cache;
|
|
auto state = revisionState();
|
|
const auto currentKey = [&] {
|
|
return RetainedUnderlayKey{state.viewport, 0, *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(ContinuousLayerRange, ResolvesInclusiveSingleAndMultiLayerRanges) {
|
|
const std::vector<std::string> layers{"bottom", "hidden-start", "middle", "hidden-end", "top"};
|
|
auto single = resolveContinuousLayerRange({true, "middle", "middle", 1}, layers);
|
|
ASSERT_TRUE(single.valid);
|
|
EXPECT_EQ(single.startIndex, 2);
|
|
EXPECT_EQ(single.endIndex, 2);
|
|
EXPECT_EQ(single.endExclusive(), 3);
|
|
|
|
auto multi = resolveContinuousLayerRange({true, "hidden-start", "hidden-end", 2}, layers);
|
|
ASSERT_TRUE(multi.valid);
|
|
EXPECT_EQ(multi.startIndex, 1);
|
|
EXPECT_EQ(multi.endIndex, 3);
|
|
EXPECT_TRUE(multi.bottomPresent());
|
|
EXPECT_TRUE(multi.topPresent(static_cast<int32_t>(layers.size())));
|
|
}
|
|
|
|
TEST(ContinuousLayerRange, RejectsMissingAndReversedEndpointsWithoutStaleIndices) {
|
|
const std::vector<std::string> layers{"a", "b", "c"};
|
|
const auto reversed = resolveContinuousLayerRange({true, "c", "a", 1}, layers);
|
|
EXPECT_FALSE(reversed.valid);
|
|
EXPECT_EQ(reversed.reason, "range-order-invalid");
|
|
|
|
const auto missingStart = resolveContinuousLayerRange({true, "missing", "c", 2}, layers);
|
|
EXPECT_FALSE(missingStart.valid);
|
|
EXPECT_EQ(missingStart.startIndex, -1);
|
|
EXPECT_EQ(missingStart.reason, "start-layer-missing");
|
|
|
|
const auto missingEnd = resolveContinuousLayerRange({true, "a", "missing", 3}, layers);
|
|
EXPECT_FALSE(missingEnd.valid);
|
|
EXPECT_EQ(missingEnd.endIndex, -1);
|
|
EXPECT_EQ(missingEnd.reason, "end-layer-missing");
|
|
}
|
|
|
|
TEST(ContinuousLayerRange, SupportsAllFourDegradedTopologies) {
|
|
const std::vector<std::string> layers{"a", "b", "c", "d"};
|
|
const auto three = resolveContinuousLayerRange({true, "b", "c", 1}, layers);
|
|
EXPECT_TRUE(three.bottomPresent());
|
|
EXPECT_TRUE(three.topPresent(4));
|
|
|
|
const auto bottomOnly = resolveContinuousLayerRange({true, "b", "d", 2}, layers);
|
|
EXPECT_TRUE(bottomOnly.bottomPresent());
|
|
EXPECT_FALSE(bottomOnly.topPresent(4));
|
|
|
|
const auto topOnly = resolveContinuousLayerRange({true, "a", "c", 3}, layers);
|
|
EXPECT_FALSE(topOnly.bottomPresent());
|
|
EXPECT_TRUE(topOnly.topPresent(4));
|
|
|
|
const auto continuousOnly = resolveContinuousLayerRange({true, "a", "d", 4}, layers);
|
|
EXPECT_TRUE(continuousOnly.valid);
|
|
EXPECT_FALSE(continuousOnly.bottomPresent());
|
|
EXPECT_FALSE(continuousOnly.topPresent(4));
|
|
}
|
|
|
|
TEST(ContinuousLayerRange, OnlyActuallyPresentRetainedRangesRequestCacheBuilds) {
|
|
RetainedUnderlayCache bottom(RetainedRangeRole::Bottom);
|
|
RetainedUnderlayCache top(RetainedRangeRole::Top);
|
|
const auto countBuilds = [&](bool bottomPresent, bool topPresent) {
|
|
const auto bottomDecision = bottom.evaluate(bottomPresent ? std::optional<RetainedUnderlayKey>(key())
|
|
: std::nullopt,
|
|
1, true);
|
|
const auto topDecision = top.evaluate(topPresent ? std::optional<RetainedUnderlayKey>(key())
|
|
: std::nullopt,
|
|
1, true);
|
|
return static_cast<int>(bottomDecision == RetainedUnderlayDecision::Build) +
|
|
static_cast<int>(topDecision == RetainedUnderlayDecision::Build);
|
|
};
|
|
EXPECT_EQ(countBuilds(true, true), 2);
|
|
EXPECT_EQ(countBuilds(true, false), 1);
|
|
EXPECT_EQ(countBuilds(false, true), 1);
|
|
EXPECT_EQ(countBuilds(false, false), 0);
|
|
}
|
|
|
|
TEST(ContinuousLayerRange, ReorderAndEndpointDeletionAreResolvedFromCurrentStyle) {
|
|
const ContinuousLayerRangeConfig config{true, "b", "c", 7};
|
|
EXPECT_TRUE(resolveContinuousLayerRange(config, {"a", "b", "c"}).valid);
|
|
EXPECT_FALSE(resolveContinuousLayerRange(config, {"a", "c", "b"}).valid);
|
|
const auto deleted = resolveContinuousLayerRange(config, {"a", "b"});
|
|
EXPECT_FALSE(deleted.valid);
|
|
EXPECT_EQ(deleted.reason, "end-layer-missing");
|
|
}
|
|
|
|
TEST(ContinuousLayerRange, RuntimeConfigurationRejectsEmptyIdsAndAdvancesMonotonically) {
|
|
RetainedUnderlayRuntimeState state;
|
|
EXPECT_FALSE(state.setContinuousLayerRange("", "end"));
|
|
EXPECT_EQ(state.continuousLayerRangeConfig().revision, 0u);
|
|
EXPECT_TRUE(state.setContinuousLayerRange("start", "end"));
|
|
EXPECT_EQ(state.continuousLayerRangeConfig().revision, 1u);
|
|
EXPECT_TRUE(state.setContinuousLayerRange("start", "end"));
|
|
EXPECT_EQ(state.continuousLayerRangeConfig().revision, 1u);
|
|
EXPECT_TRUE(state.setContinuousLayerRange("same", "same"));
|
|
const auto config = state.continuousLayerRangeConfig();
|
|
EXPECT_EQ(config.revision, 2u);
|
|
EXPECT_EQ(config.startLayerId, config.endLayerId);
|
|
}
|
|
|
|
TEST(ContinuousLayerRange, ExplicitConfigurationNeverMixesWithLegacyBoundary) {
|
|
ContinuousLayerRangeResolution valid;
|
|
valid.configured = true;
|
|
valid.valid = true;
|
|
valid.startIndex = 0;
|
|
valid.endIndex = 1;
|
|
valid.configRevision = 4;
|
|
const auto explicitPlan = selectContinuousRenderRanges(valid, 7, 10);
|
|
EXPECT_EQ(explicitPlan.continuousBegin, 0);
|
|
EXPECT_EQ(explicitPlan.continuousEndExclusive, 2);
|
|
EXPECT_FALSE(explicitPlan.bottomPresent());
|
|
EXPECT_TRUE(explicitPlan.topPresent());
|
|
|
|
valid.valid = false;
|
|
valid.reason = "start-layer-missing";
|
|
const auto invalidPlan = selectContinuousRenderRanges(valid, 7, 10);
|
|
EXPECT_FALSE(invalidPlan.retainedMode());
|
|
EXPECT_FALSE(invalidPlan.bottomPresent());
|
|
EXPECT_FALSE(invalidPlan.topPresent());
|
|
|
|
const ContinuousLayerRangeResolution unconfigured;
|
|
const auto legacyPlan = selectContinuousRenderRanges(unconfigured, 7, 10);
|
|
EXPECT_TRUE(legacyPlan.retainedMode());
|
|
EXPECT_EQ(legacyPlan.continuousBegin, 7);
|
|
EXPECT_FALSE(legacyPlan.topPresent());
|
|
}
|
|
|
|
TEST(ContinuousLayerRange, AbsentRetainedRangesDoNotCauseFastPathMiss) {
|
|
auto input = fastPathInput();
|
|
input.cacheValid = true; // all zero actually-present retained ranges are valid vacuously.
|
|
EXPECT_TRUE(evaluateParticleFastPath(input).candidate);
|
|
}
|
|
|
|
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);
|
|
|
|
input = fastPathInput();
|
|
input.unsupportedContinuousLayer = true;
|
|
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::UnsupportedContinuousLayer);
|
|
}
|
|
|
|
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());
|
|
}
|