feat: add generic render segment model
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-09-25 13:55:19 +09:00
parent 8fbb892a12
commit 016fd1fff6
29 changed files with 1409 additions and 162 deletions

View File

@@ -184,6 +184,8 @@ list(APPEND SRC_FILES
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/retained_underlay_cache.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/retained_underlay_cache.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/retained_underlay_runtime_state.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/render_segment_plan.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/render_segment_json.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/change_request.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layer_group.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/tile_layer_group.cpp
@@ -310,6 +312,7 @@ list(APPEND INCLUDE_FILES
${PROJECT_SOURCE_DIR}/include/mbgl/renderer/renderer_state.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/renderer/renderer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/renderer/retained_underlay.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/renderer/render_segment.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/shaders/program_parameters.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/shaders/shader_source.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/storage/database_file_source.hpp

View File

@@ -293,6 +293,8 @@ MLN_CORE_SOURCE = [
"src/mbgl/renderer/render_orchestrator.cpp",
"src/mbgl/renderer/render_orchestrator.hpp",
"src/mbgl/renderer/continuous_layer_range.hpp",
"src/mbgl/renderer/render_segment_plan.hpp",
"src/mbgl/renderer/render_segment_json.hpp",
"src/mbgl/renderer/render_pass.hpp",
"src/mbgl/renderer/render_source.cpp",
"src/mbgl/renderer/render_source.hpp",
@@ -752,6 +754,7 @@ MLN_CORE_HEADERS = [
"include/mbgl/renderer/renderer_observer.hpp",
"include/mbgl/renderer/renderer_state.hpp",
"include/mbgl/renderer/retained_underlay.hpp",
"include/mbgl/renderer/render_segment.hpp",
"include/mbgl/shaders/program_parameters.hpp",
"include/mbgl/storage/database_file_source.hpp",
"include/mbgl/storage/file_source.hpp",

View File

@@ -15,6 +15,7 @@
#include <mbgl/util/client_options.hpp>
#include <mbgl/util/action_journal_options.hpp>
#include <mbgl/renderer/retained_underlay.hpp>
#include <mbgl/renderer/render_segment.hpp>
#include <cstdint>
#include <string>
@@ -59,6 +60,8 @@ public:
/** Enables or disables the map-scoped retained static underlay optimization. */
void setRetainedUnderlayEnabled(bool);
bool setContinuousLayerRange(std::string startLayerId, std::string endLayerId);
bool setRenderSegmentPlan(RenderSegmentPlan);
RenderSegmentDiagnostics getRenderSegmentDiagnostics() const;
bool isRetainedUnderlayEnabled() const;
RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() const;

View File

@@ -0,0 +1,112 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace mbgl {
enum class RenderSegmentStrategy : uint8_t { Retained, Continuous };
inline const char* renderSegmentStrategyText(RenderSegmentStrategy strategy) noexcept {
return strategy == RenderSegmentStrategy::Retained ? "RETAINED" : "CONTINUOUS";
}
struct RenderSegmentDefinition {
RenderSegmentStrategy strategy = RenderSegmentStrategy::Continuous;
std::optional<std::string> startLayerId;
bool operator==(const RenderSegmentDefinition&) const = default;
};
struct RenderSegmentPlan {
uint32_t schemaVersion = 1;
std::vector<RenderSegmentDefinition> segments;
bool operator==(const RenderSegmentPlan&) const = default;
};
struct StyleLayerDiagnostics {
int32_t layerIndex = -1;
std::string layerId;
std::string layerType;
bool visible = false;
std::optional<std::string> sourceId;
std::optional<std::string> sourceLayer;
bool operator==(const StyleLayerDiagnostics&) const = default;
};
struct RenderSegmentCacheDiagnostics {
std::string cacheState{"disabled"};
uint64_t contentRevision = 0;
uint64_t build = 0;
uint64_t reuse = 0;
uint64_t invalidation = 0;
std::string lastInvalidationReason{"none"};
uint64_t textureCreate = 0;
uint64_t compositeCreate = 0;
uint64_t resourceRelease = 0;
};
struct RenderSegmentDiagnosticsEntry {
uint32_t segmentIndex = 0;
RenderSegmentStrategy strategy = RenderSegmentStrategy::Continuous;
std::optional<std::string> configuredStartLayerId;
int32_t beginIndex = -1;
int32_t endExclusive = -1;
std::optional<std::string> resolvedStartLayerId;
std::optional<std::string> resolvedEndLayerId;
std::vector<StyleLayerDiagnostics> layers;
std::optional<RenderSegmentCacheDiagnostics> cache;
};
struct RenderSegmentDiagnostics {
uint32_t schemaVersion = 1;
bool enabled = true;
bool configured = false;
bool valid = false;
std::string configSource{"unconfigured/legacy"};
uint64_t configRevision = 0;
uint64_t topologyRevision = 0;
uint32_t styleLayerCount = 0;
uint32_t segmentCount = 0;
uint32_t retainedSegmentCount = 0;
uint32_t continuousSegmentCount = 0;
bool coverageComplete = false;
bool fallback = false;
std::string lastReason{"not-configured"};
bool fastPathEligible = false;
bool fastPathActive = false;
std::string fastPathFallbackReason{"no-prepared-tree"};
uint64_t fastPathHit = 0;
uint64_t fastPathMiss = 0;
uint64_t fullCpuFrame = 0;
std::vector<StyleLayerDiagnostics> styleLayers;
std::vector<RenderSegmentDiagnosticsEntry> segments;
};
struct ResolvedRenderSegment {
uint32_t segmentIndex = 0;
RenderSegmentStrategy strategy = RenderSegmentStrategy::Continuous;
std::optional<std::string> configuredStartLayerId;
int32_t beginIndex = 0;
int32_t endExclusive = 0;
bool operator==(const ResolvedRenderSegment&) const = default;
};
struct ResolvedRenderSegmentPlan {
bool configured = false;
bool valid = false;
std::string configSource{"unconfigured/legacy"};
uint64_t configRevision = 0;
std::string reason{"not-configured"};
int32_t layerCount = 0;
std::vector<ResolvedRenderSegment> segments;
bool operator==(const ResolvedRenderSegmentPlan&) const = default;
};
} // namespace mbgl

View File

@@ -14,6 +14,7 @@ import {
FieldRuntime,
RendererRuntime,
type RetainedUnderlayDiagnostics,
type RenderSegmentDiagnostics,
type RuntimeSnapshot,
} from "@navsea/maplibre-particle-rn";
import {
@@ -67,6 +68,8 @@ export default function App() {
const [snapshot, setSnapshot] = useState<RuntimeSnapshot>(initialSnapshot);
const [retainedSnapshot, setRetainedSnapshot] =
useState<RetainedUnderlayDiagnostics | null>(null);
const [segmentSnapshot, setSegmentSnapshot] =
useState<RenderSegmentDiagnostics | null>(null);
const [mapStatus, setMapStatus] = useState("LOADING");
const [weatherStatus, setWeatherStatus] = useState("WAITING");
const [result, setResult] = useState("Waiting for map/style");
@@ -91,6 +94,11 @@ export default function App() {
const refreshRetained = useCallback(async (tag: number) => {
setRetainedSnapshot(await RendererRuntime.getRetainedUnderlayDiagnostics(tag));
}, []);
const refreshSegments = useCallback(async (tag: number) => {
const value = await RendererRuntime.getRenderSegmentDiagnostics(tag);
setSegmentSnapshot(value);
setResult(JSON.stringify(value, null, 2));
}, []);
const applyVisualToTag = useCallback(async (tag: number) => {
const results = await Promise.all([
FieldRuntime.setParticleCount(tag, Number(visual.count)),
@@ -384,6 +392,28 @@ export default function App() {
onPress={() => reactTag != null && void refreshRetained(reactTag)}
/>
</View>
<Text selectable>
Segments: {segmentSnapshot == null ? "not read" :
`${segmentSnapshot.valid ? "VALID" : "INVALID"} source=${segmentSnapshot.configSource} count=${segmentSnapshot.segmentCount} topology=${segmentSnapshot.topologyRevision}`}
</Text>
<View style={styles.row}>
<Button
title="Apply C-R plan"
disabled={reactTag == null}
onPress={() => reactTag != null && void RendererRuntime.setRenderSegmentPlan(reactTag, {
schemaVersion: 1,
segments: [
{ strategy: "CONTINUOUS" },
{ strategy: "RETAINED", startLayerId: "darkmap-basemap" },
],
}).then(() => refreshSegments(reactTag))}
/>
<Button
title="Fetch Segment Diagnostics"
disabled={reactTag == null}
onPress={() => reactTag != null && void refreshSegments(reactTag)}
/>
</View>
<Text>QBIN Format</Text>
<View style={styles.row}>

View File

@@ -70,6 +70,12 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
@ReactMethod override fun setContinuousLayerRange(reactTag: Double, startLayerId: String, endLayerId: String, promise: Promise) = withMap(reactTag, promise) { map ->
promise.resolve(if (map.setContinuousLayerRange(startLayerId, endLayerId)) "ACCEPTED" else "REJECTED")
}
@ReactMethod override fun setRenderSegmentPlanJson(reactTag: Double, planJson: String, promise: Promise) = withMap(reactTag, promise) { map ->
promise.resolve(if (map.setRenderSegmentPlan(planJson)) "ACCEPTED" else "REJECTED")
}
@ReactMethod override fun getRenderSegmentDiagnosticsJson(reactTag: Double, promise: Promise) = withMap(reactTag, promise) { map ->
promise.resolve(map.renderSegmentDiagnostics)
}
@ReactMethod override fun getRetainedUnderlayDiagnostics(reactTag: Double, promise: Promise) = withMap(reactTag, promise) { map ->
val value = map.retainedUnderlayDiagnostics
promise.resolve(Arguments.createMap().apply {

View File

@@ -169,6 +169,16 @@ RCT_EXPORT_MODULE(WeatherRuntimeBridge)
resolve([map setContinuousLayerRangeFromLayerIdentifier:startLayerId toLayerIdentifier:endLayerId] ? @"ACCEPTED" : @"REJECTED");
}];
}
- (void)setRenderSegmentPlanJson:(double)reactTag planJson:(NSString *)planJson resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
resolve([map setRenderSegmentPlanJSON:planJson] ? @"ACCEPTED" : @"REJECTED");
}];
}
- (void)getRenderSegmentDiagnosticsJson:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
resolve(map.renderSegmentDiagnosticsJSON);
}];
}
- (void)getRetainedUnderlayDiagnostics:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
MLNRetainedUnderlayDiagnostics *value = map.retainedUnderlayDiagnostics;

View File

@@ -27,6 +27,8 @@ export interface Spec extends TurboModule {
getState(reactTag: number): Promise<RuntimeSnapshot>;
setRetainedUnderlayEnabled(reactTag: number, enabled: boolean): Promise<CommandResult>;
setContinuousLayerRange(reactTag: number, startLayerId: string, endLayerId: string): Promise<CommandResult>;
setRenderSegmentPlanJson(reactTag: number, planJson: string): Promise<CommandResult>;
getRenderSegmentDiagnosticsJson(reactTag: number): Promise<string>;
getRetainedUnderlayDiagnostics(reactTag: number): Promise<RetainedUnderlaySnapshot>;
clearWindFields(reactTag: number): Promise<CommandResult>;
submitWindField(reactTag: number, metadataJson: string, payloadFileUri: string): Promise<FieldSubmitResult>;

View File

@@ -1,5 +1,5 @@
import { getNativeFieldRuntimeBridge } from "./NativeWeatherRuntimeBridge";
import type { CommandResult, RetainedUnderlayDiagnostics } from "./types";
import type { CommandResult, RenderSegmentDiagnostics, RenderSegmentPlan, RetainedUnderlayDiagnostics } from "./types";
function nativeBridge() {
const bridge = getNativeFieldRuntimeBridge();
@@ -19,4 +19,9 @@ export const RendererRuntime = {
},
getRetainedUnderlayDiagnostics: (reactTag: number): Promise<RetainedUnderlayDiagnostics> =>
nativeBridge().getRetainedUnderlayDiagnostics(reactTag),
setRenderSegmentPlan: (reactTag: number, plan: RenderSegmentPlan): Promise<CommandResult> => {
return nativeBridge().setRenderSegmentPlanJson(reactTag, JSON.stringify(plan));
},
getRenderSegmentDiagnostics: async (reactTag: number): Promise<RenderSegmentDiagnostics> =>
JSON.parse(await nativeBridge().getRenderSegmentDiagnosticsJson(reactTag)) as RenderSegmentDiagnostics,
} as const;

View File

@@ -16,6 +16,13 @@ export type {
ResolvedFieldProfileContractInput,
RuntimeSnapshot,
RetainedUnderlayDiagnostics,
RenderSegmentDefinition,
RenderSegmentDiagnostics,
RenderSegmentLayerDiagnostics,
RenderSegmentPlan,
RenderSegmentStrategy,
RetainedRenderSegmentDiagnostics,
ContinuousRenderSegmentDiagnostics,
RuntimeState,
TargetFps,
} from "./types";

View File

@@ -62,3 +62,78 @@ export type RetainedUnderlayDiagnostics = Readonly<{
fastPathMiss: number;
fullCpuFrame: number;
}>;
export type RenderSegmentStrategy = "RETAINED" | "CONTINUOUS";
export type RenderSegmentDefinition = Readonly<{
strategy: RenderSegmentStrategy;
startLayerId?: string;
}>;
export type RenderSegmentPlan = Readonly<{
schemaVersion: 1;
segments: readonly RenderSegmentDefinition[];
}>;
export type RenderSegmentLayerDiagnostics = Readonly<{
layerIndex: number;
layerId: string;
layerType: string;
visible: boolean;
sourceId: string | null;
sourceLayer: string | null;
}>;
export type RetainedRenderSegmentDiagnostics = Readonly<{
segmentIndex: number;
strategy: "RETAINED";
configuredStartLayerId: string | null;
beginIndex: number;
endExclusive: number;
resolvedStartLayerId: string | null;
resolvedEndLayerId: string | null;
layerCount: number;
layers: readonly RenderSegmentLayerDiagnostics[];
cache: Readonly<{
cacheState: string;
contentRevision: number;
build: number;
reuse: number;
invalidation: number;
lastInvalidationReason: string;
textureCreate: number;
compositeCreate: number;
resourceRelease: number;
}>;
}>;
export type ContinuousRenderSegmentDiagnostics = Readonly<{
segmentIndex: number;
strategy: "CONTINUOUS";
configuredStartLayerId: string | null;
beginIndex: number;
endExclusive: number;
resolvedStartLayerId: string | null;
resolvedEndLayerId: string | null;
layerCount: number;
layers: readonly RenderSegmentLayerDiagnostics[];
cache: null;
}>;
export type RenderSegmentDiagnostics = Readonly<{
schemaVersion: 1;
enabled: boolean;
configured: boolean;
valid: boolean;
configSource: "explicit-render-segment-plan" | "continuous-range-compat" | "unconfigured/legacy";
configRevision: number;
topologyRevision: number;
styleLayerCount: number;
segmentCount: number;
retainedSegmentCount: number;
continuousSegmentCount: number;
coverageComplete: boolean;
fallback: boolean;
lastReason: string;
fastPathEligible: boolean;
fastPathActive: boolean;
fastPathFallbackReason: string;
fastPathHit: number;
fastPathMiss: number;
fullCpuFrame: number;
styleLayers: readonly RenderSegmentLayerDiagnostics[];
segments: readonly (RetainedRenderSegmentDiagnostics | ContinuousRenderSegmentDiagnostics)[];
}>;

View File

@@ -36,6 +36,7 @@
#include <mbgl/style/image.hpp>
#include <mbgl/style/filter.hpp>
#include <mbgl/renderer/query.hpp>
#include <mbgl/renderer/render_segment_json.hpp>
#include <mbgl/particle/field/field_profile_contract_json.hpp>
#include <mbgl/particle/ingress/encoded_field_transport.hpp>
@@ -727,6 +728,19 @@ jni::jboolean NativeMapView::setContinuousLayerRange(JNIEnv& env,
jni::Make<std::string>(env, endLayerId));
}
jni::jboolean NativeMapView::setRenderSegmentPlan(JNIEnv& env, const jni::String& planJson) {
const auto plan = parseRenderSegmentPlanJSON(jni::Make<std::string>(env, planJson));
if (plan) return map->setRenderSegmentPlan(*plan);
RenderSegmentPlan invalid;
invalid.schemaVersion = 0;
map->setRenderSegmentPlan(std::move(invalid));
return false;
}
jni::Local<jni::String> NativeMapView::getRenderSegmentDiagnostics(JNIEnv& env) {
return jni::Make<jni::String>(env, renderSegmentDiagnosticsToJSON(map->getRenderSegmentDiagnostics()));
}
jni::Local<jni::Array<jni::String>> NativeMapView::getRetainedUnderlayDiagnostics(JNIEnv& env) {
const auto snapshot = map->getRetainedUnderlayDiagnostics();
const std::array<std::string, 39> values = {
@@ -1858,6 +1872,8 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
METHOD(&NativeMapView::getDebug, "nativeGetDebug"),
METHOD(&NativeMapView::setRetainedUnderlayEnabled, "nativeSetRetainedUnderlayEnabled"),
METHOD(&NativeMapView::setContinuousLayerRange, "nativeSetContinuousLayerRange"),
METHOD(&NativeMapView::setRenderSegmentPlan, "nativeSetRenderSegmentPlan"),
METHOD(&NativeMapView::getRenderSegmentDiagnostics, "nativeGetRenderSegmentDiagnostics"),
METHOD(&NativeMapView::getRetainedUnderlayDiagnostics, "nativeGetRetainedUnderlayDiagnostics"),
METHOD(&NativeMapView::getActionJournalLogFiles, "nativeGetActionJournalLogFiles"),
METHOD(&NativeMapView::getActionJournalLog, "nativeGetActionJournalLog"),

View File

@@ -205,6 +205,10 @@ public:
jni::jboolean setContinuousLayerRange(JNIEnv&, const jni::String&, const jni::String&);
jni::jboolean setRenderSegmentPlan(JNIEnv&, const jni::String&);
jni::Local<jni::String> getRenderSegmentDiagnostics(JNIEnv&);
jni::Local<jni::Array<jni::String>> getRetainedUnderlayDiagnostics(JNIEnv&);
jni::Local<jni::Array<jni::String>> getActionJournalLogFiles(JNIEnv&);

View File

@@ -121,6 +121,17 @@ public final class MapLibreMap {
return nativeMapView.setContinuousLayerRange(startLayerId, endLayerId);
}
/** Configures the renderer execution topology using schema-versioned JSON. */
public boolean setRenderSegmentPlan(@NonNull String planJson) {
return nativeMapView.setRenderSegmentPlan(planJson);
}
/** Returns the pull-based Style-to-Segment-to-Layer diagnostics snapshot as JSON. */
@NonNull
public String getRenderSegmentDiagnostics() {
return nativeMapView.getRenderSegmentDiagnostics();
}
/** Returns whether the retained static underlay optimization is enabled for this map. */
public boolean isRetainedUnderlayEnabled() {
return nativeMapView.getRetainedUnderlayDiagnostics().enabled;

View File

@@ -290,6 +290,10 @@ interface NativeMap {
boolean setContinuousLayerRange(String startLayerId, String endLayerId);
boolean setRenderSegmentPlan(String planJson);
String getRenderSegmentDiagnostics();
RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics();
String[] getActionJournalLogFiles();

View File

@@ -667,6 +667,19 @@ final class NativeMapView implements NativeMap {
return nativeSetContinuousLayerRange(startLayerId, endLayerId);
}
@Override
public boolean setRenderSegmentPlan(@NonNull String planJson) {
if (checkState("setRenderSegmentPlan")) return false;
return nativeSetRenderSegmentPlan(planJson);
}
@NonNull
@Override
public String getRenderSegmentDiagnostics() {
if (checkState("getRenderSegmentDiagnostics")) return "{\"schemaVersion\":1,\"valid\":false}";
return nativeGetRenderSegmentDiagnostics();
}
@Override
public RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() {
if (checkState("getRetainedUnderlayDiagnostics")) {
@@ -1730,6 +1743,12 @@ final class NativeMapView implements NativeMap {
@Keep
private native boolean nativeSetContinuousLayerRange(String startLayerId, String endLayerId);
@Keep
private native boolean nativeSetRenderSegmentPlan(String planJson);
@Keep
private native String nativeGetRenderSegmentDiagnostics();
@Keep
private native String[] nativeGetRetainedUnderlayDiagnostics();

View File

@@ -2321,6 +2321,12 @@ of north, the map will automatically snap to exact north.
- (BOOL)setContinuousLayerRangeFromLayerIdentifier:(NSString *)startLayerId
toLayerIdentifier:(NSString *)endLayerId;
/** Configures the schema-versioned renderer segment plan transported as JSON. */
- (BOOL)setRenderSegmentPlanJSON:(NSString *)planJSON;
/** Returns a pull-based Style-to-Segment-to-Layer diagnostics JSON snapshot. */
@property (nonatomic, readonly) NSString *renderSegmentDiagnosticsJSON;
/** Returns a thread-safe retained-underlay diagnostics snapshot without enabling periodic events. */
@property (nonatomic, readonly) MLNRetainedUnderlayDiagnostics *retainedUnderlayDiagnostics;

View File

@@ -5,6 +5,7 @@
#include <mbgl/layermanager/layer_manager.hpp>
#include <mbgl/map/camera.hpp>
#include <mbgl/map/map.hpp>
#include <mbgl/renderer/render_segment_json.hpp>
#include <mbgl/map/map_options.hpp>
#include <mbgl/map/mode.hpp>
#include <mbgl/math/wrap.hpp>
@@ -3160,6 +3161,22 @@ static void *windowScreenContext = &windowScreenContext;
return self.mbglMap.setContinuousLayerRange(startLayerId.UTF8String, endLayerId.UTF8String);
}
- (BOOL)setRenderSegmentPlanJSON:(NSString *)planJSON {
if (!_mbglMap) return NO;
const auto plan = mbgl::parseRenderSegmentPlanJSON(planJSON.UTF8String);
if (plan) return self.mbglMap.setRenderSegmentPlan(*plan);
mbgl::RenderSegmentPlan invalid;
invalid.schemaVersion = 0;
self.mbglMap.setRenderSegmentPlan(std::move(invalid));
return NO;
}
- (NSString *)renderSegmentDiagnosticsJSON {
if (!_mbglMap) return @"{\"schemaVersion\":1,\"valid\":false}";
const auto json = mbgl::renderSegmentDiagnosticsToJSON(self.mbglMap.getRenderSegmentDiagnostics());
return [NSString stringWithUTF8String:json.c_str()];
}
- (MLNRetainedUnderlayDiagnostics *)retainedUnderlayDiagnostics {
if (!_mbglMap) {
return [[MLNRetainedUnderlayDiagnostics alloc] initWithEnabled:YES

View File

@@ -108,6 +108,16 @@ bool Map::setContinuousLayerRange(std::string startLayerId, std::string endLayer
return true;
}
bool Map::setRenderSegmentPlan(RenderSegmentPlan plan) {
const bool accepted = impl->retainedUnderlayState->setRenderSegmentPlan(std::move(plan));
impl->onUpdate();
return accepted;
}
RenderSegmentDiagnostics Map::getRenderSegmentDiagnostics() const {
return impl->retainedUnderlayState->renderSegmentSnapshot();
}
bool Map::isRetainedUnderlayEnabled() const {
return impl->retainedUnderlayState->isEnabled();
}

View File

@@ -279,9 +279,8 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
imageManager->setLoaded(updateParameters->spriteLoaded);
const LayerDifference layerDiff = diffLayers(layerImpls, updateParameters->layers);
if (!layerDiff.added.empty() || !layerDiff.removed.empty() || !layerDiff.changed.empty()) {
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::LayerMutation);
}
// Layer mutations are tracked by per-segment immutable-Impl fingerprints below. Source,
// tile, image, glyph and camera changes remain deliberately global invalidations.
layerImpls = updateParameters->layers;
const bool layersAddedOrRemoved = !layerDiff.added.empty() || !layerDiff.removed.empty();
@@ -426,6 +425,35 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
continuousLayerRange,
continuousLayerRange.bottomPresent(),
continuousLayerRange.topPresent(static_cast<int32_t>(orderedLayers.size())));
const auto segmentConfig = updateParameters->retainedUnderlayState->renderSegmentPlanConfig();
const bool segmentMetadataChanged = renderSegmentPlan.configRevision != segmentConfig.revision ||
renderSegmentPlan.layerCount != static_cast<int32_t>(orderedLayers.size()) ||
renderSegmentStyleReady != updateParameters->styleLoaded || layersAddedOrRemoved ||
!layerDiff.changed.empty();
if (segmentMetadataChanged) {
renderSegmentPlan = resolveRenderSegmentPlan(
segmentConfig, orderedLayerIds, updateParameters->styleLoaded, retainedDynamicBoundary);
renderSegmentStyleReady = updateParameters->styleLoaded;
std::vector<StyleLayerDiagnostics> styleLayerDiagnostics;
styleLayerDiagnostics.reserve(orderedLayers.size());
for (std::size_t index = 0; index < orderedLayers.size(); ++index) {
const auto& layer = orderedLayers[index].get();
const auto& impl = *layer.baseImpl;
StyleLayerDiagnostics value;
value.layerIndex = static_cast<int32_t>(index);
value.layerId = layer.getID();
value.layerType = impl.getTypeInfo()->type;
value.visible = impl.visibility != style::VisibilityType::None;
if (!impl.source.empty()) value.sourceId = impl.source;
if (!impl.sourceLayer.empty()) value.sourceLayer = impl.sourceLayer;
styleLayerDiagnostics.push_back(std::move(value));
}
updateParameters->retainedUnderlayState->recordRenderSegmentResolution(
renderSegmentPlan, std::move(styleLayerDiagnostics));
}
} else {
renderSegmentPlan = {};
}
// Update all sources and initialize renderItems.
@@ -974,6 +1002,21 @@ bool RenderOrchestrator::continuousRangeSupportsParticleFastPath(int32_t begin,
return particle;
}
uint64_t RenderOrchestrator::retainedRangeContentFingerprint(int32_t begin, int32_t endExclusive) const {
const auto first = std::clamp<int32_t>(begin, 0, static_cast<int32_t>(orderedLayers.size()));
const auto last = std::clamp<int32_t>(endExclusive, first, static_cast<int32_t>(orderedLayers.size()));
uint64_t value = 1469598103934665603ULL;
for (int32_t index = first; index < last; ++index) {
const auto& layer = orderedLayers[static_cast<std::size_t>(index)].get();
const auto identity = reinterpret_cast<std::uintptr_t>(layer.baseImpl.get());
value ^= static_cast<uint64_t>(identity);
value *= 1099511628211ULL;
value ^= static_cast<uint64_t>(index);
value *= 1099511628211ULL;
}
return value;
}
ParticleFastPathDynamicState RenderOrchestrator::particleFastPathDynamicState(TimePoint timePoint) const {
ParticleFastPathDynamicState result;
result.blockingTransition = renderLight.hasTransition();

View File

@@ -120,6 +120,7 @@ public:
size_t numLayerGroups() const noexcept;
std::optional<int32_t> getRetainedDynamicBoundary() const noexcept { return retainedDynamicBoundary; }
const ContinuousLayerRangeResolution& getContinuousLayerRange() const noexcept { return continuousLayerRange; }
const ResolvedRenderSegmentPlan& getRenderSegmentPlan() const noexcept { return renderSegmentPlan; }
int32_t getOrderedLayerCount() const noexcept { return static_cast<int32_t>(orderedLayers.size()); }
bool retainedUnderlayUsesPlacement(int32_t boundary) const;
bool retainedUnderlayIsStable(TimePoint, int32_t boundary) const;
@@ -128,6 +129,7 @@ public:
bool retainedRangeIsStable(TimePoint, int32_t begin, int32_t endExclusive) const;
bool retainedRangeHasUnsupportedLayer(int32_t begin, int32_t endExclusive) const;
bool continuousRangeSupportsParticleFastPath(int32_t begin, int32_t endExclusive) const;
uint64_t retainedRangeContentFingerprint(int32_t begin, int32_t endExclusive) const;
ParticleFastPathDynamicState particleFastPathDynamicState(TimePoint) const;
uint64_t getRetainedUnderlayContentRevision() const noexcept {
return retainedUnderlayContentRevision.load(std::memory_order_acquire);
@@ -366,6 +368,8 @@ private:
// reallocation on each frame.
std::vector<Immutable<style::LayerProperties>> filteredLayersForSource;
RenderLayerReferences orderedLayers;
ResolvedRenderSegmentPlan renderSegmentPlan;
bool renderSegmentStyleReady = false;
RenderLayerReferences layersNeedPlacement;
TaggedScheduler threadPool;

View File

@@ -0,0 +1,135 @@
#pragma once
#include <mbgl/renderer/render_segment.hpp>
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
namespace mbgl {
inline std::optional<RenderSegmentPlan> parseRenderSegmentPlanJSON(const std::string& json) {
rapidjson::Document document;
if (document.Parse(json.c_str()).HasParseError() || !document.IsObject() ||
!document.HasMember("schemaVersion") || !document["schemaVersion"].IsUint() ||
document["schemaVersion"].GetUint() != 1 || !document.HasMember("segments") ||
!document["segments"].IsArray() || document.MemberCount() != 2) {
return std::nullopt;
}
RenderSegmentPlan plan;
for (const auto& value : document["segments"].GetArray()) {
if (!value.IsObject() || !value.HasMember("strategy") || !value["strategy"].IsString() ||
value.MemberCount() > 2) {
return std::nullopt;
}
for (auto member = value.MemberBegin(); member != value.MemberEnd(); ++member) {
const std::string_view name(member->name.GetString(), member->name.GetStringLength());
if (name != "strategy" && name != "startLayerId") return std::nullopt;
}
RenderSegmentDefinition definition;
const std::string strategy = value["strategy"].GetString();
if (strategy == "RETAINED") {
definition.strategy = RenderSegmentStrategy::Retained;
} else if (strategy == "CONTINUOUS") {
definition.strategy = RenderSegmentStrategy::Continuous;
} else {
return std::nullopt;
}
if (value.HasMember("startLayerId")) {
if (!value["startLayerId"].IsString()) return std::nullopt;
definition.startLayerId = value["startLayerId"].GetString();
}
plan.segments.push_back(std::move(definition));
}
return plan;
}
template <typename Writer>
inline void writeOptionalString(Writer& writer, const char* key, const std::optional<std::string>& value) {
writer.Key(key);
if (value) writer.String(value->c_str()); else writer.Null();
}
template <typename Writer>
inline void writeStyleLayerDiagnostics(Writer& writer, const StyleLayerDiagnostics& layer) {
writer.StartObject();
writer.Key("layerIndex"); writer.Int(layer.layerIndex);
writer.Key("layerId"); writer.String(layer.layerId.c_str());
writer.Key("layerType"); writer.String(layer.layerType.c_str());
writer.Key("visible"); writer.Bool(layer.visible);
writeOptionalString(writer, "sourceId", layer.sourceId);
writeOptionalString(writer, "sourceLayer", layer.sourceLayer);
writer.EndObject();
}
inline std::string renderSegmentDiagnosticsToJSON(const RenderSegmentDiagnostics& snapshot) {
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
writer.StartObject();
writer.Key("schemaVersion"); writer.Uint(snapshot.schemaVersion);
writer.Key("enabled"); writer.Bool(snapshot.enabled);
writer.Key("configured"); writer.Bool(snapshot.configured);
writer.Key("valid"); writer.Bool(snapshot.valid);
writer.Key("configSource"); writer.String(snapshot.configSource.c_str());
writer.Key("configRevision"); writer.Uint64(snapshot.configRevision);
writer.Key("topologyRevision"); writer.Uint64(snapshot.topologyRevision);
writer.Key("styleLayerCount"); writer.Uint(snapshot.styleLayerCount);
writer.Key("segmentCount"); writer.Uint(snapshot.segmentCount);
writer.Key("retainedSegmentCount"); writer.Uint(snapshot.retainedSegmentCount);
writer.Key("continuousSegmentCount"); writer.Uint(snapshot.continuousSegmentCount);
writer.Key("coverageComplete"); writer.Bool(snapshot.coverageComplete);
writer.Key("fallback"); writer.Bool(snapshot.fallback);
writer.Key("lastReason"); writer.String(snapshot.lastReason.c_str());
writer.Key("fastPathEligible"); writer.Bool(snapshot.fastPathEligible);
writer.Key("fastPathActive"); writer.Bool(snapshot.fastPathActive);
writer.Key("fastPathFallbackReason"); writer.String(snapshot.fastPathFallbackReason.c_str());
writer.Key("fastPathHit"); writer.Uint64(snapshot.fastPathHit);
writer.Key("fastPathMiss"); writer.Uint64(snapshot.fastPathMiss);
writer.Key("fullCpuFrame"); writer.Uint64(snapshot.fullCpuFrame);
writer.Key("styleLayers"); writer.StartArray();
for (const auto& layer : snapshot.styleLayers) writeStyleLayerDiagnostics(writer, layer);
writer.EndArray();
writer.Key("segments"); writer.StartArray();
for (const auto& segment : snapshot.segments) {
writer.StartObject();
writer.Key("segmentIndex"); writer.Uint(segment.segmentIndex);
writer.Key("strategy"); writer.String(renderSegmentStrategyText(segment.strategy));
writeOptionalString(writer, "configuredStartLayerId", segment.configuredStartLayerId);
writer.Key("beginIndex"); writer.Int(segment.beginIndex);
writer.Key("endExclusive"); writer.Int(segment.endExclusive);
writeOptionalString(writer, "resolvedStartLayerId", segment.resolvedStartLayerId);
writeOptionalString(writer, "resolvedEndLayerId", segment.resolvedEndLayerId);
writer.Key("layerCount"); writer.Uint(static_cast<uint32_t>(segment.layers.size()));
writer.Key("layers"); writer.StartArray();
for (const auto& layer : segment.layers) writeStyleLayerDiagnostics(writer, layer);
writer.EndArray();
writer.Key("cache");
if (!segment.cache) {
writer.Null();
} else {
const auto& cache = *segment.cache;
writer.StartObject();
writer.Key("cacheState"); writer.String(cache.cacheState.c_str());
writer.Key("contentRevision"); writer.Uint64(cache.contentRevision);
writer.Key("build"); writer.Uint64(cache.build);
writer.Key("reuse"); writer.Uint64(cache.reuse);
writer.Key("invalidation"); writer.Uint64(cache.invalidation);
writer.Key("lastInvalidationReason"); writer.String(cache.lastInvalidationReason.c_str());
writer.Key("textureCreate"); writer.Uint64(cache.textureCreate);
writer.Key("compositeCreate"); writer.Uint64(cache.compositeCreate);
writer.Key("resourceRelease"); writer.Uint64(cache.resourceRelease);
writer.EndObject();
}
writer.EndObject();
}
writer.EndArray();
writer.EndObject();
return buffer.GetString();
}
} // namespace mbgl

View File

@@ -0,0 +1,172 @@
#pragma once
#include <mbgl/renderer/render_segment.hpp>
#include <algorithm>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace mbgl {
enum class RenderSegmentConfigSource : uint8_t { UnconfiguredLegacy, ExplicitPlan, ContinuousRangeCompat };
inline const char* renderSegmentConfigSourceText(RenderSegmentConfigSource source) noexcept {
switch (source) {
case RenderSegmentConfigSource::ExplicitPlan: return "explicit-render-segment-plan";
case RenderSegmentConfigSource::ContinuousRangeCompat: return "continuous-range-compat";
case RenderSegmentConfigSource::UnconfiguredLegacy: return "unconfigured/legacy";
}
return "unconfigured/legacy";
}
struct RenderSegmentConfig {
RenderSegmentConfigSource source = RenderSegmentConfigSource::UnconfiguredLegacy;
RenderSegmentPlan plan;
std::string continuousStartLayerId;
std::string continuousEndLayerId;
uint64_t revision = 0;
};
inline ResolvedRenderSegmentPlan resolveRenderSegmentPlan(
const RenderSegmentConfig& config,
const std::vector<std::string>& orderedLayerIds,
bool styleReady,
std::optional<int32_t> legacyBoundary = std::nullopt) {
ResolvedRenderSegmentPlan result;
result.configSource = renderSegmentConfigSourceText(config.source);
result.configRevision = config.revision;
result.layerCount = static_cast<int32_t>(orderedLayerIds.size());
result.configured = config.source != RenderSegmentConfigSource::UnconfiguredLegacy;
if (!styleReady || orderedLayerIds.empty()) {
result.reason = "style-not-ready";
return result;
}
std::unordered_map<std::string, int32_t> indices;
indices.reserve(orderedLayerIds.size());
for (std::size_t i = 0; i < orderedLayerIds.size(); ++i) {
indices.emplace(orderedLayerIds[i], static_cast<int32_t>(i));
}
if (config.source == RenderSegmentConfigSource::UnconfiguredLegacy) {
if (!legacyBoundary || *legacyBoundary <= 0 || *legacyBoundary >= result.layerCount) {
result.reason = "not-configured";
return result;
}
result.segments = {
{0, RenderSegmentStrategy::Retained, std::nullopt, 0, *legacyBoundary},
{1,
RenderSegmentStrategy::Continuous,
orderedLayerIds[*legacyBoundary],
*legacyBoundary,
result.layerCount},
};
result.valid = true;
result.reason = "none";
return result;
}
if (config.source == RenderSegmentConfigSource::ContinuousRangeCompat) {
const auto start = indices.find(config.continuousStartLayerId);
const auto end = indices.find(config.continuousEndLayerId);
if (start == indices.end()) {
result.reason = "start-layer-missing";
return result;
}
if (end == indices.end()) {
result.reason = "end-layer-missing";
return result;
}
if (start->second > end->second) {
result.reason = "range-order-invalid";
return result;
}
uint32_t segmentIndex = 0;
if (start->second > 0) {
result.segments.push_back(
{segmentIndex++, RenderSegmentStrategy::Retained, std::nullopt, 0, start->second});
}
result.segments.push_back({segmentIndex++,
RenderSegmentStrategy::Continuous,
config.continuousStartLayerId,
start->second,
end->second + 1});
if (end->second + 1 < result.layerCount) {
result.segments.push_back({segmentIndex,
RenderSegmentStrategy::Retained,
orderedLayerIds[static_cast<std::size_t>(end->second + 1)],
end->second + 1,
result.layerCount});
}
result.valid = true;
result.reason = "none";
return result;
}
if (config.plan.schemaVersion != 1) {
result.reason = "unsupported-schema-version";
return result;
}
if (config.plan.segments.empty()) {
result.reason = "empty-segments";
return result;
}
if (config.plan.segments.front().startLayerId && !config.plan.segments.front().startLayerId->empty()) {
result.reason = "first-segment-start-layer-forbidden";
return result;
}
std::vector<int32_t> begins(config.plan.segments.size(), 0);
for (std::size_t i = 1; i < config.plan.segments.size(); ++i) {
const auto& startLayerId = config.plan.segments[i].startLayerId;
if (!startLayerId || startLayerId->empty()) {
result.reason = "segment-start-layer-required";
return result;
}
const auto found = indices.find(*startLayerId);
if (found == indices.end()) {
result.reason = "boundary-layer-missing";
return result;
}
begins[i] = found->second;
if (begins[i] <= begins[i - 1]) {
result.reason = begins[i] == begins[i - 1] ? "boundary-layer-duplicate" : "boundary-order-invalid";
return result;
}
}
result.segments.reserve(config.plan.segments.size());
for (std::size_t i = 0; i < config.plan.segments.size(); ++i) {
const int32_t endExclusive = i + 1 < begins.size() ? begins[i + 1] : result.layerCount;
if (begins[i] >= endExclusive) {
result.segments.clear();
result.reason = "empty-segment";
return result;
}
result.segments.push_back({static_cast<uint32_t>(i),
config.plan.segments[i].strategy,
config.plan.segments[i].startLayerId,
begins[i],
endExclusive});
}
result.valid = true;
result.reason = "none";
return result;
}
inline bool renderSegmentCoverageComplete(const ResolvedRenderSegmentPlan& plan) noexcept {
if (!plan.valid || plan.segments.empty() || plan.segments.front().beginIndex != 0 ||
plan.segments.back().endExclusive != plan.layerCount) {
return false;
}
for (std::size_t i = 0; i < plan.segments.size(); ++i) {
if (plan.segments[i].beginIndex >= plan.segments[i].endExclusive) return false;
if (i > 0 && plan.segments[i - 1].endExclusive != plan.segments[i].beginIndex) return false;
}
return true;
}
} // namespace mbgl

View File

@@ -29,7 +29,6 @@
#include <mbgl/renderer/layer_tweaker.hpp>
#include <mbgl/renderer/render_target.hpp>
#include <mbgl/renderer/retained_underlay_cache.hpp>
#include <mbgl/renderer/continuous_layer_range.hpp>
#include <mbgl/particle/renderer_debug_counters.hpp>
#if MLN_RENDER_BACKEND_METAL
@@ -52,10 +51,14 @@ using namespace style;
namespace {
ContinuousRenderRanges activeRenderRanges(const RenderOrchestrator& orchestrator) {
return selectContinuousRenderRanges(orchestrator.getContinuousLayerRange(),
orchestrator.getRetainedDynamicBoundary(),
orchestrator.getOrderedLayerCount());
const ResolvedRenderSegment* singleContinuousSegment(const ResolvedRenderSegmentPlan& plan) {
const ResolvedRenderSegment* result = nullptr;
for (const auto& segment : plan.segments) {
if (segment.strategy != RenderSegmentStrategy::Continuous) continue;
if (result) return nullptr;
result = &segment;
}
return result;
}
RendererObserver& nullObserver() {
@@ -104,9 +107,7 @@ Renderer::Impl::Impl(gfx::RendererBackend& backend_,
: orchestrator(!backend_.contextIsShared(), backend_.getThreadPool(), localFontFamily_),
backend(backend_),
observer(&nullObserver()),
pixelRatio(pixelRatio_),
retainedUnderlayCache(std::make_unique<RetainedUnderlayCache>(RetainedRangeRole::Bottom)),
retainedTopCache(std::make_unique<RetainedUnderlayCache>(RetainedRangeRole::Top)) {}
pixelRatio(pixelRatio_) {}
Renderer::Impl::~Impl() {
assert(gfx::BackendScope::exists());
@@ -140,8 +141,10 @@ void Renderer::Impl::setObserver(RendererObserver* observer_) {
void Renderer::Impl::synchronizeRetainedUnderlayState(
const std::shared_ptr<RetainedUnderlayRuntimeState>& state) {
retainedUnderlayCache->synchronizeRuntimeState(state);
retainedTopCache->synchronizeRuntimeState(state);
retainedRuntimeState = state;
for (auto& cache : retainedSegmentCaches) {
if (cache) cache->synchronizeRuntimeState(state);
}
}
Renderer::Impl::ParticleFastPathUpdateSignature Renderer::Impl::makeParticleFastPathUpdateSignature(
@@ -200,49 +203,49 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptr<UpdatePara
annotationMutation = updateParameters->annotationManager->hasPendingChanges();
}
const auto retained = retainedUnderlayCache->getDiagnostics();
const auto ranges = activeRenderRanges(orchestrator);
const auto liveRangeConfig = updateParameters->retainedUnderlayState
? updateParameters->retainedUnderlayState->continuousLayerRangeConfig()
: ContinuousLayerRangeConfig{};
const auto retained = retainedRuntimeState ? retainedRuntimeState->renderSegmentSnapshot()
: RenderSegmentDiagnostics{};
const auto& plan = orchestrator.getRenderSegmentPlan();
const auto* continuous = singleContinuousSegment(plan);
const bool oneContinuous = plan.valid && continuous;
const std::optional<int32_t> currentBoundary =
(ranges.explicitValid || !ranges.explicitConfigured) ? std::optional<int32_t>(ranges.continuousBegin)
: std::nullopt;
oneContinuous ? std::optional<int32_t>(continuous->beginIndex) : std::nullopt;
const bool hasSignature = particlePreparedSignature.has_value();
const bool viewportMatches = hasSignature && particlePreparedSignature->viewport == signature.viewport &&
particlePreparedBackendSize == backend.getDefaultRenderable().getSize();
const bool cameraMatches = hasSignature && particlePreparedSignature->cameraMatrix == signature.cameraMatrix;
const bool updateParametersMatch =
hasSignature && particleFastPathNonCameraInputsMatch(*particlePreparedSignature, signature);
const bool bottomRevisionMatches = !ranges.bottomPresent() ||
(retainedUnderlayCache->getStaticRevision() == retainedUnderlayRevisionTracker.current() &&
retainedUnderlayCache->getBuildRevision() == retainedUnderlayRevisionTracker.current());
const bool topRevisionMatches = !ranges.topPresent() ||
(retainedTopCache->getStaticRevision() == retainedUnderlayRevisionTracker.current() &&
retainedTopCache->getBuildRevision() == retainedUnderlayRevisionTracker.current());
const bool revisionMatches = bottomRevisionMatches && topRevisionMatches &&
bool retainedRevisionsMatch = retainedSegmentCaches.size() == plan.segments.size();
bool retainedCachesValid = retainedRevisionsMatch;
for (std::size_t i = 0; i < retainedSegmentCaches.size(); ++i) {
if (plan.segments[i].strategy != RenderSegmentStrategy::Retained) continue;
const auto& cache = retainedSegmentCaches[i];
retainedRevisionsMatch = retainedRevisionsMatch && cache &&
cache->getBuildRevision() == cache->getStaticRevision();
retainedCachesValid = retainedCachesValid && cache && cache->isValid();
}
const bool revisionMatches = retainedRevisionsMatch &&
particlePreparedStaticRevision == retainedUnderlayRevisionTracker.current();
const bool boundaryMatches = particlePreparedBoundary == currentBoundary &&
particlePreparedContinuousEnd == std::optional<int32_t>(ranges.continuousEndExclusive) &&
particlePreparedRangeConfigRevision == ranges.configRevision &&
liveRangeConfig.revision == ranges.configRevision &&
liveRangeConfig.configured == ranges.explicitConfigured &&
particlePreparedBottomPresent == ranges.bottomPresent() &&
particlePreparedTopPresent == ranges.topPresent();
particlePreparedContinuousEnd ==
(oneContinuous ? std::optional<int32_t>(continuous->endExclusive) : std::nullopt) &&
particlePreparedRangeConfigRevision == plan.configRevision;
const bool topologyMatches = particlePreparedTopologyRevision == retainedUnderlayTopologyTracker.current();
const bool contentRevisionMatches =
particlePreparedContentRevision == orchestrator.getRetainedUnderlayContentRevision();
const bool staticStable = (ranges.explicitValid || (!ranges.explicitConfigured && ranges.bottomPresent())) &&
(!ranges.bottomPresent() || orchestrator.retainedRangeIsStable(
updateParameters->timePoint, 0, ranges.continuousBegin)) &&
(!ranges.topPresent() || orchestrator.retainedRangeIsStable(
updateParameters->timePoint, ranges.continuousEndExclusive, ranges.layerCount));
bool staticStable = plan.valid;
for (const auto& segment : plan.segments) {
if (segment.strategy == RenderSegmentStrategy::Retained) {
staticStable = staticStable && orchestrator.retainedRangeIsStable(
updateParameters->timePoint, segment.beginIndex, segment.endExclusive);
}
}
const ParticleFastPathGateInput input{
.hasPreparedTree = !particlePreparedTree.empty(),
.retainedEnabled = retained.enabled,
.cacheValid = (!ranges.bottomPresent() || retainedUnderlayCache->isValid()) &&
(!ranges.topPresent() || retainedTopCache->isValid()),
.cacheValid = retainedCachesValid,
.revisionMatches = revisionMatches,
.contentRevisionMatches = contentRevisionMatches,
.cameraMatches = cameraMatches,
@@ -265,13 +268,13 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptr<UpdatePara
.stillImageRequest = updateParameters->stillImageRequest,
.continuousMode = updateParameters->mode == MapMode::Continuous,
.debugDisabled = updateParameters->debugOptions == MapDebugOptions::NoDebug,
.unsupportedContinuousLayer = particlePreparedContinuousUnsupported,
.supportedScene = particlePreparedSceneSupported,
.unsupportedContinuousLayer = particlePreparedContinuousUnsupported || !oneContinuous,
.supportedScene = particlePreparedSceneSupported && oneContinuous,
.annotationMutation = annotationMutation,
};
const auto decision = evaluateParticleFastPath(input);
particleFastPathLastReason = decision.reason;
retainedUnderlayCache->recordFastPathDecision(decision.candidate, decision.reason);
if (retainedRuntimeState) retainedRuntimeState->recordFastPathDecision(decision.candidate, decision.reason);
if (!decision.candidate) {
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleFastFrameMiss);
return false;
@@ -285,18 +288,15 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptr<UpdatePara
void Renderer::Impl::commitPreparedRenderTree(std::unique_ptr<RenderTree> renderTree,
const std::shared_ptr<UpdateParameters>& updateParameters) {
const auto ranges = activeRenderRanges(orchestrator);
const auto& plan = orchestrator.getRenderSegmentPlan();
const auto* continuous = singleContinuousSegment(plan);
particlePreparedSignature = makeParticleFastPathUpdateSignature(*updateParameters);
particlePreparedContentRevision = orchestrator.getRetainedUnderlayContentRevision();
particlePreparedTopologyRevision = retainedUnderlayTopologyTracker.current();
particlePreparedStaticRevision = retainedUnderlayRevisionTracker.current();
particlePreparedBoundary = (ranges.explicitValid || !ranges.explicitConfigured)
? std::optional<int32_t>(ranges.continuousBegin)
: std::nullopt;
particlePreparedContinuousEnd = ranges.continuousEndExclusive;
particlePreparedRangeConfigRevision = ranges.configRevision;
particlePreparedBottomPresent = ranges.bottomPresent();
particlePreparedTopPresent = ranges.topPresent();
particlePreparedBoundary = continuous ? std::optional<int32_t>(continuous->beginIndex) : std::nullopt;
particlePreparedContinuousEnd = continuous ? std::optional<int32_t>(continuous->endExclusive) : std::nullopt;
particlePreparedRangeConfigRevision = plan.configRevision;
particlePreparedBackendSize = backend.getDefaultRenderable().getSize();
particlePreparedTree.replace(std::move(renderTree));
}
@@ -310,8 +310,6 @@ void Renderer::Impl::clearPreparedRenderTree() {
particlePreparedBoundary.reset();
particlePreparedContinuousEnd.reset();
particlePreparedRangeConfigRevision = 0;
particlePreparedBottomPresent = false;
particlePreparedTopPresent = false;
particlePreparedContinuousUnsupported = false;
particlePreparedBackendSize = {};
particlePreparedSceneSupported = false;
@@ -324,7 +322,7 @@ void Renderer::Impl::render(const RenderTree& renderTree,
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FullRendererFrame);
if (!particleFastFrame) {
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleFullFrame);
retainedUnderlayCache->recordFullCpuFrame();
if (retainedRuntimeState) retainedRuntimeState->recordFullCpuFrame();
}
auto& context = backend.getContext();
context.setObserver(this);
@@ -499,78 +497,97 @@ void Renderer::Impl::render(const RenderTree& renderTree,
orchestrator.processChanges();
const auto ranges = activeRenderRanges(orchestrator);
const bool rangeMode = ranges.explicitValid;
const bool retainedMode = rangeMode || (!ranges.explicitConfigured && ranges.bottomPresent());
const std::optional<int32_t> retainedBoundary = retainedMode
? std::optional<int32_t>(ranges.continuousBegin) : std::nullopt;
std::optional<RetainedUnderlayKey> bottomKey;
std::optional<RetainedUnderlayKey> topKey;
const auto& segmentPlan = orchestrator.getRenderSegmentPlan();
const bool retainedMode = segmentPlan.valid;
const auto* singleContinuous = singleContinuousSegment(segmentPlan);
const auto segmentSnapshot = retainedRuntimeState ? retainedRuntimeState->renderSegmentSnapshot()
: RenderSegmentDiagnostics{};
if (retainedCacheTopologyRevision != segmentSnapshot.topologyRevision ||
retainedSegmentCaches.size() != segmentPlan.segments.size()) {
retainedSegmentCaches.clear();
retainedSegmentCaches.resize(segmentPlan.segments.size());
retainedSegmentFingerprints.assign(segmentPlan.segments.size(), 0);
bool continuousSeen = false;
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy == RenderSegmentStrategy::Continuous) {
continuousSeen = true;
continue;
}
auto cache = std::make_unique<RetainedUnderlayCache>(
segment.segmentIndex,
"retained-segment-" + std::to_string(segment.segmentIndex),
continuousSeen ? RetainedRangeRole::Top : RetainedRangeRole::Bottom);
cache->synchronizeRuntimeState(retainedRuntimeState);
retainedSegmentCaches[segment.segmentIndex] = std::move(cache);
}
retainedCacheTopologyRevision = segmentSnapshot.topologyRevision;
}
std::vector<std::optional<RetainedUnderlayKey>> segmentKeys(segmentPlan.segments.size());
std::vector<RetainedUnderlayDecision> segmentDecisions(
segmentPlan.segments.size(), RetainedUnderlayDecision::Disabled);
const std::optional<int32_t> retainedBoundary = singleContinuous
? std::optional<int32_t>(singleContinuous->beginIndex) : std::nullopt;
RetainedUnderlayTopologySnapshot retainedTopology{
.boundary = retainedBoundary,
.staticPrefix = {},
.continuousEndExclusive = retainedMode ? std::optional<int32_t>(ranges.continuousEndExclusive) : std::nullopt,
.continuousEndExclusive = singleContinuous
? std::optional<int32_t>(singleContinuous->endExclusive)
: std::nullopt,
.staticSuffix = {}};
mat4 retainedCameraMatrix;
state.getProjMatrix(retainedCameraMatrix);
if (retainedMode && ranges.bottomPresent()) {
orchestrator.visitLayerGroupsInRange(0, ranges.continuousBegin, [&](LayerGroupBase& layerGroup, size_t) {
retainedTopology.staticPrefix.push_back({.layerIndex = layerGroup.getLayerIndex(),
.groupName = layerGroup.getName(),
.groupType = static_cast<uint8_t>(layerGroup.getType())});
});
bottomKey = RetainedUnderlayKey{size, 0, ranges.continuousBegin, retainedCameraMatrix};
for (const auto& segment : segmentPlan.segments) {
retainedTopology.staticPrefix.push_back(
{.layerIndex = segment.beginIndex,
.groupName = std::string(renderSegmentStrategyText(segment.strategy)) + ":" +
std::to_string(segment.endExclusive),
.groupType = static_cast<uint8_t>(segment.strategy)});
if (segment.strategy == RenderSegmentStrategy::Retained) {
segmentKeys[segment.segmentIndex] = RetainedUnderlayKey{
size, segment.beginIndex, segment.endExclusive, retainedCameraMatrix};
}
if (rangeMode && ranges.topPresent()) {
orchestrator.visitLayerGroupsInRange(
ranges.continuousEndExclusive, ranges.layerCount, [&](LayerGroupBase& layerGroup, size_t) {
retainedTopology.staticSuffix.push_back({.layerIndex = layerGroup.getLayerIndex(),
.groupName = layerGroup.getName(),
.groupType = static_cast<uint8_t>(layerGroup.getType())});
});
topKey = RetainedUnderlayKey{
size, ranges.continuousEndExclusive, ranges.layerCount, retainedCameraMatrix};
}
const auto retainedTopologyRevision = particleFastFrame ? retainedUnderlayTopologyTracker.current()
: retainedUnderlayTopologyTracker.observe(retainedTopology);
const auto bottomGroupCount = retainedTopology.staticPrefix.size();
bool legacyInRetainedRange = false;
if (retainedMode) {
for (const auto& item : layerRenderItems) {
const auto index = static_cast<int32_t>(item.index);
if ((index < ranges.continuousBegin || (rangeMode && index >= ranges.continuousEndExclusive)) &&
item.layer.get().usesLegacyRendering()) {
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy == RenderSegmentStrategy::Retained && index >= segment.beginIndex &&
index < segment.endExclusive && item.layer.get().usesLegacyRendering()) {
legacyInRetainedRange = true;
break;
}
}
if (legacyInRetainedRange) break;
}
}
bool retainedStaticUsesPlacement = false;
bool retainedStaticStable = retainedMode;
bool retainedUnsupportedStaticLayer = false;
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy != RenderSegmentStrategy::Retained) continue;
retainedStaticUsesPlacement = retainedStaticUsesPlacement ||
orchestrator.retainedRangeUsesPlacement(segment.beginIndex, segment.endExclusive);
retainedStaticStable = retainedStaticStable && orchestrator.retainedRangeIsStable(
renderTreeParameters.timePoint, segment.beginIndex, segment.endExclusive);
retainedUnsupportedStaticLayer = retainedUnsupportedStaticLayer ||
orchestrator.retainedRangeHasUnsupportedLayer(segment.beginIndex, segment.endExclusive);
}
const bool retainedStaticUsesPlacement = retainedMode &&
((ranges.bottomPresent() && orchestrator.retainedRangeUsesPlacement(0, ranges.continuousBegin)) ||
(ranges.topPresent() && orchestrator.retainedRangeUsesPlacement(
ranges.continuousEndExclusive, ranges.layerCount)));
const bool retainedStaticStable = retainedMode &&
(!ranges.bottomPresent() || orchestrator.retainedRangeIsStable(
renderTreeParameters.timePoint, 0, ranges.continuousBegin)) &&
(!ranges.topPresent() || orchestrator.retainedRangeIsStable(
renderTreeParameters.timePoint, ranges.continuousEndExclusive, ranges.layerCount));
const bool retainedPlacementStable =
!retainedStaticUsesPlacement ||
(!renderTreeParameters.placementChanged && renderTreeParameters.symbolFadeChange == 0.0f);
const bool retainedUnsupportedStaticLayer = retainedMode &&
((ranges.bottomPresent() && orchestrator.retainedRangeHasUnsupportedLayer(0, ranges.continuousBegin)) ||
(ranges.topPresent() && orchestrator.retainedRangeHasUnsupportedLayer(
ranges.continuousEndExclusive, ranges.layerCount)));
const auto retainedContentRevision = orchestrator.getRetainedUnderlayContentRevision();
const auto retainedContentReason = orchestrator.getRetainedUnderlayContentReason();
const RetainedUnderlayRevisionState retainedRevisionState{
.viewport = size,
.cameraMatrix = retainedCameraMatrix,
.boundary = retainedBoundary,
.continuousEndExclusive = retainedMode
? std::optional<int32_t>(ranges.continuousEndExclusive)
.continuousEndExclusive = singleContinuous
? std::optional<int32_t>(singleContinuous->endExclusive)
: std::nullopt,
.rangeConfigRevision = ranges.configRevision,
.rangeConfigRevision = segmentPlan.configRevision,
.pixelRatio = updateParameters->pixelRatio,
.contentRevision = retainedContentRevision,
.topologyRevision = retainedTopologyRevision,
@@ -586,23 +603,20 @@ void Renderer::Impl::render(const RenderTree& renderTree,
const auto retainedStaticRevision = retainedUnderlayRevisionTracker.observe(
retainedRevisionState, retainedUnderlayReasonText(retainedContentReason));
const bool retainedSupported = retainedMode &&
(rangeMode || !ranges.bottomPresent() || bottomGroupCount > 0) &&
renderTreeParameters.loaded && !renderTreeParameters.has3D &&
renderTreeParameters.debugOptions == MapDebugOptions::NoDebug &&
retainedStaticStable && retainedPlacementStable &&
!legacyInRetainedRange && !retainedUnsupportedStaticLayer;
const bool fastContinuousSupported = rangeMode
? orchestrator.continuousRangeSupportsParticleFastPath(
ranges.continuousBegin, ranges.continuousEndExclusive)
: retainedSupported;
if (!particleFastFrame) particlePreparedContinuousUnsupported = rangeMode && !fastContinuousSupported;
retainedUnderlayCache->recordFastPathEligibility(retainedSupported && fastContinuousSupported);
const bool fastContinuousSupported = singleContinuous &&
orchestrator.continuousRangeSupportsParticleFastPath(
singleContinuous->beginIndex, singleContinuous->endExclusive);
if (!particleFastFrame) particlePreparedContinuousUnsupported = !singleContinuous || !fastContinuousSupported;
if (retainedRuntimeState) retainedRuntimeState->recordFastPathEligibility(
retainedSupported && fastContinuousSupported);
if (!particleFastFrame) particlePreparedSceneSupported = retainedSupported && fastContinuousSupported;
const char* retainedFallbackReason = nullptr;
if (!retainedMode) {
retainedFallbackReason = "no-boundary";
} else if (!rangeMode && ranges.bottomPresent() && bottomGroupCount == 0) {
retainedFallbackReason = "empty-static-prefix";
} else if (!renderTreeParameters.loaded) {
retainedFallbackReason = "style-not-loaded";
} else if (renderTreeParameters.has3D) {
@@ -618,36 +632,49 @@ void Renderer::Impl::render(const RenderTree& renderTree,
} else if (retainedUnsupportedStaticLayer) {
retainedFallbackReason = "unsupported";
}
auto bottomDecision = retainedUnderlayCache->evaluate(bottomKey,
retainedStaticRevision,
retainedSupported,
retainedFallbackReason,
retainedUnderlayRevisionTracker.reason().c_str());
auto topDecision = retainedTopCache->evaluate(topKey,
retainedStaticRevision,
retainedSupported,
retainedFallbackReason,
retainedUnderlayRevisionTracker.reason().c_str());
const auto wantsResources = [](RetainedUnderlayDecision decision) {
return decision == RetainedUnderlayDecision::Build || decision == RetainedUnderlayDecision::Reuse;
};
bool resourcesReady = true;
if (wantsResources(bottomDecision)) {
resourcesReady = staticData->shaders &&
retainedUnderlayCache->prepareResources(context, *staticData->shaders, *bottomKey);
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy != RenderSegmentStrategy::Retained) continue;
auto& cache = retainedSegmentCaches[segment.segmentIndex];
auto& decision = segmentDecisions[segment.segmentIndex];
const auto fingerprint = orchestrator.retainedRangeContentFingerprint(
segment.beginIndex, segment.endExclusive);
const bool layerContentChanged = retainedSegmentFingerprints[segment.segmentIndex] != 0 &&
retainedSegmentFingerprints[segment.segmentIndex] != fingerprint;
retainedSegmentFingerprints[segment.segmentIndex] = fingerprint;
const auto segmentContentRevision = retainedStaticRevision ^
(fingerprint +
0x9e3779b97f4a7c15ULL + (retainedStaticRevision << 6U) + (retainedStaticRevision >> 2U));
decision = cache->evaluate(segmentKeys[segment.segmentIndex],
segmentContentRevision,
retainedSupported,
retainedFallbackReason,
layerContentChanged ? "layer-mutation"
: retainedUnderlayRevisionTracker.reason().c_str());
if (resourcesReady && wantsResources(decision)) {
resourcesReady = staticData->shaders && cache->prepareResources(
context, *staticData->shaders, *segmentKeys[segment.segmentIndex]);
}
if (resourcesReady && wantsResources(topDecision)) {
resourcesReady = staticData->shaders &&
retainedTopCache->prepareResources(context, *staticData->shaders, *topKey);
}
if (!resourcesReady) {
if (wantsResources(bottomDecision)) retainedUnderlayCache->markBuildFailed("resource-or-compositor");
if (wantsResources(topDecision)) retainedTopCache->markBuildFailed("resource-or-compositor");
bottomDecision = topDecision = RetainedUnderlayDecision::Fallback;
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy != RenderSegmentStrategy::Retained) continue;
auto& decision = segmentDecisions[segment.segmentIndex];
if (wantsResources(decision)) {
retainedSegmentCaches[segment.segmentIndex]->markBuildFailed("resource-or-compositor");
decision = RetainedUnderlayDecision::Fallback;
}
}
}
bool useRetainedRanges = retainedSupported;
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy == RenderSegmentStrategy::Retained) {
useRetainedRanges = useRetainedRanges && wantsResources(segmentDecisions[segment.segmentIndex]);
}
}
const bool useRetainedRanges = retainedSupported &&
(!ranges.bottomPresent() || wantsResources(bottomDecision)) &&
(!ranges.topPresent() || wantsResources(topDecision));
// Upload layer groups
{
@@ -663,9 +690,9 @@ void Renderer::Impl::render(const RenderTree& renderTree,
// Tweakers are run in the upload pass so they can set up uniforms.
parameters.currentLayer = 0;
if (particleFastFrame && retainedMode) {
if (particleFastFrame && singleContinuous) {
orchestrator.visitLayerGroupsInRange(
ranges.continuousBegin, ranges.continuousEndExclusive,
singleContinuous->beginIndex, singleContinuous->endExclusive,
[&](LayerGroupBase& layerGroup, size_t ordinal) {
parameters.currentLayer = static_cast<uint32_t>(ordinal);
layerGroup.runTweakers(renderTree, parameters);
@@ -683,9 +710,9 @@ void Renderer::Impl::render(const RenderTree& renderTree,
}
// Give the layers a chance to upload
if (particleFastFrame && retainedMode) {
if (particleFastFrame && singleContinuous) {
orchestrator.visitLayerGroupsInRange(
ranges.continuousBegin, ranges.continuousEndExclusive,
singleContinuous->beginIndex, singleContinuous->endExclusive,
[&](LayerGroupBase& layerGroup, size_t) { layerGroup.upload(*uploadPass); });
} else {
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); });
@@ -698,8 +725,13 @@ void Renderer::Impl::render(const RenderTree& renderTree,
if (!particleFastFrame) {
orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); });
}
if (useRetainedRanges && ranges.bottomPresent()) retainedUnderlayCache->upload(*uploadPass);
if (useRetainedRanges && ranges.topPresent()) retainedTopCache->upload(*uploadPass);
if (useRetainedRanges) {
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy == RenderSegmentStrategy::Retained) {
retainedSegmentCaches[segment.segmentIndex]->upload(*uploadPass);
}
}
}
}
const Size atlasSize = parameters.patternAtlas.getPixelSize();
@@ -818,7 +850,7 @@ void Renderer::Impl::render(const RenderTree& renderTree,
// Actually render the layers
// Drawables
const auto drawableOpaquePass = [&] {
const auto drawableOpaquePass = [&](int32_t begin, int32_t endExclusive) {
const auto debugGroup(parameters.renderPass->createDebugGroup("drawables-opaque"));
parameters.pass = RenderPass::Opaque;
parameters.depthRangeSize = 1 - (orchestrator.numLayerGroups() + 2) * PaintParameters::numSublayers *
@@ -827,7 +859,7 @@ void Renderer::Impl::render(const RenderTree& renderTree,
// draw layer groups, opaque pass
if (useRetainedRanges && retainedMode) {
orchestrator.visitLayerGroupsInRangeReversed(
ranges.continuousBegin, ranges.continuousEndExclusive,
begin, endExclusive,
[&](LayerGroupBase& layerGroup, size_t ordinal) {
parameters.currentLayer = static_cast<uint32_t>(orchestrator.numLayerGroups() - 1 - ordinal);
layerGroup.render(orchestrator, parameters);
@@ -842,7 +874,7 @@ void Renderer::Impl::render(const RenderTree& renderTree,
}
};
const auto drawableTranslucentPass = [&] {
const auto drawableTranslucentPass = [&](int32_t begin, int32_t endExclusive) {
const auto debugGroup(parameters.renderPass->createDebugGroup("drawables-translucent"));
parameters.pass = RenderPass::Translucent;
parameters.depthRangeSize = 1 - (orchestrator.numLayerGroups() + 2) * PaintParameters::numSublayers *
@@ -851,7 +883,7 @@ void Renderer::Impl::render(const RenderTree& renderTree,
// draw layer groups, translucent pass
if (useRetainedRanges && retainedMode) {
orchestrator.visitLayerGroupsInRange(
ranges.continuousBegin, ranges.continuousEndExclusive,
begin, endExclusive,
[&](LayerGroupBase& layerGroup, size_t ordinal) {
parameters.currentLayer = static_cast<uint32_t>(orchestrator.numLayerGroups() - 1 - ordinal);
layerGroup.render(orchestrator, parameters);
@@ -877,8 +909,8 @@ void Renderer::Impl::render(const RenderTree& renderTree,
const LayerRenderItem& layerItem = *it;
const RenderItem& item = layerItem;
const bool itemInLiveRange = !useRetainedRanges || !retainedMode ||
(static_cast<int32_t>(layerItem.index) >= ranges.continuousBegin &&
static_cast<int32_t>(layerItem.index) < ranges.continuousEndExclusive);
(static_cast<int32_t>(layerItem.index) >= begin &&
static_cast<int32_t>(layerItem.index) < endExclusive);
if (itemInLiveRange && item.hasRenderPass(parameters.pass)) {
item.render(parameters);
}
@@ -903,20 +935,35 @@ void Renderer::Impl::render(const RenderTree& renderTree,
drawable3DPass();
}
drawableTargetsPass();
buildRetainedRange(*retainedUnderlayCache, bottomDecision, 0, ranges.continuousBegin);
buildRetainedRange(*retainedTopCache, topDecision, ranges.continuousEndExclusive, ranges.layerCount);
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy == RenderSegmentStrategy::Retained) {
buildRetainedRange(*retainedSegmentCaches[segment.segmentIndex],
segmentDecisions[segment.segmentIndex],
segment.beginIndex,
segment.endExclusive);
}
}
commonClearPass();
context.bindGlobalUniformBuffers(*parameters.renderPass);
if (useRetainedRanges && ranges.bottomPresent()) retainedUnderlayCache->composite(orchestrator, parameters);
drawableOpaquePass();
drawableTranslucentPass();
if (useRetainedRanges && ranges.topPresent()) retainedTopCache->composite(orchestrator, parameters);
if (useRetainedRanges) {
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy == RenderSegmentStrategy::Retained) {
retainedSegmentCaches[segment.segmentIndex]->composite(orchestrator, parameters);
} else {
drawableOpaquePass(segment.beginIndex, segment.endExclusive);
drawableTranslucentPass(segment.beginIndex, segment.endExclusive);
}
}
} else {
drawableOpaquePass(0, segmentPlan.layerCount);
drawableTranslucentPass(0, segmentPlan.layerCount);
}
drawableDebugOverlays();
// Give the layers a chance to do cleanup
if (particleFastFrame && retainedMode) {
if (particleFastFrame && singleContinuous) {
orchestrator.visitLayerGroupsInRange(
ranges.continuousBegin, ranges.continuousEndExclusive,
singleContinuous->beginIndex, singleContinuous->endExclusive,
[&](LayerGroupBase& layerGroup, size_t) {
layerGroup.postRender(orchestrator, parameters);
});
@@ -966,7 +1013,12 @@ void Renderer::Impl::render(const RenderTree& renderTree,
}
#ifndef NDEBUG
logRetainedRendererCounters(*retainedUnderlayCache, particleFastPathLastReason);
for (const auto& cache : retainedSegmentCaches) {
if (cache) {
logRetainedRendererCounters(*cache, particleFastPathLastReason);
break;
}
}
#endif
frameCount += 1;

View File

@@ -12,6 +12,7 @@
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace mbgl {
@@ -61,8 +62,10 @@ private:
const float pixelRatio;
std::unique_ptr<RenderStaticData> staticData;
std::unique_ptr<RetainedUnderlayCache> retainedUnderlayCache;
std::unique_ptr<RetainedUnderlayCache> retainedTopCache;
std::vector<std::unique_ptr<RetainedUnderlayCache>> retainedSegmentCaches;
std::vector<uint64_t> retainedSegmentFingerprints;
std::shared_ptr<RetainedUnderlayRuntimeState> retainedRuntimeState;
uint64_t retainedCacheTopologyRevision = 0;
RetainedUnderlayRevisionTracker retainedUnderlayRevisionTracker;
RetainedUnderlayTopologyTracker retainedUnderlayTopologyTracker;
ParticleFastPathPreparedTree<RenderTree> particlePreparedTree;
@@ -110,8 +113,6 @@ private:
std::optional<int32_t> particlePreparedBoundary;
std::optional<int32_t> particlePreparedContinuousEnd;
uint64_t particlePreparedRangeConfigRevision = 0;
bool particlePreparedBottomPresent = false;
bool particlePreparedTopPresent = false;
bool particlePreparedContinuousUnsupported = false;
Size particlePreparedBackendSize;
bool particlePreparedSceneSupported = false;

View File

@@ -110,7 +110,16 @@ uint64_t RetainedUnderlayRevisionTracker::observe(const RetainedUnderlayRevision
RetainedUnderlayCache::RetainedUnderlayCache(RetainedRangeRole role_)
: runtimeState(std::make_shared<RetainedUnderlayRuntimeState>()), role(role_) {}
RetainedUnderlayCache::~RetainedUnderlayCache() = default;
RetainedUnderlayCache::RetainedUnderlayCache(uint32_t segmentIndex_,
std::string label_,
RetainedRangeRole compatibilityRole)
: runtimeState(std::make_shared<RetainedUnderlayRuntimeState>()),
role(compatibilityRole),
renderSegmentIndex(segmentIndex_),
renderSegmentLabel(std::move(label_)) {}
RetainedUnderlayCache::~RetainedUnderlayCache() {
releaseResources();
}
void RetainedUnderlayCache::synchronizeRuntimeState(
const std::shared_ptr<RetainedUnderlayRuntimeState>& nextState) {
@@ -136,6 +145,9 @@ void RetainedUnderlayCache::synchronizeRuntimeState(
runtimeState->recordRangeInvalidation(role == RetainedRangeRole::Top);
runtimeState->recordRangeInvalidationReason(
role == RetainedRangeRole::Top, RetainedUnderlayReason::RuntimeDisabled);
if (renderSegmentIndex) {
runtimeState->recordSegmentInvalidation(*renderSegmentIndex, RetainedUnderlayReason::RuntimeDisabled);
}
}
}
releaseResources();
@@ -159,6 +171,9 @@ RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<Ret
releaseResources();
}
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Disabled);
if (renderSegmentIndex) {
runtimeState->recordSegmentCacheState(*renderSegmentIndex, RetainedRangeCacheState::Disabled);
}
return RetainedUnderlayDecision::Disabled;
}
if (runtimeState && !runtimeState->isEnabled()) {
@@ -166,6 +181,9 @@ RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<Ret
runtimeState->recordFallback();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Fallback);
if (renderSegmentIndex) {
runtimeState->recordSegmentCacheState(*renderSegmentIndex, RetainedRangeCacheState::Fallback);
}
return RetainedUnderlayDecision::Fallback;
}
if (!supported) {
@@ -175,6 +193,9 @@ RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<Ret
invalidate(fallbackReason ? fallbackReason : "unsupported");
if (revisionChanged) releaseResources();
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Fallback);
if (renderSegmentIndex) {
runtimeState->recordSegmentCacheState(*renderSegmentIndex, RetainedRangeCacheState::Fallback);
}
return RetainedUnderlayDecision::Fallback;
}
if (revisionChanged) {
@@ -185,7 +206,8 @@ RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<Ret
releaseResources();
key = nextKey;
} else if (!key || *key != *nextKey) {
const char* reason = !key ? "first-frame" : (key->viewport != nextKey->viewport ? "viewport" : "camera-or-boundary");
const char* reason = !key ? "first-frame"
: (key->viewport != nextKey->viewport ? "viewport" : "camera-or-boundary");
invalidate(reason);
key = nextKey;
}
@@ -200,9 +222,18 @@ RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<Ret
runtimeState->recordRangeReuse(role == RetainedRangeRole::Top);
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayReuse);
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Reuse);
if (renderSegmentIndex) {
runtimeState->recordSegmentReuse(*renderSegmentIndex);
runtimeState->recordSegmentCacheState(
*renderSegmentIndex, RetainedRangeCacheState::Reuse, observedStaticRevision);
}
return RetainedUnderlayDecision::Reuse;
}
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::BuildPending);
if (renderSegmentIndex) {
runtimeState->recordSegmentCacheState(
*renderSegmentIndex, RetainedRangeCacheState::BuildPending, observedStaticRevision);
}
return RetainedUnderlayDecision::Build;
}
@@ -214,6 +245,7 @@ bool RetainedUnderlayCache::prepareResources(gfx::Context& context,
offscreenTexture = context.createOffscreenTexture(
nextKey.viewport, gfx::TextureChannelDataType::UnsignedByte, true, true);
runtimeState->recordRangeTextureCreate(role == RetainedRangeRole::Top);
if (renderSegmentIndex) runtimeState->recordSegmentTextureCreate(*renderSegmentIndex);
compositeLayerGroup.reset();
}
if (!offscreenTexture || !offscreenTexture->isRenderable()) return false;
@@ -226,7 +258,10 @@ bool RetainedUnderlayCache::prepareResources(gfx::Context& context,
bool RetainedUnderlayCache::rebuildComposite(gfx::Context& context, gfx::ShaderRegistry& shaders) {
auto shader = context.getGenericShader(shaders, CompositeShaderName);
auto group = context.createLayerGroup(
0, 1, role == RetainedRangeRole::Top ? "retained-top-composite" : "retained-bottom-composite");
0,
1,
renderSegmentIndex ? renderSegmentLabel + "-composite"
: (role == RetainedRangeRole::Top ? "retained-top-composite" : "retained-bottom-composite"));
if (!shader || !group || !offscreenTexture) return false;
auto vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(
@@ -258,6 +293,7 @@ bool RetainedUnderlayCache::rebuildComposite(gfx::Context& context, gfx::ShaderR
for (auto& drawable : builder->clearDrawables()) group->addDrawable(std::move(drawable));
compositeLayerGroup = std::move(group);
runtimeState->recordRangeCompositeCreate(role == RetainedRangeRole::Top);
if (renderSegmentIndex) runtimeState->recordSegmentCompositeCreate(*renderSegmentIndex);
return !compositeLayerGroup->empty();
}
@@ -295,6 +331,11 @@ void RetainedUnderlayCache::markBuildSucceeded() {
++buildCount;
runtimeState->recordBuild();
runtimeState->recordRangeBuild(role == RetainedRangeRole::Top);
if (renderSegmentIndex) {
runtimeState->recordSegmentBuild(*renderSegmentIndex);
runtimeState->recordSegmentCacheState(
*renderSegmentIndex, RetainedRangeCacheState::Ready, observedStaticRevision);
}
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Ready);
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayBuild);
}
@@ -305,6 +346,9 @@ void RetainedUnderlayCache::markBuildFailed(const char* reason) {
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
invalidate(reason);
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Fallback);
if (renderSegmentIndex) {
runtimeState->recordSegmentCacheState(*renderSegmentIndex, RetainedRangeCacheState::Fallback);
}
releaseResources();
}
@@ -314,12 +358,14 @@ void RetainedUnderlayCache::invalidate(const char* reason) {
runtimeState->recordInvalidation(retainedReason(reason));
runtimeState->recordRangeInvalidation(role == RetainedRangeRole::Top);
runtimeState->recordRangeInvalidationReason(role == RetainedRangeRole::Top, retainedReason(reason));
if (renderSegmentIndex) runtimeState->recordSegmentInvalidation(*renderSegmentIndex, retainedReason(reason));
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayInvalidation);
}
void RetainedUnderlayCache::releaseResources() {
if (offscreenTexture || compositeLayerGroup) {
runtimeState->recordRangeResourceRelease(role == RetainedRangeRole::Top);
if (renderSegmentIndex) runtimeState->recordSegmentResourceRelease(*renderSegmentIndex);
}
valid = false;
buildRevision = 0;

View File

@@ -115,6 +115,7 @@ inline bool retainedLayerGroupInRange(int32_t layerIndex, int32_t boundary, Reta
class RetainedUnderlayCache {
public:
explicit RetainedUnderlayCache(RetainedRangeRole = RetainedRangeRole::Bottom);
RetainedUnderlayCache(uint32_t segmentIndex, std::string label, RetainedRangeRole compatibilityRole);
~RetainedUnderlayCache();
RetainedUnderlayDecision evaluate(const std::optional<RetainedUnderlayKey>&,
@@ -163,6 +164,8 @@ private:
std::shared_ptr<RetainedUnderlayRuntimeState> runtimeState;
uint64_t observedControlRevision = 0;
RetainedRangeRole role;
std::optional<uint32_t> renderSegmentIndex;
std::string renderSegmentLabel;
};
} // namespace mbgl

View File

@@ -2,9 +2,12 @@
#include <mbgl/renderer/particle_fast_path.hpp>
#include <mbgl/renderer/retained_underlay.hpp>
#include <mbgl/renderer/render_segment.hpp>
#include <mbgl/renderer/render_segment_plan.hpp>
#include <atomic>
#include <mutex>
#include <tuple>
#include <utility>
namespace mbgl {
@@ -117,17 +120,167 @@ public:
rangeResolution.endLayerId = rangeConfig.endLayerId;
rangeResolution.configRevision = rangeConfig.revision;
rangeResolution.reason = "style-not-ready";
renderSegmentConfig.source = RenderSegmentConfigSource::ContinuousRangeCompat;
renderSegmentConfig.continuousStartLayerId = rangeConfig.startLayerId;
renderSegmentConfig.continuousEndLayerId = rangeConfig.endLayerId;
renderSegmentConfig.plan = {};
renderSegmentConfig.revision++;
fallback.fetch_add(1, std::memory_order_relaxed);
}
controlRevision.fetch_add(1, std::memory_order_release);
return true;
}
bool setRenderSegmentPlan(RenderSegmentPlan plan) {
bool accepted = plan.schemaVersion == 1 && !plan.segments.empty();
if (accepted && plan.segments.front().startLayerId && !plan.segments.front().startLayerId->empty()) {
accepted = false;
}
for (std::size_t i = 1; accepted && i < plan.segments.size(); ++i) {
if (!plan.segments[i].startLayerId || plan.segments[i].startLayerId->empty()) accepted = false;
}
{
std::lock_guard<std::mutex> lock(rangeMutex);
if (renderSegmentConfig.source == RenderSegmentConfigSource::ExplicitPlan &&
renderSegmentConfig.plan == plan) {
return accepted;
}
renderSegmentConfig.source = RenderSegmentConfigSource::ExplicitPlan;
renderSegmentConfig.plan = std::move(plan);
renderSegmentConfig.continuousStartLayerId.clear();
renderSegmentConfig.continuousEndLayerId.clear();
renderSegmentConfig.revision++;
rangeConfig = {};
rangeResolution = {};
bottomRangePresent = false;
topRangePresent = false;
}
controlRevision.fetch_add(1, std::memory_order_release);
return accepted;
}
RenderSegmentConfig renderSegmentPlanConfig() const {
std::lock_guard<std::mutex> lock(rangeMutex);
return renderSegmentConfig;
}
ContinuousLayerRangeConfig continuousLayerRangeConfig() const {
std::lock_guard<std::mutex> lock(rangeMutex);
if (renderSegmentConfig.source == RenderSegmentConfigSource::ExplicitPlan) return {};
return rangeConfig;
}
void recordRenderSegmentResolution(const ResolvedRenderSegmentPlan& resolution,
std::vector<StyleLayerDiagnostics> styleLayers) {
std::lock_guard<std::mutex> lock(rangeMutex);
if (resolution.configRevision != renderSegmentConfig.revision && resolution.configured) return;
std::vector<std::tuple<RenderSegmentStrategy, int32_t, int32_t>> signature;
signature.reserve(resolution.segments.size());
for (const auto& segment : resolution.segments) {
signature.emplace_back(segment.strategy, segment.beginIndex, segment.endExclusive);
}
const bool topologyChanged = signature != renderSegmentTopologySignature;
if (topologyChanged) {
++renderSegmentTopologyRevision;
renderSegmentTopologySignature = signature;
}
RenderSegmentDiagnostics next;
next.enabled = isEnabled();
next.configured = resolution.configured;
next.valid = resolution.valid;
next.configSource = resolution.configSource;
next.configRevision = resolution.configRevision;
next.topologyRevision = renderSegmentTopologyRevision;
next.styleLayerCount = static_cast<uint32_t>(styleLayers.size());
next.coverageComplete = renderSegmentCoverageComplete(resolution);
next.fallback = resolution.configured && !resolution.valid;
next.lastReason = resolution.reason;
next.styleLayers = std::move(styleLayers);
next.fastPathEligible = fastPathEligible.load(std::memory_order_acquire);
next.fastPathActive = fastPathActive.load(std::memory_order_acquire);
next.fastPathFallbackReason = particleFastPathMissReasonText(fastPathReason.load(std::memory_order_acquire));
next.fastPathHit = fastPathHit.load(std::memory_order_relaxed);
next.fastPathMiss = fastPathMiss.load(std::memory_order_relaxed);
next.fullCpuFrame = fullCpuFrame.load(std::memory_order_relaxed);
next.segments.reserve(resolution.segments.size());
for (const auto& segment : resolution.segments) {
RenderSegmentDiagnosticsEntry entry;
entry.segmentIndex = segment.segmentIndex;
entry.strategy = segment.strategy;
entry.configuredStartLayerId = segment.configuredStartLayerId;
entry.beginIndex = segment.beginIndex;
entry.endExclusive = segment.endExclusive;
if (segment.beginIndex >= 0 && segment.beginIndex < static_cast<int32_t>(next.styleLayers.size())) {
entry.resolvedStartLayerId = next.styleLayers[segment.beginIndex].layerId;
}
if (segment.endExclusive > segment.beginIndex &&
segment.endExclusive <= static_cast<int32_t>(next.styleLayers.size())) {
entry.resolvedEndLayerId = next.styleLayers[segment.endExclusive - 1].layerId;
}
if (segment.beginIndex >= 0 && segment.endExclusive <= static_cast<int32_t>(next.styleLayers.size())) {
entry.layers.assign(next.styleLayers.begin() + segment.beginIndex,
next.styleLayers.begin() + segment.endExclusive);
}
if (segment.strategy == RenderSegmentStrategy::Retained) {
++next.retainedSegmentCount;
entry.cache.emplace();
if (!topologyChanged && segment.segmentIndex < renderSegmentDiagnostics.segments.size() &&
renderSegmentDiagnostics.segments[segment.segmentIndex].cache) {
entry.cache = renderSegmentDiagnostics.segments[segment.segmentIndex].cache;
}
} else {
++next.continuousSegmentCount;
}
next.segments.push_back(std::move(entry));
}
next.segmentCount = static_cast<uint32_t>(next.segments.size());
renderSegmentDiagnostics = std::move(next);
}
RenderSegmentDiagnostics renderSegmentSnapshot() const {
std::lock_guard<std::mutex> lock(rangeMutex);
auto result = renderSegmentDiagnostics;
result.enabled = isEnabled();
result.fastPathEligible = fastPathEligible.load(std::memory_order_acquire);
result.fastPathActive = fastPathActive.load(std::memory_order_acquire);
result.fastPathFallbackReason = particleFastPathMissReasonText(fastPathReason.load(std::memory_order_acquire));
result.fastPathHit = fastPathHit.load(std::memory_order_relaxed);
result.fastPathMiss = fastPathMiss.load(std::memory_order_relaxed);
result.fullCpuFrame = fullCpuFrame.load(std::memory_order_relaxed);
return result;
}
void recordSegmentCacheState(uint32_t segmentIndex,
RetainedRangeCacheState state,
uint64_t contentRevision = 0) {
std::lock_guard<std::mutex> lock(rangeMutex);
if (segmentIndex >= renderSegmentDiagnostics.segments.size()) return;
auto& entry = renderSegmentDiagnostics.segments[segmentIndex];
if (!entry.cache) return;
entry.cache->cacheState = retainedRangeCacheStateText(state);
entry.cache->contentRevision = contentRevision;
}
void recordSegmentBuild(uint32_t index) { updateSegmentCache(index, [](auto& value) { ++value.build; }); }
void recordSegmentReuse(uint32_t index) { updateSegmentCache(index, [](auto& value) { ++value.reuse; }); }
void recordSegmentInvalidation(uint32_t index, RetainedUnderlayReason reason) {
updateSegmentCache(index, [reason](auto& value) {
++value.invalidation;
value.lastInvalidationReason = retainedUnderlayReasonText(reason);
});
}
void recordSegmentTextureCreate(uint32_t index) {
updateSegmentCache(index, [](auto& value) { ++value.textureCreate; });
}
void recordSegmentCompositeCreate(uint32_t index) {
updateSegmentCache(index, [](auto& value) { ++value.compositeCreate; });
}
void recordSegmentResourceRelease(uint32_t index) {
updateSegmentCache(index, [](auto& value) { ++value.resourceRelease; });
}
void recordRangeResolution(const ContinuousLayerRangeResolution& value, bool bottom, bool top) {
std::lock_guard<std::mutex> lock(rangeMutex);
if (value.configRevision != rangeConfig.revision) return;
@@ -241,6 +394,14 @@ public:
fastPathActive.store(false, std::memory_order_release);
}
private:
template <typename Fn>
void updateSegmentCache(uint32_t index, Fn&& fn) {
std::lock_guard<std::mutex> lock(rangeMutex);
if (index >= renderSegmentDiagnostics.segments.size()) return;
auto& cache = renderSegmentDiagnostics.segments[index].cache;
if (cache) fn(*cache);
}
std::atomic<bool> enabled{true};
std::atomic<uint64_t> controlRevision{0};
std::atomic<uint64_t> build{0};
@@ -275,6 +436,10 @@ private:
ContinuousLayerRangeResolution rangeResolution;
bool bottomRangePresent = false;
bool topRangePresent = false;
RenderSegmentConfig renderSegmentConfig;
RenderSegmentDiagnostics renderSegmentDiagnostics;
uint64_t renderSegmentTopologyRevision = 0;
std::vector<std::tuple<RenderSegmentStrategy, int32_t, int32_t>> renderSegmentTopologySignature;
};
} // namespace mbgl

View File

@@ -1,9 +1,13 @@
#include <mbgl/renderer/retained_underlay_cache.hpp>
#include <mbgl/renderer/particle_fast_path.hpp>
#include <mbgl/renderer/continuous_layer_range.hpp>
#include <mbgl/renderer/render_segment_json.hpp>
#include <mbgl/renderer/render_segment_plan.hpp>
#include <gtest/gtest.h>
#include <algorithm>
#include <initializer_list>
#include <utility>
#include <vector>
@@ -635,3 +639,282 @@ TEST(ParticleFastPath, PreparedTreeReplacementOwnsLifetime) {
EXPECT_EQ(destroyed, 2);
EXPECT_TRUE(slot.empty());
}
namespace {
RenderSegmentConfig explicitSegments(std::initializer_list<RenderSegmentDefinition> definitions) {
RenderSegmentConfig config;
config.source = RenderSegmentConfigSource::ExplicitPlan;
config.plan.segments.assign(definitions.begin(), definitions.end());
config.revision = 1;
return config;
}
std::vector<std::string> segmentLayerIds() {
return {"background", "water", "weather", "particle", "land", "ais"};
}
} // namespace
TEST(RenderSegmentPlan, SingleRetainedAndSingleContinuousCoverWholeStyle) {
auto retained = resolveRenderSegmentPlan(
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt}}), segmentLayerIds(), true);
ASSERT_TRUE(retained.valid);
ASSERT_EQ(retained.segments.size(), 1u);
EXPECT_EQ(retained.segments[0].beginIndex, 0);
EXPECT_EQ(retained.segments[0].endExclusive, 6);
EXPECT_TRUE(renderSegmentCoverageComplete(retained));
auto continuous = resolveRenderSegmentPlan(
explicitSegments({{RenderSegmentStrategy::Continuous, std::nullopt}}), segmentLayerIds(), true);
ASSERT_TRUE(continuous.valid);
EXPECT_EQ(continuous.segments[0].strategy, RenderSegmentStrategy::Continuous);
EXPECT_TRUE(renderSegmentCoverageComplete(continuous));
}
TEST(RenderSegmentPlan, PreservesCRRAndRRCWithoutMerging) {
auto crr = resolveRenderSegmentPlan(
explicitSegments({{RenderSegmentStrategy::Continuous, std::nullopt},
{RenderSegmentStrategy::Retained, "weather"},
{RenderSegmentStrategy::Retained, "land"}}),
segmentLayerIds(), true);
ASSERT_TRUE(crr.valid);
ASSERT_EQ(crr.segments.size(), 3u);
EXPECT_EQ(crr.segments[1].endExclusive, crr.segments[2].beginIndex);
EXPECT_EQ(crr.segments[1].strategy, RenderSegmentStrategy::Retained);
EXPECT_EQ(crr.segments[2].strategy, RenderSegmentStrategy::Retained);
auto rrc = resolveRenderSegmentPlan(
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Retained, "weather"},
{RenderSegmentStrategy::Continuous, "land"}}),
segmentLayerIds(), true);
ASSERT_TRUE(rrc.valid);
ASSERT_EQ(rrc.segments.size(), 3u);
EXPECT_EQ(rrc.segments[0].strategy, RenderSegmentStrategy::Retained);
EXPECT_EQ(rrc.segments[1].strategy, RenderSegmentStrategy::Retained);
}
TEST(RenderSegmentPlan, ResolvesFiveAlternatingSegmentsWithGlobalIndices) {
auto plan = resolveRenderSegmentPlan(
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "water"},
{RenderSegmentStrategy::Retained, "weather"},
{RenderSegmentStrategy::Continuous, "particle"},
{RenderSegmentStrategy::Retained, "land"}}),
segmentLayerIds(), true);
ASSERT_TRUE(plan.valid);
ASSERT_EQ(plan.segments.size(), 5u);
for (std::size_t i = 0; i < plan.segments.size(); ++i) {
EXPECT_EQ(plan.segments[i].segmentIndex, i);
}
EXPECT_EQ(plan.segments[3].beginIndex, 3);
EXPECT_EQ(plan.segments[4].endExclusive, 6);
EXPECT_TRUE(renderSegmentCoverageComplete(plan));
const auto continuousCount = std::count_if(plan.segments.begin(), plan.segments.end(), [](const auto& segment) {
return segment.strategy == RenderSegmentStrategy::Continuous;
});
ASSERT_EQ(continuousCount, 2);
auto fastInput = fastPathInput();
fastInput.unsupportedContinuousLayer = true;
const auto fastDecision = evaluateParticleFastPath(fastInput);
EXPECT_FALSE(fastDecision.candidate);
EXPECT_EQ(fastDecision.reason, ParticleFastPathMissReason::UnsupportedContinuousLayer);
}
TEST(RenderSegmentPlan, RejectsMissingDuplicateReversedAndEmptyDefinitions) {
auto empty = resolveRenderSegmentPlan(explicitSegments({}), segmentLayerIds(), true);
EXPECT_FALSE(empty.valid);
EXPECT_EQ(empty.reason, "empty-segments");
auto missing = resolveRenderSegmentPlan(
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "missing"}}),
segmentLayerIds(), true);
EXPECT_FALSE(missing.valid);
EXPECT_EQ(missing.reason, "boundary-layer-missing");
auto duplicate = resolveRenderSegmentPlan(
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "weather"},
{RenderSegmentStrategy::Retained, "weather"}}),
segmentLayerIds(), true);
EXPECT_FALSE(duplicate.valid);
EXPECT_EQ(duplicate.reason, "boundary-layer-duplicate");
auto reversed = resolveRenderSegmentPlan(
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "land"},
{RenderSegmentStrategy::Retained, "weather"}}),
segmentLayerIds(), true);
EXPECT_FALSE(reversed.valid);
EXPECT_EQ(reversed.reason, "boundary-order-invalid");
}
TEST(RenderSegmentPlan, HiddenBoundaryDoesNotAffectResolution) {
const auto before = resolveRenderSegmentPlan(
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "weather"}}),
segmentLayerIds(), true);
const auto after = resolveRenderSegmentPlan(
explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "weather"}}),
segmentLayerIds(), true);
EXPECT_EQ(before, after);
}
TEST(RenderSegmentPlan, ContinuousRangeCompatibilityMapsToOneTwoOrThreeSegments) {
RenderSegmentConfig config;
config.source = RenderSegmentConfigSource::ContinuousRangeCompat;
config.continuousStartLayerId = "weather";
config.continuousEndLayerId = "particle";
config.revision = 8;
auto rcr = resolveRenderSegmentPlan(config, segmentLayerIds(), true);
ASSERT_TRUE(rcr.valid);
ASSERT_EQ(rcr.segments.size(), 3u);
EXPECT_EQ(rcr.segments[0].strategy, RenderSegmentStrategy::Retained);
EXPECT_EQ(rcr.segments[1].strategy, RenderSegmentStrategy::Continuous);
EXPECT_EQ(rcr.segments[2].strategy, RenderSegmentStrategy::Retained);
EXPECT_EQ(rcr.configSource, "continuous-range-compat");
config.continuousStartLayerId = "background";
config.continuousEndLayerId = "ais";
auto continuousOnly = resolveRenderSegmentPlan(config, segmentLayerIds(), true);
ASSERT_TRUE(continuousOnly.valid);
ASSERT_EQ(continuousOnly.segments.size(), 1u);
EXPECT_EQ(continuousOnly.segments[0].strategy, RenderSegmentStrategy::Continuous);
}
TEST(RenderSegmentPlan, BoundaryDeletionFailsClosedAndRestores) {
const auto config = explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "weather"}});
auto valid = resolveRenderSegmentPlan(config, segmentLayerIds(), true);
ASSERT_TRUE(valid.valid);
auto missing = resolveRenderSegmentPlan(
config, {"background", "water", "particle", "land", "ais"}, true);
EXPECT_FALSE(missing.valid);
EXPECT_TRUE(missing.segments.empty());
EXPECT_EQ(missing.reason, "boundary-layer-missing");
EXPECT_TRUE(resolveRenderSegmentPlan(config, segmentLayerIds(), true).valid);
}
TEST(RenderSegmentPlan, InsertedLayerNaturallyJoinsContainingSegment) {
const auto config = explicitSegments({{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "weather"},
{RenderSegmentStrategy::Retained, "land"}});
auto plan = resolveRenderSegmentPlan(
config, {"background", "water", "weather", "inserted", "particle", "land", "ais"}, true);
ASSERT_TRUE(plan.valid);
EXPECT_EQ(plan.segments[1].beginIndex, 2);
EXPECT_EQ(plan.segments[1].endExclusive, 5);
EXPECT_EQ(plan.segments[2].beginIndex, 5);
}
TEST(RenderSegmentDiagnostics, VisibilityChangesMetadataButNotTopologyRevision) {
RetainedUnderlayRuntimeState state;
RenderSegmentPlan configured;
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "weather"}};
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
const auto resolved = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
std::vector<StyleLayerDiagnostics> visible;
for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) {
visible.push_back({static_cast<int32_t>(i), segmentLayerIds()[i], "fill", true, std::nullopt, std::nullopt});
}
state.recordRenderSegmentResolution(resolved, visible);
const auto revision = state.renderSegmentSnapshot().topologyRevision;
visible[2].visible = false;
state.recordRenderSegmentResolution(resolved, visible);
const auto hidden = state.renderSegmentSnapshot();
EXPECT_EQ(hidden.topologyRevision, revision);
EXPECT_FALSE(hidden.styleLayers[2].visible);
EXPECT_EQ(hidden.segments[1].beginIndex, 2);
}
TEST(RenderSegmentDiagnostics, CacheOffKeepsResolvedTopology) {
RetainedUnderlayRuntimeState state;
RenderSegmentPlan configured;
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "weather"}};
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
const auto resolved = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
std::vector<StyleLayerDiagnostics> layers;
for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) {
layers.push_back({static_cast<int32_t>(i), segmentLayerIds()[i], "fill", true, std::nullopt, std::nullopt});
}
state.recordRenderSegmentResolution(resolved, std::move(layers));
state.setEnabled(false);
const auto snapshot = state.renderSegmentSnapshot();
EXPECT_FALSE(snapshot.enabled);
EXPECT_TRUE(snapshot.valid);
EXPECT_TRUE(snapshot.coverageComplete);
EXPECT_EQ(snapshot.segmentCount, 2u);
}
TEST(RenderSegmentDiagnostics, InvalidPlanStillReturnsOrderedStyleLayers) {
RetainedUnderlayRuntimeState state;
RenderSegmentPlan configured;
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "missing"}};
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
const auto resolved = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true);
ASSERT_FALSE(resolved.valid);
std::vector<StyleLayerDiagnostics> layers;
for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) {
layers.push_back({static_cast<int32_t>(i), segmentLayerIds()[i], "fill", true, std::nullopt, std::nullopt});
}
state.recordRenderSegmentResolution(resolved, layers);
const auto snapshot = state.renderSegmentSnapshot();
EXPECT_FALSE(snapshot.valid);
EXPECT_TRUE(snapshot.fallback);
EXPECT_TRUE(snapshot.segments.empty());
ASSERT_EQ(snapshot.styleLayers.size(), segmentLayerIds().size());
EXPECT_EQ(snapshot.styleLayers.front().layerId, "background");
EXPECT_EQ(snapshot.styleLayers.back().layerId, "ais");
}
TEST(RenderSegmentDiagnostics, SnapshotCoversStyleExactlyAndSerializesContinuousCacheAsNull) {
RetainedUnderlayRuntimeState state;
RenderSegmentPlan configured;
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
{RenderSegmentStrategy::Continuous, "weather"},
{RenderSegmentStrategy::Retained, "land"}};
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
const auto config = state.renderSegmentPlanConfig();
const auto resolved = resolveRenderSegmentPlan(config, segmentLayerIds(), true);
std::vector<StyleLayerDiagnostics> layers;
for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) {
layers.push_back({static_cast<int32_t>(i), segmentLayerIds()[i], "fill", true, std::nullopt, std::nullopt});
}
layers[1].sourceId = "chart";
layers[1].sourceLayer = "water";
state.recordRenderSegmentResolution(resolved, layers);
const auto snapshot = state.renderSegmentSnapshot();
ASSERT_TRUE(snapshot.valid);
ASSERT_TRUE(snapshot.coverageComplete);
ASSERT_EQ(snapshot.styleLayers.size(), 6u);
ASSERT_EQ(snapshot.segments.size(), 3u);
EXPECT_TRUE(snapshot.segments[0].cache.has_value());
EXPECT_FALSE(snapshot.segments[1].cache.has_value());
std::vector<std::string> flattened;
for (const auto& segment : snapshot.segments) {
for (const auto& layer : segment.layers) flattened.push_back(layer.layerId);
}
EXPECT_EQ(flattened, segmentLayerIds());
EXPECT_EQ(snapshot.styleLayers[1].sourceId, std::optional<std::string>{"chart"});
EXPECT_EQ(snapshot.segments[0].layers[1].sourceLayer, std::optional<std::string>{"water"});
const auto json = renderSegmentDiagnosticsToJSON(snapshot);
EXPECT_NE(json.find("\"coverageComplete\":true"), std::string::npos);
EXPECT_NE(json.find("\"cache\":null"), std::string::npos);
}
TEST(RenderSegmentPlan, StrictJSONRejectsUnknownFieldsStrategyAndInvalidFirstBoundary) {
EXPECT_FALSE(parseRenderSegmentPlanJSON(
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED","unexpected":true}]})"));
EXPECT_FALSE(parseRenderSegmentPlanJSON(
R"({"schemaVersion":1,"segments":[{"strategy":"DYNAMIC"}]})"));
auto parsed = parseRenderSegmentPlanJSON(
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED","startLayerId":"water"}]})");
ASSERT_TRUE(parsed);
RetainedUnderlayRuntimeState state;
EXPECT_FALSE(state.setRenderSegmentPlan(std::move(*parsed)));
}