feat: add STEP-093 runtime validation tooling

This commit is contained in:
tei
2026-09-24 14:39:39 +09:00
parent 8a5d88008c
commit b3ec8edc87
20 changed files with 765 additions and 24 deletions

View File

@@ -19,15 +19,33 @@ struct RetainedUnderlayDiagnostics {
std::string endLayerId;
int32_t startIndex = -1;
int32_t endIndex = -1;
int32_t continuousBegin = -1;
int32_t continuousEndExclusive = -1;
bool bottomPresent = false;
std::string bottomCacheState{"disabled"};
uint64_t bottomBuild = 0;
uint64_t bottomReuse = 0;
uint64_t bottomInvalidation = 0;
std::string bottomLastInvalidationReason{"none"};
uint64_t bottomTextureCreate = 0;
uint64_t bottomCompositeCreate = 0;
uint64_t bottomResourceRelease = 0;
bool topPresent = false;
std::string topCacheState{"disabled"};
uint64_t topBuild = 0;
uint64_t topReuse = 0;
uint64_t topInvalidation = 0;
std::string topLastInvalidationReason{"none"};
uint64_t topTextureCreate = 0;
uint64_t topCompositeCreate = 0;
uint64_t topResourceRelease = 0;
bool rangeFallback = false;
bool fastPathEligible = false;
bool fastPathActive = false;
std::string fastPathFallbackReason{"no-prepared-tree"};
uint64_t fastPathHit = 0;
uint64_t fastPathMiss = 0;
uint64_t fullCpuFrame = 0;
};
struct ContinuousLayerRangeConfig {

View File

@@ -364,6 +364,14 @@ export default function App() {
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>
{retainedSnapshot != null && (
<Text selectable>
{`Range ${retainedSnapshot.rangeValid ? "VALID" : "INVALID"} [${retainedSnapshot.startIndex},${retainedSnapshot.endIndex}] continuous=[${retainedSnapshot.continuousBegin},${retainedSnapshot.continuousEndExclusive})\n`}
{`Bottom ${retainedSnapshot.bottomPresent ? retainedSnapshot.bottomCacheState : "absent"} b/r/i=${retainedSnapshot.bottomBuild}/${retainedSnapshot.bottomReuse}/${retainedSnapshot.bottomInvalidation} tex/comp/rel=${retainedSnapshot.bottomTextureCreate}/${retainedSnapshot.bottomCompositeCreate}/${retainedSnapshot.bottomResourceRelease}\n`}
{`Top ${retainedSnapshot.topPresent ? retainedSnapshot.topCacheState : "absent"} b/r/i=${retainedSnapshot.topBuild}/${retainedSnapshot.topReuse}/${retainedSnapshot.topInvalidation} tex/comp/rel=${retainedSnapshot.topTextureCreate}/${retainedSnapshot.topCompositeCreate}/${retainedSnapshot.topResourceRelease}\n`}
{`Fast eligible=${retainedSnapshot.fastPathEligible} active=${retainedSnapshot.fastPathActive} hit/miss/full=${retainedSnapshot.fastPathHit}/${retainedSnapshot.fastPathMiss}/${retainedSnapshot.fullCpuFrame} reason=${retainedSnapshot.fastPathFallbackReason}`}
</Text>
)}
<View style={styles.row}>
<Button
title={`Retained ${retainedSnapshot?.enabled === false ? "OFF" : "ON"}`}

View File

@@ -85,15 +85,33 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
putString("endLayerId", value.endLayerId)
putInt("startIndex", value.startIndex)
putInt("endIndex", value.endIndex)
putInt("continuousBegin", value.continuousBegin)
putInt("continuousEndExclusive", value.continuousEndExclusive)
putBoolean("bottomPresent", value.bottomPresent)
putString("bottomCacheState", value.bottomCacheState)
putDouble("bottomBuild", value.bottomBuild.toDouble())
putDouble("bottomReuse", value.bottomReuse.toDouble())
putDouble("bottomInvalidation", value.bottomInvalidation.toDouble())
putString("bottomLastInvalidationReason", value.bottomLastInvalidationReason)
putDouble("bottomTextureCreate", value.bottomTextureCreate.toDouble())
putDouble("bottomCompositeCreate", value.bottomCompositeCreate.toDouble())
putDouble("bottomResourceRelease", value.bottomResourceRelease.toDouble())
putBoolean("topPresent", value.topPresent)
putString("topCacheState", value.topCacheState)
putDouble("topBuild", value.topBuild.toDouble())
putDouble("topReuse", value.topReuse.toDouble())
putDouble("topInvalidation", value.topInvalidation.toDouble())
putBoolean("fallback", value.fallback)
putString("topLastInvalidationReason", value.topLastInvalidationReason)
putDouble("topTextureCreate", value.topTextureCreate.toDouble())
putDouble("topCompositeCreate", value.topCompositeCreate.toDouble())
putDouble("topResourceRelease", value.topResourceRelease.toDouble())
putBoolean("rangeFallback", value.rangeFallback)
putBoolean("fastPathEligible", value.fastPathEligible)
putBoolean("fastPathActive", value.fastPathActive)
putString("fastPathFallbackReason", value.fastPathFallbackReason)
putDouble("fastPathHit", value.fastPathHit.toDouble())
putDouble("fastPathMiss", value.fastPathMiss.toDouble())
putDouble("fullCpuFrame", value.fullCpuFrame.toDouble())
})
}
@ReactMethod override fun clearWindFields(reactTag: Double, promise: Promise) = command(reactTag, promise) { it.clearWindFields() }

View File

@@ -177,10 +177,25 @@ RCT_EXPORT_MODULE(WeatherRuntimeBridge)
@"lastReason": value.lastReason, @"rangeConfigured": @(value.rangeConfigured),
@"rangeValid": @(value.rangeValid), @"startLayerId": value.startLayerId,
@"endLayerId": value.endLayerId, @"startIndex": @(value.startIndex), @"endIndex": @(value.endIndex),
@"bottomPresent": @(value.bottomPresent), @"bottomBuild": @(value.bottomBuild),
@"continuousBegin": @(value.continuousBegin), @"continuousEndExclusive": @(value.continuousEndExclusive),
@"bottomPresent": @(value.bottomPresent), @"bottomCacheState": value.bottomCacheState,
@"bottomBuild": @(value.bottomBuild),
@"bottomReuse": @(value.bottomReuse), @"bottomInvalidation": @(value.bottomInvalidation),
@"topPresent": @(value.topPresent), @"topBuild": @(value.topBuild),
@"topReuse": @(value.topReuse), @"topInvalidation": @(value.topInvalidation) });
@"bottomLastInvalidationReason": value.bottomLastInvalidationReason,
@"bottomTextureCreate": @(value.bottomTextureCreate),
@"bottomCompositeCreate": @(value.bottomCompositeCreate),
@"bottomResourceRelease": @(value.bottomResourceRelease),
@"topPresent": @(value.topPresent), @"topCacheState": value.topCacheState,
@"topBuild": @(value.topBuild), @"topReuse": @(value.topReuse),
@"topInvalidation": @(value.topInvalidation),
@"topLastInvalidationReason": value.topLastInvalidationReason,
@"topTextureCreate": @(value.topTextureCreate),
@"topCompositeCreate": @(value.topCompositeCreate),
@"topResourceRelease": @(value.topResourceRelease),
@"rangeFallback": @(value.rangeFallback), @"fastPathEligible": @(value.fastPathEligible),
@"fastPathActive": @(value.fastPathActive), @"fastPathFallbackReason": value.fastPathFallbackReason,
@"fastPathHit": @(value.fastPathHit), @"fastPathMiss": @(value.fastPathMiss),
@"fullCpuFrame": @(value.fullCpuFrame) });
}];
}

View File

