#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace mln; namespace mln { struct RenderOrchestratorTeardownTestAccess { static void add(RenderOrchestrator& orchestrator, std::unique_ptr source) { source->setObserver(&orchestrator); orchestrator.renderSources.emplace(source->baseImpl->id, std::move(source)); } static void enqueue(RenderOrchestrator& orchestrator, UniqueChangeRequestVec& changes) { orchestrator.addChanges(changes); } }; } // namespace mln namespace { class TestUniformBuffers final : public gfx::UniformBufferArray { public: void bind(gfx::RenderPass&) override {} protected: std::unique_ptr copy(const gfx::UniformBuffer&) override { return nullptr; } }; class TestLayerGroup final : public LayerGroup { public: explicit TestLayerGroup(int32_t index) : LayerGroup(index, 0, "step097-error-group") {} const gfx::UniformBufferArray& getUniformBuffers() const override { return buffers; } gfx::UniformBufferArray& mutableUniformBuffers() override { return buffers; } private: TestUniformBuffers buffers; }; class ErrorObserver final : public RendererObserver { public: void onRenderError(std::exception_ptr error) override { ++errors; try { std::rethrow_exception(error); } catch (const std::runtime_error& e) { message = e.what(); } } int errors = 0; std::string message; }; } // namespace TEST(RenderOrchestratorError, RangeVisitorReportsOpaqueAndTranslucentFailures) { TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; RenderOrchestrator orchestrator(false, scheduler, std::nullopt); ErrorObserver observer; orchestrator.setObserver(&observer); ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared(0))); ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared(1))); bool failed = false; orchestrator.visitLayerGroupsInRangeReversed(0, 2, [](LayerGroupBase&, size_t) { throw std::runtime_error("opaque failure"); }, &failed); EXPECT_TRUE(failed); EXPECT_EQ(observer.errors, 2); EXPECT_EQ(observer.message, "opaque failure"); failed = false; observer.errors = 0; orchestrator.visitLayerGroupsInRange(0, 2, [](LayerGroupBase&, size_t) { throw std::runtime_error("translucent failure"); }, &failed); EXPECT_TRUE(failed); EXPECT_EQ(observer.errors, 2); EXPECT_EQ(observer.message, "translucent failure"); failed = false; int visits = 0; orchestrator.visitLayerGroupsInRange(0, 2, [&](LayerGroupBase&, size_t) { ++visits; }, &failed); EXPECT_FALSE(failed); EXPECT_EQ(visits, 2); } TEST(RenderOrchestratorError, FailedRangeCannotReuseAndCleanRetryCanRecover) { TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; RenderOrchestrator orchestrator(false, scheduler, std::nullopt); ErrorObserver observer; orchestrator.setObserver(&observer); ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared(0))); RetainedUnderlayCache cache; RetainedUnderlayKey key{{4, 4}, 0, 1, {}}; key.cameraMatrix[0] = key.cameraMatrix[5] = key.cameraMatrix[10] = key.cameraMatrix[15] = 1.0; for (bool opaque : {true, false}) { EXPECT_EQ(cache.evaluate(key, 1, true), RetainedUnderlayDecision::Build); bool failed = false; const auto throwOnRender = [](LayerGroupBase&, size_t) { throw std::runtime_error("render failure"); }; if (opaque) orchestrator.visitLayerGroupsInRangeReversed(0, 1, throwOnRender, &failed); else orchestrator.visitLayerGroupsInRange(0, 1, throwOnRender, &failed); ASSERT_TRUE(failed); cache.markBuildFailed("layer-render-error"); EXPECT_FALSE(cache.isValid()); EXPECT_EQ(cache.evaluate(key, 1, true), RetainedUnderlayDecision::Build); } bool failed = false; orchestrator.visitLayerGroupsInRange(0, 1, [](LayerGroupBase&, size_t) {}, &failed); ASSERT_FALSE(failed); cache.markBuildSucceeded(); EXPECT_EQ(cache.evaluate(key, 1, true), RetainedUnderlayDecision::Reuse); } namespace { class TeardownTile final : public Tile { public: TeardownTile(TileObserver* observer_) : Tile(Kind::Geometry, OverscaledTileID{0, 0, {0, 0, 0}}, "step101-source", observer_) {} ~TeardownTile() override { observer->onTileAction(id, sourceID, TileOperation::Cancelled); } std::unique_ptr createRenderData() override { return nullptr; } void cancel() override {} bool layerPropertiesUpdated(const Immutable&) override { return true; } void emitChanged() { observer->onTileChanged(*this); } void emitError() { observer->onTileError(*this, std::make_exception_ptr(std::runtime_error("tile error"))); } void emitAction() { observer->onTileAction(id, sourceID, TileOperation::Cancelled); } }; class TeardownSource final : public RenderSource { public: TeardownSource(const TaggedScheduler& scheduler_, std::atomic& deferred_, std::atomic& renderJobs_, std::atomic& detached_) : RenderSource(makeMutable( "step101-source", style::GeoJSONOptions::defaultOptions())), scheduler(scheduler_), deferred(deferred_), renderJobs(renderJobs_), detached(detached_), tile(std::make_unique(static_cast(this))) {} ~TeardownSource() override { if (observer != oldObserver) ++detached; tile.reset(); // A pending tile emits Cancelled synchronously during source destruction. scheduler.schedule([&count = deferred] { ++count; }); scheduler.runOnRenderThread([&count = renderJobs] { ++count; }); } void expectDetachFrom(RenderSourceObserver* observer_) { oldObserver = observer_; } void emitAll() { tile->emitChanged(); tile->emitError(); tile->emitAction(); } bool isLoaded() const override { return false; } void update(Immutable, const std::vector>&, bool, bool, const TileParameters&) override {} std::unique_ptr createRenderItem() override { return nullptr; } void prepare(const SourcePrepareParameters&) override {} void updateFadingTiles() override {} bool hasFadingTiles() const override { return false; } RenderSourceReadinessStats getReadinessDiagnostics() const override { return {.tileBacked = true, .tileCount = std::nullopt, .incompleteTileCount = std::nullopt, .loadedTileCount = std::nullopt, .hasFadingTiles = false}; } std::unordered_map> queryRenderedFeatures( const ScreenLineString&, const TransformState&, const std::unordered_map&, const RenderedQueryOptions&, const mat4&) const override { return {}; } std::vector querySourceFeatures(const SourceQueryOptions&) const override { return {}; } void reduceMemoryUse() override {} void dumpDebugLogs() const override {} private: TaggedScheduler scheduler; std::atomic& deferred; std::atomic& renderJobs; std::atomic& detached; RenderSourceObserver* oldObserver = nullptr; std::unique_ptr tile; }; class CountingSourceObserver final : public RenderSourceObserver { public: void onTileChanged(RenderSource&, const OverscaledTileID&) override { ++changed; } void onTileError(RenderSource&, const OverscaledTileID&, std::exception_ptr) override { ++errors; } void onTileAction(RenderSource&, TileOperation, const OverscaledTileID&, const std::string&) override { ++actions; } int changed = 0; int errors = 0; int actions = 0; }; } // namespace TEST(RenderOrchestratorTeardown, NullObserverAndDetachCoverEveryTileCallback) { TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; std::atomic deferred{0}; std::atomic renderJobs{0}; std::atomic detached{0}; auto source = std::make_unique(scheduler, deferred, renderJobs, detached); source->emitAll(); // Constructor default NullObserver. CountingSourceObserver observer; source->setObserver(&observer); source->expectDetachFrom(&observer); source->emitAll(); EXPECT_EQ(observer.changed, 1); EXPECT_EQ(observer.errors, 1); EXPECT_EQ(observer.actions, 1); source->setObserver(nullptr); source->emitAll(); source.reset(); // Cancelled must not reach the detached observer. EXPECT_EQ(detached.load(), 1); EXPECT_EQ(observer.changed, 1); EXPECT_EQ(observer.errors, 1); EXPECT_EQ(observer.actions, 1); scheduler.waitForEmpty(); scheduler.runRenderJobs(); } TEST(RenderOrchestratorTeardown, PendingTileCancellationAndSecondDrainRepeatSafely) { TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; std::atomic deferred{0}; std::atomic renderJobs{0}; std::atomic detached{0}; for (int iteration = 0; iteration < 20; ++iteration) { auto orchestrator = std::make_unique(false, scheduler, std::nullopt); auto source = std::make_unique(scheduler, deferred, renderJobs, detached); source->expectDetachFrom(orchestrator.get()); RenderOrchestratorTeardownTestAccess::add(*orchestrator, std::move(source)); orchestrator.reset(); EXPECT_EQ(detached.load(), iteration + 1); // Source was detached before its tile cancelled. EXPECT_EQ(deferred.load(), iteration + 1); // Work queued by source destruction was drained. scheduler.runRenderJobs(); // Render jobs have a separate backend-owned queue. EXPECT_EQ(renderJobs.load(), iteration + 1); } } namespace { std::shared_ptr step117TreeParameters( const Immutable>>& layers, const Immutable>>& sources, const std::shared_ptr& runtime, TimePoint now) { TransformState state; state.setSize({640, 480}); static const style::Light light; // Keep unrelated light revisions fixed across sourceDiff steps. return std::make_shared(UpdateParameters{ .styleLoaded = true, .mode = MapMode::Continuous, .pixelRatio = 1.0f, .debugOptions = MapDebugOptions::NoDebug, .timePoint = now, .transformState = state, .glyphURL = {}, .fontFaces = {}, .spriteLoaded = true, .transitionOptions = {Milliseconds(0), std::nullopt, false}, .light = light.impl, .images = makeMutable>>(), .sources = sources, .layers = layers, .annotationManager = {}, .fileSource = {}, .prefetchZoomDelta = 0, .stillImageRequest = false, .crossSourceCollisions = true, .retainedUnderlayState = runtime, }); } } // namespace TEST(Step117Safety, RealMissingSourceTreeHasNoRenderItemButStillFailsClosed) { TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; RenderOrchestrator orchestrator(false, scheduler, std::nullopt); auto runtime = std::make_shared(); ASSERT_TRUE(runtime->setRenderSegmentPlan({1, {{RenderSegmentStrategy::Retained, std::nullopt}}})); style::SymbolLayer symbol("step117-symbol", "step117-source"); const Immutable>> layers = makeMutable>>( std::initializer_list>{symbol.baseImpl}); const Immutable>> absent = makeMutable>>(); const auto now = TimePoint{} + Seconds(10); auto tree = orchestrator.createRenderTree(step117TreeParameters(layers, absent, runtime, now), nullptr); ASSERT_TRUE(tree); EXPECT_TRUE(tree->getLayerRenderItemMap().empty()); EXPECT_EQ(orchestrator.numLayerGroups(), 0u); auto missing = orchestrator.retainedSegmentSourceState(0, 1); EXPECT_STREQ(missing.reason, "missing-source"); EXPECT_FALSE(missing.ready); EXPECT_EQ(missing.revision, 0u); EXPECT_TRUE(orchestrator.retainedRangeHasPlacementCapableLayer(0, 1)); const auto fingerprint = orchestrator.retainedRangeContentFingerprint(0, 1); tree.reset(); // Match Renderer: release reference-based render items before source replacement. const auto sourceList = []() -> Immutable>> { return makeMutable>>( std::initializer_list>{ makeMutable("step117-source", style::GeoJSONOptions::defaultOptions())}); }; // An unsubmitted test-only source exercises actual sourceDiff creation and // same-ID replacement. It supplies no fake tide or production data. tree = orchestrator.createRenderTree(step117TreeParameters(layers, sourceList(), runtime, now + Seconds(1)), nullptr); ASSERT_TRUE(tree); ASSERT_EQ(tree->getLayerRenderItemMap().size(), 1u); const auto added = orchestrator.retainedSegmentSourceState(0, 1); EXPECT_STRNE(added.reason, "missing-source"); EXPECT_NE(added.revision, missing.revision); EXPECT_EQ(orchestrator.retainedRangeContentFingerprint(0, 1), fingerprint); EXPECT_FALSE(renderSegmentPlacementGatePassed(true, tree->getParameters().placementChanged, tree->getParameters().symbolFadeChange, orchestrator.retainedPlacementTransitionActive(now + Seconds(1)))); tree.reset(); tree = orchestrator.createRenderTree(step117TreeParameters(layers, sourceList(), runtime, now + Seconds(2)), nullptr); ASSERT_TRUE(tree); const auto replaced = orchestrator.retainedSegmentSourceState(0, 1); EXPECT_NE(replaced.revision, added.revision); tree.reset(); tree = orchestrator.createRenderTree(step117TreeParameters(layers, absent, runtime, now + Seconds(3)), nullptr); ASSERT_TRUE(tree); EXPECT_TRUE(tree->getLayerRenderItemMap().empty()); missing = orchestrator.retainedSegmentSourceState(0, 1); EXPECT_STREQ(missing.reason, "missing-source"); EXPECT_FALSE(missing.ready); // Source tombstone does not supply readiness. EXPECT_EQ(missing.revision, 0u); // No Renderer/backend draw is invoked; zero RenderItems is not a GPU certificate. } TEST(Step117Safety, QueuedGroupRemovalIsNotAppliedUntilProcessChanges) { TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; RenderOrchestrator orchestrator(false, scheduler, std::nullopt); auto symbolGroup = std::make_shared(2); auto collisionGroup = std::make_shared(2); ASSERT_TRUE(orchestrator.addLayerGroup(symbolGroup)); ASSERT_TRUE(orchestrator.addLayerGroup(collisionGroup)); UniqueChangeRequestVec changes; changes.emplace_back(std::make_unique(symbolGroup)); changes.emplace_back(std::make_unique(collisionGroup)); RenderOrchestratorTeardownTestAccess::enqueue(orchestrator, changes); EXPECT_TRUE(changes.empty()); EXPECT_TRUE(orchestrator.particleFastPathDynamicState(TimePoint{}).pendingChanges); unsigned visits = 0; orchestrator.visitLayerGroupsInRange(0, 63, [&](LayerGroupBase&, size_t) { ++visits; }); EXPECT_EQ(visits, 2u); orchestrator.processChanges(); EXPECT_FALSE(orchestrator.particleFastPathDynamicState(TimePoint{}).pendingChanges); visits = 0; orchestrator.visitLayerGroupsInRange(0, 63, [&](LayerGroupBase&, size_t) { ++visits; }); EXPECT_EQ(visits, 0u); EXPECT_EQ(orchestrator.numLayerGroups(), 0u); // This is real queue/registry coverage with test groups, not actual GPU draws. } TEST(Step117Safety, RealAsyncCallbacksAndContextStyleCleanupAdvanceContentEpoch) { TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; RenderOrchestrator orchestrator(false, scheduler, std::nullopt); std::atomic deferred{0}; std::atomic renderJobs{0}; std::atomic detached{0}; auto source = std::make_unique(scheduler, deferred, renderJobs, detached); source->setObserver(&orchestrator); const auto changesEpoch = [&](auto event, RetainedUnderlayReason expected) { const auto before = orchestrator.getRetainedUnderlayContentRevision(); event(); EXPECT_GT(orchestrator.getRetainedUnderlayContentRevision(), before); EXPECT_EQ(orchestrator.getRetainedUnderlayContentReason(), expected); ParticleFastPathGateInput input{ .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, }; input.contentRevisionMatches = before == orchestrator.getRetainedUnderlayContentRevision(); EXPECT_EQ(particleFastGateFailureMask(input), 1u << 4); EXPECT_FALSE(evaluateParticleFastPath(input).candidate); EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::ContentMutation); }; changesEpoch([&] { static_cast(orchestrator).onTileChanged(*source, OverscaledTileID{0, 0, {0, 0, 0}}); }, RetainedUnderlayReason::TileMutation); changesEpoch([&] { static_cast(orchestrator).onGlyphsLoaded(FontStack{}, GlyphRange{0, 255}); }, RetainedUnderlayReason::GlyphMutation); changesEpoch([&] { static_cast(orchestrator).onStyleImageMissing("step117-image", [] {}); }, RetainedUnderlayReason::ImageMutation); changesEpoch([&] { orchestrator.markContextLost(); }, RetainedUnderlayReason::ContextReset); changesEpoch([&] { orchestrator.clearData(); }, RetainedUnderlayReason::StyleMutation); source->setObserver(nullptr); source.reset(); scheduler.waitForEmpty(); scheduler.runRenderJobs(); // Actual callback dispatch, but no network delivery, GPU, or Android assertion. } namespace { void step119Certify(SymbolResultCertificate& certificate, SymbolResultBinding binding) { SymbolRenderResult result; result.append(255.0f); EXPECT_FALSE(certificate.observeFull(result, binding, true)); certificate.completeFull(binding, true, false); EXPECT_TRUE(certificate.observeFull(result, binding, true)); certificate.completeFull(binding, true, true); EXPECT_TRUE(certificate.observeFull(result, binding, true)); certificate.completeFull(binding, true, false, true); EXPECT_TRUE(certificate.canFast(binding)); } } // namespace TEST(Step119SymbolResults, RealTileGlyphImageStyleContextCallbacksRejectBuiltCertificate) { TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; RenderOrchestrator orchestrator(false, scheduler, std::nullopt); std::atomic deferred{0}, jobs{0}, detached{0}; auto source = std::make_unique(scheduler, deferred, jobs, detached); const auto rejects = [&](auto event) { SymbolResultBinding binding; binding.content = orchestrator.getRetainedUnderlayContentRevision(); SymbolResultCertificate certificate; step119Certify(certificate, binding); event(); binding.content = orchestrator.getRetainedUnderlayContentRevision(); EXPECT_FALSE(certificate.canFast(binding)); }; rejects([&] { static_cast(orchestrator).onTileChanged(*source, OverscaledTileID{2, 1, {2, 1, 1}}); }); rejects([&] { static_cast(orchestrator).onGlyphsLoaded({}, {0, 255}); }); rejects([&] { static_cast(orchestrator).onStyleImageMissing("step119-icon", [] {}); }); rejects([&] { orchestrator.markContextLost(); }); rejects([&] { orchestrator.clearData(); }); source->setObserver(nullptr); source.reset(); scheduler.waitForEmpty(); scheduler.runRenderJobs(); } TEST(Step119SymbolResults, AppliedCrossSegmentRegistryRebuildChangesExactEvidence) { TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; RenderOrchestrator orchestrator(false, scheduler, std::nullopt); auto runtime = std::make_shared(); style::BackgroundLayer lower("step119-lower"), continuous("step119-middle"), upper("step119-upper"); const Immutable>> layers = makeMutable>>( std::initializer_list>{lower.baseImpl, continuous.baseImpl, upper.baseImpl}); const Immutable>> sources = makeMutable>>(); auto tree = orchestrator.createRenderTree(step117TreeParameters(layers, sources, runtime, TimePoint{}), nullptr); ASSERT_TRUE(tree); orchestrator.processChanges(); auto group = std::make_shared(0); ASSERT_TRUE(orchestrator.addLayerGroup(group)); ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared(2))); const auto before = orchestrator.retainedSymbolRenderResult(2, 3, 1, 0); ASSERT_TRUE(before.complete); SymbolResultCertificate certificate; SymbolResultBinding binding; step119Certify(certificate, binding); UniqueChangeRequestVec changes; changes.emplace_back(std::make_unique(group)); RenderOrchestratorTeardownTestAccess::enqueue(orchestrator, changes); const auto pending = orchestrator.retainedSymbolRenderResult(2, 3, 1, 0); EXPECT_FALSE(pending.complete); EXPECT_FALSE(certificate.observeFull(pending, binding, true)); orchestrator.processChanges(); EXPECT_EQ(orchestrator.numLayerGroups(), 1u); const auto after = orchestrator.retainedSymbolRenderResult(2, 3, 1, 0); EXPECT_TRUE(after.complete); EXPECT_NE(before.bytes, after.bytes); // actual global count + original ordinal changed EXPECT_FALSE(certificate.canFast(binding)); } TEST(Step119SymbolResults, RangeDrawFailureInvalidatesContentAndCompletedEvidence) { TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; RenderOrchestrator orchestrator(false, scheduler, std::nullopt); ErrorObserver observer; orchestrator.setObserver(&observer); ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared(0))); SymbolResultBinding binding; binding.content = orchestrator.getRetainedUnderlayContentRevision(); SymbolResultCertificate certificate; step119Certify(certificate, binding); const auto errors = orchestrator.getRenderErrorRevision(); bool failed = false; orchestrator.visitLayerGroupsInRange(0, 1, [](LayerGroupBase&, size_t) { throw std::runtime_error("step119-upload-or-draw"); }, &failed); EXPECT_TRUE(failed); EXPECT_EQ(observer.errors, 1); EXPECT_GT(orchestrator.getRenderErrorRevision(), errors); binding.content = orchestrator.getRetainedUnderlayContentRevision(); EXPECT_FALSE(certificate.canReuse(binding)); EXPECT_FALSE(certificate.canFast(binding)); } TEST(Step119SymbolResults, RealSourceReplacementAndMissingSourceRetainFailClosed) { TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; RenderOrchestrator orchestrator(false, scheduler, std::nullopt); auto runtime = std::make_shared(); ASSERT_TRUE(runtime->setRenderSegmentPlan({1, {{RenderSegmentStrategy::Retained, std::nullopt}}})); style::SymbolLayer symbol("step119-symbol", "step119-source"); const Immutable>> layers = makeMutable>>( std::initializer_list>{symbol.baseImpl}); const auto sourceList = []() -> Immutable>> { return makeMutable>>( std::initializer_list>{ makeMutable("step119-source", style::GeoJSONOptions::defaultOptions())}); }; auto tree = orchestrator.createRenderTree(step117TreeParameters(layers, sourceList(), runtime, TimePoint{}), nullptr); ASSERT_TRUE(tree); SymbolResultBinding binding; binding.content = orchestrator.getRetainedUnderlayContentRevision(); SymbolResultCertificate certificate; step119Certify(certificate, binding); tree.reset(); tree = orchestrator.createRenderTree(step117TreeParameters(layers, sourceList(), runtime, TimePoint{} + Seconds(1)), nullptr); ASSERT_TRUE(tree); binding.content = orchestrator.getRetainedUnderlayContentRevision(); EXPECT_FALSE(certificate.canFast(binding)); tree.reset(); const Immutable>> absent = makeMutable>>(); tree = orchestrator.createRenderTree(step117TreeParameters(layers, absent, runtime, TimePoint{} + Seconds(2)), nullptr); ASSERT_TRUE(tree); EXPECT_STREQ(orchestrator.retainedSegmentSourceState(0, 1).reason, "missing-source"); binding.content = orchestrator.getRetainedUnderlayContentRevision(); EXPECT_FALSE(certificate.canFast(binding)); }