feat: add continuous retained render ranges
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-09-24 11:46:19 +09:00
parent b29e564944
commit a9eaec6b65
25 changed files with 857 additions and 120 deletions

View File

@@ -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;

View File

@@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
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

View File

@@ -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() }

View File

@@ -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) });
}];
}

View File

@@ -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<RuntimeSnapshot>;
detach(reactTag: number): Promise<void>;
getState(reactTag: number): Promise<RuntimeSnapshot>;
setRetainedUnderlayEnabled(reactTag: number, enabled: boolean): Promise<CommandResult>;
setContinuousLayerRange(reactTag: number, startLayerId: string, endLayerId: string): Promise<CommandResult>;
getRetainedUnderlayDiagnostics(reactTag: number): Promise<RetainedUnderlaySnapshot>;
clearWindFields(reactTag: number): Promise<CommandResult>;
submitWindField(reactTag: number, metadataJson: string, payloadFileUri: string): Promise<FieldSubmitResult>;

View File

@@ -13,6 +13,10 @@ function nativeBridge() {
export const RendererRuntime = {
setRetainedUnderlayEnabled: (reactTag: number, enabled: boolean): Promise<CommandResult> =>
nativeBridge().setRetainedUnderlayEnabled(reactTag, enabled),
setContinuousLayerRange: (reactTag: number, startLayerId: string, endLayerId: string): Promise<CommandResult> => {
if (startLayerId.length === 0 || endLayerId.length === 0) return Promise.resolve("REJECTED");
return nativeBridge().setContinuousLayerRange(reactTag, startLayerId, endLayerId);
},
getRetainedUnderlayDiagnostics: (reactTag: number): Promise<RetainedUnderlayDiagnostics> =>
nativeBridge().getRetainedUnderlayDiagnostics(reactTag),
} as const;

View File

@@ -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;
}>;

View File

@@ -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<std::string>(env, startLayerId),
jni::Make<std::string>(env, endLayerId));
}
jni::Local<jni::Array<jni::String>> NativeMapView::getRetainedUnderlayDiagnostics(JNIEnv& env) {
const auto snapshot = map->getRetainedUnderlayDiagnostics();
const std::array<std::string, 6> values = {
const std::array<std::string, 20> 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<jni::String>::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"),

View File

@@ -203,6 +203,8 @@ public:
void setRetainedUnderlayEnabled(JNIEnv&, jni::jboolean);
jni::jboolean setContinuousLayerRange(JNIEnv&, const jni::String&, const jni::String&);
jni::Local<jni::Array<jni::String>> getRetainedUnderlayDiagnostics(JNIEnv&);
jni::Local<jni::Array<jni::String>> getActionJournalLogFiles(JNIEnv&);

View File

@@ -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;

View File

@@ -288,6 +288,8 @@ interface NativeMap {
void setRetainedUnderlayEnabled(boolean enabled);
boolean setContinuousLayerRange(String startLayerId, String endLayerId);
RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics();
String[] getActionJournalLogFiles();

View File

@@ -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();

View File

@@ -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"
});
}
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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();
}

View File

@@ -0,0 +1,80 @@
#pragma once
#include <mbgl/renderer/retained_underlay.hpp>
#include <cstdint>
#include <string>
#include <vector>
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<int32_t> 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<std::string>& 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<int32_t>(index);
if (orderedLayerIds[index] == config.endLayerId) result.endIndex = static_cast<int32_t>(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

View File

@@ -1,4 +1,5 @@
#include <mbgl/renderer/render_orchestrator.hpp>
#include <mbgl/renderer/continuous_layer_range.hpp>
#include <mbgl/annotation/annotation_manager.hpp>
#include <mbgl/layermanager/layer_manager.hpp>
@@ -406,6 +407,26 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
orderedLayers[index].get().startsRetainedDynamicRange());
}
retainedDynamicBoundary = boundaryTracker.value();
std::vector<std::string> 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<int32_t>(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<std::size_t>(
boundary > 0 ? static_cast<std::size_t>(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<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()));
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<std::size_t>(
boundary > 0 ? static_cast<std::size_t>(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<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()));
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<std::size_t>(
boundary > 0 ? static_cast<std::size_t>(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<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()));
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<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()));
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();

View File