@@ -12,8 +12,14 @@ type RuntimeSnapshot = { state: RuntimeState; playbackState: PlaybackState; targ
type RetainedUnderlaySnapshot = {
enabled: boolean; build: number; reuse: number; fallback: number; invalidation: number; lastReason: string;
rangeConfigured: boolean; rangeValid: boolean; startLayerId: string; endLayerId: string;
startIndex: number; endIndex: number; bottomPresent: boolean; bottomBuild: number; bottomReuse: number;
bottomInvalidation: number; topPresent: boolean; topBuild: number; topReuse: number; topInvalidation: number;
startIndex: number; endIndex: number; continuousBegin: number; continuousEndExclusive: number;
bottomPresent: boolean; bottomCacheState: string; bottomBuild: number; bottomReuse: number;
bottomInvalidation: number; bottomLastInvalidationReason: string; bottomTextureCreate: number;
bottomCompositeCreate: number; bottomResourceRelease: number;
topPresent: boolean; topCacheState: string; topBuild: number; topReuse: number; topInvalidation: number;
topLastInvalidationReason: string; topTextureCreate: number; topCompositeCreate: number;
topResourceRelease: number; rangeFallback: boolean; fastPathEligible: boolean; fastPathActive: boolean;
fastPathFallbackReason: string; fastPathHit: number; fastPathMiss: number; fullCpuFrame: number;
};
export interface Spec extends TurboModule {
attach(reactTag: number): Promise<RuntimeSnapshot>;

View File

@@ -34,12 +34,31 @@ export type RetainedUnderlayDiagnostics = Readonly<{
endLayerId: string;
startIndex: number;
endIndex: number;
continuousBegin: number;
continuousEndExclusive: number;
bottomPresent: boolean;
bottomCacheState: string;
bottomBuild: number;
bottomReuse: number;
bottomInvalidation: number;
bottomLastInvalidationReason: string;
bottomTextureCreate: number;
bottomCompositeCreate: number;
bottomResourceRelease: number;
topPresent: boolean;
topCacheState: string;
topBuild: number;
topReuse: number;
topInvalidation: number;
topLastInvalidationReason: string;
topTextureCreate: number;
topCompositeCreate: number;
topResourceRelease: number;
rangeFallback: boolean;
fastPathEligible: boolean;
fastPathActive: boolean;
fastPathFallbackReason: string;
fastPathHit: number;
fastPathMiss: number;
fullCpuFrame: number;
}>;

View File

@@ -729,7 +729,7 @@ jni::jboolean NativeMapView::setContinuousLayerRange(JNIEnv& env,
jni::Local<jni::Array<jni::String>> NativeMapView::getRetainedUnderlayDiagnostics(JNIEnv& env) {
const auto snapshot = map->getRetainedUnderlayDiagnostics();
const std::array<std::string, 20> values = {
const std::array<std::string, 39> values = {
snapshot.enabled ? "true" : "false",
std::to_string(snapshot.build),
std::to_string(snapshot.reuse),
@@ -742,14 +742,33 @@ jni::Local<jni::Array<jni::String>> NativeMapView::getRetainedUnderlayDiagnostic
snapshot.endLayerId,
std::to_string(snapshot.startIndex),
std::to_string(snapshot.endIndex),
std::to_string(snapshot.continuousBegin),
std::to_string(snapshot.continuousEndExclusive),
snapshot.bottomPresent ? "true" : "false",
snapshot.bottomCacheState,
std::to_string(snapshot.bottomBuild),
std::to_string(snapshot.bottomReuse),
std::to_string(snapshot.bottomInvalidation),
snapshot.bottomLastInvalidationReason,
std::to_string(snapshot.bottomTextureCreate),
std::to_string(snapshot.bottomCompositeCreate),
std::to_string(snapshot.bottomResourceRelease),
snapshot.topPresent ? "true" : "false",
snapshot.topCacheState,
std::to_string(snapshot.topBuild),
std::to_string(snapshot.topReuse),
std::to_string(snapshot.topInvalidation),
snapshot.topLastInvalidationReason,
std::to_string(snapshot.topTextureCreate),
std::to_string(snapshot.topCompositeCreate),
std::to_string(snapshot.topResourceRelease),
snapshot.rangeFallback ? "true" : "false",
snapshot.fastPathEligible ? "true" : "false",
snapshot.fastPathActive ? "true" : "false",
snapshot.fastPathFallbackReason,
std::to_string(snapshot.fastPathHit),
std::to_string(snapshot.fastPathMiss),
std::to_string(snapshot.fullCpuFrame),
};
auto result = jni::Array<jni::String>::New(env, values.size());
for (std::size_t index = 0; index < values.size(); ++index) {

View File

@@ -18,18 +18,37 @@ public final class RetainedUnderlayDiagnostics {
@NonNull public final String endLayerId;
public final int startIndex;
public final int endIndex;
public final int continuousBegin;
public final int continuousEndExclusive;
public final boolean bottomPresent;
@NonNull public final String bottomCacheState;
public final long bottomBuild;
public final long bottomReuse;
public final long bottomInvalidation;
@NonNull public final String bottomLastInvalidationReason;
public final long bottomTextureCreate;
public final long bottomCompositeCreate;
public final long bottomResourceRelease;
public final boolean topPresent;
@NonNull public final String topCacheState;
public final long topBuild;
public final long topReuse;
public final long topInvalidation;
@NonNull public final String topLastInvalidationReason;
public final long topTextureCreate;
public final long topCompositeCreate;
public final long topResourceRelease;
public final boolean rangeFallback;
public final boolean fastPathEligible;
public final boolean fastPathActive;
@NonNull public final String fastPathFallbackReason;
public final long fastPathHit;
public final long fastPathMiss;
public final long fullCpuFrame;
RetainedUnderlayDiagnostics(@NonNull String[] values) {
if (values.length != 20) {
throw new IllegalArgumentException("Retained-range diagnostics require 20 values");
if (values.length != 39) {
throw new IllegalArgumentException("Retained-range diagnostics require 39 values");
}
enabled = Boolean.parseBoolean(values[0]);
build = Long.parseLong(values[1]);
@@ -43,20 +62,41 @@ public final class RetainedUnderlayDiagnostics {
endLayerId = values[9];
startIndex = Integer.parseInt(values[10]);
endIndex = Integer.parseInt(values[11]);
bottomPresent = Boolean.parseBoolean(values[12]);
bottomBuild = Long.parseLong(values[13]);
bottomReuse = Long.parseLong(values[14]);
bottomInvalidation = Long.parseLong(values[15]);
topPresent = Boolean.parseBoolean(values[16]);
topBuild = Long.parseLong(values[17]);
topReuse = Long.parseLong(values[18]);
topInvalidation = Long.parseLong(values[19]);
continuousBegin = Integer.parseInt(values[12]);
continuousEndExclusive = Integer.parseInt(values[13]);
bottomPresent = Boolean.parseBoolean(values[14]);
bottomCacheState = values[15];
bottomBuild = Long.parseLong(values[16]);
bottomReuse = Long.parseLong(values[17]);
bottomInvalidation = Long.parseLong(values[18]);
bottomLastInvalidationReason = values[19];
bottomTextureCreate = Long.parseLong(values[20]);
bottomCompositeCreate = Long.parseLong(values[21]);
bottomResourceRelease = Long.parseLong(values[22]);
topPresent = Boolean.parseBoolean(values[23]);
topCacheState = values[24];
topBuild = Long.parseLong(values[25]);
topReuse = Long.parseLong(values[26]);
topInvalidation = Long.parseLong(values[27]);
topLastInvalidationReason = values[28];
topTextureCreate = Long.parseLong(values[29]);
topCompositeCreate = Long.parseLong(values[30]);
topResourceRelease = Long.parseLong(values[31]);
rangeFallback = Boolean.parseBoolean(values[32]);
fastPathEligible = Boolean.parseBoolean(values[33]);
fastPathActive = Boolean.parseBoolean(values[34]);
fastPathFallbackReason = values[35];
fastPathHit = Long.parseLong(values[36]);
fastPathMiss = Long.parseLong(values[37]);
fullCpuFrame = Long.parseLong(values[38]);
}
static RetainedUnderlayDiagnostics unavailable() {
return new RetainedUnderlayDiagnostics(new String[] {
"true", "0", "0", "0", "0", "unavailable", "false", "false", "", "", "-1", "-1",
"false", "0", "0", "0", "false", "0", "0", "0"
"-1", "-1", "false", "disabled", "0", "0", "0", "none", "0", "0", "0",
"false", "disabled", "0", "0", "0", "none", "0", "0", "0", "false", "false",
"false", "no-prepared-tree", "0", "0", "0"
});
}
}

View File

@@ -288,6 +288,7 @@
</activity>
<activity
android:name=".activity.particle.ParticleCoreLayerValidationActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:description="@string/description_particle_core_layer_validation"
android:exported="true"
android:label="@string/activity_particle_core_layer_validation">

View File

@@ -2,6 +2,8 @@ package org.maplibre.android.testapp.activity.particle
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.InputType
import android.util.Log
import android.widget.Button
@@ -28,6 +30,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private lateinit var mapView: MapView
private lateinit var maplibreMap: MapLibreMap
private lateinit var status: TextView
private lateinit var diagnostics: TextView
private var active = true
private var enabled = true
private var maskMode = MASK_GLOBAL
@@ -51,6 +54,21 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private var navSeaPbfRenderEnabled = true
private var navSeaPbfLayerVisibility = emptyList<NavSeaPbfLayerVisibility>()
private var retainedCacheEnabled = true
private var step093Mode = false
private var step093Topology = Step093Topology.BOTTOM_CONTINUOUS_TOP
private var step093Variant = Step093Variant.DEFAULT
private var step093StartVisible = true
private var step093EndVisible = true
private var step093CameraPreset = 0
private val step093Handler = Handler(Looper.getMainLooper())
private val step093DiagnosticsPoll = object : Runnable {
override fun run() {
if (step093Mode && this@ParticleCoreLayerValidationActivity::maplibreMap.isInitialized) {
refreshStep093Diagnostics(log = false)
step093Handler.postDelayed(this, STEP093_DIAGNOSTICS_INTERVAL_MS)
}
}
}
private var realWeatherTuningMenu: PopupMenu? = null
private var realWeatherMenuAnchor: android.view.View? = null
private var openRealWeatherTuningAfterStyle = false
@@ -88,6 +106,13 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
?: NavSeaPbfPosition.AFTER_CLIP
navSeaPbfRenderEnabled = savedInstanceState?.getBoolean(STATE_NAVSEA_PBF_RENDER_ENABLED, true) ?: true
retainedCacheEnabled = savedInstanceState?.getBoolean(STATE_RETAINED_CACHE_ENABLED, true) ?: true
step093Mode = savedInstanceState?.getBoolean(STATE_STEP093_MODE, false) ?: false
step093Topology = savedInstanceState?.getString(STATE_STEP093_TOPOLOGY)
?.let { value -> Step093Topology.values().firstOrNull { it.name == value } }
?: Step093Topology.BOTTOM_CONTINUOUS_TOP
step093Variant = savedInstanceState?.getString(STATE_STEP093_VARIANT)
?.let { value -> Step093Variant.values().firstOrNull { it.name == value } }
?: Step093Variant.DEFAULT
super.onCreate(savedInstanceState)
Log.i(
TAG,
@@ -98,6 +123,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
mapView = findViewById(R.id.mapView)
status = findViewById(R.id.validationStatus)
diagnostics = findViewById(R.id.validationDiagnostics)
mapView.addOnDidFailLoadingMapListener { errorMessage ->
if (navSeaPbfMode || purePbfMode) {
Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=$errorMessage")
@@ -119,7 +145,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
updateRetainedCacheButtonState()
loadValidationStyle(active = active, enabled = enabled, reason = "INITIAL")
if (step093Mode) loadStep093Style(configureBeforeStyle = false)
else loadValidationStyle(active = active, enabled = enabled, reason = "INITIAL")
}
findViewById<Button>(R.id.validationActive).setOnClickListener {
@@ -251,6 +278,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
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.validationMenuStep093).setOnClickListener { showStep093Menu(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)) }
}
@@ -264,8 +292,32 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
TOP("TOP")
}
private enum class Step093Topology(val label: String, val bottom: Boolean, val top: Boolean) {
BOTTOM_CONTINUOUS_TOP("A BOTTOM + CONTINUOUS + TOP", true, true),
BOTTOM_CONTINUOUS("B BOTTOM + CONTINUOUS", true, false),
CONTINUOUS_TOP("C CONTINUOUS + TOP", false, true),
CONTINUOUS_ONLY("D CONTINUOUS ONLY", false, false),
SINGLE_LAYER("E SINGLE-LAYER CONTINUOUS", true, true)
}
private enum class Step093Variant {
DEFAULT,
CONTINUOUS_ADD,
CONTINUOUS_REORDER,
BOTTOM_ADD,
BOTTOM_REORDER,
TOP_ADD,
TOP_REORDER,
CROSS_BOTTOM,
CROSS_CONTINUOUS,
CROSS_TOP,
START_MISSING,
END_MISSING,
FAST_PATH_FAIL_CLOSED
}
private fun activateRealWeather(anchor: android.view.View?) {
if (isPbfMatrixMode()) leaveRealWeatherMode()
if (isPbfMatrixMode() || step093Mode) leaveRealWeatherMode()
realWeatherMenuAnchor = anchor ?: findViewById(R.id.validationMenuRealWeather)
if (realWeatherMode) {
if (realWeatherTuningMenu == null) showRealWeatherTuning(realWeatherMenuAnchor!!) else dismissRealWeatherTuning()
@@ -296,6 +348,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
private fun leaveRealWeatherMode() {
step093Mode = false
step093Handler.removeCallbacks(step093DiagnosticsPoll)
dismissRealWeatherTuning()
weatherRuntime?.dispose()
weatherRuntime = null
@@ -381,6 +435,233 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
)
}
private fun showStep093Menu(anchor: android.view.View) {
PopupMenu(this, anchor).apply {
Step093Topology.values().forEachIndexed { index, topology ->
menu.add(0, index, index, topology.label)
}
menu.add(0, 10, 10, "MUTATION / FAST PATH")
menu.add(0, 11, 11, "ENDPOINT / VISIBILITY")
menu.add(0, 12, 12, "INVALID RANGE")
menu.add(0, 13, 13, "CAMERA NEXT")
menu.add(0, 14, 14, "READ DIAGNOSTICS")
menu.add(0, 15, 15, "RESET DEFAULT A")
setOnMenuItemClickListener { item ->
when (item.itemId) {
in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId])
10 -> showStep093MutationMenu(anchor)
11 -> showStep093EndpointMenu(anchor)
12 -> showStep093InvalidMenu(anchor)
13 -> applyStep093CameraPreset()
14 -> refreshStep093Diagnostics(log = true)
15 -> applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP)
}
true
}
show()
}
}
private fun showStep093MutationMenu(anchor: android.view.View) {
val actions = arrayOf(
"CONTINUOUS ADD", "CONTINUOUS REORDER", "BOTTOM ADD", "BOTTOM REORDER",
"TOP ADD", "TOP REORDER", "CROSS → BOTTOM", "CROSS → CONTINUOUS",
"CROSS → TOP", "FAST FAIL-CLOSED", "RESTORE DEFAULT"
)
PopupMenu(this, anchor).apply {
actions.forEachIndexed { index, label -> menu.add(0, index, index, label) }
setOnMenuItemClickListener { item ->
val variant = when (item.itemId) {
0 -> Step093Variant.CONTINUOUS_ADD
1 -> Step093Variant.CONTINUOUS_REORDER
2 -> Step093Variant.BOTTOM_ADD
3 -> Step093Variant.BOTTOM_REORDER
4 -> Step093Variant.TOP_ADD
5 -> Step093Variant.TOP_REORDER
6 -> Step093Variant.CROSS_BOTTOM
7 -> Step093Variant.CROSS_CONTINUOUS
8 -> Step093Variant.CROSS_TOP
9 -> Step093Variant.FAST_PATH_FAIL_CLOSED
else -> Step093Variant.DEFAULT
}
applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP, variant)
true
}
show()
}
}
private fun showStep093EndpointMenu(anchor: android.view.View) {
val actions = arrayOf(
"START VISIBLE ↔ HIDDEN", "END VISIBLE ↔ HIDDEN", "DELETE START", "RESTORE START",
"DELETE END", "RESTORE END"
)
PopupMenu(this, anchor).apply {
actions.forEachIndexed { index, label -> menu.add(0, index, index, label) }
setOnMenuItemClickListener { item ->
when (item.itemId) {
0 -> toggleStep093Endpoint(start = true)
1 -> toggleStep093Endpoint(start = false)
2 -> applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP, Step093Variant.START_MISSING)
3 -> applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP)
4 -> applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP, Step093Variant.END_MISSING)
5 -> applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP)
}
true
}
show()
}
}
private fun showStep093InvalidMenu(anchor: android.view.View) {
val actions = arrayOf(
"MISSING START", "MISSING END", "REVERSED", "CONFIGURE WHILE STYLE LOADING", "EMPTY ID", "RESTORE VALID"
)
PopupMenu(this, anchor).apply {
actions.forEachIndexed { index, label -> menu.add(0, index, index, label) }
setOnMenuItemClickListener { item ->
if (!ensureStep093Scene()) {
Log.i(TAG, "STEP093_ACTION scene initialized; repeat invalid-range action after READY")
return@setOnMenuItemClickListener true
}
when (item.itemId) {
0 -> setStep093Range("step093-missing-start", STEP093_PARTICLE_LAYER_ID, "INVALID_MISSING_START")
1 -> setStep093Range(STEP093_WEATHER_LAYER_ID, "step093-missing-end", "INVALID_MISSING_END")
2 -> setStep093Range(STEP093_PARTICLE_LAYER_ID, STEP093_WEATHER_LAYER_ID, "INVALID_REVERSED")
3 -> applyStep093Scene(
Step093Topology.BOTTOM_CONTINUOUS_TOP,
Step093Variant.DEFAULT,
configureBeforeStyle = true
)
4 -> {
val accepted = maplibreMap.setContinuousLayerRange("", STEP093_PARTICLE_LAYER_ID)
Log.i(TAG, "STEP093_RANGE action=EMPTY_ID accepted=$accepted expected=false")
refreshStep093Diagnostics(log = true)
}
5 -> setStep093Range(STEP093_WEATHER_LAYER_ID, STEP093_PARTICLE_LAYER_ID, "RESTORE_VALID")
}
true
}
show()
}
}
private fun ensureStep093Scene(): Boolean {
if (step093Mode && styleReady) return true
applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP)
return false
}
private fun applyStep093Scene(
topology: Step093Topology,
variant: Step093Variant = Step093Variant.DEFAULT,
configureBeforeStyle: Boolean = false
) {
leaveRealWeatherMode()
step093Mode = true
step093Topology = topology
step093Variant = variant
step093StartVisible = true
step093EndVisible = true
timeWindowMode = true
productionFeedMode = false
hostSubmissionMode = true
realWeatherMode = false
pureParticleMode = false
purePbfMode = false
navSeaPbfMode = false
active = true
enabled = true
retainedCacheEnabled = true
maplibreMap.setRetainedUnderlayEnabled(true)
updateRetainedCacheButtonState()
loadStep093Style(configureBeforeStyle)
}
private fun loadStep093Style(configureBeforeStyle: Boolean) {
weatherRuntime?.dispose()
weatherRuntime = null
styleReady = false
val epoch = ++styleLoadEpoch
val range = step093Range()
status.text = "STEP-093 · ${step093Topology.label} · ${step093Variant.name} · LOADING"
maplibreMap.setStyle(Style.Builder().fromJson(step093StyleJson())) {
if (!step093Mode || epoch != styleLoadEpoch) return@setStyle
styleReady = true
if (!configureBeforeStyle) maplibreMap.setContinuousLayerRange(range.first, range.second)
submitHostValidationDataset()
fitRealWeatherBounds()
status.text = "STEP-093 · ${step093Topology.label} · ${step093Variant.name}"
startStep093DiagnosticsPolling()
refreshStep093Diagnostics(log = true)
Log.i(
TAG,
"STEP093_SCENE_READY topology=${step093Topology.name} variant=${step093Variant.name} " +
"range=${range.first}..${range.second}"
)
}
if (configureBeforeStyle) {
maplibreMap.setContinuousLayerRange(range.first, range.second)
Log.i(TAG, "STEP093_RANGE action=CONFIGURE_WHILE_STYLE_LOADING start=${range.first} end=${range.second}")
refreshStep093Diagnostics(log = true)
}
}
private fun step093Range(): Pair<String, String> = if (step093Topology == Step093Topology.SINGLE_LAYER) {
STEP093_PARTICLE_LAYER_ID to STEP093_PARTICLE_LAYER_ID
} else {
STEP093_WEATHER_LAYER_ID to STEP093_PARTICLE_LAYER_ID
}
private fun setStep093Range(start: String, end: String, action: String) {
val accepted = maplibreMap.setContinuousLayerRange(start, end)
Log.i(TAG, "STEP093_RANGE action=$action accepted=$accepted start=$start end=$end")
refreshStep093Diagnostics(log = true)
}
private fun toggleStep093Endpoint(start: Boolean) {
if (!ensureStep093Scene()) return
val id = if (start) step093Range().first else step093Range().second
val visible = if (start) !step093StartVisible else !step093EndVisible
val accepted = maplibreMap.setStyleLayerVisibility(id, visible)
if (start) step093StartVisible = visible else step093EndVisible = visible
Log.i(TAG, "STEP093_VISIBILITY endpoint=${if (start) "start" else "end"} id=$id visible=$visible accepted=$accepted")
refreshStep093Diagnostics(log = true)
}
private fun applyStep093CameraPreset() {
if (!ensureStep093Scene()) return
step093CameraPreset = (step093CameraPreset + 1) % 4
val position = when (step093CameraPreset) {
1 -> CameraPosition.Builder().target(LatLng(31.0, 139.0)).zoom(3.5).bearing(0.0).tilt(0.0).build()
2 -> CameraPosition.Builder().target(LatLng(25.0, 132.0)).zoom(4.5).bearing(55.0).tilt(0.0).build()
3 -> CameraPosition.Builder().target(LatLng(37.0, 145.0)).zoom(5.0).bearing(120.0).tilt(45.0).build()
else -> CameraPosition.Builder().target(LatLng(28.0, 135.0)).zoom(3.0).bearing(0.0).tilt(0.0).build()
}
maplibreMap.animateCamera(CameraUpdateFactory.newCameraPosition(position), 700)
Log.i(TAG, "STEP093_CAMERA preset=$step093CameraPreset position=$position")
}
private fun startStep093DiagnosticsPolling() {
step093Handler.removeCallbacks(step093DiagnosticsPoll)
step093Handler.post(step093DiagnosticsPoll)
}
private fun refreshStep093Diagnostics(log: Boolean) {
val snapshot = maplibreMap.retainedUnderlayDiagnostics
val cacheCount = (if (snapshot.bottomPresent) 1 else 0) + (if (snapshot.topPresent) 1 else 0)
diagnostics.text = buildString {
append("Range cfg=${snapshot.rangeConfigured} valid=${snapshot.rangeValid} fallback=${snapshot.rangeFallback} reason=${snapshot.lastReason}\n")
append("${snapshot.startLayerId}[${snapshot.startIndex}]..${snapshot.endLayerId}[${snapshot.endIndex}] C=[${snapshot.continuousBegin},${snapshot.continuousEndExclusive}) caches=$cacheCount\n")
append("B ${snapshot.bottomPresent}/${snapshot.bottomCacheState} b/r/i=${snapshot.bottomBuild}/${snapshot.bottomReuse}/${snapshot.bottomInvalidation} res=${snapshot.bottomTextureCreate}/${snapshot.bottomCompositeCreate}/${snapshot.bottomResourceRelease} ${snapshot.bottomLastInvalidationReason}\n")
append("T ${snapshot.topPresent}/${snapshot.topCacheState} b/r/i=${snapshot.topBuild}/${snapshot.topReuse}/${snapshot.topInvalidation} res=${snapshot.topTextureCreate}/${snapshot.topCompositeCreate}/${snapshot.topResourceRelease} ${snapshot.topLastInvalidationReason}\n")
append("Fast e/a=${snapshot.fastPathEligible}/${snapshot.fastPathActive} h/m/full=${snapshot.fastPathHit}/${snapshot.fastPathMiss}/${snapshot.fullCpuFrame} ${snapshot.fastPathFallbackReason}")
}
if (log) {
Log.i(TAG, "STEP093_DIAGNOSTICS ${diagnostics.text.toString().replace('\n', ' ')}")
}
}
private fun dismissRealWeatherTuning() { realWeatherTuningMenu?.dismiss(); realWeatherTuningMenu = null }
private fun showRealWeatherTuning(anchor: android.view.View) {
@@ -564,7 +845,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
private fun enterPbfMatrix() {
if (realWeatherMode) leaveRealWeatherMode()
if (realWeatherMode || step093Mode) leaveRealWeatherMode()
if (!isPbfMatrixMode()) {
preRealWeatherVisuals = ParticleVisuals(particleCount, lineWidth, particleLength, particleOpacity, trailOpacity, trailFade)
particleCount = REAL_WEATHER_PARTICLE_COUNT
@@ -1277,6 +1558,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
private fun statusText(): String {
if (step093Mode) {
return "STEP-093 · ${step093Topology.label} · ${step093Variant.name} · " +
"CACHE ${if (retainedCacheEnabled) "ON" else "OFF"}"
}
val mask = when (maskMode) {
MASK_OCEAN_A -> "OCEAN A"
MASK_OCEAN_B -> "OCEAN B"
@@ -1318,6 +1603,94 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
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)}"
private fun step093StyleJson(): String {
val sources = JSONObject()
.put(PARTICLE_SOURCE_ID, JSONObject().put("type", "raster-array").put("host-field-submission", true))
.put("step093-bottom-shape", step093GeoJsonSource("""{"type":"Polygon","coordinates":[[[112,0],[158,0],[158,56],[112,56],[112,0]]]}"""))
.put("step093-semi-shape", step093GeoJsonSource("""{"type":"Polygon","coordinates":[[[134,8],[151,8],[151,49],[134,49],[134,8]]]}"""))
.put("step093-opaque-shape", step093GeoJsonSource("""{"type":"Polygon","coordinates":[[[124,18],[136,18],[136,42],[124,42],[124,18]]]}"""))
.put("step093-line-shape", step093GeoJsonSource("""{"type":"LineString","coordinates":[[115,6],[154,50],[154,6],[115,50]]}"""))
fun fill(id: String, source: String, color: String, opacity: Double) = JSONObject()
.put("id", id).put("type", "fill").put("source", source)
.put("paint", JSONObject().put("fill-color", color).put("fill-opacity", opacity))
fun line(id: String, source: String, color: String, width: Double) = JSONObject()
.put("id", id).put("type", "line").put("source", source)
.put("paint", JSONObject().put("line-color", color).put("line-width", width).put("line-opacity", 0.95))
val layers = JSONArray()
if (step093Topology.bottom) {
layers.put(JSONObject().put("id", STEP093_BOTTOM_BACKGROUND_ID).put("type", "background")
.put("paint", JSONObject().put("background-color", "#0b2942")))
val bottomBase = fill(STEP093_BOTTOM_FILL_ID, "step093-bottom-shape", "#176b87", 0.72)
val bottomExtra = fill(STEP093_BOTTOM_EXTRA_ID, "step093-semi-shape", "#2aa198", 0.18)
if (step093Variant == Step093Variant.BOTTOM_REORDER) {
layers.put(bottomExtra).put(bottomBase)
} else {
layers.put(bottomBase)
if (step093Variant == Step093Variant.BOTTOM_ADD) layers.put(bottomExtra)
}
if (step093Variant == Step093Variant.CROSS_BOTTOM) {
layers.put(fill(STEP093_CROSS_LAYER_ID, "step093-opaque-shape", "#9b59b6", 0.18))
}
}
if (step093Variant != Step093Variant.START_MISSING) {
layers.put(
JSONObject().put("id", STEP093_WEATHER_LAYER_ID).put("type", "weather-color")
.put("source", PARTICLE_SOURCE_ID)
.put("paint", JSONObject().put("weather-color-opacity", 0.32))
)
}
if (step093Variant == Step093Variant.CONTINUOUS_ADD ||
step093Variant == Step093Variant.CONTINUOUS_REORDER) {
val first = fill(STEP093_CONTINUOUS_A_ID, "step093-semi-shape", "#ff00ff", 0.16)
val second = fill(STEP093_CONTINUOUS_B_ID, "step093-opaque-shape", "#00ffff", 0.12)
if (step093Variant == Step093Variant.CONTINUOUS_REORDER) layers.put(second).put(first)
else layers.put(first).put(second)
}
if (step093Variant == Step093Variant.CROSS_CONTINUOUS) {
layers.put(fill(STEP093_CROSS_LAYER_ID, "step093-opaque-shape", "#9b59b6", 0.18))
}
if (step093Variant == Step093Variant.FAST_PATH_FAIL_CLOSED) {
layers.put(fill(STEP093_UNKNOWN_CONTINUOUS_ID, "step093-semi-shape", "#ff5500", 0.20))
}
if (step093Variant != Step093Variant.END_MISSING) layers.put(navSeaParticleLayer(active = true))
if (step093Variant == Step093Variant.CROSS_TOP) {
layers.put(fill(STEP093_CROSS_LAYER_ID, "step093-opaque-shape", "#9b59b6", 0.18))
}
if (step093Topology.top) {
val semi = fill(STEP093_TOP_SEMI_ID, "step093-semi-shape", "#ffd166", 0.42)
val opaque = fill(STEP093_TOP_OPAQUE_ID, "step093-opaque-shape", "#ef476f", 1.0)
val outline = line(STEP093_TOP_LINE_ID, "step093-line-shape", "#ffffff", 6.0)
val extra = line(STEP093_TOP_EXTRA_ID, "step093-opaque-shape", "#00ff99", 3.0)
if (step093Variant == Step093Variant.TOP_REORDER) {
layers.put(outline).put(opaque).put(semi)
} else {
layers.put(semi).put(opaque).put(outline)
if (step093Variant == Step093Variant.TOP_ADD) layers.put(extra)
}
}
return JSONObject()
.put("version", 8)
.put("name", "STEP-093 Continuous Range Runtime Validation")
.put("sources", sources)
.put("layers", layers)
.toString()
}
private fun step093GeoJsonSource(geometryJson: String): JSONObject = JSONObject()
.put("type", "geojson")
.put(
"data",
JSONObject()
.put("type", "Feature")
.put("properties", JSONObject())
.put("geometry", JSONObject(geometryJson))
)
private fun validationStyle(active: Boolean, enabled: Boolean, reason: String): String {
val fixtureId = fixtureId()
val nextFixtureId = if (timeWindowMode) {
@@ -1396,11 +1769,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
""".trimIndent()
}
override fun onStart() { super.onStart(); mapView.onStart() }
override fun onStart() { super.onStart(); mapView.onStart(); if (step093Mode) startStep093DiagnosticsPolling() }
override fun onResume() { super.onResume(); mapView.onResume() }
override fun onPause() { mapView.onPause(); super.onPause() }
override fun onStop() { mapView.onStop(); super.onStop() }
override fun onDestroy() { weatherRuntime?.dispose(); weatherRuntime = null; mapView.onDestroy(); super.onDestroy() }
override fun onStop() { step093Handler.removeCallbacks(step093DiagnosticsPoll); mapView.onStop(); super.onStop() }
override fun onDestroy() { step093Handler.removeCallbacks(step093DiagnosticsPoll); weatherRuntime?.dispose(); weatherRuntime = null; mapView.onDestroy(); super.onDestroy() }
override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() }
override fun onSaveInstanceState(outState: Bundle) {
@@ -1425,6 +1798,9 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
outState.putString(STATE_NAVSEA_PBF_POSITION, navSeaPbfPosition.name)
outState.putBoolean(STATE_NAVSEA_PBF_RENDER_ENABLED, navSeaPbfRenderEnabled)
outState.putBoolean(STATE_RETAINED_CACHE_ENABLED, retainedCacheEnabled)
outState.putBoolean(STATE_STEP093_MODE, step093Mode)
outState.putString(STATE_STEP093_TOPOLOGY, step093Topology.name)
outState.putString(STATE_STEP093_VARIANT, step093Variant.name)
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
@@ -1458,9 +1834,26 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
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 STATE_STEP093_MODE = "particle_validation.step093_mode"
private const val STATE_STEP093_TOPOLOGY = "particle_validation.step093_topology"
private const val STATE_STEP093_VARIANT = "particle_validation.step093_variant"
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"
private const val STEP093_DIAGNOSTICS_INTERVAL_MS = 1000L
private const val STEP093_WEATHER_LAYER_ID = WEATHER_COLOR_LAYER_ID
private const val STEP093_PARTICLE_LAYER_ID = PARTICLE_LAYER_ID
private const val STEP093_BOTTOM_BACKGROUND_ID = "step093-bottom-background"
private const val STEP093_BOTTOM_FILL_ID = "step093-bottom-fill"
private const val STEP093_BOTTOM_EXTRA_ID = "step093-bottom-extra"
private const val STEP093_CONTINUOUS_A_ID = "step093-continuous-a"
private const val STEP093_CONTINUOUS_B_ID = "step093-continuous-b"
private const val STEP093_UNKNOWN_CONTINUOUS_ID = "step093-unknown-continuous"
private const val STEP093_CROSS_LAYER_ID = "step093-cross-layer"
private const val STEP093_TOP_SEMI_ID = "step093-top-semi"
private const val STEP093_TOP_OPAQUE_ID = "step093-top-opaque"
private const val STEP093_TOP_LINE_ID = "step093-top-line"
private const val STEP093_TOP_EXTRA_ID = "step093-top-extra"
private const val MASK_GLOBAL = 0
private const val MASK_OCEAN_A = 1
private const val MASK_OCEAN_B = 2

View File

@@ -3,6 +3,7 @@
<org.maplibre.android.maps.MapView android:id="@id/mapView" android:layout_width="match_parent" android:layout_height="match_parent" />
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="#DD102030" android:orientation="vertical" android:padding="4dp">
<TextView android:id="@+id/validationStatus" android:layout_width="match_parent" android:layout_height="42dp" android:gravity="center_vertical" android:maxLines="2" android:paddingLeft="4dp" android:paddingRight="4dp" android:textColor="@android:color/white" android:textSize="12sp" />
<TextView android:id="@+id/validationDiagnostics" android:layout_width="match_parent" android:layout_height="92dp" android:fontFamily="monospace" android:gravity="center_vertical" android:maxLines="6" android:paddingLeft="4dp" android:paddingRight="4dp" android:text="STEP-093 diagnostics: open STEP 093" android:textColor="#FFDD88" android:textSize="9sp" />
<LinearLayout android:layout_width="match_parent" android:layout_height="40dp" android:orientation="horizontal">
<Button android:id="@+id/validationMenuTime" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="TIME" />
<Button android:id="@+id/validationMenuVisual" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="VIS" />
@@ -12,6 +13,7 @@
<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/validationMenuStep093" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="STEP 093" 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. -->

View File

@@ -44,14 +44,33 @@ MLN_EXPORT
@property (nonatomic, copy, readonly) NSString *endLayerId;
@property (nonatomic, readonly) NSInteger startIndex;
@property (nonatomic, readonly) NSInteger endIndex;
@property (nonatomic, readonly) NSInteger continuousBegin;
@property (nonatomic, readonly) NSInteger continuousEndExclusive;
@property (nonatomic, readonly) BOOL bottomPresent;
@property (nonatomic, copy, readonly) NSString *bottomCacheState;
@property (nonatomic, readonly) uint64_t bottomBuild;
@property (nonatomic, readonly) uint64_t bottomReuse;
@property (nonatomic, readonly) uint64_t bottomInvalidation;
@property (nonatomic, copy, readonly) NSString *bottomLastInvalidationReason;
@property (nonatomic, readonly) uint64_t bottomTextureCreate;
@property (nonatomic, readonly) uint64_t bottomCompositeCreate;
@property (nonatomic, readonly) uint64_t bottomResourceRelease;
@property (nonatomic, readonly) BOOL topPresent;
@property (nonatomic, copy, readonly) NSString *topCacheState;
@property (nonatomic, readonly) uint64_t topBuild;
@property (nonatomic, readonly) uint64_t topReuse;
@property (nonatomic, readonly) uint64_t topInvalidation;
@property (nonatomic, copy, readonly) NSString *topLastInvalidationReason;
@property (nonatomic, readonly) uint64_t topTextureCreate;
@property (nonatomic, readonly) uint64_t topCompositeCreate;
@property (nonatomic, readonly) uint64_t topResourceRelease;
@property (nonatomic, readonly) BOOL rangeFallback;
@property (nonatomic, readonly) BOOL fastPathEligible;
@property (nonatomic, readonly) BOOL fastPathActive;
@property (nonatomic, copy, readonly) NSString *fastPathFallbackReason;
@property (nonatomic, readonly) uint64_t fastPathHit;
@property (nonatomic, readonly) uint64_t fastPathMiss;
@property (nonatomic, readonly) uint64_t fullCpuFrame;
- (instancetype)init NS_UNAVAILABLE;

View File

@@ -267,6 +267,13 @@ int processIsTranslated() {
_endLayerId = @"";
_startIndex = -1;
_endIndex = -1;
_continuousBegin = -1;
_continuousEndExclusive = -1;
_bottomCacheState = @"disabled";
_bottomLastInvalidationReason = @"none";
_topCacheState = @"disabled";
_topLastInvalidationReason = @"none";
_fastPathFallbackReason = @"no-prepared-tree";
}
return self;
}
@@ -278,14 +285,33 @@ int processIsTranslated() {
_endLayerId = [NSString stringWithUTF8String:snapshot.endLayerId.c_str()];
_startIndex = snapshot.startIndex;
_endIndex = snapshot.endIndex;
_continuousBegin = snapshot.continuousBegin;
_continuousEndExclusive = snapshot.continuousEndExclusive;
_bottomPresent = snapshot.bottomPresent;
_bottomCacheState = [NSString stringWithUTF8String:snapshot.bottomCacheState.c_str()];
_bottomBuild = snapshot.bottomBuild;
_bottomReuse = snapshot.bottomReuse;
_bottomInvalidation = snapshot.bottomInvalidation;
_bottomLastInvalidationReason = [NSString stringWithUTF8String:snapshot.bottomLastInvalidationReason.c_str()];
_bottomTextureCreate = snapshot.bottomTextureCreate;
_bottomCompositeCreate = snapshot.bottomCompositeCreate;
_bottomResourceRelease = snapshot.bottomResourceRelease;
_topPresent = snapshot.topPresent;
_topCacheState = [NSString stringWithUTF8String:snapshot.topCacheState.c_str()];
_topBuild = snapshot.topBuild;
_topReuse = snapshot.topReuse;
_topInvalidation = snapshot.topInvalidation;
_topLastInvalidationReason = [NSString stringWithUTF8String:snapshot.topLastInvalidationReason.c_str()];
_topTextureCreate = snapshot.topTextureCreate;
_topCompositeCreate = snapshot.topCompositeCreate;
_topResourceRelease = snapshot.topResourceRelease;
_rangeFallback = snapshot.rangeFallback;
_fastPathEligible = snapshot.fastPathEligible;
_fastPathActive = snapshot.fastPathActive;
_fastPathFallbackReason = [NSString stringWithUTF8String:snapshot.fastPathFallbackReason.c_str()];
_fastPathHit = snapshot.fastPathHit;
_fastPathMiss = snapshot.fastPathMiss;
_fullCpuFrame = snapshot.fullCpuFrame;
}
@end

View File

@@ -28,6 +28,7 @@ enum class ParticleFastPathMissReason : uint8_t {
StillImageRequest,
UnsupportedMode,
DebugOptions,
UnsupportedContinuousLayer,
UnsupportedScene,
AnnotationMutation,
};
@@ -56,6 +57,7 @@ inline const char* particleFastPathMissReasonText(ParticleFastPathMissReason rea
case ParticleFastPathMissReason::StillImageRequest: return "still-image-request";
case ParticleFastPathMissReason::UnsupportedMode: return "unsupported-mode";
case ParticleFastPathMissReason::DebugOptions: return "debug-options";
case ParticleFastPathMissReason::UnsupportedContinuousLayer: return "unsupported-continuous-layer";
case ParticleFastPathMissReason::UnsupportedScene: return "unsupported-scene";
case ParticleFastPathMissReason::AnnotationMutation: return "annotation-mutation";
}
@@ -84,6 +86,7 @@ struct ParticleFastPathGateInput {
bool stillImageRequest = false;
bool continuousMode = false;
bool debugDisabled = false;
bool unsupportedContinuousLayer = false;
bool supportedScene = false;
bool annotationMutation = false;
};
@@ -115,6 +118,9 @@ inline ParticleFastPathDecision evaluateParticleFastPath(const ParticleFastPathG
if (input.stillImageRequest) return {false, ParticleFastPathMissReason::StillImageRequest};
if (!input.continuousMode) return {false, ParticleFastPathMissReason::UnsupportedMode};
if (!input.debugDisabled) return {false, ParticleFastPathMissReason::DebugOptions};
if (input.unsupportedContinuousLayer) {
return {false, ParticleFastPathMissReason::UnsupportedContinuousLayer};
}
if (!input.supportedScene) return {false, ParticleFastPathMissReason::UnsupportedScene};
if (input.annotationMutation) return {false, ParticleFastPathMissReason::AnnotationMutation};
return {true, ParticleFastPathMissReason::None};

View File

@@ -265,11 +265,13 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptr<UpdatePara
.stillImageRequest = updateParameters->stillImageRequest,
.continuousMode = updateParameters->mode == MapMode::Continuous,
.debugDisabled = updateParameters->debugOptions == MapDebugOptions::NoDebug,
.unsupportedContinuousLayer = particlePreparedContinuousUnsupported,
.supportedScene = particlePreparedSceneSupported,
.annotationMutation = annotationMutation,
};
const auto decision = evaluateParticleFastPath(input);
particleFastPathLastReason = decision.reason;
retainedUnderlayCache->recordFastPathDecision(decision.candidate, decision.reason);
if (!decision.candidate) {
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleFastFrameMiss);
return false;
@@ -310,6 +312,7 @@ void Renderer::Impl::clearPreparedRenderTree() {
particlePreparedRangeConfigRevision = 0;
particlePreparedBottomPresent = false;
particlePreparedTopPresent = false;
particlePreparedContinuousUnsupported = false;
particlePreparedBackendSize = {};
particlePreparedSceneSupported = false;
}
@@ -321,6 +324,7 @@ void Renderer::Impl::render(const RenderTree& renderTree,
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FullRendererFrame);
if (!particleFastFrame) {
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleFullFrame);
retainedUnderlayCache->recordFullCpuFrame();
}
auto& context = backend.getContext();
context.setObserver(this);
@@ -591,6 +595,8 @@ void Renderer::Impl::render(const RenderTree& renderTree,
? orchestrator.continuousRangeSupportsParticleFastPath(
ranges.continuousBegin, ranges.continuousEndExclusive)
: retainedSupported;
if (!particleFastFrame) particlePreparedContinuousUnsupported = rangeMode && !fastContinuousSupported;
retainedUnderlayCache->recordFastPathEligibility(retainedSupported && fastContinuousSupported);
if (!particleFastFrame) particlePreparedSceneSupported = retainedSupported && fastContinuousSupported;
const char* retainedFallbackReason = nullptr;
if (!retainedMode) {

View File

@@ -112,6 +112,7 @@ private:
uint64_t particlePreparedRangeConfigRevision = 0;
bool particlePreparedBottomPresent = false;
bool particlePreparedTopPresent = false;
bool particlePreparedContinuousUnsupported = false;
Size particlePreparedBackendSize;
bool particlePreparedSceneSupported = false;
ParticleFastPathMissReason particleFastPathLastReason = ParticleFastPathMissReason::NoPreparedTree;

View File

@@ -134,11 +134,14 @@ void RetainedUnderlayCache::synchronizeRuntimeState(
valid = false;
lastInvalidationReason = "runtime-disabled";
runtimeState->recordRangeInvalidation(role == RetainedRangeRole::Top);
runtimeState->recordRangeInvalidationReason(
role == RetainedRangeRole::Top, RetainedUnderlayReason::RuntimeDisabled);
}
}
releaseResources();
if (!runtimeState->isEnabled()) {
lastInvalidationReason = "runtime-disabled";
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Fallback);
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayInvalidation);
}
}
@@ -155,12 +158,14 @@ RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<Ret
invalidate(fallbackReason ? fallbackReason : "no-boundary");
releaseResources();
}
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Disabled);
return RetainedUnderlayDecision::Disabled;
}
if (runtimeState && !runtimeState->isEnabled()) {
++fallbackCount;
runtimeState->recordFallback();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Fallback);
return RetainedUnderlayDecision::Fallback;
}
if (!supported) {
@@ -169,6 +174,7 @@ RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<Ret
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
invalidate(fallbackReason ? fallbackReason : "unsupported");
if (revisionChanged) releaseResources();
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Fallback);
return RetainedUnderlayDecision::Fallback;
}
if (revisionChanged) {
@@ -193,8 +199,10 @@ RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<Ret
runtimeState->recordReuse();
runtimeState->recordRangeReuse(role == RetainedRangeRole::Top);
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayReuse);
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Reuse);
return RetainedUnderlayDecision::Reuse;
}
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::BuildPending);
return RetainedUnderlayDecision::Build;
}
@@ -205,6 +213,7 @@ bool RetainedUnderlayCache::prepareResources(gfx::Context& context,
if (!offscreenTexture || offscreenTexture->getSize() != nextKey.viewport) {
offscreenTexture = context.createOffscreenTexture(
nextKey.viewport, gfx::TextureChannelDataType::UnsignedByte, true, true);
runtimeState->recordRangeTextureCreate(role == RetainedRangeRole::Top);
compositeLayerGroup.reset();
}
if (!offscreenTexture || !offscreenTexture->isRenderable()) return false;
@@ -248,6 +257,7 @@ bool RetainedUnderlayCache::rebuildComposite(gfx::Context& context, gfx::ShaderR
builder->flush(context);
for (auto& drawable : builder->clearDrawables()) group->addDrawable(std::move(drawable));
compositeLayerGroup = std::move(group);
runtimeState->recordRangeCompositeCreate(role == RetainedRangeRole::Top);
return !compositeLayerGroup->empty();
}
@@ -285,6 +295,7 @@ void RetainedUnderlayCache::markBuildSucceeded() {
++buildCount;
runtimeState->recordBuild();
runtimeState->recordRangeBuild(role == RetainedRangeRole::Top);
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Ready);
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayBuild);
}
@@ -293,6 +304,7 @@ void RetainedUnderlayCache::markBuildFailed(const char* reason) {
runtimeState->recordFallback();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
invalidate(reason);
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Fallback);
releaseResources();
}
@@ -301,10 +313,14 @@ void RetainedUnderlayCache::invalidate(const char* reason) {
lastInvalidationReason = reason ? reason : "unknown";
runtimeState->recordInvalidation(retainedReason(reason));
runtimeState->recordRangeInvalidation(role == RetainedRangeRole::Top);
runtimeState->recordRangeInvalidationReason(role == RetainedRangeRole::Top, retainedReason(reason));
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayInvalidation);
}
void RetainedUnderlayCache::releaseResources() {
if (offscreenTexture || compositeLayerGroup) {
runtimeState->recordRangeResourceRelease(role == RetainedRangeRole::Top);
}
valid = false;
buildRevision = 0;
key.reset();

View File

@@ -140,6 +140,11 @@ public:
uint64_t getBuildRevision() const noexcept { return buildRevision; }
const std::string& getLastInvalidationReason() const noexcept { return lastInvalidationReason; }
RetainedUnderlayDiagnostics getDiagnostics() const { return runtimeState->snapshot(); }
void recordFastPathEligibility(bool eligible) { runtimeState->recordFastPathEligibility(eligible); }
void recordFastPathDecision(bool active, ParticleFastPathMissReason reason) {
runtimeState->recordFastPathDecision(active, reason);
}
void recordFullCpuFrame() { runtimeState->recordFullCpuFrame(); }
private:
bool rebuildComposite(gfx::Context&, gfx::ShaderRegistry&);

View File

@@ -1,5 +1,6 @@
#pragma once
#include <mbgl/renderer/particle_fast_path.hpp>
#include <mbgl/renderer/retained_underlay.hpp>
#include <atomic>
@@ -8,6 +9,19 @@
namespace mbgl {
enum class RetainedRangeCacheState : uint8_t { Disabled, BuildPending, Ready, Reuse, Fallback };
inline const char* retainedRangeCacheStateText(RetainedRangeCacheState state) noexcept {
switch (state) {
case RetainedRangeCacheState::Disabled: return "disabled";
case RetainedRangeCacheState::BuildPending: return "build-pending";
case RetainedRangeCacheState::Ready: return "ready";
case RetainedRangeCacheState::Reuse: return "reuse";
case RetainedRangeCacheState::Fallback: return "fallback";
}
return "disabled";
}
enum class RetainedUnderlayReason : uint8_t {
None,
RuntimeDisabled,
@@ -152,6 +166,8 @@ public:
result.endLayerId = rangeResolution.endLayerId;
result.startIndex = rangeResolution.startIndex;
result.endIndex = rangeResolution.endIndex;
result.continuousBegin = rangeResolution.begin();
result.continuousEndExclusive = rangeResolution.endExclusive();
result.bottomPresent = bottomRangePresent;
result.topPresent = topRangePresent;
result.rangeFallback = rangeResolution.configured && !rangeResolution.valid;
@@ -159,6 +175,25 @@ public:
result.lastReason = rangeResolution.reason;
}
}
result.bottomCacheState = retainedRangeCacheStateText(bottomCacheState.load(std::memory_order_acquire));
result.bottomLastInvalidationReason =
retainedUnderlayReasonText(bottomLastInvalidationReason.load(std::memory_order_acquire));
result.bottomTextureCreate = bottomTextureCreate.load(std::memory_order_relaxed);
result.bottomCompositeCreate = bottomCompositeCreate.load(std::memory_order_relaxed);
result.bottomResourceRelease = bottomResourceRelease.load(std::memory_order_relaxed);
result.topCacheState = retainedRangeCacheStateText(topCacheState.load(std::memory_order_acquire));
result.topLastInvalidationReason =
retainedUnderlayReasonText(topLastInvalidationReason.load(std::memory_order_acquire));
result.topTextureCreate = topTextureCreate.load(std::memory_order_relaxed);
result.topCompositeCreate = topCompositeCreate.load(std::memory_order_relaxed);
result.topResourceRelease = topResourceRelease.load(std::memory_order_relaxed);
result.fastPathEligible = fastPathEligible.load(std::memory_order_acquire);
result.fastPathActive = fastPathActive.load(std::memory_order_acquire);
result.fastPathFallbackReason =
particleFastPathMissReasonText(fastPathReason.load(std::memory_order_acquire));
result.fastPathHit = fastPathHit.load(std::memory_order_relaxed);
result.fastPathMiss = fastPathMiss.load(std::memory_order_relaxed);
result.fullCpuFrame = fullCpuFrame.load(std::memory_order_relaxed);
return result;
}
@@ -178,6 +213,33 @@ public:
void recordRangeInvalidation(bool top) noexcept {
(top ? topInvalidation : bottomInvalidation).fetch_add(1, std::memory_order_relaxed);
}
void recordRangeCacheState(bool top, RetainedRangeCacheState state) noexcept {
(top ? topCacheState : bottomCacheState).store(state, std::memory_order_release);
}
void recordRangeInvalidationReason(bool top, RetainedUnderlayReason reason) noexcept {
(top ? topLastInvalidationReason : bottomLastInvalidationReason).store(reason, std::memory_order_release);
}
void recordRangeTextureCreate(bool top) noexcept {
(top ? topTextureCreate : bottomTextureCreate).fetch_add(1, std::memory_order_relaxed);
}
void recordRangeCompositeCreate(bool top) noexcept {
(top ? topCompositeCreate : bottomCompositeCreate).fetch_add(1, std::memory_order_relaxed);
}
void recordRangeResourceRelease(bool top) noexcept {
(top ? topResourceRelease : bottomResourceRelease).fetch_add(1, std::memory_order_relaxed);
}
void recordFastPathEligibility(bool eligible) noexcept {
fastPathEligible.store(eligible, std::memory_order_release);
}
void recordFastPathDecision(bool active, ParticleFastPathMissReason reason) noexcept {
fastPathActive.store(active, std::memory_order_release);
fastPathReason.store(reason, std::memory_order_release);
(active ? fastPathHit : fastPathMiss).fetch_add(1, std::memory_order_relaxed);
}
void recordFullCpuFrame() noexcept {
fullCpuFrame.fetch_add(1, std::memory_order_relaxed);
fastPathActive.store(false, std::memory_order_release);
}
private:
std::atomic<bool> enabled{true};
std::atomic<uint64_t> controlRevision{0};
@@ -192,6 +254,22 @@ private:
std::atomic<uint64_t> topBuild{0};
std::atomic<uint64_t> topReuse{0};
std::atomic<uint64_t> topInvalidation{0};
std::atomic<RetainedRangeCacheState> bottomCacheState{RetainedRangeCacheState::Disabled};
std::atomic<RetainedUnderlayReason> bottomLastInvalidationReason{RetainedUnderlayReason::None};
std::atomic<uint64_t> bottomTextureCreate{0};
std::atomic<uint64_t> bottomCompositeCreate{0};
std::atomic<uint64_t> bottomResourceRelease{0};
std::atomic<RetainedRangeCacheState> topCacheState{RetainedRangeCacheState::Disabled};
std::atomic<RetainedUnderlayReason> topLastInvalidationReason{RetainedUnderlayReason::None};
std::atomic<uint64_t> topTextureCreate{0};
std::atomic<uint64_t> topCompositeCreate{0};
std::atomic<uint64_t> topResourceRelease{0};
std::atomic<bool> fastPathEligible{false};
std::atomic<bool> fastPathActive{false};
std::atomic<ParticleFastPathMissReason> fastPathReason{ParticleFastPathMissReason::NoPreparedTree};
std::atomic<uint64_t> fastPathHit{0};
std::atomic<uint64_t> fastPathMiss{0};
std::atomic<uint64_t> fullCpuFrame{0};
mutable std::mutex rangeMutex;
ContinuousLayerRangeConfig rangeConfig;
ContinuousLayerRangeResolution rangeResolution;

View File

@@ -57,6 +57,7 @@ ParticleFastPathGateInput fastPathInput() {
.stillImageRequest = false,
.continuousMode = true,
.debugDisabled = true,
.unsupportedContinuousLayer = false,
.supportedScene = true,
.annotationMutation = false,
};
@@ -124,6 +125,46 @@ TEST(RetainedUnderlayCache, BuildReuseAndInvalidation) {
EXPECT_EQ(cache.getLastInvalidationReason(), "camera-or-boundary");
}
TEST(RetainedUnderlayCache, RuntimeValidationDiagnosticsExposeRangeResourcesAndFastPath) {
RetainedUnderlayRuntimeState state;
ASSERT_TRUE(state.setContinuousLayerRange("weather", "particle"));
const auto config = state.continuousLayerRangeConfig();
state.recordRangeResolution(
{.configured = true,
.valid = true,
.startLayerId = "weather",
.endLayerId = "particle",
.startIndex = 2,
.endIndex = 3,
.configRevision = config.revision,
.reason = "explicit-range"},
true,
true);
state.recordRangeCacheState(false, RetainedRangeCacheState::Reuse);
state.recordRangeInvalidationReason(false, RetainedUnderlayReason::Viewport);
state.recordRangeTextureCreate(false);
state.recordRangeCompositeCreate(false);
state.recordRangeResourceRelease(false);
state.recordFastPathEligibility(true);
state.recordFastPathDecision(true, ParticleFastPathMissReason::None);
state.recordFullCpuFrame();
const auto snapshot = state.snapshot();
EXPECT_TRUE(snapshot.rangeValid);
EXPECT_EQ(snapshot.continuousBegin, 2);
EXPECT_EQ(snapshot.continuousEndExclusive, 4);
EXPECT_EQ(snapshot.bottomCacheState, "reuse");
EXPECT_EQ(snapshot.bottomLastInvalidationReason, "viewport");
EXPECT_EQ(snapshot.bottomTextureCreate, 1u);
EXPECT_EQ(snapshot.bottomCompositeCreate, 1u);
EXPECT_EQ(snapshot.bottomResourceRelease, 1u);
EXPECT_TRUE(snapshot.fastPathEligible);
EXPECT_FALSE(snapshot.fastPathActive);
EXPECT_EQ(snapshot.fastPathHit, 1u);
EXPECT_EQ(snapshot.fastPathMiss, 0u);
EXPECT_EQ(snapshot.fullCpuFrame, 1u);
}
TEST(RetainedUnderlayRevision, MonotonicAcrossCameraViewportStyleAndSourceChanges) {
RetainedUnderlayRevisionTracker tracker;
RetainedUnderlayCache cache;
@@ -556,6 +597,10 @@ TEST(ParticleFastPath, PendingStillAndUnsupportedPathsReject) {
input = fastPathInput();
input.supportedScene = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::UnsupportedScene);
input = fastPathInput();
input.unsupportedContinuousLayer = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::UnsupportedContinuousLayer);
}
TEST(ParticleFastPath, FullFrameCanRestabilizeCandidate) {