feat(step102): isolate placement-free retained segments

This commit is contained in:
tei
2026-10-08 09:59:58 +09:00
parent 6df21dac38
commit 0ce551ad37
14 changed files with 922 additions and 27 deletions

View File

@@ -1157,6 +1157,149 @@ TEST(RenderSegmentLocality, FailedBuildCannotReuseOldTexture) {
EXPECT_EQ(cache.evaluate(viewportKey, 8, true), RetainedUnderlayDecision::Build);
}
TEST(RenderSegmentPlacementFree, StructuralCapabilityAndConservativeGate) {
const std::vector<StyleLayerDiagnostics> layers{
{0, "background", "background", true, std::nullopt, std::nullopt},
{1, "particle", "raster-particle", true, "field", std::nullopt},
{2, "label", "symbol", true, "chart", std::nullopt}};
EXPECT_FALSE(renderSegmentHasPlacementCapableLayer(layers, 0, 1));
EXPECT_TRUE(renderSegmentHasPlacementCapableLayer(layers, 2, 3));
// The symbol's current placementData may be empty; style structure still wins.
EXPECT_TRUE(renderSegmentHasPlacementCapableLayer(layers, 2, 3));
EXPECT_TRUE(renderSegmentHasPlacementCapableLayer(layers, -1, 1));
EXPECT_TRUE(renderSegmentPlacementGatePassed(false, true, 1.0f, true));
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, true, 0.0f, false));
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false, 1.0f, false));
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false, 0.0f, true));
EXPECT_TRUE(renderSegmentPlacementGatePassed(true, false, 0.0f, false));
}
TEST(RenderSegmentPlacementFree, MixedExecutionKeepsFreeRangesInStyleOrder) {
ResolvedRenderSegmentPlan plan;
plan.valid = true;
plan.layerCount = 5;
plan.segments = {{0, RenderSegmentStrategy::Retained, std::nullopt, 0, 1},
{1, RenderSegmentStrategy::Continuous, std::nullopt, 1, 2},
{2, RenderSegmentStrategy::Retained, std::nullopt, 2, 3},
{3, RenderSegmentStrategy::Continuous, std::nullopt, 3, 4},
{4, RenderSegmentStrategy::Retained, std::nullopt, 4, 5}};
RetainedUnderlayCache r0, r1, r2;
const auto k0 = key({640, 480}, 1);
const auto k1 = key({640, 480}, 3);
const auto k2 = key({640, 480}, 5);
EXPECT_EQ(r0.evaluate(k0, 1, true), RetainedUnderlayDecision::Build);
EXPECT_EQ(r1.evaluate(k1, 1, true), RetainedUnderlayDecision::Build);
EXPECT_EQ(r2.evaluate(k2, 1, true), RetainedUnderlayDecision::Build);
r0.markBuildSucceeded(); r1.markBuildSucceeded(); r2.markBuildSucceeded();
const bool freeGate = renderSegmentPlacementGatePassed(false, true, 1.0f, true);
const bool symbolGate = renderSegmentPlacementGatePassed(true, true, 1.0f, true);
EXPECT_EQ(r0.evaluate(k0, 1, freeGate), RetainedUnderlayDecision::Reuse);
EXPECT_EQ(r1.evaluate(k1, 1, symbolGate, "static-placement"), RetainedUnderlayDecision::Fallback);
EXPECT_EQ(r1.getLastInvalidationReason(), "static-placement");
EXPECT_EQ(r2.evaluate(k2, 1, freeGate), RetainedUnderlayDecision::Reuse);
EXPECT_NE(r0.getLastInvalidationReason(), "static-placement");
EXPECT_NE(r2.getLastInvalidationReason(), "static-placement");
std::vector<std::string> events;
visitRenderSegmentExecution(plan,
[](uint32_t index) { return index == 0 || index == 4; },
[&](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<std::string>{"composite:0", "live:1", "live:2",
"live:3", "composite:4"}));
// Source-local mutation/fade in R1 leaves both independent retained caches reusable.
EXPECT_EQ(r0.evaluate(k0, 1, freeGate), RetainedUnderlayDecision::Reuse);
EXPECT_EQ(r2.evaluate(k2, 1, freeGate), RetainedUnderlayDecision::Reuse);
}
TEST(RenderSegmentPlacementFree, RealGlobalGatesStillForceGlobalLive) {
RenderSegmentGlobalLiveGates gates{true, false, false, true, true, true, false};
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "none");
gates.has3D = true;
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "3d");
gates.has3D = false; gates.debugOptions = true;
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "debug-options");
gates.debugOptions = false; gates.continuousMode = false;
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "map-mode");
gates.continuousMode = true; gates.spriteLoaded = false;
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "sprite-not-loaded");
gates.spriteLoaded = true; gates.globalResourcesReady = false;
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "global-resource-not-ready");
gates.globalResourcesReady = true; gates.globalLightTransition = true;
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "global-light-transition");
gates.globalLightTransition = false; gates.retainedMode = false;
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "no-boundary");
}
TEST(RenderSegmentPlacementFree, TopologyAndCameraChangesCannotReuseOldRetainedKey) {
RetainedUnderlayTopologyTracker tracker;
RetainedUnderlayTopologySnapshot topology;
topology.staticPrefix = {{0, "RETAINED:1", 0}, {1, "CONTINUOUS:2", 1},
{2, "RETAINED:3", 0}, {3, "CONTINUOUS:4", 1}};
const auto firstRevision = tracker.observe(topology);
RetainedUnderlayCache cache;
const auto firstKey = key({640, 480}, 3);
EXPECT_EQ(cache.evaluate(firstKey, firstRevision, true), RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
EXPECT_EQ(cache.evaluate(firstKey, firstRevision, true), RetainedUnderlayDecision::Reuse);
topology.staticPrefix.resize(2); // R-C-R-C becomes R-C.
EXPECT_GT(tracker.observe(topology), firstRevision);
EXPECT_EQ(cache.evaluate(firstKey, tracker.current(), true, nullptr, "layer-group-mutation"),
RetainedUnderlayDecision::Build);
EXPECT_FALSE(cache.isValid());
cache.markBuildSucceeded();
EXPECT_EQ(cache.evaluate(key({640, 480}, 4), tracker.current(), true), RetainedUnderlayDecision::Build);
EXPECT_FALSE(cache.isValid());
}
TEST(RenderSegmentPlacementFree, DiagnosticsRecordActualMainBufferAndDoNotMutateOnRead) {
RetainedUnderlayRuntimeState state;
RenderSegmentPlan configured;
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "particle"},
{RenderSegmentStrategy::Retained, "label"}};
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
const std::vector<std::string> ids{"background", "particle", "label"};
const auto plan = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), ids, true);
ASSERT_TRUE(plan.valid);
state.recordRenderSegmentResolution(plan, {
{0, "background", "background", true, std::nullopt, std::nullopt},
{1, "particle", "raster-particle", true, "field", std::nullopt},
{2, "label", "symbol", true, "chart", std::nullopt}});
auto snapshot = state.renderSegmentSnapshot();
ASSERT_EQ(snapshot.segments.size(), 3u);
EXPECT_FALSE(snapshot.segments[0].placementCapable);
EXPECT_TRUE(snapshot.segments[2].placementCapable);
state.recordSegmentExecution(0, true, true, true, 7, "reuse", nullptr);
state.recordMainBufferExecution(snapshot.topologyRevision, true, 1.0f, true, true,
"global-live", "global-light-transition",
{true, true, false}, {"live", "live", "live"});
snapshot = state.renderSegmentSnapshot();
ASSERT_TRUE(snapshot.segments[0].cache);
EXPECT_EQ(snapshot.segments[0].cache->finalDecision, "reuse");
EXPECT_EQ(snapshot.segments[0].actualExecution, "live");
EXPECT_EQ(snapshot.mainBufferExecutionMode, "global-live");
EXPECT_EQ(snapshot.globalLiveFallbackReason, "global-light-transition");
EXPECT_TRUE(snapshot.placementChanged);
EXPECT_FLOAT_EQ(snapshot.symbolFadeChange, 1.0f);
EXPECT_TRUE(snapshot.placementTransitionActive);
EXPECT_TRUE(snapshot.crossSourceCollisions);
state.recordMainBufferExecution(snapshot.topologyRevision, true, 1.0f, true, true,
"mixed", "none", {true, true, false},
{"composite", "live", "live"});
const auto mixed = state.renderSegmentSnapshot();
EXPECT_EQ(mixed.segments[0].actualExecution, "composite");
EXPECT_EQ(mixed.segments[2].actualExecution, "live");
const auto json = renderSegmentDiagnosticsToJSON(mixed);
EXPECT_EQ(renderSegmentDiagnosticsToJSON(state.renderSegmentSnapshot()), json);
EXPECT_NE(json.find("\"placementCapable\":true"), std::string::npos);
EXPECT_NE(json.find("\"placementGatePassed\":false"), std::string::npos);
EXPECT_NE(json.find("\"actualExecution\":\"composite\""), std::string::npos);
EXPECT_NE(json.find("\"mainBufferExecutionMode\":\"mixed\""), std::string::npos);
EXPECT_EQ(state.renderSegmentSnapshot().topologyRevision, mixed.topologyRevision);
}
TEST(RenderSegmentPlan, StrictJSONRejectsUnknownFieldsStrategyAndInvalidFirstBoundary) {
EXPECT_FALSE(parseRenderSegmentPlanJSON(
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED","unexpected":true}]})"));