#include #include #include #include #include #include #include #include #include #include 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 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(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 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(layers.size()))); } TEST(ContinuousLayerRange, RejectsMissingAndReversedEndpointsWithoutStaleIndices) { const std::vector 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 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(key()) : std::nullopt, 1, true); const auto topDecision = top.evaluate(topPresent ? std::optional(key()) : std::nullopt, 1, true); return static_cast(bottomDecision == RetainedUnderlayDecision::Build) + static_cast(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}}, .continuousEndExclusive = std::nullopt, .staticSuffix = {}, }; 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}}, .continuousEndExclusive = std::nullopt, .staticSuffix = {}, }; 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}}, .continuousEndExclusive = std::nullopt, .staticSuffix = {}, }; 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); 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 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()); } namespace { RenderSegmentConfig explicitSegments(std::initializer_list definitions) { RenderSegmentConfig config; config.source = RenderSegmentConfigSource::ExplicitPlan; config.plan.segments.assign(definitions.begin(), definitions.end()); config.revision = 1; return config; } std::vector 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 visible; for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) { visible.push_back({static_cast(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 layers; for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) { layers.push_back({static_cast(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 layers; for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) { layers.push_back({static_cast(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 layers; for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) { layers.push_back({static_cast(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 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{"chart"}); EXPECT_EQ(snapshot.segments[0].layers[1].sourceLayer, std::optional{"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 layers; for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) { layers.push_back({static_cast(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); state.recordSegmentExecution(0, true, true, true, 17, "reuse", nullptr); 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", .affectedSegmentIndices = {}, .dependencyScope = "none"}; 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{4}); EXPECT_EQ(first.sources[1].sourceId, "dynamic"); EXPECT_FALSE(first.sources[1].isLoaded); EXPECT_EQ(first.sources[1].incompleteTileCount, std::optional{1}); ASSERT_EQ(first.segments.size(), 3u); EXPECT_EQ(first.segments[0].dependentSourceIds, std::vector({"chart"})); EXPECT_EQ(first.segments[1].dependentSourceIds, std::vector({"dynamic"})); EXPECT_EQ(first.segments[2].dependentSourceIds, std::vector({"ais", "chart"})); EXPECT_TRUE(first.segments[0].unknownDependencies.empty()); EXPECT_EQ(first.segments[2].unknownDependencies, std::vector({"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({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); EXPECT_EQ(second.segments[0].cache->sourceRevision, 17u); EXPECT_EQ(second.segments[0].cache->finalDecision, "reuse"); 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); EXPECT_NE(json.find("\"sourceRevision\":17"), std::string::npos); } TEST(RenderSegmentLocality, SourceRevisionsAreScopedAndRecreationUsesNewIdentity) { const std::vector layers{ {0, "background", "background", true, std::nullopt, std::nullopt}, {1, "a0", "fill", true, "a", std::nullopt}, {2, "b", "circle", true, "b", std::nullopt}, {3, "a1", "line", true, "a", std::nullopt}}; const auto r0 = renderSegmentDependencies(layers, 0, 2); const auto r1 = renderSegmentDependencies(layers, 2, 3); const auto r2 = renderSegmentDependencies(layers, 3, 4); const auto sourceLess = renderSegmentDependencies(layers, 0, 1); EXPECT_EQ(r0.sourceIds, std::vector({"a"})); EXPECT_EQ(r2.sourceIds, r0.sourceIds); EXPECT_TRUE(sourceLess.sourceIds.empty()); RenderSegmentRevisionBook book; book.advanceSource("a"); book.advanceSource("b"); const auto beforeA = book.revisionFor(r0); const auto beforeB = book.revisionFor(r1); const auto beforeA2 = book.revisionFor(r2); const auto beforeSourceLess = book.revisionFor(sourceLess); book.advanceSource("a"); EXPECT_NE(book.revisionFor(r0), beforeA); EXPECT_NE(book.revisionFor(r2), beforeA2); EXPECT_EQ(book.revisionFor(r1), beforeB); EXPECT_EQ(book.revisionFor(sourceLess), beforeSourceLess); const auto beforeRecreate = book.revisionFor(r0); book.advanceSource("a"); // removal book.advanceSource("a"); // recreation with the same ID EXPECT_NE(book.revisionFor(r0), beforeRecreate); book.advanceGlobal(); // image, glyph, style, context or unknown mutation EXPECT_NE(book.revisionFor(r1), beforeB); EXPECT_NE(book.revisionFor(sourceLess), beforeSourceLess); const auto unknown = renderSegmentDependencies( {{0, "custom", "custom", true, std::nullopt, std::nullopt}}, 0, 1); EXPECT_TRUE(unknown.unknown); EXPECT_FALSE(book.revisionFor(unknown)); const auto symbol = renderSegmentDependencies( {{0, "label", "symbol", true, "a", std::nullopt}}, 0, 1); EXPECT_FALSE(symbol.unknown); EXPECT_EQ(symbol.sourceIds, std::vector({"a"})); EXPECT_EQ(symbol.globalPrerequisites, std::vector({"symbol/glyph/image/placement"})); } TEST(RenderSegmentLocality, ReadinessAndFadeFollowOnlyDependencies) { const RenderSegmentDependencies r0{{"static"}, {}, false}; const RenderSegmentDependencies r1{{"dynamic"}, {}, false}; const auto lookup = [](bool dynamicLoaded, bool dynamicFading) { return [=](const std::string& id) -> std::optional { if (id == "static") return RenderSegmentSourceStatus{true, false}; if (id == "dynamic") return RenderSegmentSourceStatus{dynamicLoaded, dynamicFading}; return std::nullopt; }; }; EXPECT_TRUE(assessRenderSegmentSources(r0, lookup(false, false)).stable); EXPECT_FALSE(assessRenderSegmentSources(r1, lookup(false, false)).ready); EXPECT_TRUE(assessRenderSegmentSources(r0, lookup(true, true)).stable); const auto fading = assessRenderSegmentSources(r1, lookup(true, true)); EXPECT_TRUE(fading.ready); EXPECT_FALSE(fading.stable); EXPECT_STREQ(fading.reason, "source-fading"); EXPECT_FALSE(assessRenderSegmentSources({{"missing"}, {}, false}, lookup(true, false)).ready); EXPECT_FALSE(assessRenderSegmentSources({{}, {}, true}, lookup(true, false)).ready); } TEST(RenderSegmentLocality, MixedExecutionDrawsEachLiveRangeOnceInStyleOrder) { ResolvedRenderSegmentPlan plan; plan.valid = true; plan.layerCount = 5; plan.segments = {{0, RenderSegmentStrategy::Retained, std::nullopt, 0, 1}, {1, RenderSegmentStrategy::Continuous, std::nullopt, 1, 3}, {2, RenderSegmentStrategy::Retained, std::nullopt, 3, 5}}; std::vector events; visitRenderSegmentExecution(plan, [](uint32_t index) { return index == 0; }, [&](uint32_t index) { events.push_back("composite:" + std::to_string(index)); }, [&](int32_t begin, int32_t end) { for (auto i = begin; i < end; ++i) events.push_back("live:" + std::to_string(i)); }); EXPECT_EQ(events, (std::vector{"composite:0", "live:1", "live:2", "live:3", "live:4"})); events.clear(); plan.valid = false; visitRenderSegmentExecution(plan, [](uint32_t) { return true; }, [&](uint32_t) { ADD_FAILURE() << "invalid plan composite"; }, [&](int32_t begin, int32_t end) { for (auto i = begin; i < end; ++i) events.push_back("live:" + std::to_string(i)); }); EXPECT_EQ(events.size(), 5u); } TEST(RenderSegmentLocality, FailedBuildCannotReuseOldTexture) { RetainedUnderlayCache cache; const auto viewportKey = key(); EXPECT_EQ(cache.evaluate(viewportKey, 7, true), RetainedUnderlayDecision::Build); cache.markBuildSucceeded(); EXPECT_EQ(cache.evaluate(viewportKey, 7, true), RetainedUnderlayDecision::Reuse); EXPECT_EQ(cache.evaluate(viewportKey, 8, true), RetainedUnderlayDecision::Build); cache.markBuildFailed("resource-or-compositor"); EXPECT_FALSE(cache.isValid()); EXPECT_EQ(cache.evaluate(viewportKey, 8, true), RetainedUnderlayDecision::Build); } 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); }