test: record STEP-117 PBF R cache admission blockers
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-10-11 13:03:47 +09:00
parent 43e5f5b915
commit 696899e113
8 changed files with 827 additions and 0 deletions

View File

@@ -3,6 +3,8 @@
#include <mln/renderer/continuous_layer_range.hpp>
#include <mln/renderer/render_segment_json.hpp>
#include <mln/renderer/render_segment_plan.hpp>
#include <mln/renderer/update_parameters.hpp>
#include <mln/text/placement.hpp>
#include <gtest/gtest.h>
@@ -66,6 +68,33 @@ ParticleFastPathGateInput fastPathInput() {
.annotationMutation = false,
};
}
std::shared_ptr<UpdateParameters> step117PlacementParameters(TimePoint now,
style::TransitionOptions transitions = {}) {
TransformState state;
state.setSize({640, 480});
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 = transitions,
.light = style::Light{}.impl,
.images = makeMutable<std::vector<Immutable<style::Image::Impl>>>(),
.sources = makeMutable<std::vector<Immutable<style::Source::Impl>>>(),
.layers = makeMutable<std::vector<Immutable<style::Layer::Impl>>>(),
.annotationManager = {},
.fileSource = {},
.prefetchZoomDelta = 0,
.stillImageRequest = false,
.crossSourceCollisions = true,
});
}
} // namespace
TEST(RetainedDynamicBoundary, SelectsFirstCandidate) {
@@ -1305,6 +1334,137 @@ TEST(Step116Safety, AnnotationMutationAndLayerOrderDisableReloadKeepAllShadowFai
EXPECT_EQ(particleFastGateFailureMask(unmodified), 0u); // predicate-only; no rendered Fast claim
}
TEST(Step117Safety, RealPlacementControllerHasNoFirstSymbolBuildWindow) {
PlacementController controller;
RetainedUnderlayCache cache;
unsigned recomputed = 0;
unsigned recent = 0;
const auto start = TimePoint{} + Seconds(10);
for (unsigned frame = 0; frame < 120; ++frame) {
const auto now = start + Milliseconds(frame * 30);
const bool changed = !controller.placementIsRecent(now, 0.0f);
if (changed) {
++recomputed;
auto placement = Placement::create(step117PlacementParameters(now), controller.getPlacement());
// Exercise the real Continuous commit even with no buckets. This is
// a controller lifecycle regression, not a Symbol GPU equivalence test.
placement->placeLayers({});
controller.setPlacement(std::move(placement));
EXPECT_FLOAT_EQ(controller.getPlacement()->symbolFadeChange(now), 0.0f);
EXPECT_FALSE(controller.hasTransitions(now));
} else {
++recent;
controller.setPlacementStale();
EXPECT_TRUE(controller.hasTransitions(now));
EXPECT_GT(controller.getPlacement()->symbolFadeChange(now), 0.0f);
}
const bool supported = renderSegmentPlacementGatePassed(
true, changed, controller.getPlacement()->symbolFadeChange(now), controller.hasTransitions(now));
EXPECT_FALSE(supported);
EXPECT_EQ(cache.evaluate(key(), 1, supported, "static-placement"), RetainedUnderlayDecision::Fallback);
EXPECT_FALSE(cache.isValid());
}
EXPECT_GT(recomputed, 0u);
EXPECT_GT(recent, 0u);
EXPECT_EQ(cache.getBuildCount(), 0u);
EXPECT_EQ(cache.getReuseCount(), 0u);
}
TEST(Step117Safety, RealPlacementDisabledOrZeroDurationIsNotAResultCertificate) {
const auto now = TimePoint{} + Seconds(10);
for (const bool enabled : {false, true}) {
SCOPED_TRACE(enabled);
PlacementController controller;
auto placement = Placement::create(step117PlacementParameters(
now, style::TransitionOptions{Milliseconds(0), std::nullopt, enabled}), controller.getPlacement());
placement->placeLayers({});
controller.setPlacement(std::move(placement));
EXPECT_FLOAT_EQ(controller.getPlacement()->symbolFadeChange(now), 1.0f);
EXPECT_FALSE(controller.hasTransitions(now));
EXPECT_EQ(controller.placementIsRecent(now, 0.0f), enabled);
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, true, 1.0f, controller.hasTransitions(now)));
controller.setPlacementStale();
EXPECT_EQ(controller.hasTransitions(now + Seconds(5)), enabled);
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false,
controller.getPlacement()->symbolFadeChange(now + Seconds(5)), controller.hasTransitions(now + Seconds(5))));
}
}
TEST(Step117Safety, MissingAndSameNameRestorationCannotReuseAnOldCache) {
// Test-only source status; no source is added to the production PBF style.
const RenderSegmentDependencies dependencies{{"step117-source"}, {}, false};
RenderSegmentRevisionBook book;
RetainedUnderlayCache cache;
bool exists = false;
const auto lookup = [&](const std::string&) -> std::optional<RenderSegmentSourceStatus> {
if (exists) return RenderSegmentSourceStatus{true, false};
return std::nullopt;
};
auto missing = assessRenderSegmentSources(dependencies, lookup);
EXPECT_FALSE(book.revisionFor(dependencies));
EXPECT_EQ(cache.evaluate(key(), 0, missing.stable, missing.reason), RetainedUnderlayDecision::Fallback);
EXPECT_EQ(cache.getBuildCount(), 0u);
exists = true;
book.advanceSource("step117-source");
const auto initial = book.revisionFor(dependencies);
ASSERT_TRUE(initial);
EXPECT_EQ(cache.evaluate(key(), *initial, assessRenderSegmentSources(dependencies, lookup).stable),
RetainedUnderlayDecision::Build);
cache.markBuildSucceeded(); // State-machine fixture only; no GPU draw is claimed.
EXPECT_EQ(cache.evaluate(key(), *initial, true), RetainedUnderlayDecision::Reuse);
const auto reuseBefore = cache.getReuseCount();
exists = false;
book.advanceSource("step117-source");
const auto removed = book.revisionFor(dependencies);
ASSERT_TRUE(removed);
missing = assessRenderSegmentSources(dependencies, lookup);
EXPECT_EQ(cache.evaluate(key(), *removed, missing.stable, missing.reason), RetainedUnderlayDecision::Fallback);
EXPECT_FALSE(cache.isValid());
EXPECT_EQ(cache.getBuildRevision(), 0u);
EXPECT_EQ(cache.getReuseCount(), reuseBefore);
exists = true;
book.advanceSource("step117-source");
const auto restored = book.revisionFor(dependencies);
ASSERT_TRUE(restored);
EXPECT_NE(restored, initial);
EXPECT_NE(restored, removed);
EXPECT_EQ(cache.evaluate(key(), *restored, true), RetainedUnderlayDecision::Build);
EXPECT_EQ(cache.getReuseCount(), reuseBefore);
}
TEST(Step117Safety, CameraStyleAnnotationAndContextRequireRebuildBeforeReuse) {
using Mutation = void (*)(RetainedUnderlayRevisionState&);
const std::array<Mutation, 5> mutations{{
[](auto& state) { state.cameraMatrix[12] += 1.0; },
[](auto& state) { state.viewport = {800, 600}; },
[](auto& state) { ++state.contentRevision; }, // style/glyph/image/context epoch
[](auto& state) { ++state.topologyRevision; }, // annotation/layer group
[](auto& state) { ++state.rangeConfigRevision; },
}};
for (std::size_t i = 0; i < mutations.size(); ++i) {
SCOPED_TRACE(i);
RetainedUnderlayRevisionTracker tracker;
RetainedUnderlayCache cache;
auto state = revisionState();
const auto cacheKey = [&] { return RetainedUnderlayKey{state.viewport, 0, *state.boundary, state.cameraMatrix}; };
const auto oldRevision = tracker.observe(state);
ASSERT_EQ(cache.evaluate(cacheKey(), oldRevision, true), RetainedUnderlayDecision::Build);
cache.markBuildSucceeded(); // State-machine fixture only.
mutations[i](state);
const auto newRevision = tracker.observe(state);
EXPECT_NE(newRevision, oldRevision);
EXPECT_EQ(cache.evaluate(cacheKey(), newRevision, true), RetainedUnderlayDecision::Build);
EXPECT_FALSE(cache.isValid());
EXPECT_EQ(cache.getReuseCount(), 0u);
auto input = fastPathInput();
input.cacheValid = cache.isValid();
input.revisionMatches = cache.getBuildRevision() == newRevision;
EXPECT_FALSE(evaluateParticleFastPath(input).candidate);
EXPECT_NE(particleFastGateFailureMask(input) & (1u << 2), 0u);
EXPECT_NE(particleFastGateFailureMask(input) & (1u << 3), 0u);
}
}
TEST(RenderSegmentLocality, MixedExecutionDrawsEachLiveRangeOnceInStyleOrder) {
ResolvedRenderSegmentPlan plan;
plan.valid = true;