From 8a5214d92dd2f8899db76d2194da42999fa0c87d Mon Sep 17 00:00:00 2001 From: tei Date: Tue, 6 Oct 2026 15:15:12 +0900 Subject: [PATCH] diagnostics: add STEP-099 render source readiness audit --- include/mbgl/renderer/render_segment.hpp | 54 ++++ packages/maplibre-particle-rn/src/index.ts | 2 + packages/maplibre-particle-rn/src/types.ts | 37 +++ .../ParticleCoreLayerValidationActivity.kt | 253 +++++++++++++++++- .../particle/Step099ReadinessFixture.kt | 141 ++++++++++ src/mbgl/renderer/render_orchestrator.cpp | 169 +++++++++++- src/mbgl/renderer/render_orchestrator.hpp | 22 +- src/mbgl/renderer/render_segment_json.hpp | 53 ++++ src/mbgl/renderer/render_source.hpp | 21 ++ .../retained_underlay_runtime_state.hpp | 45 ++++ .../renderer/sources/render_tile_source.cpp | 17 ++ .../renderer/sources/render_tile_source.hpp | 1 + .../renderer/retained_underlay_cache.test.cpp | 109 ++++++++ 13 files changed, 915 insertions(+), 9 deletions(-) create mode 100644 platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step099ReadinessFixture.kt diff --git a/include/mbgl/renderer/render_segment.hpp b/include/mbgl/renderer/render_segment.hpp index 329a3855d..8015ccb8b 100644 --- a/include/mbgl/renderer/render_segment.hpp +++ b/include/mbgl/renderer/render_segment.hpp @@ -38,6 +38,50 @@ struct StyleLayerDiagnostics { bool operator==(const StyleLayerDiagnostics&) const = default; }; +struct RenderSourceReadinessDiagnostics { + std::string sourceId; + std::string sourceType; + bool enabled = false; + bool isLoaded = false; + bool tileBacked = false; + std::optional tileCount; + std::optional incompleteTileCount; + std::optional loadedTileCount; + bool hasFadingTiles = false; + uint64_t lastSourceMutationSequence = 0; + uint64_t lastTileChangedSequence = 0; + uint64_t lastTileActionSequence = 0; + std::string lastTileAction{"none"}; + std::string lastMutationReason{"none"}; + + bool operator==(const RenderSourceReadinessDiagnostics&) const = default; +}; + +struct RenderMutationDiagnostics { + uint64_t sequence = 0; + uint64_t contentRevision = 0; + std::string eventType{"none"}; + std::string reason{"none"}; + std::optional sourceId; + std::vector affectedSegmentIndices; + std::string dependencyScope{"none"}; + + bool operator==(const RenderMutationDiagnostics&) const = default; +}; + +struct RendererReadinessDiagnostics { + bool styleLoaded = false; + bool imageManagerLoaded = false; + bool renderOrchestratorLoaded = false; + bool spriteLoaded = false; + bool hasFadingTiles = false; + uint64_t contentRevision = 0; + std::vector sources; + RenderMutationDiagnostics lastMutation; + + bool operator==(const RendererReadinessDiagnostics&) const = default; +}; + struct RenderSegmentCacheDiagnostics { std::string cacheState{"disabled"}; uint64_t contentRevision = 0; @@ -59,6 +103,8 @@ struct RenderSegmentDiagnosticsEntry { std::optional resolvedStartLayerId; std::optional resolvedEndLayerId; std::vector layers; + std::vector dependentSourceIds; + std::vector unknownDependencies; std::optional cache; }; @@ -83,6 +129,14 @@ struct RenderSegmentDiagnostics { uint64_t fastPathHit = 0; uint64_t fastPathMiss = 0; uint64_t fullCpuFrame = 0; + bool styleLoaded = false; + bool imageManagerLoaded = false; + bool renderOrchestratorLoaded = false; + bool spriteLoaded = false; + bool hasFadingTiles = false; + uint64_t contentRevision = 0; + std::vector sources; + RenderMutationDiagnostics lastMutation; std::vector styleLayers; std::vector segments; }; diff --git a/packages/maplibre-particle-rn/src/index.ts b/packages/maplibre-particle-rn/src/index.ts index f305c7285..08bafbcb6 100644 --- a/packages/maplibre-particle-rn/src/index.ts +++ b/packages/maplibre-particle-rn/src/index.ts @@ -19,6 +19,8 @@ export type { RenderSegmentDefinition, RenderSegmentDiagnostics, RenderSegmentLayerDiagnostics, + RenderSourceReadinessDiagnostics, + RenderMutationDiagnostics, RenderSegmentPlan, RenderSegmentStrategy, RetainedRenderSegmentDiagnostics, diff --git a/packages/maplibre-particle-rn/src/types.ts b/packages/maplibre-particle-rn/src/types.ts index c664ebe43..d34f3c14d 100644 --- a/packages/maplibre-particle-rn/src/types.ts +++ b/packages/maplibre-particle-rn/src/types.ts @@ -89,6 +89,8 @@ export type RetainedRenderSegmentDiagnostics = Readonly<{ resolvedEndLayerId: string | null; layerCount: number; layers: readonly RenderSegmentLayerDiagnostics[]; + dependentSourceIds: readonly string[]; + unknownDependencies: readonly string[]; cache: Readonly<{ cacheState: string; contentRevision: number; @@ -111,8 +113,35 @@ export type ContinuousRenderSegmentDiagnostics = Readonly<{ resolvedEndLayerId: string | null; layerCount: number; layers: readonly RenderSegmentLayerDiagnostics[]; + dependentSourceIds: readonly string[]; + unknownDependencies: readonly string[]; cache: null; }>; +export type RenderSourceReadinessDiagnostics = Readonly<{ + sourceId: string; + sourceType: string; + enabled: boolean; + isLoaded: boolean; + tileBacked: boolean; + tileCount: number | null; + incompleteTileCount: number | null; + loadedTileCount: number | null; + hasFadingTiles: boolean; + lastSourceMutationSequence: number; + lastTileChangedSequence: number; + lastTileActionSequence: number; + lastTileAction: string; + lastMutationReason: string; +}>; +export type RenderMutationDiagnostics = Readonly<{ + sequence: number; + contentRevision: number; + eventType: string; + reason: string; + sourceId: string | null; + affectedSegmentIndices: readonly number[]; + dependencyScope: string; +}>; export type RenderSegmentDiagnostics = Readonly<{ schemaVersion: 1; enabled: boolean; @@ -134,6 +163,14 @@ export type RenderSegmentDiagnostics = Readonly<{ fastPathHit: number; fastPathMiss: number; fullCpuFrame: number; + styleLoaded: boolean; + imageManagerLoaded: boolean; + renderOrchestratorLoaded: boolean; + spriteLoaded: boolean; + hasFadingTiles: boolean; + contentRevision: number; + sources: readonly RenderSourceReadinessDiagnostics[]; + lastMutation: RenderMutationDiagnostics; styleLayers: readonly RenderSegmentLayerDiagnostics[]; segments: readonly (RetainedRenderSegmentDiagnostics | ContinuousRenderSegmentDiagnostics)[]; }>; diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index 01db93bde..b920af616 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -4,6 +4,7 @@ import android.graphics.Color import android.os.Bundle import android.os.Handler import android.os.Looper +import android.os.SystemClock import android.text.InputType import android.util.Log import android.widget.Button @@ -23,7 +24,10 @@ import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.Style import org.maplibre.android.maps.WeatherRuntime import org.maplibre.android.style.layers.PaintPropertyValue +import org.maplibre.android.style.sources.GeoJsonSource import org.maplibre.android.testapp.R +import org.maplibre.geojson.Feature +import org.maplibre.geojson.Point /** Android OpenGL host for the formal raster-array to raster-particle fixture path. */ class ParticleCoreLayerValidationActivity : AppCompatActivity() { @@ -57,6 +61,75 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var retainedCacheEnabled = true private var step093Mode = false private var step097Topology: Step097StencilFixture.Topology? = null + private var step099Scenario: Step099ReadinessFixture.Scenario? = null + private var step099Tick = 0L + private var step099LastOverallLoaded: Boolean? = null + private var step099FalseSinceMs: Long? = null + private var step099FalseWindowCount = 0L + private var step099FalseWindowTotalMs = 0L + private var step099FalseWindowMaxMs = 0L + private var step099Running = false + private val step099Handler = Handler(Looper.getMainLooper()) + private val step099ReadinessTransitionPoll = object : Runnable { + override fun run() { + if (step099Running && step099Scenario != null && this@ParticleCoreLayerValidationActivity::maplibreMap.isInitialized && styleReady) { + val now = SystemClock.elapsedRealtime() + val snapshot = JSONObject(maplibreMap.getRenderSegmentDiagnostics()) + val loaded = snapshot.optBoolean("renderOrchestratorLoaded") + val previous = step099LastOverallLoaded + if (previous == null || previous != loaded) { + var falseWindowMs: Long? = null + if (!loaded) { + step099FalseSinceMs = now + } else { + falseWindowMs = step099FalseSinceMs?.let { start -> + (now - start).also { duration -> + step099FalseWindowCount += 1 + step099FalseWindowTotalMs += duration + step099FalseWindowMaxMs = maxOf(step099FalseWindowMaxMs, duration) + } + } + step099FalseSinceMs = null + } + Log.i( + TAG, + "STEP099_LOADED_TRANSITION elapsedMs=${now} from=${previous} to=${loaded} " + + "falseWindowMs=${falseWindowMs ?: -1} source=${snapshot.optJSONObject("lastMutation")?.optString("sourceId")}" + ) + step099LastOverallLoaded = loaded + } + } + if (step099Running) step099Handler.postDelayed(this, STEP099_STATE_SAMPLE_INTERVAL_MS) + } + } + private val step099DiagnosticsPoll = object : Runnable { + override fun run() { + val scenario = step099Scenario ?: return + if (!step099Running) return + if (this@ParticleCoreLayerValidationActivity::maplibreMap.isInitialized && styleReady) { + step099Tick += 1 + when (scenario) { + Step099ReadinessFixture.Scenario.T2_GEOJSON_1HZ -> { + val sourceId = if (step099Tick % 2L == 0L) { + Step099ReadinessFixture.DYNAMIC_SOURCE + } else { + Step099ReadinessFixture.DYNAMIC_SOURCE_B + } + updateStep099GeoJson(sourceId, step099Tick) + if (step099Tick % 2L == 0L) refreshStep099Diagnostics(log = true) + } + Step099ReadinessFixture.Scenario.T4_RCR -> { + if (step099Tick % 2L == 0L) { + updateStep099GeoJson(Step099ReadinessFixture.DYNAMIC_SOURCE, step099Tick) + refreshStep099Diagnostics(log = true) + } + } + else -> if (step099Tick % 2L == 0L) refreshStep099Diagnostics(log = true) + } + } + if (step099Running) step099Handler.postDelayed(this, STEP099_TICK_INTERVAL_MS) + } + } private var step093Topology = Step093Topology.BOTTOM_CONTINUOUS_TOP private var step093Variant = Step093Variant.DEFAULT private var step093StartVisible = true @@ -454,6 +527,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { menu.add(0, 14, 14, "READ DIAGNOSTICS") menu.add(0, 15, 15, "RESET DEFAULT A") menu.add(0, 16, 16, "STEP-097 SAME SOURCE STENCIL") + menu.add(0, 17, 17, "STEP-099 READINESS") setOnMenuItemClickListener { item -> when (item.itemId) { in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId]) @@ -464,6 +538,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { 14 -> refreshStep093Diagnostics(log = true) 15 -> applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP) 16 -> showStep097StencilMenu(anchor) + 17 -> showStep099Menu(anchor) } true } @@ -491,7 +566,145 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } } + private fun showStep099Menu(anchor: android.view.View) { + PopupMenu(this, anchor).apply { + Step099ReadinessFixture.Scenario.values().forEachIndexed { index, scenario -> + menu.add(0, index, index, scenario.label) + } + menu.add(0, 10, 10, "STOP / RETURN TO DEFAULT") + setOnMenuItemClickListener { item -> + if (item.itemId in Step099ReadinessFixture.Scenario.values().indices) { + applyStep099Scenario(Step099ReadinessFixture.Scenario.values()[item.itemId]) + } else if (item.itemId == 10) { + stopStep099Scenario() + loadValidationStyle(active, enabled, "STEP099_EXIT") + } + true + } + show() + } + } + + private fun applyStep099Scenario(scenario: Step099ReadinessFixture.Scenario) { + stopStep093DiagnosticsPolling() + step093Mode = false + step097Topology = null + step099Scenario = scenario + step099Running = true + step099Tick = 0L + step099LastOverallLoaded = null + step099FalseSinceMs = null + step099FalseWindowCount = 0L + step099FalseWindowTotalMs = 0L + step099FalseWindowMaxMs = 0L + timeWindowMode = false + productionFeedMode = false + hostSubmissionMode = false + realWeatherMode = false + weatherRuntime?.dispose() + weatherRuntime = null + step099Handler.removeCallbacksAndMessages(null) + val accepted = maplibreMap.setRenderSegmentPlan(Step099ReadinessFixture.planJson(scenario)) + styleReady = false + val epoch = ++styleLoadEpoch + status.text = "STEP-099 · ${scenario.label} · LOADING" + Log.i(TAG, "STEP099_SCENARIO_START scenario=${scenario.name} planAccepted=$accepted") + maplibreMap.setStyle(Style.Builder().fromJson(Step099ReadinessFixture.styleJson(scenario))) { + if (!step099Running || step099Scenario != scenario || epoch != styleLoadEpoch) return@setStyle + styleReady = true + status.text = "STEP-099 · ${scenario.label}" + refreshStep099Diagnostics(log = true) + step099Handler.post(step099ReadinessTransitionPoll) + step099Handler.post(step099DiagnosticsPoll) + maplibreMap.moveCamera(CameraUpdateFactory.newCameraPosition( + CameraPosition.Builder().target(LatLng(28.0, 135.0)).zoom(3.0).build() + )) + } + } + + private fun stopStep099Scenario() { + step099Running = false + step099Handler.removeCallbacksAndMessages(null) + val previous = step099Scenario + step099Scenario = null + if (previous != null) { + val average = if (step099FalseWindowCount == 0L) 0 else step099FalseWindowTotalMs / step099FalseWindowCount + val open = step099FalseSinceMs?.let { SystemClock.elapsedRealtime() - it } ?: 0L + Log.i(TAG, "STEP099_AGGREGATE scenario=${previous.name} falseWindowCount=$step099FalseWindowCount " + + "falseWindowAvgMs=$average falseWindowMaxMs=$step099FalseWindowMaxMs openFalseWindowMs=$open") + } + step099LastOverallLoaded = null + step099FalseSinceMs = null + } + + private fun updateStep099GeoJson(sourceId: String, tick: Long) { + val source = maplibreMap.style?.getSourceAs(sourceId) ?: return + val offset = (tick % 20L) * 0.12 + val longitude = if (sourceId == Step099ReadinessFixture.DYNAMIC_SOURCE) 135.0 + offset else 136.0 - offset + val latitude = if (sourceId == Step099ReadinessFixture.DYNAMIC_SOURCE) 28.0 + offset * 0.3 else 29.0 - offset * 0.3 + source.setGeoJson(Feature.fromGeometry(Point.fromLngLat(longitude, latitude))) + Log.i(TAG, "STEP099_SOURCE_UPDATE scenario=${step099Scenario?.name} sourceId=$sourceId tick=$tick") + } + + private fun refreshStep099Diagnostics(log: Boolean) { + if (!this::maplibreMap.isInitialized || !styleReady) return + val snapshot = JSONObject(maplibreMap.getRenderSegmentDiagnostics()) + val sources = snapshot.optJSONArray("sources") + val sourceSummary = buildList { + for (index in 0 until (sources?.length() ?: 0)) { + val source = sources?.optJSONObject(index) ?: continue + add("${source.optString("sourceId")}:${source.optString("sourceType")}:" + + "${source.optBoolean("enabled")}/${source.optBoolean("isLoaded")}" + + " tiles=${source.opt("tileCount")}/${source.opt("incompleteTileCount")}/${source.opt("loadedTileCount")}" + + " fade=${source.optBoolean("hasFadingTiles")}") + } + } + val segments = snapshot.optJSONArray("segments") + val segmentSummary = buildList { + for (index in 0 until (segments?.length() ?: 0)) { + val segment = segments?.optJSONObject(index) ?: continue + val dependencies = segment.optJSONArray("dependentSourceIds") + val dependencyText = buildList { + for (dependencyIndex in 0 until (dependencies?.length() ?: 0)) { + add(dependencies?.optString(dependencyIndex).orEmpty()) + } + }.joinToString(",") + val cache = segment.optJSONObject("cache") + val cacheText = if (cache == null) { + "cache=-" + } else { + "cache=${cache.optString("cacheState")}@${cache.optLong("contentRevision")}" + + " b=${cache.optLong("build")} r=${cache.optLong("reuse")}" + + " i=${cache.optLong("invalidation")}/${cache.optString("lastInvalidationReason")}" + } + add("${segment.optInt("segmentIndex")}:${segment.optString("strategy")}" + + "[${segment.optInt("beginIndex")},${segment.optInt("endExclusive")})" + + " deps=[$dependencyText] $cacheText") + } + } + val mutation = snapshot.optJSONObject("lastMutation") + val affectedSegments = mutation?.optJSONArray("affectedSegmentIndices") + val affectedText = buildList { + for (index in 0 until (affectedSegments?.length() ?: 0)) { + add(affectedSegments?.optInt(index).toString()) + } + }.joinToString(",") + val message = "scenario=${step099Scenario?.name} overall=${snapshot.optBoolean("renderOrchestratorLoaded")} " + + "style=${snapshot.optBoolean("styleLoaded")} image=${snapshot.optBoolean("imageManagerLoaded")} " + + "sprite=${snapshot.optBoolean("spriteLoaded")} fade=${snapshot.optBoolean("hasFadingTiles")} " + + "rev=${snapshot.optLong("contentRevision")} sources=${sourceSummary.joinToString(";")} " + + "last=${mutation?.optLong("sequence")}/${mutation?.optString("sourceId")}/${mutation?.optString("reason")}" + + " affected=[$affectedText] scope=${mutation?.optString("dependencyScope")} " + + "fast=${snapshot.optBoolean("fastPathActive")}/${snapshot.optBoolean("fastPathEligible")}" + + "/${snapshot.optString("fastPathFallbackReason")}" + + " h=${snapshot.optLong("fastPathHit")} m=${snapshot.optLong("fastPathMiss")}" + + " full=${snapshot.optLong("fullCpuFrame")} segments=${segmentSummary.joinToString(";")}" + diagnostics.text = "STEP-099 $message" + if (log) Log.i(TAG, "STEP099_AGGREGATE_SAMPLE $message") + } + private fun applyStep097StencilScene(topology: Step097StencilFixture.Topology) { + stopStep099Scenario() leaveRealWeatherMode() step097Topology = topology retainedCacheEnabled = true @@ -630,6 +843,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { variant: Step093Variant = Step093Variant.DEFAULT, configureBeforeStyle: Boolean = false ) { + stopStep099Scenario() leaveRealWeatherMode() step093Mode = true step093Topology = topology @@ -720,6 +934,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { step093Handler.post(step093DiagnosticsPoll) } + private fun stopStep093DiagnosticsPolling() { + step093Handler.removeCallbacks(step093DiagnosticsPoll) + } + private fun refreshStep093Diagnostics(log: Boolean) { val snapshot = maplibreMap.retainedUnderlayDiagnostics val cacheCount = (if (snapshot.bottomPresent) 1 else 0) + (if (snapshot.topPresent) 1 else 0) @@ -1145,6 +1363,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") { + if (step099Scenario != null) stopStep099Scenario() step097V4GoldenReady = false if (pureParticleMode) { loadPureParticleStyle(active, enabled, reason) @@ -1904,11 +2123,37 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { """.trimIndent() } - override fun onStart() { super.onStart(); mapView.onStart(); if (step093Mode || step097Topology != null) startStep093DiagnosticsPolling() } + override fun onStart() { + super.onStart() + mapView.onStart() + if (step099Scenario != null) { + step099Running = true + step099LastOverallLoaded = null + step099FalseSinceMs = null + step099Handler.removeCallbacksAndMessages(null) + if (styleReady) { + step099Handler.post(step099ReadinessTransitionPoll) + step099Handler.post(step099DiagnosticsPoll) + } + } else if (step093Mode || step097Topology != null) startStep093DiagnosticsPolling() + } override fun onResume() { super.onResume(); mapView.onResume() } override fun onPause() { mapView.onPause(); super.onPause() } - 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 onStop() { + step093Handler.removeCallbacks(step093DiagnosticsPoll) + step099Running = false + step099Handler.removeCallbacksAndMessages(null) + mapView.onStop() + super.onStop() + } + override fun onDestroy() { + step093Handler.removeCallbacks(step093DiagnosticsPoll) + stopStep099Scenario() + weatherRuntime?.dispose() + weatherRuntime = null + mapView.onDestroy() + super.onDestroy() + } override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() } override fun onSaveInstanceState(outState: Bundle) { @@ -1978,6 +2223,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { 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 STEP099_STATE_SAMPLE_INTERVAL_MS = 200L + private const val STEP099_TICK_INTERVAL_MS = 500L 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" diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step099ReadinessFixture.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step099ReadinessFixture.kt new file mode 100644 index 000000000..52920b889 --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step099ReadinessFixture.kt @@ -0,0 +1,141 @@ +package org.maplibre.android.testapp.activity.particle + +import org.json.JSONArray +import org.json.JSONObject + +/** STEP-099 offline readiness/invalidation fixtures. Diagnostics only; no renderer semantics are changed. */ +internal object Step099ReadinessFixture { + enum class Scenario(val label: String) { + T1_STATIC("T1 STATIC"), + T2_GEOJSON_1HZ("T2 GEOJSON 1HZ"), + T3_PERMANENT_UNREADY("T3 PERMANENT UNREADY"), + T4_RCR("T4 R-C-R") + } + + const val PARTICLE_SOURCE = "particle-fixture" + const val STATIC_SOURCE = "step099-static" + const val DYNAMIC_SOURCE = "step099-dynamic" + const val DYNAMIC_SOURCE_B = "step099-dynamic-b" + const val UNREADY_SOURCE = "step099-unready-raster-array" + + const val STATIC_LAYER = "step099-static-fill" + const val WEATHER_LAYER = "step099-weather" + const val PARTICLE_LAYER = "step099-particle" + const val DYNAMIC_LAYER = "step099-dynamic-circle" + const val DYNAMIC_LAYER_B = "step099-dynamic-circle-b" + const val UPPER_STATIC_LAYER = "step099-static-upper-fill" + + fun styleJson(scenario: Scenario): String { + val sources = JSONObject() + .put(PARTICLE_SOURCE, JSONObject() + .put("type", "raster-array") + .put("fixture", "uniform-east") + .put("metadata", "asset://integration/particle-fixtures/uniform-east/metadata.json") + .put("field", "asset://integration/particle-fixtures/uniform-east/field.f32")) + .put(STATIC_SOURCE, geoJsonSource( + """{"type":"Polygon","coordinates":[[[112,0],[158,0],[158,56],[112,56],[112,0]]]}""" + )) + + if (scenario == Scenario.T2_GEOJSON_1HZ || scenario == Scenario.T4_RCR) { + sources.put(DYNAMIC_SOURCE, geoJsonSource( + """{"type":"Point","coordinates":[135.0,28.0]}""" + )) + } + if (scenario == Scenario.T2_GEOJSON_1HZ) { + sources.put(DYNAMIC_SOURCE_B, geoJsonSource( + """{"type":"Point","coordinates":[136.0,29.0]}""" + )) + } + if (scenario == Scenario.T3_PERMANENT_UNREADY) { + // A valid host-submission RasterArraySource with no submitted field is intentionally isLoaded=false. + // It has no layer on purpose: STEP-099 verifies whether a globally present but unrelated Source + // can still hold RenderOrchestrator::isLoaded() false. + sources.put(UNREADY_SOURCE, JSONObject() + .put("type", "raster-array") + .put("host-field-submission", true)) + } + + val layers = JSONArray() + .put(JSONObject() + .put("id", "step099-background") + .put("type", "background") + .put("paint", JSONObject().put("background-color", "#0b2942"))) + .put(JSONObject() + .put("id", STATIC_LAYER) + .put("type", "fill") + .put("source", STATIC_SOURCE) + .put("paint", JSONObject() + .put("fill-color", "#176b87") + .put("fill-opacity", 0.58))) + .put(JSONObject() + .put("id", WEATHER_LAYER) + .put("type", "weather-color") + .put("source", PARTICLE_SOURCE) + .put("paint", JSONObject().put("weather-color-opacity", 0.32))) + .put(JSONObject() + .put("id", PARTICLE_LAYER) + .put("type", "raster-particle") + .put("source", PARTICLE_SOURCE) + .put("paint", JSONObject() + .put("raster-particle-active", true) + .put("raster-particle-particle-count", 1024))) + + if (scenario == Scenario.T2_GEOJSON_1HZ || scenario == Scenario.T4_RCR) { + layers.put(JSONObject() + .put("id", DYNAMIC_LAYER) + .put("type", "circle") + .put("source", DYNAMIC_SOURCE) + .put("paint", JSONObject() + .put("circle-radius", 12) + .put("circle-color", "#ffcc00") + .put("circle-opacity", 0.95))) + } + if (scenario == Scenario.T2_GEOJSON_1HZ) { + layers.put(JSONObject() + .put("id", DYNAMIC_LAYER_B) + .put("type", "circle") + .put("source", DYNAMIC_SOURCE_B) + .put("paint", JSONObject() + .put("circle-radius", 10) + .put("circle-color", "#ff6600") + .put("circle-opacity", 0.90))) + } + layers.put(JSONObject() + .put("id", UPPER_STATIC_LAYER) + .put("type", "fill") + .put("source", STATIC_SOURCE) + .put("paint", JSONObject().put("fill-color", "#d3a640").put("fill-opacity", 0.38))) + + return JSONObject() + .put("version", 8) + .put("name", "STEP-099 Source Readiness Diagnostics") + .put("sources", sources) + .put("layers", layers) + .toString() + } + + fun planJson(scenario: Scenario): String { + val segments = JSONArray() + fun add(strategy: String, start: String? = null) { + val segment = JSONObject().put("strategy", strategy) + if (start != null) segment.put("startLayerId", start) + segments.put(segment) + } + + add("RETAINED") + add("CONTINUOUS", WEATHER_LAYER) + add("RETAINED", if (scenario == Scenario.T2_GEOJSON_1HZ || scenario == Scenario.T4_RCR) DYNAMIC_LAYER else UPPER_STATIC_LAYER) + + return JSONObject() + .put("schemaVersion", 1) + .put("segments", segments) + .toString() + } + + private fun geoJsonSource(geometryJson: String): JSONObject = JSONObject() + .put("type", "geojson") + .put("data", JSONObject() + .put("type", "Feature") + .put("properties", JSONObject()) + .put("geometry", JSONObject(geometryJson))) +} diff --git a/src/mbgl/renderer/render_orchestrator.cpp b/src/mbgl/renderer/render_orchestrator.cpp index cecaf7921..716cb4759 100644 --- a/src/mbgl/renderer/render_orchestrator.cpp +++ b/src/mbgl/renderer/render_orchestrator.cpp @@ -65,6 +65,50 @@ RendererObserver& nullObserver() { return observer; } +const char* sourceTypeText(style::SourceType type) noexcept { + switch (type) { + case style::SourceType::Vector: return "vector"; + case style::SourceType::Raster: return "raster"; + case style::SourceType::RasterDEM: return "raster-dem"; + case style::SourceType::GeoJSON: return "geojson"; + case style::SourceType::Video: return "video"; + case style::SourceType::Annotations: return "annotations"; + case style::SourceType::Image: return "image"; + case style::SourceType::CustomVector: return "customvector"; + case style::SourceType::RasterArray: return "raster-array"; + } + return "unknown"; +} + +const char* tileOperationText(TileOperation op) noexcept { + switch (op) { + case TileOperation::RequestedFromCache: return "RequestedFromCache"; + case TileOperation::RequestedFromNetwork: return "RequestedFromNetwork"; + case TileOperation::LoadFromNetwork: return "LoadFromNetwork"; + case TileOperation::LoadFromCache: return "LoadFromCache"; + case TileOperation::StartParse: return "StartParse"; + case TileOperation::EndParse: return "EndParse"; + case TileOperation::Error: return "Error"; + case TileOperation::Cancelled: return "Cancelled"; + case TileOperation::NullOp: return "NullOp"; + } + return "Unknown"; +} + +const char* readinessEventType(RetainedUnderlayReason reason) noexcept { + switch (reason) { + case RetainedUnderlayReason::SourceMutation: return "SourceMutation"; + case RetainedUnderlayReason::TileMutation: return "TileMutation"; + case RetainedUnderlayReason::ImageMutation: return "ImageMutation"; + case RetainedUnderlayReason::GlyphMutation: return "GlyphMutation"; + case RetainedUnderlayReason::StyleMutation: return "StyleMutation"; + case RetainedUnderlayReason::LayerMutation: + case RetainedUnderlayReason::LayerGroupMutation: return "LayerMutation"; + case RetainedUnderlayReason::ContextReset: return "ContextReset"; + default: return "OtherMutation"; + } +} + class RenderTreeImpl final : public RenderTree { public: RenderTreeImpl(std::unique_ptr parameters_, @@ -153,9 +197,89 @@ RenderOrchestrator::~RenderOrchestrator() { threadPool.waitForEmpty(); } -void RenderOrchestrator::advanceRetainedUnderlayContentRevision(RetainedUnderlayReason reason) noexcept { +void RenderOrchestrator::recordReadinessMutation(const char* eventType, + RetainedUnderlayReason reason, + const std::optional& sourceId, + const std::string& tileAction) noexcept { + try { + std::lock_guard lock(readinessDiagnosticsMutex); + const auto sequence = ++readinessEventSequence; + lastReadinessMutation.sequence = sequence; + lastReadinessMutation.contentRevision = retainedUnderlayContentRevision.load(std::memory_order_acquire); + lastReadinessMutation.eventType = eventType; + lastReadinessMutation.reason = retainedUnderlayReasonText(reason); + lastReadinessMutation.sourceId = sourceId; + lastReadinessMutation.affectedSegmentIndices.clear(); + lastReadinessMutation.dependencyScope = sourceId ? "unresolved-until-snapshot" : "shared/global/unknown"; + + if (sourceId) { + auto& state = sourceReadinessEvents[*sourceId]; + state.lastMutationReason = retainedUnderlayReasonText(reason); + if (reason == RetainedUnderlayReason::SourceMutation) state.lastSourceMutationSequence = sequence; + if (reason == RetainedUnderlayReason::TileMutation) state.lastTileChangedSequence = sequence; + if (!tileAction.empty()) { + state.lastTileActionSequence = sequence; + state.lastTileAction = tileAction; + } + } + } catch (...) { + // Diagnostic recording is best-effort and must not escape into renderer mutation paths. + } +} + +void RenderOrchestrator::advanceRetainedUnderlayContentRevision( + RetainedUnderlayReason reason, + const std::optional& sourceId) noexcept { retainedUnderlayContentReason.store(reason, std::memory_order_release); retainedUnderlayContentRevision.fetch_add(1, std::memory_order_acq_rel); + recordReadinessMutation(readinessEventType(reason), reason, sourceId); +} + +RendererReadinessDiagnostics RenderOrchestrator::buildReadinessDiagnostics(bool styleLoaded, + bool spriteLoaded) const { + RendererReadinessDiagnostics result; + result.styleLoaded = styleLoaded; + result.imageManagerLoaded = imageManager->isLoaded(); + result.spriteLoaded = spriteLoaded; + result.contentRevision = retainedUnderlayContentRevision.load(std::memory_order_acquire); + + std::unordered_map sourceEvents; + { + std::lock_guard lock(readinessDiagnosticsMutex); + // Snapshot-only: do not prune or otherwise mutate diagnostic history from a query path. + // Entries for sources no longer present are naturally ignored below because we enumerate + // the current renderSources set and only look up matching ids. + sourceEvents = sourceReadinessEvents; + result.lastMutation = lastReadinessMutation; + } + + result.renderOrchestratorLoaded = result.imageManagerLoaded; + result.sources.reserve(renderSources.size()); + for (const auto& [sourceId, renderSource] : renderSources) { + const auto stats = renderSource->getReadinessDiagnostics(); + RenderSourceReadinessDiagnostics source; + source.sourceId = sourceId; + source.sourceType = sourceTypeText(renderSource->baseImpl->type); + source.enabled = renderSource->isEnabled(); + source.isLoaded = renderSource->isLoaded(); + source.tileBacked = stats.tileBacked; + source.tileCount = stats.tileCount; + source.incompleteTileCount = stats.incompleteTileCount; + source.loadedTileCount = stats.loadedTileCount; + source.hasFadingTiles = stats.hasFadingTiles; + result.hasFadingTiles = result.hasFadingTiles || stats.hasFadingTiles; + result.renderOrchestratorLoaded = result.renderOrchestratorLoaded && source.isLoaded; + if (const auto it = sourceEvents.find(sourceId); it != sourceEvents.end()) { + source.lastSourceMutationSequence = it->second.lastSourceMutationSequence; + source.lastTileChangedSequence = it->second.lastTileChangedSequence; + source.lastTileActionSequence = it->second.lastTileActionSequence; + source.lastTileAction = it->second.lastTileAction; + source.lastMutationReason = it->second.lastMutationReason; + } + result.sources.push_back(std::move(source)); + } + std::ranges::sort(result.sources, {}, &RenderSourceReadinessDiagnostics::sourceId); + return result; } void RenderOrchestrator::markContextLost() { @@ -361,6 +485,15 @@ std::unique_ptr RenderOrchestrator::createRenderTree( const SourceDifference sourceDiff = diffSources(sourceImpls, updateParameters->sources); if (!sourceDiff.added.empty() || !sourceDiff.removed.empty() || !sourceDiff.changed.empty()) { advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::SourceMutation); + for (const auto& entry : sourceDiff.added) { + recordReadinessMutation("SourceMutation", RetainedUnderlayReason::SourceMutation, entry.first); + } + for (const auto& entry : sourceDiff.removed) { + recordReadinessMutation("SourceMutation", RetainedUnderlayReason::SourceMutation, entry.first); + } + for (const auto& entry : sourceDiff.changed) { + recordReadinessMutation("SourceMutation", RetainedUnderlayReason::SourceMutation, entry.first); + } } sourceImpls = updateParameters->sources; @@ -519,7 +652,17 @@ std::unique_ptr RenderOrchestrator::createRenderTree( addChanges(changes); } + // Readiness is sampled for diagnostics only. Preserve the renderer's formal load gate. renderTreeParameters->loaded = updateParameters->styleLoaded && isLoaded(); + if (updateParameters->retainedUnderlayState) { + try { + auto readinessDiagnostics = + buildReadinessDiagnostics(updateParameters->styleLoaded, updateParameters->spriteLoaded); + updateParameters->retainedUnderlayState->recordRendererReadiness(std::move(readinessDiagnostics)); + } catch (...) { + // A failed diagnostic snapshot must not affect the render tree or its formal loaded gate. + } + } if (!isMapModeContinuous && !renderTreeParameters->loaded) { return nullptr; } @@ -835,7 +978,7 @@ void RenderOrchestrator::setFeatureState(const std::string& sourceID, if (RenderSource* renderSource = getRenderSource(sourceID)) { renderSource->setFeatureState(sourceLayerID, featureID, state); - advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::SourceMutation); + advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::SourceMutation, sourceID); } } @@ -858,7 +1001,7 @@ void RenderOrchestrator::removeFeatureState(const std::string& sourceID, if (RenderSource* renderSource = getRenderSource(sourceID)) { renderSource->removeFeatureState(sourceLayerID, featureID, stateKey); - advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::SourceMutation); + advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::SourceMutation, sourceID); } } @@ -1080,6 +1223,13 @@ void RenderOrchestrator::clearData() { imageManager->clear(); glyphManager->evict(fontStacks(*layerImpls)); + try { + std::lock_guard lock(readinessDiagnosticsMutex); + sourceReadinessEvents.clear(); + lastReadinessMutation = {}; + } catch (...) { + // Keep renderer style cleanup independent from optional diagnostic history cleanup. + } advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::StyleMutation); } @@ -1295,10 +1445,10 @@ void RenderOrchestrator::onTileError(RenderSource& source, const OverscaledTileI observer->onResourceError(error); } -void RenderOrchestrator::onTileChanged(RenderSource&, const OverscaledTileID&) { +void RenderOrchestrator::onTileChanged(RenderSource& source, const OverscaledTileID&) { MLN_TRACE_FUNC(); - advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::TileMutation); + advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::TileMutation, source.baseImpl->id); observer->onInvalidate(); } @@ -1306,6 +1456,15 @@ void RenderOrchestrator::onTileAction(RenderSource&, TileOperation op, const OverscaledTileID& id, const std::string& sourceID) { + try { + std::lock_guard lock(readinessDiagnosticsMutex); + const auto sequence = ++readinessEventSequence; + auto& state = sourceReadinessEvents[sourceID]; + state.lastTileActionSequence = sequence; + state.lastTileAction = tileOperationText(op); + } catch (...) { + // Tile action observation is best-effort; always forward the original observer callback. + } observer->onTileAction(op, id, sourceID); } diff --git a/src/mbgl/renderer/render_orchestrator.hpp b/src/mbgl/renderer/render_orchestrator.hpp index 28b3395ba..505eb857a 100644 --- a/src/mbgl/renderer/render_orchestrator.hpp +++ b/src/mbgl/renderer/render_orchestrator.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -332,7 +333,14 @@ private: /// Move changes into the pending set, clearing the provided collection void addChanges(UniqueChangeRequestVec&); - void advanceRetainedUnderlayContentRevision(RetainedUnderlayReason) noexcept; + void advanceRetainedUnderlayContentRevision( + RetainedUnderlayReason, + const std::optional& sourceId = std::nullopt) noexcept; + void recordReadinessMutation(const char* eventType, + RetainedUnderlayReason reason, + const std::optional& sourceId = std::nullopt, + const std::string& tileAction = {}) noexcept; + RendererReadinessDiagnostics buildReadinessDiagnostics(bool styleLoaded, bool spriteLoaded) const; RendererObserver* observer; @@ -366,6 +374,18 @@ private: std::atomic retainedUnderlayContentRevision{1}; std::atomic retainedUnderlayContentReason{RetainedUnderlayReason::None}; + struct SourceReadinessEventState { + uint64_t lastSourceMutationSequence = 0; + uint64_t lastTileChangedSequence = 0; + uint64_t lastTileActionSequence = 0; + std::string lastTileAction{"none"}; + std::string lastMutationReason{"none"}; + }; + mutable std::mutex readinessDiagnosticsMutex; + uint64_t readinessEventSequence = 0; + std::unordered_map sourceReadinessEvents; + RenderMutationDiagnostics lastReadinessMutation; + #if MLN_RENDER_BACKEND_OPENGL bool androidGoldfishMitigationEnabled{false}; #endif diff --git a/src/mbgl/renderer/render_segment_json.hpp b/src/mbgl/renderer/render_segment_json.hpp index 553b4c980..1e8bffa21 100644 --- a/src/mbgl/renderer/render_segment_json.hpp +++ b/src/mbgl/renderer/render_segment_json.hpp @@ -81,6 +81,20 @@ inline void writeOptionalString(Writer& writer, const char* key, const std::opti if (value) writer.String(value->c_str()); else writer.Null(); } +template +inline void writeOptionalUint32(Writer& writer, const char* key, const std::optional& value) { + writer.Key(key); + if (value) writer.Uint(*value); else writer.Null(); +} + +template +inline void writeStringArray(Writer& writer, const char* key, const std::vector& values) { + writer.Key(key); + writer.StartArray(); + for (const auto& value : values) writer.String(value.c_str()); + writer.EndArray(); +} + template inline void writeStyleLayerDiagnostics(Writer& writer, const StyleLayerDiagnostics& layer) { writer.StartObject(); @@ -117,6 +131,43 @@ inline std::string renderSegmentDiagnosticsToJSON(const RenderSegmentDiagnostics writer.Key("fastPathHit"); writer.Uint64(snapshot.fastPathHit); writer.Key("fastPathMiss"); writer.Uint64(snapshot.fastPathMiss); writer.Key("fullCpuFrame"); writer.Uint64(snapshot.fullCpuFrame); + writer.Key("styleLoaded"); writer.Bool(snapshot.styleLoaded); + writer.Key("imageManagerLoaded"); writer.Bool(snapshot.imageManagerLoaded); + writer.Key("renderOrchestratorLoaded"); writer.Bool(snapshot.renderOrchestratorLoaded); + writer.Key("spriteLoaded"); writer.Bool(snapshot.spriteLoaded); + writer.Key("hasFadingTiles"); writer.Bool(snapshot.hasFadingTiles); + writer.Key("contentRevision"); writer.Uint64(snapshot.contentRevision); + writer.Key("sources"); writer.StartArray(); + for (const auto& source : snapshot.sources) { + writer.StartObject(); + writer.Key("sourceId"); writer.String(source.sourceId.c_str()); + writer.Key("sourceType"); writer.String(source.sourceType.c_str()); + writer.Key("enabled"); writer.Bool(source.enabled); + writer.Key("isLoaded"); writer.Bool(source.isLoaded); + writer.Key("tileBacked"); writer.Bool(source.tileBacked); + writeOptionalUint32(writer, "tileCount", source.tileCount); + writeOptionalUint32(writer, "incompleteTileCount", source.incompleteTileCount); + writeOptionalUint32(writer, "loadedTileCount", source.loadedTileCount); + writer.Key("hasFadingTiles"); writer.Bool(source.hasFadingTiles); + writer.Key("lastSourceMutationSequence"); writer.Uint64(source.lastSourceMutationSequence); + writer.Key("lastTileChangedSequence"); writer.Uint64(source.lastTileChangedSequence); + writer.Key("lastTileActionSequence"); writer.Uint64(source.lastTileActionSequence); + writer.Key("lastTileAction"); writer.String(source.lastTileAction.c_str()); + writer.Key("lastMutationReason"); writer.String(source.lastMutationReason.c_str()); + writer.EndObject(); + } + writer.EndArray(); + writer.Key("lastMutation"); writer.StartObject(); + writer.Key("sequence"); writer.Uint64(snapshot.lastMutation.sequence); + writer.Key("contentRevision"); writer.Uint64(snapshot.lastMutation.contentRevision); + writer.Key("eventType"); writer.String(snapshot.lastMutation.eventType.c_str()); + writer.Key("reason"); writer.String(snapshot.lastMutation.reason.c_str()); + writeOptionalString(writer, "sourceId", snapshot.lastMutation.sourceId); + writer.Key("affectedSegmentIndices"); writer.StartArray(); + for (const auto index : snapshot.lastMutation.affectedSegmentIndices) writer.Uint(index); + writer.EndArray(); + writer.Key("dependencyScope"); writer.String(snapshot.lastMutation.dependencyScope.c_str()); + writer.EndObject(); writer.Key("styleLayers"); writer.StartArray(); for (const auto& layer : snapshot.styleLayers) writeStyleLayerDiagnostics(writer, layer); writer.EndArray(); @@ -134,6 +185,8 @@ inline std::string renderSegmentDiagnosticsToJSON(const RenderSegmentDiagnostics writer.Key("layers"); writer.StartArray(); for (const auto& layer : segment.layers) writeStyleLayerDiagnostics(writer, layer); writer.EndArray(); + writeStringArray(writer, "dependentSourceIds", segment.dependentSourceIds); + writeStringArray(writer, "unknownDependencies", segment.unknownDependencies); writer.Key("cache"); if (!segment.cache) { writer.Null(); diff --git a/src/mbgl/renderer/render_source.hpp b/src/mbgl/renderer/render_source.hpp index 424161a43..097af3312 100644 --- a/src/mbgl/renderer/render_source.hpp +++ b/src/mbgl/renderer/render_source.hpp @@ -11,8 +11,10 @@ #include #include +#include #include #include +#include #include #include @@ -49,6 +51,14 @@ public: using RenderTiles = std::shared_ptr>>; +struct RenderSourceReadinessStats { + bool tileBacked = false; + std::optional tileCount; + std::optional incompleteTileCount; + std::optional loadedTileCount; + bool hasFadingTiles = false; +}; + class RenderSource : protected TileObserver { public: static std::unique_ptr create(const Immutable&, const TaggedScheduler&); @@ -68,6 +78,17 @@ public: virtual void prepare(const SourcePrepareParameters&) = 0; virtual void updateFadingTiles() = 0; virtual bool hasFadingTiles() const = 0; + // Read on the renderer thread while the source and its tile pyramid are owned by RenderOrchestrator. + // This is an observational summary and must not define or modify isLoaded(). + virtual RenderSourceReadinessStats getReadinessDiagnostics() const { + return { + .tileBacked = false, + .tileCount = std::nullopt, + .incompleteTileCount = std::nullopt, + .loadedTileCount = std::nullopt, + .hasFadingTiles = hasFadingTiles(), + }; + } // If supported, returns a shared list of RenderTiles, sorted by tile id and // excluding tiles hold for fade; returns nullptr otherwise. virtual RenderTiles getRenderTiles() const { return nullptr; } diff --git a/src/mbgl/renderer/retained_underlay_runtime_state.hpp b/src/mbgl/renderer/retained_underlay_runtime_state.hpp index 41785950c..408cc5618 100644 --- a/src/mbgl/renderer/retained_underlay_runtime_state.hpp +++ b/src/mbgl/renderer/retained_underlay_runtime_state.hpp @@ -5,8 +5,10 @@ #include #include +#include #include #include +#include #include #include @@ -224,6 +226,18 @@ public: entry.layers.assign(next.styleLayers.begin() + segment.beginIndex, next.styleLayers.begin() + segment.endExclusive); } + std::set dependentSourceIds; + std::set unknownDependencies; + for (const auto& layer : entry.layers) { + if (layer.sourceId) dependentSourceIds.insert(*layer.sourceId); + if (layer.layerType == "symbol") { + unknownDependencies.insert("symbol/glyph/image/placement"); + } else if (layer.layerType == "custom" || layer.layerType == "custom-drawable") { + unknownDependencies.insert("custom"); + } + } + entry.dependentSourceIds.assign(dependentSourceIds.begin(), dependentSourceIds.end()); + entry.unknownDependencies.assign(unknownDependencies.begin(), unknownDependencies.end()); if (segment.strategy == RenderSegmentStrategy::Retained) { ++next.retainedSegmentCount; entry.cache.emplace(); @@ -240,6 +254,37 @@ public: renderSegmentDiagnostics = std::move(next); } + void recordRendererReadiness(RendererReadinessDiagnostics readiness) { + std::lock_guard lock(rangeMutex); + renderSegmentDiagnostics.styleLoaded = readiness.styleLoaded; + renderSegmentDiagnostics.imageManagerLoaded = readiness.imageManagerLoaded; + renderSegmentDiagnostics.renderOrchestratorLoaded = readiness.renderOrchestratorLoaded; + renderSegmentDiagnostics.spriteLoaded = readiness.spriteLoaded; + renderSegmentDiagnostics.hasFadingTiles = readiness.hasFadingTiles; + renderSegmentDiagnostics.contentRevision = readiness.contentRevision; + renderSegmentDiagnostics.sources = std::move(readiness.sources); + + auto mutation = std::move(readiness.lastMutation); + mutation.affectedSegmentIndices.clear(); + if (mutation.sequence != 0) { + if (mutation.sourceId) { + for (const auto& segment : renderSegmentDiagnostics.segments) { + if (std::find(segment.dependentSourceIds.begin(), + segment.dependentSourceIds.end(), + *mutation.sourceId) != segment.dependentSourceIds.end()) { + mutation.affectedSegmentIndices.push_back(segment.segmentIndex); + } + } + mutation.dependencyScope = mutation.affectedSegmentIndices.empty() + ? "source-not-in-render-segments" + : "source-local-diagnostic"; + } else { + mutation.dependencyScope = "shared/global/unknown"; + } + } + renderSegmentDiagnostics.lastMutation = std::move(mutation); + } + RenderSegmentDiagnostics renderSegmentSnapshot() const { std::lock_guard lock(rangeMutex); auto result = renderSegmentDiagnostics; diff --git a/src/mbgl/renderer/sources/render_tile_source.cpp b/src/mbgl/renderer/sources/render_tile_source.cpp index 647031910..bcc248356 100644 --- a/src/mbgl/renderer/sources/render_tile_source.cpp +++ b/src/mbgl/renderer/sources/render_tile_source.cpp @@ -486,6 +486,23 @@ bool RenderTileSource::hasFadingTiles() const { return tilePyramid.hasFadingTiles(); } +RenderSourceReadinessStats RenderTileSource::getReadinessDiagnostics() const { + RenderSourceReadinessStats result; + result.tileBacked = true; + result.tileCount = static_cast(tilePyramid.getTiles().size()); + uint32_t incomplete = 0; + uint32_t loaded = 0; + for (const auto& entry : tilePyramid.getTiles()) { + const auto& tile = *entry.second; + if (!tile.isComplete()) ++incomplete; + if (tile.isLoaded()) ++loaded; + } + result.incompleteTileCount = incomplete; + result.loadedTileCount = loaded; + result.hasFadingTiles = tilePyramid.hasFadingTiles(); + return result; +} + RenderTiles RenderTileSource::getRenderTiles() const { if (!filteredRenderTiles) { auto result = std::make_shared>>(); diff --git a/src/mbgl/renderer/sources/render_tile_source.hpp b/src/mbgl/renderer/sources/render_tile_source.hpp index 619d4d5c5..bb92a057c 100644 --- a/src/mbgl/renderer/sources/render_tile_source.hpp +++ b/src/mbgl/renderer/sources/render_tile_source.hpp @@ -23,6 +23,7 @@ public: void prepare(const SourcePrepareParameters&) override; void updateFadingTiles() override; bool hasFadingTiles() const override; + RenderSourceReadinessStats getReadinessDiagnostics() const override; RenderTiles getRenderTiles() const override; RenderTiles getRenderTilesSortedByYPosition() const override; diff --git a/test/renderer/retained_underlay_cache.test.cpp b/test/renderer/retained_underlay_cache.test.cpp index 515f78b8b..f3b4fda75 100644 --- a/test/renderer/retained_underlay_cache.test.cpp +++ b/test/renderer/retained_underlay_cache.test.cpp @@ -934,6 +934,115 @@ TEST(RenderSegmentDiagnostics, SnapshotCoversStyleExactlyAndSerializesContinuous EXPECT_NE(json.find("\"cache\":null"), std::string::npos); } + +TEST(RenderSegmentDiagnostics, ReadinessSnapshotMapsSourcesToSegmentsWithoutSideEffects) { + RetainedUnderlayRuntimeState state; + RenderSegmentPlan configured; + configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt}, + {RenderSegmentStrategy::Continuous, "weather"}, + {RenderSegmentStrategy::Retained, "land"}}; + ASSERT_TRUE(state.setRenderSegmentPlan(configured)); + const auto resolved = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true); + + std::vector layers; + for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) { + layers.push_back({static_cast(i), segmentLayerIds()[i], "fill", true, std::nullopt, std::nullopt}); + } + layers[1].sourceId = "chart"; + layers[2].sourceId = "dynamic"; + layers[3].sourceId = "dynamic"; + layers[4].sourceId = "chart"; + layers[5].layerType = "symbol"; + layers[5].sourceId = "ais"; + state.recordRenderSegmentResolution(resolved, layers); + state.recordSegmentBuild(0); + state.recordSegmentCacheState(0, RetainedRangeCacheState::Ready, 41); + + RendererReadinessDiagnostics readiness; + readiness.styleLoaded = true; + readiness.imageManagerLoaded = true; + readiness.renderOrchestratorLoaded = false; + readiness.spriteLoaded = true; + readiness.hasFadingTiles = false; + readiness.contentRevision = 42; + readiness.sources = { + {.sourceId = "chart", + .sourceType = "vector", + .enabled = true, + .isLoaded = true, + .tileBacked = true, + .tileCount = 4, + .incompleteTileCount = 0, + .loadedTileCount = 4}, + {.sourceId = "dynamic", + .sourceType = "raster", + .enabled = true, + .isLoaded = false, + .tileBacked = true, + .tileCount = 1, + .incompleteTileCount = 1, + .loadedTileCount = 0, + .lastSourceMutationSequence = 11, + .lastTileChangedSequence = 12, + .lastTileActionSequence = 13, + .lastTileAction = "StartParse", + .lastMutationReason = "tile-mutation"}}; + readiness.lastMutation = {.sequence = 12, + .contentRevision = 42, + .eventType = "TileMutation", + .reason = "tile-mutation", + .sourceId = "dynamic"}; + state.recordRendererReadiness(readiness); + + const auto first = state.renderSegmentSnapshot(); + ASSERT_EQ(first.sources.size(), 2u); + EXPECT_TRUE(first.styleLoaded); + EXPECT_TRUE(first.imageManagerLoaded); + EXPECT_FALSE(first.renderOrchestratorLoaded); + EXPECT_EQ(first.contentRevision, 42u); + EXPECT_EQ(first.sources[0].sourceId, "chart"); + EXPECT_TRUE(first.sources[0].isLoaded); + EXPECT_EQ(first.sources[0].tileCount, std::optional{4}); + EXPECT_EQ(first.sources[1].sourceId, "dynamic"); + EXPECT_FALSE(first.sources[1].isLoaded); + EXPECT_EQ(first.sources[1].incompleteTileCount, std::optional{1}); + ASSERT_EQ(first.segments.size(), 3u); + EXPECT_EQ(first.segments[0].dependentSourceIds, std::vector({"chart"})); + EXPECT_EQ(first.segments[1].dependentSourceIds, std::vector({"dynamic"})); + EXPECT_EQ(first.segments[2].dependentSourceIds, std::vector({"ais", "chart"})); + EXPECT_TRUE(first.segments[0].unknownDependencies.empty()); + EXPECT_EQ(first.segments[2].unknownDependencies, + std::vector({"symbol/glyph/image/placement"})); + EXPECT_EQ(first.segments[0].dependentSourceIds.size(), 1u); + const auto backgroundLayer = std::find_if(first.segments[0].layers.begin(), first.segments[0].layers.end(), + [](const auto& layer) { return layer.layerId == "background"; }); + ASSERT_NE(backgroundLayer, first.segments[0].layers.end()); + EXPECT_FALSE(backgroundLayer->sourceId.has_value()); + EXPECT_EQ(first.lastMutation.affectedSegmentIndices, std::vector({1})); + EXPECT_EQ(first.lastMutation.dependencyScope, "source-local-diagnostic"); + + const auto topologyRevision = first.topologyRevision; + const auto cacheBuild = first.segments[0].cache->build; + state.recordRendererReadiness(readiness); + const auto second = state.renderSegmentSnapshot(); + EXPECT_EQ(second.topologyRevision, topologyRevision); + ASSERT_TRUE(second.segments[0].cache); + EXPECT_EQ(second.segments[0].cache->build, cacheBuild); + EXPECT_EQ(second.segments[0].cache->contentRevision, 41u); + const auto third = state.renderSegmentSnapshot(); + EXPECT_EQ(third.topologyRevision, second.topologyRevision); + EXPECT_EQ(third.contentRevision, second.contentRevision); + EXPECT_EQ(third.sources, second.sources); + EXPECT_EQ(third.lastMutation, second.lastMutation); + + const auto json = renderSegmentDiagnosticsToJSON(second); + EXPECT_EQ(renderSegmentDiagnosticsToJSON(state.renderSegmentSnapshot()), json); + EXPECT_NE(json.find("\"renderOrchestratorLoaded\":false"), std::string::npos); + EXPECT_NE(json.find("\"incompleteTileCount\":1"), std::string::npos); + EXPECT_NE(json.find("\"dependentSourceIds\":[\"dynamic\"]"), std::string::npos); + EXPECT_NE(json.find("\"affectedSegmentIndices\":[1]"), std::string::npos); +} + TEST(RenderSegmentPlan, StrictJSONRejectsUnknownFieldsStrategyAndInvalidFirstBoundary) { EXPECT_FALSE(parseRenderSegmentPlanJSON( R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED","unexpected":true}]})"));