@@ -119,9 +119,15 @@ public:
bool removeLayerGroup(const LayerGroupBasePtr&);
size_t numLayerGroups() const noexcept;
std::optional<int32_t> getRetainedDynamicBoundary() const noexcept { return retainedDynamicBoundary; }
const ContinuousLayerRangeResolution& getContinuousLayerRange() const noexcept { return continuousLayerRange; }
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;
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 <typename Func /* void(LayerGroupBase&, size_t originalOrdinal) */>
void visitLayerGroupsInRange(int32_t begin, int32_t endExclusive, Func f) {
size_t ordinal = static_cast<size_t>(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 <typename Func /* void(LayerGroupBase&, size_t originalOrdinal) */>
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<size_t>(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 <typename Func /* void(LayerGroupBase&, size_t originalOrdinal) */>
void visitLayerGroupsBefore(int32_t boundary, Func f) {
size_t ordinal = 0;
@@ -343,6 +375,7 @@ private:
using LayerGroupMap = std::multimap<int32_t, LayerGroupBasePtr>;
LayerGroupMap layerGroupsByLayerIndex;
std::optional<int32_t> retainedDynamicBoundary;
ContinuousLayerRangeResolution continuousLayerRange;
std::vector<RenderTargetPtr> renderTargets;
RenderItem::DebugLayerGroupMap debugLayerGroups;

View File

@@ -29,6 +29,7 @@
#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
@@ -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>()) {}
retainedUnderlayCache(std::make_unique<RetainedUnderlayCache>(RetainedRangeRole::Bottom)),
retainedTopCache(std::make_unique<RetainedUnderlayCache>(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<RetainedUnderlayRuntimeState>& state) {
retainedUnderlayCache->synchronizeRuntimeState(state);
retainedTopCache->synchronizeRuntimeState(state);
}
Renderer::Impl::ParticleFastPathUpdateSignature Renderer::Impl::makeParticleFastPathUpdateSignature(
@@ -192,28 +201,48 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptr<UpdatePara
}
const auto retained = retainedUnderlayCache->getDiagnostics();
const auto currentBoundary = orchestrator.getRetainedDynamicBoundary();
const auto ranges = activeRenderRanges(orchestrator);
const auto liveRangeConfig = updateParameters->retainedUnderlayState
? updateParameters->retainedUnderlayState->continuousLayerRangeConfig()
: ContinuousLayerRangeConfig{};
const std::optional<int32_t> currentBoundary =
(ranges.explicitValid || !ranges.explicitConfigured) ? std::optional<int32_t>(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<int32_t>(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<UpdatePara
void Renderer::Impl::commitPreparedRenderTree(std::unique_ptr<RenderTree> renderTree,
const std::shared_ptr<UpdateParameters>& 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<int32_t>(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<RetainedUnderlayKey> 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<int32_t> retainedBoundary = retainedMode
? std::optional<int32_t>(ranges.continuousBegin) : std::nullopt;
std::optional<RetainedUnderlayKey> bottomKey;
std::optional<RetainedUnderlayKey> topKey;
RetainedUnderlayTopologySnapshot retainedTopology{
.boundary = retainedBoundary,
.staticPrefix = {},
.continuousEndExclusive = retainedMode ? std::optional<int32_t>(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<uint8_t>(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<uint8_t>(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<uint32_t>(*retainedBoundary) && item.layer.get().usesLegacyRendering()) {
legacyBelowBoundary = true;
const auto index = static_cast<int32_t>(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<int32_t>(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<uint32_t>(ordinal);
layerGroup.runTweakers(renderTree, parameters);
});
if (particleFastFrame && retainedMode) {
orchestrator.visitLayerGroupsInRange(
ranges.continuousBegin, ranges.continuousEndExclusive,
[&](LayerGroupBase& layerGroup, size_t ordinal) {
parameters.currentLayer = static_cast<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<int32_t>(item.index) >= ranges.continuousBegin &&
static_cast<int32_t>(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); });

View File

@@ -62,6 +62,7 @@ private:
const float pixelRatio;
std::unique_ptr<RenderStaticData> staticData;
std::unique_ptr<RetainedUnderlayCache> retainedUnderlayCache;
std::unique_ptr<RetainedUnderlayCache> retainedTopCache;
RetainedUnderlayRevisionTracker retainedUnderlayRevisionTracker;
RetainedUnderlayTopologyTracker retainedUnderlayTopologyTracker;
ParticleFastPathPreparedTree<RenderTree> particlePreparedTree;
@@ -107,6 +108,10 @@ private:
uint64_t particlePreparedTopologyRevision = 0;
uint64_t particlePreparedStaticRevision = 0;
std::optional<int32_t> particlePreparedBoundary;
std::optional<int32_t> particlePreparedContinuousEnd;
uint64_t particlePreparedRangeConfigRevision = 0;
bool particlePreparedBottomPresent = false;
bool particlePreparedTopPresent = false;
Size particlePreparedBackendSize;
bool particlePreparedSceneSupported = false;
ParticleFastPathMissReason particleFastPathLastReason = ParticleFastPathMissReason::NoPreparedTree;

View File

@@ -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<RetainedUnderlayRuntimeState>()) {}
RetainedUnderlayCache::RetainedUnderlayCache(RetainedRangeRole role_)
: runtimeState(std::make_shared<RetainedUnderlayRuntimeState>()), 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::optional<Ret
if (valid) {
++reuseCount;
runtimeState->recordReuse();
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<gfx::VertexVector<HeatmapTextureLayoutVertex>>(
@@ -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);
}

View File

@@ -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<int32_t> boundary;
std::vector<RetainedUnderlayTopologyEntry> staticPrefix;
std::optional<int32_t> continuousEndExclusive;
std::vector<RetainedUnderlayTopologyEntry> staticSuffix;
bool operator==(const RetainedUnderlayTopologySnapshot&) const = default;
};
@@ -60,6 +65,8 @@ struct RetainedUnderlayRevisionState {
Size viewport;
mat4 cameraMatrix{};
std::optional<int32_t> boundary;
std::optional<int32_t> 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<RetainedUnderlayKey>&,
@@ -150,6 +157,7 @@ private:
std::string lastInvalidationReason;
std::shared_ptr<RetainedUnderlayRuntimeState> runtimeState;
uint64_t observedControlRevision = 0;
RetainedRangeRole role;
};
} // namespace mbgl

View File

@@ -3,6 +3,8 @@
#include <mbgl/renderer/retained_underlay.hpp>
#include <atomic>
#include <mutex>
#include <utility>
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<std::mutex> 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<std::mutex> lock(rangeMutex);
return rangeConfig;
}
void recordRangeResolution(const ContinuousLayerRangeResolution& value, bool bottom, bool top) {
std::lock_guard<std::mutex> 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<std::mutex> 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<bool> enabled{true};
std::atomic<uint64_t> controlRevision{0};
@@ -109,6 +186,17 @@ private:
std::atomic<uint64_t> fallback{0};
std::atomic<uint64_t> invalidation{0};
std::atomic<RetainedUnderlayReason> lastReason{RetainedUnderlayReason::None};
std::atomic<uint64_t> bottomBuild{0};
std::atomic<uint64_t> bottomReuse{0};
std::atomic<uint64_t> bottomInvalidation{0};
std::atomic<uint64_t> topBuild{0};
std::atomic<uint64_t> topReuse{0};
std::atomic<uint64_t> topInvalidation{0};
mutable std::mutex rangeMutex;
ContinuousLayerRangeConfig rangeConfig;
ContinuousLayerRangeResolution rangeResolution;
bool bottomRangePresent = false;
bool topRangePresent = false;
};
} // namespace mbgl

View File

@@ -1,5 +1,6 @@
#include <mbgl/renderer/retained_underlay_cache.hpp>
#include <mbgl/renderer/particle_fast_path.hpp>
#include <mbgl/renderer/continuous_layer_range.hpp>
#include <gtest/gtest.h>
@@ -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<std::string> 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<int32_t>(layers.size())));
}
TEST(ContinuousLayerRange, RejectsMissingAndReversedEndpointsWithoutStaleIndices) {
const std::vector<std::string> 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<std::string> 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<RetainedUnderlayKey>(key())
: std::nullopt,
1, true);
const auto topDecision = top.evaluate(topPresent ? std::optional<RetainedUnderlayKey>(key())
: std::nullopt,
1, true);
return static_cast<int>(bottomDecision == RetainedUnderlayDecision::Build) +
static_cast<int>(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();