feat: add retained underlay runtime controls

This commit is contained in:
tei
2026-09-22 16:18:58 +09:00
parent 9ad2fe5d58
commit 8384c8bd24
31 changed files with 1181 additions and 59 deletions

View File

@@ -182,6 +182,7 @@ list(APPEND SRC_FILES
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/render_target.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/retained_underlay_cache.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/retained_underlay_cache.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/retained_underlay_runtime_state.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/change_request.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layer_group.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/tile_layer_group.cpp
@@ -307,6 +308,7 @@ list(APPEND INCLUDE_FILES
${PROJECT_SOURCE_DIR}/include/mbgl/renderer/renderer_observer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/renderer/renderer_state.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/renderer/renderer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/renderer/retained_underlay.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/shaders/program_parameters.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/shaders/shader_source.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/storage/database_file_source.hpp

View File

@@ -14,6 +14,7 @@
#include <mbgl/storage/resource_options.hpp>
#include <mbgl/util/client_options.hpp>
#include <mbgl/util/action_journal_options.hpp>
#include <mbgl/renderer/retained_underlay.hpp>
#include <cstdint>
#include <string>
@@ -55,6 +56,11 @@ public:
/// Triggers a repaint.
void triggerRepaint();
/** Enables or disables the map-scoped retained static underlay optimization. */
void setRetainedUnderlayEnabled(bool);
bool isRetainedUnderlayEnabled() const;
RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() const;
style::Style& getStyle();
const style::Style& getStyle() const;

View File

@@ -0,0 +1,17 @@
#pragma once
#include <cstdint>
#include <string>
namespace mbgl {
struct RetainedUnderlayDiagnostics {
bool enabled = true;
uint64_t build = 0;
uint64_t reuse = 0;
uint64_t fallback = 0;
uint64_t invalidation = 0;
std::string lastReason;
};
} // namespace mbgl

View File

@@ -10,7 +10,12 @@ import {
findNodeHandle,
} from "react-native";
import { Camera, Map } from "@maplibre/maplibre-react-native";
import { FieldRuntime, type RuntimeSnapshot } from "@navsea/maplibre-particle-rn";
import {
FieldRuntime,
RendererRuntime,
type RetainedUnderlayDiagnostics,
type RuntimeSnapshot,
} from "@navsea/maplibre-particle-rn";
import {
materializeV4Frame as materializeV2Frame,
qbinGoldenDatasets,
@@ -60,6 +65,8 @@ function valueSourceFor(id: WeatherDatasetId): "SCALAR" | "VECTOR_MAGNITUDE" {
export default function App() {
const [reactTag, setReactTag] = useState<number | null>(null);
const [snapshot, setSnapshot] = useState<RuntimeSnapshot>(initialSnapshot);
const [retainedSnapshot, setRetainedSnapshot] =
useState<RetainedUnderlayDiagnostics | null>(null);
const [mapStatus, setMapStatus] = useState("LOADING");
const [weatherStatus, setWeatherStatus] = useState("WAITING");
const [result, setResult] = useState("Waiting for map/style");
@@ -81,6 +88,9 @@ export default function App() {
const refresh = useCallback(async (tag: number) => {
setSnapshot(await FieldRuntime.getState(tag));
}, []);
const refreshRetained = useCallback(async (tag: number) => {
setRetainedSnapshot(await RendererRuntime.getRetainedUnderlayDiagnostics(tag));
}, []);
const applyVisualToTag = useCallback(async (tag: number) => {
const results = await Promise.all([
FieldRuntime.setParticleCount(tag, Number(visual.count)),
@@ -254,12 +264,12 @@ export default function App() {
try {
await FieldRuntime.attach(tag);
setReactTag(tag);
await refresh(tag);
await Promise.all([refresh(tag), refreshRetained(tag)]);
setResult("FieldRuntime attached / particle.10");
} catch (error) {
setResult("attach error: " + String(error));
}
}, [refresh]);
}, [refresh, refreshRetained]);
const onStyleReady = useCallback((event: unknown) => {
const tag = findNodeHandle((event as { target?: unknown }).target as never);
if (tag != null) void attach(tag);
@@ -300,6 +310,12 @@ export default function App() {
setParticleVisible(next);
}
}, [particleVisible, reactTag]);
const toggleRetained = useCallback(async () => {
if (reactTag == null) return;
const next = !(retainedSnapshot?.enabled ?? true);
const result = await RendererRuntime.setRetainedUnderlayEnabled(reactTag, next);
if (result === "ACCEPTED") await refreshRetained(reactTag);
}, [reactTag, refreshRetained, retainedSnapshot?.enabled]);
const updateVisual = useCallback(
(key: keyof typeof initialVisual, value: string) => {
@@ -344,6 +360,22 @@ export default function App() {
</Text>
<Text selectable>Source: {activeSource}</Text>
<Text selectable>{result}</Text>
<Text selectable>
Retained: {retainedSnapshot == null ? "not read" :
`${retainedSnapshot.enabled ? "ON" : "OFF"} build=${retainedSnapshot.build} reuse=${retainedSnapshot.reuse} fallback=${retainedSnapshot.fallback} invalidation=${retainedSnapshot.invalidation} reason=${retainedSnapshot.lastReason}`}
</Text>
<View style={styles.row}>
<Button
title={`Retained ${retainedSnapshot?.enabled === false ? "OFF" : "ON"}`}
disabled={reactTag == null}
onPress={() => void toggleRetained()}
/>
<Button
title="Refresh retained"
disabled={reactTag == null}
onPress={() => reactTag != null && void refreshRetained(reactTag)}
/>
</View>
<Text>QBIN Format</Text>
<View style={styles.row}>

View File

@@ -63,6 +63,21 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
}
@ReactMethod override fun getState(reactTag: Double, promise: Promise) = withRuntime(reactTag, promise) { runtime -> promise.resolve(snapshot(runtime)) }
@ReactMethod override fun setRetainedUnderlayEnabled(reactTag: Double, enabled: Boolean, promise: Promise) = withMap(reactTag, promise) { map ->
map.setRetainedUnderlayEnabled(enabled)
promise.resolve("ACCEPTED")
}
@ReactMethod override fun getRetainedUnderlayDiagnostics(reactTag: Double, promise: Promise) = withMap(reactTag, promise) { map ->
val value = map.retainedUnderlayDiagnostics
promise.resolve(Arguments.createMap().apply {
putBoolean("enabled", value.enabled)
putDouble("build", value.build.toDouble())
putDouble("reuse", value.reuse.toDouble())
putDouble("fallback", value.fallback.toDouble())
putDouble("invalidation", value.invalidation.toDouble())
putString("lastReason", value.lastReason)
})
}
@ReactMethod override fun clearWindFields(reactTag: Double, promise: Promise) = command(reactTag, promise) { it.clearWindFields() }
@ReactMethod override fun clearFields(reactTag: Double, promise: Promise) = command(reactTag, promise) { it.clearFields() }

View File

@@ -158,6 +158,20 @@ RCT_EXPORT_MODULE(WeatherRuntimeBridge)
[self withMapForTag:reactTag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { [map disposeParticleFieldRuntime]; resolve(nil); }];
}
- (void)getState:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { resolve(snapshot(runtime)); }]; }
- (void)setRetainedUnderlayEnabled:(double)reactTag enabled:(BOOL)enabled resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
map.retainedUnderlayEnabled = enabled;
resolve(@"ACCEPTED");
}];
}
- (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 });
}];
}
- (void)command:(double)tag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, NSError **))action {
[self withMapForTag:tag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleFieldRuntimeCommandResult result = action(runtime, &error); resolve(error ? @"REJECTED" : commandString(result)); }];

View File

@@ -9,10 +9,13 @@ 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 };
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>;
getRetainedUnderlayDiagnostics(reactTag: number): Promise<RetainedUnderlaySnapshot>;
clearWindFields(reactTag: number): Promise<CommandResult>;
submitWindField(reactTag: number, metadataJson: string, payloadFileUri: string): Promise<FieldSubmitResult>;
clearFields(reactTag: number): Promise<CommandResult>;

View File

@@ -0,0 +1,18 @@
import { getNativeFieldRuntimeBridge } from "./NativeWeatherRuntimeBridge";
import type { CommandResult, RetainedUnderlayDiagnostics } from "./types";
function nativeBridge() {
const bridge = getNativeFieldRuntimeBridge();
if (bridge == null) {
throw new Error("@navsea/maplibre-particle-rn RendererRuntime requires the WeatherRuntimeBridge native module");
}
return bridge;
}
/** Pull-based, map-scoped renderer controls. No renderer cache state is owned by React. */
export const RendererRuntime = {
setRetainedUnderlayEnabled: (reactTag: number, enabled: boolean): Promise<CommandResult> =>
nativeBridge().setRetainedUnderlayEnabled(reactTag, enabled),
getRetainedUnderlayDiagnostics: (reactTag: number): Promise<RetainedUnderlayDiagnostics> =>
nativeBridge().getRetainedUnderlayDiagnostics(reactTag),
} as const;

View File

