feat: add retained underlay runtime controls
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
17
include/mbgl/renderer/retained_underlay.hpp
Normal file
17
include/mbgl/renderer/retained_underlay.hpp
Normal 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
|
||||
@@ -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}>
|
||||
|
||||
@@ -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() }
|
||||
|
||||
|
||||
@@ -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)); }];
|
||||
|
||||
@@ -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>;
|
||||
|
||||
18
packages/maplibre-particle-rn/src/RendererRuntime.ts
Normal file
18
packages/maplibre-particle-rn/src/RendererRuntime.ts
Normal 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;
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}>;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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&);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -286,6 +286,10 @@ interface NativeMap {
|
||||
|
||||
boolean getDebug();
|
||||
|
||||
void setRetainedUnderlayEnabled(boolean enabled);
|
||||
|
||||
RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics();
|
||||
|
||||
String[] getActionJournalLogFiles();
|
||||
|
||||
String[] getActionJournalLog();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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,11 +1214,15 @@ 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")
|
||||
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")
|
||||
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() {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -134,6 +134,7 @@ void Map::Impl::onUpdate() {
|
||||
.stillImageRequest = bool(stillImageRequest),
|
||||
.crossSourceCollisions = crossSourceCollisions,
|
||||
.fastPFOREnabled = fastPFOREnabled,
|
||||
.retainedUnderlayState = retainedUnderlayState,
|
||||
.tileLodMinRadius = tileLodMinRadius,
|
||||
.tileLodScale = tileLodScale,
|
||||
.tileLodPitchThreshold = tileLodPitchThreshold,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
94
src/mbgl/renderer/retained_underlay_runtime_state.hpp
Normal file
94
src/mbgl/renderer/retained_underlay_runtime_state.hpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user