feat: admit settled Symbol render results to retained cache
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-10-11 16:47:21 +09:00
parent 869930046b
commit 3d989a00a8
26 changed files with 1534 additions and 104 deletions

View File

@@ -10,8 +10,10 @@
#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>
@@ -435,3 +437,137 @@ TEST(Step117Safety, RealAsyncCallbacksAndContextStyleCleanupAdvanceContentEpoch)
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));
}

View File

@@ -5,11 +5,18 @@
#include <mln/renderer/render_segment_plan.hpp>
#include <mln/renderer/update_parameters.hpp>
#include <mln/text/placement.hpp>
#include <mln/renderer/symbol_render_result.hpp>
#include <mln/renderer/symbol_render_inputs.hpp>
#include <mln/renderer/buckets/symbol_bucket.hpp>
#include <mln/renderer/symbol_shader_provenance.hpp>
#include <mln/shaders/symbol_layer_ubo.hpp>
#include <gtest/gtest.h>
#include <algorithm>
#include <initializer_list>
#include <limits>
#include <stdexcept>
#include <utility>
#include <vector>
@@ -98,6 +105,265 @@ std::shared_ptr<UpdateParameters> step117PlacementParameters(TimePoint now,
}
} // namespace
namespace {
SymbolResultBinding step119Binding() {
SymbolResultBinding binding;
binding.viewport = {640, 480};
binding.camera = key().cameraMatrix;
binding.content = binding.topology = binding.config = 1;
return binding;
}
SymbolRenderResult step119BufferResult(const SymbolBucket::Buffer& buffer, float fade) {
SymbolRenderResult result;
const auto& dynamic = buffer.dynamicAttributeData();
const auto& opacity = buffer.opacityAttributeData();
result.appendBlock(dynamic.getRawData(), dynamic.bytes());
result.appendBlock(opacity.getRawData(), opacity.bytes());
for (const auto& vertex : opacity.vector()) {
result.complete = result.complete && symbolOpacityIsSettled(vertex.a1[0], fade);
}
return result;
}
void step119Settle(SymbolResultCertificate& certificate, const SymbolRenderResult& result,
const SymbolResultBinding& binding) {
EXPECT_FALSE(certificate.observeFull(result, binding, true));
certificate.completeFull(binding, true, false);
EXPECT_TRUE(certificate.observeFull(result, binding, true));
EXPECT_FALSE(certificate.candidateHasBuild(binding));
certificate.completeFull(binding, true, true);
EXPECT_TRUE(certificate.canReuse(binding));
EXPECT_FALSE(certificate.canFast(binding));
EXPECT_TRUE(certificate.observeFull(result, binding, true));
EXPECT_TRUE(certificate.candidateHasBuild(binding));
certificate.completeFull(binding, true, false, true);
EXPECT_TRUE(certificate.canFast(binding));
}
} // namespace
TEST(Step119SymbolResults, FreshSettledBuildReuseWithRealControllerAndBuffer) {
const auto start = TimePoint{} + Seconds(10);
PlacementController controller;
SymbolBucket::Buffer buffer;
buffer.dynamicAttributeData().emplace_back(SymbolBucket::dynamicLayoutAttributes({20, 30}, 0));
buffer.opacityAttributeData().emplace_back(SymbolBucket::opacityAttributes(true, 1));
SymbolResultCertificate certificate;
RetainedUnderlayCache cache;
const auto binding = step119Binding();
unsigned builds = 0, reuses = 0, calculations = 0;
for (unsigned frame = 0; frame < 50; ++frame) {
const auto now = start + Milliseconds(frame * 30);
if (!controller.placementIsRecent(now, 0)) {
auto placement = Placement::create(step117PlacementParameters(now), controller.getPlacement());
placement->placeLayers({});
controller.setPlacement(std::move(placement));
++calculations;
} else {
controller.setPlacementStale();
EXPECT_TRUE(controller.hasTransitions(now)); // scheduling preserved
EXPECT_FALSE(controller.hasActiveSymbolFade(now));
}
const auto result = step119BufferResult(buffer, controller.getPlacement()->symbolFadeChange(now));
const bool stable = certificate.observeFull(result, binding, !controller.hasActiveSymbolFade(now));
const auto decision = cache.evaluate(key(), certificate.current(), stable, "static-placement");
EXPECT_FALSE(certificate.canFast(binding)); // Full still in flight
if (decision == RetainedUnderlayDecision::Build) { cache.markBuildSucceeded(); ++builds; }
if (decision == RetainedUnderlayDecision::Reuse) ++reuses;
certificate.completeFull(binding, true, decision == RetainedUnderlayDecision::Build,
decision == RetainedUnderlayDecision::Reuse);
}
EXPECT_GT(calculations, 1u);
EXPECT_EQ(builds, 1u);
EXPECT_GT(reuses, 0u);
EXPECT_TRUE(certificate.canFast(binding));
// Cache submission here is a state-machine fixture, not a GPU draw proof.
}
TEST(Step119SymbolResults, ActualPackedOpacityRejectsFadeAndAcceptsShaderEndpoints) {
for (unsigned opacity = 0; opacity <= 127; ++opacity) {
const float packedIn = static_cast<float>(opacity * 2 + 1);
const float packedOut = static_cast<float>(opacity * 2);
EXPECT_EQ(symbolOpacityIsSettled(packedIn, 0), opacity == 127);
EXPECT_EQ(symbolOpacityIsSettled(packedOut, 0), opacity == 0);
EXPECT_TRUE(symbolOpacityIsSettled(packedIn, 1));
EXPECT_TRUE(symbolOpacityIsSettled(packedOut, 1));
EXPECT_TRUE(symbolOpacityIsSettled(packedIn, 5));
}
EXPECT_FALSE(symbolOpacityIsSettled(1, 0.5f));
EXPECT_FALSE(symbolOpacityIsSettled(254, 0.5f));
EXPECT_FALSE(symbolOpacityIsSettled(1.5f, 1));
EXPECT_FALSE(symbolOpacityIsSettled(256, 1));
EXPECT_FALSE(symbolOpacityIsSettled(1, std::numeric_limits<float>::infinity()));
EXPECT_FALSE(symbolOpacityIsSettled(1, std::numeric_limits<float>::quiet_NaN()));
EXPECT_FALSE(symbolOpacityIsSettled(255, -1));
SymbolBucket::Buffer buffer;
buffer.opacityAttributeData().emplace_back(SymbolBucket::opacityAttributes(true, 0));
SymbolResultCertificate certificate;
const auto binding = step119Binding();
const auto fading = step119BufferResult(buffer, 0.5f);
ASSERT_FALSE(fading.complete);
EXPECT_FALSE(certificate.observeFull(fading, binding, true));
certificate.completeFull(binding, true, true);
EXPECT_FALSE(certificate.canReuse(binding));
}
TEST(Step119SymbolResults, SamePlacedBooleanDoesNotProveVariableAnchorOrOrientation) {
SymbolBucket::Buffer buffer;
buffer.dynamicAttributeData().emplace_back(SymbolBucket::dynamicLayoutAttributes({20, 30}, 0));
buffer.opacityAttributeData().emplace_back(SymbolBucket::opacityAttributes(true, 1));
const auto binding = step119Binding();
for (const auto changed : {SymbolBucket::dynamicLayoutAttributes({21, 30}, 0),
SymbolBucket::dynamicLayoutAttributes({20, 30}, 1)}) {
buffer.dynamicAttributeData().at(0) = SymbolBucket::dynamicLayoutAttributes({20, 30}, 0);
SymbolResultCertificate certificate;
step119Settle(certificate, step119BufferResult(buffer, 1), binding);
buffer.dynamicAttributeData().at(0) = changed;
EXPECT_FALSE(certificate.observeFull(step119BufferResult(buffer, 1), binding, true));
EXPECT_FALSE(certificate.canFast(binding));
}
}
TEST(Step119SymbolResults, CameraViewportPixelRatioAndCollisionConfigurationInvalidate) {
SymbolRenderResult result;
result.append(255.0f);
const auto original = step119Binding();
std::vector<SymbolResultBinding> updates(7, original);
updates[0].camera[12] += 1; // pan
updates[1].camera[0] *= 2; // zoom
updates[2].camera[1] = 0.5; // rotation/pitch projection
updates[3].viewport.width += 1;
updates[4].pixelRatio = 2;
updates[5].crossSourceCollisions = false;
updates[6].config += 1;
for (const auto& update : updates) {
SymbolResultCertificate certificate;
step119Settle(certificate, result, original);
EXPECT_FALSE(certificate.canFast(update));
EXPECT_FALSE(certificate.observeFull(result, update, true));
}
CollisionGroups shared(true), isolated(false);
EXPECT_EQ(shared.get("A").first, shared.get("B").first);
EXPECT_NE(isolated.get("A").first, isolated.get("B").first);
}
TEST(Step119SymbolResults, FailedUploadDrawSubmissionAndUnreadableResultsCannotCertify) {
SymbolRenderResult result;
result.append(255.0f);
const auto binding = step119Binding();
SymbolResultCertificate certificate;
EXPECT_FALSE(certificate.observeFull(result, binding, true));
certificate.completeFull(binding, false, false);
EXPECT_FALSE(certificate.observeFull(result, binding, true));
certificate.completeFull(binding, true, false);
EXPECT_TRUE(certificate.observeFull(result, binding, true));
certificate.completeFull(binding, true, false); // resource/compositor failed
EXPECT_FALSE(certificate.canReuse(binding));
result.complete = false;
EXPECT_FALSE(certificate.observeFull(result, binding, true));
certificate.completeFull(binding, true, true);
EXPECT_FALSE(certificate.canFast(binding));
SymbolRenderResult oversize;
oversize.appendBytes(nullptr, SymbolRenderResult::maxBytes + 1);
EXPECT_FALSE(oversize.complete);
auto attributes = std::make_shared<gfx::VertexAttributeArray>();
const auto& attribute = attributes->set(shaders::idSymbolFadeOpacityAttribute);
ASSERT_TRUE(attribute);
auto opacity = std::make_shared<SymbolBucket::OpacityAttributeVector>();
opacity->emplace_back(SymbolBucket::opacityAttributes(true, 1));
attribute->setSharedRawData(opacity, 0, 0, sizeof(SymbolOpacityAttributes), gfx::AttributeDataType::Float);
SymbolRenderResult notUploaded;
appendSymbolAttributes(notUploaded, attributes);
EXPECT_FALSE(notUploaded.complete); // real CPU vector without a GPU buffer
opacity->release();
SymbolRenderResult releasedWithoutBacking;
appendSymbolAttributes(releasedWithoutBacking, attributes);
EXPECT_FALSE(releasedWithoutBacking.complete);
}
TEST(Step119SymbolResults, ExactPayloadAndAtlasIdentityRequireNewResultRevision) {
const auto binding = step119Binding();
SymbolRenderResult original;
original.append(255.0f);
original.append(0.0f); // uniform / draw ordering test payload
for (unsigned component = 0; component < original.bytes.size(); ++component) {
SymbolResultCertificate certificate;
step119Settle(certificate, original, binding);
auto changed = original;
changed.bytes[component] ^= 1;
const auto revision = certificate.current();
EXPECT_FALSE(certificate.observeFull(changed, binding, true));
EXPECT_FALSE(certificate.candidateHasBuild(binding));
EXPECT_GT(certificate.current(), revision);
EXPECT_FALSE(certificate.matchesCandidate(original));
}
// Shader/texture identities are kept alive as part of the exact evidence.
auto oldAtlas = std::make_shared<unsigned>(1), newAtlas = std::make_shared<unsigned>(1);
SymbolRenderResult oldResult, newResult;
oldResult.retain(oldAtlas);
newResult.retain(newAtlas);
SymbolResultCertificate certificate;
step119Settle(certificate, oldResult, binding);
EXPECT_FALSE(certificate.observeFull(newResult, binding, true));
// Payload coverage is not a texture readback or real wrap/collision draw.
}
TEST(Step119SymbolResults, ActualUniformAndWrappedTileInputsDifferEvenWithSamePlacement) {
shaders::SymbolDrawableUBO uniform{};
uniform.matrix[0] = 1;
uniform.opacity_t = 0.5;
const auto payload = [](const shaders::SymbolDrawableUBO& ubo, const OverscaledTileID& tile) {
SymbolRenderResult result;
result.appendBytes(&ubo, offsetof(shaders::SymbolDrawableUBO, halo_blur_t) + sizeof(float));
const auto name = util::toString(tile);
result.appendBlock(name.data(), name.size());
return result;
};
const auto binding = step119Binding();
const OverscaledTileID tile{2, 0, {2, 1, 1}}, wrapped{2, 1, {2, 1, 1}};
const auto original = payload(uniform, tile);
SymbolResultCertificate certificate;
step119Settle(certificate, original, binding);
EXPECT_FALSE(certificate.matchesCandidate(payload(uniform, wrapped)));
uniform.opacity_t = 0.75;
EXPECT_FALSE(certificate.observeFull(payload(uniform, tile), binding, true));
EXPECT_FALSE(certificate.canFast(binding));
}
TEST(Step119SymbolResults, AnnotationGateBlocksOtherwiseCertifiedResultAndKeeps24GateAgreement) {
SymbolRenderResult result;
result.append(255.0f);
const auto binding = step119Binding();
SymbolResultCertificate certificate;
step119Settle(certificate, result, binding);
auto input = fastPathInput();
input.staticStable = certificate.canFast(binding);
ASSERT_TRUE(evaluateParticleFastPath(input).candidate);
input.annotationMutation = true;
EXPECT_FALSE(evaluateParticleFastPath(input).candidate);
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::AnnotationMutation);
EXPECT_EQ(particleFastGateFailureMask(input), 1u << 23);
}
namespace {
class Step119UnknownSymbolProgram final : public gfx::ShaderProgramBase {
public:
const std::string_view typeName() const noexcept override { return "GenericGLShader"; }
std::optional<size_t> getSamplerLocation(size_t) const override { return std::nullopt; }
const gfx::VertexAttributeArray& getVertexAttributes() const override { throw std::logic_error("unknown shader"); }
const gfx::VertexAttributeArray& getInstanceAttributes() const override { throw std::logic_error("unknown shader"); }
};
} // namespace
TEST(Step119SymbolResults, UnknownShaderCannotBorrowNativeFadeContractByName) {
Step119UnknownSymbolProgram replacement;
EXPECT_FALSE(SymbolShaderProvenance::get().supports(replacement));
EXPECT_TRUE(symbolUniformDefine("HAS_UNIFORM_u_opacity"));
EXPECT_TRUE(symbolUniformDefine("HAS_UNIFORM_u_sorted_instance"));
EXPECT_FALSE(symbolUniformDefine("HAS_UNIFORM_u_opacity 1\n#define main custom"));
EXPECT_FALSE(symbolUniformDefine("HAS_UNIFORM_u_custom_animation"));
}
TEST(RetainedDynamicBoundary, SelectsFirstCandidate) {
RetainedDynamicBoundaryTracker none;
none.consider(0, false);