diagnostics: add STEP-099 render source readiness audit

This commit is contained in:
tei
2026-10-06 15:15:12 +09:00
parent e8b451fe70
commit 8a5214d92d
13 changed files with 915 additions and 9 deletions

View File

@@ -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<uint32_t> tileCount;
std::optional<uint32_t> incompleteTileCount;
std::optional<uint32_t> 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<std::string> sourceId;
std::vector<uint32_t> 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<RenderSourceReadinessDiagnostics> 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<std::string> resolvedStartLayerId;
std::optional<std::string> resolvedEndLayerId;
std::vector<StyleLayerDiagnostics> layers;
std::vector<std::string> dependentSourceIds;
std::vector<std::string> unknownDependencies;
std::optional<RenderSegmentCacheDiagnostics> 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<RenderSourceReadinessDiagnostics> sources;
RenderMutationDiagnostics lastMutation;
std::vector<StyleLayerDiagnostics> styleLayers;
std::vector<RenderSegmentDiagnosticsEntry> segments;
};

View File

@@ -19,6 +19,8 @@ export type {
RenderSegmentDefinition,
RenderSegmentDiagnostics,
RenderSegmentLayerDiagnostics,
RenderSourceReadinessDiagnostics,
RenderMutationDiagnostics,
RenderSegmentPlan,
RenderSegmentStrategy,
RetainedRenderSegmentDiagnostics,

View File

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

View File

@@ -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<GeoJsonSource>(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"

View File

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

View File

@@ -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<RenderTreeParameters> 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<std::string>& sourceId,
const std::string& tileAction) noexcept {
try {
std::lock_guard<std::mutex> 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<std::string>& 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<std::string, SourceReadinessEventState> sourceEvents;
{
std::lock_guard<std::mutex> 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<RenderTree> 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<RenderTree> 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<std::mutex> 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<std::mutex> 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);
}

View File

@@ -20,6 +20,7 @@
#include <atomic>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
@@ -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<std::string>& sourceId = std::nullopt) noexcept;
void recordReadinessMutation(const char* eventType,
RetainedUnderlayReason reason,
const std::optional<std::string>& sourceId = std::nullopt,
const std::string& tileAction = {}) noexcept;
RendererReadinessDiagnostics buildReadinessDiagnostics(bool styleLoaded, bool spriteLoaded) const;
RendererObserver* observer;
@@ -366,6 +374,18 @@ private:
std::atomic<uint64_t> retainedUnderlayContentRevision{1};
std::atomic<RetainedUnderlayReason> 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<std::string, SourceReadinessEventState> sourceReadinessEvents;
RenderMutationDiagnostics lastReadinessMutation;
#if MLN_RENDER_BACKEND_OPENGL
bool androidGoldfishMitigationEnabled{false};
#endif

View File

@@ -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 <typename Writer>
inline void writeOptionalUint32(Writer& writer, const char* key, const std::optional<uint32_t>& value) {
writer.Key(key);
if (value) writer.Uint(*value); else writer.Null();
}
template <typename Writer>
inline void writeStringArray(Writer& writer, const char* key, const std::vector<std::string>& values) {
writer.Key(key);
writer.StartArray();
for (const auto& value : values) writer.String(value.c_str());
writer.EndArray();
}
template <typename Writer>
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();

View File

@@ -11,8 +11,10 @@
#include <mbgl/style/source_impl.hpp>
#include <mbgl/style/layer_properties.hpp>
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <unordered_map>
#include <vector>
@@ -49,6 +51,14 @@ public:
using RenderTiles = std::shared_ptr<const std::vector<std::reference_wrapper<const RenderTile>>>;
struct RenderSourceReadinessStats {
bool tileBacked = false;
std::optional<uint32_t> tileCount;
std::optional<uint32_t> incompleteTileCount;
std::optional<uint32_t> loadedTileCount;
bool hasFadingTiles = false;
};
class RenderSource : protected TileObserver {
public:
static std::unique_ptr<RenderSource> create(const Immutable<style::Source::Impl>&, 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; }

View File

@@ -5,8 +5,10 @@
#include <mbgl/renderer/render_segment.hpp>
#include <mbgl/renderer/render_segment_plan.hpp>
#include <algorithm>
#include <atomic>
#include <mutex>
#include <set>
#include <tuple>
#include <utility>
@@ -224,6 +226,18 @@ public:
entry.layers.assign(next.styleLayers.begin() + segment.beginIndex,
next.styleLayers.begin() + segment.endExclusive);
}
std::set<std::string> dependentSourceIds;
std::set<std::string> 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<std::mutex> 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<std::mutex> lock(rangeMutex);
auto result = renderSegmentDiagnostics;

View File

@@ -486,6 +486,23 @@ bool RenderTileSource::hasFadingTiles() const {
return tilePyramid.hasFadingTiles();
}
RenderSourceReadinessStats RenderTileSource::getReadinessDiagnostics() const {
RenderSourceReadinessStats result;
result.tileBacked = true;
result.tileCount = static_cast<uint32_t>(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<std::vector<std::reference_wrapper<const RenderTile>>>();

View File

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

View File

@@ -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<StyleLayerDiagnostics> layers;
for (std::size_t i = 0; i < segmentLayerIds().size(); ++i) {
layers.push_back({static_cast<int32_t>(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<uint32_t>{4});
EXPECT_EQ(first.sources[1].sourceId, "dynamic");
EXPECT_FALSE(first.sources[1].isLoaded);
EXPECT_EQ(first.sources[1].incompleteTileCount, std::optional<uint32_t>{1});
ASSERT_EQ(first.segments.size(), 3u);
EXPECT_EQ(first.segments[0].dependentSourceIds, std::vector<std::string>({"chart"}));
EXPECT_EQ(first.segments[1].dependentSourceIds, std::vector<std::string>({"dynamic"}));
EXPECT_EQ(first.segments[2].dependentSourceIds, std::vector<std::string>({"ais", "chart"}));
EXPECT_TRUE(first.segments[0].unknownDependencies.empty());
EXPECT_EQ(first.segments[2].unknownDependencies,
std::vector<std::string>({"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<uint32_t>({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}]})"));