Files
MapLibreParticleLab/test/renderer/render_orchestrator_error.test.cpp
tei 3d989a00a8
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
feat: admit settled Symbol render results to retained cache
2026-10-11 16:47:21 +09:00

574 lines
27 KiB
C++

#include <mln/actor/scheduler.hpp>
#include <mln/gfx/uniform_buffer.hpp>
#include <mln/renderer/layer_group.hpp>
#include <mln/renderer/change_request.hpp>
#include <mln/renderer/render_orchestrator.hpp>
#include <mln/renderer/renderer_observer.hpp>
#include <mln/renderer/retained_underlay_cache.hpp>
#include <mln/renderer/particle_fast_path.hpp>
#include <mln/renderer/update_parameters.hpp>
#include <mln/renderer/render_source.hpp>
#include <mln/renderer/render_source_observer.hpp>
#include <mln/renderer/tile_render_data.hpp>
#include <mln/renderer/symbol_render_result.hpp>
#include <mln/style/sources/geojson_source_impl.hpp>
#include <mln/style/layers/symbol_layer.hpp>
#include <mln/style/layers/background_layer.hpp>
#include <mln/tile/tile.hpp>
#include <mln/tile/tile_observer.hpp>
#include <gtest/gtest.h>
#include <atomic>
#include <initializer_list>
#include <stdexcept>
using namespace mln;
namespace mln {
struct RenderOrchestratorTeardownTestAccess {
static void add(RenderOrchestrator& orchestrator, std::unique_ptr<RenderSource> 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<gfx::UniformBuffer> 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<TestLayerGroup>(0)));
ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared<TestLayerGroup>(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<TestLayerGroup>(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<TileRenderData> createRenderData() override { return nullptr; }
void cancel() override {}
bool layerPropertiesUpdated(const Immutable<style::LayerProperties>&) 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<int>& deferred_, std::atomic<int>& renderJobs_,
std::atomic<int>& detached_)
: RenderSource(makeMutable<style::GeoJSONSource::Impl>(
"step101-source", style::GeoJSONOptions::defaultOptions())),
scheduler(scheduler_), deferred(deferred_), renderJobs(renderJobs_), detached(detached_),
tile(std::make_unique<TeardownTile>(static_cast<TileObserver*>(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<style::Source::Impl>, const std::vector<Immutable<style::LayerProperties>>&,
bool, bool, const TileParameters&) override {}
std::unique_ptr<RenderItem> 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<std::string, std::vector<Feature>> queryRenderedFeatures(
const ScreenLineString&, const TransformState&,
const std::unordered_map<std::string, const RenderLayer*>&,
const RenderedQueryOptions&, const mat4&) const override { return {}; }
std::vector<Feature> querySourceFeatures(const SourceQueryOptions&) const override { return {}; }
void reduceMemoryUse() override {}
void dumpDebugLogs() const override {}
private:
TaggedScheduler scheduler;
std::atomic<int>& deferred;
std::atomic<int>& renderJobs;
std::atomic<int>& detached;
RenderSourceObserver* oldObserver = nullptr;
std::unique_ptr<TeardownTile> 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<int> deferred{0};
std::atomic<int> renderJobs{0};
std::atomic<int> detached{0};
auto source = std::make_unique<TeardownSource>(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<int> deferred{0};
std::atomic<int> renderJobs{0};
std::atomic<int> detached{0};
for (int iteration = 0; iteration < 20; ++iteration) {
auto orchestrator = std::make_unique<RenderOrchestrator>(false, scheduler, std::nullopt);
auto source = std::make_unique<TeardownSource>(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<UpdateParameters> step117TreeParameters(
const Immutable<std::vector<Immutable<style::Layer::Impl>>>& layers,
const Immutable<std::vector<Immutable<style::Source::Impl>>>& sources,
const std::shared_ptr<RetainedUnderlayRuntimeState>& 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>(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<std::vector<Immutable<style::Image::Impl>>>(),
.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<RetainedUnderlayRuntimeState>();
ASSERT_TRUE(runtime->setRenderSegmentPlan({1, {{RenderSegmentStrategy::Retained, std::nullopt}}}));
style::SymbolLayer symbol("step117-symbol", "step117-source");
const Immutable<std::vector<Immutable<style::Layer::Impl>>> layers =
makeMutable<std::vector<Immutable<style::Layer::Impl>>>(
std::initializer_list<Immutable<style::Layer::Impl>>{symbol.baseImpl});
const Immutable<std::vector<Immutable<style::Source::Impl>>> absent =
makeMutable<std::vector<Immutable<style::Source::Impl>>>();
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<std::vector<Immutable<style::Source::Impl>>> {
return makeMutable<std::vector<Immutable<style::Source::Impl>>>(
std::initializer_list<Immutable<style::Source::Impl>>{
makeMutable<style::GeoJSONSource::Impl>("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<TestLayerGroup>(2);
auto collisionGroup = std::make_shared<TestLayerGroup>(2);
ASSERT_TRUE(orchestrator.addLayerGroup(symbolGroup));
ASSERT_TRUE(orchestrator.addLayerGroup(collisionGroup));
UniqueChangeRequestVec changes;
changes.emplace_back(std::make_unique<RemoveLayerGroupRequest>(symbolGroup));
changes.emplace_back(std::make_unique<RemoveLayerGroupRequest>(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<int> deferred{0};
std::atomic<int> renderJobs{0};
std::atomic<int> detached{0};
auto source = std::make_unique<TeardownSource>(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<RenderSourceObserver&>(orchestrator).onTileChanged(*source, OverscaledTileID{0, 0, {0, 0, 0}});
}, RetainedUnderlayReason::TileMutation);
changesEpoch([&] {
static_cast<GlyphManagerObserver&>(orchestrator).onGlyphsLoaded(FontStack{}, GlyphRange{0, 255});
}, RetainedUnderlayReason::GlyphMutation);
changesEpoch([&] {
static_cast<ImageManagerObserver&>(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<int> deferred{0}, jobs{0}, detached{0};
auto source = std::make_unique<TeardownSource>(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<RenderSourceObserver&>(orchestrator).onTileChanged(*source, OverscaledTileID{2, 1, {2, 1, 1}});
});
rejects([&] { static_cast<GlyphManagerObserver&>(orchestrator).onGlyphsLoaded({}, {0, 255}); });
rejects([&] { static_cast<ImageManagerObserver&>(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<RetainedUnderlayRuntimeState>();
style::BackgroundLayer lower("step119-lower"), continuous("step119-middle"), upper("step119-upper");
const Immutable<std::vector<Immutable<style::Layer::Impl>>> layers =
makeMutable<std::vector<Immutable<style::Layer::Impl>>>(
std::initializer_list<Immutable<style::Layer::Impl>>{lower.baseImpl, continuous.baseImpl, upper.baseImpl});
const Immutable<std::vector<Immutable<style::Source::Impl>>> sources =
makeMutable<std::vector<Immutable<style::Source::Impl>>>();
auto tree = orchestrator.createRenderTree(step117TreeParameters(layers, sources, runtime, TimePoint{}), nullptr);
ASSERT_TRUE(tree);
orchestrator.processChanges();
auto group = std::make_shared<TestLayerGroup>(0);
ASSERT_TRUE(orchestrator.addLayerGroup(group));
ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared<TestLayerGroup>(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<RemoveLayerGroupRequest>(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<TestLayerGroup>(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<RetainedUnderlayRuntimeState>();
ASSERT_TRUE(runtime->setRenderSegmentPlan({1, {{RenderSegmentStrategy::Retained, std::nullopt}}}));
style::SymbolLayer symbol("step119-symbol", "step119-source");
const Immutable<std::vector<Immutable<style::Layer::Impl>>> layers =
makeMutable<std::vector<Immutable<style::Layer::Impl>>>(
std::initializer_list<Immutable<style::Layer::Impl>>{symbol.baseImpl});
const auto sourceList = []() -> Immutable<std::vector<Immutable<style::Source::Impl>>> {
return makeMutable<std::vector<Immutable<style::Source::Impl>>>(
std::initializer_list<Immutable<style::Source::Impl>>{
makeMutable<style::GeoJSONSource::Impl>("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<std::vector<Immutable<style::Source::Impl>>> absent =
makeMutable<std::vector<Immutable<style::Source::Impl>>>();
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));
}