Files
MapLibreParticleLab/docs/报告/STEP-099_RenderSource就绪与RenderSegment失效诊断_Codex完成报告_v1.md
tei c4b481fd2c
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
docs: close STEP-099 render source diagnostics
2026-10-06 15:33:12 +09:00

537 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 已优化。