#include #include #include #include #include using namespace mbgl; namespace { RetainedUnderlayKey key(Size size = {640, 480}, int32_t boundary = 4) { RetainedUnderlayKey result{size, boundary, {}}; result.cameraMatrix[0] = 1.0; result.cameraMatrix[5] = 1.0; result.cameraMatrix[10] = 1.0; result.cameraMatrix[15] = 1.0; return result; } RetainedUnderlayRevisionState revisionState(Size size = {640, 480}, int32_t boundary = 4) { RetainedUnderlayRevisionState result; result.viewport = size; result.boundary = boundary; result.pixelRatio = 2.0f; result.contentRevision = 1; result.topologyRevision = 1; result.styleLoaded = true; result.renderTreeLoaded = true; result.staticStable = true; result.placementStable = true; result.cameraMatrix = key(size, boundary).cameraMatrix; return result; } ParticleFastPathGateInput fastPathInput() { return { .hasPreparedTree = true, .retainedEnabled = true, .cacheValid = true, .revisionMatches = true, .contentRevisionMatches = true, .cameraMatches = true, .viewportMatches = true, .updateParametersMatch = true, .styleLoaded = true, .boundaryMatches = true, .topologyMatches = true, .staticStable = true, .hasPlacementTransition = false, .hasPendingChanges = false, .hasNonParticleTransition = false, .hasNonParticleRepaint = false, .hasFadingTiles = false, .particleNeedsRepaint = true, .stillImageRequest = false, .continuousMode = true, .debugDisabled = true, .supportedScene = true, .annotationMutation = false, }; } } // namespace TEST(RetainedDynamicBoundary, SelectsFirstCandidate) { RetainedDynamicBoundaryTracker none; none.consider(0, false); none.consider(3, false); EXPECT_FALSE(none.value()); RetainedDynamicBoundaryTracker particleOnly; particleOnly.consider(5, true); EXPECT_EQ(particleOnly.value(), 5); RetainedDynamicBoundaryTracker weatherAndParticle; weatherAndParticle.consider(7, true); weatherAndParticle.consider(2, true); EXPECT_EQ(weatherAndParticle.value(), 2); RetainedDynamicBoundaryTracker particleAndWeather; particleAndWeather.consider(2, true); particleAndWeather.consider(7, true); EXPECT_EQ(particleAndWeather.value(), 2); } TEST(RetainedLayerGroupRange, PreservesForwardReverseOriginalOrdinals) { const std::vector indices{0, 1, 3, 5, 8}; std::vector beforeForward; std::vector fromForward; std::vector beforeReverse; std::vector 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{0, 1, 2})); EXPECT_EQ(fromForward, (std::vector{3, 4})); EXPECT_EQ(beforeReverse, (std::vector{2, 1, 0})); EXPECT_EQ(fromReverse, (std::vector{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(); RetainedUnderlayCache cache; cache.synchronizeRuntimeState(state); EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Build); cache.markBuildSucceeded(); EXPECT_TRUE(cache.isValid()); EXPECT_TRUE(state->setEnabled(false)); cache.synchronizeRuntimeState(state); EXPECT_FALSE(cache.isValid()); EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Fallback); auto disabled = state->snapshot(); EXPECT_FALSE(disabled.enabled); EXPECT_EQ(disabled.build, 1u); EXPECT_EQ(disabled.fallback, 1u); EXPECT_EQ(disabled.invalidation, 2u); // first-frame plus the runtime disable. EXPECT_EQ(disabled.lastReason, "runtime-disabled"); EXPECT_TRUE(state->setEnabled(true)); cache.synchronizeRuntimeState(state); EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Build); EXPECT_TRUE(state->snapshot().enabled); } TEST(ParticleFastPath, ParticleOnlyStableRevisionIsCandidate) { const auto decision = evaluateParticleFastPath(fastPathInput()); EXPECT_TRUE(decision.candidate); EXPECT_EQ(decision.reason, ParticleFastPathMissReason::None); } TEST(ParticleFastPath, RevisionCameraAndViewportMismatchReject) { auto input = fastPathInput(); input.revisionMatches = false; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::RevisionMismatch); input = fastPathInput(); input.cameraMatches = false; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::CameraChanged); input = fastPathInput(); input.viewportMatches = false; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::ViewportChanged); } TEST(ParticleFastPath, NonParticleTransitionAndRepaintReject) { auto input = fastPathInput(); input.hasNonParticleTransition = true; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::NonParticleTransition); input = fastPathInput(); input.hasNonParticleRepaint = true; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::NonParticleRepaint); } TEST(ParticleFastPath, SettledCurrentPlacementAllowsCandidateAfterPlacementFullFrame) { // A full frame may have run placement and captured placementChanged=true. // That previous-frame event is deliberately not a fast-path gate input; // only the controller's current transition state is authoritative. auto input = fastPathInput(); input.hasPlacementTransition = false; EXPECT_TRUE(evaluateParticleFastPath(input).candidate); } TEST(ParticleFastPath, ActivePlacementFadeAndFadingTilesReject) { auto input = fastPathInput(); input.hasPlacementTransition = true; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::PlacementOrFade); input = fastPathInput(); input.hasFadingTiles = true; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::FadingTiles); } TEST(ParticleFastPath, CacheDisabledAndSemanticMutationReject) { auto input = fastPathInput(); input.retainedEnabled = false; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::RetainedDisabled); input = fastPathInput(); input.cacheValid = false; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::CacheInvalid); input = fastPathInput(); input.contentRevisionMatches = false; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::ContentMutation); input = fastPathInput(); input.updateParametersMatch = false; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::UpdateParametersChanged); input = fastPathInput(); input.topologyMatches = false; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::TopologyChanged); input = fastPathInput(); input.styleLoaded = false; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::StyleNotLoaded); } TEST(ParticleFastPath, PendingStillAndUnsupportedPathsReject) { auto input = fastPathInput(); input.hasPendingChanges = true; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::PendingChanges); input = fastPathInput(); input.stillImageRequest = true; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::StillImageRequest); input = fastPathInput(); input.supportedScene = false; EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::UnsupportedScene); } TEST(ParticleFastPath, FullFrameCanRestabilizeCandidate) { auto input = fastPathInput(); input.contentRevisionMatches = false; EXPECT_FALSE(evaluateParticleFastPath(input).candidate); input.contentRevisionMatches = true; input.revisionMatches = true; input.cacheValid = true; input.staticStable = true; EXPECT_TRUE(evaluateParticleFastPath(input).candidate); } TEST(ParticleFastPath, PreparedTreeReplacementOwnsLifetime) { struct TrackedTree { TrackedTree(int value_, int& destroyed_) : value(value_), destroyed(destroyed_) {} ~TrackedTree() { ++destroyed; } int value; int& destroyed; }; int destroyed = 0; ParticleFastPathPreparedTree slot; slot.replace(std::make_unique(1, destroyed)); EXPECT_EQ(slot.get()->value, 1); slot.replace(std::make_unique(2, destroyed)); EXPECT_EQ(destroyed, 1); EXPECT_EQ(slot.get()->value, 2); slot.reset(); EXPECT_EQ(destroyed, 2); EXPECT_TRUE(slot.empty()); }