@@ -1,4 +1,5 @@
export { FieldRuntime } from "./FieldRuntime";
export { RendererRuntime } from "./RendererRuntime";
export type {
ColorChannel,
CommandResult,
@@ -14,6 +15,7 @@ export type {
RequiredPlaneContractInput,
ResolvedFieldProfileContractInput,
RuntimeSnapshot,
RetainedUnderlayDiagnostics,
RuntimeState,
TargetFps,
} from "./types";

View File

@@ -21,3 +21,11 @@ export type FieldValueSource = "SCALAR" | "VECTOR_MAGNITUDE";
export type ColorChannel = number;
export type FieldColorStop = Readonly<{ value: number; red: ColorChannel; green: ColorChannel; blue: ColorChannel; alpha: ColorChannel }>;
export type RuntimeSnapshot = Readonly<{ state: RuntimeState; playbackState: PlaybackState; targetFps: TargetFps; disposed: boolean }>;
export type RetainedUnderlayDiagnostics = Readonly<{
enabled: boolean;
build: number;
reuse: number;
fallback: number;
invalidation: number;
lastReason: string;
}>;

View File

@@ -716,6 +716,27 @@ jni::jboolean NativeMapView::getDebug(JNIEnv&) {
return map->getDebug() != DebugOptions::NoDebug;
}
void NativeMapView::setRetainedUnderlayEnabled(JNIEnv&, jni::jboolean enabled) {
map->setRetainedUnderlayEnabled(enabled);
}
jni::Local<jni::Array<jni::String>> NativeMapView::getRetainedUnderlayDiagnostics(JNIEnv& env) {
const auto snapshot = map->getRetainedUnderlayDiagnostics();
const std::array<std::string, 6> 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,
};
auto result = jni::Array<jni::String>::New(env, values.size());
for (std::size_t index = 0; index < values.size(); ++index) {
result.Set(env, index, jni::Make<jni::String>(env, values[index]));
}
return result;
}
jni::Local<jni::Array<jni::String>> NativeMapView::getActionJournalLogFiles(JNIEnv& env) {
const auto& actionJournal = map->getActionJournal();
if (!actionJournal) {
@@ -1795,6 +1816,8 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
METHOD(&NativeMapView::addMarkers, "nativeAddMarkers"),
METHOD(&NativeMapView::setDebug, "nativeSetDebug"),
METHOD(&NativeMapView::getDebug, "nativeGetDebug"),
METHOD(&NativeMapView::setRetainedUnderlayEnabled, "nativeSetRetainedUnderlayEnabled"),
METHOD(&NativeMapView::getRetainedUnderlayDiagnostics, "nativeGetRetainedUnderlayDiagnostics"),
METHOD(&NativeMapView::getActionJournalLogFiles, "nativeGetActionJournalLogFiles"),
METHOD(&NativeMapView::getActionJournalLog, "nativeGetActionJournalLog"),
METHOD(&NativeMapView::clearActionJournalLog, "nativeClearActionJournalLog"),

View File

@@ -201,6 +201,10 @@ public:
jni::jboolean getDebug(JNIEnv&);
void setRetainedUnderlayEnabled(JNIEnv&, jni::jboolean);
jni::Local<jni::Array<jni::String>> getRetainedUnderlayDiagnostics(JNIEnv&);
jni::Local<jni::Array<jni::String>> getActionJournalLogFiles(JNIEnv&);
jni::Local<jni::Array<jni::String>> getActionJournalLog(JNIEnv&);

View File

@@ -111,6 +111,22 @@ public final class MapLibreMap {
nativeMapView.triggerRepaint();
}
/** Enables or disables the renderer's map-scoped retained static underlay optimization. */
public void setRetainedUnderlayEnabled(boolean enabled) {
nativeMapView.setRetainedUnderlayEnabled(enabled);
}
/** Returns whether the retained static underlay optimization is enabled for this map. */
public boolean isRetainedUnderlayEnabled() {
return nativeMapView.getRetainedUnderlayDiagnostics().enabled;
}
/** Returns a thread-safe, pull-based retained-underlay diagnostics snapshot. */
@NonNull
public RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() {
return nativeMapView.getRetainedUnderlayDiagnostics();
}
/**
* Query rendering statistics overlay status.
*/

View File

@@ -286,6 +286,10 @@ interface NativeMap {
boolean getDebug();
void setRetainedUnderlayEnabled(boolean enabled);
RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics();
String[] getActionJournalLogFiles();
String[] getActionJournalLog();

View File

@@ -653,6 +653,22 @@ final class NativeMapView implements NativeMap {
return nativeGetDebug();
}
@Override
public void setRetainedUnderlayEnabled(boolean enabled) {
if (checkState("setRetainedUnderlayEnabled")) {
return;
}
nativeSetRetainedUnderlayEnabled(enabled);
}
@Override
public RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() {
if (checkState("getRetainedUnderlayDiagnostics")) {
return new RetainedUnderlayDiagnostics(new String[] {"true", "0", "0", "0", "0", "unavailable"});
}
return new RetainedUnderlayDiagnostics(nativeGetRetainedUnderlayDiagnostics());
}
@Override
public String[] getActionJournalLogFiles() {
if (checkState("getActionJournalLogFiles")) {
@@ -1702,6 +1718,12 @@ final class NativeMapView implements NativeMap {
@Keep
private native boolean nativeGetDebug();
@Keep
private native void nativeSetRetainedUnderlayEnabled(boolean enabled);
@Keep
private native String[] nativeGetRetainedUnderlayDiagnostics();
@Keep
private native String[] nativeGetActionJournalLogFiles();

View File

@@ -0,0 +1,27 @@
package org.maplibre.android.maps;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
/** A pull-based snapshot of this map's retained-underlay renderer state. */
@Keep
public final class RetainedUnderlayDiagnostics {
public final boolean enabled;
public final long build;
public final long reuse;
public final long fallback;
public final long invalidation;
@NonNull public final String lastReason;
RetainedUnderlayDiagnostics(@NonNull String[] values) {
if (values.length != 6) {
throw new IllegalArgumentException("Retained-underlay diagnostics require six values");
}
enabled = Boolean.parseBoolean(values[0]);
build = Long.parseLong(values[1]);
reuse = Long.parseLong(values[2]);
fallback = Long.parseLong(values[3]);
invalidation = Long.parseLong(values[4]);
lastReason = values[5];
}
}

View File

@@ -1,12 +1,17 @@
package org.maplibre.android.testapp.activity.particle
import android.os.Bundle
import android.graphics.Color
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.PopupMenu
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONArray
import org.json.JSONObject
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
@@ -39,6 +44,13 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private var productionFeedMode = false
private var hostSubmissionMode = false
private var realWeatherMode = false
private var pureParticleMode = false
private var purePbfMode = false
private var navSeaPbfMode = false
private var navSeaPbfPosition = NavSeaPbfPosition.AFTER_CLIP
private var navSeaPbfRenderEnabled = true
private var navSeaPbfLayerVisibility = emptyList<NavSeaPbfLayerVisibility>()
private var retainedCacheEnabled = true
private var realWeatherTuningMenu: PopupMenu? = null
private var realWeatherMenuAnchor: android.view.View? = null
private var openRealWeatherTuningAfterStyle = false
@@ -57,7 +69,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
maskMode = savedInstanceState?.getInt(STATE_MASK_MODE, MASK_GLOBAL) ?: MASK_GLOBAL
fixtureMode = savedInstanceState?.getInt(STATE_FIXTURE_MODE, FIXTURE_UNIFORM_EAST) ?: FIXTURE_UNIFORM_EAST
timeMix = savedInstanceState?.getFloat(STATE_TIME_MIX, 0.0f) ?: 0.0f
particleCount = savedInstanceState?.getInt(STATE_PARTICLE_COUNT, PARTICLE_COUNT_256) ?: PARTICLE_COUNT_256
particleCount = (savedInstanceState?.getInt(STATE_PARTICLE_COUNT, PARTICLE_COUNT_256) ?: PARTICLE_COUNT_256)
.coerceIn(MIN_PARTICLE_COUNT, MAX_PARTICLE_COUNT)
lineWidth = savedInstanceState?.getFloat(STATE_LINE_WIDTH, DEFAULT_LINE_WIDTH) ?: DEFAULT_LINE_WIDTH
particleLength = savedInstanceState?.getFloat(STATE_PARTICLE_LENGTH, DEFAULT_PARTICLE_LENGTH) ?: DEFAULT_PARTICLE_LENGTH
particleOpacity = savedInstanceState?.getFloat(STATE_PARTICLE_OPACITY, DEFAULT_PARTICLE_OPACITY) ?: DEFAULT_PARTICLE_OPACITY
@@ -67,6 +80,14 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
timeWindowMode = savedInstanceState?.getBoolean(STATE_TIME_WINDOW, false) ?: false
hostSubmissionMode = savedInstanceState?.getBoolean(STATE_HOST_SUBMISSION, false) ?: false
realWeatherMode = savedInstanceState?.getBoolean(STATE_REAL_WEATHER, false) ?: false
pureParticleMode = savedInstanceState?.getBoolean(STATE_PURE_PARTICLE, false) ?: false
purePbfMode = savedInstanceState?.getBoolean(STATE_PURE_PBF, false) ?: false
navSeaPbfMode = savedInstanceState?.getBoolean(STATE_NAVSEA_PBF, false) ?: false
navSeaPbfPosition = savedInstanceState?.getString(STATE_NAVSEA_PBF_POSITION)
?.let { value -> NavSeaPbfPosition.values().firstOrNull { it.name == value } }
?: NavSeaPbfPosition.AFTER_CLIP
navSeaPbfRenderEnabled = savedInstanceState?.getBoolean(STATE_NAVSEA_PBF_RENDER_ENABLED, true) ?: true
retainedCacheEnabled = savedInstanceState?.getBoolean(STATE_RETAINED_CACHE_ENABLED, true) ?: true
super.onCreate(savedInstanceState)
Log.i(
TAG,
@@ -78,13 +99,16 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
mapView = findViewById(R.id.mapView)
status = findViewById(R.id.validationStatus)
mapView.addOnDidFailLoadingMapListener { errorMessage ->
if (realWeatherMode) {
if (navSeaPbfMode || purePbfMode) {
Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=$errorMessage")
} else if (realWeatherMode) {
Log.w(TAG, "ParticleField: event=BASEMAP_TILE_ERROR provider=GSI reason=$errorMessage failSoft=true")
}
}
mapView.onCreate(savedInstanceState)
mapView.getMapAsync { map ->
maplibreMap = map
maplibreMap.setRetainedUnderlayEnabled(retainedCacheEnabled)
if (savedInstanceState == null) {
map.cameraPosition = CameraPosition.Builder()
.target(LatLng(0.0, 0.0))
@@ -92,6 +116,9 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
.build()
}
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
updateRetainedCacheButtonState()
loadValidationStyle(active = active, enabled = enabled, reason = "INITIAL")
}
@@ -160,10 +187,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
findViewById<Button>(R.id.validationParticle256).setOnClickListener { setParticleCount(PARTICLE_COUNT_256) }
findViewById<Button>(R.id.validationParticle1024).setOnClickListener { setParticleCount(PARTICLE_COUNT_1024) }
findViewById<Button>(R.id.validationParticle4096).setOnClickListener { setParticleCount(PARTICLE_COUNT_4096) }
findViewById<Button>(R.id.validationParticle16384).setOnClickListener { setParticleCount(PARTICLE_COUNT_16384) }
findViewById<Button>(R.id.validationParticle65536).setOnClickListener { setParticleCount(PARTICLE_COUNT_65536) }
findViewById<Button>(R.id.validationParticle262144).setOnClickListener { setParticleCount(PARTICLE_COUNT_262144) }
findViewById<Button>(R.id.validationParticle1048576).setOnClickListener { setParticleCount(PARTICLE_COUNT_1048576) }
findViewById<Button>(R.id.validationParticle10000).setOnClickListener { setParticleCount(MAX_PARTICLE_COUNT) }
findViewById<Button>(R.id.validationAdvanceWindow).setOnClickListener {
if (this::maplibreMap.isInitialized) advanceRuntimeFieldWindow()
}
@@ -221,15 +245,27 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
findViewById<Button>(R.id.validationMenuTime).setOnClickListener { dismissRealWeatherTuning(); showTimeMenu(it) }
findViewById<Button>(R.id.validationMenuVisual).setOnClickListener { dismissRealWeatherTuning(); showMenu(it, "VIS", intArrayOf(R.id.validationVisualDefault, R.id.validationVisualColor, R.id.validationVisualSizeDown, R.id.validationVisualSizeUp, R.id.validationVisualLengthDown, R.id.validationVisualLengthUp, R.id.validationVisualOpacityDown, R.id.validationVisualOpacityUp, R.id.validationVisualTrailDown, R.id.validationVisualTrailUp, R.id.validationVisualFadeDown, R.id.validationVisualFadeUp)) }
findViewById<Button>(R.id.validationMenuCount).setOnClickListener { dismissRealWeatherTuning(); showMenu(it, "COUNT", intArrayOf(R.id.validationParticle256, R.id.validationParticle1024, R.id.validationParticle4096, R.id.validationParticle16384, R.id.validationParticle65536, R.id.validationParticle262144, R.id.validationParticle1048576)) }
findViewById<Button>(R.id.validationMenuCount).setOnClickListener { dismissRealWeatherTuning(); showCountMenu(it) }
findViewById<Button>(R.id.validationMenuMap).setOnClickListener { dismissRealWeatherTuning(); showMenu(it, "MAP", intArrayOf(R.id.validationGlobal, R.id.validationOceanA, R.id.validationOceanB, R.id.validationDateline, R.id.validationWorldWrap)) }
findViewById<Button>(R.id.validationMenuRealWeather).setOnClickListener { activateRealWeather(it) }
findViewById<Button>(R.id.validationMenuNavSeaPbf).setOnClickListener { showNavSeaPbfMenu(it) }
findViewById<Button>(R.id.validationMenuPbfMode).setOnClickListener { showPbfModeMenu(it) }
findViewById<Button>(R.id.validationMenuRetainedCache).setOnClickListener { showRetainedCacheMenu(it) }
findViewById<Button>(R.id.validationMenuMore).setOnClickListener { dismissRealWeatherTuning(); showMenu(it, "MORE", intArrayOf(R.id.validationActive, R.id.validationStatic, R.id.validationDisable, R.id.validationEnable, R.id.validationUniformEast, R.id.validationUniformNorth, R.id.validationVortex, R.id.validationTimeWindow, R.id.validationHostFieldSubmit, R.id.validationProductionInvalid, R.id.validationHostFieldDuplicate, R.id.validationHostFieldInvalid, R.id.validationAdvanceWindow, R.id.validationDuplicateWindow)) }
}
private data class ParticleVisuals(val count: Int, val width: Float, val length: Float, val opacity: Float, val trailOpacity: Float, val trailFade: Float)
private enum class NavSeaPbfPosition(val label: String) {
BOTTOM("BOTTOM"),
AFTER_SEA("AFTER_SEA"),
AFTER_CLIP("AFTER_CLIP"),
BEFORE_SYMBOL_CLUSTER("BEFORE_SYMBOL_CLUSTER"),
TOP("TOP")
}
private fun activateRealWeather(anchor: android.view.View?) {
if (isPbfMatrixMode()) leaveRealWeatherMode()
realWeatherMenuAnchor = anchor ?: findViewById(R.id.validationMenuRealWeather)
if (realWeatherMode) {
if (realWeatherTuningMenu == null) showRealWeatherTuning(realWeatherMenuAnchor!!) else dismissRealWeatherTuning()
@@ -246,11 +282,16 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
productionFeedMode = false
hostSubmissionMode = true
realWeatherMode = true
pureParticleMode = false
purePbfMode = false
navSeaPbfMode = false
fixtureMode = FIXTURE_UNIFORM_EAST
timeMix = 0.0f
timelineState = "NOT READY"
openRealWeatherTuningAfterStyle = true
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "REAL_WEATHER")
}
@@ -258,7 +299,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
dismissRealWeatherTuning()
weatherRuntime?.dispose()
weatherRuntime = null
if (realWeatherMode) {
if (realWeatherMode || isPbfMatrixMode()) {
preRealWeatherVisuals?.let { visuals ->
particleCount = visuals.count; lineWidth = visuals.width; particleLength = visuals.length
particleOpacity = visuals.opacity; trailOpacity = visuals.trailOpacity; trailFade = visuals.trailFade
@@ -266,8 +307,14 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
preRealWeatherVisuals = null
realWeatherMode = false
pureParticleMode = false
purePbfMode = false
navSeaPbfMode = false
navSeaPbfLayerVisibility = emptyList()
openRealWeatherTuningAfterStyle = false
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
}
private fun updateRealWeatherButtonState() {
@@ -277,10 +324,67 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
button.setTextColor(if (realWeatherMode) Color.YELLOW else Color.WHITE)
}
private fun updateNavSeaPbfButtonState() {
val button = findViewById<Button>(R.id.validationMenuNavSeaPbf)
button.isSelected = navSeaPbfMode
button.isActivated = navSeaPbfMode
button.setTextColor(if (navSeaPbfMode) Color.YELLOW else Color.WHITE)
}
private fun updatePbfModeButtonState() {
val button = findViewById<Button>(R.id.validationMenuPbfMode)
button.isSelected = isPbfMatrixMode()
button.isActivated = isPbfMatrixMode()
button.setTextColor(if (isPbfMatrixMode()) Color.YELLOW else Color.WHITE)
}
private fun updateRetainedCacheButtonState() {
val button = findViewById<Button>(R.id.validationMenuRetainedCache)
button.text = if (retainedCacheEnabled) "CACHE ON" else "CACHE OFF"
button.setTextColor(if (retainedCacheEnabled) Color.GREEN else Color.YELLOW)
}
private fun showRetainedCacheMenu(anchor: android.view.View) {
PopupMenu(this, anchor).apply {
menu.add(0, 0, 0, "CACHE ON")
menu.add(0, 1, 1, "CACHE OFF")
menu.add(0, 2, 2, "READ SNAPSHOT")
setOnMenuItemClickListener { item ->
when (item.itemId) {
0 -> setRetainedCacheEnabled(true)
1 -> setRetainedCacheEnabled(false)
2 -> logRetainedCacheSnapshot()
}
true
}
show()
}
}
private fun setRetainedCacheEnabled(enabled: Boolean) {
retainedCacheEnabled = enabled
maplibreMap.setRetainedUnderlayEnabled(enabled)
updateRetainedCacheButtonState()
status.text = statusText()
Log.i(TAG, "RETAINED_CACHE_CONTROL enabled=$enabled")
logRetainedCacheSnapshot()
}
private fun logRetainedCacheSnapshot() {
val snapshot = maplibreMap.retainedUnderlayDiagnostics
retainedCacheEnabled = snapshot.enabled
updateRetainedCacheButtonState()
Log.i(
TAG,
"RETAINED_CACHE_SNAPSHOT enabled=${snapshot.enabled} build=${snapshot.build} reuse=${snapshot.reuse} " +
"fallback=${snapshot.fallback} invalidation=${snapshot.invalidation} lastReason=${snapshot.lastReason}"
)
}
private fun dismissRealWeatherTuning() { realWeatherTuningMenu?.dismiss(); realWeatherTuningMenu = null }
private fun showRealWeatherTuning(anchor: android.view.View) {
if (!realWeatherMode) return
if (!usesRealWeatherAssets()) return
PopupMenu(this, anchor).also { popup ->
realWeatherTuningMenu = popup
popup.menu.add(0, 100, 0, "PARTICLE TUNING").isEnabled = false
@@ -338,6 +442,199 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
setVisualFloat("raster-particle-trail-fade", REAL_WEATHER_TRAIL_FADE) { trailFade = it }
}
private fun showNavSeaPbfMenu(anchor: android.view.View) {
dismissRealWeatherTuning()
val positions = NavSeaPbfPosition.values()
PopupMenu(this, anchor).apply {
positions.forEachIndexed { index, position ->
menu.add(0, index, index, position.label).isChecked = navSeaPbfMode && navSeaPbfPosition == position
}
menu.add(0, 101, 101, "PBF ON").apply {
isEnabled = hasProductionPbfStyle() && styleReady
isChecked = hasProductionPbfStyle() && navSeaPbfRenderEnabled
}
menu.add(0, 102, 102, "PBF OFF").apply {
isEnabled = hasProductionPbfStyle() && styleReady
isChecked = hasProductionPbfStyle() && !navSeaPbfRenderEnabled
}
menu.add(0, 100, 100, "PARTICLE TUNING")
setOnMenuItemClickListener { item ->
if (item.itemId in positions.indices) {
activateNavSeaPbf(positions[item.itemId], anchor)
} else if (item.itemId == 101) {
setNavSeaPbfRenderEnabled(true)
} else if (item.itemId == 102) {
setNavSeaPbfRenderEnabled(false)
} else if (item.itemId == 100) {
realWeatherMenuAnchor = anchor
showRealWeatherTuning(anchor)
}
true
}
show()
}
}
private fun activateNavSeaPbf(position: NavSeaPbfPosition, anchor: android.view.View) {
enterPbfMatrix()
realWeatherMenuAnchor = anchor
timeWindowMode = true
productionFeedMode = false
hostSubmissionMode = true
realWeatherMode = false
pureParticleMode = false
purePbfMode = false
navSeaPbfMode = true
navSeaPbfPosition = position
fixtureMode = FIXTURE_UNIFORM_EAST
timeMix = 0.0f
timelineState = "NOT READY"
openRealWeatherTuningAfterStyle = false
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "NAVSEA_PBF_${position.name}")
}
private fun showPbfModeMenu(anchor: android.view.View) {
PopupMenu(this, anchor).apply {
menu.add(0, 0, 0, "PURE PARTICLE").isChecked = pureParticleMode
menu.add(0, 1, 1, "PURE PBF").isChecked = purePbfMode
menu.add(0, 2, 2, "NAVSEA PBF").isChecked = navSeaPbfMode
setOnMenuItemClickListener { item ->
when (item.itemId) {
0 -> activatePureParticle(anchor)
1 -> activatePurePbf(anchor)
2 -> activateNavSeaPbf(navSeaPbfPosition, anchor)
}
true
}
show()
}
}
private fun showCountMenu(anchor: android.view.View) {
PopupMenu(this, anchor).apply {
menu.add(0, 0, 0, "256")
menu.add(0, 1, 1, "1K")
menu.add(0, 2, 2, "4K")
menu.add(0, 3, 3, "10K")
menu.add(0, 4, 4, "CUSTOM COUNT")
setOnMenuItemClickListener { item ->
when (item.itemId) {
0 -> setParticleCount(PARTICLE_COUNT_256)
1 -> setParticleCount(PARTICLE_COUNT_1024)
2 -> setParticleCount(PARTICLE_COUNT_4096)
3 -> setParticleCount(MAX_PARTICLE_COUNT)
4 -> showCustomParticleCountDialog()
}
true
}
show()
}
}
private fun showCustomParticleCountDialog() {
val input = EditText(this).apply {
inputType = InputType.TYPE_CLASS_NUMBER
setText(particleCount.toString())
selectAll()
hint = "$MIN_PARTICLE_COUNT..$MAX_PARTICLE_COUNT"
contentDescription = "CUSTOM PARTICLE COUNT"
}
val dialog = AlertDialog.Builder(this)
.setTitle("CUSTOM PARTICLE COUNT")
.setView(input)
.setNegativeButton("CANCEL", null)
.setPositiveButton("APPLY", null)
.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val requested = input.text.toString().toIntOrNull()
if (requested == null || requested !in MIN_PARTICLE_COUNT..MAX_PARTICLE_COUNT) {
input.error = "Enter an integer from $MIN_PARTICLE_COUNT to $MAX_PARTICLE_COUNT"
} else if (setParticleCount(requested)) {
dialog.dismiss()
} else {
input.error = "Particle layer is not ready"
}
}
}
dialog.show()
}
private fun enterPbfMatrix() {
if (realWeatherMode) leaveRealWeatherMode()
if (!isPbfMatrixMode()) {
preRealWeatherVisuals = ParticleVisuals(particleCount, lineWidth, particleLength, particleOpacity, trailOpacity, trailFade)
particleCount = REAL_WEATHER_PARTICLE_COUNT
lineWidth = REAL_WEATHER_LINE_WIDTH
particleLength = REAL_WEATHER_STREAK_LENGTH
particleOpacity = REAL_WEATHER_PARTICLE_OPACITY
trailOpacity = REAL_WEATHER_TRAIL_OPACITY
trailFade = REAL_WEATHER_TRAIL_FADE
realWeatherTargetFps = WeatherRuntime.TargetFps.FPS_30
}
}
private fun activatePureParticle(anchor: android.view.View) {
enterPbfMatrix()
realWeatherMenuAnchor = anchor
timeWindowMode = true
productionFeedMode = false
hostSubmissionMode = true
realWeatherMode = false
pureParticleMode = true
purePbfMode = false
navSeaPbfMode = false
fixtureMode = FIXTURE_UNIFORM_EAST
timeMix = 0.0f
timelineState = "NOT READY"
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "PURE_PARTICLE")
}
private fun activatePurePbf(anchor: android.view.View) {
enterPbfMatrix()
realWeatherMenuAnchor = anchor
timeWindowMode = false
productionFeedMode = false
hostSubmissionMode = false
realWeatherMode = false
pureParticleMode = false
purePbfMode = true
navSeaPbfMode = false
timelineState = "NOT READY"
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "PURE_PBF")
}
private fun isPbfMatrixMode(): Boolean = pureParticleMode || purePbfMode || navSeaPbfMode
private fun hasProductionPbfStyle(): Boolean = purePbfMode || navSeaPbfMode
private fun usesRealWeatherAssets(): Boolean = realWeatherMode || pureParticleMode || navSeaPbfMode
private fun setNavSeaPbfRenderEnabled(enabled: Boolean) {
if (!hasProductionPbfStyle() || !styleReady) return
if (navSeaPbfRenderEnabled == enabled) return
navSeaPbfRenderEnabled = enabled
applyNavSeaPbfRenderState()
status.text = statusText()
Log.i(TAG, "NAVSEA_PBF_RENDER enabled=$enabled affectedLayers=${navSeaPbfLayerVisibility.size}")
}
private fun applyNavSeaPbfRenderState() {
navSeaPbfLayerVisibility.forEach { layer ->
maplibreMap.setStyleLayerVisibility(
layer.id,
navSeaPbfRenderEnabled && layer.originallyVisible
)
}
}
private fun showMenu(anchor: android.view.View, title: String, actions: IntArray) {
PopupMenu(this, anchor).apply {
actions.forEachIndexed { index, id -> menu.add(0, index, index, actionLabel(id)) }
@@ -383,7 +680,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
val commandAccepted = when {
accepted == WeatherRuntime.CommandResult.ACCEPTED -> true
realWeatherMode -> false
usesRealWeatherAssets() -> false
else -> maplibreMap.controlRasterArraySourceTimeline(PARTICLE_SOURCE_ID, command, value)
}
if (!commandAccepted) {
@@ -403,10 +700,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
R.id.validationProductionInvalid -> "INVALID PROD"
R.id.validationHostFieldDuplicate -> "HOST DUPLICATE"
R.id.validationHostFieldInvalid -> "HOST INVALID"
R.id.validationParticle1048576 -> "1M"
R.id.validationParticle262144 -> "256K"
R.id.validationParticle65536 -> "64K"
R.id.validationParticle16384 -> "16K"
R.id.validationParticle10000 -> "10K"
R.id.validationParticle4096 -> "4K"
R.id.validationParticle1024 -> "1K"
R.id.validationParticle256 -> "256"
@@ -440,6 +734,18 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") {
if (pureParticleMode) {
loadPureParticleStyle(active, enabled, reason)
return
}
if (purePbfMode) {
loadPurePbfStyle(reason)
return
}
if (navSeaPbfMode) {
loadNavSeaPbfStyle(active, enabled, reason)
return
}
weatherRuntime?.dispose()
weatherRuntime = null
this.active = active
@@ -463,6 +769,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
status.text = statusText()
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
if (openRealWeatherTuningAfterStyle && realWeatherMode) {
openRealWeatherTuningAfterStyle = false
realWeatherMenuAnchor?.let { showRealWeatherTuning(it) }
@@ -470,6 +777,276 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
}
private data class NavSeaPbfStyle(
val json: String,
val productionLayerCount: Int,
val seaAreaFill: Boolean,
val clipOutline: Boolean,
val belowCount: Int,
val aboveCount: Int,
val anchorLayer: String,
val pbfLayerVisibility: List<NavSeaPbfLayerVisibility>
)
private data class NavSeaPbfLayerVisibility(val id: String, val originallyVisible: Boolean)
private data class NavSeaProductionStyle(
val root: JSONObject,
val layers: JSONArray,
val layerIds: List<String>,
val seaIndex: Int,
val clipIndex: Int,
val specialPatternIndex: Int,
val hazardPointsIndex: Int,
val pbfLayerVisibility: List<NavSeaPbfLayerVisibility>
)
private fun loadPureParticleStyle(active: Boolean, enabled: Boolean, reason: String) {
weatherRuntime?.dispose()
weatherRuntime = null
this.active = active
this.enabled = enabled
styleReady = false
navSeaPbfLayerVisibility = emptyList()
val epoch = ++styleLoadEpoch
Log.i(TAG, "ParticleGPU: event=STYLE_LOAD epoch=$epoch reason=$reason")
maplibreMap.setStyle(Style.Builder().fromJson(pureParticleStyle(active, enabled))) {
if (pureParticleMode && epoch == styleLoadEpoch) {
styleReady = true
weatherRuntime = WeatherRuntime(
maplibreMap,
WeatherRuntime.Config(PARTICLE_SOURCE_ID, WEATHER_COLOR_LAYER_ID, PARTICLE_LAYER_ID)
)
weatherRuntime?.setTargetFps(realWeatherTargetFps)
setNavSeaPbfCamera(logNavSeaReady = false)
if (hostSubmissionMode) submitRealWeatherDataset()
status.text = statusText()
Log.i(
TAG,
"PURE_PARTICLE_READY camera=$NAVSEA_CAMERA_LATITUDE,$NAVSEA_CAMERA_LONGITUDE " +
"particleCount=$particleCount targetFps=${realWeatherTargetFps.name.removePrefix("FPS_")}"
)
}
}
}
private fun loadPurePbfStyle(reason: String) {
weatherRuntime?.dispose()
weatherRuntime = null
styleReady = false
navSeaPbfLayerVisibility = emptyList()
val epoch = ++styleLoadEpoch
Log.i(TAG, "ParticleGPU: event=STYLE_LOAD epoch=$epoch reason=$reason")
maplibreMap.setStyle(Style.Builder().fromUri(NAVSEA_PBF_STYLE_URL)) { productionStyle ->
if (purePbfMode && epoch == styleLoadEpoch) {
val production = try {
inspectNavSeaProductionStyle(productionStyle.json)
} catch (error: Exception) {
Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=PURE_PBF_CONTRACT", error)
null
}
if (production != null) {
styleReady = true
navSeaPbfLayerVisibility = production.pbfLayerVisibility
applyNavSeaPbfRenderState()
setNavSeaPbfCamera(logNavSeaReady = false)
status.text = statusText()
Log.i(
TAG,
"PURE_PBF_READY layerCount=${production.layers.length()} seaAreaFill=${production.seaIndex >= 0} " +
"clipOutline=${production.clipIndex >= 0} camera=$NAVSEA_CAMERA_LATITUDE,$NAVSEA_CAMERA_LONGITUDE"
)
}
}
}
}
private fun loadNavSeaPbfStyle(active: Boolean, enabled: Boolean, reason: String) {
weatherRuntime?.dispose()
weatherRuntime = null
this.active = active
this.enabled = enabled
styleReady = false
navSeaPbfLayerVisibility = emptyList()
val epoch = ++styleLoadEpoch
status.text = statusText()
Log.i(TAG, "ParticleGPU: event=STYLE_LOAD epoch=$epoch reason=$reason")
Log.i(TAG, "NAVSEA_PBF_STYLE_LOAD url=$NAVSEA_PBF_STYLE_URL position=${navSeaPbfPosition.name}")
maplibreMap.setStyle(Style.Builder().fromUri(NAVSEA_PBF_STYLE_URL)) { productionStyle ->
if (navSeaPbfMode && epoch == styleLoadEpoch) {
val augmented = try {
buildNavSeaPbfStyle(productionStyle.json, active, enabled, navSeaPbfPosition)
} catch (error: Exception) {
Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=AUGMENT_FAILED position=${navSeaPbfPosition.name}", error)
null
}
if (augmented != null) {
maplibreMap.setStyle(Style.Builder().fromJson(augmented.json)) {
if (navSeaPbfMode && epoch == styleLoadEpoch) {
styleReady = true
navSeaPbfLayerVisibility = augmented.pbfLayerVisibility
applyNavSeaPbfRenderState()
weatherRuntime = WeatherRuntime(
maplibreMap,
WeatherRuntime.Config(PARTICLE_SOURCE_ID, WEATHER_COLOR_LAYER_ID, PARTICLE_LAYER_ID)
)
weatherRuntime?.setTargetFps(realWeatherTargetFps)
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
Log.i(
TAG,
"NAVSEA_PBF_STYLE_READY layerCount=${augmented.productionLayerCount} " +
"seaAreaFill=${augmented.seaAreaFill} clipOutline=${augmented.clipOutline}"
)
Log.i(
TAG,
"NAVSEA_PBF_POSITION position=${navSeaPbfPosition.name} belowCount=${augmented.belowCount} " +
"aboveCount=${augmented.aboveCount} anchorLayer=${augmented.anchorLayer}"
)
setNavSeaPbfCamera()
if (hostSubmissionMode) submitRealWeatherDataset()
status.text = statusText()
}
}
}
}
}
}
private fun buildNavSeaPbfStyle(
productionJson: String,
active: Boolean,
enabled: Boolean,
position: NavSeaPbfPosition
): NavSeaPbfStyle {
val production = inspectNavSeaProductionStyle(productionJson)
val root = production.root
val sources = root.getJSONObject("sources")
check(!sources.has(PARTICLE_SOURCE_ID)) { "production style already contains $PARTICLE_SOURCE_ID" }
sources.put(PARTICLE_SOURCE_ID, JSONObject().put("type", "raster-array").put("host-field-submission", true))
val productionLayers = production.layers
val layerIds = production.layerIds
val seaIndex = production.seaIndex
val clipIndex = production.clipIndex
val specialPatternIndex = production.specialPatternIndex
val hazardPointsIndex = production.hazardPointsIndex
val insertionIndex = when (position) {
NavSeaPbfPosition.BOTTOM -> 0
NavSeaPbfPosition.AFTER_SEA -> seaIndex + 1
NavSeaPbfPosition.AFTER_CLIP -> clipIndex + 1
NavSeaPbfPosition.BEFORE_SYMBOL_CLUSTER -> hazardPointsIndex
NavSeaPbfPosition.TOP -> productionLayers.length()
}
val anchorLayer = when (position) {
NavSeaPbfPosition.BOTTOM -> layerIds.first()
NavSeaPbfPosition.AFTER_SEA -> NAVSEA_SEA_AREA_LAYER_ID
NavSeaPbfPosition.AFTER_CLIP -> NAVSEA_CLIP_OUTLINE_LAYER_ID
NavSeaPbfPosition.BEFORE_SYMBOL_CLUSTER -> NAVSEA_SPECIAL_PATTERN_LAYER_ID
NavSeaPbfPosition.TOP -> layerIds.last()
}
val augmentedLayers = JSONArray()
for (index in 0..productionLayers.length()) {
if (enabled && index == insertionIndex) augmentedLayers.put(navSeaParticleLayer(active))
if (index < productionLayers.length()) augmentedLayers.put(productionLayers.get(index))
}
root.put("layers", augmentedLayers)
return NavSeaPbfStyle(
json = root.toString(),
productionLayerCount = productionLayers.length(),
seaAreaFill = seaIndex >= 0,
clipOutline = clipIndex >= 0,
belowCount = insertionIndex,
aboveCount = productionLayers.length() - insertionIndex,
anchorLayer = anchorLayer,
pbfLayerVisibility = production.pbfLayerVisibility
)
}
private fun inspectNavSeaProductionStyle(productionJson: String): NavSeaProductionStyle {
val root = JSONObject(productionJson)
val layers = root.getJSONArray("layers")
val layerIds = (0 until layers.length()).map { layers.getJSONObject(it).getString("id") }
val pbfLayerVisibility = (0 until layers.length()).mapNotNull { index ->
val layer = layers.getJSONObject(index)
if (layer.optString("source") != NAVSEA_PBF_SOURCE_ID) null else NavSeaPbfLayerVisibility(
id = layer.getString("id"),
originallyVisible = layer.optJSONObject("layout")?.optString("visibility", "visible") != "none"
)
}
val result = NavSeaProductionStyle(
root = root,
layers = layers,
layerIds = layerIds,
seaIndex = layerIds.indexOf(NAVSEA_SEA_AREA_LAYER_ID),
clipIndex = layerIds.indexOf(NAVSEA_CLIP_OUTLINE_LAYER_ID),
specialPatternIndex = layerIds.indexOf(NAVSEA_SPECIAL_PATTERN_LAYER_ID),
hazardPointsIndex = layerIds.indexOf(NAVSEA_HAZARD_POINTS_LAYER_ID),
pbfLayerVisibility = pbfLayerVisibility
)
check(
layers.length() == NAVSEA_PRODUCTION_LAYER_COUNT && result.seaIndex == 3 && result.clipIndex == 31 &&
result.specialPatternIndex == 37 && result.hazardPointsIndex == 38 && pbfLayerVisibility.isNotEmpty()
) {
"production style contract changed: count=${layers.length()} sea=${result.seaIndex} clip=${result.clipIndex} " +
"specialPattern=${result.specialPatternIndex} hazardPoints=${result.hazardPointsIndex} " +
"pbfLayers=${pbfLayerVisibility.size}"
}
return result
}
private fun pureParticleStyle(active: Boolean, enabled: Boolean): String {
val particleLayer = if (enabled) navSeaParticleLayer(active) else null
return JSONObject()
.put("version", 8)
.put("name", "Pure Particle Validation")
.put(
"sources",
JSONObject().put(
PARTICLE_SOURCE_ID,
JSONObject().put("type", "raster-array").put("host-field-submission", true)
)
)
.put("layers", JSONArray().apply { if (particleLayer != null) put(particleLayer) })
.toString()
}
private fun navSeaParticleLayer(active: Boolean): JSONObject = JSONObject()
.put("id", PARTICLE_LAYER_ID)
.put("type", "raster-particle")
.put("source", PARTICLE_SOURCE_ID)
.put(
"paint",
JSONObject()
.put("raster-particle-active", active)
.put("raster-particle-mask-mode", maskMode)
.put("raster-particle-time-mix", timeMix)
.put("raster-particle-particle-count", particleCount)
.put("raster-particle-line-width", lineWidth)
.put("raster-particle-particle-length", particleLength)
.put("raster-particle-particle-opacity", particleOpacity)
.put("raster-particle-trail-opacity", trailOpacity)
.put("raster-particle-trail-fade", trailFade)
.put("raster-particle-speed-color-enabled", speedColorEnabled)
)
private fun setNavSeaPbfCamera(logNavSeaReady: Boolean = true) {
maplibreMap.cameraPosition = CameraPosition.Builder()
.target(LatLng(NAVSEA_CAMERA_LATITUDE, NAVSEA_CAMERA_LONGITUDE))
.zoom(NAVSEA_CAMERA_ZOOM)
.bearing(NAVSEA_CAMERA_BEARING)
.tilt(NAVSEA_CAMERA_PITCH)
.build()
if (logNavSeaReady) {
Log.i(
TAG,
"NAVSEA_PBF_CAMERA_READY center=$NAVSEA_CAMERA_LATITUDE,$NAVSEA_CAMERA_LONGITUDE " +
"zoom=$NAVSEA_CAMERA_ZOOM bearing=$NAVSEA_CAMERA_BEARING pitch=$NAVSEA_CAMERA_PITCH"
)
}
}
private fun setValidationProperties(active: Boolean, maskMode: Int, fixtureMode: Int = this.fixtureMode) {
this.active = active
this.maskMode = maskMode
@@ -509,23 +1086,13 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
Log.i(TAG, "ParticleGPU: event=MIX_PROPERTY_ONLY value=$timeMix styleEpoch=$styleLoadEpoch")
}
private fun setParticleCount(requestedParticleCount: Int) {
if (!styleReady) {
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=STYLE_LOADING requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
return
}
if (!enabled) {
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=DISABLED requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
return
}
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REQUEST requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
if (!setParticleFloat("raster-particle-particle-count", requestedParticleCount.toFloat())) {
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=CORE_PROPERTY_REJECTED requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
return
}
private fun setParticleCount(requestedParticleCount: Int): Boolean {
if (requestedParticleCount !in MIN_PARTICLE_COUNT..MAX_PARTICLE_COUNT || !styleReady || !enabled) return false
if (!setParticleFloat("raster-particle-particle-count", requestedParticleCount.toFloat())) return false
particleCount = requestedParticleCount
status.text = statusText()
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_APPLIED requestedParticleCount=$requestedParticleCount effectiveParticleCount=$particleCount styleEpoch=$styleLoadEpoch")
Log.i(TAG, "PARTICLE_COUNT_CHANGED count=$particleCount")
return true
}
private fun setVisualFloat(property: String, requested: Float, apply: (Float) -> Unit) {
@@ -572,7 +1139,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
}
return runtimeResult == WeatherRuntime.CommandResult.ACCEPTED ||
(!realWeatherMode && runtimeResult == null && maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, property, value))
(!usesRealWeatherAssets() && runtimeResult == null && maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, property, value))
}
private fun resetVisualDefaults() {
@@ -647,12 +1214,16 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
}
timelineState = if (results.size == 4 && results.all { it.accepted() }) "READY" else "NOT READY"
fitRealWeatherBounds()
if (!navSeaPbfMode && !pureParticleMode) fitRealWeatherBounds()
status.text = statusText()
Log.i(TAG, "ParticleField: event=REAL_WEATHER_INITIAL_WINDOW_READY results=${results.joinToString(",") { it.name }} timeline=$timelineState")
if (navSeaPbfMode) {
Log.i(TAG, "NAVSEA_PBF_PARTICLE_READY source=$PARTICLE_SOURCE_ID weatherColor=false")
} else if (!pureParticleMode) {
Log.i(TAG, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY source=$PARTICLE_SOURCE_ID scale=0..30mps")
Log.i(TAG, "ParticleField: event=REAL_WEATHER_COMPOSITE_READY source=$PARTICLE_SOURCE_ID basemap=GSI failSoft=true color=weather-color particle=raster-particle")
}
}
private fun fitRealWeatherBounds() {
val bounds = LatLngBounds.Builder()
@@ -721,9 +1292,28 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} else {
"EAST"
}
val playback = if (timeWindowMode) " · ${if (realWeatherMode) "REAL WEATHER" else if (hostSubmissionMode) "HOST" else if (productionFeedMode) "PRODUCTION" else "FIXTURE"} · TL $timelineState · $timelineSpeed" else ""
val summary = "$mask · $runtime · $fields$playback · MIX ${(timeMix * 100).toInt()}%"
return if (realWeatherMode) "$summary\n${realWeatherHudValues()}" else "$summary · $particleCount · W ${"%.1f".format(lineWidth)} L ${"%.3f".format(particleLength)} O ${(particleOpacity * 100).toInt()} T ${(trailOpacity * 100).toInt()} F ${(trailFade * 100).toInt()} C ${if (speedColorEnabled) "ON" else "OFF"}"
val playbackMode = when {
pureParticleMode -> "PURE PARTICLE"
purePbfMode -> "PURE PBF"
navSeaPbfMode -> "NAVSEA PBF"
realWeatherMode -> "REAL WEATHER"
hostSubmissionMode -> "HOST"
productionFeedMode -> "PRODUCTION"
else -> "FIXTURE"
}
val playback = if (timeWindowMode) " · $playbackMode · TL $timelineState · $timelineSpeed" else ""
val scene = when {
navSeaPbfMode -> "NAVSEA PBF · ${navSeaPbfPosition.label} · PBF ${if (navSeaPbfRenderEnabled) "ON" else "OFF"} · CACHE ${if (retainedCacheEnabled) "ON" else "OFF"} · "
pureParticleMode -> "PURE PARTICLE · CACHE ${if (retainedCacheEnabled) "ON" else "OFF"} · "
purePbfMode -> "PURE PBF · PBF ${if (navSeaPbfRenderEnabled) "ON" else "OFF"} · CACHE ${if (retainedCacheEnabled) "ON" else "OFF"} · "
else -> ""
}
val summary = "$scene$mask · $runtime · $fields$playback · MIX ${(timeMix * 100).toInt()}%"
return when {
purePbfMode -> summary
usesRealWeatherAssets() -> "$summary\n${realWeatherHudValues()}"
else -> "$summary · $particleCount · W ${"%.1f".format(lineWidth)} L ${"%.3f".format(particleLength)} O ${(particleOpacity * 100).toInt()} T ${(trailOpacity * 100).toInt()} F ${(trailFade * 100).toInt()} C ${if (speedColorEnabled) "ON" else "OFF"}"
}
}
private fun realWeatherHudValues(): String = "FPS ${realWeatherTargetFps.name.removePrefix("FPS_")} · P $particleCount · W ${"%.2f".format(lineWidth)} · L ${"%.4f".format(particleLength)} · O ${"%.2f".format(particleOpacity)} · T ${"%.2f".format(trailOpacity)} · F ${"%.2f".format(trailFade)}"
@@ -829,6 +1419,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
outState.putBoolean(STATE_TIME_WINDOW, timeWindowMode)
outState.putBoolean(STATE_HOST_SUBMISSION, hostSubmissionMode)
outState.putBoolean(STATE_REAL_WEATHER, realWeatherMode)
outState.putBoolean(STATE_PURE_PARTICLE, pureParticleMode)
outState.putBoolean(STATE_PURE_PBF, purePbfMode)
outState.putBoolean(STATE_NAVSEA_PBF, navSeaPbfMode)
outState.putString(STATE_NAVSEA_PBF_POSITION, navSeaPbfPosition.name)
outState.putBoolean(STATE_NAVSEA_PBF_RENDER_ENABLED, navSeaPbfRenderEnabled)
outState.putBoolean(STATE_RETAINED_CACHE_ENABLED, retainedCacheEnabled)
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
@@ -856,6 +1452,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private const val STATE_TIME_WINDOW = "particle_validation.time_window"
private const val STATE_HOST_SUBMISSION = "particle_validation.host_submission"
private const val STATE_REAL_WEATHER = "particle_validation.real_weather"
private const val STATE_PURE_PARTICLE = "particle_validation.pure_particle"
private const val STATE_PURE_PBF = "particle_validation.pure_pbf"
private const val STATE_NAVSEA_PBF = "particle_validation.navsea_pbf"
private const val STATE_NAVSEA_PBF_POSITION = "particle_validation.navsea_pbf_position"
private const val STATE_NAVSEA_PBF_RENDER_ENABLED = "particle_validation.navsea_pbf_render_enabled"
private const val STATE_RETAINED_CACHE_ENABLED = "particle_validation.retained_cache_enabled"
private const val PARTICLE_LAYER_ID = "particle-raster-core"
private const val WEATHER_COLOR_LAYER_ID = "weather-color-real"
private const val PARTICLE_SOURCE_ID = "particle-fixture"
@@ -871,10 +1473,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private const val PARTICLE_COUNT_2048 = 2048
private const val PARTICLE_COUNT_4096 = 4096
private const val PARTICLE_COUNT_8192 = 8192
private const val PARTICLE_COUNT_16384 = 16384
private const val PARTICLE_COUNT_65536 = 65536
private const val PARTICLE_COUNT_262144 = 262144
private const val PARTICLE_COUNT_1048576 = 1048576
private const val MIN_PARTICLE_COUNT = 1
private const val MAX_PARTICLE_COUNT = 10000
private const val DEFAULT_LINE_WIDTH = 2.0f
private const val DEFAULT_PARTICLE_LENGTH = 0.018f
private const val DEFAULT_PARTICLE_OPACITY = 1.0f
@@ -886,6 +1486,18 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private const val REAL_WEATHER_PARTICLE_OPACITY = 0.75f
private const val REAL_WEATHER_TRAIL_OPACITY = 0.55f
private const val REAL_WEATHER_TRAIL_FADE = 0.88f
private val REAL_WEATHER_PARTICLE_COUNTS = intArrayOf(PARTICLE_COUNT_256, PARTICLE_COUNT_512, PARTICLE_COUNT_1024, PARTICLE_COUNT_2048, PARTICLE_COUNT_4096, PARTICLE_COUNT_8192, PARTICLE_COUNT_16384)
private const val NAVSEA_PBF_STYLE_URL = "https://pbf.jpcn.ccwu.cc/style/style.navsea-delivery-full-semantic-extentfix.json"
private const val NAVSEA_PBF_SOURCE_ID = "navsea_delivery"
private const val NAVSEA_PRODUCTION_LAYER_COUNT = 63
private const val NAVSEA_SEA_AREA_LAYER_ID = "sea-area-fill"
private const val NAVSEA_CLIP_OUTLINE_LAYER_ID = "clip-outline"
private const val NAVSEA_SPECIAL_PATTERN_LAYER_ID = "special-pattern-line-754"
private const val NAVSEA_HAZARD_POINTS_LAYER_ID = "hazard-points"
private const val NAVSEA_CAMERA_LATITUDE = 33.45
private const val NAVSEA_CAMERA_LONGITUDE = 129.97
private const val NAVSEA_CAMERA_ZOOM = 10.5
private const val NAVSEA_CAMERA_BEARING = 0.0
private const val NAVSEA_CAMERA_PITCH = 0.0
private val REAL_WEATHER_PARTICLE_COUNTS = intArrayOf(PARTICLE_COUNT_256, PARTICLE_COUNT_512, PARTICLE_COUNT_1024, PARTICLE_COUNT_2048, PARTICLE_COUNT_4096, PARTICLE_COUNT_8192, MAX_PARTICLE_COUNT)
}
}

View File

@@ -9,13 +9,16 @@
<Button android:id="@+id/validationMenuCount" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="COUNT" />
<Button android:id="@+id/validationMenuMap" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="MAP" />
<Button android:id="@+id/validationMenuRealWeather" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="REAL WEATHER" android:textSize="10sp" />
<Button android:id="@+id/validationMenuPbfMode" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="PBF MODE" android:textSize="9sp" />
<Button android:id="@+id/validationMenuNavSeaPbf" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="NAVSEA PBF" android:textSize="9sp" />
<Button android:id="@+id/validationMenuRetainedCache" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="CACHE ON" android:textSize="9sp" />
<Button android:id="@+id/validationMenuMore" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="MORE" />
</LinearLayout>
<!-- Existing action IDs are invisible menu targets; no permanent button grid. -->
<LinearLayout android:visibility="gone" android:layout_width="1dp" android:layout_height="1dp" android:orientation="vertical">
<Button style="@style/ParticleValidationButton" android:id="@+id/validationActive"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationStatic"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDisable"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationEnable"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationGlobal"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationOceanA"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationOceanB"/>
<Button style="@style/ParticleValidationButton" android:id="@+id/validationUniformEast"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationUniformNorth"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVortex"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationTimeWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix0"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix50"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix100"/>
<Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle256"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle1024"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle4096"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle16384"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle65536"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle262144"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle1048576"/>
<Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle256"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle1024"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle4096"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle10000"/>
<Button style="@style/ParticleValidationButton" android:id="@+id/validationAdvanceWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDuplicateWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationProductionInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldSubmit"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationRealWeather"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldDuplicate"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualDefault"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualColor"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDateline"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationWorldWrap"/>
</LinearLayout>
</LinearLayout>

View File

@@ -28,6 +28,21 @@ NS_ASSUME_NONNULL_BEGIN
@protocol MLNFeature;
@protocol MLNLocationManager;
/** A pull-based snapshot of one map view's retained-underlay renderer state. */
MLN_EXPORT
@interface MLNRetainedUnderlayDiagnostics : NSObject
@property (nonatomic, readonly, getter=isEnabled) BOOL enabled;
@property (nonatomic, readonly) uint64_t build;
@property (nonatomic, readonly) uint64_t reuse;
@property (nonatomic, readonly) uint64_t fallback;
@property (nonatomic, readonly) uint64_t invalidation;
@property (nonatomic, copy, readonly) NSString *lastReason;
- (instancetype)init NS_UNAVAILABLE;
@end
/** Options for ``MLNMapView/decelerationRate``. */
typedef CGFloat MLNMapViewDecelerationRate NS_TYPED_EXTENSIBLE_ENUM;
@@ -2266,6 +2281,12 @@ of north, the map will automatically snap to exact north.
*/
@property (nonatomic) MLNMapDebugMaskOptions debugMask;
/** Enables or disables the map-scoped retained static underlay optimization. */
@property (nonatomic, getter=isRetainedUnderlayEnabled) BOOL retainedUnderlayEnabled;
/** Returns a thread-safe retained-underlay diagnostics snapshot without enabling periodic events. */
@property (nonatomic, readonly) MLNRetainedUnderlayDiagnostics *retainedUnderlayDiagnostics;
/**
Returns the status of the rendering statistics overlay.
*/

View File

@@ -239,6 +239,34 @@ int processIsTranslated() {
#endif
@interface MLNRetainedUnderlayDiagnostics ()
- (instancetype)initWithEnabled:(BOOL)enabled
build:(uint64_t)build
reuse:(uint64_t)reuse
fallback:(uint64_t)fallback
invalidation:(uint64_t)invalidation
lastReason:(NSString *)lastReason;
@end
@implementation MLNRetainedUnderlayDiagnostics
- (instancetype)initWithEnabled:(BOOL)enabled
build:(uint64_t)build
reuse:(uint64_t)reuse
fallback:(uint64_t)fallback
invalidation:(uint64_t)invalidation
lastReason:(NSString *)lastReason {
if ((self = [super init])) {
_enabled = enabled;
_build = build;
_reuse = reuse;
_fallback = fallback;
_invalidation = invalidation;
_lastReason = [lastReason copy];
}
return self;
}
@end
class MLNAnnotationContext;
const MLNMapViewDecelerationRate MLNMapViewDecelerationRateNormal =
@@ -3070,6 +3098,32 @@ static void *windowScreenContext = &windowScreenContext;
self.mbglMap.setDebug(options);
}
- (BOOL)isRetainedUnderlayEnabled {
return _mbglMap ? self.mbglMap.isRetainedUnderlayEnabled() : YES;
}
- (void)setRetainedUnderlayEnabled:(BOOL)enabled {
if (_mbglMap) self.mbglMap.setRetainedUnderlayEnabled(enabled);
}
- (MLNRetainedUnderlayDiagnostics *)retainedUnderlayDiagnostics {
if (!_mbglMap) {
return [[MLNRetainedUnderlayDiagnostics alloc] initWithEnabled:YES
build:0
reuse:0
fallback:0
invalidation:0
lastReason:@"unavailable"];
}
const auto snapshot = self.mbglMap.getRetainedUnderlayDiagnostics();
return [[MLNRetainedUnderlayDiagnostics alloc] initWithEnabled:snapshot.enabled
build:snapshot.build
reuse:snapshot.reuse
fallback:snapshot.fallback
invalidation:snapshot.invalidation
lastReason:[NSString stringWithUTF8String:snapshot.lastReason.c_str()]];
}
- (void)resetNorth {
MLNLogInfo(@"Resetting the map rotation to a northern heading — a direction of 0 degrees.");
[self resetNorthAnimated:YES];

View File

@@ -94,6 +94,20 @@ void Map::triggerRepaint() {
impl->onUpdate();
}
void Map::setRetainedUnderlayEnabled(bool enabled) {
if (impl->retainedUnderlayState->setEnabled(enabled)) {
impl->onUpdate();
}
}
bool Map::isRetainedUnderlayEnabled() const {
return impl->retainedUnderlayState->isEnabled();
}
RetainedUnderlayDiagnostics Map::getRetainedUnderlayDiagnostics() const {
return impl->retainedUnderlayState->snapshot();
}
// MARK: - Style
style::Style& Map::getStyle() {

View File

@@ -134,6 +134,7 @@ void Map::Impl::onUpdate() {
.stillImageRequest = bool(stillImageRequest),
.crossSourceCollisions = crossSourceCollisions,
.fastPFOREnabled = fastPFOREnabled,
.retainedUnderlayState = retainedUnderlayState,
.tileLodMinRadius = tileLodMinRadius,
.tileLodScale = tileLodScale,
.tileLodPitchThreshold = tileLodPitchThreshold,

View File

@@ -13,6 +13,7 @@
#include <mbgl/style/style.hpp>
#include <mbgl/util/size.hpp>
#include <mbgl/tile/tile_operation.hpp>
#include <mbgl/renderer/retained_underlay_runtime_state.hpp>
#include <numbers>
@@ -100,6 +101,8 @@ public:
std::unique_ptr<style::Style> style;
AnnotationManager annotationManager;
std::shared_ptr<RetainedUnderlayRuntimeState> retainedUnderlayState =
std::make_shared<RetainedUnderlayRuntimeState>();
bool cameraMutated = false;

View File

@@ -35,6 +35,7 @@ void Renderer::setObserver(RendererObserver* observer) {
void Renderer::render(const std::shared_ptr<UpdateParameters>& updateParameters) {
MLN_TRACE_FUNC();
assert(updateParameters);
impl->synchronizeRetainedUnderlayState(updateParameters->retainedUnderlayState);
const bool styleChanged = impl->styleLoaded && !updateParameters->styleLoaded;
impl->styleLoaded = updateParameters->styleLoaded;
if (!impl->dynamicTextureAtlas || styleChanged) {

View File

@@ -59,23 +59,25 @@ RendererObserver& nullObserver() {
constexpr uint64_t RetainedCounterLogIntervalFrames = 120;
void logRetainedRendererCounters(const RetainedUnderlayCache& cache) {
const auto snapshot = particle::rendererDebugCounterSnapshot();
if (snapshot.fullRendererFrame == 0 ||
snapshot.fullRendererFrame % RetainedCounterLogIntervalFrames != 0) {
const auto detailed = particle::rendererDebugCounterSnapshot();
const auto retained = cache.getDiagnostics();
if (detailed.fullRendererFrame == 0 ||
detailed.fullRendererFrame % RetainedCounterLogIntervalFrames != 0) {
return;
}
std::ostringstream message;
message << "ParticleRetained:"
<< " frame=" << snapshot.fullRendererFrame
<< " build=" << snapshot.retainedUnderlayBuild
<< " reuse=" << snapshot.retainedUnderlayReuse
<< " fallback=" << snapshot.retainedUnderlayFallback
<< " invalidation=" << snapshot.retainedUnderlayInvalidation
<< " reason=" << cache.getLastInvalidationReason()
<< " staticDraw=" << snapshot.retainedUnderlayStaticGroupDraw
<< " dynamicDraw=" << snapshot.retainedDynamicGroupDraw
<< " particleAdvance=" << snapshot.particleSimulationAdvance;
<< " frame=" << detailed.fullRendererFrame
<< " enabled=" << (retained.enabled ? "true" : "false")
<< " build=" << retained.build
<< " reuse=" << retained.reuse
<< " fallback=" << retained.fallback
<< " invalidation=" << retained.invalidation
<< " reason=" << retained.lastReason
<< " staticDraw=" << detailed.retainedUnderlayStaticGroupDraw
<< " dynamicDraw=" << detailed.retainedDynamicGroupDraw
<< " particleAdvance=" << detailed.particleSimulationAdvance;
Log::Info(Event::Render, message.str());
}
#endif
@@ -121,6 +123,11 @@ void Renderer::Impl::setObserver(RendererObserver* observer_) {
observer = observer_ ? observer_ : &nullObserver();
}
void Renderer::Impl::synchronizeRetainedUnderlayState(
const std::shared_ptr<RetainedUnderlayRuntimeState>& state) {
retainedUnderlayCache->synchronizeRuntimeState(state);
}
void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<UpdateParameters>& updateParameters) {
MLN_TRACE_FUNC();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FullRendererFrame);

View File

@@ -17,6 +17,7 @@ class RendererObserver;
class RenderStaticData;
class RenderTree;
class RetainedUnderlayCache;
class RetainedUnderlayRuntimeState;
namespace gfx {
class RendererBackend;
@@ -42,6 +43,7 @@ private:
void setObserver(RendererObserver*);
void render(const RenderTree&, const std::shared_ptr<UpdateParameters>&);
void synchronizeRetainedUnderlayState(const std::shared_ptr<RetainedUnderlayRuntimeState>&);
void reduceMemoryUse();

View File

@@ -16,21 +16,71 @@
#include <mbgl/renderer/render_static_data.hpp>
#include <mbgl/shaders/shader_defines.hpp>
#include <string_view>
namespace mbgl {
namespace {
constexpr auto CompositeShaderName = "RetainedUnderlayCompositeShader";
RetainedUnderlayReason retainedReason(const char* reason) noexcept {
if (!reason) return RetainedUnderlayReason::Unknown;
const std::string_view value(reason);
if (value == "runtime-disabled") return RetainedUnderlayReason::RuntimeDisabled;
if (value == "first-frame") return RetainedUnderlayReason::FirstFrame;
if (value == "viewport") return RetainedUnderlayReason::Viewport;
if (value == "camera-or-boundary") return RetainedUnderlayReason::CameraOrBoundary;
if (value == "no-boundary") return RetainedUnderlayReason::NoBoundary;
if (value == "empty-static-prefix") return RetainedUnderlayReason::EmptyStaticPrefix;
if (value == "style-not-loaded") return RetainedUnderlayReason::StyleNotLoaded;
if (value == "3d") return RetainedUnderlayReason::ThreeDimensional;
if (value == "debug-options") return RetainedUnderlayReason::DebugOptions;
if (value == "static-transition") return RetainedUnderlayReason::StaticTransition;
if (value == "static-placement") return RetainedUnderlayReason::StaticPlacement;
if (value == "legacy-ordering") return RetainedUnderlayReason::LegacyOrdering;
if (value == "resource-or-compositor") return RetainedUnderlayReason::ResourceOrCompositor;
if (value == "unsupported") return RetainedUnderlayReason::Unsupported;
return RetainedUnderlayReason::Unknown;
}
}
RetainedUnderlayCache::RetainedUnderlayCache() = default;
RetainedUnderlayCache::RetainedUnderlayCache()
: runtimeState(std::make_shared<RetainedUnderlayRuntimeState>()) {}
RetainedUnderlayCache::~RetainedUnderlayCache() = default;
void RetainedUnderlayCache::synchronizeRuntimeState(
const std::shared_ptr<RetainedUnderlayRuntimeState>& nextState) {
if (nextState && nextState != runtimeState) {
releaseResources();
runtimeState = nextState;
observedControlRevision = runtimeState->getControlRevision();
}
if (!runtimeState) return;
const auto revision = runtimeState->getControlRevision();
if (revision == observedControlRevision) return;
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().
releaseResources();
if (!runtimeState->isEnabled()) {
lastInvalidationReason = "runtime-disabled";
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayInvalidation);
}
}
RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<RetainedUnderlayKey>& nextKey,
bool supported,
const char* fallbackReason) {
if (!nextKey) return RetainedUnderlayDecision::Disabled;
if (runtimeState && !runtimeState->isEnabled()) {
++fallbackCount;
runtimeState->recordFallback();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
return RetainedUnderlayDecision::Fallback;
}
if (!supported) {
++fallbackCount;
runtimeState->recordFallback();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
invalidate(fallbackReason ? fallbackReason : "unsupported");
return RetainedUnderlayDecision::Fallback;
@@ -42,6 +92,7 @@ RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<Ret
}
if (valid) {
++reuseCount;
runtimeState->recordReuse();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayReuse);
return RetainedUnderlayDecision::Reuse;
}
@@ -131,11 +182,13 @@ void RetainedUnderlayCache::composite(RenderOrchestrator& orchestrator, PaintPar
void RetainedUnderlayCache::markBuildSucceeded() {
valid = true;
++buildCount;
runtimeState->recordBuild();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayBuild);
}
void RetainedUnderlayCache::markBuildFailed(const char* reason) {
++fallbackCount;
runtimeState->recordFallback();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
invalidate(reason);
}
@@ -143,7 +196,15 @@ void RetainedUnderlayCache::markBuildFailed(const char* reason) {
void RetainedUnderlayCache::invalidate(const char* reason) {
valid = false;
lastInvalidationReason = reason ? reason : "unknown";
runtimeState->recordInvalidation(retainedReason(reason));
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayInvalidation);
}
void RetainedUnderlayCache::releaseResources() {
valid = false;
key.reset();
compositeLayerGroup.reset();
offscreenTexture.reset();
}
} // namespace mbgl

View File

@@ -2,6 +2,7 @@
#include <mbgl/util/mat4.hpp>
#include <mbgl/util/size.hpp>
#include <mbgl/renderer/retained_underlay_runtime_state.hpp>
#include <cstdint>
#include <memory>
@@ -54,6 +55,7 @@ public:
RetainedUnderlayDecision evaluate(const std::optional<RetainedUnderlayKey>&, bool supported,
const char* fallbackReason = nullptr);
void synchronizeRuntimeState(const std::shared_ptr<RetainedUnderlayRuntimeState>&);
bool prepareResources(gfx::Context&, gfx::ShaderRegistry&, const RetainedUnderlayKey&);
void upload(gfx::UploadPass&);
void beginBuild(PaintParameters&);
@@ -68,9 +70,11 @@ public:
uint64_t getReuseCount() const noexcept { return reuseCount; }
uint64_t getFallbackCount() const noexcept { return fallbackCount; }
const std::string& getLastInvalidationReason() const noexcept { return lastInvalidationReason; }
RetainedUnderlayDiagnostics getDiagnostics() const { return runtimeState->snapshot(); }
private:
bool rebuildComposite(gfx::Context&, gfx::ShaderRegistry&);
void releaseResources();
std::optional<RetainedUnderlayKey> key;
std::unique_ptr<gfx::OffscreenTexture> offscreenTexture;
@@ -80,6 +84,8 @@ private:
uint64_t reuseCount = 0;
uint64_t fallbackCount = 0;
std::string lastInvalidationReason;
std::shared_ptr<RetainedUnderlayRuntimeState> runtimeState;
uint64_t observedControlRevision = 0;
};
} // namespace mbgl

View File

@@ -0,0 +1,94 @@
#pragma once
#include <mbgl/renderer/retained_underlay.hpp>
#include <atomic>
namespace mbgl {
enum class RetainedUnderlayReason : uint8_t {
None,
RuntimeDisabled,
FirstFrame,
Viewport,
CameraOrBoundary,
NoBoundary,
EmptyStaticPrefix,
StyleNotLoaded,
ThreeDimensional,
DebugOptions,
StaticTransition,
StaticPlacement,
LegacyOrdering,
ResourceOrCompositor,
Unsupported,
Unknown,
};
inline const char* retainedUnderlayReasonText(RetainedUnderlayReason reason) noexcept {
switch (reason) {
case RetainedUnderlayReason::None: return "none";
case RetainedUnderlayReason::RuntimeDisabled: return "runtime-disabled";
case RetainedUnderlayReason::FirstFrame: return "first-frame";
case RetainedUnderlayReason::Viewport: return "viewport";
case RetainedUnderlayReason::CameraOrBoundary: return "camera-or-boundary";
case RetainedUnderlayReason::NoBoundary: return "no-boundary";
case RetainedUnderlayReason::EmptyStaticPrefix: return "empty-static-prefix";
case RetainedUnderlayReason::StyleNotLoaded: return "style-not-loaded";
case RetainedUnderlayReason::ThreeDimensional: return "3d";
case RetainedUnderlayReason::DebugOptions: return "debug-options";
case RetainedUnderlayReason::StaticTransition: return "static-transition";
case RetainedUnderlayReason::StaticPlacement: return "static-placement";
case RetainedUnderlayReason::LegacyOrdering: return "legacy-ordering";
case RetainedUnderlayReason::ResourceOrCompositor: return "resource-or-compositor";
case RetainedUnderlayReason::Unsupported: return "unsupported";
case RetainedUnderlayReason::Unknown: return "unknown";
}
return "unknown";
}
class RetainedUnderlayRuntimeState {
public:
bool setEnabled(bool value) noexcept {
const bool previous = enabled.exchange(value, std::memory_order_acq_rel);
if (previous == value) return false;
controlRevision.fetch_add(1, std::memory_order_release);
if (!value) {
invalidation.fetch_add(1, std::memory_order_relaxed);
lastReason.store(RetainedUnderlayReason::RuntimeDisabled, std::memory_order_release);
}
return true;
}
bool isEnabled() const noexcept { return enabled.load(std::memory_order_acquire); }
uint64_t getControlRevision() const noexcept { return controlRevision.load(std::memory_order_acquire); }
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)),
};
}
void recordBuild() noexcept { build.fetch_add(1, std::memory_order_relaxed); }
void recordReuse() noexcept { reuse.fetch_add(1, std::memory_order_relaxed); }
void recordFallback() noexcept { fallback.fetch_add(1, std::memory_order_relaxed); }
void recordInvalidation(RetainedUnderlayReason reason) noexcept {
lastReason.store(reason, std::memory_order_release);
invalidation.fetch_add(1, std::memory_order_relaxed);
}
private:
std::atomic<bool> enabled{true};
std::atomic<uint64_t> controlRevision{0};
std::atomic<uint64_t> build{0};
std::atomic<uint64_t> reuse{0};
std::atomic<uint64_t> fallback{0};
std::atomic<uint64_t> invalidation{0};
std::atomic<RetainedUnderlayReason> lastReason{RetainedUnderlayReason::None};
};
} // namespace mbgl

View File

@@ -9,8 +9,10 @@
#include <mbgl/text/glyph.hpp>
#include <mbgl/util/chrono.hpp>
#include <mbgl/util/immutable.hpp>
#include <mbgl/renderer/retained_underlay_runtime_state.hpp>
#include <numbers>
#include <memory>
#include <vector>
#include <mapbox/std/weak.hpp>
@@ -50,6 +52,8 @@ public:
const bool fastPFOREnabled = false;
std::shared_ptr<RetainedUnderlayRuntimeState> retainedUnderlayState;
double tileLodMinRadius = 3;
double tileLodScale = 1;
double tileLodPitchThreshold = (60.0 / 180.0) * std::numbers::pi;

View File

@@ -87,3 +87,29 @@ TEST(RetainedUnderlayCache, UnsupportedAndResourceFailureFallBackSafely) {
EXPECT_FALSE(cache.isValid());
EXPECT_EQ(cache.getFallbackCount(), 2u);
}
TEST(RetainedUnderlayCache, RuntimeControlDisablesAndReenablesMapScopedCache) {
auto state = std::make_shared<RetainedUnderlayRuntimeState>();
RetainedUnderlayCache cache;
cache.synchronizeRuntimeState(state);
EXPECT_EQ(cache.evaluate(key(), true), RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
EXPECT_TRUE(cache.isValid());
EXPECT_TRUE(state->setEnabled(false));
cache.synchronizeRuntimeState(state);
EXPECT_FALSE(cache.isValid());
EXPECT_EQ(cache.evaluate(key(), true), RetainedUnderlayDecision::Fallback);
auto disabled = state->snapshot();
EXPECT_FALSE(disabled.enabled);
EXPECT_EQ(disabled.build, 1u);
EXPECT_EQ(disabled.fallback, 1u);
EXPECT_EQ(disabled.invalidation, 2u); // first-frame plus the runtime disable.
EXPECT_EQ(disabled.lastReason, "runtime-disabled");
EXPECT_TRUE(state->setEnabled(true));
cache.synchronizeRuntimeState(state);
EXPECT_EQ(cache.evaluate(key(), true), RetainedUnderlayDecision::Build);
EXPECT_TRUE(state->snapshot().enabled);
}