Compare commits
2 Commits
e8b451fe70
...
c4b481fd2c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4b481fd2c | ||
|
|
8a5214d92d |
175
docs/50_Step记录/STEP-099_RenderSource就绪与RenderSegment失效诊断_v1.md
Normal file
175
docs/50_Step记录/STEP-099_RenderSource就绪与RenderSegment失效诊断_v1.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# STEP-099 RenderSource 就绪与 RenderSegment 失效诊断
|
||||
|
||||
状态:**CLOSED / PASS**
|
||||
日期:2026-10-06(JST)
|
||||
|
||||
## 基线
|
||||
|
||||
- 仓库:`/Users/tei/dev/MapLibreParticleLab-step099`
|
||||
- 分支:`step099-render-source-diagnostics`
|
||||
- frozen release:`maplibre-particle/rn/13.4.1-particle.17`
|
||||
- frozen release commit:`e8b451fe70054107773e82484c80e90df3889147`
|
||||
- implementation commit:`8a5214d92dd2f8899db76d2194da42999fa0c87d`
|
||||
- Integration/NavSea:未修改。
|
||||
|
||||
## 本 Step 完成内容
|
||||
|
||||
- Renderer readiness snapshot additive 暴露 style/image manager/render orchestrator/sprite readiness、fading、global content revision、per-source 状态和最近 mutation。
|
||||
- per-source diagnostics 暴露 sourceId/type/enabled/isLoaded;tile-backed source 提供 tileCount/incomplete/loaded/fading。
|
||||
- RenderSegment diagnostics 只读推导 `dependentSourceIds` / `unknownDependencies`;source-less layer 不制造 source dependency,共享 source 可出现在多个 segment。
|
||||
- mutation sequence/source/tile action/reason 只消费既有 renderer 事件;正式 revision 增长逻辑不变。
|
||||
- 正式 loaded 判定保持:
|
||||
`renderTreeParameters->loaded = updateParameters->styleLoaded && isLoaded();`
|
||||
- diagnostics snapshot 不进入正式 loaded/cache/fast path/segment execution。
|
||||
- RN 仅增加 diagnostics 类型字段;没有 NavSea 私有 API,没有改变业务 API。
|
||||
- Android Test App 提供 T1 STATIC、T2 GEOJSON_1HZ、T3 PERMANENT_UNREADY、T4 R-C-R。
|
||||
|
||||
## Native 验证
|
||||
|
||||
Metal host `RenderSegmentDiagnostics.*`:
|
||||
|
||||
- 6/6 PASS;
|
||||
- 新增 `ReadinessSnapshotMapsSourcesToSegmentsWithoutSideEffects` PASS;
|
||||
- 覆盖 source readiness、tile stats、source→segment、shared source、source-less 与重复 query 无副作用。
|
||||
|
||||
最终源码再次尝试 Metal configure 时当前 Mac 缺 `libuv` pkg-config,因此第二次 host rerun 未进入 build;不虚报 PASS。最终代码已经被 Android 全量 native build 成功编译并由 T1~T4 运行覆盖。
|
||||
|
||||
Vulkan:因当前 Mac 缺 `libuv`,NOT RUN tests。
|
||||
|
||||
Jest:NOT RUN,用户未要求。
|
||||
|
||||
## Android 构建
|
||||
|
||||
首轮 Android NDK 暴露:
|
||||
|
||||
```text
|
||||
error: missing field 'tileCount' initializer
|
||||
[-Werror,-Wmissing-designated-field-initializers]
|
||||
```
|
||||
|
||||
只补全 `RenderSourceReadinessStats` 默认空字段显式初始化。
|
||||
|
||||
随后 native build 通过,但 strip 阶段因 Mac 磁盘仅剩约 2 GiB:
|
||||
|
||||
```text
|
||||
No space left on device
|
||||
```
|
||||
|
||||
仅清理可再生 Gradle 9.5.1 cache / 失败中间产物后恢复约 12 GiB,再次增量构建:
|
||||
|
||||
- `BUILD SUCCESSFUL in 4m 54s`
|
||||
- 86 actionable tasks:7 executed / 79 up-to-date
|
||||
- OpenGL Debug / x86_64
|
||||
- APK:61,923,024 bytes
|
||||
- libmaplibre.so:39,602,856 bytes
|
||||
- SHA-256:
|
||||
`5de73f0358b835a78d3ae7e2ac22f4efdb2a66775b8c63654a8bb7313957d90a`
|
||||
|
||||
设备:
|
||||
|
||||
- `192.168.200.139:5555`
|
||||
- FZG1-2
|
||||
- Android 13 / API 33 / x86_64
|
||||
|
||||
## T1~T4 结果
|
||||
|
||||
### T1 STATIC
|
||||
|
||||
- 1 个启动 false window;
|
||||
- avg/max = 204 ms;
|
||||
- 随后 global loaded 稳定为 true;
|
||||
- retained cache 可持续 reuse;
|
||||
- fast path 可进入 active。
|
||||
|
||||
### T2 GEOJSON_1HZ
|
||||
|
||||
- 两个 GeoJSON source 交错更新,总 update 122 次,约 61 次/源;
|
||||
- 10 个 loaded=false window;
|
||||
- avg 203 ms;
|
||||
- max 205 ms;
|
||||
- open 0;
|
||||
- 约 60 s 中 false 总时长约 2.03 s,约 3.4%。
|
||||
|
||||
结论:存在 readiness thrash,但不是“几乎永远 false”。
|
||||
|
||||
### T3 PERMANENT_UNREADY
|
||||
|
||||
`step099-unready-raster-array`:
|
||||
|
||||
- 无 layer;
|
||||
- enabled=false;
|
||||
- isLoaded=false;
|
||||
- 其他主要 source loaded。
|
||||
|
||||
结果:
|
||||
|
||||
- global renderOrchestratorLoaded=false 持续;
|
||||
- 观察结束 open false window = 21,459 ms;
|
||||
- retained cache 持续 style-not-loaded fallback。
|
||||
|
||||
结论:单个与 segment 无关的 permanently unready source 足以拉低 V17 全局 loaded。
|
||||
|
||||
### T4 R-C-R
|
||||
|
||||
- R0 deps=[step099-static]
|
||||
- C deps=[particle-fixture]
|
||||
- R1 deps=[org.maplibre.annotations, step099-dynamic, step099-static]
|
||||
- dynamic update:61 次
|
||||
|
||||
结果:
|
||||
|
||||
- loaded false windows:4
|
||||
- avg 203 ms
|
||||
- max 207 ms
|
||||
- contentRevision:1760 → 2540
|
||||
- diagnostics 对 dynamic source 正确给出 affected=[2]
|
||||
- R0 不依赖 dynamic source,但:
|
||||
- build 1 → 121
|
||||
- invalidation 20 → 200
|
||||
|
||||
结论:dependency diagnostics 已能表达 source-local 影响,但 V17 正式 content revision/cache invalidation 仍为 global。
|
||||
|
||||
## Q1~Q7
|
||||
|
||||
1. Q1:**YES**。permanently unloaded source 会造成全局 style-not-loaded。
|
||||
2. Q2:**YES**。1 Hz GeoJSON 会产生约 200 ms readiness false window;T2 avg 203 / max 205 ms。
|
||||
3. Q3:**NO(本次观测)**。双 1 Hz source 并未让 global loaded 几乎始终 false。
|
||||
4. Q4:**YES**。GeoJSON tile mutation 推进 global content revision,并使 retained cache 重建/失效。
|
||||
5. Q5:**YES**。不引用动态 GeoJSON 的 R0 仍受 global revision 影响;源码还确认 `retainedRangeIsStable` 全局检查所有 source fading。
|
||||
6. Q6:fast path 仍有 global update signature(sources/layers/images 等)、global content revision、global fade、pendingChanges、placement/light/non-particle transition/repaint、annotation mutation、retained revision/cache/topology/boundary,以及 singleContinuous gate。
|
||||
7. Q7 blocker:
|
||||
- runtime confirmed:`PERMANENT_SOURCE_UNLOADED`、`DYNAMIC_SOURCE_READINESS_THRASH`、`GLOBAL_CONTENT_REVISION`
|
||||
- source confirmed:`GLOBAL_FADE`、`GLOBAL_UPDATE_SIGNATURE`
|
||||
- OTHER:pending/placement/light/repaint/annotation/topology/boundary/singleContinuous。
|
||||
|
||||
## 证据
|
||||
|
||||
忽略目录:
|
||||
|
||||
`artifacts/maplibre-particle/android-validation/`
|
||||
|
||||
- `build-info.txt`
|
||||
- `build.log`
|
||||
- `MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk`
|
||||
- `step099-logcat.txt`
|
||||
- `step099-device-info.txt`
|
||||
- `step099-metal-gtest.log`
|
||||
|
||||
STEP-099 log SHA-256:
|
||||
|
||||
`0f15d75dde77e7dd2f4d98c46a4c1501a628205e8aae62130b0cb96db6b8482f`
|
||||
|
||||
## 边界结论
|
||||
|
||||
本 Step 未修改:
|
||||
|
||||
- retained cache 算法;
|
||||
- fast path gate;
|
||||
- singleContinuous / multi-continuous;
|
||||
- segment-local invalidation 正式语义;
|
||||
- RenderSegment execution;
|
||||
- NavSea;
|
||||
- Source isLoaded;
|
||||
- RN 业务 API。
|
||||
|
||||
下一阶段应拆分处理 global source readiness、source/segment-local content revision/invalidation,再独立处理 fast-path global gates。
|
||||
@@ -0,0 +1,536 @@
|
||||
# STEP-099 RenderSource 就绪与 RenderSegment 失效诊断 — Codex 完成报告 v1
|
||||
|
||||
## 1. 执行结论
|
||||
|
||||
**CLOSED / PASS(诊断阶段完成)**。
|
||||
|
||||
STEP-099 按任务书保持“纯诊断、fail-closed、不优化正式语义”的范围完成。Renderer/Source/RenderSegment diagnostics、Android Test App T1~T4 场景、定向 native 测试和 Android 实机运行证据均已取得,Q1~Q7 可以据实回答。
|
||||
|
||||
本 Step **没有**修改 retained cache 算法、fast path gate、`singleContinuousSegment`、multi-continuous 行为、segment-local invalidation、RenderSegment 执行语义、NavSea、Source `isLoaded()` 定义或 RN 业务 API 语义。
|
||||
|
||||
## 2. 基线与 Git
|
||||
|
||||
- 仓库:`/Users/tei/dev/MapLibreParticleLab-step099`
|
||||
- branch:`step099-render-source-diagnostics`
|
||||
- frozen release tag:`maplibre-particle/rn/13.4.1-particle.17`
|
||||
- frozen release commit / start base:`e8b451fe70054107773e82484c80e90df3889147`
|
||||
- implementation commit:`8a5214d92dd2f8899db76d2194da42999fa0c87d`
|
||||
- implementation message:`diagnostics: add STEP-099 render source readiness audit`
|
||||
- Integration/NavSea:未修改。
|
||||
- 文档 closeout commit、push result、`origin/particle/main` Final HEAD:Git 提交无法在自身内容中自指其最终 SHA,按项目既有规则由最终交付回读记录。
|
||||
|
||||
## 3. 实际修改范围
|
||||
|
||||
生产源码 / diagnostics:
|
||||
|
||||
- `include/mbgl/renderer/render_segment.hpp`
|
||||
- `src/mbgl/renderer/render_orchestrator.cpp`
|
||||
- `src/mbgl/renderer/render_orchestrator.hpp`
|
||||
- `src/mbgl/renderer/render_segment_json.hpp`
|
||||
- `src/mbgl/renderer/render_source.hpp`
|
||||
- `src/mbgl/renderer/retained_underlay_runtime_state.hpp`
|
||||
- `src/mbgl/renderer/sources/render_tile_source.cpp`
|
||||
- `src/mbgl/renderer/sources/render_tile_source.hpp`
|
||||
- `packages/maplibre-particle-rn/src/index.ts`
|
||||
- `packages/maplibre-particle-rn/src/types.ts`
|
||||
|
||||
测试 / Test App:
|
||||
|
||||
- `test/renderer/retained_underlay_cache.test.cpp`
|
||||
- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt`
|
||||
- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step099ReadinessFixture.kt`
|
||||
|
||||
项目记录:
|
||||
|
||||
- `docs/50_Step记录/STEP-099_RenderSource就绪与RenderSegment失效诊断_v1.md`
|
||||
- `docs/报告/STEP-099_RenderSource就绪与RenderSegment失效诊断_Codex完成报告_v1.md`
|
||||
- `粒子开发记录.md`
|
||||
|
||||
## 4. 诊断能力
|
||||
|
||||
### 4.1 全局 readiness
|
||||
|
||||
新增只读 snapshot:
|
||||
|
||||
- `styleLoaded`
|
||||
- `imageManagerLoaded`
|
||||
- `renderOrchestratorLoaded`
|
||||
- `spriteLoaded`
|
||||
- `hasFadingTiles`
|
||||
- global retained `contentRevision`
|
||||
- RenderSegment plan/config/topology 信息
|
||||
- 最近 mutation sequence / reason / source / affected segment diagnostics
|
||||
|
||||
正式 loaded 路径保持原式:
|
||||
|
||||
```cpp
|
||||
renderTreeParameters->loaded = updateParameters->styleLoaded && isLoaded();
|
||||
```
|
||||
|
||||
diagnostics snapshot 在旁路采样,失败被 best-effort 隔离,不决定正式 loaded。
|
||||
|
||||
### 4.2 per-source readiness
|
||||
|
||||
每个 RenderSource additive 暴露:
|
||||
|
||||
- sourceId / sourceType
|
||||
- enabled / isLoaded
|
||||
- tileBacked
|
||||
- tileCount / incompleteTileCount / loadedTileCount
|
||||
- hasFadingTiles
|
||||
- last source/tile mutation sequence
|
||||
- last tile action / reason
|
||||
|
||||
默认非 tile source 返回完整显式初始化的空统计,Android NDK `-Werror` 下已验证通过;未改变任何 Source `isLoaded()` 实现。
|
||||
|
||||
### 4.3 RenderSegment 只读依赖
|
||||
|
||||
每个 segment additive 暴露:
|
||||
|
||||
- segmentIndex / strategy / beginIndex / endExclusive
|
||||
- `dependentSourceIds`
|
||||
- `unknownDependencies`
|
||||
|
||||
规则:
|
||||
|
||||
- 从实际 ordered layers 的 sourceId 推导;
|
||||
- shared source 可出现在多个 segment;
|
||||
- source-less background 不制造假依赖;
|
||||
- symbol/custom 等无法仅靠 sourceId 完整表达的依赖标记为 unknown/global;
|
||||
- 依赖图只用于诊断,不参与 cache/loaded/fast path/execution。
|
||||
|
||||
### 4.4 mutation 定位
|
||||
|
||||
Source/tile mutation 复用已有回调记录 sequence/source/reason/action。原 `retainedUnderlayContentRevision` 的增长次数与正式调用路径不变;diagnostics 不增加额外 revision。
|
||||
|
||||
query 路径不清理或修改 event history,重复 snapshot/JSON 查询保持无副作用。
|
||||
|
||||
## 5. Native 定向测试
|
||||
|
||||
### 5.1 Metal host 定向 GTest
|
||||
|
||||
实际执行:
|
||||
|
||||
```bash
|
||||
cmake --preset macos-metal
|
||||
cmake --build build-macos-metal --target mbgl-test-runner --parallel 3
|
||||
build-macos-metal/mbgl-test-runner --gtest_filter='RenderSegmentDiagnostics.*'
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- `RenderSegmentDiagnostics.*`:**6/6 PASS**
|
||||
- 新增 `ReadinessSnapshotMapsSourcesToSegmentsWithoutSideEffects`:PASS
|
||||
- 覆盖 source loaded/unloaded、tile stats、source→segment、shared source、source-less、重复 query、topology/cache/content revision 无副作用。
|
||||
|
||||
证据:
|
||||
`artifacts/maplibre-particle/android-validation/step099-metal-gtest.log`
|
||||
|
||||
SHA-256:
|
||||
`f0618750ead03eae1b251126dd49488a9269a120373a66e3453fa9aaa1dc0f10`
|
||||
|
||||
说明:该 6/6 发生在 Android NDK 首轮暴露“默认 readiness struct designated initializer 未显式列完 optional 字段”之前;随后仅补全默认空字段初始化。最终源码由 Android 全量 native build 成功覆盖。为了在最终源码上重跑 Metal,重新 configure 时当前 Mac 缺 `libuv` pkg-config,configure fail;没有安装系统依赖来制造 PASS。
|
||||
|
||||
### 5.2 Vulkan
|
||||
|
||||
`macos-vulkan` configure:**BLOCKED / NOT RUN TESTS**。
|
||||
|
||||
原因:
|
||||
|
||||
```text
|
||||
The following required packages were not found:
|
||||
- libuv
|
||||
```
|
||||
|
||||
这是当前 Mac host 环境依赖,不是 STEP-099 源码编译错误。
|
||||
|
||||
### 5.3 Jest
|
||||
|
||||
**NOT RUN**。用户未要求 Jest,任务未加入 Jest。
|
||||
|
||||
## 6. Android 构建与设备
|
||||
|
||||
### 6.1 首轮 Android 编译问题
|
||||
|
||||
首轮 x86_64 OpenGL build 在 Android NDK `-Werror` 下发现:
|
||||
|
||||
```text
|
||||
render_source.hpp:84:51:
|
||||
error: missing field 'tileCount' initializer
|
||||
[-Werror,-Wmissing-designated-field-initializers]
|
||||
```
|
||||
|
||||
修复仅补全 `RenderSourceReadinessStats` 默认空字段初始化,不改变运行语义。
|
||||
|
||||
随后 native C++ 已越过原失败点并完整编译。
|
||||
|
||||
### 6.2 磁盘阻塞与恢复
|
||||
|
||||
第一次完整 native build 后,在:
|
||||
|
||||
`MapLibreAndroidTestApp:stripOpenglDebugDebugSymbols`
|
||||
|
||||
因 Mac 仅剩约 2 GiB 出现:
|
||||
|
||||
```text
|
||||
No space left on device
|
||||
```
|
||||
|
||||
RDC 只清理可再生 Gradle 9.5.1 cache / 本轮失败中间产物,保留 Android native object;可用空间恢复到约 12 GiB 后增量重跑。
|
||||
|
||||
### 6.3 最终 APK
|
||||
|
||||
命令:
|
||||
|
||||
```bash
|
||||
./tools/particle_android_validation/build_validation_apk.sh
|
||||
```
|
||||
|
||||
最终结果:
|
||||
|
||||
- Gradle:**BUILD SUCCESSFUL in 4m 54s**
|
||||
- actionable tasks:86(7 executed / 79 up-to-date)
|
||||
- variant:OpenGL Debug
|
||||
- ABI:x86_64
|
||||
- APK:61,923,024 bytes
|
||||
- `libmaplibre.so`:39,602,856 bytes
|
||||
- APK SHA-256:
|
||||
`5de73f0358b835a78d3ae7e2ac22f4efdb2a66775b8c63654a8bb7313957d90a`
|
||||
|
||||
构建时 metadata:
|
||||
|
||||
- base commit:`e8b451fe70054107773e82484c80e90df3889147`
|
||||
- `git_dirty=true`
|
||||
- 构建内容与随后形成的 implementation commit `8a5214d92` 源码一致;当前 source tree 对 implementation commit clean,剩余仅 docs closeout。
|
||||
|
||||
### 6.4 Android 设备
|
||||
|
||||
Mac ADB 直接连接:
|
||||
|
||||
- serial:`192.168.200.139:5555`
|
||||
- model:`FZG1-2`
|
||||
- Android:13
|
||||
- API:33
|
||||
- ABI:x86_64
|
||||
|
||||
APK 覆盖安装成功,`ParticleCoreLayerValidationActivity` 冷启动成功。
|
||||
|
||||
运行 log:
|
||||
`artifacts/maplibre-particle/android-validation/step099-logcat.txt`
|
||||
|
||||
log SHA-256:
|
||||
`0f15d75dde77e7dd2f4d98c46a4c1501a628205e8aae62130b0cb96db6b8482f`
|
||||
|
||||
## 7. T1~T4 实测
|
||||
|
||||
### T1 STATIC
|
||||
|
||||
场景:
|
||||
|
||||
- R0:静态 GeoJSON retained
|
||||
- C:本地静态 RasterArray field 驱动 WeatherColor + Particle
|
||||
- R1:静态 retained
|
||||
- camera fixed
|
||||
|
||||
结果:
|
||||
|
||||
- 启动阶段 1 个 false window:204 ms;
|
||||
- 随后 `renderOrchestratorLoaded=true` 稳定;
|
||||
- `particle-fixture` 与 `step099-static` 均 loaded;
|
||||
- retained cache 建立后持续 reuse;
|
||||
- fast path 进入 `active=true / eligible=true / reason=none`;
|
||||
- 没有持续 style-not-loaded。
|
||||
|
||||
aggregate:
|
||||
|
||||
```text
|
||||
falseWindowCount=1
|
||||
falseWindowAvgMs=204
|
||||
falseWindowMaxMs=204
|
||||
openFalseWindowMs=0
|
||||
```
|
||||
|
||||
结论:静态基线可稳定进入 loaded/cache reuse/fast path。
|
||||
|
||||
### T2 GEOJSON_1HZ
|
||||
|
||||
两个 GeoJSON source 每 500 ms 交错更新,因此每个约 1 Hz。
|
||||
|
||||
实际:
|
||||
|
||||
- 总 source update:122 次,约 61 次/源;
|
||||
- 60 秒量级观察中,出现 10 个 loaded=false window;
|
||||
- 平均 203 ms;
|
||||
- 最大 205 ms;
|
||||
- 最终 open window = 0;
|
||||
- 总 false 时间约 2.03 s,约占 60 s 的 3.4%,不是“几乎始终 false”。
|
||||
|
||||
aggregate:
|
||||
|
||||
```text
|
||||
falseWindowCount=10
|
||||
falseWindowAvgMs=203
|
||||
falseWindowMaxMs=205
|
||||
openFalseWindowMs=0
|
||||
```
|
||||
|
||||
同时 final sample:
|
||||
|
||||
- global contentRevision 已到 1679;
|
||||
- R0 仅依赖 `step099-static`,但 cache build=241、invalidation=362;
|
||||
- R1 依赖动态 GeoJSON,也同样 build=241;
|
||||
- mutation diagnostics 能正确指出动态 source 只 `affected=[2]`,但正式 cache invalidation 仍非局部。
|
||||
|
||||
### T3 PERMANENT_UNREADY
|
||||
|
||||
额外加入:
|
||||
|
||||
`step099-unready-raster-array`
|
||||
|
||||
它:
|
||||
|
||||
- 没有 layer;
|
||||
- 因此 `enabled=false`;
|
||||
- host-submission 无 field;
|
||||
- `isLoaded=false`。
|
||||
|
||||
其他主要 source 均 loaded。
|
||||
|
||||
实测:
|
||||
|
||||
- `renderOrchestratorLoaded=false` 持续;
|
||||
- retained segment cache 持续 fallback;
|
||||
- `lastInvalidationReason=style-not-loaded`;
|
||||
- 观察结束仍没有恢复。
|
||||
|
||||
aggregate:
|
||||
|
||||
```text
|
||||
falseWindowCount=0
|
||||
openFalseWindowMs=21459
|
||||
```
|
||||
|
||||
结论:即使 source 与当前 layer/segment 无关且 disabled,只要仍在 `renderSources` 且 `isLoaded=false`,V17 全局 `RenderOrchestrator::isLoaded()` 就保持 false。
|
||||
|
||||
### T4 R-C-R
|
||||
|
||||
结构:
|
||||
|
||||
- R0:`deps=[step099-static]`
|
||||
- C:`deps=[particle-fixture]`,WeatherColor + Particle
|
||||
- R1:`deps=[org.maplibre.annotations, step099-dynamic, step099-static]`
|
||||
- `step099-dynamic` 约 1 Hz 更新
|
||||
|
||||
实际:
|
||||
|
||||
- dynamic update:61 次;
|
||||
- loaded false window:4;
|
||||
- 平均 203 ms;
|
||||
- 最大 207 ms;
|
||||
- global contentRevision:1760 → 2540;
|
||||
- mutation diagnostics:`step099-dynamic` 对应 `affected=[2]`;
|
||||
- R0 并不依赖 `step099-dynamic`,但:
|
||||
- build:1 → 121
|
||||
- invalidation:20 → 200
|
||||
- R1 同步得到相同级别 build/invalidation 增长;
|
||||
- fast path 在稳定窗口可恢复 active,但 global revision / transient readiness 会反复制造 full CPU work。
|
||||
|
||||
aggregate:
|
||||
|
||||
```text
|
||||
falseWindowCount=4
|
||||
falseWindowAvgMs=203
|
||||
falseWindowMaxMs=207
|
||||
openFalseWindowMs=0
|
||||
```
|
||||
|
||||
结论:依赖诊断已经能准确表达“R1 source-local”,但 V17 正式 content revision/cache invalidation 仍是 global。
|
||||
|
||||
## 8. Q1~Q7
|
||||
|
||||
### Q1:单个 permanently unloaded source 会造成 V17 style-not-loaded 吗?
|
||||
|
||||
**YES / CONFIRMED。**
|
||||
|
||||
T3 中 `step099-unready-raster-array` 无 layer、`enabled=false`、`isLoaded=false`;其他主要 source loaded,但整体 `renderOrchestratorLoaded=false` 持续 21.459 s 未恢复,retained cache 持续 `style-not-loaded` fallback。
|
||||
|
||||
源码事实与运行事实一致:`RenderOrchestrator::isLoaded()` 遍历全部 `renderSources`,不按 enabled/segment dependency 排除 source。
|
||||
|
||||
### Q2:1 Hz GeoJSON 会周期性让 isLoaded=false 吗?平均/最大窗口?
|
||||
|
||||
**YES / CONFIRMED。**
|
||||
|
||||
T2 双 source 交错各约 1 Hz:
|
||||
|
||||
- 10 次 false window;
|
||||
- avg 203 ms;
|
||||
- max 205 ms。
|
||||
|
||||
T4 单动态 source 约 1 Hz:
|
||||
|
||||
- 4 次 false window;
|
||||
- avg 203 ms;
|
||||
- max 207 ms。
|
||||
|
||||
### Q3:错开的多个 1 Hz GeoJSON 会导致 global loaded 几乎无法稳定为 true 吗?
|
||||
|
||||
**NO / 本次观测不支持。**
|
||||
|
||||
T2 约 60 s 中总 false 时间约 2.03 s,约 3.4%;绝大多数时间 global loaded=true。它确实产生 readiness thrash,但不是“几乎永远 false”。
|
||||
|
||||
### Q4:GeoJSON tile mutation 会推进 global retainedUnderlayContentRevision 并影响 retained cache 吗?
|
||||
|
||||
**YES / CONFIRMED。**
|
||||
|
||||
T4 global contentRevision 1760 → 2540。仅依赖静态 source 的 R0 也从 build 1 → 121、invalidation 20 → 200。
|
||||
|
||||
### Q5:不引用 GeoJSON 的 retained segment 仍会受它的 loaded/fading/tile mutation 影响吗?
|
||||
|
||||
**YES。**
|
||||
|
||||
运行证据:
|
||||
|
||||
- T4 diagnostics 正确报告动态 source `affected=[2]`;
|
||||
- 但 R0 `deps=[step099-static]` 仍随动态 source mutation 重建/失效。
|
||||
|
||||
源码证据:
|
||||
|
||||
- `retainedRangeIsStable(begin,end)` 虽为 range API,但最后遍历 **全部 `renderSources`**,任一 source `hasFadingTiles()` 都让任意 retained range unstable。
|
||||
|
||||
本次 T4 稳态 `fade=false`,因此 R0 的实际反复失效主要直接证明 global content revision/tile mutation;GLOBAL_FADE 属源码确认、未在本轮场景主动触发的全局 gate。
|
||||
|
||||
### Q6:`tryRenderParticleFastFrame` 仍有哪些 global gate 会阻挡未来 localized cache CPU fast path?
|
||||
|
||||
**确认仍存在:**
|
||||
|
||||
1. **GLOBAL_UPDATE_SIGNATURE**
|
||||
- `particleFastPathNonCameraInputsMatch` 全局比较:
|
||||
- styleLoaded / spriteLoaded
|
||||
- light
|
||||
- images
|
||||
- **sources**
|
||||
- **layers**
|
||||
- fontFaces / fileSource / glyphURL
|
||||
- transition / placement / tile LOD 等全局输入。
|
||||
|
||||
2. **GLOBAL_CONTENT_REVISION**
|
||||
- `particlePreparedContentRevision == orchestrator.getRetainedUnderlayContentRevision()`。
|
||||
|
||||
3. **GLOBAL_FADE**
|
||||
- `particleFastPathDynamicState` 遍历全部 renderSources 汇总 fading;
|
||||
- `retainedRangeIsStable` 也遍历全部 renderSources。
|
||||
|
||||
4. **GLOBAL_PENDING / TRANSITION / REPAINT**
|
||||
- pendingChanges
|
||||
- renderLight transition
|
||||
- 任意 layer transition
|
||||
- 任意非粒子 layer repaint
|
||||
- placement transition。
|
||||
|
||||
5. **GLOBAL_ANNOTATION_MUTATION**
|
||||
- AnnotationManager `hasPendingChanges()`。
|
||||
|
||||
6. **RETAINED REVISION / CACHE / TOPOLOGY / BOUNDARY**
|
||||
- retained cache validity/static revision
|
||||
- global retained revision tracker
|
||||
- topology revision
|
||||
- boundary/config revision。
|
||||
|
||||
7. **singleContinuousSegment**
|
||||
- 仍要求 plan 恰好能由一个 Continuous segment 表达;
|
||||
- multi-continuous 继续 fail closed,本 Step 未修改。
|
||||
|
||||
因此即使未来 cache invalidation 变成 source/segment-local,fast path 仍有多组 global gate 需要后续 Step 单独设计,不能在 STEP-099 顺手放宽。
|
||||
|
||||
### Q7:blocker 分类
|
||||
|
||||
**运行态 CONFIRMED:**
|
||||
|
||||
- `PERMANENT_SOURCE_UNLOADED`
|
||||
- `DYNAMIC_SOURCE_READINESS_THRASH`
|
||||
- `GLOBAL_CONTENT_REVISION`
|
||||
|
||||
**源码 CONFIRMED,本轮未主动触发独立运行场景:**
|
||||
|
||||
- `GLOBAL_FADE`
|
||||
- `GLOBAL_UPDATE_SIGNATURE`
|
||||
|
||||
**OTHER:**
|
||||
|
||||
- global pendingChanges
|
||||
- placement/light/non-particle transition/repaint
|
||||
- annotation mutation
|
||||
- topology/boundary/revision/cache validity
|
||||
- singleContinuous fail-closed
|
||||
|
||||
## 9. 任务边界审计
|
||||
|
||||
确认未改:
|
||||
|
||||
- retained cache 核心算法;
|
||||
- fast path gate 逻辑;
|
||||
- `singleContinuousSegment`;
|
||||
- multi-continuous 策略;
|
||||
- segment-local invalidation 正式语义;
|
||||
- RenderSegment execution;
|
||||
- NavSea;
|
||||
- private NavSea Native API;
|
||||
- RN 业务 API;
|
||||
- Source `isLoaded()` 定义。
|
||||
|
||||
新增 dependency / affectedSegmentIndices 只是观察信息;它没有参与正式 invalidation。
|
||||
|
||||
## 10. 证据文件
|
||||
|
||||
忽略目录:
|
||||
|
||||
`artifacts/maplibre-particle/android-validation/`
|
||||
|
||||
主要文件:
|
||||
|
||||
- `build-info.txt`
|
||||
- `build.log`
|
||||
- `MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk`
|
||||
- `step099-logcat.txt`
|
||||
- `step099-device-info.txt`
|
||||
- `step099-metal-gtest.log`
|
||||
- `step099-cmake-metal.log`
|
||||
|
||||
APK SHA-256:
|
||||
|
||||
`5de73f0358b835a78d3ae7e2ac22f4efdb2a66775b8c63654a8bb7313957d90a`
|
||||
|
||||
STEP-099 log SHA-256:
|
||||
|
||||
`0f15d75dde77e7dd2f4d98c46a4c1501a628205e8aae62130b0cb96db6b8482f`
|
||||
|
||||
## 11. 下一阶段建议
|
||||
|
||||
STEP-099 只负责定位,不在本 Step 修复。
|
||||
|
||||
下一阶段应基于本轮证据拆开处理:
|
||||
|
||||
1. 先决定 global source readiness 是否要引入“影响当前 style/render segment 的 readiness 范围”,解决 permanently unready / transient unrelated source 对全局 loaded 的影响;
|
||||
2. 再设计 source/segment-local content revision 与 invalidation,避免 R1 GeoJSON mutation 重建 R0;
|
||||
3. 最后单独审查 fast-path global signature/fade/pending/placement 等 gate,不能把 cache locality 与 fast-path locality 混成一个修改;
|
||||
4. 保持 Style Z-order 唯一事实来源和当前通用 Render Segment 模型。
|
||||
|
||||
## 12. 最终状态
|
||||
|
||||
**STEP-099 CLOSED / PASS。**
|
||||
|
||||
Pass 含义仅为:
|
||||
|
||||
- diagnostics 能回答任务问题;
|
||||
- Native 定向测试已有 PASS 证据;
|
||||
- 最终 Android x86_64 OpenGL APK BUILD SUCCESS;
|
||||
- FZG1-2 T1~T4 实跑完成;
|
||||
- Q1~Q7 有源码/运行证据;
|
||||
- 未改变正式渲染语义。
|
||||
|
||||
不代表:
|
||||
|
||||
- Vulkan host 已通过;
|
||||
- iOS runtime 已测试;
|
||||
- 下一阶段 localized readiness/invalidation 已实现;
|
||||
- fast path global gates 已优化。
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -19,6 +19,8 @@ export type {
|
||||
RenderSegmentDefinition,
|
||||
RenderSegmentDiagnostics,
|
||||
RenderSegmentLayerDiagnostics,
|
||||
RenderSourceReadinessDiagnostics,
|
||||
RenderMutationDiagnostics,
|
||||
RenderSegmentPlan,
|
||||
RenderSegmentStrategy,
|
||||
RetainedRenderSegmentDiagnostics,
|
||||
|
||||
@@ -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)[];
|
||||
}>;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>>>();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}]})"));
|
||||
|
||||
10
粒子开发记录.md
10
粒子开发记录.md
@@ -1022,3 +1022,13 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
|
||||
|
||||
- 统一发行 Android 四 ABI 和 Maven Local 通过;首次 iOS Metal Bazel 在 render_weather_color_layer.cpp 编译时找不到新增 WeatherColor lifecycle 头文件。
|
||||
- 在 bazel/core.bzl 的 MLN_CORE_HEADERS 精确登记该头文件;保留 WeatherColor 业务实现不变。由 RDC 重跑完整 v16 构建、公开回读,iPad 人工验收仍待执行。
|
||||
|
||||
### 2026-10-06 STEP-099 RenderSource readiness 与 RenderSegment 失效诊断(CLOSED / PASS)
|
||||
|
||||
- frozen release 为 `maplibre-particle/rn/13.4.1-particle.17` / `e8b451fe70054107773e82484c80e90df3889147`;implementation commit 为 `8a5214d92dd2f8899db76d2194da42999fa0c87d`。未修改 Integration/NavSea。
|
||||
- 完成 renderer/source readiness additive snapshot、tile stats、mutation source/tile action、只读 source→segment dependency 与 Android T1~T4。正式 `renderTreeParameters->loaded = styleLoaded && isLoaded()`、Source `isLoaded()`、retained cache、fast path、singleContinuous、多 Continuous 和 RenderSegment execution 均未改变。
|
||||
- Metal host `RenderSegmentDiagnostics.*` 6/6 PASS;最终 Android x86_64 OpenGL APK BUILD SUCCESSFUL in 4m54s,APK 61,923,024 bytes,SHA-256 `5de73f0358b835a78d3ae7e2ac22f4efdb2a66775b8c63654a8bb7313957d90a`。Vulkan/最终 Metal rerun 因当前 Mac 缺 libuv pkg-config 未执行,不虚报 PASS。
|
||||
- FZG1-2 / Android 13 / API33 实跑:T1 静态仅 1 个 204ms 启动 false window 后稳定;T2 双 GeoJSON 各约 1Hz,10 个 false window、avg 203ms、max 205ms;T3 无 layer 的 disabled RasterArray 仍 isLoaded=false,使 global loaded 持续 false 21.459s;T4 61 次动态更新产生 4 个 false window、avg 203ms、max 207ms。
|
||||
- T4 证实 dependency diagnostics 能正确给动态 source `affected=[2]`,但 global contentRevision 1760→2540,R0 虽只依赖 `step099-static`,cache build 仍 1→121、invalidation 20→200,确认 `GLOBAL_CONTENT_REVISION`。源码审计同时确认 global fade/update signature/pending/placement/repaint/annotation/singleContinuous 等 fast-path gate 仍存在。
|
||||
- Q1~Q7 与 blocker 分类已闭合:runtime confirmed `PERMANENT_SOURCE_UNLOADED`、`DYNAMIC_SOURCE_READINESS_THRASH`、`GLOBAL_CONTENT_REVISION`;source confirmed `GLOBAL_FADE`、`GLOBAL_UPDATE_SIGNATURE`。
|
||||
- 完成报告:`docs/报告/STEP-099_RenderSource就绪与RenderSegment失效诊断_Codex完成报告_v1.md`;Step:`docs/50_Step记录/STEP-099_RenderSource就绪与RenderSegment失效诊断_v1.md`;运行证据保存在忽略目录 `artifacts/maplibre-particle/android-validation/`。
|
||||
|
||||
Reference in New Issue
Block a user