1108 lines
49 KiB
C++
1108 lines
49 KiB
C++
#include <mbgl/renderer/retained_underlay_cache.hpp>
|
|
#include <mbgl/renderer/particle_fast_path.hpp>
|
|
#include <mbgl/renderer/continuous_layer_range.hpp>
|
|
#include <mbgl/renderer/render_segment_json.hpp>
|
|
#include <mbgl/renderer/render_segment_plan.hpp>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <initializer_list>
|
|
#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());
|
|
}
|
|
|
|
namespace {
|
|
RenderSegmentConfig explicitSegments(std::initializer_list<RenderSegmentDefinition> definitions) {
|
|
RenderSegmentConfig config;
|
|
config.source = RenderSegmentConfigSource::ExplicitPlan;
|
|
config.plan.segments.assign(definitions.begin(), definitions.end());
|
|
config.revision = 1;
|
|
return config;
|
|
}
|
|
|
|
std::vector<std::string> segmentLayerIds() {
|
|
return {"background", "water", "weather", "particle", "land", "ais"};
|
|
}
|
|
} // namespace
|
|
|
|
TEST(RenderSegmentPlan, SingleRetainedAndSingleContinuousCoverWholeStyle) {
|
|
auto retained = resolveRenderSegmentPlan(
|
|
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt}}), segmentLayerIds(), true);
|
|
ASSERT_TRUE(retained.valid);
|
|
ASSERT_EQ(retained.segments.size(), 1u);
|
|
EXPECT_EQ(retained.segments[0].beginIndex, 0);
|
|
EXPECT_EQ(retained.segments[0].endExclusive, 6);
|
|
EXPECT_TRUE(renderSegmentCoverageComplete(retained));
|
|
|
|
auto continuous = resolveRenderSegmentPlan(
|
|
explicitSegments({{RenderSegmentStrategy::Continuous, std::nullopt}}), segmentLayerIds(), true);
|
|
ASSERT_TRUE(continuous.valid);
|
|
EXPECT_EQ(continuous.segments[0].strategy, RenderSegmentStrategy::Continuous);
|
|
EXPECT_TRUE(renderSegmentCoverageComplete(continuous));
|
|
}
|
|
|
|
TEST(RenderSegmentPlan, PreservesCRRAndRRCWithoutMerging) {
|
|
auto crr = resolveRenderSegmentPlan(
|
|
explicitSegments({{RenderSegmentStrategy::Continuous, std::nullopt},
|
|
{RenderSegmentStrategy::Retained, "weather"},
|
|
{RenderSegmentStrategy::Retained, "land"}}),
|
|
segmentLayerIds(), true);
|
|
ASSERT_TRUE(crr.valid);
|
|
ASSERT_EQ(crr.segments.size(), 3u);
|
|
EXPECT_EQ(crr.segments[1].endExclusive, crr.segments[2].beginIndex);
|
|
EXPECT_EQ(crr.segments[1].strategy, RenderSegmentStrategy::Retained);
|
|
EXPECT_EQ(crr.segments[2].strategy, RenderSegmentStrategy::Retained);
|
|
|
|
auto rrc = resolveRenderSegmentPlan(
|
|
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Retained, "weather"},
|
|
{RenderSegmentStrategy::Continuous, "land"}}),
|
|
segmentLayerIds(), true);
|
|
ASSERT_TRUE(rrc.valid);
|
|
ASSERT_EQ(rrc.segments.size(), 3u);
|
|
EXPECT_EQ(rrc.segments[0].strategy, RenderSegmentStrategy::Retained);
|
|
EXPECT_EQ(rrc.segments[1].strategy, RenderSegmentStrategy::Retained);
|
|
}
|
|
|
|
TEST(RenderSegmentPlan, ResolvesFiveAlternatingSegmentsWithGlobalIndices) {
|
|
auto plan = resolveRenderSegmentPlan(
|
|
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "water"},
|
|
{RenderSegmentStrategy::Retained, "weather"},
|
|
{RenderSegmentStrategy::Continuous, "particle"},
|
|
{RenderSegmentStrategy::Retained, "land"}}),
|
|
segmentLayerIds(), true);
|
|
ASSERT_TRUE(plan.valid);
|
|
ASSERT_EQ(plan.segments.size(), 5u);
|
|
for (std::size_t i = 0; i < plan.segments.size(); ++i) {
|
|
EXPECT_EQ(plan.segments[i].segmentIndex, i);
|
|
}
|
|
EXPECT_EQ(plan.segments[3].beginIndex, 3);
|
|
EXPECT_EQ(plan.segments[4].endExclusive, 6);
|
|
EXPECT_TRUE(renderSegmentCoverageComplete(plan));
|
|
|
|
const auto continuousCount = std::count_if(plan.segments.begin(), plan.segments.end(), [](const auto& segment) {
|
|
return segment.strategy == RenderSegmentStrategy::Continuous;
|
|
});
|
|
ASSERT_EQ(continuousCount, 2);
|
|
auto fastInput = fastPathInput();
|
|
fastInput.unsupportedContinuousLayer = true;
|
|
const auto fastDecision = evaluateParticleFastPath(fastInput);
|
|
EXPECT_FALSE(fastDecision.candidate);
|
|
EXPECT_EQ(fastDecision.reason, ParticleFastPathMissReason::UnsupportedContinuousLayer);
|
|
}
|
|
|
|
TEST(RenderSegmentPlan, RejectsMissingDuplicateReversedAndEmptyDefinitions) {
|
|
auto empty = resolveRenderSegmentPlan(explicitSegments({}), segmentLayerIds(), true);
|
|
EXPECT_FALSE(empty.valid);
|
|
EXPECT_EQ(empty.reason, "empty-segments");
|
|
|
|
auto missing = resolveRenderSegmentPlan(
|
|
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "missing"}}),
|
|
segmentLayerIds(), true);
|
|
EXPECT_FALSE(missing.valid);
|
|
EXPECT_EQ(missing.reason, "boundary-layer-missing");
|
|
|
|
auto duplicate = resolveRenderSegmentPlan(
|
|
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "weather"},
|
|
{RenderSegmentStrategy::Retained, "weather"}}),
|
|
segmentLayerIds(), true);
|
|
EXPECT_FALSE(duplicate.valid);
|
|
EXPECT_EQ(duplicate.reason, "boundary-layer-duplicate");
|
|
|
|
auto reversed = resolveRenderSegmentPlan(
|
|
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "land"},
|
|
{RenderSegmentStrategy::Retained, "weather"}}),
|
|
segmentLayerIds(), true);
|
|
EXPECT_FALSE(reversed.valid);
|
|
EXPECT_EQ(reversed.reason, "boundary-order-invalid");
|
|
}
|
|
|
|
TEST(RenderSegmentPlan, HiddenBoundaryDoesNotAffectResolution) {
|
|
const auto before = resolveRenderSegmentPlan(
|
|
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "weather"}}),
|
|
segmentLayerIds(), true);
|
|
const auto after = resolveRenderSegmentPlan(
|
|
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "weather"}}),
|
|
segmentLayerIds(), true);
|
|
EXPECT_EQ(before, after);
|
|
}
|
|
|
|
TEST(RenderSegmentPlan, ContinuousRangeCompatibilityMapsToOneTwoOrThreeSegments) {
|
|
RenderSegmentConfig config;
|
|
config.source = RenderSegmentConfigSource::ContinuousRangeCompat;
|
|
config.continuousStartLayerId = "weather";
|
|
config.continuousEndLayerId = "particle";
|
|
config.revision = 8;
|
|
auto rcr = resolveRenderSegmentPlan(config, segmentLayerIds(), true);
|
|
ASSERT_TRUE(rcr.valid);
|
|
ASSERT_EQ(rcr.segments.size(), 3u);
|
|
EXPECT_EQ(rcr.segments[0].strategy, RenderSegmentStrategy::Retained);
|
|
EXPECT_EQ(rcr.segments[1].strategy, RenderSegmentStrategy::Continuous);
|
|
EXPECT_EQ(rcr.segments[2].strategy, RenderSegmentStrategy::Retained);
|
|
EXPECT_EQ(rcr.configSource, "continuous-range-compat");
|
|
|
|
config.continuousStartLayerId = "background";
|
|
config.continuousEndLayerId = "ais";
|
|
auto continuousOnly = resolveRenderSegmentPlan(config, segmentLayerIds(), true);
|
|
ASSERT_TRUE(continuousOnly.valid);
|
|
ASSERT_EQ(continuousOnly.segments.size(), 1u);
|
|
EXPECT_EQ(continuousOnly.segments[0].strategy, RenderSegmentStrategy::Continuous);
|
|
}
|
|
|
|
TEST(RenderSegmentPlan, BoundaryDeletionFailsClosedAndRestores) {
|
|
const auto config = explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "weather"}});
|
|
auto valid = resolveRenderSegmentPlan(config, segmentLayerIds(), true);
|
|
ASSERT_TRUE(valid.valid);
|
|
auto missing = resolveRenderSegmentPlan(
|
|
config, {"background", "water", "particle", "land", "ais"}, true);
|
|
EXPECT_FALSE(missing.valid);
|
|
EXPECT_TRUE(missing.segments.empty());
|
|
EXPECT_EQ(missing.reason, "boundary-layer-missing");
|
|
EXPECT_TRUE(resolveRenderSegmentPlan(config, segmentLayerIds(), true).valid);
|
|
}
|
|
|
|
TEST(RenderSegmentPlan, InsertedLayerNaturallyJoinsContainingSegment) {
|
|
const auto config = explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "weather"},
|
|
{RenderSegmentStrategy::Retained, "land"}});
|
|
auto plan = resolveRenderSegmentPlan(
|
|
config, {"background", "water", "weather", "inserted", "particle", "land", "ais"}, true);
|
|
ASSERT_TRUE(plan.valid);
|
|
EXPECT_EQ(plan.segments[1].beginIndex, 2);
|
|
EXPECT_EQ(plan.segments[1].endExclusive, 5);
|
|
EXPECT_EQ(plan.segments[2].beginIndex, 5);
|
|
}
|
|
|
|
TEST(RenderSegmentDiagnostics, VisibilityChangesMetadataButNotTopologyRevision) {
|
|
RetainedUnderlayRuntimeState state;
|
|
RenderSegmentPlan configured;
|
|
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "weather"}};
|
|
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
|
|
const auto resolved = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
|
|
std::vector<StyleLayerDiagnostics> visible;
|
|
for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) {
|
|
visible.push_back({static_cast<int32_t>(i), segmentLayerIds()[i], "fill", true, std::nullopt, std::nullopt});
|
|
}
|
|
state.recordRenderSegmentResolution(resolved, visible);
|
|
const auto revision = state.renderSegmentSnapshot().topologyRevision;
|
|
visible[2].visible = false;
|
|
state.recordRenderSegmentResolution(resolved, visible);
|
|
const auto hidden = state.renderSegmentSnapshot();
|
|
EXPECT_EQ(hidden.topologyRevision, revision);
|
|
EXPECT_FALSE(hidden.styleLayers[2].visible);
|
|
EXPECT_EQ(hidden.segments[1].beginIndex, 2);
|
|
}
|
|
|
|
TEST(RenderSegmentDiagnostics, OldCacheReleaseCannotChargeReplacementAtSameIndex) {
|
|
RetainedUnderlayRuntimeState state;
|
|
RenderSegmentPlan configured;
|
|
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "weather"}};
|
|
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
|
|
const auto oldPlan = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
|
|
state.recordRenderSegmentResolution(oldPlan, {});
|
|
const auto oldRevision = state.renderSegmentSnapshot().topologyRevision;
|
|
|
|
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "land"}};
|
|
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
|
|
const auto newPlan = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
|
|
state.recordRenderSegmentResolution(newPlan, {});
|
|
const auto newRevision = state.renderSegmentSnapshot().topologyRevision;
|
|
ASSERT_NE(oldRevision, newRevision);
|
|
|
|
state.recordSegmentResourceRelease(0, oldRevision);
|
|
auto snapshot = state.renderSegmentSnapshot();
|
|
ASSERT_TRUE(snapshot.segments[0].cache);
|
|
EXPECT_EQ(snapshot.segments[0].cache->resourceRelease, 0u);
|
|
state.recordSegmentResourceRelease(0, newRevision);
|
|
snapshot = state.renderSegmentSnapshot();
|
|
EXPECT_EQ(snapshot.segments[0].cache->resourceRelease, 1u);
|
|
}
|
|
|
|
TEST(RenderSegmentDiagnostics, CacheOffKeepsResolvedTopology) {
|
|
RetainedUnderlayRuntimeState state;
|
|
RenderSegmentPlan configured;
|
|
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "weather"}};
|
|
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
|
|
const auto resolved = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
|
|
std::vector<StyleLayerDiagnostics> layers;
|
|
for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) {
|
|
layers.push_back({static_cast<int32_t>(i), segmentLayerIds()[i], "fill", true, std::nullopt, std::nullopt});
|
|
}
|
|
state.recordRenderSegmentResolution(resolved, std::move(layers));
|
|
state.setEnabled(false);
|
|
const auto snapshot = state.renderSegmentSnapshot();
|
|
EXPECT_FALSE(snapshot.enabled);
|
|
EXPECT_TRUE(snapshot.valid);
|
|
EXPECT_TRUE(snapshot.coverageComplete);
|
|
EXPECT_EQ(snapshot.segmentCount, 2u);
|
|
}
|
|
|
|
TEST(RenderSegmentDiagnostics, InvalidPlanStillReturnsOrderedStyleLayers) {
|
|
RetainedUnderlayRuntimeState state;
|
|
RenderSegmentPlan configured;
|
|
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "missing"}};
|
|
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
|
|
const auto resolved = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
|
|
ASSERT_FALSE(resolved.valid);
|
|
std::vector<StyleLayerDiagnostics> layers;
|
|
for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) {
|
|
layers.push_back({static_cast<int32_t>(i), segmentLayerIds()[i], "fill", true, std::nullopt, std::nullopt});
|
|
}
|
|
state.recordRenderSegmentResolution(resolved, layers);
|
|
const auto snapshot = state.renderSegmentSnapshot();
|
|
EXPECT_FALSE(snapshot.valid);
|
|
EXPECT_TRUE(snapshot.fallback);
|
|
EXPECT_TRUE(snapshot.segments.empty());
|
|
ASSERT_EQ(snapshot.styleLayers.size(), segmentLayerIds().size());
|
|
EXPECT_EQ(snapshot.styleLayers.front().layerId, "background");
|
|
EXPECT_EQ(snapshot.styleLayers.back().layerId, "ais");
|
|
}
|
|
|
|
TEST(RenderSegmentDiagnostics, SnapshotCoversStyleExactlyAndSerializesContinuousCacheAsNull) {
|
|
RetainedUnderlayRuntimeState state;
|
|
RenderSegmentPlan configured;
|
|
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "weather"},
|
|
{RenderSegmentStrategy::Retained, "land"}};
|
|
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
|
|
const auto config = state.renderSegmentPlanConfig();
|
|
const auto resolved = resolveRenderSegmentPlan(config, segmentLayerIds(), true);
|
|
std::vector<StyleLayerDiagnostics> layers;
|
|
for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) {
|
|
layers.push_back({static_cast<int32_t>(i), segmentLayerIds()[i], "fill", true, std::nullopt, std::nullopt});
|
|
}
|
|
layers[1].sourceId = "chart";
|
|
layers[1].sourceLayer = "water";
|
|
state.recordRenderSegmentResolution(resolved, layers);
|
|
const auto snapshot = state.renderSegmentSnapshot();
|
|
ASSERT_TRUE(snapshot.valid);
|
|
ASSERT_TRUE(snapshot.coverageComplete);
|
|
ASSERT_EQ(snapshot.styleLayers.size(), 6u);
|
|
ASSERT_EQ(snapshot.segments.size(), 3u);
|
|
EXPECT_TRUE(snapshot.segments[0].cache.has_value());
|
|
EXPECT_FALSE(snapshot.segments[1].cache.has_value());
|
|
std::vector<std::string> flattened;
|
|
for (const auto& segment : snapshot.segments) {
|
|
for (const auto& layer : segment.layers) flattened.push_back(layer.layerId);
|
|
}
|
|
EXPECT_EQ(flattened, segmentLayerIds());
|
|
EXPECT_EQ(snapshot.styleLayers[1].sourceId, std::optional<std::string>{"chart"});
|
|
EXPECT_EQ(snapshot.segments[0].layers[1].sourceLayer, std::optional<std::string>{"water"});
|
|
const auto json = renderSegmentDiagnosticsToJSON(snapshot);
|
|
EXPECT_NE(json.find("\"coverageComplete\":true"), std::string::npos);
|
|
EXPECT_NE(json.find("\"cache\":null"), std::string::npos);
|
|
}
|
|
|
|
|
|
TEST(RenderSegmentDiagnostics, ReadinessSnapshotMapsSourcesToSegmentsWithoutSideEffects) {
|
|
RetainedUnderlayRuntimeState state;
|
|
RenderSegmentPlan configured;
|
|
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
|
|
{RenderSegmentStrategy::Continuous, "weather"},
|
|
{RenderSegmentStrategy::Retained, "land"}};
|
|
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
|
|
const auto resolved = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
|
|
|
|
std::vector<StyleLayerDiagnostics> layers;
|
|
for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) {
|
|
layers.push_back({static_cast<int32_t>(i), segmentLayerIds()[i], "fill", true, std::nullopt, std::nullopt});
|
|
}
|
|
layers[1].sourceId = "chart";
|
|
layers[2].sourceId = "dynamic";
|
|
layers[3].sourceId = "dynamic";
|
|
layers[4].sourceId = "chart";
|
|
layers[5].layerType = "symbol";
|
|
layers[5].sourceId = "ais";
|
|
state.recordRenderSegmentResolution(resolved, layers);
|
|
state.recordSegmentBuild(0);
|
|
state.recordSegmentCacheState(0, RetainedRangeCacheState::Ready, 41);
|
|
|
|
RendererReadinessDiagnostics readiness;
|
|
readiness.styleLoaded = true;
|
|
readiness.imageManagerLoaded = true;
|
|
readiness.renderOrchestratorLoaded = false;
|
|
readiness.spriteLoaded = true;
|
|
readiness.hasFadingTiles = false;
|
|
readiness.contentRevision = 42;
|
|
readiness.sources = {
|
|
{.sourceId = "chart",
|
|
.sourceType = "vector",
|
|
.enabled = true,
|
|
.isLoaded = true,
|
|
.tileBacked = true,
|
|
.tileCount = 4,
|
|
.incompleteTileCount = 0,
|
|
.loadedTileCount = 4},
|
|
{.sourceId = "dynamic",
|
|
.sourceType = "raster",
|
|
.enabled = true,
|
|
.isLoaded = false,
|
|
.tileBacked = true,
|
|
.tileCount = 1,
|
|
.incompleteTileCount = 1,
|
|
.loadedTileCount = 0,
|
|
.lastSourceMutationSequence = 11,
|
|
.lastTileChangedSequence = 12,
|
|
.lastTileActionSequence = 13,
|
|
.lastTileAction = "StartParse",
|
|
.lastMutationReason = "tile-mutation"}};
|
|
readiness.lastMutation = {.sequence = 12,
|
|
.contentRevision = 42,
|
|
.eventType = "TileMutation",
|
|
.reason = "tile-mutation",
|
|
.sourceId = "dynamic"};
|
|
state.recordRendererReadiness(readiness);
|
|
|
|
const auto first = state.renderSegmentSnapshot();
|
|
ASSERT_EQ(first.sources.size(), 2u);
|
|
EXPECT_TRUE(first.styleLoaded);
|
|
EXPECT_TRUE(first.imageManagerLoaded);
|
|
EXPECT_FALSE(first.renderOrchestratorLoaded);
|
|
EXPECT_EQ(first.contentRevision, 42u);
|
|
EXPECT_EQ(first.sources[0].sourceId, "chart");
|
|
EXPECT_TRUE(first.sources[0].isLoaded);
|
|
EXPECT_EQ(first.sources[0].tileCount, std::optional<uint32_t>{4});
|
|
EXPECT_EQ(first.sources[1].sourceId, "dynamic");
|
|
EXPECT_FALSE(first.sources[1].isLoaded);
|
|
EXPECT_EQ(first.sources[1].incompleteTileCount, std::optional<uint32_t>{1});
|
|
ASSERT_EQ(first.segments.size(), 3u);
|
|
EXPECT_EQ(first.segments[0].dependentSourceIds, std::vector<std::string>({"chart"}));
|
|
EXPECT_EQ(first.segments[1].dependentSourceIds, std::vector<std::string>({"dynamic"}));
|
|
EXPECT_EQ(first.segments[2].dependentSourceIds, std::vector<std::string>({"ais", "chart"}));
|
|
EXPECT_TRUE(first.segments[0].unknownDependencies.empty());
|
|
EXPECT_EQ(first.segments[2].unknownDependencies,
|
|
std::vector<std::string>({"symbol/glyph/image/placement"}));
|
|
EXPECT_EQ(first.segments[0].dependentSourceIds.size(), 1u);
|
|
const auto backgroundLayer = std::find_if(first.segments[0].layers.begin(), first.segments[0].layers.end(),
|
|
[](const auto& layer) { return layer.layerId == "background"; });
|
|
ASSERT_NE(backgroundLayer, first.segments[0].layers.end());
|
|
EXPECT_FALSE(backgroundLayer->sourceId.has_value());
|
|
EXPECT_EQ(first.lastMutation.affectedSegmentIndices, std::vector<uint32_t>({1}));
|
|
EXPECT_EQ(first.lastMutation.dependencyScope, "source-local-diagnostic");
|
|
|
|
const auto topologyRevision = first.topologyRevision;
|
|
const auto cacheBuild = first.segments[0].cache->build;
|
|
state.recordRendererReadiness(readiness);
|
|
const auto second = state.renderSegmentSnapshot();
|
|
EXPECT_EQ(second.topologyRevision, topologyRevision);
|
|
ASSERT_TRUE(second.segments[0].cache);
|
|
EXPECT_EQ(second.segments[0].cache->build, cacheBuild);
|
|
EXPECT_EQ(second.segments[0].cache->contentRevision, 41u);
|
|
const auto third = state.renderSegmentSnapshot();
|
|
EXPECT_EQ(third.topologyRevision, second.topologyRevision);
|
|
EXPECT_EQ(third.contentRevision, second.contentRevision);
|
|
EXPECT_EQ(third.sources, second.sources);
|
|
EXPECT_EQ(third.lastMutation, second.lastMutation);
|
|
|
|
const auto json = renderSegmentDiagnosticsToJSON(second);
|
|
EXPECT_EQ(renderSegmentDiagnosticsToJSON(state.renderSegmentSnapshot()), json);
|
|
EXPECT_NE(json.find("\"renderOrchestratorLoaded\":false"), std::string::npos);
|
|
EXPECT_NE(json.find("\"incompleteTileCount\":1"), std::string::npos);
|
|
EXPECT_NE(json.find("\"dependentSourceIds\":[\"dynamic\"]"), std::string::npos);
|
|
EXPECT_NE(json.find("\"affectedSegmentIndices\":[1]"), std::string::npos);
|
|
}
|
|
|
|
TEST(RenderSegmentPlan, StrictJSONRejectsUnknownFieldsStrategyAndInvalidFirstBoundary) {
|
|
EXPECT_FALSE(parseRenderSegmentPlanJSON(
|
|
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED","unexpected":true}]})"));
|
|
EXPECT_FALSE(parseRenderSegmentPlanJSON(
|
|
R"({"schemaVersion":1,"segments":[{"strategy":"DYNAMIC"}]})"));
|
|
auto parsed = parseRenderSegmentPlanJSON(
|
|
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED","startLayerId":"water"}]})");
|
|
ASSERT_TRUE(parsed);
|
|
RetainedUnderlayRuntimeState state;
|
|
EXPECT_FALSE(state.setRenderSegmentPlan(std::move(*parsed)));
|
|
}
|
|
|
|
TEST(RenderSegmentPlan, StrictJSONRejectsNulDuplicateKeysAndUnknownSchema) {
|
|
EXPECT_FALSE(parseRenderSegmentPlanJSON(
|
|
R"({"schemaVersion":2,"segments":[{"strategy":"RETAINED"}]})"));
|
|
EXPECT_FALSE(parseRenderSegmentPlanJSON(
|
|
R"({"schemaVersion":1,"schemaVersion":1,"segments":[{"strategy":"RETAINED"}]})"));
|
|
EXPECT_FALSE(parseRenderSegmentPlanJSON(
|
|
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED","strategy":"CONTINUOUS"}]})"));
|
|
EXPECT_FALSE(parseRenderSegmentPlanJSON(
|
|
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED\u0000suffix"}]})"));
|
|
EXPECT_FALSE(parseRenderSegmentPlanJSON(
|
|
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED"},{"strategy":"CONTINUOUS","startLayerId":"water\u0000suffix"}]})"));
|
|
|
|
std::string rawNul = R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED"}]})";
|
|
rawNul.insert(rawNul.end() - 2, '\0');
|
|
rawNul.insert(rawNul.end() - 2, 'x');
|
|
EXPECT_FALSE(parseRenderSegmentPlanJSON(rawNul));
|
|
}
|
|
|
|
TEST(RenderSegmentPlan, InvalidJSONClearsRendererPlanAndValidPlanRecovers) {
|
|
constexpr auto validJSON =
|
|
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED"},{"strategy":"CONTINUOUS","startLayerId":"water"}]})";
|
|
RetainedUnderlayRuntimeState state;
|
|
auto valid = renderSegmentPlanFromJSONOrInvalid(validJSON);
|
|
ASSERT_TRUE(state.setRenderSegmentPlan(std::move(valid)));
|
|
auto resolved = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
|
|
ASSERT_TRUE(resolved.valid);
|
|
|
|
auto invalid = renderSegmentPlanFromJSONOrInvalid(
|
|
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED","strategy":"CONTINUOUS"}]})");
|
|
EXPECT_FALSE(state.setRenderSegmentPlan(std::move(invalid)));
|
|
auto cleared = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
|
|
EXPECT_FALSE(cleared.valid);
|
|
EXPECT_EQ(cleared.reason, "unsupported-schema-version");
|
|
|
|
auto missingBoundary = renderSegmentPlanFromJSONOrInvalid(
|
|
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED"},{"strategy":"CONTINUOUS"}]})");
|
|
EXPECT_FALSE(state.setRenderSegmentPlan(std::move(missingBoundary)));
|
|
EXPECT_FALSE(resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true).valid);
|
|
|
|
auto reversedBoundary = renderSegmentPlanFromJSONOrInvalid(
|
|
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED"},{"strategy":"CONTINUOUS","startLayerId":"water"},{"strategy":"RETAINED","startLayerId":"background"}]})");
|
|
ASSERT_TRUE(state.setRenderSegmentPlan(std::move(reversedBoundary)));
|
|
auto reversed = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
|
|
EXPECT_FALSE(reversed.valid);
|
|
EXPECT_EQ(reversed.reason, "boundary-order-invalid");
|
|
|
|
auto recovered = renderSegmentPlanFromJSONOrInvalid(validJSON);
|
|
ASSERT_TRUE(state.setRenderSegmentPlan(std::move(recovered)));
|
|
EXPECT_TRUE(resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true).valid);
|
|
}
|