From a9eaec6b65cc26aa70e03002bdc1326c46cad245 Mon Sep 17 00:00:00 2001 From: tei Date: Thu, 24 Sep 2026 11:46:19 +0900 Subject: [PATCH] feat: add continuous retained render ranges --- include/mbgl/map/map.hpp | 1 + include/mbgl/renderer/retained_underlay.hpp | 39 +++ .../weather/WeatherRuntimeBridgeModule.kt | 18 ++ .../ios/WeatherRuntimeBridge.mm | 13 +- .../src/NativeWeatherRuntimeBridge.ts | 8 +- .../src/RendererRuntime.ts | 4 + packages/maplibre-particle-rn/src/types.ts | 14 + .../src/cpp/native_map_view.cpp | 24 +- .../src/cpp/native_map_view.hpp | 2 + .../maplibre/android/maps/MapLibreMap.java | 5 + .../org/maplibre/android/maps/NativeMap.java | 2 + .../maplibre/android/maps/NativeMapView.java | 11 +- .../maps/RetainedUnderlayDiagnostics.java | 39 ++- platform/ios/src/MLNMapView.h | 18 ++ platform/ios/src/MLNMapView.mm | 32 +- src/mbgl/map/map.cpp | 8 + src/mbgl/renderer/continuous_layer_range.hpp | 80 +++++ src/mbgl/renderer/render_orchestrator.cpp | 67 +++- src/mbgl/renderer/render_orchestrator.hpp | 33 ++ src/mbgl/renderer/renderer_impl.cpp | 288 ++++++++++++------ src/mbgl/renderer/renderer_impl.hpp | 5 + src/mbgl/renderer/retained_underlay_cache.cpp | 17 +- src/mbgl/renderer/retained_underlay_cache.hpp | 12 +- .../retained_underlay_runtime_state.hpp | 104 ++++++- .../renderer/retained_underlay_cache.test.cpp | 133 +++++++- 25 files changed, 857 insertions(+), 120 deletions(-) create mode 100644 src/mbgl/renderer/continuous_layer_range.hpp diff --git a/include/mbgl/map/map.hpp b/include/mbgl/map/map.hpp index 439f953ea..20c106f53 100644 --- a/include/mbgl/map/map.hpp +++ b/include/mbgl/map/map.hpp @@ -58,6 +58,7 @@ public: /** Enables or disables the map-scoped retained static underlay optimization. */ void setRetainedUnderlayEnabled(bool); + bool setContinuousLayerRange(std::string startLayerId, std::string endLayerId); bool isRetainedUnderlayEnabled() const; RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() const; diff --git a/include/mbgl/renderer/retained_underlay.hpp b/include/mbgl/renderer/retained_underlay.hpp index 62ca5ac2a..87a192605 100644 --- a/include/mbgl/renderer/retained_underlay.hpp +++ b/include/mbgl/renderer/retained_underlay.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include namespace mbgl { @@ -12,6 +13,44 @@ struct RetainedUnderlayDiagnostics { uint64_t fallback = 0; uint64_t invalidation = 0; std::string lastReason; + bool rangeConfigured = false; + bool rangeValid = false; + std::string startLayerId; + std::string endLayerId; + int32_t startIndex = -1; + int32_t endIndex = -1; + bool bottomPresent = false; + uint64_t bottomBuild = 0; + uint64_t bottomReuse = 0; + uint64_t bottomInvalidation = 0; + bool topPresent = false; + uint64_t topBuild = 0; + uint64_t topReuse = 0; + uint64_t topInvalidation = 0; + bool rangeFallback = false; +}; + +struct ContinuousLayerRangeConfig { + bool configured = false; + std::string startLayerId; + std::string endLayerId; + uint64_t revision = 0; +}; + +struct ContinuousLayerRangeResolution { + bool configured = false; + bool valid = false; + std::string startLayerId; + std::string endLayerId; + int32_t startIndex = -1; + int32_t endIndex = -1; + uint64_t configRevision = 0; + std::string reason{"not-configured"}; + + int32_t begin() const noexcept { return startIndex; } + int32_t endExclusive() const noexcept { return valid ? endIndex + 1 : -1; } + bool bottomPresent() const noexcept { return valid && startIndex > 0; } + bool topPresent(int32_t layerCount) const noexcept { return valid && endExclusive() < layerCount; } }; } // namespace mbgl diff --git a/packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt b/packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt index ebed4bfda..a6a9888e7 100644 --- a/packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt +++ b/packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt @@ -67,6 +67,9 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) : map.setRetainedUnderlayEnabled(enabled) promise.resolve("ACCEPTED") } + @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 getRetainedUnderlayDiagnostics(reactTag: Double, promise: Promise) = withMap(reactTag, promise) { map -> val value = map.retainedUnderlayDiagnostics promise.resolve(Arguments.createMap().apply { @@ -76,6 +79,21 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) : putDouble("fallback", value.fallback.toDouble()) putDouble("invalidation", value.invalidation.toDouble()) putString("lastReason", value.lastReason) + putBoolean("rangeConfigured", value.rangeConfigured) + putBoolean("rangeValid", value.rangeValid) + putString("startLayerId", value.startLayerId) + putString("endLayerId", value.endLayerId) + putInt("startIndex", value.startIndex) + putInt("endIndex", value.endIndex) + putBoolean("bottomPresent", value.bottomPresent) + putDouble("bottomBuild", value.bottomBuild.toDouble()) + putDouble("bottomReuse", value.bottomReuse.toDouble()) + putDouble("bottomInvalidation", value.bottomInvalidation.toDouble()) + putBoolean("topPresent", value.topPresent) + putDouble("topBuild", value.topBuild.toDouble()) + putDouble("topReuse", value.topReuse.toDouble()) + putDouble("topInvalidation", value.topInvalidation.toDouble()) + putBoolean("fallback", value.fallback) }) } @ReactMethod override fun clearWindFields(reactTag: Double, promise: Promise) = command(reactTag, promise) { it.clearWindFields() } diff --git a/packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.mm b/packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.mm index 8a8586e56..403d93de4 100644 --- a/packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.mm +++ b/packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.mm @@ -164,12 +164,23 @@ RCT_EXPORT_MODULE(WeatherRuntimeBridge) resolve(@"ACCEPTED"); }]; } +- (void)setContinuousLayerRange:(double)reactTag startLayerId:(NSString *)startLayerId endLayerId:(NSString *)endLayerId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { + resolve([map setContinuousLayerRangeFromLayerIdentifier:startLayerId toLayerIdentifier:endLayerId] ? @"ACCEPTED" : @"REJECTED"); + }]; +} - (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; resolve(@{ @"enabled": @(value.enabled), @"build": @(value.build), @"reuse": @(value.reuse), @"fallback": @(value.fallback), @"invalidation": @(value.invalidation), - @"lastReason": value.lastReason }); + @"lastReason": value.lastReason, @"rangeConfigured": @(value.rangeConfigured), + @"rangeValid": @(value.rangeValid), @"startLayerId": value.startLayerId, + @"endLayerId": value.endLayerId, @"startIndex": @(value.startIndex), @"endIndex": @(value.endIndex), + @"bottomPresent": @(value.bottomPresent), @"bottomBuild": @(value.bottomBuild), + @"bottomReuse": @(value.bottomReuse), @"bottomInvalidation": @(value.bottomInvalidation), + @"topPresent": @(value.topPresent), @"topBuild": @(value.topBuild), + @"topReuse": @(value.topReuse), @"topInvalidation": @(value.topInvalidation) }); }]; } diff --git a/packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts b/packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts index 11f83c3db..26dbed8b8 100644 --- a/packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts +++ b/packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts @@ -9,12 +9,18 @@ type FieldSubmitResult = "REJECTED_SOURCE_MODE" | "REJECTED_ADAPTER" | "REJECTED type EncodedFieldSubmitResult = "ACCEPTED_INSERTED" | "ACCEPTED_REPLACED_REVISION" | "DUPLICATE" | "REJECTED_DECODE" | "REJECTED_CONTRACT" | "REJECTED_INVALID_FIELD_KEY" | "REJECTED_IDENTITY_CONFLICT" | "REJECTED_STALE_REVISION" | "REJECTED_INCOMPATIBLE" | "REJECTED_CAPACITY"; type ColorStop = { speedMps: number; red: number; green: number; blue: number; alpha: number }; type RuntimeSnapshot = { state: RuntimeState; playbackState: PlaybackState; targetFps: TargetFps; disposed: boolean }; -type RetainedUnderlaySnapshot = { enabled: boolean; build: number; reuse: number; fallback: number; invalidation: number; lastReason: string }; +type RetainedUnderlaySnapshot = { + enabled: boolean; build: number; reuse: number; fallback: number; invalidation: number; lastReason: string; + rangeConfigured: boolean; rangeValid: boolean; startLayerId: string; endLayerId: string; + startIndex: number; endIndex: number; bottomPresent: boolean; bottomBuild: number; bottomReuse: number; + bottomInvalidation: number; topPresent: boolean; topBuild: number; topReuse: number; topInvalidation: number; +}; export interface Spec extends TurboModule { attach(reactTag: number): Promise; detach(reactTag: number): Promise; getState(reactTag: number): Promise; setRetainedUnderlayEnabled(reactTag: number, enabled: boolean): Promise; + setContinuousLayerRange(reactTag: number, startLayerId: string, endLayerId: string): Promise; getRetainedUnderlayDiagnostics(reactTag: number): Promise; clearWindFields(reactTag: number): Promise; submitWindField(reactTag: number, metadataJson: string, payloadFileUri: string): Promise; diff --git a/packages/maplibre-particle-rn/src/RendererRuntime.ts b/packages/maplibre-particle-rn/src/RendererRuntime.ts index 63628d427..ebed69326 100644 --- a/packages/maplibre-particle-rn/src/RendererRuntime.ts +++ b/packages/maplibre-particle-rn/src/RendererRuntime.ts @@ -13,6 +13,10 @@ function nativeBridge() { export const RendererRuntime = { setRetainedUnderlayEnabled: (reactTag: number, enabled: boolean): Promise => nativeBridge().setRetainedUnderlayEnabled(reactTag, enabled), + setContinuousLayerRange: (reactTag: number, startLayerId: string, endLayerId: string): Promise => { + if (startLayerId.length === 0 || endLayerId.length === 0) return Promise.resolve("REJECTED"); + return nativeBridge().setContinuousLayerRange(reactTag, startLayerId, endLayerId); + }, getRetainedUnderlayDiagnostics: (reactTag: number): Promise => nativeBridge().getRetainedUnderlayDiagnostics(reactTag), } as const; diff --git a/packages/maplibre-particle-rn/src/types.ts b/packages/maplibre-particle-rn/src/types.ts index df1211c72..b7df32d17 100644 --- a/packages/maplibre-particle-rn/src/types.ts +++ b/packages/maplibre-particle-rn/src/types.ts @@ -28,4 +28,18 @@ export type RetainedUnderlayDiagnostics = Readonly<{ fallback: number; invalidation: number; lastReason: string; + rangeConfigured: boolean; + rangeValid: boolean; + startLayerId: string; + endLayerId: string; + startIndex: number; + endIndex: number; + bottomPresent: boolean; + bottomBuild: number; + bottomReuse: number; + bottomInvalidation: number; + topPresent: boolean; + topBuild: number; + topReuse: number; + topInvalidation: number; }>; diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp index fc6e6b88d..1e1845670 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp @@ -720,15 +720,36 @@ void NativeMapView::setRetainedUnderlayEnabled(JNIEnv&, jni::jboolean enabled) { map->setRetainedUnderlayEnabled(enabled); } +jni::jboolean NativeMapView::setContinuousLayerRange(JNIEnv& env, + const jni::String& startLayerId, + const jni::String& endLayerId) { + return map->setContinuousLayerRange(jni::Make(env, startLayerId), + jni::Make(env, endLayerId)); +} + jni::Local> NativeMapView::getRetainedUnderlayDiagnostics(JNIEnv& env) { const auto snapshot = map->getRetainedUnderlayDiagnostics(); - const std::array values = { + const std::array values = { snapshot.enabled ? "true" : "false", std::to_string(snapshot.build), std::to_string(snapshot.reuse), std::to_string(snapshot.fallback), std::to_string(snapshot.invalidation), snapshot.lastReason, + snapshot.rangeConfigured ? "true" : "false", + snapshot.rangeValid ? "true" : "false", + snapshot.startLayerId, + snapshot.endLayerId, + std::to_string(snapshot.startIndex), + std::to_string(snapshot.endIndex), + snapshot.bottomPresent ? "true" : "false", + std::to_string(snapshot.bottomBuild), + std::to_string(snapshot.bottomReuse), + std::to_string(snapshot.bottomInvalidation), + snapshot.topPresent ? "true" : "false", + std::to_string(snapshot.topBuild), + std::to_string(snapshot.topReuse), + std::to_string(snapshot.topInvalidation), }; auto result = jni::Array::New(env, values.size()); for (std::size_t index = 0; index < values.size(); ++index) { @@ -1817,6 +1838,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) { METHOD(&NativeMapView::setDebug, "nativeSetDebug"), METHOD(&NativeMapView::getDebug, "nativeGetDebug"), METHOD(&NativeMapView::setRetainedUnderlayEnabled, "nativeSetRetainedUnderlayEnabled"), + METHOD(&NativeMapView::setContinuousLayerRange, "nativeSetContinuousLayerRange"), METHOD(&NativeMapView::getRetainedUnderlayDiagnostics, "nativeGetRetainedUnderlayDiagnostics"), METHOD(&NativeMapView::getActionJournalLogFiles, "nativeGetActionJournalLogFiles"), METHOD(&NativeMapView::getActionJournalLog, "nativeGetActionJournalLog"), diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp index 65c76ffd4..4bbc14621 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp @@ -203,6 +203,8 @@ public: void setRetainedUnderlayEnabled(JNIEnv&, jni::jboolean); + jni::jboolean setContinuousLayerRange(JNIEnv&, const jni::String&, const jni::String&); + jni::Local> getRetainedUnderlayDiagnostics(JNIEnv&); jni::Local> getActionJournalLogFiles(JNIEnv&); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java index df120a33c..c978b7125 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java @@ -116,6 +116,11 @@ public final class MapLibreMap { nativeMapView.setRetainedUnderlayEnabled(enabled); } + /** Configures the inclusive Style-layer interval rendered continuously. */ + public boolean setContinuousLayerRange(@NonNull String startLayerId, @NonNull String endLayerId) { + return nativeMapView.setContinuousLayerRange(startLayerId, endLayerId); + } + /** Returns whether the retained static underlay optimization is enabled for this map. */ public boolean isRetainedUnderlayEnabled() { return nativeMapView.getRetainedUnderlayDiagnostics().enabled; diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java index 69e470bd7..9d2db0a87 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java @@ -288,6 +288,8 @@ interface NativeMap { void setRetainedUnderlayEnabled(boolean enabled); + boolean setContinuousLayerRange(String startLayerId, String endLayerId); + RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics(); String[] getActionJournalLogFiles(); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java index 02a2c33de..0b6b901d6 100755 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java @@ -661,10 +661,16 @@ final class NativeMapView implements NativeMap { nativeSetRetainedUnderlayEnabled(enabled); } + @Override + public boolean setContinuousLayerRange(@NonNull String startLayerId, @NonNull String endLayerId) { + if (checkState("setContinuousLayerRange")) return false; + return nativeSetContinuousLayerRange(startLayerId, endLayerId); + } + @Override public RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() { if (checkState("getRetainedUnderlayDiagnostics")) { - return new RetainedUnderlayDiagnostics(new String[] {"true", "0", "0", "0", "0", "unavailable"}); + return RetainedUnderlayDiagnostics.unavailable(); } return new RetainedUnderlayDiagnostics(nativeGetRetainedUnderlayDiagnostics()); } @@ -1721,6 +1727,9 @@ final class NativeMapView implements NativeMap { @Keep private native void nativeSetRetainedUnderlayEnabled(boolean enabled); + @Keep + private native boolean nativeSetContinuousLayerRange(String startLayerId, String endLayerId); + @Keep private native String[] nativeGetRetainedUnderlayDiagnostics(); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RetainedUnderlayDiagnostics.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RetainedUnderlayDiagnostics.java index 79cb15843..7b117fd70 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RetainedUnderlayDiagnostics.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RetainedUnderlayDiagnostics.java @@ -12,10 +12,24 @@ public final class RetainedUnderlayDiagnostics { public final long fallback; public final long invalidation; @NonNull public final String lastReason; + public final boolean rangeConfigured; + public final boolean rangeValid; + @NonNull public final String startLayerId; + @NonNull public final String endLayerId; + public final int startIndex; + public final int endIndex; + public final boolean bottomPresent; + public final long bottomBuild; + public final long bottomReuse; + public final long bottomInvalidation; + public final boolean topPresent; + public final long topBuild; + public final long topReuse; + public final long topInvalidation; RetainedUnderlayDiagnostics(@NonNull String[] values) { - if (values.length != 6) { - throw new IllegalArgumentException("Retained-underlay diagnostics require six values"); + if (values.length != 20) { + throw new IllegalArgumentException("Retained-range diagnostics require 20 values"); } enabled = Boolean.parseBoolean(values[0]); build = Long.parseLong(values[1]); @@ -23,5 +37,26 @@ public final class RetainedUnderlayDiagnostics { fallback = Long.parseLong(values[3]); invalidation = Long.parseLong(values[4]); lastReason = values[5]; + rangeConfigured = Boolean.parseBoolean(values[6]); + rangeValid = Boolean.parseBoolean(values[7]); + startLayerId = values[8]; + endLayerId = values[9]; + startIndex = Integer.parseInt(values[10]); + endIndex = Integer.parseInt(values[11]); + bottomPresent = Boolean.parseBoolean(values[12]); + bottomBuild = Long.parseLong(values[13]); + bottomReuse = Long.parseLong(values[14]); + bottomInvalidation = Long.parseLong(values[15]); + topPresent = Boolean.parseBoolean(values[16]); + topBuild = Long.parseLong(values[17]); + topReuse = Long.parseLong(values[18]); + topInvalidation = Long.parseLong(values[19]); + } + + static RetainedUnderlayDiagnostics unavailable() { + return new RetainedUnderlayDiagnostics(new String[] { + "true", "0", "0", "0", "0", "unavailable", "false", "false", "", "", "-1", "-1", + "false", "0", "0", "0", "false", "0", "0", "0" + }); } } diff --git a/platform/ios/src/MLNMapView.h b/platform/ios/src/MLNMapView.h index feec7e757..6ae61f767 100644 --- a/platform/ios/src/MLNMapView.h +++ b/platform/ios/src/MLNMapView.h @@ -38,6 +38,20 @@ MLN_EXPORT @property (nonatomic, readonly) uint64_t fallback; @property (nonatomic, readonly) uint64_t invalidation; @property (nonatomic, copy, readonly) NSString *lastReason; +@property (nonatomic, readonly) BOOL rangeConfigured; +@property (nonatomic, readonly) BOOL rangeValid; +@property (nonatomic, copy, readonly) NSString *startLayerId; +@property (nonatomic, copy, readonly) NSString *endLayerId; +@property (nonatomic, readonly) NSInteger startIndex; +@property (nonatomic, readonly) NSInteger endIndex; +@property (nonatomic, readonly) BOOL bottomPresent; +@property (nonatomic, readonly) uint64_t bottomBuild; +@property (nonatomic, readonly) uint64_t bottomReuse; +@property (nonatomic, readonly) uint64_t bottomInvalidation; +@property (nonatomic, readonly) BOOL topPresent; +@property (nonatomic, readonly) uint64_t topBuild; +@property (nonatomic, readonly) uint64_t topReuse; +@property (nonatomic, readonly) uint64_t topInvalidation; - (instancetype)init NS_UNAVAILABLE; @@ -2284,6 +2298,10 @@ of north, the map will automatically snap to exact north. /** Enables or disables the map-scoped retained static underlay optimization. */ @property (nonatomic, getter=isRetainedUnderlayEnabled) BOOL retainedUnderlayEnabled; +/** Configures the inclusive Style-layer interval rendered continuously. Empty identifiers are rejected. */ +- (BOOL)setContinuousLayerRangeFromLayerIdentifier:(NSString *)startLayerId + toLayerIdentifier:(NSString *)endLayerId; + /** Returns a thread-safe retained-underlay diagnostics snapshot without enabling periodic events. */ @property (nonatomic, readonly) MLNRetainedUnderlayDiagnostics *retainedUnderlayDiagnostics; diff --git a/platform/ios/src/MLNMapView.mm b/platform/ios/src/MLNMapView.mm index d5f79b341..2d9df59c8 100644 --- a/platform/ios/src/MLNMapView.mm +++ b/platform/ios/src/MLNMapView.mm @@ -246,6 +246,7 @@ int processIsTranslated() { fallback:(uint64_t)fallback invalidation:(uint64_t)invalidation lastReason:(NSString *)lastReason; +- (void)applyRangeSnapshot:(const mbgl::RetainedUnderlayDiagnostics&)snapshot; @end @implementation MLNRetainedUnderlayDiagnostics @@ -262,9 +263,30 @@ int processIsTranslated() { _fallback = fallback; _invalidation = invalidation; _lastReason = [lastReason copy]; + _startLayerId = @""; + _endLayerId = @""; + _startIndex = -1; + _endIndex = -1; } return self; } + +- (void)applyRangeSnapshot:(const mbgl::RetainedUnderlayDiagnostics&)snapshot { + _rangeConfigured = snapshot.rangeConfigured; + _rangeValid = snapshot.rangeValid; + _startLayerId = [NSString stringWithUTF8String:snapshot.startLayerId.c_str()]; + _endLayerId = [NSString stringWithUTF8String:snapshot.endLayerId.c_str()]; + _startIndex = snapshot.startIndex; + _endIndex = snapshot.endIndex; + _bottomPresent = snapshot.bottomPresent; + _bottomBuild = snapshot.bottomBuild; + _bottomReuse = snapshot.bottomReuse; + _bottomInvalidation = snapshot.bottomInvalidation; + _topPresent = snapshot.topPresent; + _topBuild = snapshot.topBuild; + _topReuse = snapshot.topReuse; + _topInvalidation = snapshot.topInvalidation; +} @end class MLNAnnotationContext; @@ -3106,6 +3128,12 @@ static void *windowScreenContext = &windowScreenContext; if (_mbglMap) self.mbglMap.setRetainedUnderlayEnabled(enabled); } +- (BOOL)setContinuousLayerRangeFromLayerIdentifier:(NSString *)startLayerId + toLayerIdentifier:(NSString *)endLayerId { + if (!_mbglMap || startLayerId.length == 0 || endLayerId.length == 0) return NO; + return self.mbglMap.setContinuousLayerRange(startLayerId.UTF8String, endLayerId.UTF8String); +} + - (MLNRetainedUnderlayDiagnostics *)retainedUnderlayDiagnostics { if (!_mbglMap) { return [[MLNRetainedUnderlayDiagnostics alloc] initWithEnabled:YES @@ -3116,12 +3144,14 @@ static void *windowScreenContext = &windowScreenContext; lastReason:@"unavailable"]; } const auto snapshot = self.mbglMap.getRetainedUnderlayDiagnostics(); - return [[MLNRetainedUnderlayDiagnostics alloc] initWithEnabled:snapshot.enabled + MLNRetainedUnderlayDiagnostics *result = [[MLNRetainedUnderlayDiagnostics alloc] initWithEnabled:snapshot.enabled build:snapshot.build reuse:snapshot.reuse fallback:snapshot.fallback invalidation:snapshot.invalidation lastReason:[NSString stringWithUTF8String:snapshot.lastReason.c_str()]]; + [result applyRangeSnapshot:snapshot]; + return result; } - (void)resetNorth { diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index 65f716c2d..94f419039 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -100,6 +100,14 @@ void Map::setRetainedUnderlayEnabled(bool enabled) { } } +bool Map::setContinuousLayerRange(std::string startLayerId, std::string endLayerId) { + if (!impl->retainedUnderlayState->setContinuousLayerRange(std::move(startLayerId), std::move(endLayerId))) { + return false; + } + impl->onUpdate(); + return true; +} + bool Map::isRetainedUnderlayEnabled() const { return impl->retainedUnderlayState->isEnabled(); } diff --git a/src/mbgl/renderer/continuous_layer_range.hpp b/src/mbgl/renderer/continuous_layer_range.hpp new file mode 100644 index 000000000..578e62bf5 --- /dev/null +++ b/src/mbgl/renderer/continuous_layer_range.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include +#include +#include + +namespace mbgl { + +struct ContinuousRenderRanges { + bool explicitConfigured = false; + bool explicitValid = false; + int32_t layerCount = 0; + int32_t continuousBegin = 0; + int32_t continuousEndExclusive = 0; + uint64_t configRevision = 0; + + bool retainedMode() const noexcept { + return explicitValid || (!explicitConfigured && continuousBegin > 0); + } + bool bottomPresent() const noexcept { return retainedMode() && continuousBegin > 0; } + bool topPresent() const noexcept { + return explicitValid && continuousEndExclusive < layerCount; + } +}; + +inline ContinuousRenderRanges selectContinuousRenderRanges( + const ContinuousLayerRangeResolution& explicitRange, + std::optional legacyBoundary, + int32_t layerCount) { + ContinuousRenderRanges ranges; + ranges.explicitConfigured = explicitRange.configured; + ranges.explicitValid = explicitRange.valid; + ranges.layerCount = layerCount; + ranges.continuousEndExclusive = layerCount; + ranges.configRevision = explicitRange.configRevision; + if (explicitRange.valid) { + ranges.continuousBegin = explicitRange.begin(); + ranges.continuousEndExclusive = explicitRange.endExclusive(); + } else if (!explicitRange.configured && legacyBoundary) { + ranges.continuousBegin = *legacyBoundary; + } + return ranges; +} + +inline ContinuousLayerRangeResolution resolveContinuousLayerRange( + const ContinuousLayerRangeConfig& config, + const std::vector& orderedLayerIds) { + ContinuousLayerRangeResolution result; + result.configured = config.configured; + result.startLayerId = config.startLayerId; + result.endLayerId = config.endLayerId; + result.configRevision = config.revision; + if (!config.configured) { + result.reason = "not-configured"; + return result; + } + if (orderedLayerIds.empty()) { + result.reason = "style-not-ready"; + return result; + } + for (std::size_t index = 0; index < orderedLayerIds.size(); ++index) { + if (orderedLayerIds[index] == config.startLayerId) result.startIndex = static_cast(index); + if (orderedLayerIds[index] == config.endLayerId) result.endIndex = static_cast(index); + } + if (result.startIndex < 0) { + result.reason = "start-layer-missing"; + } else if (result.endIndex < 0) { + result.reason = "end-layer-missing"; + } else if (result.startIndex > result.endIndex) { + result.reason = "range-order-invalid"; + } else { + result.valid = true; + result.reason = "none"; + } + return result; +} + +} // namespace mbgl diff --git a/src/mbgl/renderer/render_orchestrator.cpp b/src/mbgl/renderer/render_orchestrator.cpp index 8188185f2..a4f557cfa 100644 --- a/src/mbgl/renderer/render_orchestrator.cpp +++ b/src/mbgl/renderer/render_orchestrator.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -406,6 +407,26 @@ std::unique_ptr RenderOrchestrator::createRenderTree( orderedLayers[index].get().startsRetainedDynamicRange()); } retainedDynamicBoundary = boundaryTracker.value(); + std::vector orderedLayerIds; + orderedLayerIds.reserve(orderedLayers.size()); + for (const auto& layer : orderedLayers) orderedLayerIds.push_back(layer.get().getID()); + continuousLayerRange = resolveContinuousLayerRange( + updateParameters->retainedUnderlayState + ? updateParameters->retainedUnderlayState->continuousLayerRangeConfig() + : ContinuousLayerRangeConfig{}, + orderedLayerIds); + if (continuousLayerRange.configured && !updateParameters->styleLoaded) { + continuousLayerRange.valid = false; + continuousLayerRange.startIndex = -1; + continuousLayerRange.endIndex = -1; + continuousLayerRange.reason = "style-not-ready"; + } + if (updateParameters->retainedUnderlayState) { + updateParameters->retainedUnderlayState->recordRangeResolution( + continuousLayerRange, + continuousLayerRange.bottomPresent(), + continuousLayerRange.topPresent(static_cast(orderedLayers.size()))); + } // Update all sources and initialize renderItems. for (const auto& sourceImpl : *sourceImpls) { @@ -894,21 +915,29 @@ bool RenderOrchestrator::hasTransitions(TimePoint timePoint) const { } bool RenderOrchestrator::retainedUnderlayUsesPlacement(int32_t boundary) const { - const auto staticLayerCount = std::min( - boundary > 0 ? static_cast(boundary) : 0, orderedLayers.size()); - for (std::size_t index = 0; index < staticLayerCount; ++index) { + return retainedRangeUsesPlacement(0, boundary); +} + +bool RenderOrchestrator::retainedRangeUsesPlacement(int32_t begin, int32_t endExclusive) const { + const auto first = std::clamp(begin, 0, static_cast(orderedLayers.size())); + const auto last = std::clamp(endExclusive, first, static_cast(orderedLayers.size())); + for (int32_t index = first; index < last; ++index) { if (orderedLayers[index].get().needsPlacement()) return true; } return false; } bool RenderOrchestrator::retainedUnderlayIsStable(TimePoint timePoint, int32_t boundary) const { - if (renderLight.hasTransition()) return false; - if (retainedUnderlayUsesPlacement(boundary) && placementController.hasTransitions(timePoint)) return false; + return retainedRangeIsStable(timePoint, 0, boundary); +} - const auto staticLayerCount = std::min( - boundary > 0 ? static_cast(boundary) : 0, orderedLayers.size()); - for (std::size_t index = 0; index < staticLayerCount; ++index) { +bool RenderOrchestrator::retainedRangeIsStable(TimePoint timePoint, int32_t begin, int32_t endExclusive) const { + if (renderLight.hasTransition()) return false; + if (retainedRangeUsesPlacement(begin, endExclusive) && placementController.hasTransitions(timePoint)) return false; + + const auto first = std::clamp(begin, 0, static_cast(orderedLayers.size())); + const auto last = std::clamp(endExclusive, first, static_cast(orderedLayers.size())); + for (int32_t index = first; index < last; ++index) { const auto& layer = orderedLayers[index].get(); if (layer.hasTransition() || layer.needsRepaint()) return false; } @@ -920,15 +949,31 @@ bool RenderOrchestrator::retainedUnderlayIsStable(TimePoint timePoint, int32_t b } bool RenderOrchestrator::retainedUnderlayHasUnsupportedLayer(int32_t boundary) const { - const auto staticLayerCount = std::min( - boundary > 0 ? static_cast(boundary) : 0, orderedLayers.size()); - for (std::size_t index = 0; index < staticLayerCount; ++index) { + return retainedRangeHasUnsupportedLayer(0, boundary); +} + +bool RenderOrchestrator::retainedRangeHasUnsupportedLayer(int32_t begin, int32_t endExclusive) const { + const auto first = std::clamp(begin, 0, static_cast(orderedLayers.size())); + const auto last = std::clamp(endExclusive, first, static_cast(orderedLayers.size())); + for (int32_t index = first; index < last; ++index) { const std::string_view type = orderedLayers[index].get().baseImpl->getTypeInfo()->type; if (type == "custom" || type == "custom-drawable") return true; } return false; } +bool RenderOrchestrator::continuousRangeSupportsParticleFastPath(int32_t begin, int32_t endExclusive) const { + const auto first = std::clamp(begin, 0, static_cast(orderedLayers.size())); + const auto last = std::clamp(endExclusive, first, static_cast(orderedLayers.size())); + bool particle = false; + for (int32_t index = first; index < last; ++index) { + const auto& layer = orderedLayers[index].get(); + if (!layer.startsRetainedDynamicRange()) return false; + particle = particle || layer.isParticleAnimationLayer(); + } + return particle; +} + ParticleFastPathDynamicState RenderOrchestrator::particleFastPathDynamicState(TimePoint timePoint) const { ParticleFastPathDynamicState result; result.blockingTransition = renderLight.hasTransition(); diff --git a/src/mbgl/renderer/render_orchestrator.hpp b/src/mbgl/renderer/render_orchestrator.hpp index dff783a42..d358562e7 100644 --- a/src/mbgl/renderer/render_orchestrator.hpp +++ b/src/mbgl/renderer/render_orchestrator.hpp @@ -119,9 +119,15 @@ public: bool removeLayerGroup(const LayerGroupBasePtr&); size_t numLayerGroups() const noexcept; std::optional getRetainedDynamicBoundary() const noexcept { return retainedDynamicBoundary; } + const ContinuousLayerRangeResolution& getContinuousLayerRange() const noexcept { return continuousLayerRange; } + int32_t getOrderedLayerCount() const noexcept { return static_cast(orderedLayers.size()); } bool retainedUnderlayUsesPlacement(int32_t boundary) const; bool retainedUnderlayIsStable(TimePoint, int32_t boundary) const; bool retainedUnderlayHasUnsupportedLayer(int32_t boundary) const; + bool retainedRangeUsesPlacement(int32_t begin, int32_t endExclusive) const; + 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; ParticleFastPathDynamicState particleFastPathDynamicState(TimePoint) const; uint64_t getRetainedUnderlayContentRevision() const noexcept { return retainedUnderlayContentRevision.load(std::memory_order_acquire); @@ -144,6 +150,32 @@ public: } } + template + void visitLayerGroupsInRange(int32_t begin, int32_t endExclusive, Func f) { + size_t ordinal = static_cast(std::distance(layerGroupsByLayerIndex.begin(), + layerGroupsByLayerIndex.lower_bound(begin))); + for (auto it = layerGroupsByLayerIndex.lower_bound(begin); + it != layerGroupsByLayerIndex.end() && it->first < endExclusive; ++it, ++ordinal) { + if (it->second) { + try { f(*it->second, ordinal); } catch (...) { observer->onRenderError(std::current_exception()); } + } + } + } + + template + void visitLayerGroupsInRangeReversed(int32_t begin, int32_t endExclusive, Func f) { + auto first = layerGroupsByLayerIndex.lower_bound(begin); + auto last = layerGroupsByLayerIndex.lower_bound(endExclusive); + size_t ordinal = static_cast(std::distance(layerGroupsByLayerIndex.begin(), last)); + while (last != first) { + --last; + --ordinal; + if (last->second) { + try { f(*last->second, ordinal); } catch (...) { observer->onRenderError(std::current_exception()); } + } + } + } + template void visitLayerGroupsBefore(int32_t boundary, Func f) { size_t ordinal = 0; @@ -343,6 +375,7 @@ private: using LayerGroupMap = std::multimap; LayerGroupMap layerGroupsByLayerIndex; std::optional retainedDynamicBoundary; + ContinuousLayerRangeResolution continuousLayerRange; std::vector renderTargets; RenderItem::DebugLayerGroupMap debugLayerGroups; diff --git a/src/mbgl/renderer/renderer_impl.cpp b/src/mbgl/renderer/renderer_impl.cpp index 6d040d7ca..e492dbf2a 100644 --- a/src/mbgl/renderer/renderer_impl.cpp +++ b/src/mbgl/renderer/renderer_impl.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #if MLN_RENDER_BACKEND_METAL @@ -51,6 +52,12 @@ using namespace style; namespace { +ContinuousRenderRanges activeRenderRanges(const RenderOrchestrator& orchestrator) { + return selectContinuousRenderRanges(orchestrator.getContinuousLayerRange(), + orchestrator.getRetainedDynamicBoundary(), + orchestrator.getOrderedLayerCount()); +} + RendererObserver& nullObserver() { static RendererObserver observer; return observer; @@ -98,7 +105,8 @@ Renderer::Impl::Impl(gfx::RendererBackend& backend_, backend(backend_), observer(&nullObserver()), pixelRatio(pixelRatio_), - retainedUnderlayCache(std::make_unique()) {} + retainedUnderlayCache(std::make_unique(RetainedRangeRole::Bottom)), + retainedTopCache(std::make_unique(RetainedRangeRole::Top)) {} Renderer::Impl::~Impl() { assert(gfx::BackendScope::exists()); @@ -133,6 +141,7 @@ void Renderer::Impl::setObserver(RendererObserver* observer_) { void Renderer::Impl::synchronizeRetainedUnderlayState( const std::shared_ptr& state) { retainedUnderlayCache->synchronizeRuntimeState(state); + retainedTopCache->synchronizeRuntimeState(state); } Renderer::Impl::ParticleFastPathUpdateSignature Renderer::Impl::makeParticleFastPathUpdateSignature( @@ -192,28 +201,48 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptrgetDiagnostics(); - const auto currentBoundary = orchestrator.getRetainedDynamicBoundary(); + const auto ranges = activeRenderRanges(orchestrator); + const auto liveRangeConfig = updateParameters->retainedUnderlayState + ? updateParameters->retainedUnderlayState->continuousLayerRangeConfig() + : ContinuousLayerRangeConfig{}; + const std::optional currentBoundary = + (ranges.explicitValid || !ranges.explicitConfigured) ? std::optional(ranges.continuousBegin) + : 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 revisionMatches = - retainedUnderlayCache->getStaticRevision() == retainedUnderlayRevisionTracker.current() && - retainedUnderlayCache->getBuildRevision() == retainedUnderlayRevisionTracker.current() && - particlePreparedStaticRevision == retainedUnderlayRevisionTracker.current(); - const bool boundaryMatches = particlePreparedBoundary == currentBoundary; + 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 && + particlePreparedStaticRevision == retainedUnderlayRevisionTracker.current(); + const bool boundaryMatches = particlePreparedBoundary == currentBoundary && + particlePreparedContinuousEnd == std::optional(ranges.continuousEndExclusive) && + particlePreparedRangeConfigRevision == ranges.configRevision && + liveRangeConfig.revision == ranges.configRevision && + liveRangeConfig.configured == ranges.explicitConfigured && + particlePreparedBottomPresent == ranges.bottomPresent() && + particlePreparedTopPresent == ranges.topPresent(); const bool topologyMatches = particlePreparedTopologyRevision == retainedUnderlayTopologyTracker.current(); const bool contentRevisionMatches = particlePreparedContentRevision == orchestrator.getRetainedUnderlayContentRevision(); - const bool staticStable = - currentBoundary && orchestrator.retainedUnderlayIsStable(updateParameters->timePoint, *currentBoundary); + 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)); const ParticleFastPathGateInput input{ .hasPreparedTree = !particlePreparedTree.empty(), .retainedEnabled = retained.enabled, - .cacheValid = retainedUnderlayCache->isValid(), + .cacheValid = (!ranges.bottomPresent() || retainedUnderlayCache->isValid()) && + (!ranges.topPresent() || retainedTopCache->isValid()), .revisionMatches = revisionMatches, .contentRevisionMatches = contentRevisionMatches, .cameraMatches = cameraMatches, @@ -254,11 +283,18 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptr renderTree, const std::shared_ptr& updateParameters) { + const auto ranges = activeRenderRanges(orchestrator); particlePreparedSignature = makeParticleFastPathUpdateSignature(*updateParameters); particlePreparedContentRevision = orchestrator.getRetainedUnderlayContentRevision(); particlePreparedTopologyRevision = retainedUnderlayTopologyTracker.current(); particlePreparedStaticRevision = retainedUnderlayRevisionTracker.current(); - particlePreparedBoundary = orchestrator.getRetainedDynamicBoundary(); + particlePreparedBoundary = (ranges.explicitValid || !ranges.explicitConfigured) + ? std::optional(ranges.continuousBegin) + : std::nullopt; + particlePreparedContinuousEnd = ranges.continuousEndExclusive; + particlePreparedRangeConfigRevision = ranges.configRevision; + particlePreparedBottomPresent = ranges.bottomPresent(); + particlePreparedTopPresent = ranges.topPresent(); particlePreparedBackendSize = backend.getDefaultRenderable().getSize(); particlePreparedTree.replace(std::move(renderTree)); } @@ -270,6 +306,10 @@ void Renderer::Impl::clearPreparedRenderTree() { particlePreparedTopologyRevision = 0; particlePreparedStaticRevision = 0; particlePreparedBoundary.reset(); + particlePreparedContinuousEnd.reset(); + particlePreparedRangeConfigRevision = 0; + particlePreparedBottomPresent = false; + particlePreparedTopPresent = false; particlePreparedBackendSize = {}; particlePreparedSceneSupported = false; } @@ -455,46 +495,78 @@ void Renderer::Impl::render(const RenderTree& renderTree, orchestrator.processChanges(); - std::optional retainedKey; - const auto retainedBoundary = orchestrator.getRetainedDynamicBoundary(); - RetainedUnderlayTopologySnapshot retainedTopology{.boundary = retainedBoundary, .staticPrefix = {}}; + const auto ranges = activeRenderRanges(orchestrator); + const bool rangeMode = ranges.explicitValid; + const bool retainedMode = rangeMode || (!ranges.explicitConfigured && ranges.bottomPresent()); + const std::optional retainedBoundary = retainedMode + ? std::optional(ranges.continuousBegin) : std::nullopt; + std::optional bottomKey; + std::optional topKey; + RetainedUnderlayTopologySnapshot retainedTopology{ + .boundary = retainedBoundary, + .staticPrefix = {}, + .continuousEndExclusive = retainedMode ? std::optional(ranges.continuousEndExclusive) : std::nullopt, + .staticSuffix = {}}; mat4 retainedCameraMatrix; state.getProjMatrix(retainedCameraMatrix); - if (retainedBoundary) { - orchestrator.visitLayerGroupsBefore(*retainedBoundary, [&](LayerGroupBase& layerGroup, size_t) { + 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(layerGroup.getType())}); }); - retainedKey = RetainedUnderlayKey{size, *retainedBoundary, retainedCameraMatrix}; + bottomKey = RetainedUnderlayKey{size, 0, ranges.continuousBegin, 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(layerGroup.getType())}); + }); + topKey = RetainedUnderlayKey{ + size, ranges.continuousEndExclusive, ranges.layerCount, retainedCameraMatrix}; } const auto retainedTopologyRevision = particleFastFrame ? retainedUnderlayTopologyTracker.current() : retainedUnderlayTopologyTracker.observe(retainedTopology); - const auto retainedStaticGroupCount = retainedTopology.staticPrefix.size(); - bool legacyBelowBoundary = false; - if (retainedBoundary) { + const auto bottomGroupCount = retainedTopology.staticPrefix.size(); + bool legacyInRetainedRange = false; + if (retainedMode) { for (const auto& item : layerRenderItems) { - if (item.index < static_cast(*retainedBoundary) && item.layer.get().usesLegacyRendering()) { - legacyBelowBoundary = true; + const auto index = static_cast(item.index); + if ((index < ranges.continuousBegin || (rangeMode && index >= ranges.continuousEndExclusive)) && + item.layer.get().usesLegacyRendering()) { + legacyInRetainedRange = true; break; } } } - const bool retainedStaticUsesPlacement = - retainedBoundary && orchestrator.retainedUnderlayUsesPlacement(*retainedBoundary); - const bool retainedStaticStable = - retainedBoundary && orchestrator.retainedUnderlayIsStable(renderTreeParameters.timePoint, *retainedBoundary); + 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 = - retainedBoundary && orchestrator.retainedUnderlayHasUnsupportedLayer(*retainedBoundary); + 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(ranges.continuousEndExclusive) + : std::nullopt, + .rangeConfigRevision = ranges.configRevision, .pixelRatio = updateParameters->pixelRatio, .contentRevision = retainedContentRevision, .topologyRevision = retainedTopologyRevision, @@ -509,38 +581,67 @@ void Renderer::Impl::render(const RenderTree& renderTree, }; const auto retainedStaticRevision = retainedUnderlayRevisionTracker.observe( retainedRevisionState, retainedUnderlayReasonText(retainedContentReason)); - const bool retainedSupported = retainedBoundary && retainedStaticGroupCount > 0 && + const bool retainedSupported = retainedMode && + (rangeMode || !ranges.bottomPresent() || bottomGroupCount > 0) && renderTreeParameters.loaded && !renderTreeParameters.has3D && renderTreeParameters.debugOptions == MapDebugOptions::NoDebug && retainedStaticStable && retainedPlacementStable && - !legacyBelowBoundary && !retainedUnsupportedStaticLayer; - if (!particleFastFrame) particlePreparedSceneSupported = retainedSupported; - const char* retainedFallbackReason = !retainedBoundary ? "no-boundary" - : retainedStaticGroupCount == 0 ? "empty-static-prefix" - : !renderTreeParameters.loaded ? "style-not-loaded" - : renderTreeParameters.has3D ? "3d" - : renderTreeParameters.debugOptions != MapDebugOptions::NoDebug - ? "debug-options" - : !retainedStaticStable - ? "static-transition" - : !retainedPlacementStable - ? "static-placement" - : legacyBelowBoundary ? "legacy-ordering" - : retainedUnsupportedStaticLayer ? "unsupported" - : nullptr; - auto retainedDecision = retainedUnderlayCache->evaluate(retainedKey, - retainedStaticRevision, - retainedSupported, - retainedFallbackReason, - retainedUnderlayRevisionTracker.reason().c_str()); - if ((retainedDecision == RetainedUnderlayDecision::Build || - retainedDecision == RetainedUnderlayDecision::Reuse) && - (!staticData->shaders || !retainedUnderlayCache->prepareResources(context, *staticData->shaders, *retainedKey))) { - retainedUnderlayCache->markBuildFailed("resource-or-compositor"); - retainedDecision = RetainedUnderlayDecision::Fallback; + !legacyInRetainedRange && !retainedUnsupportedStaticLayer; + const bool fastContinuousSupported = rangeMode + ? orchestrator.continuousRangeSupportsParticleFastPath( + ranges.continuousBegin, ranges.continuousEndExclusive) + : retainedSupported; + 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) { + retainedFallbackReason = "3d"; + } else if (renderTreeParameters.debugOptions != MapDebugOptions::NoDebug) { + retainedFallbackReason = "debug-options"; + } else if (!retainedStaticStable) { + retainedFallbackReason = "static-transition"; + } else if (!retainedPlacementStable) { + retainedFallbackReason = "static-placement"; + } else if (legacyInRetainedRange) { + retainedFallbackReason = "legacy-ordering"; + } else if (retainedUnsupportedStaticLayer) { + retainedFallbackReason = "unsupported"; } - const bool useRetainedUnderlay = retainedDecision == RetainedUnderlayDecision::Build || - retainedDecision == RetainedUnderlayDecision::Reuse; + 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); + } + 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; + } + const bool useRetainedRanges = retainedSupported && + (!ranges.bottomPresent() || wantsResources(bottomDecision)) && + (!ranges.topPresent() || wantsResources(topDecision)); // Upload layer groups { @@ -556,11 +657,13 @@ 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 && retainedBoundary) { - orchestrator.visitLayerGroupsFrom(*retainedBoundary, [&](LayerGroupBase& layerGroup, size_t ordinal) { - parameters.currentLayer = static_cast(ordinal); - layerGroup.runTweakers(renderTree, parameters); - }); + if (particleFastFrame && retainedMode) { + orchestrator.visitLayerGroupsInRange( + ranges.continuousBegin, ranges.continuousEndExclusive, + [&](LayerGroupBase& layerGroup, size_t ordinal) { + parameters.currentLayer = static_cast(ordinal); + layerGroup.runTweakers(renderTree, parameters); + }); } else { orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.runTweakers(renderTree, parameters); @@ -574,9 +677,10 @@ void Renderer::Impl::render(const RenderTree& renderTree, } // Give the layers a chance to upload - if (particleFastFrame && retainedBoundary) { - orchestrator.visitLayerGroupsFrom( - *retainedBoundary, [&](LayerGroupBase& layerGroup, size_t) { layerGroup.upload(*uploadPass); }); + if (particleFastFrame && retainedMode) { + orchestrator.visitLayerGroupsInRange( + ranges.continuousBegin, ranges.continuousEndExclusive, + [&](LayerGroupBase& layerGroup, size_t) { layerGroup.upload(*uploadPass); }); } else { orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); }); } @@ -588,7 +692,8 @@ void Renderer::Impl::render(const RenderTree& renderTree, if (!particleFastFrame) { orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); }); } - if (useRetainedUnderlay) retainedUnderlayCache->upload(*uploadPass); + if (useRetainedRanges && ranges.bottomPresent()) retainedUnderlayCache->upload(*uploadPass); + if (useRetainedRanges && ranges.topPresent()) retainedTopCache->upload(*uploadPass); } const Size atlasSize = parameters.patternAtlas.getPixelSize(); @@ -652,30 +757,33 @@ void Renderer::Impl::render(const RenderTree& renderTree, [&](RenderTarget& renderTarget) { renderTarget.render(orchestrator, renderTree, parameters); }); }; - const auto buildRetainedUnderlay = [&] { - if (retainedDecision != RetainedUnderlayDecision::Build || !retainedBoundary) return; - retainedUnderlayCache->beginBuild(parameters); + const auto buildRetainedRange = [&](RetainedUnderlayCache& cache, + RetainedUnderlayDecision decision, + int32_t begin, + int32_t endExclusive) { + if (!useRetainedRanges || decision != RetainedUnderlayDecision::Build || begin >= endExclusive) return; + cache.beginBuild(parameters); context.bindGlobalUniformBuffers(*parameters.renderPass); parameters.depthRangeSize = 1 - (orchestrator.numLayerGroups() + 2) * PaintParameters::numSublayers * PaintParameters::depthEpsilon; parameters.pass = RenderPass::Opaque; - orchestrator.visitLayerGroupsBeforeReversed(*retainedBoundary, - [&](LayerGroupBase& layerGroup, size_t ordinal) { + orchestrator.visitLayerGroupsInRangeReversed(begin, endExclusive, + [&](LayerGroupBase& layerGroup, size_t ordinal) { parameters.currentLayer = static_cast(orchestrator.numLayerGroups() - 1 - ordinal); layerGroup.render(orchestrator, parameters); particle::incrementRendererDebugCounter( particle::RendererDebugCounter::RetainedUnderlayStaticGroupDraw); }); parameters.pass = RenderPass::Translucent; - orchestrator.visitLayerGroupsBefore(*retainedBoundary, [&](LayerGroupBase& layerGroup, size_t ordinal) { + orchestrator.visitLayerGroupsInRange(begin, endExclusive, [&](LayerGroupBase& layerGroup, size_t ordinal) { parameters.currentLayer = static_cast(orchestrator.numLayerGroups() - 1 - ordinal); layerGroup.render(orchestrator, parameters); particle::incrementRendererDebugCounter( particle::RendererDebugCounter::RetainedUnderlayStaticGroupDraw); }); context.unbindGlobalUniformBuffers(*parameters.renderPass); - retainedUnderlayCache->finishBuild(parameters); - retainedUnderlayCache->markBuildSucceeded(); + cache.finishBuild(parameters); + cache.markBuildSucceeded(); }; const auto commonClearPass = [&] { @@ -711,9 +819,10 @@ void Renderer::Impl::render(const RenderTree& renderTree, PaintParameters::depthEpsilon; // draw layer groups, opaque pass - if (useRetainedUnderlay && retainedBoundary) { - orchestrator.visitLayerGroupsFromReversed(*retainedBoundary, - [&](LayerGroupBase& layerGroup, size_t ordinal) { + if (useRetainedRanges && retainedMode) { + orchestrator.visitLayerGroupsInRangeReversed( + ranges.continuousBegin, ranges.continuousEndExclusive, + [&](LayerGroupBase& layerGroup, size_t ordinal) { parameters.currentLayer = static_cast(orchestrator.numLayerGroups() - 1 - ordinal); layerGroup.render(orchestrator, parameters); particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedDynamicGroupDraw); @@ -734,8 +843,10 @@ void Renderer::Impl::render(const RenderTree& renderTree, PaintParameters::depthEpsilon; // draw layer groups, translucent pass - if (useRetainedUnderlay && retainedBoundary) { - orchestrator.visitLayerGroupsFrom(*retainedBoundary, [&](LayerGroupBase& layerGroup, size_t ordinal) { + if (useRetainedRanges && retainedMode) { + orchestrator.visitLayerGroupsInRange( + ranges.continuousBegin, ranges.continuousEndExclusive, + [&](LayerGroupBase& layerGroup, size_t ordinal) { parameters.currentLayer = static_cast(orchestrator.numLayerGroups() - 1 - ordinal); layerGroup.render(orchestrator, parameters); particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedDynamicGroupDraw); @@ -758,7 +869,10 @@ void Renderer::Impl::render(const RenderTree& renderTree, for (auto it = layerRenderItems.begin(); it != layerRenderItems.end() && i >= 0; ++it, --i) { parameters.currentLayer = i; const RenderItem& item = *it; - if (item.hasRenderPass(parameters.pass)) { + const bool itemInLiveRange = !useRetainedRanges || !retainedMode || + (static_cast(item.index) >= ranges.continuousBegin && + static_cast(item.index) < ranges.continuousEndExclusive); + if (itemInLiveRange && item.hasRenderPass(parameters.pass)) { item.render(parameters); } } @@ -782,19 +896,23 @@ void Renderer::Impl::render(const RenderTree& renderTree, drawable3DPass(); } drawableTargetsPass(); - buildRetainedUnderlay(); + buildRetainedRange(*retainedUnderlayCache, bottomDecision, 0, ranges.continuousBegin); + buildRetainedRange(*retainedTopCache, topDecision, ranges.continuousEndExclusive, ranges.layerCount); commonClearPass(); context.bindGlobalUniformBuffers(*parameters.renderPass); - if (useRetainedUnderlay) retainedUnderlayCache->composite(orchestrator, parameters); + if (useRetainedRanges && ranges.bottomPresent()) retainedUnderlayCache->composite(orchestrator, parameters); drawableOpaquePass(); drawableTranslucentPass(); + if (useRetainedRanges && ranges.topPresent()) retainedTopCache->composite(orchestrator, parameters); drawableDebugOverlays(); // Give the layers a chance to do cleanup - if (particleFastFrame && retainedBoundary) { - orchestrator.visitLayerGroupsFrom(*retainedBoundary, [&](LayerGroupBase& layerGroup, size_t) { - layerGroup.postRender(orchestrator, parameters); - }); + if (particleFastFrame && retainedMode) { + orchestrator.visitLayerGroupsInRange( + ranges.continuousBegin, ranges.continuousEndExclusive, + [&](LayerGroupBase& layerGroup, size_t) { + layerGroup.postRender(orchestrator, parameters); + }); } else { orchestrator.visitLayerGroups( [&](LayerGroupBase& layerGroup) { layerGroup.postRender(orchestrator, parameters); }); diff --git a/src/mbgl/renderer/renderer_impl.hpp b/src/mbgl/renderer/renderer_impl.hpp index 3ec032454..84b57c0b8 100644 --- a/src/mbgl/renderer/renderer_impl.hpp +++ b/src/mbgl/renderer/renderer_impl.hpp @@ -62,6 +62,7 @@ private: const float pixelRatio; std::unique_ptr staticData; std::unique_ptr retainedUnderlayCache; + std::unique_ptr retainedTopCache; RetainedUnderlayRevisionTracker retainedUnderlayRevisionTracker; RetainedUnderlayTopologyTracker retainedUnderlayTopologyTracker; ParticleFastPathPreparedTree particlePreparedTree; @@ -107,6 +108,10 @@ private: uint64_t particlePreparedTopologyRevision = 0; uint64_t particlePreparedStaticRevision = 0; std::optional particlePreparedBoundary; + std::optional particlePreparedContinuousEnd; + uint64_t particlePreparedRangeConfigRevision = 0; + bool particlePreparedBottomPresent = false; + bool particlePreparedTopPresent = false; Size particlePreparedBackendSize; bool particlePreparedSceneSupported = false; ParticleFastPathMissReason particleFastPathLastReason = ParticleFastPathMissReason::NoPreparedTree; diff --git a/src/mbgl/renderer/retained_underlay_cache.cpp b/src/mbgl/renderer/retained_underlay_cache.cpp index 38d6e681c..c2a48c456 100644 --- a/src/mbgl/renderer/retained_underlay_cache.cpp +++ b/src/mbgl/renderer/retained_underlay_cache.cpp @@ -77,7 +77,9 @@ uint64_t RetainedUnderlayRevisionTracker::observe(const RetainedUnderlayRevision lastReason = "viewport"; } else if (previous.pixelRatio != next.pixelRatio) { lastReason = "pixel-ratio"; - } else if (previous.cameraMatrix != next.cameraMatrix || previous.boundary != next.boundary) { + } else if (previous.cameraMatrix != next.cameraMatrix || previous.boundary != next.boundary || + previous.continuousEndExclusive != next.continuousEndExclusive || + previous.rangeConfigRevision != next.rangeConfigRevision) { lastReason = "camera-or-boundary"; } else if (previous.topologyRevision != next.topologyRevision) { lastReason = "layer-group-mutation"; @@ -106,8 +108,8 @@ uint64_t RetainedUnderlayRevisionTracker::observe(const RetainedUnderlayRevision return revision; } -RetainedUnderlayCache::RetainedUnderlayCache() - : runtimeState(std::make_shared()) {} +RetainedUnderlayCache::RetainedUnderlayCache(RetainedRangeRole role_) + : runtimeState(std::make_shared()), role(role_) {} RetainedUnderlayCache::~RetainedUnderlayCache() = default; void RetainedUnderlayCache::synchronizeRuntimeState( @@ -123,6 +125,7 @@ void RetainedUnderlayCache::synchronizeRuntimeState( observedControlRevision = revision; // GPU resources are owned and released on the renderer thread. The public setter changes // the shared enabled bit first and schedules this synchronization through Map::onUpdate(). + if (valid || key) invalidate(runtimeState->isEnabled() ? "camera-or-boundary" : "runtime-disabled"); releaseResources(); if (!runtimeState->isEnabled()) { lastInvalidationReason = "runtime-disabled"; @@ -178,6 +181,7 @@ RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optionalrecordReuse(); + runtimeState->recordRangeReuse(role == RetainedRangeRole::Top); particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayReuse); return RetainedUnderlayDecision::Reuse; } @@ -202,7 +206,8 @@ 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, "retained-underlay-composite"); + auto group = context.createLayerGroup( + 0, 1, role == RetainedRangeRole::Top ? "retained-top-composite" : "retained-bottom-composite"); if (!shader || !group || !offscreenTexture) return false; auto vertices = std::make_shared>( @@ -242,7 +247,7 @@ void RetainedUnderlayCache::upload(gfx::UploadPass& uploadPass) { void RetainedUnderlayCache::beginBuild(PaintParameters& parameters) { parameters.renderPass = parameters.encoder->createRenderPass( - "retained static underlay", + role == RetainedRangeRole::Top ? "retained top range" : "retained bottom range", {.renderable = *offscreenTexture, .clearColor = Color{0.0f, 0.0f, 0.0f, 0.0f}, .clearDepth = 1.0f, @@ -269,6 +274,7 @@ void RetainedUnderlayCache::markBuildSucceeded() { buildRevision = observedStaticRevision; ++buildCount; runtimeState->recordBuild(); + runtimeState->recordRangeBuild(role == RetainedRangeRole::Top); particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayBuild); } @@ -284,6 +290,7 @@ void RetainedUnderlayCache::invalidate(const char* reason) { valid = false; lastInvalidationReason = reason ? reason : "unknown"; runtimeState->recordInvalidation(retainedReason(reason)); + runtimeState->recordRangeInvalidation(role == RetainedRangeRole::Top); particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayInvalidation); } diff --git a/src/mbgl/renderer/retained_underlay_cache.hpp b/src/mbgl/renderer/retained_underlay_cache.hpp index fdff71899..392348d9d 100644 --- a/src/mbgl/renderer/retained_underlay_cache.hpp +++ b/src/mbgl/renderer/retained_underlay_cache.hpp @@ -25,12 +25,15 @@ class UploadPass; struct RetainedUnderlayKey { Size viewport; - int32_t boundary = 0; + int32_t begin = 0; + int32_t endExclusive = 0; mat4 cameraMatrix{}; bool operator==(const RetainedUnderlayKey&) const = default; }; +enum class RetainedRangeRole : uint8_t { Bottom, Top }; + struct RetainedUnderlayTopologyEntry { int32_t layerIndex = 0; std::string groupName; @@ -42,6 +45,8 @@ struct RetainedUnderlayTopologyEntry { struct RetainedUnderlayTopologySnapshot { std::optional boundary; std::vector staticPrefix; + std::optional continuousEndExclusive; + std::vector staticSuffix; bool operator==(const RetainedUnderlayTopologySnapshot&) const = default; }; @@ -60,6 +65,8 @@ struct RetainedUnderlayRevisionState { Size viewport; mat4 cameraMatrix{}; std::optional boundary; + std::optional continuousEndExclusive; + uint64_t rangeConfigRevision = 0; float pixelRatio = 1.0f; uint64_t contentRevision = 0; uint64_t topologyRevision = 0; @@ -107,7 +114,7 @@ inline bool retainedLayerGroupInRange(int32_t layerIndex, int32_t boundary, Reta class RetainedUnderlayCache { public: - RetainedUnderlayCache(); + explicit RetainedUnderlayCache(RetainedRangeRole = RetainedRangeRole::Bottom); ~RetainedUnderlayCache(); RetainedUnderlayDecision evaluate(const std::optional&, @@ -150,6 +157,7 @@ private: std::string lastInvalidationReason; std::shared_ptr runtimeState; uint64_t observedControlRevision = 0; + RetainedRangeRole role; }; } // namespace mbgl diff --git a/src/mbgl/renderer/retained_underlay_runtime_state.hpp b/src/mbgl/renderer/retained_underlay_runtime_state.hpp index 5978a7c1d..59fee46a5 100644 --- a/src/mbgl/renderer/retained_underlay_runtime_state.hpp +++ b/src/mbgl/renderer/retained_underlay_runtime_state.hpp @@ -3,6 +3,8 @@ #include #include +#include +#include namespace mbgl { @@ -83,15 +85,81 @@ public: bool isEnabled() const noexcept { return enabled.load(std::memory_order_acquire); } uint64_t getControlRevision() const noexcept { return controlRevision.load(std::memory_order_acquire); } + bool setContinuousLayerRange(std::string startLayerId, std::string endLayerId) { + if (startLayerId.empty() || endLayerId.empty()) return false; + { + std::lock_guard lock(rangeMutex); + if (rangeConfig.configured && rangeConfig.startLayerId == startLayerId && + rangeConfig.endLayerId == endLayerId) { + return true; + } + rangeConfig.configured = true; + rangeConfig.startLayerId = std::move(startLayerId); + rangeConfig.endLayerId = std::move(endLayerId); + rangeConfig.revision++; + rangeResolution = {}; + rangeResolution.configured = true; + rangeResolution.startLayerId = rangeConfig.startLayerId; + rangeResolution.endLayerId = rangeConfig.endLayerId; + rangeResolution.configRevision = rangeConfig.revision; + rangeResolution.reason = "style-not-ready"; + fallback.fetch_add(1, std::memory_order_relaxed); + } + controlRevision.fetch_add(1, std::memory_order_release); + return true; + } + + ContinuousLayerRangeConfig continuousLayerRangeConfig() const { + std::lock_guard lock(rangeMutex); + return rangeConfig; + } + + void recordRangeResolution(const ContinuousLayerRangeResolution& value, bool bottom, bool top) { + std::lock_guard lock(rangeMutex); + if (value.configRevision != rangeConfig.revision) return; + const bool changed = rangeResolution.configRevision != value.configRevision || + rangeResolution.valid != value.valid || + rangeResolution.startIndex != value.startIndex || + rangeResolution.endIndex != value.endIndex || + rangeResolution.reason != value.reason; + rangeResolution = value; + bottomRangePresent = bottom; + topRangePresent = top; + if (changed && value.configured && !value.valid) { + fallback.fetch_add(1, std::memory_order_relaxed); + } + } + RetainedUnderlayDiagnostics snapshot() const { - return { - isEnabled(), - build.load(std::memory_order_relaxed), - reuse.load(std::memory_order_relaxed), - fallback.load(std::memory_order_relaxed), - invalidation.load(std::memory_order_relaxed), - retainedUnderlayReasonText(lastReason.load(std::memory_order_acquire)), - }; + RetainedUnderlayDiagnostics result; + result.enabled = isEnabled(); + result.build = build.load(std::memory_order_relaxed); + result.reuse = reuse.load(std::memory_order_relaxed); + result.fallback = fallback.load(std::memory_order_relaxed); + result.invalidation = invalidation.load(std::memory_order_relaxed); + result.lastReason = retainedUnderlayReasonText(lastReason.load(std::memory_order_acquire)); + result.bottomBuild = bottomBuild.load(std::memory_order_relaxed); + result.bottomReuse = bottomReuse.load(std::memory_order_relaxed); + result.bottomInvalidation = bottomInvalidation.load(std::memory_order_relaxed); + result.topBuild = topBuild.load(std::memory_order_relaxed); + result.topReuse = topReuse.load(std::memory_order_relaxed); + result.topInvalidation = topInvalidation.load(std::memory_order_relaxed); + { + std::lock_guard lock(rangeMutex); + result.rangeConfigured = rangeResolution.configured; + result.rangeValid = rangeResolution.valid; + result.startLayerId = rangeResolution.startLayerId; + result.endLayerId = rangeResolution.endLayerId; + result.startIndex = rangeResolution.startIndex; + result.endIndex = rangeResolution.endIndex; + result.bottomPresent = bottomRangePresent; + result.topPresent = topRangePresent; + result.rangeFallback = rangeResolution.configured && !rangeResolution.valid; + if (result.rangeFallback) { + result.lastReason = rangeResolution.reason; + } + } + return result; } void recordBuild() noexcept { build.fetch_add(1, std::memory_order_relaxed); } @@ -101,6 +169,15 @@ public: lastReason.store(reason, std::memory_order_release); invalidation.fetch_add(1, std::memory_order_relaxed); } + void recordRangeBuild(bool top) noexcept { + (top ? topBuild : bottomBuild).fetch_add(1, std::memory_order_relaxed); + } + void recordRangeReuse(bool top) noexcept { + (top ? topReuse : bottomReuse).fetch_add(1, std::memory_order_relaxed); + } + void recordRangeInvalidation(bool top) noexcept { + (top ? topInvalidation : bottomInvalidation).fetch_add(1, std::memory_order_relaxed); + } private: std::atomic enabled{true}; std::atomic controlRevision{0}; @@ -109,6 +186,17 @@ private: std::atomic fallback{0}; std::atomic invalidation{0}; std::atomic lastReason{RetainedUnderlayReason::None}; + std::atomic bottomBuild{0}; + std::atomic bottomReuse{0}; + std::atomic bottomInvalidation{0}; + std::atomic topBuild{0}; + std::atomic topReuse{0}; + std::atomic topInvalidation{0}; + mutable std::mutex rangeMutex; + ContinuousLayerRangeConfig rangeConfig; + ContinuousLayerRangeResolution rangeResolution; + bool bottomRangePresent = false; + bool topRangePresent = false; }; } // namespace mbgl diff --git a/test/renderer/retained_underlay_cache.test.cpp b/test/renderer/retained_underlay_cache.test.cpp index c5ee2e248..e7afece95 100644 --- a/test/renderer/retained_underlay_cache.test.cpp +++ b/test/renderer/retained_underlay_cache.test.cpp @@ -1,5 +1,6 @@ #include #include +#include #include @@ -10,7 +11,7 @@ using namespace mbgl; namespace { RetainedUnderlayKey key(Size size = {640, 480}, int32_t boundary = 4) { - RetainedUnderlayKey result{size, boundary, {}}; + RetainedUnderlayKey result{size, 0, boundary, {}}; result.cameraMatrix[0] = 1.0; result.cameraMatrix[5] = 1.0; result.cameraMatrix[10] = 1.0; @@ -128,7 +129,7 @@ TEST(RetainedUnderlayRevision, MonotonicAcrossCameraViewportStyleAndSourceChange RetainedUnderlayCache cache; auto state = revisionState(); const auto currentKey = [&] { - return RetainedUnderlayKey{state.viewport, *state.boundary, state.cameraMatrix}; + return RetainedUnderlayKey{state.viewport, 0, *state.boundary, state.cameraMatrix}; }; EXPECT_EQ(tracker.observe(state), 1u); EXPECT_EQ(tracker.observe(state), 1u); @@ -191,6 +192,134 @@ TEST(RetainedUnderlayRevision, MonotonicAcrossCameraViewportStyleAndSourceChange RetainedUnderlayDecision::Build); } +TEST(ContinuousLayerRange, ResolvesInclusiveSingleAndMultiLayerRanges) { + const std::vector layers{"bottom", "hidden-start", "middle", "hidden-end", "top"}; + auto single = resolveContinuousLayerRange({true, "middle", "middle", 1}, layers); + ASSERT_TRUE(single.valid); + EXPECT_EQ(single.startIndex, 2); + EXPECT_EQ(single.endIndex, 2); + EXPECT_EQ(single.endExclusive(), 3); + + auto multi = resolveContinuousLayerRange({true, "hidden-start", "hidden-end", 2}, layers); + ASSERT_TRUE(multi.valid); + EXPECT_EQ(multi.startIndex, 1); + EXPECT_EQ(multi.endIndex, 3); + EXPECT_TRUE(multi.bottomPresent()); + EXPECT_TRUE(multi.topPresent(static_cast(layers.size()))); +} + +TEST(ContinuousLayerRange, RejectsMissingAndReversedEndpointsWithoutStaleIndices) { + const std::vector layers{"a", "b", "c"}; + const auto reversed = resolveContinuousLayerRange({true, "c", "a", 1}, layers); + EXPECT_FALSE(reversed.valid); + EXPECT_EQ(reversed.reason, "range-order-invalid"); + + const auto missingStart = resolveContinuousLayerRange({true, "missing", "c", 2}, layers); + EXPECT_FALSE(missingStart.valid); + EXPECT_EQ(missingStart.startIndex, -1); + EXPECT_EQ(missingStart.reason, "start-layer-missing"); + + const auto missingEnd = resolveContinuousLayerRange({true, "a", "missing", 3}, layers); + EXPECT_FALSE(missingEnd.valid); + EXPECT_EQ(missingEnd.endIndex, -1); + EXPECT_EQ(missingEnd.reason, "end-layer-missing"); +} + +TEST(ContinuousLayerRange, SupportsAllFourDegradedTopologies) { + const std::vector layers{"a", "b", "c", "d"}; + const auto three = resolveContinuousLayerRange({true, "b", "c", 1}, layers); + EXPECT_TRUE(three.bottomPresent()); + EXPECT_TRUE(three.topPresent(4)); + + const auto bottomOnly = resolveContinuousLayerRange({true, "b", "d", 2}, layers); + EXPECT_TRUE(bottomOnly.bottomPresent()); + EXPECT_FALSE(bottomOnly.topPresent(4)); + + const auto topOnly = resolveContinuousLayerRange({true, "a", "c", 3}, layers); + EXPECT_FALSE(topOnly.bottomPresent()); + EXPECT_TRUE(topOnly.topPresent(4)); + + const auto continuousOnly = resolveContinuousLayerRange({true, "a", "d", 4}, layers); + EXPECT_TRUE(continuousOnly.valid); + EXPECT_FALSE(continuousOnly.bottomPresent()); + EXPECT_FALSE(continuousOnly.topPresent(4)); +} + +TEST(ContinuousLayerRange, OnlyActuallyPresentRetainedRangesRequestCacheBuilds) { + RetainedUnderlayCache bottom(RetainedRangeRole::Bottom); + RetainedUnderlayCache top(RetainedRangeRole::Top); + const auto countBuilds = [&](bool bottomPresent, bool topPresent) { + const auto bottomDecision = bottom.evaluate(bottomPresent ? std::optional(key()) + : std::nullopt, + 1, true); + const auto topDecision = top.evaluate(topPresent ? std::optional(key()) + : std::nullopt, + 1, true); + return static_cast(bottomDecision == RetainedUnderlayDecision::Build) + + static_cast(topDecision == RetainedUnderlayDecision::Build); + }; + EXPECT_EQ(countBuilds(true, true), 2); + EXPECT_EQ(countBuilds(true, false), 1); + EXPECT_EQ(countBuilds(false, true), 1); + EXPECT_EQ(countBuilds(false, false), 0); +} + +TEST(ContinuousLayerRange, ReorderAndEndpointDeletionAreResolvedFromCurrentStyle) { + const ContinuousLayerRangeConfig config{true, "b", "c", 7}; + EXPECT_TRUE(resolveContinuousLayerRange(config, {"a", "b", "c"}).valid); + EXPECT_FALSE(resolveContinuousLayerRange(config, {"a", "c", "b"}).valid); + const auto deleted = resolveContinuousLayerRange(config, {"a", "b"}); + EXPECT_FALSE(deleted.valid); + EXPECT_EQ(deleted.reason, "end-layer-missing"); +} + +TEST(ContinuousLayerRange, RuntimeConfigurationRejectsEmptyIdsAndAdvancesMonotonically) { + RetainedUnderlayRuntimeState state; + EXPECT_FALSE(state.setContinuousLayerRange("", "end")); + EXPECT_EQ(state.continuousLayerRangeConfig().revision, 0u); + EXPECT_TRUE(state.setContinuousLayerRange("start", "end")); + EXPECT_EQ(state.continuousLayerRangeConfig().revision, 1u); + EXPECT_TRUE(state.setContinuousLayerRange("start", "end")); + EXPECT_EQ(state.continuousLayerRangeConfig().revision, 1u); + EXPECT_TRUE(state.setContinuousLayerRange("same", "same")); + const auto config = state.continuousLayerRangeConfig(); + EXPECT_EQ(config.revision, 2u); + EXPECT_EQ(config.startLayerId, config.endLayerId); +} + +TEST(ContinuousLayerRange, ExplicitConfigurationNeverMixesWithLegacyBoundary) { + ContinuousLayerRangeResolution valid; + valid.configured = true; + valid.valid = true; + valid.startIndex = 0; + valid.endIndex = 1; + valid.configRevision = 4; + const auto explicitPlan = selectContinuousRenderRanges(valid, 7, 10); + EXPECT_EQ(explicitPlan.continuousBegin, 0); + EXPECT_EQ(explicitPlan.continuousEndExclusive, 2); + EXPECT_FALSE(explicitPlan.bottomPresent()); + EXPECT_TRUE(explicitPlan.topPresent()); + + valid.valid = false; + valid.reason = "start-layer-missing"; + const auto invalidPlan = selectContinuousRenderRanges(valid, 7, 10); + EXPECT_FALSE(invalidPlan.retainedMode()); + EXPECT_FALSE(invalidPlan.bottomPresent()); + EXPECT_FALSE(invalidPlan.topPresent()); + + const ContinuousLayerRangeResolution unconfigured; + const auto legacyPlan = selectContinuousRenderRanges(unconfigured, 7, 10); + EXPECT_TRUE(legacyPlan.retainedMode()); + EXPECT_EQ(legacyPlan.continuousBegin, 7); + EXPECT_FALSE(legacyPlan.topPresent()); +} + +TEST(ContinuousLayerRange, AbsentRetainedRangesDoNotCauseFastPathMiss) { + auto input = fastPathInput(); + input.cacheValid = true; // all zero actually-present retained ranges are valid vacuously. + EXPECT_TRUE(evaluateParticleFastPath(input).candidate); +} + TEST(RetainedUnderlayRevision, RepeatedFramesWithoutSemanticMutationDoNotAdvanceRevision) { RetainedUnderlayRevisionTracker tracker; auto state = revisionState();