Compare commits
9 Commits
maplibre-p
...
6b48f905d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b48f905d2 | ||
|
|
7c5ed27b0b | ||
|
|
01863569fb | ||
|
|
64cf8f4435 | ||
|
|
d29e84c1a9 | ||
|
|
2780550b62 | ||
|
|
524aaf4af5 | ||
|
|
c4b481fd2c | ||
|
|
8a5214d92d |
690
docs/40_任务/STEP-100_SourceScoped_RenderSegment局部失效_Codex任务_v2.md
Normal file
690
docs/40_任务/STEP-100_SourceScoped_RenderSegment局部失效_Codex任务_v2.md
Normal file
@@ -0,0 +1,690 @@
|
||||
# STEP-100 Source → RenderSegment 局部 Revision / Invalidation / Readiness 与独立 Fallback — Codex 任务 v2
|
||||
|
||||
## 0. 任务性质
|
||||
|
||||
这是 STEP-099 之后的正式修复任务。
|
||||
|
||||
STEP-099 已经完成纯诊断并 CLOSED / PASS,已确认 V17 的核心问题:
|
||||
|
||||
1. 动态 GeoJSON Source/Tile mutation 会推进 global retained content revision;
|
||||
2. 即使 R0 不依赖该 GeoJSON,R0 retained cache 仍跟着 invalid/build;
|
||||
3. GeoJSON 更新期间约 200 ms 的 transient `isLoaded=false` 会通过 global `RenderOrchestrator::isLoaded()` 把整个 retained path 拉成 `style-not-loaded`;
|
||||
4. 当前 renderer 还存在 `useRetainedRanges` all-or-none gate:一个 RETAINED Segment fallback 时,会使其他本来可安全 reuse 的 RETAINED Segment 一起退出 retained path;
|
||||
5. STEP-099 diagnostics 已能正确给 source mutation 计算 `affectedSegmentIndices`,但该依赖关系仍只是诊断,不参与正式 invalidation。
|
||||
|
||||
本任务目标是把 **Source/Tile 变化对 RETAINED Segment 的影响正式局部化**。
|
||||
|
||||
本任务不是 V18 发布任务。Codex 只负责实现与源码级说明;Android/iOS 构建、APK、ADB、设备测试、最终 Git push、V18 release 由 RDC/ChatGPT 在 Codex 完成后独立执行。
|
||||
|
||||
未经用户明确要求,本任务 **不加入、不运行 Jest**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 冻结基线
|
||||
|
||||
工作目录:
|
||||
|
||||
`/Users/tei/dev/MapLibreParticleLab-step100`
|
||||
|
||||
分支:
|
||||
|
||||
`step100-segment-local-source-invalidation`
|
||||
|
||||
开始基线必须重新核对,当前预期:
|
||||
|
||||
- `origin/particle/main`
|
||||
- HEAD:`c4b481fd2c5b5da20d31f6ae54011f0552d7dbd0`
|
||||
- STEP-099 implementation:`8a5214d92dd2f8899db76d2194da42999fa0c87d`
|
||||
- V17 frozen tag:`maplibre-particle/rn/13.4.1-particle.17`
|
||||
- V17 frozen commit:`e8b451fe70054107773e82484c80e90df3889147`
|
||||
|
||||
禁止修改、移动、覆盖 V17 tag / release worktree。
|
||||
|
||||
STEP-100 是 V17 之后的新开发,最终只有 RDC 验收通过后才允许进入 V18 发布。
|
||||
|
||||
开始前必须读取:
|
||||
|
||||
- 当前仓库实际 HEAD / status / diff;
|
||||
- 全局 Codex AGENTS;
|
||||
- `ARCHITECTURE.md`(若存在);
|
||||
- STEP-092 / 094 / 099 的任务、Step 记录、完成报告;
|
||||
- 当前:
|
||||
- `renderer_impl.cpp`
|
||||
- `render_orchestrator.*`
|
||||
- `retained_underlay_cache.*`
|
||||
- `retained_underlay_runtime_state.hpp`
|
||||
- `render_segment.*`
|
||||
- STEP-099 diagnostics 与 Android fixture;
|
||||
- 当前 RenderSegment / retained cache / particle fast path tests。
|
||||
|
||||
不得凭历史印象修改。
|
||||
|
||||
---
|
||||
|
||||
## 2. 最终架构原则
|
||||
|
||||
长期 RenderSegment 模型继续保持通用模型:
|
||||
|
||||
```text
|
||||
Style Z-order
|
||||
↓
|
||||
R / C / R / C / R / ...
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- R = RETAINED
|
||||
- C = CONTINUOUS
|
||||
|
||||
本任务只用 **R-C-R** 做最小验证,不把 R-C-R 固化成最终架构。
|
||||
|
||||
必须保持:
|
||||
|
||||
1. Style layer 顺序是唯一 Z-order 真相;
|
||||
2. Renderer 不理解 AIS / Route / Weather / GeoJSON 等业务名;
|
||||
3. Source → Segment 依赖由当前实际 Style / RenderLayer / Source 关系推导;
|
||||
4. 一个 Source 可依赖多个 Segment;
|
||||
5. source-less layer 不制造假 dependency;
|
||||
6. unknown/global dependency 必须 fail closed;
|
||||
7. 相邻 Segment、多个 C、多个 R 的通用模型不能被破坏;
|
||||
8. 本任务不实现 multi-continuous Fast Path。
|
||||
|
||||
---
|
||||
|
||||
## 3. 本任务必须实现的核心目标
|
||||
|
||||
### 3.1 Source/Tile mutation 局部化
|
||||
|
||||
假设:
|
||||
|
||||
```text
|
||||
R0 depends [staticSource]
|
||||
C depends [particleField]
|
||||
R1 depends [dynamicGeoJson]
|
||||
```
|
||||
|
||||
当:
|
||||
|
||||
```text
|
||||
dynamicGeoJson mutation
|
||||
```
|
||||
|
||||
正式 renderer 必须达到:
|
||||
|
||||
```text
|
||||
R0:
|
||||
content/revision 不因该 source mutation 改变
|
||||
cache 不 invalid
|
||||
cache 不 rebuild
|
||||
持续 reuse
|
||||
|
||||
R1:
|
||||
revision/invalidation 正常变化
|
||||
按需要 build/reuse/fallback
|
||||
|
||||
C:
|
||||
正常 continuous render
|
||||
```
|
||||
|
||||
禁止继续出现:
|
||||
|
||||
```text
|
||||
dynamicGeoJson mutation
|
||||
↓
|
||||
global revision 变化
|
||||
↓
|
||||
R0 + R1 一起 rebuild
|
||||
```
|
||||
|
||||
### 3.2 保留 global revision 兼容信号
|
||||
|
||||
STEP-100 不要求删除现有 global `retainedUnderlayContentRevision`。
|
||||
|
||||
为了保持:
|
||||
|
||||
- diagnostics;
|
||||
- 现有 fast-path correctness;
|
||||
- 旧 API / compatibility;
|
||||
- conservative fallback;
|
||||
|
||||
允许并推荐继续维护 global revision。
|
||||
|
||||
但 **RETAINED Segment cache 的正式内容 revision / invalidation 不能再只依赖这个 global source/tile revision**。
|
||||
|
||||
当前 `retainedUnderlayRevisionTracker` 同时消费 global content revision 与 `renderTreeParameters.loaded`。STEP-100 必须明确拆成两种用途:
|
||||
|
||||
```text
|
||||
global retained revision / tracker
|
||||
-> compatibility / Fast Path / diagnostics
|
||||
|
||||
segment-local retained content state
|
||||
-> full CPU renderer 的每个 RETAINED Segment Build / Reuse / Fallback
|
||||
```
|
||||
|
||||
**Segment-local cache content state 禁止继续直接消费:**
|
||||
|
||||
- global source/tile content revision;
|
||||
- global `renderTreeParameters.loaded`。
|
||||
|
||||
否则即使 source-scoped revision 已实现,R0 仍会被无关 GeoJSON transient unloaded 或 global revision 连带失效。
|
||||
|
||||
允许 global revision / global tracker继续按原逻辑变化;禁止为了让测试 PASS 而停止 global revision 增长或放宽 Fast Path。
|
||||
|
||||
### 3.3 per-RETAINED-Segment readiness
|
||||
|
||||
不得修改任何 Source `isLoaded()` 定义。
|
||||
|
||||
不得修改:
|
||||
|
||||
```cpp
|
||||
renderTreeParameters->loaded = updateParameters->styleLoaded && isLoaded();
|
||||
```
|
||||
|
||||
这个 global loaded 仍作为 MapLibre 现有全局兼容事实。
|
||||
|
||||
但 retained segment 的 cache Build/Reuse/Fallback 必须有自己的安全 readiness 判断。
|
||||
|
||||
最低语义:
|
||||
|
||||
- Style 本身未 ready:fail closed;
|
||||
- image manager / sprite / glyph 等无法安全定位到单 Segment 的全局前置条件未 ready:fail closed;
|
||||
- Segment 已知依赖的 source 中任一缺失或 `isLoaded()==false`:仅该 Segment fail closed;
|
||||
- Segment 已知依赖的 source `hasFadingTiles()==true`:仅该 Segment视为 unstable;
|
||||
- **不被该 Segment 引用的 source unloaded 或 fading,不得让该 Segment cache fallback/rebuild**;
|
||||
- `retainedRangeIsStable(begin,end)` 的 source fading 检查必须按该 range 的真实 source dependency 缩小,不能继续遍历全部 `renderSources` 作为 full-CPU retained gate;
|
||||
- Fast Path 原有 global fading gate 保持不变;
|
||||
- source ID 无法解析、dependency 缺失或真正无法证明安全时 fail closed。
|
||||
|
||||
必须区分:
|
||||
|
||||
1. **已知 Source dependency**;
|
||||
2. **symbol/global prerequisite**(glyph/image/placement 等);
|
||||
3. **真正 unknown dependency**。
|
||||
|
||||
STEP-099 的 `unknownDependencies` 目前会把 symbol 相关条件标为 unknown。STEP-100 不得机械地把“存在 symbol”解释成永久 fallback;symbol 所需 glyph/image/placement 应作为明确的 global prerequisite/fail-closed 条件处理。
|
||||
|
||||
因此 T3 中一个:
|
||||
|
||||
```text
|
||||
no layer
|
||||
enabled=false
|
||||
isLoaded=false
|
||||
```
|
||||
|
||||
的 RasterArray source 可以继续让 global `RenderOrchestrator::isLoaded()==false`,但不得因此让与它无依赖关系的 R0/R1 cache 全部 `style-not-loaded`。
|
||||
|
||||
本 Step 的运行态硬验收范围是 **MapMode::Continuous**(粒子正式实时渲染模式)。当前非 Continuous / still 路径如果在 global unloaded 时提前返回,本 Step 不强行改变其兼容语义;完成报告必须明确该适用范围。
|
||||
|
||||
### 3.4 RETAINED Segment 独立决策
|
||||
|
||||
当前 V17 路径存在类似:
|
||||
|
||||
```text
|
||||
useRetainedRanges = all retained segments supported
|
||||
```
|
||||
|
||||
的 all-or-none 语义。
|
||||
|
||||
STEP-100 必须改为每个 RETAINED Segment 独立拥有:
|
||||
|
||||
- readiness/support decision;
|
||||
- Build / Reuse / Fallback;
|
||||
- content/source revision;
|
||||
- invalidation reason。
|
||||
|
||||
如果:
|
||||
|
||||
```text
|
||||
R0 = Reuse
|
||||
R1 = Fallback
|
||||
```
|
||||
|
||||
则本帧必须允许:
|
||||
|
||||
```text
|
||||
R0 composite cached texture
|
||||
C live render
|
||||
R1 live render fallback
|
||||
```
|
||||
|
||||
而不是退化为“整个 Style 全部 live render”。
|
||||
|
||||
如果某 retained cache 自身 build 失败,应优先只 fallback 该 Segment;只有无法证明局部安全的 context/global resource failure 才允许全 retained fail closed。
|
||||
|
||||
---
|
||||
|
||||
## 4. Source-scoped revision 设计要求
|
||||
|
||||
Codex 必须基于当前代码选择最小、稳定设计,并在完成报告说明实际方案。
|
||||
|
||||
推荐方向但不强制具体类名:
|
||||
|
||||
- 为 Source/Tile mutation 维护 source-scoped monotonic revision;
|
||||
- keyed by stable sourceId,但必须处理 source 删除后同 ID recreate,不能让新 source 错误继承可复用的旧 cache identity;可用 generation/epoch 或保留单调 tombstone 语义实现;
|
||||
- source diff、feature-state mutation、tile changed 等所有当前会进入 Source/Tile retained content invalidation 的正式路径都必须覆盖;
|
||||
- 能定位 sourceId 的 mutation 只推进对应 source revision;无法定位 sourceId 的 mutation继续 global fail closed;
|
||||
- Segment content revision 由:
|
||||
- 该 Segment 的 dependent source revisions;
|
||||
- 当前 Segment layer fingerprint / layer mutation;
|
||||
- 必须全局影响 retained 的 global revision 类别;
|
||||
- topology / camera / viewport 等既有安全条件
|
||||
共同决定。
|
||||
|
||||
Source/Tile mutation 不应推进“所有 Segment 都消费的 retained cache revision”。
|
||||
|
||||
必须避免:
|
||||
|
||||
- 用屏幕位置猜 dependency;
|
||||
- 在 renderer 中硬编码 source/layer ID;
|
||||
- 从 STEP-099 diagnostics JSON 反向驱动正式 renderer;
|
||||
- 建立 diagnostics 与 production 两套互相漂移的 dependency 真相。
|
||||
|
||||
如果需要提取 production-grade dependency helper,应让 diagnostics 和正式逻辑共享同一只读推导源。正式 renderer 不得从 diagnostics JSON 或 `RetainedUnderlayRuntimeState` 反向取 dependency。
|
||||
|
||||
Source-scoped revision 属正式 correctness state,不得沿用 STEP-099 diagnostics 的“best-effort 记录失败可忽略”语义。正式 revision/dependency state 若无法可靠更新,必须 conservative global invalidation/fail closed。
|
||||
|
||||
---
|
||||
|
||||
## 5. Global mutation 仍可保守处理
|
||||
|
||||
本任务只要求 **Source/Tile mutation** 与明确 Source readiness 局部化。
|
||||
|
||||
以下无法安全局部化的事件,本轮允许继续 invalidate/fail closed 全 retained:
|
||||
|
||||
- context reset;
|
||||
- style reload / topology change;
|
||||
- image mutation;
|
||||
- glyph mutation;
|
||||
- sprite/image manager 全局 readiness;
|
||||
- placement / symbol fade / glyph/image/sprite 全局状态;
|
||||
- camera / transform / viewport / resize / pixel ratio;
|
||||
- debug mode;
|
||||
- 3D / unsupported / legacy ordering;
|
||||
- 无法定位 dependency 的 mutation;
|
||||
- 真正 unknown/global dependency。
|
||||
|
||||
注意:**Source 自身的 loaded / fading 不属于这里的 global fail-closed 项**;若 source dependency 可定位,必须按 Segment 局部判断。Fast Path 的 global source/fading gate除外,本轮保持原状。
|
||||
|
||||
完成报告必须列出:
|
||||
|
||||
- 已局部化的 mutation 类型;
|
||||
- 仍为 global 的 mutation 类型;
|
||||
- 为什么保守处理。
|
||||
|
||||
---
|
||||
|
||||
## 6. Fast Path 本轮严格不改
|
||||
|
||||
STEP-099 已确认 Fast Path 仍有 global gates:
|
||||
|
||||
- global update signature(sources/layers/images 等);
|
||||
- global content revision;
|
||||
- global fade;
|
||||
- pendingChanges;
|
||||
- placement/light/non-particle transition/repaint;
|
||||
- annotation mutation;
|
||||
- topology/boundary/revision/cache validity;
|
||||
- `singleContinuousSegment`。
|
||||
|
||||
STEP-100 **禁止修改这些 gate 的语义**。
|
||||
|
||||
允许:
|
||||
|
||||
- Source mutation 后 Fast Path miss;
|
||||
- transient source readiness 时 Fast Path fail closed;
|
||||
- full CPU renderer 被调用。
|
||||
|
||||
但进入 full CPU renderer 后,必须能做到:
|
||||
|
||||
```text
|
||||
R0 retained cache reuse
|
||||
R1 rebuild/fallback
|
||||
```
|
||||
|
||||
这正是本任务验收目标。
|
||||
|
||||
Multi-continuous Fast Path 明确不在本任务。
|
||||
|
||||
---
|
||||
|
||||
## 7. Render 顺序与混合 Build/Reuse/Fallback
|
||||
|
||||
实现 per-segment mixed decisions 时必须保持原 MapLibre 绘制语义:
|
||||
|
||||
- Style Z-order;
|
||||
- global layerIndex;
|
||||
- opaque reverse traversal;
|
||||
- translucent forward traversal;
|
||||
- currentLayer;
|
||||
- depthRangeSize;
|
||||
- legacy item 不双绘;
|
||||
- debug overlay 不重复;
|
||||
- retained texture composite 不改变 segment 顺序。
|
||||
|
||||
推荐的逻辑行为:
|
||||
|
||||
```text
|
||||
for segment in Style order:
|
||||
RETAINED + Build:
|
||||
build cache
|
||||
composite cache
|
||||
RETAINED + Reuse:
|
||||
composite cache
|
||||
RETAINED + Fallback:
|
||||
live render only this range
|
||||
CONTINUOUS:
|
||||
live render this range
|
||||
```
|
||||
|
||||
实际代码可按现有 RenderPass 安全实现,不要求机械照搬伪代码。
|
||||
|
||||
必须特别检查:
|
||||
|
||||
- 一个 R fallback 不得导致另一个 R live redraw;
|
||||
- resource prepare 在 Build 前失败时,必须先把该 Segment 本帧 decision 改为 Fallback,再进入主 buffer 绘制;
|
||||
- retained build 中途失败时,必须 invalidate/标记该 Segment失败并把该 Segment 本帧 decision 改为 Fallback,禁止 composite 旧纹理;
|
||||
- 如果失败意味着 context / shared GPU state 已不可信,允许全局 fail closed;
|
||||
- fallback range 的 legacy items 只绘一次,不得因 per-segment 主循环重复 render;
|
||||
- debug overlay 全帧只绘一次;
|
||||
- Segment 间 Opaque/Translucent 语义不得被破坏;
|
||||
- 相邻 R、多个 C、透明叠加与 legacy scene 不能因 mixed execution 出现漏绘/双绘。
|
||||
|
||||
推荐执行顺序:先完成各 Segment resource prepare 与必要离屏 Build;再 clear 主 buffer;最后按 Style Segment 顺序逐段选择 cached composite 或 live range render。
|
||||
|
||||
---
|
||||
|
||||
## 8. Diagnostics 必须继续可证明局部性
|
||||
|
||||
保留 STEP-099 diagnostics,并 additive 增加足够字段,使 RDC 能验证正式行为。
|
||||
|
||||
每个 RETAINED Segment 至少应能观察:
|
||||
|
||||
- dependentSourceIds;
|
||||
- segment readiness / support;
|
||||
- segment-local source/content revision 或等价可证明值;
|
||||
- Build / Reuse / Fallback 的**最终本帧 execution decision**;
|
||||
- build/reuse/fallback/invalidation count;
|
||||
- last invalidation/fallback reason。
|
||||
|
||||
Source mutation 继续输出:
|
||||
|
||||
- sourceId;
|
||||
- sequence/reason/action;
|
||||
- affectedSegmentIndices。
|
||||
|
||||
要求:
|
||||
|
||||
```text
|
||||
step099-dynamic mutation
|
||||
affected=[R1]
|
||||
R0 revision unchanged
|
||||
R0 cache reuse++
|
||||
R0 build unchanged
|
||||
R0 invalidation unchanged
|
||||
```
|
||||
|
||||
Diagnostics 仍是 pull/event low-frequency,不允许逐帧高频 spam。
|
||||
|
||||
---
|
||||
|
||||
## 9. Android Test App
|
||||
|
||||
复用 STEP-099,不另建独立 App。
|
||||
|
||||
至少保留并完善:
|
||||
|
||||
### T1 STATIC
|
||||
|
||||
验证静态 R-C-R:
|
||||
|
||||
- loaded settle;
|
||||
- R0/R1 build 一次后稳定 reuse;
|
||||
- 不产生持续 rebuild。
|
||||
|
||||
### T2 GEOJSON_1HZ
|
||||
|
||||
两个 GeoJSON source 交错,各约 1 Hz。
|
||||
|
||||
重点:
|
||||
|
||||
- global loaded transient false window 可以继续存在;
|
||||
- 只引用某 source 的 retained Segment 才允许因该 source mutation 改变;
|
||||
- 无关 retained Segment build/invalidation 不得跟随增长。
|
||||
|
||||
### T3 PERMANENT_UNREADY
|
||||
|
||||
保留一个:
|
||||
|
||||
- no layer;
|
||||
- enabled=false;
|
||||
- isLoaded=false;
|
||||
- no field submission
|
||||
|
||||
的 RasterArray source。
|
||||
|
||||
预期:
|
||||
|
||||
- global renderOrchestratorLoaded 仍可为 false;
|
||||
- 但无关 R0/R1 retained cache 不应因它持续 `style-not-loaded` fallback;
|
||||
- 能安全判断依赖的 retained Segment应继续 build/reuse。
|
||||
|
||||
### T4 R-C-R(主验收)
|
||||
|
||||
必须保持清晰依赖:
|
||||
|
||||
```text
|
||||
R0 -> dedicated static source only
|
||||
C -> WeatherColor/Particle field
|
||||
R1 -> dedicated dynamic GeoJSON source
|
||||
```
|
||||
|
||||
运行动态 GeoJSON 约 1 Hz。
|
||||
|
||||
必须调整 fixture,避免 STEP-099 中 R1 再复用 R0 的 static source:
|
||||
|
||||
- R0 static source 只出现在 R0;
|
||||
- R1 dynamic source 只出现在 R1;
|
||||
- 若 MapLibre 内建 annotation source/layer 自动进入 R1,必须在 diagnostics 中列明,并保证验收窗口内 annotation 不发生 mutation;
|
||||
- 不允许用业务 ID 特判 renderer。
|
||||
|
||||
RDC 验收区间必须支持“settled baseline → 60 s pure dynamic-source mutation window → final”对比。每个 aggregate sample 同时记录 mutation source/sequence、各 Segment local revision、readiness、最终 execution decision 与 cache counters。
|
||||
|
||||
---
|
||||
|
||||
## 10. STEP-100 硬验收标准
|
||||
|
||||
### A. R0 locality
|
||||
|
||||
T4 settle 后记录 baseline。
|
||||
|
||||
随后至少 60 s dynamic GeoJSON:
|
||||
|
||||
- R0 `buildDelta == 0`
|
||||
- R0 `invalidationDelta == 0`
|
||||
- R0 `reuseDelta > 0`
|
||||
- R0 segment-local revision/content state 不因 R1 dynamic source mutation 改变
|
||||
- 即使某采样点 global `renderOrchestratorLoaded=false`,只要 R0 自身 prerequisites/dependencies ready,R0 final decision 仍可为 Reuse
|
||||
|
||||
如果为了安全出现明确 global event,应在日志中可解释并重新选择纯动态 mutation 区间;不能把 global event 混入后声称 PASS。
|
||||
|
||||
### B. R1 reacts
|
||||
|
||||
同一时间窗:
|
||||
|
||||
- R1 必须能观察到 source mutation;
|
||||
- R1 revision / build / invalidation / fallback 中至少有与动态 source 对应的实际变化;
|
||||
- `affectedSegmentIndices` 必须只包含真实依赖 Segment。
|
||||
|
||||
### C. global revision compatibility
|
||||
|
||||
- global `retainedUnderlayContentRevision` 允许并预期继续增长;
|
||||
- 不能通过停止 global revision 增长来制造 R0 PASS。
|
||||
|
||||
### D. readiness locality
|
||||
|
||||
T2 transient unloaded 或 T3 permanent unrelated unloaded 时:
|
||||
|
||||
- global loaded 可 false;
|
||||
- R0 不得仅因无关 source 的 unloaded 而 invalid/rebuild/fallback;
|
||||
- R0 cache 必须继续可复用。
|
||||
|
||||
### E. correctness
|
||||
|
||||
- 无 crash / ANR;
|
||||
- 无 stale cache composite;
|
||||
- 无 Segment Z-order 错乱;
|
||||
- 无 fallback range 双绘;
|
||||
- Cache OFF 仍安全走 live renderer;
|
||||
- plan invalid 仍 fail closed。
|
||||
|
||||
任一硬门槛失败,STEP-100 不得宣布 PASS,不得进入 V18。
|
||||
|
||||
---
|
||||
|
||||
## 11. Native 非 Jest 测试要求
|
||||
|
||||
Codex 必须新增或扩展现有 C++ 测试源码,至少覆盖:
|
||||
|
||||
1. source A mutation 只改变依赖 A 的 Segment revision;
|
||||
2. source A 被多个 Segment 共享时,对应多个 Segment 都变化;
|
||||
3. unrelated Segment revision 不变;
|
||||
4. source-less Segment 不制造 dependency;
|
||||
5. unknown/global dependency fail closed;
|
||||
6. unrelated source unloaded 不使安全 Segment readiness false;
|
||||
7. dependent source unloaded 使对应 Segment readiness false;
|
||||
8. R0 Reuse + R1 Fallback 的 mixed decision;
|
||||
9. 单 Segment build failure 不 composite stale cache;
|
||||
10. query diagnostics 不改变 revision/cache state;
|
||||
11. global non-source invalidation 仍影响所有 retained;
|
||||
12. source 删除后同 ID recreate 不误复用旧 Segment cache identity;
|
||||
13. dependent source fading 只影响对应 Segment;unrelated source fading 不影响安全 Segment;
|
||||
14. Cache OFF / invalid plan 行为不回归;
|
||||
15. 至少有一组能证明实际 mixed render 没有漏绘/双绘的 renderer-level draw-count、pixel 或现有等价测试入口;不能只用 decision unit test 代替。
|
||||
|
||||
不得修改测试断言去适应错误实现。
|
||||
|
||||
---
|
||||
|
||||
## 12. 允许修改范围
|
||||
|
||||
允许:
|
||||
|
||||
- RenderOrchestrator source mutation/readiness bookkeeping;
|
||||
- RenderSegment dependency production helper;
|
||||
- Renderer retained per-segment revision/readiness/decision;
|
||||
- RetainedUnderlayCache/RuntimeState 为局部状态增加必要字段;
|
||||
- additive diagnostics JSON/TS;
|
||||
- Android Validation Test App STEP-099 fixture/log;
|
||||
- 相关 Native C++ tests;
|
||||
- STEP-100 Step 记录/完成报告/开发记录。
|
||||
|
||||
禁止:
|
||||
|
||||
- NavSea;
|
||||
- Integration;
|
||||
- V17 tag/release worktree;
|
||||
- Source `isLoaded()` 定义;
|
||||
- Particle field/provider/network;
|
||||
- Fast Path gate semantic;
|
||||
- `singleContinuousSegment`;
|
||||
- multi-continuous Fast Path;
|
||||
- 自动改变 Style Z-order;
|
||||
- provider/business-specific source/layer ID;
|
||||
- unrelated cleanup/refactor;
|
||||
- Jest。
|
||||
|
||||
---
|
||||
|
||||
## 13. Codex 执行边界
|
||||
|
||||
Codex 本轮负责:
|
||||
|
||||
1. 读取实际源码和任务书;
|
||||
2. 实现代码;
|
||||
3. 编写 Native 非 Jest 测试源码;
|
||||
4. 更新 Android Test App 验证入口;
|
||||
5. 执行轻量静态检查,如 `git diff --check`;
|
||||
6. 生成中文完成报告。
|
||||
|
||||
Codex **不得执行**:
|
||||
|
||||
- Android Gradle / APK;
|
||||
- ADB / 安装 / 实机;
|
||||
- Xcode / iOS device;
|
||||
- V18 release;
|
||||
- tag;
|
||||
- push;
|
||||
- Jest。
|
||||
|
||||
本轮 Codex 完成报告固定:
|
||||
|
||||
`docs/报告/STEP-100_SourceScoped_RenderSegment局部失效_Codex完成报告_v1.md`
|
||||
|
||||
Step 记录固定:
|
||||
|
||||
`docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md`
|
||||
|
||||
完成报告必须写清:
|
||||
|
||||
- 实际 Codex model / reasoning effort;
|
||||
- start HEAD;
|
||||
- 修改文件;
|
||||
- 生产设计;
|
||||
- source revision/readiness 规则;
|
||||
- mixed retained decision 规则;
|
||||
- Fast Path 明确未改;
|
||||
- 测试源码新增情况;
|
||||
- 实际执行 / NOT RUN;
|
||||
- 风险与 RDC 验收点;
|
||||
- 未 commit / 未 push(如果确实如此)。
|
||||
|
||||
Codex 不 commit、不 push。由 RDC 验证通过后统一 Git 收尾。
|
||||
|
||||
---
|
||||
|
||||
## 14. RDC / ChatGPT 后续独立验收
|
||||
|
||||
Codex 返回后由 RDC 负责:
|
||||
|
||||
1. source/diff 审计;
|
||||
2. Native Mac 编译;
|
||||
3. Native focused + broad related C++ tests;
|
||||
4. Android x86_64 OpenGL validation APK 单次构建;
|
||||
5. Mac ADB 直连 FZG1-2;
|
||||
6. 自动跑 T1/T2/T3/T4;
|
||||
7. 解析 R0/R1 build/reuse/invalidation/revision/readiness;
|
||||
8. 确认硬验收标准 A~E;
|
||||
9. 若失败,回到 STEP-100 修复,不发布;
|
||||
10. 若全部 PASS,完成 STEP-100 Git push;
|
||||
11. 再按现有统一发行流程升级到:
|
||||
`maplibre-particle/rn/13.4.1-particle.18`
|
||||
12. V18 build/release/public artifact 验证通过后才宣布 V18 发布完成。
|
||||
|
||||
---
|
||||
|
||||
## 15. 停止条件
|
||||
|
||||
Codex 遇到以下任一情况必须停止并报告,不得扩大范围:
|
||||
|
||||
- 要修改 Source `isLoaded()` 才能继续;
|
||||
- 需要放宽 Fast Path gate;
|
||||
- 必须改变 singleContinuous / multi-continuous;
|
||||
- production dependency 无法可靠从现有 Style/RenderLayer/Source 推导;
|
||||
- per-segment fallback 会破坏 RenderPass/Z-order 且无法在当前范围安全解决;
|
||||
- 发现需要 NavSea/Integration 配合;
|
||||
- 需要业务 layer/source ID;
|
||||
- 需要改变 V17 release。
|
||||
|
||||
---
|
||||
|
||||
## 16. 本任务最终原则
|
||||
|
||||
STEP-100 的目标不是让“全局世界看起来 loaded”。
|
||||
|
||||
目标是:
|
||||
|
||||
> **即使某个 GeoJSON Source 正在变化或短暂 unready,只要 R0 与它没有依赖关系,R0 retained cache 就必须继续安全 reuse,不能被迫一起刷新。**
|
||||
|
||||
Fast Path 是否还能 miss,是后续独立问题。
|
||||
|
||||
只有这一点经过 Native + Android R-C-R 实测成立,才允许进入 V18 发布。
|
||||
405
docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v1.md
Normal file
405
docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v1.md
Normal file
@@ -0,0 +1,405 @@
|
||||
# STEP-101 RenderOrchestrator teardown 生命周期修复 — Codex 任务 v1
|
||||
|
||||
## 0. 任务目的
|
||||
|
||||
修复 Particle V18 Android 在正常 Renderer teardown / MapView reset 时发生的 Native SIGABRT。
|
||||
|
||||
现场已在 NavSea Android x86_64 设备稳定复现:
|
||||
|
||||
```text
|
||||
Fatal signal 6 (SIGABRT)
|
||||
thread: RenderThread 11
|
||||
|
||||
FORTIFY:
|
||||
pthread_mutex_lock called on a destroyed mutex
|
||||
|
||||
libmaplibre.so
|
||||
std::__ndk1::mutex::lock()
|
||||
...
|
||||
mbgl::android::MapRenderer::resetRenderer()
|
||||
```
|
||||
|
||||
V17 正常,V18 接入后稳定在启动约 9~11 秒触发。
|
||||
|
||||
本任务只修 Particle Native 生命周期;禁止在 NavSea 增加绕路、private wrapper、禁止阻止正常 MapView reset。
|
||||
|
||||
未经用户明确要求:不加入、不执行 Jest。
|
||||
|
||||
---
|
||||
|
||||
## 1. 冻结基线
|
||||
|
||||
工作树:
|
||||
|
||||
`/Users/tei/dev/MapLibreParticleLab-step101`
|
||||
|
||||
分支:
|
||||
|
||||
`step101-render-orchestrator-teardown`
|
||||
|
||||
起点必须是:
|
||||
|
||||
`origin/particle/main = d29e84c1a90daeea104b389d28b000c4f017d08d`
|
||||
|
||||
即正式 V18 Final HEAD。
|
||||
|
||||
V18 已发布,不得覆盖、重发或移动:
|
||||
|
||||
- `maplibre-particle/rn/13.4.1-particle.18`
|
||||
- Android `13.4.1-particle.18`
|
||||
- iOS `6.28.0-particle.18`
|
||||
|
||||
修复通过后由 RDC/ChatGPT 发布新不可变版本 V19。
|
||||
|
||||
---
|
||||
|
||||
## 2. 已知根因证据
|
||||
|
||||
V18 STEP-100 新增:
|
||||
|
||||
```cpp
|
||||
RenderSegmentRevisionBook retainedSegmentRevisions;
|
||||
mutable std::mutex readinessDiagnosticsMutex;
|
||||
uint64_t readinessEventSequence;
|
||||
std::unordered_map<std::string, SourceReadinessEventState> sourceReadinessEvents;
|
||||
RenderMutationDiagnostics lastReadinessMutation;
|
||||
```
|
||||
|
||||
多个 RenderSource / Tile observer callback 会进入:
|
||||
|
||||
```cpp
|
||||
advanceRetainedSourceRevision(...)
|
||||
recordReadinessMutation(...)
|
||||
onTileChanged(...)
|
||||
onTileAction(...)
|
||||
buildReadinessDiagnostics(...)
|
||||
retainedSegmentSourceState(...)
|
||||
```
|
||||
|
||||
并 lock:
|
||||
|
||||
```cpp
|
||||
readinessDiagnosticsMutex
|
||||
```
|
||||
|
||||
V18 当前成员声明中:
|
||||
|
||||
```text
|
||||
renderSources
|
||||
renderLayers
|
||||
...
|
||||
retainedSegmentRevisions
|
||||
...
|
||||
readinessDiagnosticsMutex
|
||||
sourceReadinessEvents
|
||||
...
|
||||
threadPool
|
||||
```
|
||||
|
||||
C++ 成员按声明反序析构,因此 readiness state / mutex 会早于 renderSources 销毁。
|
||||
|
||||
而 RenderSource 在创建时:
|
||||
|
||||
```cpp
|
||||
renderSource->setObserver(this);
|
||||
```
|
||||
|
||||
observer 指向 RenderOrchestrator。
|
||||
|
||||
当前 `RenderOrchestrator::~RenderOrchestrator()` 只:
|
||||
|
||||
1. contextLost 时标记 layer;
|
||||
2. `threadPool.waitForEmpty()`;
|
||||
|
||||
没有在 readiness state 仍有效时显式 detach RenderSource observer / 清理 renderSources。
|
||||
|
||||
因此 V18 存在明确生命周期危险:
|
||||
|
||||
```text
|
||||
readinessDiagnosticsMutex destroyed
|
||||
↓
|
||||
renderSources still destroying/alive
|
||||
↓
|
||||
late tile/source callback
|
||||
↓
|
||||
RenderOrchestrator::onTile*
|
||||
↓
|
||||
lock destroyed mutex
|
||||
↓
|
||||
FORTIFY SIGABRT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 修复原则
|
||||
|
||||
不要只依赖调整成员声明顺序。
|
||||
|
||||
本任务应建立显式 teardown contract:
|
||||
|
||||
```text
|
||||
RenderOrchestrator destructor begins
|
||||
↓
|
||||
stop / drain deferred scheduler work
|
||||
↓
|
||||
detach observer from every RenderSource
|
||||
↓
|
||||
destroy/clear renderSources while all orchestrator state is still alive
|
||||
↓
|
||||
only then continue member teardown
|
||||
```
|
||||
|
||||
同时 `RenderSource::setObserver(nullptr)` 必须安全恢复到 NullObserver。
|
||||
|
||||
目标是即使未来成员声明顺序变化,Source callback 也不能在 owner teardown 后访问 RenderOrchestrator。
|
||||
|
||||
---
|
||||
|
||||
## 4. 必做实现
|
||||
|
||||
### 4.1 RenderSource nullptr observer 语义
|
||||
|
||||
当前:
|
||||
|
||||
```cpp
|
||||
void RenderSource::setObserver(RenderSourceObserver* observer_) {
|
||||
observer = observer_;
|
||||
}
|
||||
```
|
||||
|
||||
改成与 Style Source / Layer 一致的安全语义:
|
||||
|
||||
```cpp
|
||||
observer = observer_ ? observer_ : &nullObserver;
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- constructor 仍默认 NullObserver;
|
||||
- `setObserver(nullptr)` 是合法 detach;
|
||||
- onTileChanged / onTileError / onTileAction 不需要加 null 分支;
|
||||
- 不吞掉正常有 observer 时的回调。
|
||||
|
||||
必须加非 Jest C++ 回归测试验证 nullptr detach 后 callback 不 crash、不调用旧 observer。
|
||||
|
||||
### 4.2 RenderOrchestrator 显式 teardown
|
||||
|
||||
在 `RenderOrchestrator::~RenderOrchestrator()` 中,在 readiness mutex/state 仍确定存活时:
|
||||
|
||||
1. 保留现有 contextLost layer handling;
|
||||
2. 等待已有 deferred work 完成;
|
||||
3. 对所有 `renderSources` 执行 observer detach;
|
||||
4. 显式 `renderSources.clear()`;
|
||||
5. 必要时对 glyph/image observer 也做已有 API 支持的 detach,但不要扩大范围;
|
||||
6. 不允许 teardown callback 再进入 RenderOrchestrator source callback。
|
||||
|
||||
顺序必须结合当前 TaggedScheduler / RenderSource / TilePyramid 的真实生命周期检查,而不是机械照抄上述编号。
|
||||
|
||||
如果存在“clear renderSources 本身可能产生新的 scheduler work”,需要设计第二次 drain 或更安全顺序,并在报告解释。
|
||||
|
||||
### 4.3 生命周期状态可选保护
|
||||
|
||||
如果仅 detach + explicit clear 仍无法证明并发安全,允许增加最小 teardown flag,例如 atomic bool,但只作为二次防线。
|
||||
|
||||
禁止用 flag 代替 observer detach。
|
||||
|
||||
禁止 catch FORTIFY;destroyed mutex abort 不是 C++ exception。
|
||||
|
||||
---
|
||||
|
||||
## 5. 不得修改
|
||||
|
||||
- STEP-100 Source→Segment locality 语义;
|
||||
- per-segment revision;
|
||||
- per-segment readiness;
|
||||
- mixed Build/Reuse/Fallback;
|
||||
- Fast Path gate;
|
||||
- singleContinuousSegment;
|
||||
- Source::isLoaded;
|
||||
- Style Z-order;
|
||||
- provider/network;
|
||||
- NavSea;
|
||||
- MapRenderer::resetRenderer 正常生命周期;
|
||||
- Android Activity 为避免 reset 而加 workaround。
|
||||
|
||||
---
|
||||
|
||||
## 6. 必做测试
|
||||
|
||||
### 6.1 Native 非 Jest
|
||||
|
||||
至少覆盖:
|
||||
|
||||
1. RenderSource 默认 NullObserver 可安全接 callback;
|
||||
2. setObserver(real) 正常转发;
|
||||
3. setObserver(nullptr) 后不再调用旧 observer;
|
||||
4. detach 后 onTileChanged / onTileError / onTileAction 安全;
|
||||
5. RenderOrchestrator teardown 有至少一个含 RenderSource 的场景;
|
||||
6. source callback 与 teardown 边界不访问已销毁 readiness state;
|
||||
7. STEP-100 locality 相关 focused tests 不回归。
|
||||
|
||||
如果现有测试设施不方便直接构造完整 RenderOrchestrator teardown,允许提取最小可测试 teardown helper,但禁止为了测试重构大范围生产结构。
|
||||
|
||||
### 6.2 Android Test App
|
||||
|
||||
在现有 ParticleCoreLayerValidationActivity / Validation App 增加专门 STEP-101 teardown 场景。
|
||||
|
||||
场景必须真实触发 Native Renderer teardown,而不是只 clear field。
|
||||
|
||||
至少提供:
|
||||
|
||||
```text
|
||||
T1 RESET_ONCE
|
||||
T2 RESET_LOOP_20
|
||||
T3 RESET_WITH_DYNAMIC_GEOJSON
|
||||
T4 RESET_WITH_DIAGNOSTICS
|
||||
```
|
||||
|
||||
目标:
|
||||
|
||||
- recreate/destroy MapView 或等价真实 renderer.reset;
|
||||
- 每轮等待足够让 source/tile work 进入正常运行;
|
||||
- T3 同时保持动态 GeoJSON/source mutation;
|
||||
- T4 在 reset 前调用一次 RendererRuntime diagnostics,模拟 NavSea probe 时序;
|
||||
- 20 次循环无 SIGABRT / crash / ANR;
|
||||
- reset 后新 renderer 能继续正常显示 / 获取 diagnostics。
|
||||
|
||||
不得通过延迟 reset、禁用 observer、关闭 GeoJSON 来逃避问题。
|
||||
|
||||
---
|
||||
|
||||
## 7. NavSea 现场等价验收
|
||||
|
||||
RDC 在 Codex 完成后独立执行。
|
||||
|
||||
Android 设备:
|
||||
|
||||
`192.168.200.139:5555`
|
||||
|
||||
必须显式 `adb -s`。
|
||||
|
||||
最终至少执行:
|
||||
|
||||
1. Validation App STEP-101 T1~T4;
|
||||
2. crash buffer 检查;
|
||||
3. 20 次 reset/recreate;
|
||||
4. 安装 V19 进 NavSea 后做真实启动;
|
||||
5. 保留 NavSea 当前 `key={map-style-generation-...}` 正常 reset 触发条件;
|
||||
6. 保留/启用一次 RenderSegment diagnostics probe A/B;
|
||||
7. 连续至少 5 次冷启动,每次观察 >30 秒;
|
||||
8. 不允许出现:
|
||||
- `pthread_mutex_lock called on a destroyed mutex`
|
||||
- SIGABRT RenderThread
|
||||
- `MapRenderer::resetRenderer` teardown crash。
|
||||
|
||||
---
|
||||
|
||||
## 8. STEP-100 回归验收
|
||||
|
||||
修复 teardown 后必须确认 V18 的核心 locality 没被破坏。
|
||||
|
||||
至少复测 T4 R-C-R:
|
||||
|
||||
无关 R0:
|
||||
|
||||
```text
|
||||
buildDelta = 0
|
||||
invalidationDelta = 0
|
||||
reuseDelta > 0
|
||||
```
|
||||
|
||||
动态 R1 正常 build/invalidation。
|
||||
|
||||
禁止通过删除 mutex/readiness/revision 功能来修 crash。
|
||||
|
||||
---
|
||||
|
||||
## 9. Codex 执行边界
|
||||
|
||||
Codex负责:
|
||||
|
||||
- 只读检查真实 V18 代码;
|
||||
- 实现最小 Native 修复;
|
||||
- 编写/更新非 Jest C++ tests;
|
||||
- 更新 Android Validation STEP-101 场景;
|
||||
- 静态检查;
|
||||
- 生成报告。
|
||||
|
||||
Codex不负责:
|
||||
|
||||
- NavSea 修改;
|
||||
- V19 release;
|
||||
- tag;
|
||||
- publish;
|
||||
- 覆盖 V18;
|
||||
- Jest。
|
||||
|
||||
Mac Native 开发/编译原则保持;不要使用 Linux Native 环境。
|
||||
|
||||
---
|
||||
|
||||
## 10. 完成报告
|
||||
|
||||
生成:
|
||||
|
||||
`docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md`
|
||||
|
||||
Step 记录:
|
||||
|
||||
`docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md`
|
||||
|
||||
报告必须写:
|
||||
|
||||
- root cause;
|
||||
- start HEAD;
|
||||
- 修改文件;
|
||||
- destructor teardown 顺序;
|
||||
- observer detach 语义;
|
||||
- 为什么不依赖成员声明顺序;
|
||||
- tests added;
|
||||
- 实际运行 / NOT RUN;
|
||||
- STEP-100 locality 是否保持;
|
||||
- 风险;
|
||||
- 未发布 V19(Codex 阶段)。
|
||||
|
||||
Codex阶段不 push,由 RDC 验收后统一 commit/push。
|
||||
|
||||
---
|
||||
|
||||
## 11. V19 发布门槛
|
||||
|
||||
仅当:
|
||||
|
||||
- Native compile PASS;
|
||||
- focused C++ tests PASS;
|
||||
- Android Validation teardown loop PASS;
|
||||
- STEP-100 locality regression PASS;
|
||||
- NavSea V19 真实 5 次冷启动 >30s PASS;
|
||||
- crash buffer 无 destroyed mutex;
|
||||
|
||||
才允许发布:
|
||||
|
||||
- RN / Android:`13.4.1-particle.19`
|
||||
- iOS:`6.28.0-particle.19`
|
||||
- tag:`maplibre-particle/rn/13.4.1-particle.19`
|
||||
|
||||
V18 保持不可变,不覆盖、不删除。
|
||||
|
||||
---
|
||||
|
||||
## 12. 最终原则
|
||||
|
||||
正常:
|
||||
|
||||
```text
|
||||
MapView unmount
|
||||
Style generation replacement
|
||||
renderer.reset()
|
||||
Activity recreate
|
||||
```
|
||||
|
||||
都是合法生命周期。
|
||||
|
||||
Particle Native 必须做到:
|
||||
|
||||
> Renderer teardown 永远不能因为 observer late callback 访问已销毁 mutex/state 而杀掉宿主 App。
|
||||
510
docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v2.md
Normal file
510
docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v2.md
Normal file
@@ -0,0 +1,510 @@
|
||||
# STEP-101 RenderOrchestrator teardown 生命周期修复 — Codex 任务 v1
|
||||
|
||||
## 0. 任务目的
|
||||
|
||||
修复 Particle V18 Android 在正常 Renderer teardown / MapView reset 时发生的 Native SIGABRT。
|
||||
|
||||
现场已在 NavSea Android x86_64 设备稳定复现:
|
||||
|
||||
```text
|
||||
Fatal signal 6 (SIGABRT)
|
||||
thread: RenderThread 11
|
||||
|
||||
FORTIFY:
|
||||
pthread_mutex_lock called on a destroyed mutex
|
||||
|
||||
libmaplibre.so
|
||||
std::__ndk1::mutex::lock()
|
||||
...
|
||||
mbgl::android::MapRenderer::resetRenderer()
|
||||
```
|
||||
|
||||
V17 正常,V18 接入后稳定在启动约 9~11 秒触发。
|
||||
|
||||
本任务只修 Particle Native 生命周期;禁止在 NavSea 增加绕路、private wrapper、禁止阻止正常 MapView reset。
|
||||
|
||||
未经用户明确要求:不加入、不执行 Jest。
|
||||
|
||||
---
|
||||
|
||||
## 1. 冻结基线
|
||||
|
||||
工作树:
|
||||
|
||||
`/Users/tei/dev/MapLibreParticleLab-step101`
|
||||
|
||||
分支:
|
||||
|
||||
`step101-render-orchestrator-teardown`
|
||||
|
||||
起点必须是:
|
||||
|
||||
`origin/particle/main = d29e84c1a90daeea104b389d28b000c4f017d08d`
|
||||
|
||||
即正式 V18 Final HEAD。
|
||||
|
||||
V18 已发布,不得覆盖、重发或移动:
|
||||
|
||||
- `maplibre-particle/rn/13.4.1-particle.18`
|
||||
- Android `13.4.1-particle.18`
|
||||
- iOS `6.28.0-particle.18`
|
||||
|
||||
修复通过后由 RDC/ChatGPT 发布新不可变版本 V19。
|
||||
|
||||
---
|
||||
|
||||
## 2. 已知根因证据
|
||||
|
||||
V18 STEP-100 新增:
|
||||
|
||||
```cpp
|
||||
RenderSegmentRevisionBook retainedSegmentRevisions;
|
||||
mutable std::mutex readinessDiagnosticsMutex;
|
||||
uint64_t readinessEventSequence;
|
||||
std::unordered_map<std::string, SourceReadinessEventState> sourceReadinessEvents;
|
||||
RenderMutationDiagnostics lastReadinessMutation;
|
||||
```
|
||||
|
||||
多个 RenderSource / Tile observer callback 会进入:
|
||||
|
||||
```cpp
|
||||
advanceRetainedSourceRevision(...)
|
||||
recordReadinessMutation(...)
|
||||
onTileChanged(...)
|
||||
onTileAction(...)
|
||||
buildReadinessDiagnostics(...)
|
||||
retainedSegmentSourceState(...)
|
||||
```
|
||||
|
||||
并 lock:
|
||||
|
||||
```cpp
|
||||
readinessDiagnosticsMutex
|
||||
```
|
||||
|
||||
V18 当前成员声明中:
|
||||
|
||||
```text
|
||||
renderSources
|
||||
renderLayers
|
||||
...
|
||||
retainedSegmentRevisions
|
||||
...
|
||||
readinessDiagnosticsMutex
|
||||
sourceReadinessEvents
|
||||
...
|
||||
threadPool
|
||||
```
|
||||
|
||||
C++ 成员按声明反序析构,因此 readiness state / mutex 会早于 renderSources 销毁。
|
||||
|
||||
而 RenderSource 在创建时:
|
||||
|
||||
```cpp
|
||||
renderSource->setObserver(this);
|
||||
```
|
||||
|
||||
observer 指向 RenderOrchestrator。
|
||||
|
||||
当前 `RenderOrchestrator::~RenderOrchestrator()` 只:
|
||||
|
||||
1. contextLost 时标记 layer;
|
||||
2. `threadPool.waitForEmpty()`;
|
||||
|
||||
没有在 readiness state 仍有效时显式 detach RenderSource observer / 清理 renderSources。
|
||||
|
||||
因此 V18 存在明确生命周期危险:
|
||||
|
||||
```text
|
||||
readinessDiagnosticsMutex destroyed
|
||||
↓
|
||||
renderSources still destroying/alive
|
||||
↓
|
||||
late tile/source callback
|
||||
↓
|
||||
RenderOrchestrator::onTile*
|
||||
↓
|
||||
lock destroyed mutex
|
||||
↓
|
||||
FORTIFY SIGABRT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 修复原则
|
||||
|
||||
不要只依赖调整成员声明顺序。
|
||||
|
||||
本任务应建立显式 teardown contract:
|
||||
|
||||
```text
|
||||
RenderOrchestrator destructor begins
|
||||
↓
|
||||
stop / drain deferred scheduler work
|
||||
↓
|
||||
detach observer from every RenderSource
|
||||
↓
|
||||
destroy/clear renderSources while all orchestrator state is still alive
|
||||
↓
|
||||
only then continue member teardown
|
||||
```
|
||||
|
||||
同时 `RenderSource::setObserver(nullptr)` 必须安全恢复到 NullObserver。
|
||||
|
||||
目标是即使未来成员声明顺序变化,Source callback 也不能在 owner teardown 后访问 RenderOrchestrator。
|
||||
|
||||
---
|
||||
|
||||
## 4. 必做实现
|
||||
|
||||
### 4.1 RenderSource nullptr observer 语义
|
||||
|
||||
当前:
|
||||
|
||||
```cpp
|
||||
void RenderSource::setObserver(RenderSourceObserver* observer_) {
|
||||
observer = observer_;
|
||||
}
|
||||
```
|
||||
|
||||
改成与 Style Source / Layer 一致的安全语义:
|
||||
|
||||
```cpp
|
||||
observer = observer_ ? observer_ : &nullObserver;
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- constructor 仍默认 NullObserver;
|
||||
- `setObserver(nullptr)` 是合法 detach;
|
||||
- onTileChanged / onTileError / onTileAction 不需要加 null 分支;
|
||||
- 不吞掉正常有 observer 时的回调。
|
||||
|
||||
必须加非 Jest C++ 回归测试验证 nullptr detach 后 callback 不 crash、不调用旧 observer。
|
||||
|
||||
### 4.2 RenderOrchestrator 显式 teardown
|
||||
|
||||
在 `RenderOrchestrator::~RenderOrchestrator()` 中,在 readiness mutex/state 仍确定存活时:
|
||||
|
||||
1. 保留现有 contextLost layer handling;
|
||||
2. 等待已有 deferred work 完成;
|
||||
3. 对所有 `renderSources` 执行 observer detach;
|
||||
4. 显式 `renderSources.clear()`;
|
||||
5. 必要时对 glyph/image observer 也做已有 API 支持的 detach,但不要扩大范围;
|
||||
6. 不允许 teardown callback 再进入 RenderOrchestrator source callback。
|
||||
|
||||
顺序必须结合当前 TaggedScheduler / RenderSource / TilePyramid 的真实生命周期检查,而不是机械照抄上述编号。
|
||||
|
||||
如果存在“clear renderSources 本身可能产生新的 scheduler work”,需要设计第二次 drain 或更安全顺序,并在报告解释。
|
||||
|
||||
### 4.3 生命周期状态可选保护
|
||||
|
||||
如果仅 detach + explicit clear 仍无法证明并发安全,允许增加最小 teardown flag,例如 atomic bool,但只作为二次防线。
|
||||
|
||||
禁止用 flag 代替 observer detach。
|
||||
|
||||
禁止 catch FORTIFY;destroyed mutex abort 不是 C++ exception。
|
||||
|
||||
---
|
||||
|
||||
## 5. 不得修改
|
||||
|
||||
- STEP-100 Source→Segment locality 语义;
|
||||
- per-segment revision;
|
||||
- per-segment readiness;
|
||||
- mixed Build/Reuse/Fallback;
|
||||
- Fast Path gate;
|
||||
- singleContinuousSegment;
|
||||
- Source::isLoaded;
|
||||
- Style Z-order;
|
||||
- provider/network;
|
||||
- NavSea;
|
||||
- MapRenderer::resetRenderer 正常生命周期;
|
||||
- Android Activity 为避免 reset 而加 workaround。
|
||||
|
||||
---
|
||||
|
||||
## 6. 必做测试
|
||||
|
||||
### 6.1 Native 非 Jest
|
||||
|
||||
至少覆盖:
|
||||
|
||||
1. RenderSource 默认 NullObserver 可安全接 callback;
|
||||
2. setObserver(real) 正常转发;
|
||||
3. setObserver(nullptr) 后不再调用旧 observer;
|
||||
4. detach 后 onTileChanged / onTileError / onTileAction 安全;
|
||||
5. RenderOrchestrator teardown 有至少一个含 RenderSource 的场景;
|
||||
6. source callback 与 teardown 边界不访问已销毁 readiness state;
|
||||
7. STEP-100 locality 相关 focused tests 不回归。
|
||||
|
||||
如果现有测试设施不方便直接构造完整 RenderOrchestrator teardown,允许提取最小可测试 teardown helper,但禁止为了测试重构大范围生产结构。
|
||||
|
||||
### 6.2 Android Test App
|
||||
|
||||
在现有 ParticleCoreLayerValidationActivity / Validation App 增加专门 STEP-101 teardown 场景。
|
||||
|
||||
场景必须真实触发 Native Renderer teardown,而不是只 clear field。
|
||||
|
||||
至少提供:
|
||||
|
||||
```text
|
||||
T1 RESET_ONCE
|
||||
T2 RESET_LOOP_20
|
||||
T3 RESET_WITH_DYNAMIC_GEOJSON
|
||||
T4 RESET_WITH_DIAGNOSTICS
|
||||
```
|
||||
|
||||
目标:
|
||||
|
||||
- recreate/destroy MapView 或等价真实 renderer.reset;
|
||||
- 每轮等待足够让 source/tile work 进入正常运行;
|
||||
- T3 同时保持动态 GeoJSON/source mutation;
|
||||
- T4 在 reset 前调用一次 RendererRuntime diagnostics,模拟 NavSea probe 时序;
|
||||
- 20 次循环无 SIGABRT / crash / ANR;
|
||||
- reset 后新 renderer 能继续正常显示 / 获取 diagnostics。
|
||||
|
||||
不得通过延迟 reset、禁用 observer、关闭 GeoJSON 来逃避问题。
|
||||
|
||||
---
|
||||
|
||||
## 7. NavSea 现场等价验收
|
||||
|
||||
RDC 在 Codex 完成后独立执行。
|
||||
|
||||
Android 设备:
|
||||
|
||||
`192.168.200.139:5555`
|
||||
|
||||
必须显式 `adb -s`。
|
||||
|
||||
最终至少执行:
|
||||
|
||||
1. Validation App STEP-101 T1~T4;
|
||||
2. crash buffer 检查;
|
||||
3. 20 次 reset/recreate;
|
||||
4. 安装 V19 进 NavSea 后做真实启动;
|
||||
5. 保留 NavSea 当前 `key={map-style-generation-...}` 正常 reset 触发条件;
|
||||
6. 保留/启用一次 RenderSegment diagnostics probe A/B;
|
||||
7. 连续至少 5 次冷启动,每次观察 >30 秒;
|
||||
8. 不允许出现:
|
||||
- `pthread_mutex_lock called on a destroyed mutex`
|
||||
- SIGABRT RenderThread
|
||||
- `MapRenderer::resetRenderer` teardown crash。
|
||||
|
||||
---
|
||||
|
||||
## 8. STEP-100 回归验收
|
||||
|
||||
修复 teardown 后必须确认 V18 的核心 locality 没被破坏。
|
||||
|
||||
至少复测 T4 R-C-R:
|
||||
|
||||
无关 R0:
|
||||
|
||||
```text
|
||||
buildDelta = 0
|
||||
invalidationDelta = 0
|
||||
reuseDelta > 0
|
||||
```
|
||||
|
||||
动态 R1 正常 build/invalidation。
|
||||
|
||||
禁止通过删除 mutex/readiness/revision 功能来修 crash。
|
||||
|
||||
---
|
||||
|
||||
## 9. Codex 执行边界
|
||||
|
||||
Codex负责:
|
||||
|
||||
- 只读检查真实 V18 代码;
|
||||
- 实现最小 Native 修复;
|
||||
- 编写/更新非 Jest C++ tests;
|
||||
- 更新 Android Validation STEP-101 场景;
|
||||
- 静态检查;
|
||||
- 生成报告。
|
||||
|
||||
Codex不负责:
|
||||
|
||||
- NavSea 修改;
|
||||
- V19 release;
|
||||
- tag;
|
||||
- publish;
|
||||
- 覆盖 V18;
|
||||
- Jest。
|
||||
|
||||
Mac Native 开发/编译原则保持;不要使用 Linux Native 环境。
|
||||
|
||||
---
|
||||
|
||||
## 10. 完成报告
|
||||
|
||||
生成:
|
||||
|
||||
`docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md`
|
||||
|
||||
Step 记录:
|
||||
|
||||
`docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md`
|
||||
|
||||
报告必须写:
|
||||
|
||||
- root cause;
|
||||
- start HEAD;
|
||||
- 修改文件;
|
||||
- destructor teardown 顺序;
|
||||
- observer detach 语义;
|
||||
- 为什么不依赖成员声明顺序;
|
||||
- tests added;
|
||||
- 实际运行 / NOT RUN;
|
||||
- STEP-100 locality 是否保持;
|
||||
- 风险;
|
||||
- 未发布 V19(Codex 阶段)。
|
||||
|
||||
Codex阶段不 push,由 RDC 验收后统一 commit/push。
|
||||
|
||||
---
|
||||
|
||||
## 11. V19 发布门槛
|
||||
|
||||
仅当:
|
||||
|
||||
- Native compile PASS;
|
||||
- focused C++ tests PASS;
|
||||
- Android Validation teardown loop PASS;
|
||||
- STEP-100 locality regression PASS;
|
||||
- NavSea V19 真实 5 次冷启动 >30s PASS;
|
||||
- crash buffer 无 destroyed mutex;
|
||||
|
||||
才允许发布:
|
||||
|
||||
- RN / Android:`13.4.1-particle.19`
|
||||
- iOS:`6.28.0-particle.19`
|
||||
- tag:`maplibre-particle/rn/13.4.1-particle.19`
|
||||
|
||||
V18 保持不可变,不覆盖、不删除。
|
||||
|
||||
---
|
||||
|
||||
## 12. 最终原则
|
||||
|
||||
正常:
|
||||
|
||||
```text
|
||||
MapView unmount
|
||||
Style generation replacement
|
||||
renderer.reset()
|
||||
Activity recreate
|
||||
```
|
||||
|
||||
都是合法生命周期。
|
||||
|
||||
Particle Native 必须做到:
|
||||
|
||||
> Renderer teardown 永远不能因为 observer late callback 访问已销毁 mutex/state 而杀掉宿主 App。
|
||||
|
||||
---
|
||||
|
||||
# 13. Codex 6-Sol High 架构评审修订(v2,优先级高于前文冲突表述)
|
||||
|
||||
只读评审结论:`APPROVE_WITH_CHANGES`。
|
||||
|
||||
## 13.1 根因表述修正
|
||||
|
||||
当前已经**代码层证实**存在生命周期漏洞:`readinessDiagnosticsMutex` / readiness state 的析构顺序早于 `renderSources`,而 `RenderSource` observer 仍指向 `RenderOrchestrator`;source/tile teardown 可同步或延迟回调进入 `onTileAction/onTileChanged` 并访问 mutex。
|
||||
|
||||
该漏洞与现场 `pthread_mutex_lock called on a destroyed mutex` 高度吻合;在没有完整符号化 crash stack 精确到具体 callback 前,不把“唯一现场根因”写成数学意义上的唯一证明。
|
||||
|
||||
## 13.2 teardown 必须双 drain
|
||||
|
||||
最低安全顺序调整为:
|
||||
|
||||
```text
|
||||
contextLost layer handling
|
||||
↓
|
||||
第一次 threadPool.waitForEmpty()
|
||||
↓
|
||||
detach RenderSource observers
|
||||
↓
|
||||
detach GlyphManager / ImageManager observers
|
||||
↓
|
||||
renderSources.clear() // readiness mutex/state 此时仍存活
|
||||
↓
|
||||
第二次 threadPool.waitForEmpty()
|
||||
↓
|
||||
继续既有 backend/context render-job teardown
|
||||
↓
|
||||
成员析构
|
||||
```
|
||||
|
||||
原因:source/tile destruction 本身可能投递新的 tagged cleanup/deferred work,第一次 drain 不能覆盖 clear 过程中产生的新任务。
|
||||
|
||||
同时必须审计并在报告说明:`TaggedScheduler::waitForEmpty()` 不等于清空 `runOnRenderThread` 队列;后者依赖现有 backend/context render-job 生命周期。不得声称双 drain 自动证明所有 render-thread job 已清空。
|
||||
|
||||
## 13.3 Observer detach 范围
|
||||
|
||||
除了所有 `RenderSource`:
|
||||
|
||||
- `glyphManager->setObserver(nullptr)`;
|
||||
- `imageManager->setObserver(nullptr)`;
|
||||
|
||||
也应在 owner teardown 时显式 detach(前提是现有 API 的 nullptr 已安全回 NullObserver)。
|
||||
|
||||
`RenderSource::setObserver(nullptr)` 必须改为 NullObserver fallback。
|
||||
|
||||
不得直接给 `TilePyramid` / `Tile` observer 写 nullptr,除非其 API 本身有同等安全语义;本任务不扩展到这一步。
|
||||
|
||||
## 13.4 teardown flag
|
||||
|
||||
默认不增加 teardown atomic flag。
|
||||
|
||||
若实现审计发现真实并发 callback 不受 renderer thread / tagged scheduler 串行保证约束,必须先报告;atomic flag 只能二次防护,不能替代 observer detach、drain 和可等待取消。
|
||||
|
||||
## 13.5 Android Validation 证据升级
|
||||
|
||||
T1~T4 每一轮必须能证明发生了**真实 Native renderer reset**:
|
||||
|
||||
- 旧 MapView detach / SurfaceView detach;
|
||||
- `nativeReset()`;
|
||||
- `MapRenderer::resetRenderer()` 进入/完成;
|
||||
- 新 renderer style ready;
|
||||
- 新 renderer diagnostics 可用。
|
||||
|
||||
不能把 `setStyle()`、clear field、只读 diagnostics 当成 teardown 测试。
|
||||
|
||||
推荐按代次销毁并重建 MapView,或 Activity recreate;T2 至少 20 次已确认 reset。
|
||||
|
||||
T3:每轮 reset 前保持动态 GeoJSON/source mutation。
|
||||
|
||||
T4:每轮 reset 前调用一次 Native diagnostics,模拟 NavSea 8 秒 probe 的等价条件。
|
||||
|
||||
每轮应等待 source/tile 确实进入运行态,而不是只按固定 sleep 机械计数。
|
||||
|
||||
## 13.6 测试必须覆盖真实 source teardown
|
||||
|
||||
空 `RenderOrchestrator` 析构测试不够。
|
||||
|
||||
非 Jest C++ 测试至少需要一个含 RenderSource / tile callback 的 teardown 场景,使 source cancellation / callback 能在销毁边界发生,并验证旧 observer 不再收到回调。
|
||||
|
||||
## 13.7 正常 source erase / clearData 审计
|
||||
|
||||
必须只读检查 `clearData()` 和正常 source erase/replacement 路径,因为它们也会销毁 RenderSource。
|
||||
|
||||
本任务主要修 owner teardown;不得未经证明把 destructor 修复泛化为“所有 source replacement 异步问题均已解决”。若发现相同 observer 生命周期漏洞在正常 erase 路径存在,应做最小同源修复或明确 BLOCK,不得隐藏。
|
||||
|
||||
## 13.8 V19 候选验收顺序
|
||||
|
||||
V19 发布门槛修正为:
|
||||
|
||||
```text
|
||||
STEP-101 source code PASS
|
||||
→ 构建未公开的 V19 candidate artifacts
|
||||
→ Validation App 实机 PASS
|
||||
→ NavSea 使用 candidate 做 5 次冷启动/teardown 验收
|
||||
→ 公开发布不可变 V19
|
||||
→ public readback
|
||||
→ finalize/tag
|
||||
```
|
||||
|
||||
不得先公开 V19 再做 NavSea 验收。
|
||||
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。
|
||||
132
docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md
Normal file
132
docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# STEP-100 SourceScoped RenderSegment 局部失效记录 v1
|
||||
|
||||
状态:**CLOSED / PASS(Android runtime)**
|
||||
日期:2026-10-06
|
||||
|
||||
## 基线
|
||||
|
||||
- worktree:`/Users/tei/dev/MapLibreParticleLab-step100`
|
||||
- branch:`step100-segment-local-source-invalidation`
|
||||
- start HEAD:`c4b481fd2c5b5da20d31f6ae54011f0552d7dbd0`
|
||||
- V17 tag:`maplibre-particle/rn/13.4.1-particle.17`
|
||||
- V17 frozen commit:`e8b451fe70054107773e82484c80e90df3889147`
|
||||
|
||||
## 实现
|
||||
|
||||
- Source/Tile mutation 增加 source-scoped revision;
|
||||
- global retained content revision 继续保留;
|
||||
- production renderer 与 diagnostics 共用 source→segment dependency;
|
||||
- RETAINED Segment 独立 readiness / stability / Build / Reuse / Fallback;
|
||||
- unrelated source unloaded/fading 不再拖垮该 Segment;
|
||||
- full CPU mixed execution 支持 R0 Reuse + C Live + R1 Fallback;
|
||||
- Fast Path gate、singleContinuous、多 Continuous语义未改;
|
||||
- Source::isLoaded 与 global renderTree loaded 语义未改。
|
||||
|
||||
## Android Build
|
||||
|
||||
标准 validation pipeline 最终:
|
||||
|
||||
```text
|
||||
RC=0
|
||||
BUILD SUCCESS
|
||||
variant=OpenglDebug
|
||||
abi=x86_64
|
||||
apk_bytes=61956640
|
||||
libmaplibre_bytes=39636472
|
||||
sha256=86e22bf081bf7d3196f0bf0e7c56af51bde01e9f0b01e0bab238cb794273315a
|
||||
```
|
||||
|
||||
RDC 另修复 validation script 的 pipefail/SIGPIPE 141:
|
||||
`awk ... {print $1; exit}` → 完整消费管道。
|
||||
|
||||
## FZG1-2
|
||||
|
||||
- Android 13
|
||||
- API 33
|
||||
- x86_64
|
||||
- ADB:`192.168.200.139:5555`
|
||||
|
||||
### T1 STATIC
|
||||
|
||||
- false window:1
|
||||
- avg/max:203 ms
|
||||
- R0/R1 build=1 后持续 reuse
|
||||
|
||||
### T4 R-C-R 主验收
|
||||
|
||||
窗口:
|
||||
- baseline:tick 8
|
||||
- final:tick 130
|
||||
- 约 61.6 s
|
||||
|
||||
global revision:
|
||||
`128 → 973`
|
||||
|
||||
R0:
|
||||
```text
|
||||
build 1 → 1
|
||||
invalidation 20 → 20
|
||||
reuse 132 → 4042
|
||||
sourceRevision unchanged
|
||||
```
|
||||
|
||||
R1:
|
||||
```text
|
||||
build 5 → 135
|
||||
invalidation 26 → 211
|
||||
```
|
||||
|
||||
结论:
|
||||
- R0 buildDelta=0
|
||||
- R0 invalidationDelta=0
|
||||
- R0 reuseDelta=+3910
|
||||
- global revision 仍增长
|
||||
- dynamic source affected=[2]
|
||||
|
||||
**主硬门槛 PASS。**
|
||||
|
||||
### T2 GEOJSON 1HZ
|
||||
|
||||
- 约 35 s
|
||||
- falseWindowCount=6
|
||||
- avg=203 ms
|
||||
- max=205 ms
|
||||
- R0 build=1 / invalidation=18 保持稳定,reuse 增长
|
||||
|
||||
### T3 PERMANENT UNREADY
|
||||
|
||||
global loaded=false 持续约 21.468 s。
|
||||
|
||||
unready source:
|
||||
- no layer
|
||||
- enabled=false
|
||||
- isLoaded=false
|
||||
|
||||
R0/R1:
|
||||
- build=1 不变
|
||||
- invalidation=19 不变
|
||||
- reuse:11 → 1200
|
||||
- decision=reuse
|
||||
|
||||
说明 global loaded 兼容事实仍为 false,但无关 retained cache 继续安全复用。
|
||||
|
||||
## Native Host Test
|
||||
|
||||
Codex 已新增 `RenderSegmentLocality.*` 测试源码。
|
||||
|
||||
当前 Intel Mac 缺 `libuv`,Metal host configure 被环境阻塞。RDC 曾尝试 Homebrew 安装,但发现会连带升级 CMake/OpenSSL/Python 且 Intel 无 bottle 支持,已主动停止。
|
||||
|
||||
因此:
|
||||
- Native host GTest:ENV BLOCKED / NOT RUN
|
||||
- Android full native build:PASS
|
||||
- Android runtime:PASS
|
||||
|
||||
Jest:NOT RUN。
|
||||
|
||||
## 结论
|
||||
|
||||
STEP-100 功能目标成立:
|
||||
|
||||
> GeoJSON Source 更新只刷新依赖它的 retained segment;无关 retained segment 不再一起刷新。
|
||||
|
||||
允许进入 V18 unified release。
|
||||
54
docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md
Normal file
54
docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# STEP-101 RenderOrchestrator 销毁生命周期修复记录 v1
|
||||
|
||||
状态:**CLOSED / PASS(Native + Android runtime)**
|
||||
日期:2026-10-07
|
||||
|
||||
## 目标与基线
|
||||
|
||||
- 工作区:`/Users/tei/dev/MapLibreParticleLab-step101`
|
||||
- 分支:`step101-render-orchestrator-teardown`
|
||||
- start HEAD:`d29e84c1a90daeea104b389d28b000c4f017d08d`
|
||||
- implementation commit:`64cf8f44350eb8ec4b45360db4d3f9505ac4ac0b`(`fix: make render orchestrator teardown safe`),已 push 到 `origin/particle/main` 并回读一致。
|
||||
- 工作合同:`docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v2.md`,第 13 节优先于前文冲突项。
|
||||
- V18 为不可变已发布版本;本阶段未创建、发布或标记 V19。
|
||||
- 目标:修复 RenderSource/Tile 销毁时 observer 可能回调已析构的 RenderOrchestrator readiness mutex/state 的生命周期漏洞。
|
||||
|
||||
V18 成员声明使 readiness mutex/state 早于 `renderSources` 自动析构。Tile 销毁时同步发出 `Cancelled` 回调的路径可达,且与现场 Android destroyed mutex SIGABRT 高度吻合;没有完整符号化栈与修复后等价实机结果,暂不把它写成唯一确认的现场根因。
|
||||
|
||||
## 必做项进度
|
||||
|
||||
- [x] `RenderSource::setObserver(nullptr)` 恢复 NullObserver,保留正常 observer 转发。
|
||||
- [x] Orchestrator 析构:保留 contextLost layer handling;第一次 tagged drain;detach RenderSource、GlyphManager、ImageManager observer;在 readiness state 存活时清理 sources;第二次 tagged drain。
|
||||
- [x] 静态审计独立 render-job 队列:`waitForEmpty()` 不排空该队列;OpenGL context 析构的 `runRenderJobs(true)` 属于后续 backend 清理,测试中也单独调用 `runRenderJobs()`。不能以双 drain 代替 render-job 验收。
|
||||
- [x] 新增非 Jest C++ 测试源码:默认/真实/detach observer、三种 tile callback、Cancelled、source 清理后第二次 drain、render-job 单独处理和重复 teardown。helper 像生产代码一样注册 Orchestrator observer,并断言 source 销毁前旧 observer 已被替换。
|
||||
- [x] Android Validation 增加 T1 RESET_ONCE、T2 RESET_LOOP_20、T3 RESET_WITH_DYNAMIC_GEOJSON、T4 RESET_WITH_DIAGNOSTICS;通过 Activity recreate 触发 MapView 销毁/新建,并输出 SurfaceView detach、nativeReset、resetRenderer、style ready 与 diagnostics 日志供逐次配对。
|
||||
- [x] 只读审计正常 `renderSources.erase()` / `clearData()`:两者在 Orchestrator 存活期间执行;TileCache 析构等待其延迟释放任务结束。本阶段没有扩大其行为修改,运行态异步边界仍待 RDC 验证。
|
||||
- [x] 静态核对 STEP-100 locality、Fast Path、`singleContinuousSegment`、`Source::isLoaded()` 等代码未被本次 diff 修改。
|
||||
- [x] Native:Android full native build PASS;focused Android C++ TestRunner 6/6 PASS。Mac host 因 libuv 环境阻塞,未虚报。
|
||||
- [x] Android Validation APK build PASS;T1~T4 实机 PASS;T2 完整 20 次真实 reset/recreate,crash buffer 为空。
|
||||
- [x] STEP-100 T4 R-C-R locality 回归 PASS:R0 buildDelta=0、invalidationDelta=0、reuseDelta=+3878。
|
||||
- [ ] V19 candidate、NavSea 五次冷启动与正式 V19 发布:属于后续发行 Gate,尚未执行。
|
||||
|
||||
## 允许与禁止范围
|
||||
|
||||
实际变更限于 Orchestrator/RenderSource 生命周期、对应 C++ 测试、Android Validation 场景及 Java/JNI reset 观测日志。未修改 NavSea、provider/network、STEP-100 locality 实现、发布坐标或 V18 产物。未运行 Jest。
|
||||
|
||||
## 检查与当前结果
|
||||
|
||||
本次验证仅包含源码 diff 静态审计与 `git diff --check`。构建、链接、GTest、Gradle/APK、ADB、设备、NavSea、发布均 **NOT RUN**;源码交付不能记为运行态 PASS。完成报告:`docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md`。
|
||||
|
||||
## 阻塞与下一步
|
||||
|
||||
当前没有已观测编译失败;实际编译与场景行为仍未知。由 RDC 按完成报告的命令独立编译、运行 Native 测试、Android T1~T4、STEP-100 locality,并核对每次 `RESET_REQUEST` 对应 SurfaceView detach、`nativeReset`、`resetRenderer` 和新 renderer ready。随后才进入 V19 candidate 与 NavSea 等价验收;全部门槛满足后才可考虑正式发布。
|
||||
|
||||
|
||||
## RDC 最终证据
|
||||
|
||||
- Android Validation APK:61,957,678 bytes;`libmaplibre.so` 39,637,512 bytes;SHA-256 `14cbd35ed0bc1366389ddea69925901013e3b209ed3575462421a7920c73c721`。
|
||||
- T1~T4 teardown:全部 PASS;T2 20 次 reset;动态 GeoJSON / diagnostics 条件均无 crash。
|
||||
- STEP-100 locality:global revision 62→894;R0 build 1→1、invalidation 19→19、reuse 12→3890;PASS。
|
||||
- Android C++ focused TestRunner:4 个 `RenderSegmentLocality` + 2 个 `RenderOrchestratorTeardown` 全部 PASS;status=0。
|
||||
- Mac host GTest:ENV BLOCKED(本机缺可用 libuv pkg-config),不影响 Android full native/device 闭环结论。
|
||||
- Jest:NOT RUN。
|
||||
|
||||
**STEP-101 CLOSED / PASS。** 下一阶段进入 V19 candidate → NavSea 等价冷启动 Gate → 不可变公开发行。
|
||||
@@ -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 已优化。
|
||||
454
docs/报告/STEP-100_SourceScoped_RenderSegment局部失效_Codex完成报告_v1.md
Normal file
454
docs/报告/STEP-100_SourceScoped_RenderSegment局部失效_Codex完成报告_v1.md
Normal file
@@ -0,0 +1,454 @@
|
||||
# STEP-100 SourceScoped RenderSegment 局部失效 — 完成报告 v1
|
||||
|
||||
## 1. 最终结论
|
||||
|
||||
**CLOSED / PASS(功能与 Android 运行态目标确认)**。
|
||||
|
||||
Codex 实现使用:
|
||||
- model:`gpt-6-sol`
|
||||
- reasoning effort:`high`
|
||||
|
||||
RDC 独立完成:
|
||||
- 源码 diff 审计;
|
||||
- Android x86_64 OpenGL 全量 native build;
|
||||
- Validation APK;
|
||||
- FZG1-2 自动安装与 T1/T2/T3/T4;
|
||||
- STEP-100 60 秒 locality window;
|
||||
- validation build script 小型工程修复;
|
||||
- Git diff/status 审计。
|
||||
|
||||
本 Step 核心目标已确认:
|
||||
|
||||
> 动态 GeoJSON Source 更新只影响依赖它的 RETAINED Segment;无关 R0 cache 不再跟着 invalid/rebuild,并且在 global loaded=false 时仍可安全 reuse。
|
||||
|
||||
Native host GTest 因当前 Intel Mac 缺 `libuv` 未执行;曾尝试 Homebrew 安装,但 Homebrew 要连带升级 CMake/OpenSSL/Python 且 Intel 已无 bottle 支持,因此 RDC 主动终止,未把环境变化作为任务依赖。Android 最终 C++ native build 与设备运行态用于本轮功能验收。
|
||||
|
||||
Jest:**NOT RUN**,用户未要求。
|
||||
|
||||
## 2. Git / 基线
|
||||
|
||||
- worktree:`/Users/tei/dev/MapLibreParticleLab-step100`
|
||||
- branch:`step100-segment-local-source-invalidation`
|
||||
- start HEAD:`c4b481fd2c5b5da20d31f6ae54011f0552d7dbd0`
|
||||
- start remote:`origin/particle/main = c4b481fd2c5b5da20d31f6ae54011f0552d7dbd0`
|
||||
- frozen V17 tag:`maplibre-particle/rn/13.4.1-particle.17`
|
||||
- frozen V17 commit:`e8b451fe70054107773e82484c80e90df3889147`
|
||||
- V17 tag/release worktree:未修改。
|
||||
|
||||
最终 STEP-100 commit / push / remote HEAD 在 Git 收尾后由最终交付回读记录,避免文档自引用。
|
||||
|
||||
## 3. 实际修改
|
||||
|
||||
生产源码:
|
||||
|
||||
- `include/mbgl/renderer/render_segment.hpp`
|
||||
- `src/mbgl/renderer/render_orchestrator.hpp`
|
||||
- `src/mbgl/renderer/render_orchestrator.cpp`
|
||||
- `src/mbgl/renderer/renderer_impl.cpp`
|
||||
- `src/mbgl/renderer/retained_underlay_cache.cpp`
|
||||
- `src/mbgl/renderer/retained_underlay_runtime_state.hpp`
|
||||
- `src/mbgl/renderer/render_segment_json.hpp`
|
||||
- `packages/maplibre-particle-rn/src/types.ts`
|
||||
|
||||
Test App / tests:
|
||||
|
||||
- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step099ReadinessFixture.kt`
|
||||
- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt`
|
||||
- `test/renderer/retained_underlay_cache.test.cpp`
|
||||
|
||||
工具:
|
||||
|
||||
- `tools/particle_android_validation/build_validation_apk.sh`
|
||||
- RDC 修复 `unzip | awk ... exit` 在 `pipefail` 下偶发 SIGPIPE/141;
|
||||
- 仅移除 awk 的提前 exit,不改变 APK 验证语义。
|
||||
|
||||
文档:
|
||||
|
||||
- `docs/40_任务/STEP-100_SourceScoped_RenderSegment局部失效_Codex任务_v2.md`
|
||||
- `docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md`
|
||||
- 本报告;
|
||||
- `粒子开发记录.md`
|
||||
|
||||
## 4. 生产设计
|
||||
|
||||
### 4.1 Source-scoped revision
|
||||
|
||||
新增正式 source-scoped revision bookkeeping。
|
||||
|
||||
明确的 Source/Tile mutation:
|
||||
- source diff;
|
||||
- feature state;
|
||||
- tile changed;
|
||||
|
||||
推进对应 sourceId revision。
|
||||
|
||||
现有 global `retainedUnderlayContentRevision` 继续增长,供:
|
||||
- compatibility;
|
||||
- diagnostics;
|
||||
- Fast Path fail-closed。
|
||||
|
||||
RETAINED Segment cache 不再直接以 global Source/Tile revision 作为内容 revision。
|
||||
|
||||
### 4.2 Segment dependency
|
||||
|
||||
正式 renderer 与 diagnostics 共用 source→segment 依赖推导。
|
||||
|
||||
规则:
|
||||
- layer sourceId 决定 source dependency;
|
||||
- source-less background 不制造假 dependency;
|
||||
- shared source 可影响多个 segment;
|
||||
- symbol 的 glyph/image/placement 作为明确 global prerequisite;
|
||||
- 真正无法证明的 dependency fail closed。
|
||||
|
||||
### 4.3 Per-segment readiness
|
||||
|
||||
未修改:
|
||||
- `Source::isLoaded()`;
|
||||
- `renderTreeParameters->loaded = styleLoaded && isLoaded()`。
|
||||
|
||||
full CPU retained path 增加 per-segment source readiness/stability:
|
||||
|
||||
- dependent source unloaded → 仅该 Segment fallback;
|
||||
- dependent source fading → 仅该 Segment unstable;
|
||||
- unrelated source unloaded/fading → 不影响该 Segment;
|
||||
- style/image/sprite/glyph/placement/context 等 global prerequisite 仍 fail closed。
|
||||
|
||||
### 4.4 Mixed Build / Reuse / Fallback
|
||||
|
||||
移除 retained full-CPU path 的 all-or-none `useRetainedRanges` 依赖。
|
||||
|
||||
每个 RETAINED Segment独立决定:
|
||||
|
||||
- Build
|
||||
- Reuse
|
||||
- Fallback
|
||||
|
||||
主 buffer 按 Style Segment 顺序执行:
|
||||
|
||||
- RETAINED Build/Reuse → composite cache
|
||||
- RETAINED Fallback → live render only that range
|
||||
- CONTINUOUS → live render
|
||||
|
||||
因此允许:
|
||||
|
||||
```text
|
||||
R0 = Reuse
|
||||
C = Live
|
||||
R1 = Fallback
|
||||
```
|
||||
|
||||
不再因为 R1 fallback 把 R0 一起变成 live redraw。
|
||||
|
||||
Fast Path gate / `singleContinuousSegment` / multi-continuous Fast Path:**未修改**。
|
||||
|
||||
## 5. Android build
|
||||
|
||||
首次标准 validation script 的 Gradle 本体实际结果:
|
||||
|
||||
```text
|
||||
BUILD SUCCESSFUL in 12s
|
||||
86 actionable tasks: 6 executed, 80 up-to-date
|
||||
```
|
||||
|
||||
但脚本在 Gradle 成功后以 141 退出。RDC 用 `bash -x` 精确定位到:
|
||||
|
||||
```bash
|
||||
LIB_BYTES="$(unzip ... | awk '/.../ {print $1; exit}')"
|
||||
```
|
||||
|
||||
在 `set -o pipefail` 下触发 SIGPIPE。
|
||||
|
||||
RDC 仅去掉 awk 的 `exit`,重跑标准脚本:
|
||||
|
||||
```text
|
||||
RC=0
|
||||
[validation-apk] BUILD SUCCESS
|
||||
```
|
||||
|
||||
最终 APK:
|
||||
|
||||
- variant:OpenglDebug
|
||||
- ABI:x86_64
|
||||
- apk bytes:61,956,640
|
||||
- libmaplibre.so bytes:39,636,472
|
||||
- SHA-256:
|
||||
`86e22bf081bf7d3196f0bf0e7c56af51bde01e9f0b01e0bab238cb794273315a`
|
||||
|
||||
证据:
|
||||
`artifacts/maplibre-particle/android-validation/`
|
||||
|
||||
## 6. 设备
|
||||
|
||||
- serial:`192.168.200.139:5555`
|
||||
- model:FZG1-2
|
||||
- Android:13
|
||||
- API:33
|
||||
- ABI:x86_64
|
||||
|
||||
APK 覆盖安装成功,`ParticleCoreLayerValidationActivity` 冷启动成功。
|
||||
|
||||
STEP-100 log:
|
||||
`artifacts/maplibre-particle/android-validation/step100-logcat.txt`
|
||||
|
||||
log SHA-256:
|
||||
`e9d18ea987d12cb30e321d0d9c669b2588972aac89e5277920bf1fa9fee8146b`
|
||||
|
||||
## 7. T1 STATIC
|
||||
|
||||
运行约 20 秒。
|
||||
|
||||
aggregate:
|
||||
|
||||
```text
|
||||
falseWindowCount=1
|
||||
falseWindowAvgMs=203
|
||||
falseWindowMaxMs=203
|
||||
openFalseWindowMs=0
|
||||
```
|
||||
|
||||
最终:
|
||||
|
||||
- R0 build=1
|
||||
- R0 reuse=1217
|
||||
- R0 decision=reuse
|
||||
- R1 build=1
|
||||
- R1 reuse=1187
|
||||
- R1 decision=reuse
|
||||
|
||||
静态 R-C-R 正常。
|
||||
|
||||
## 8. T4 R-C-R 主验收
|
||||
|
||||
结构:
|
||||
|
||||
```text
|
||||
R0 -> step099-static
|
||||
C -> particle-fixture
|
||||
R1 -> step099-dynamic (+ framework annotation source if present)
|
||||
```
|
||||
|
||||
window marker:
|
||||
|
||||
```text
|
||||
baseline tick=8 17:05:21.642
|
||||
final tick=130 17:06:23.240
|
||||
```
|
||||
|
||||
窗口约 61.6 秒。
|
||||
|
||||
dynamic update 总计:68 次。
|
||||
|
||||
global revision:
|
||||
|
||||
```text
|
||||
128 -> 973
|
||||
```
|
||||
|
||||
说明 global compatibility revision **仍正常增长**,没有通过停掉 global revision“作弊”。
|
||||
|
||||
### R0
|
||||
|
||||
baseline:
|
||||
|
||||
```text
|
||||
build=1
|
||||
reuse=132
|
||||
invalidation=20
|
||||
srcRev=5127273714767523329
|
||||
decision=reuse
|
||||
```
|
||||
|
||||
final:
|
||||
|
||||
```text
|
||||
build=1
|
||||
reuse=4042
|
||||
invalidation=20
|
||||
srcRev=5127273714767523329
|
||||
decision=reuse
|
||||
```
|
||||
|
||||
Delta:
|
||||
|
||||
```text
|
||||
buildDelta = 0
|
||||
invalidationDelta = 0
|
||||
reuseDelta = +3910
|
||||
sourceRevision = unchanged
|
||||
```
|
||||
|
||||
**硬门槛 PASS。**
|
||||
|
||||
### R1
|
||||
|
||||
baseline:
|
||||
|
||||
```text
|
||||
build=5
|
||||
invalidation=26
|
||||
```
|
||||
|
||||
final:
|
||||
|
||||
```text
|
||||
build=135
|
||||
invalidation=211
|
||||
```
|
||||
|
||||
动态 Source mutation 正常驱动 R1。
|
||||
|
||||
mutation diagnostics 持续:
|
||||
|
||||
```text
|
||||
sourceId=step099-dynamic
|
||||
affected=[2]
|
||||
scope=source-local-diagnostic
|
||||
```
|
||||
|
||||
### loaded false windows
|
||||
|
||||
T4:
|
||||
|
||||
```text
|
||||
falseWindowCount=5
|
||||
falseWindowAvgMs=202
|
||||
falseWindowMaxMs=203
|
||||
openFalseWindowMs=0
|
||||
```
|
||||
|
||||
即 transient global loaded=false 仍存在,但不再让 R0 build/invalidation 增长。
|
||||
|
||||
## 9. T2 双 GeoJSON readiness 回归
|
||||
|
||||
约 35 秒。
|
||||
|
||||
aggregate:
|
||||
|
||||
```text
|
||||
falseWindowCount=6
|
||||
falseWindowAvgMs=203
|
||||
falseWindowMaxMs=205
|
||||
openFalseWindowMs=0
|
||||
```
|
||||
|
||||
R0 在动态窗口保持:
|
||||
|
||||
- build=1
|
||||
- invalidation=18
|
||||
- revision unchanged
|
||||
- reuse 持续增长。
|
||||
|
||||
R1 随两个动态 GeoJSON 持续 build/invalidate。
|
||||
|
||||
结论:
|
||||
**DYNAMIC_SOURCE_READINESS_THRASH 仍是 global loaded 事实,但不再误伤无关 retained cache。**
|
||||
|
||||
## 10. T3 permanently unready unrelated source
|
||||
|
||||
`step099-unready-raster-array`:
|
||||
|
||||
- no layer;
|
||||
- enabled=false;
|
||||
- isLoaded=false。
|
||||
|
||||
global:
|
||||
|
||||
```text
|
||||
renderOrchestratorLoaded=false
|
||||
openFalseWindowMs=21468
|
||||
```
|
||||
|
||||
约 21.468 秒持续 false。
|
||||
|
||||
与此同时:
|
||||
|
||||
R0:
|
||||
- build=1
|
||||
- invalidation=19
|
||||
- reuse:11 → 1200
|
||||
- decision=reuse
|
||||
|
||||
R1:
|
||||
- build=1
|
||||
- invalidation=19
|
||||
- reuse:11 → 1200
|
||||
- decision=reuse
|
||||
|
||||
因此:
|
||||
|
||||
> global loaded 仍保持旧兼容语义,但 unrelated permanently-unready source 已不能把无关 RETAINED Segment 拖成 style-not-loaded fallback。
|
||||
|
||||
**硬门槛 PASS。**
|
||||
|
||||
## 11. Mixed fallback
|
||||
|
||||
T4 中 R1 `fallbackCount` 从约 21 增长到 76,而 R0 fallback 计数保持不变,同时 R0 reuse 持续增长。
|
||||
|
||||
这证明运行期间真实发生了:
|
||||
|
||||
```text
|
||||
R0 = Reuse
|
||||
R1 = transient Fallback / rebuild
|
||||
```
|
||||
|
||||
而不是只有“两个段都 ready”的理想场景。
|
||||
|
||||
未观察到 crash / ANR。
|
||||
|
||||
## 12. Fast Path 边界
|
||||
|
||||
RDC diff 审计确认未修改:
|
||||
|
||||
- `tryRenderParticleFastFrame`
|
||||
- `particleFastPathNonCameraInputsMatch`
|
||||
- `singleContinuousSegment`
|
||||
- multi-continuous fail-closed
|
||||
- global Fast Path signature/fade/contentRevision/pending/placement 等 gate。
|
||||
|
||||
STEP-100 只修 full CPU retained locality。
|
||||
|
||||
## 13. Native host tests
|
||||
|
||||
Codex 已新增 `RenderSegmentLocality.*` C++ 测试源码。
|
||||
|
||||
RDC 尝试恢复 `macos-metal` test runner,但 host configure 需要 `libuv`,当前未安装。
|
||||
|
||||
尝试 `brew install libuv` 时发现 Intel Homebrew 会连带升级:
|
||||
- CMake
|
||||
- OpenSSL
|
||||
- Python
|
||||
- ca-certificates 等
|
||||
|
||||
且当前 Intel macOS 已无官方 bottle 支持。为避免污染开发环境,RDC 主动停止。
|
||||
|
||||
因此:
|
||||
|
||||
- Native host GTest:**ENV BLOCKED / NOT RUN**
|
||||
- Android C++ full native build:**PASS**
|
||||
- Android device runtime:**PASS**
|
||||
|
||||
## 14. Jest / iOS
|
||||
|
||||
- Jest:NOT RUN。
|
||||
- iOS runtime:本 Step 未运行。
|
||||
- V18 unified release:在 STEP-100 Git 收尾后单独执行。
|
||||
|
||||
## 15. 最终判定
|
||||
|
||||
STEP-100 的核心实现目标全部成立:
|
||||
|
||||
1. global revision 继续增长;
|
||||
2. R1 GeoJSON mutation 不再 invalid/rebuild R0;
|
||||
3. R0 buildDelta=0;
|
||||
4. R0 invalidationDelta=0;
|
||||
5. R0 reuseDelta>0;
|
||||
6. transient unrelated unloaded 不影响 R0 reuse;
|
||||
7. permanent unrelated unloaded 不影响 R0 reuse;
|
||||
8. mixed Reuse/Fallback 实际发生;
|
||||
9. Fast Path 语义未放宽。
|
||||
|
||||
因此本 Step 功能与 Android runtime 验收:
|
||||
|
||||
**PASS / CLOSED**。
|
||||
|
||||
下一步允许进入 V18 unified release。
|
||||
184
docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md
Normal file
184
docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# STEP-101 RenderOrchestrator 销毁生命周期修复 Codex 完成报告 v1
|
||||
|
||||
## 1. 执行结论
|
||||
|
||||
**PASS(RDC Native / Android runtime):V18 teardown destroyed-mutex 回归已修复并通过设备闭环。** Codex 阶段仅交付源码;随后 RDC 独立完成 Android full Native build、focused C++ TestRunner、STEP-101 T1~T4、20 次真实 renderer reset/recreate、STEP-100 locality 回归与 crash buffer 检查。V19 尚未在本报告阶段公开发布。
|
||||
|
||||
## 2. Git 基线
|
||||
|
||||
- 工作区:`/Users/tei/dev/MapLibreParticleLab-step101`
|
||||
- 分支:`step101-render-orchestrator-teardown`
|
||||
- start HEAD:`d29e84c1a90daeea104b389d28b000c4f017d08d`
|
||||
- implementation commit:`64cf8f44350eb8ec4b45360db4d3f9505ac4ac0b`
|
||||
- implementation message:`fix: make render orchestrator teardown safe`
|
||||
- push result:**PASS**,`origin/particle/main` 独立回读与 implementation commit 一致。
|
||||
- 文档 closeout commit / 最终远端 HEAD:Git 提交无法在自身内容中自指其最终 SHA,按项目既有规则由最终交付回读记录。
|
||||
- 子模块:`git submodule status` 列出的子模块均以 `-` 开头,表示当前工作区未初始化;本轮未修改或初始化子模块。
|
||||
- V18 Android/RN `13.4.1-particle.18`、iOS `6.28.0-particle.18` 保持不变;V19 **未发布**。
|
||||
|
||||
## 3. 根因与实现
|
||||
|
||||
V18 `readinessDiagnosticsMutex` 等成员声明在 `renderSources` 之后,按 C++ 反序析构会先于 sources 消失。RenderSource/Tile 销毁存在同步 `Cancelled` callback 到 Orchestrator 的可达路径,随后可能锁已销毁 mutex。这是已证实的生命周期漏洞,与 Android FORTIFY 报错高度吻合。虽未取得精确到最终 callback 的完整符号化现场栈,但同一设备上 V18 条件稳定复现,而 STEP-101 修复后 T1~T4、20 次 reset、动态 GeoJSON 与 diagnostics 条件均不再崩溃,因此将其作为本次 Android teardown 回归的已确认工程根因。
|
||||
|
||||
当前源码的析构顺序为:处理 contextLost layers → 第一次 `threadPool.waitForEmpty()` → detach 各 RenderSource、GlyphManager、ImageManager observer → readiness state 仍活着时 `renderSources.clear()` → 第二次 `threadPool.waitForEmpty()` → 常规成员析构。`RenderSource::setObserver(nullptr)` 恢复 NullObserver,tile 回调无需新增空指针分支。显式 detach 与清理建立 owner teardown 合同,不依赖今后成员顺序碰巧安全;没有增加 teardown flag。
|
||||
|
||||
render-job 队列独立于 tagged scheduler work。静态检查显示 tile/worker 销毁可投递捕获 render data/bucket 的 render jobs;`waitForEmpty()` 不处理它们。OpenGL 同步 Android reset 路径先 `renderer.reset()`、再 `backend.reset()`,其 Context 析构调用 `runRenderJobs(true)`;若启用 async renderer cleanup,backend 的清理时点另由后续生命周期决定。Metal Context 与 Vulkan backend 也有各自的 `runRenderJobs(true)` 清理入口,均未在本轮运行验证。测试显式 `runRenderJobs()`,但没有构建或设备证据证明所有 backend 模式均安全。这是 RDC 必须核对的剩余风险。
|
||||
|
||||
正常 source remove 使用 `renderSources.erase()`,`clearData()` 使用 `renderSources.clear()`;两者均在 Orchestrator 实体及 readiness state 存活时执行。`TileCache::~TileCache()` 会清理 pending releases 并等待先前 deferred deletion 结束。这个审计没有证成任意外部并发调用都安全,本轮不修改正常 erase/clearData 行为,也不声称所有 source replacement 异步问题已解决。
|
||||
|
||||
## 4. 实际修改与审计结论
|
||||
|
||||
源码/测试差异共七个文件:
|
||||
|
||||
1. `src/mbgl/renderer/render_orchestrator.cpp`:显式 detach、source clear 与双 drain。
|
||||
2. `src/mbgl/renderer/render_orchestrator.hpp`:仅为 focused test 开放 friend helper。
|
||||
3. `src/mbgl/renderer/render_source.cpp`:nullptr observer 回退 NullObserver。
|
||||
4. `test/renderer/render_orchestrator_error.test.cpp`:新增两个非 Jest teardown tests;helper 注册 Orchestrator observer,测试断言 source 销毁前 observer 已 detach、Cancelled 不再到旧 observer、第二次 drain 和独立 render-job 队列。
|
||||
5. `platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp`:`nativeReset` 与 `resetRenderer` 入口/完成日志,仅用于配对观测,未改变 reset 调用顺序。
|
||||
6. `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/renderer/surfaceview/SurfaceViewMapRenderer.java`:SurfaceView detach 与 Java `nativeReset()` 进入/返回日志。
|
||||
7. `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt`:T1~T4,tile ready/diagnostics、动态 GeoJSON、Activity recreate、重建与完成日志;保存状态里的空场景不会在配置变更后重新从 Intent 启动。
|
||||
|
||||
本轮新增/更新文档:本报告、`docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md`、`粒子开发记录.md`。既有未跟踪的 STEP-101 任务书 v1/v2 没有修改。
|
||||
|
||||
与 v2 合同静态核对:observer、双 drain、render-job 独立审计、四个 Android 场景均有对应代码;STEP-100 locality/fast path/source loaded 语义无本次差异。T2 代码计的是 reset **请求**,20 次 Native reset 必须由 log 中每轮 Java `nativeReset`、JNI `nativeReset`、`MapRenderer::resetRenderer entered/completed` 和新 renderer ready 独立确认,不能凭 UI DONE 计数认定。T3 使用已有 T4 R-C-R fixture 并持续更新动态 GeoJSON;T4 在 reset 前额外取 diagnostics。现有 C++ test 使用最小 tile-backed Source,并非实际 Android source;设备等价验收仍不可省略。
|
||||
|
||||
## 5. 实际检查与未执行项目
|
||||
|
||||
- `git diff --check`:**PASS**(已跟踪 diff 的 whitespace/error 静态检查;未跟踪的新文档不在此命令检查范围)。
|
||||
- 源码 diff/合同静态审计:完成;补了 observer detach 断言、Java/JNI reset 边界日志、T3 mutation 证据及场景恢复边界。
|
||||
- CMake/Ninja/Native compile、C++ GTest:**NOT RUN**。
|
||||
- Android Gradle、Validation APK、AAR/RN candidate:**NOT RUN**。
|
||||
- ADB、T1~T4、crash buffer、STEP-100 locality 运行态、NavSea 冷启动:**NOT RUN**。
|
||||
- Jest、iOS/Metal build 与设备、publish/tag/commit/push:**NOT RUN**。
|
||||
- 产物路径、大小、SHA-256、运行日志:本轮没有新产物,均无结果。
|
||||
|
||||
## 6. RDC 精确验证命令与验收证据
|
||||
|
||||
在本工作区执行 Mac Native 定向验证(若既有 `libuv` 环境问题仍在,保留首个错误并标为环境阻塞):
|
||||
|
||||
```bash
|
||||
cmake --preset macos-metal
|
||||
cmake --build build-macos-metal --target mbgl-test-runner --parallel 3
|
||||
build-macos-metal/mbgl-test-runner --gtest_filter='RenderOrchestratorTeardown.*:RenderSegmentLocality.*'
|
||||
```
|
||||
|
||||
Android Validation APK 使用固定入口,不手拼 Gradle:
|
||||
|
||||
```bash
|
||||
./tools/particle_android_validation/build_validation_apk.sh
|
||||
```
|
||||
|
||||
RDC 使用显式设备 serial。每个场景先清日志、停止旧 Activity,再单独启动;`T2` 等待 20 轮结束。安装前核对 APK 文件、SHA-256、ABI 与运行设备:
|
||||
|
||||
```bash
|
||||
adb -s 192.168.200.139:5555 install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk
|
||||
adb -s 192.168.200.139:5555 logcat -c
|
||||
adb -s 192.168.200.139:5555 shell am force-stop org.maplibre.android.testapp
|
||||
adb -s 192.168.200.139:5555 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity --es step101_scenario T2_RESET_LOOP_20
|
||||
adb -s 192.168.200.139:5555 logcat -v threadtime -d > /tmp/step101-t2-logcat.txt
|
||||
rg -n 'STEP101_|SurfaceView detached; nativeReset|nativeReset returned|MapRenderer::nativeReset|MapRenderer::resetRenderer|pthread_mutex_lock called on a destroyed mutex|Fatal signal|ANR' /tmp/step101-t2-logcat.txt
|
||||
```
|
||||
|
||||
对 T1/T3/T4 分别重复相同 `logcat -c`、`force-stop`、`am start`、日志保存步骤,将 `step101_scenario` 依次替换为 `T1_RESET_ONCE`、`T3_RESET_WITH_DYNAMIC_GEOJSON`、`T4_RESET_WITH_DIAGNOSTICS`。逐轮配对 `RESET_REQUEST`、`MAPVIEW_DESTROY_ENTER/RETURN`、`SurfaceView detached; nativeReset entered`、JNI `MapRenderer::nativeReset entered/completed`、Native `resetRenderer entered/completed`、新 `STYLE_READY` 与 `NEW_RENDERER_READY`;T2 需至少 20 个完整 reset/recreate 配对,且无 SIGABRT/ANR。T3 核对 `STEP101_DYNAMIC_SOURCE_MUTATION` 在每轮 reset 前持续出现,T4 核对 `PRE_RESET_DIAGNOSTICS` 与 reset 后 diagnostics。STEP-100 T4 R-C-R 另按既有 Validation 菜单复跑,核对无关 R0 `buildDelta=0`、`invalidationDelta=0`、`reuseDelta>0`,动态 R1 正常 build/invalidation。
|
||||
|
||||
上述只是交接命令,**本轮均未执行**。V19 candidate 必须先于 NavSea 临时锁包和五次超过 30 秒的冷启动;crash buffer 与 locality 均通过后,才可进入不可变发布。NavSea 外部工作区/候选制品命令由 RDC 按其实际环境另行确定,本轮不伪造路径或结果。
|
||||
|
||||
## 7. 已知问题、未完成事项与下一步
|
||||
|
||||
静态审计无法证明 Android 每次 Activity recreate 都实际完成 native reset,也无法证明异步 backend 清理和真实 Source 并发场景无 late callback。Native 编译与定向测试、四个 Validation 场景、STEP-100 locality、NavSea 等价五次冷启动、crash buffer 均待 RDC;任何失败均应保留现场并按首个真实错误回传。无理由在本阶段修改 NavSea、发布 V19 或重写 STEP-100 语义。
|
||||
|
||||
|
||||
## 8. RDC 独立验收(2026-10-07)
|
||||
|
||||
### 8.1 Android full Native / Validation APK
|
||||
|
||||
标准入口:
|
||||
|
||||
```text
|
||||
./tools/particle_android_validation/build_validation_apk.sh
|
||||
```
|
||||
|
||||
结果:**PASS**。
|
||||
|
||||
```text
|
||||
variant=OpenglDebug
|
||||
abi=x86_64
|
||||
apk_bytes=61957678
|
||||
libmaplibre_bytes=39637512
|
||||
sha256=14cbd35ed0bc1366389ddea69925901013e3b209ed3575462421a7920c73c721
|
||||
```
|
||||
|
||||
该构建实际编译了本 Step 修改后的 C++ Core、`libmaplibre.so`、JNI/Java reset 日志和 Android Validation T1~T4。
|
||||
|
||||
### 8.2 STEP-101 Android teardown runtime
|
||||
|
||||
设备:FZG1-2 / Android 13 / API 33 / x86_64 / `192.168.200.139:5555`。
|
||||
|
||||
- T1 `RESET_ONCE`:真实 `MapRenderer::resetRenderer entered/completed`、`nativeReset entered/completed`、新 renderer `STYLE_READY / NEW_RENDERER_READY`;PASS。
|
||||
- T2 `RESET_LOOP_20`:完整运行至 iteration 20,最终 `STEP101_SCENARIO_DONE ... resetsRequested=20`;逐轮出现 destroy/reset/new renderer ready;PASS。
|
||||
- T3 `RESET_WITH_DYNAMIC_GEOJSON`:reset 前动态 GeoJSON mutation 持续产生,reset 完成后新 renderer 继续 ready;PASS。
|
||||
- T4 `RESET_WITH_DIAGNOSTICS`:reset 前 `getRenderSegmentDiagnostics()` 可读,随后真实 reset 完成并重建;PASS。
|
||||
- 四组验收 crash buffer 均为空;未出现 `pthread_mutex_lock called on a destroyed mutex`、SIGABRT 或 ANR。
|
||||
|
||||
### 8.3 STEP-100 locality 回归
|
||||
|
||||
R-C-R T4 重新运行 60+ 秒,稳定窗口:
|
||||
|
||||
```text
|
||||
global revision: 62 -> 894
|
||||
|
||||
R0 retained:
|
||||
build 1 -> 1 delta 0
|
||||
invalidation 19 -> 19 delta 0
|
||||
reuse 12 -> 3890 delta +3878
|
||||
|
||||
R1 retained:
|
||||
build / invalidation 持续增长
|
||||
```
|
||||
|
||||
动态 source mutation 仍只影响对应 segment;R0 source revision 保持不变。STEP-101 teardown 修复没有破坏 STEP-100 Source→Segment locality。
|
||||
|
||||
### 8.4 Focused Android C++ TestRunner
|
||||
|
||||
Mac host `mbgl-test-runner` 仍因本机缺失可用 `libuv` pkg-config 包而 ENV BLOCKED;没有安装或升级系统依赖来掩盖该环境事实。
|
||||
|
||||
改用仓库已有 Android Native C++ TestRunner,x86_64 build **PASS**。RDC 为 TestRunner 临时打包真实 `test/fixtures/` 到 `data.zip`,仅满足 Android runner 既有解压契约;该临时 asset 已删除,不进入 Git。
|
||||
|
||||
设备过滤执行:
|
||||
|
||||
```text
|
||||
RenderSegmentLocality.SourceRevisionsAreScopedAndRecreationUsesNewIdentity PASS
|
||||
RenderSegmentLocality.ReadinessAndFadeFollowOnlyDependencies PASS
|
||||
RenderSegmentLocality.MixedExecutionDrawsEachLiveRangeOnceInStyleOrder PASS
|
||||
RenderSegmentLocality.FailedBuildCannotReuseOldTexture PASS
|
||||
RenderOrchestratorTeardown.NullObserverAndDetachCoverEveryTileCallback PASS
|
||||
RenderOrchestratorTeardown.PendingTileCancellationAndSecondDrainRepeatSafely PASS
|
||||
TestRunner finished with status: '0'
|
||||
```
|
||||
|
||||
最终 TestRunner APK(含临时 fixtures,仅作验收证据)SHA-256:
|
||||
|
||||
```text
|
||||
e1b39b3eb035175b79dd60dbe39496ea982c2dd6e2f1593fd60dd9630f393712
|
||||
```
|
||||
|
||||
crash buffer 为空。
|
||||
|
||||
首次 TestRunner 编译暴露 6 个测试源码级 C++20 designated-initializer / protected-base conversion 问题;RDC 仅修正测试源码字段补齐和显式转换,未改变生产语义。修复后 Android C++ TestRunner build 与上述 filtered tests 均 PASS。
|
||||
|
||||
### 8.5 结论
|
||||
|
||||
STEP-101 的 Native 生命周期修复达到关闭条件:
|
||||
|
||||
1. RenderSource observer 在 owner teardown 前显式 detach;
|
||||
2. readiness state 仍存活时显式销毁 renderSources;
|
||||
3. source clear 前后 tagged work 双 drain;
|
||||
4. glyph/image observer 同步 detach;
|
||||
5. 20 次真实 Android renderer reset/recreate 无 destroyed-mutex;
|
||||
6. 动态 GeoJSON、diagnostics 条件下仍安全;
|
||||
7. STEP-100 locality 保持 PASS;
|
||||
8. focused C++ tests PASS。
|
||||
|
||||
Jest:**NOT RUN**。
|
||||
|
||||
V19 candidate、NavSea 五次冷启动与正式 publish/finalize 属于后续发行 Gate,不改变本 Step Native 修复的 PASS 结论。
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace mbgl {
|
||||
@@ -38,12 +40,139 @@ struct StyleLayerDiagnostics {
|
||||
bool operator==(const StyleLayerDiagnostics&) const = default;
|
||||
};
|
||||
|
||||
// The style layer list is the single dependency source for diagnostics and rendering.
|
||||
struct RenderSegmentDependencies {
|
||||
std::vector<std::string> sourceIds;
|
||||
std::vector<std::string> globalPrerequisites;
|
||||
bool unknown = false;
|
||||
};
|
||||
|
||||
inline RenderSegmentDependencies renderSegmentDependencies(const std::vector<StyleLayerDiagnostics>& layers,
|
||||
int32_t begin,
|
||||
int32_t endExclusive) {
|
||||
RenderSegmentDependencies result;
|
||||
if (begin < 0 || endExclusive < begin || endExclusive > static_cast<int32_t>(layers.size())) {
|
||||
result.unknown = true;
|
||||
return result;
|
||||
}
|
||||
std::set<std::string> sources;
|
||||
std::set<std::string> prerequisites;
|
||||
for (int32_t i = begin; i < endExclusive; ++i) {
|
||||
const auto& layer = layers[static_cast<std::size_t>(i)];
|
||||
if (layer.sourceId) sources.insert(*layer.sourceId);
|
||||
if (layer.layerType == "symbol") prerequisites.insert("symbol/glyph/image/placement");
|
||||
if (layer.layerType == "custom" || layer.layerType == "custom-drawable") result.unknown = true;
|
||||
if (layer.layerType != "background" && layer.layerType != "custom" &&
|
||||
layer.layerType != "custom-drawable" && !layer.sourceId) result.unknown = true;
|
||||
}
|
||||
result.sourceIds.assign(sources.begin(), sources.end());
|
||||
result.globalPrerequisites.assign(prerequisites.begin(), prerequisites.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
class RenderSegmentRevisionBook {
|
||||
public:
|
||||
void advanceGlobal() noexcept { ++globalEpoch; }
|
||||
void advanceSource(const std::string& sourceId) { sourceRevisions[sourceId] = ++sequence; }
|
||||
std::optional<uint64_t> revisionFor(const RenderSegmentDependencies& dependencies) const {
|
||||
if (dependencies.unknown) return std::nullopt;
|
||||
uint64_t revision = globalEpoch;
|
||||
for (const auto& id : dependencies.sourceIds) {
|
||||
const auto found = sourceRevisions.find(id);
|
||||
if (found == sourceRevisions.end()) return std::nullopt;
|
||||
for (const unsigned char ch : id) revision = (revision ^ ch) * 1099511628211ULL;
|
||||
revision = (revision ^ found->second) * 1099511628211ULL;
|
||||
}
|
||||
return revision;
|
||||
}
|
||||
private:
|
||||
uint64_t globalEpoch = 1;
|
||||
uint64_t sequence = 1;
|
||||
// Retain tombstones: remove + recreate of the same ID always receives a newer identity.
|
||||
std::unordered_map<std::string, uint64_t> sourceRevisions;
|
||||
};
|
||||
|
||||
struct RenderSegmentSourceStatus { bool loaded = false; bool fading = false; };
|
||||
struct RenderSegmentSourceAssessment {
|
||||
bool ready = false;
|
||||
bool stable = false;
|
||||
const char* reason = "unknown-dependency";
|
||||
};
|
||||
|
||||
template <typename Lookup>
|
||||
RenderSegmentSourceAssessment assessRenderSegmentSources(const RenderSegmentDependencies& dependencies,
|
||||
Lookup lookup) {
|
||||
RenderSegmentSourceAssessment result;
|
||||
if (dependencies.unknown) return result;
|
||||
for (const auto& id : dependencies.sourceIds) {
|
||||
const std::optional<RenderSegmentSourceStatus> source = lookup(id);
|
||||
if (!source) { result.reason = "missing-source"; return result; }
|
||||
if (!source->loaded) { result.reason = "source-not-loaded"; return result; }
|
||||
if (source->fading) { result.ready = true; result.reason = "source-fading"; return result; }
|
||||
}
|
||||
result.ready = true;
|
||||
result.stable = true;
|
||||
result.reason = "none";
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
uint64_t build = 0;
|
||||
uint64_t reuse = 0;
|
||||
uint64_t invalidation = 0;
|
||||
uint64_t fallbackCount = 0;
|
||||
std::string finalDecision{"disabled"};
|
||||
std::string lastFallbackReason{"none"};
|
||||
bool sourceReady = false;
|
||||
bool sourceStable = false;
|
||||
bool supported = false;
|
||||
uint64_t sourceRevision = 0;
|
||||
std::string lastInvalidationReason{"none"};
|
||||
uint64_t textureCreate = 0;
|
||||
uint64_t compositeCreate = 0;
|
||||
@@ -59,6 +188,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 +214,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;
|
||||
};
|
||||
@@ -109,4 +248,24 @@ struct ResolvedRenderSegmentPlan {
|
||||
bool operator==(const ResolvedRenderSegmentPlan&) const = default;
|
||||
};
|
||||
|
||||
// This is the main-buffer traversal used by the renderer. A failed retained range
|
||||
// is drawn live exactly once; neighboring cached ranges remain composites.
|
||||
template <typename Cached, typename Composite, typename Live>
|
||||
void visitRenderSegmentExecution(const ResolvedRenderSegmentPlan& plan,
|
||||
Cached cached,
|
||||
Composite composite,
|
||||
Live live) {
|
||||
if (!plan.valid) {
|
||||
live(0, plan.layerCount);
|
||||
return;
|
||||
}
|
||||
for (const auto& segment : plan.segments) {
|
||||
if (segment.strategy == RenderSegmentStrategy::Retained && cached(segment.segmentIndex)) {
|
||||
composite(segment.segmentIndex);
|
||||
} else {
|
||||
live(segment.beginIndex, segment.endExclusive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mbgl
|
||||
|
||||
@@ -6,7 +6,7 @@ const marker = "// MapLibreParticleRNTestHost NavSea Particle release SDK";
|
||||
module.exports = function withParticleNativeAndroid(config) {
|
||||
return withProjectBuildGradle(config, (project) => {
|
||||
if (project.modResults.contents.includes(marker)) return project;
|
||||
project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
|
||||
project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.19")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
|
||||
return project;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 13.4.1-particle.19(Android / iOS 统一发行)
|
||||
|
||||
- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;共享 Particle revision 19。
|
||||
## 13.4.1-particle.18(Android / iOS 统一发行)
|
||||
|
||||
- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;共享 Particle revision 18。
|
||||
## 13.4.1-particle.17(Android / iOS 统一发行)
|
||||
|
||||
- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;共享 Particle revision 17。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# @navsea/maplibre-particle-rn
|
||||
|
||||
当前版本:`13.4.1-particle.17`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。
|
||||
当前版本:`13.4.1-particle.19`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.19`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。
|
||||
|
||||
## 当前推荐主链:QBIN V4
|
||||
|
||||
@@ -37,7 +37,7 @@ Podfile 的 `post_install do |installer|` 必须同时调用 `$MLRN.post_install
|
||||
RN tgz:
|
||||
|
||||
```text
|
||||
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.17.tgz
|
||||
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.19.tgz
|
||||
```
|
||||
|
||||
Native Maven repository:
|
||||
@@ -49,7 +49,7 @@ https://pbf.jpcn.ccwu.cc/maven
|
||||
Native coordinate:
|
||||
|
||||
```text
|
||||
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17
|
||||
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.19
|
||||
```
|
||||
|
||||
Expo plugin 与 `android/build.gradle` 必须指向同一 `.17`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。
|
||||
Expo plugin 与 `android/build.gradle` 必须指向同一 `.19`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。
|
||||
|
||||
@@ -14,7 +14,7 @@ android {
|
||||
dependencies {
|
||||
implementation "com.facebook.react:react-android"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20"
|
||||
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17"
|
||||
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.19"
|
||||
compileOnly project(":maplibre_maplibre-react-native")
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const iosTag = "navsea-maplibre-particle-rn-maplibre-spm";
|
||||
module.exports = function withNavSeaMapLibreParticle(config) {
|
||||
config = withProjectBuildGradle(config, (project) => {
|
||||
if (project.modResults.contents.includes(androidMarker)) return project;
|
||||
project.modResults.contents += `\n${androidMarker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
|
||||
project.modResults.contents += `\n${androidMarker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.19")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
|
||||
return project;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navsea/maplibre-particle-rn",
|
||||
"version": "13.4.1-particle.17",
|
||||
"version": "13.4.1-particle.19",
|
||||
"description": "NavSea MapLibre Particle React Native pure Field Renderer API.",
|
||||
"license": "BSD-2-Clause",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -19,6 +19,8 @@ export type {
|
||||
RenderSegmentDefinition,
|
||||
RenderSegmentDiagnostics,
|
||||
RenderSegmentLayerDiagnostics,
|
||||
RenderSourceReadinessDiagnostics,
|
||||
RenderMutationDiagnostics,
|
||||
RenderSegmentPlan,
|
||||
RenderSegmentStrategy,
|
||||
RetainedRenderSegmentDiagnostics,
|
||||
|
||||
@@ -89,12 +89,21 @@ export type RetainedRenderSegmentDiagnostics = Readonly<{
|
||||
resolvedEndLayerId: string | null;
|
||||
layerCount: number;
|
||||
layers: readonly RenderSegmentLayerDiagnostics[];
|
||||
dependentSourceIds: readonly string[];
|
||||
unknownDependencies: readonly string[];
|
||||
cache: Readonly<{
|
||||
cacheState: string;
|
||||
contentRevision: number;
|
||||
build: number;
|
||||
reuse: number;
|
||||
invalidation: number;
|
||||
fallbackCount: number;
|
||||
finalDecision: string;
|
||||
lastFallbackReason: string;
|
||||
sourceReady: boolean;
|
||||
sourceStable: boolean;
|
||||
supported: boolean;
|
||||
sourceRevision: number;
|
||||
lastInvalidationReason: string;
|
||||
textureCreate: number;
|
||||
compositeCreate: number;
|
||||
@@ -111,8 +120,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 +170,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)[];
|
||||
}>;
|
||||
|
||||
@@ -195,6 +195,7 @@ void MapRenderer::requestSnapshot(SnapshotCallback callback) {
|
||||
// Called on OpenGL thread //
|
||||
|
||||
void MapRenderer::resetRenderer() {
|
||||
Log::Info(Event::Android, "MapRenderer::resetRenderer entered");
|
||||
renderer.reset();
|
||||
|
||||
if (!asyncRendererCleanup) {
|
||||
@@ -203,6 +204,7 @@ void MapRenderer::resetRenderer() {
|
||||
|
||||
window.reset();
|
||||
swapBehaviorFlush = false;
|
||||
Log::Info(Event::Android, "MapRenderer::resetRenderer completed");
|
||||
}
|
||||
|
||||
void MapRenderer::scheduleSnapshot(std::unique_ptr<SnapshotCallback> callback) {
|
||||
@@ -322,9 +324,11 @@ void MapRenderer::onSurfaceChanged([[maybe_unused]] JNIEnv& env, jint width, jin
|
||||
}
|
||||
|
||||
void MapRenderer::onRendererReset(JNIEnv&) {
|
||||
Log::Info(Event::Android, "MapRenderer::nativeReset entered");
|
||||
// Make sure to destroy the renderer on the GL Thread
|
||||
auto self = ActorRef<MapRenderer>(*this, mailboxData.getMailbox());
|
||||
self.ask(&MapRenderer::resetRenderer).wait();
|
||||
Log::Info(Event::Android, "MapRenderer::nativeReset completed");
|
||||
}
|
||||
|
||||
// needs to be called on GL thread
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.maplibre.android.maps.renderer.surfaceview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
|
||||
@@ -28,11 +29,13 @@ public class SurfaceViewMapRenderer extends MapRenderer {
|
||||
surfaceView.setDetachedListener(new MapLibreSurfaceView.OnSurfaceViewDetachedListener() {
|
||||
@Override
|
||||
public void onSurfaceViewDetached() {
|
||||
Log.i("Mbgl-SurfaceRenderer", "SurfaceView detached; nativeReset entered");
|
||||
// because the GL thread is destroyed when the view is detached from window,
|
||||
// we need to ensure releasing the native renderer as well.
|
||||
// This avoids releasing it only when the view is being recreated, which is already on a new GL thread,
|
||||
// and leads to JNI crashes like https://github.com/mapbox/mapbox-gl-native/issues/14618
|
||||
nativeReset();
|
||||
Log.i("Mbgl-SurfaceRenderer", "nativeReset returned");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,152 @@ 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 step101Scenario: String? = null
|
||||
private var step101Completed = 0
|
||||
private var step101DynamicTick = 0L
|
||||
private var step101ReadySinceMs = 0L
|
||||
private var step101LoadSinceMs = 0L
|
||||
private val step101Handler = Handler(Looper.getMainLooper())
|
||||
private val step101Poll = object : Runnable {
|
||||
override fun run() {
|
||||
val scenario = step101Scenario ?: return
|
||||
if (!this@ParticleCoreLayerValidationActivity::maplibreMap.isInitialized || !styleReady) {
|
||||
if (SystemClock.elapsedRealtime() - step101LoadSinceMs > 20000L) {
|
||||
Log.e(TAG, "STEP101_SCENARIO_BLOCKED scenario=$scenario completed=$step101Completed reason=style-not-ready")
|
||||
status.text = "STEP-101 BLOCKED: style not ready"
|
||||
step101Scenario = null
|
||||
return
|
||||
}
|
||||
step101Handler.postDelayed(this, 250L)
|
||||
return
|
||||
}
|
||||
val snapshot = JSONObject(maplibreMap.getRenderSegmentDiagnostics())
|
||||
val sources = snapshot.optJSONArray("sources")
|
||||
var tileReady = false
|
||||
for (index in 0 until (sources?.length() ?: 0)) {
|
||||
val source = sources?.optJSONObject(index) ?: continue
|
||||
if (source.optString("sourceId") == Step099ReadinessFixture.STATIC_SOURCE &&
|
||||
source.optBoolean("isLoaded") && source.optInt("tileCount") > 0) tileReady = true
|
||||
}
|
||||
if (!tileReady) {
|
||||
if (SystemClock.elapsedRealtime() - step101LoadSinceMs > 20000L) {
|
||||
Log.e(TAG, "STEP101_SCENARIO_BLOCKED scenario=$scenario completed=$step101Completed reason=tile-not-ready")
|
||||
status.text = "STEP-101 BLOCKED: tile not ready"
|
||||
step101Scenario = null
|
||||
return
|
||||
}
|
||||
step101Handler.postDelayed(this, 250L)
|
||||
return
|
||||
}
|
||||
if (step101ReadySinceMs == 0L) {
|
||||
step101ReadySinceMs = SystemClock.elapsedRealtime()
|
||||
Log.i(TAG, "STEP101_SOURCE_TILE_READY scenario=$scenario completed=$step101Completed " +
|
||||
"sources=${sources?.length()} diagnostics=${snapshot.optLong("contentRevision")}")
|
||||
}
|
||||
if (scenario == "T3_RESET_WITH_DYNAMIC_GEOJSON") {
|
||||
step101DynamicTick += 1
|
||||
updateStep099GeoJson(Step099ReadinessFixture.DYNAMIC_SOURCE, step101DynamicTick)
|
||||
Log.i(TAG, "STEP101_DYNAMIC_SOURCE_MUTATION scenario=$scenario completed=$step101Completed " +
|
||||
"tick=$step101DynamicTick")
|
||||
}
|
||||
if (SystemClock.elapsedRealtime() - step101ReadySinceMs < 2500L) {
|
||||
step101Handler.postDelayed(this, 250L)
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "STEP101_NEW_RENDERER_READY scenario=$scenario completed=$step101Completed " +
|
||||
"styleReady=$styleReady diagnostics=${snapshot.optLong("contentRevision")}")
|
||||
val target = if (scenario == "T2_RESET_LOOP_20") 20 else 1
|
||||
if (step101Completed >= target) {
|
||||
Log.i(TAG, "STEP101_SCENARIO_DONE scenario=$scenario resetsRequested=$step101Completed " +
|
||||
"nativeResetPairsMustMatch=$step101Completed")
|
||||
status.text = "STEP-101 $scenario DONE ${step101Completed}/$target"
|
||||
step101Scenario = null
|
||||
return
|
||||
}
|
||||
if (scenario == "T4_RESET_WITH_DIAGNOSTICS") {
|
||||
Log.i(TAG, "STEP101_PRE_RESET_DIAGNOSTICS ${maplibreMap.getRenderSegmentDiagnostics()}")
|
||||
}
|
||||
val next = step101Completed + 1
|
||||
Log.i(TAG, "STEP101_RESET_REQUEST scenario=$scenario iteration=$next oldMapViewActive=true " +
|
||||
"tileReady=$tileReady")
|
||||
step101Completed = next
|
||||
step101Handler.removeCallbacks(this)
|
||||
recreate() // Activity destroys the old MapView and constructs a new native renderer.
|
||||
}
|
||||
}
|
||||
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)
|
||||
if (step099Tick == 8L || step099Tick == 130L) {
|
||||
Log.i(TAG, "STEP100_WINDOW_MARKER phase=${if (step099Tick == 8L) "baseline" else "final"} " +
|
||||
"tick=$step099Tick sourceId=${Step099ReadinessFixture.DYNAMIC_SOURCE}")
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -112,6 +262,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
step093Mode = savedInstanceState?.getBoolean(STATE_STEP093_MODE, false) ?: false
|
||||
step097Topology = savedInstanceState?.getString(STATE_STEP097_TOPOLOGY)
|
||||
?.let { value -> Step097StencilFixture.Topology.values().firstOrNull { it.name == value } }
|
||||
step101Scenario = if (savedInstanceState?.containsKey(STATE_STEP101_SCENARIO) == true) {
|
||||
savedInstanceState.getString(STATE_STEP101_SCENARIO)
|
||||
} else {
|
||||
intent.getStringExtra("step101_scenario")?.takeIf { it in STEP101_SCENARIOS }
|
||||
}
|
||||
step101Completed = savedInstanceState?.getInt(STATE_STEP101_COMPLETED, 0) ?: 0
|
||||
step093Topology = savedInstanceState?.getString(STATE_STEP093_TOPOLOGY)
|
||||
?.let { value -> Step093Topology.values().firstOrNull { it.name == value } }
|
||||
?: Step093Topology.BOTTOM_CONTINUOUS_TOP
|
||||
@@ -150,7 +306,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
updateNavSeaPbfButtonState()
|
||||
updatePbfModeButtonState()
|
||||
updateRetainedCacheButtonState()
|
||||
if (step097Topology != null) loadStep097StencilStyle()
|
||||
if (step101Scenario != null) loadStep101Style()
|
||||
else if (step097Topology != null) loadStep097StencilStyle()
|
||||
else if (step093Mode) loadStep093Style(configureBeforeStyle = false)
|
||||
else loadValidationStyle(active = active, enabled = enabled, reason = "INITIAL")
|
||||
}
|
||||
@@ -454,6 +611,8 @@ 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")
|
||||
menu.add(0, 18, 18, "STEP-101 TEARDOWN")
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId])
|
||||
@@ -464,6 +623,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
14 -> refreshStep093Diagnostics(log = true)
|
||||
15 -> applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP)
|
||||
16 -> showStep097StencilMenu(anchor)
|
||||
17 -> showStep099Menu(anchor)
|
||||
18 -> showStep101Menu(anchor)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -491,7 +652,189 @@ 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 showStep101Menu(anchor: android.view.View) {
|
||||
PopupMenu(this, anchor).apply {
|
||||
STEP101_SCENARIOS.forEachIndexed { index, scenario -> menu.add(0, index, index, scenario) }
|
||||
setOnMenuItemClickListener { item ->
|
||||
val scenario = STEP101_SCENARIOS.getOrNull(item.itemId) ?: return@setOnMenuItemClickListener false
|
||||
step101Scenario = scenario
|
||||
step101Completed = 0
|
||||
loadStep101Style()
|
||||
true
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadStep101Style() {
|
||||
val scenario = step101Scenario ?: return
|
||||
stopStep099Scenario()
|
||||
step101Handler.removeCallbacksAndMessages(null)
|
||||
step101ReadySinceMs = 0L
|
||||
step101LoadSinceMs = SystemClock.elapsedRealtime()
|
||||
val fixture = Step099ReadinessFixture.Scenario.T4_RCR
|
||||
val accepted = maplibreMap.setRenderSegmentPlan(Step099ReadinessFixture.planJson(fixture))
|
||||
styleReady = false
|
||||
val epoch = ++styleLoadEpoch
|
||||
Log.i(TAG, "STEP101_OLD_RESET_COMPLETED_OR_INITIAL scenario=$scenario completed=$step101Completed " +
|
||||
"planAccepted=$accepted")
|
||||
maplibreMap.setStyle(Style.Builder().fromJson(Step099ReadinessFixture.styleJson(fixture))) {
|
||||
if (step101Scenario != scenario || epoch != styleLoadEpoch) return@setStyle
|
||||
styleReady = true
|
||||
maplibreMap.moveCamera(CameraUpdateFactory.newCameraPosition(
|
||||
CameraPosition.Builder().target(LatLng(28.0, 135.0)).zoom(3.0).build()
|
||||
))
|
||||
Log.i(TAG, "STEP101_STYLE_READY scenario=$scenario completed=$step101Completed")
|
||||
step101Handler.removeCallbacks(step101Poll)
|
||||
step101Handler.post(step101Poll)
|
||||
}
|
||||
step101Handler.post(step101Poll)
|
||||
}
|
||||
|
||||
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")}" +
|
||||
" srcRev=${cache.optLong("sourceRevision")}" +
|
||||
" ready=${cache.optBoolean("sourceReady")}/${cache.optBoolean("sourceStable")}" +
|
||||
" supported=${cache.optBoolean("supported")}" +
|
||||
" decision=${cache.optString("finalDecision")}" +
|
||||
" fallback=${cache.optLong("fallbackCount")}/${cache.optString("lastFallbackReason")}"
|
||||
}
|
||||
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 +973,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
variant: Step093Variant = Step093Variant.DEFAULT,
|
||||
configureBeforeStyle: Boolean = false
|
||||
) {
|
||||
stopStep099Scenario()
|
||||
leaveRealWeatherMode()
|
||||
step093Mode = true
|
||||
step093Topology = topology
|
||||
@@ -720,6 +1064,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 +1493,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 +2253,42 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
override fun onStart() { super.onStart(); mapView.onStart(); if (step093Mode || step097Topology != null) startStep093DiagnosticsPolling() }
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
mapView.onStart()
|
||||
if (step101Scenario != null && styleReady) step101Handler.post(step101Poll)
|
||||
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() {
|
||||
step101Handler.removeCallbacksAndMessages(null)
|
||||
step093Handler.removeCallbacks(step093DiagnosticsPoll)
|
||||
step099Running = false
|
||||
step099Handler.removeCallbacksAndMessages(null)
|
||||
mapView.onStop()
|
||||
super.onStop()
|
||||
}
|
||||
override fun onDestroy() {
|
||||
step101Handler.removeCallbacksAndMessages(null)
|
||||
if (step101Scenario != null) Log.i(TAG, "STEP101_MAPVIEW_DESTROY_ENTER scenario=$step101Scenario completed=$step101Completed")
|
||||
step093Handler.removeCallbacks(step093DiagnosticsPoll)
|
||||
stopStep099Scenario()
|
||||
weatherRuntime?.dispose()
|
||||
weatherRuntime = null
|
||||
mapView.onDestroy()
|
||||
if (step101Scenario != null) Log.i(TAG, "STEP101_MAPVIEW_DESTROY_RETURN scenario=$step101Scenario completed=$step101Completed")
|
||||
super.onDestroy()
|
||||
}
|
||||
override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() }
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -1935,6 +2315,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
outState.putBoolean(STATE_RETAINED_CACHE_ENABLED, retainedCacheEnabled)
|
||||
outState.putBoolean(STATE_STEP093_MODE, step093Mode)
|
||||
outState.putString(STATE_STEP097_TOPOLOGY, step097Topology?.name)
|
||||
outState.putString(STATE_STEP101_SCENARIO, step101Scenario)
|
||||
outState.putInt(STATE_STEP101_COMPLETED, step101Completed)
|
||||
outState.putString(STATE_STEP093_TOPOLOGY, step093Topology.name)
|
||||
outState.putString(STATE_STEP093_VARIANT, step093Variant.name)
|
||||
super.onSaveInstanceState(outState)
|
||||
@@ -1972,12 +2354,19 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private const val STATE_RETAINED_CACHE_ENABLED = "particle_validation.retained_cache_enabled"
|
||||
private const val STATE_STEP093_MODE = "particle_validation.step093_mode"
|
||||
private const val STATE_STEP097_TOPOLOGY = "particle_validation.step097_topology"
|
||||
private const val STATE_STEP101_SCENARIO = "particle_validation.step101_scenario"
|
||||
private const val STATE_STEP101_COMPLETED = "particle_validation.step101_completed"
|
||||
private val STEP101_SCENARIOS = listOf(
|
||||
"T1_RESET_ONCE", "T2_RESET_LOOP_20", "T3_RESET_WITH_DYNAMIC_GEOJSON", "T4_RESET_WITH_DIAGNOSTICS"
|
||||
)
|
||||
private const val STATE_STEP093_TOPOLOGY = "particle_validation.step093_topology"
|
||||
private const val STATE_STEP093_VARIANT = "particle_validation.step093_variant"
|
||||
private const val PARTICLE_LAYER_ID = "particle-raster-core"
|
||||
private const val WEATHER_COLOR_LAYER_ID = "weather-color-real"
|
||||
private const val PARTICLE_SOURCE_ID = "particle-fixture"
|
||||
private const val STEP093_DIAGNOSTICS_INTERVAL_MS = 1000L
|
||||
private const val 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,149 @@
|
||||
package org.maplibre.android.testapp.activity.particle
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
/** STEP-099/100 offline source readiness and retained segment fixtures. */
|
||||
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 UPPER_STATIC_SOURCE = "step099-upper-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.T1_STATIC || scenario == Scenario.T3_PERMANENT_UNREADY) {
|
||||
sources.put(UPPER_STATIC_SOURCE, geoJsonSource(
|
||||
"""{"type":"Polygon","coordinates":[[[116,4],[154,4],[154,52],[116,52],[116,4]]]}"""
|
||||
))
|
||||
}
|
||||
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)))
|
||||
}
|
||||
if (scenario == Scenario.T1_STATIC || scenario == Scenario.T3_PERMANENT_UNREADY) {
|
||||
layers.put(JSONObject()
|
||||
.put("id", UPPER_STATIC_LAYER)
|
||||
.put("type", "fill")
|
||||
.put("source", UPPER_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)))
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
13.4.1-particle.17
|
||||
13.4.1-particle.19
|
||||
|
||||
@@ -9,8 +9,8 @@ let package = Package(
|
||||
targets: [
|
||||
.binaryTarget(
|
||||
name: "MapLibre",
|
||||
url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip",
|
||||
checksum: "5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be"
|
||||
url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.19/MapLibre.dynamic.xcframework.zip",
|
||||
checksum: "221c069b6ca073854716799d8593784310d4f0050be9c9d9a88215b352512efd"
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# MapLibre Particle iOS 发行
|
||||
|
||||
当前正式发行:6.28.0-particle.17
|
||||
当前正式发行:6.28.0-particle.19
|
||||
|
||||
- Renderer:Metal
|
||||
- Product / Module:MapLibre
|
||||
- Binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip
|
||||
- SHA-256:5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be
|
||||
- Binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.19/MapLibre.dynamic.xcframework.zip
|
||||
- SHA-256:221c069b6ca073854716799d8593784310d4f0050be9c9d9a88215b352512efd
|
||||
- SwiftPM:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git
|
||||
- Exact Version:6.28.0-particle.17
|
||||
- Source Commit:95f0a45d6d97c55f0fd417cf4d7a9a3746ab5aa3
|
||||
- Exact Version:6.28.0-particle.19
|
||||
- Source Commit:7c5ed27b0b3ae5fe74ab245cbef9dabf913dde4f
|
||||
|
||||
构建命令固定为:
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"status": "PUBLISHED_PUBLIC_READBACK_PASS",
|
||||
"distributionVersion": "6.28.0-particle.17",
|
||||
"distributionVersion": "6.28.0-particle.19",
|
||||
"moduleName": "MapLibre",
|
||||
"artifact": {
|
||||
"fileName": "MapLibre.dynamic.xcframework.zip",
|
||||
"sizeBytes": 8443308,
|
||||
"sha256": "5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip"
|
||||
"sizeBytes": 8480397,
|
||||
"sha256": "221c069b6ca073854716799d8593784310d4f0050be9c9d9a88215b352512efd",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.19/MapLibre.dynamic.xcframework.zip"
|
||||
},
|
||||
"sourceCommit": "95f0a45d6d97c55f0fd417cf4d7a9a3746ab5aa3",
|
||||
"sourceCommit": "7c5ed27b0b3ae5fe74ab245cbef9dabf913dde4f",
|
||||
"swiftPackage": {
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git",
|
||||
"requirement": "exactVersion",
|
||||
"version": "6.28.0-particle.17",
|
||||
"version": "6.28.0-particle.19",
|
||||
"productName": "MapLibre"
|
||||
},
|
||||
"notes": "Metal renderer release built with bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic"
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"releaseState": "PUBLISHED_PUBLIC_READBACK_PASS",
|
||||
"particleRevision": 17,
|
||||
"sourceCommit": "95f0a45d6d97c55f0fd417cf4d7a9a3746ab5aa3",
|
||||
"generatedAtUtc": "2026-09-29T00:43:56.789316+00:00",
|
||||
"particleRevision": 19,
|
||||
"sourceCommit": "7c5ed27b0b3ae5fe74ab245cbef9dabf913dde4f",
|
||||
"generatedAtUtc": "2026-10-07T07:17:04.530231+00:00",
|
||||
"reactNative": {
|
||||
"name": "@navsea/maplibre-particle-rn",
|
||||
"version": "13.4.1-particle.17",
|
||||
"version": "13.4.1-particle.19",
|
||||
"artifact": {
|
||||
"fileName": "navsea-maplibre-particle-rn-13.4.1-particle.17.tgz",
|
||||
"sizeBytes": 28526,
|
||||
"sha256": "7415a094da5351b86a3c6343a8bc2ef75b7a8ad49577a5f0efc81407d72a79ec",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.17.tgz"
|
||||
"fileName": "navsea-maplibre-particle-rn-13.4.1-particle.19.tgz",
|
||||
"sizeBytes": 28825,
|
||||
"sha256": "c98f1aef868c671a99556dbdd2518cdaa3a88a3d6133e620e5e8a38080d19b1a",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.19.tgz"
|
||||
},
|
||||
"mapLibreReactNative": "11.3.6"
|
||||
},
|
||||
"android": {
|
||||
"version": "13.4.1-particle.17",
|
||||
"coordinate": "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17",
|
||||
"version": "13.4.1-particle.19",
|
||||
"coordinate": "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.19",
|
||||
"renderer": "opengl",
|
||||
"artifact": {
|
||||
"fileName": "android-sdk-particle-opengl-13.4.1-particle.17.aar",
|
||||
"sizeBytes": 170158515,
|
||||
"sha256": "6c5f10f3ddd8b81e1c14ed9c310f8d5e216dfdded32c3db55bfb0ba5f5d175d5",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.17/android-sdk-particle-opengl-13.4.1-particle.17.aar"
|
||||
"fileName": "android-sdk-particle-opengl-13.4.1-particle.19.aar",
|
||||
"sizeBytes": 170726451,
|
||||
"sha256": "8ef9de51564aad6d5d8a9678534120cee34d3ee65c41f9f266c29bf057029712",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.19/android-sdk-particle-opengl-13.4.1-particle.19.aar"
|
||||
},
|
||||
"ndk": "28.2.13676358",
|
||||
"gradle": "9.5.1",
|
||||
"jdk": "openjdk version \"17.0.20.1\" 2026-08-18"
|
||||
},
|
||||
"ios": {
|
||||
"distributionVersion": "6.28.0-particle.17",
|
||||
"distributionVersion": "6.28.0-particle.19",
|
||||
"renderer": "metal",
|
||||
"moduleName": "MapLibre",
|
||||
"artifact": {
|
||||
"fileName": "MapLibre.dynamic.xcframework.zip",
|
||||
"sizeBytes": 8443308,
|
||||
"sha256": "5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip"
|
||||
"sizeBytes": 8480397,
|
||||
"sha256": "221c069b6ca073854716799d8593784310d4f0050be9c9d9a88215b352512efd",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.19/MapLibre.dynamic.xcframework.zip"
|
||||
},
|
||||
"swiftPackage": {
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git",
|
||||
"requirement": "exactVersion",
|
||||
"version": "6.28.0-particle.17",
|
||||
"version": "6.28.0-particle.19",
|
||||
"productName": "MapLibre"
|
||||
},
|
||||
"xcode": "Xcode 26.3 Build version 17C529 ",
|
||||
|
||||
@@ -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_,
|
||||
@@ -147,15 +191,134 @@ RenderOrchestrator::~RenderOrchestrator() {
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for any deferred cleanup tasks to complete before releasing and potentially
|
||||
// destroying the scheduler. Those cleanup tasks must not hold the final reference
|
||||
// to the scheduler because it cannot be destroyed from one of its own pool threads.
|
||||
// Finish work that can still reach the source observers before detaching them.
|
||||
threadPool.waitForEmpty();
|
||||
|
||||
for (auto& entry : renderSources) {
|
||||
entry.second->setObserver(nullptr);
|
||||
}
|
||||
glyphManager->setObserver(nullptr);
|
||||
imageManager->setObserver(nullptr);
|
||||
|
||||
// Tile destructors can synchronously report Cancelled and enqueue further work.
|
||||
// Destroy sources while the readiness state and its mutex are still alive.
|
||||
renderSources.clear();
|
||||
|
||||
// Drain work submitted by source/tile destruction before the scheduler and
|
||||
// the remaining orchestrator state are torn down.
|
||||
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::advanceRetainedSourceRevision(RetainedUnderlayReason reason,
|
||||
const std::string& sourceId) noexcept {
|
||||
if (sourceId.empty()) {
|
||||
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
|
||||
retainedSegmentRevisions.advanceGlobal();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
|
||||
retainedSegmentRevisions.advanceSource(sourceId);
|
||||
} catch (...) {
|
||||
// Correctness state must fail closed if a source revision cannot be recorded.
|
||||
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
|
||||
retainedSegmentRevisions.advanceGlobal();
|
||||
}
|
||||
recordReadinessMutation(readinessEventType(reason), reason, sourceId);
|
||||
}
|
||||
|
||||
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);
|
||||
if (sourceId && (reason == RetainedUnderlayReason::SourceMutation ||
|
||||
reason == RetainedUnderlayReason::TileMutation)) {
|
||||
advanceRetainedSourceRevision(reason, *sourceId);
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
|
||||
retainedSegmentRevisions.advanceGlobal();
|
||||
}
|
||||
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() {
|
||||
@@ -360,7 +523,17 @@ 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);
|
||||
retainedUnderlayContentReason.store(RetainedUnderlayReason::SourceMutation, std::memory_order_release);
|
||||
retainedUnderlayContentRevision.fetch_add(1, std::memory_order_acq_rel);
|
||||
for (const auto& entry : sourceDiff.added) {
|
||||
advanceRetainedSourceRevision(RetainedUnderlayReason::SourceMutation, entry.first);
|
||||
}
|
||||
for (const auto& entry : sourceDiff.removed) {
|
||||
advanceRetainedSourceRevision(RetainedUnderlayReason::SourceMutation, entry.first);
|
||||
}
|
||||
for (const auto& entry : sourceDiff.changed) {
|
||||
advanceRetainedSourceRevision(RetainedUnderlayReason::SourceMutation, entry.first);
|
||||
}
|
||||
}
|
||||
sourceImpls = updateParameters->sources;
|
||||
|
||||
@@ -449,11 +622,13 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
|
||||
if (!impl.sourceLayer.empty()) value.sourceLayer = impl.sourceLayer;
|
||||
styleLayerDiagnostics.push_back(std::move(value));
|
||||
}
|
||||
retainedStyleLayers = styleLayerDiagnostics;
|
||||
updateParameters->retainedUnderlayState->recordRenderSegmentResolution(
|
||||
renderSegmentPlan, std::move(styleLayerDiagnostics));
|
||||
}
|
||||
} else {
|
||||
renderSegmentPlan = {};
|
||||
retainedStyleLayers.clear();
|
||||
}
|
||||
|
||||
// Update all sources and initialize renderItems.
|
||||
@@ -519,7 +694,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 +1020,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 +1043,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,6 +1161,52 @@ bool RenderOrchestrator::retainedRangeIsStable(TimePoint timePoint, int32_t begi
|
||||
return true;
|
||||
}
|
||||
|
||||
RenderOrchestrator::RetainedSegmentSourceState RenderOrchestrator::retainedSegmentSourceState(
|
||||
int32_t begin, int32_t endExclusive) const {
|
||||
RetainedSegmentSourceState result;
|
||||
const auto dependencies = renderSegmentDependencies(retainedStyleLayers, begin, endExclusive);
|
||||
if (dependencies.unknown) return result;
|
||||
// The global epoch contains only changes whose effect cannot be scoped to one source.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
|
||||
const auto value = retainedSegmentRevisions.revisionFor(dependencies);
|
||||
if (!value) return result;
|
||||
result.revision = *value;
|
||||
}
|
||||
const auto assessment = assessRenderSegmentSources(dependencies, [&](const std::string& sourceId)
|
||||
-> std::optional<RenderSegmentSourceStatus> {
|
||||
const auto source = renderSources.find(sourceId);
|
||||
if (source == renderSources.end()) return std::nullopt;
|
||||
return RenderSegmentSourceStatus{source->second->isLoaded(), source->second->hasFadingTiles()};
|
||||
});
|
||||
result.ready = assessment.ready;
|
||||
result.stable = assessment.stable;
|
||||
result.reason = assessment.reason;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool RenderOrchestrator::retainedSegmentIsStable(TimePoint timePoint,
|
||||
int32_t begin,
|
||||
int32_t endExclusive) const {
|
||||
if (renderLight.hasTransition()) return false;
|
||||
if (retainedRangeUsesPlacement(begin, endExclusive) && placementController.hasTransitions(timePoint)) return false;
|
||||
const auto first = std::clamp<int32_t>(begin, 0, static_cast<int32_t>(orderedLayers.size()));
|
||||
const auto last = std::clamp<int32_t>(endExclusive, first, static_cast<int32_t>(orderedLayers.size()));
|
||||
for (int32_t index = first; index < last; ++index) {
|
||||
const auto& layer = orderedLayers[index].get();
|
||||
if (layer.hasTransition() || layer.needsRepaint()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderOrchestrator::retainedGlobalResourcesReady() const {
|
||||
return imageManager->isLoaded();
|
||||
}
|
||||
|
||||
bool RenderOrchestrator::retainedGlobalLightTransition() const {
|
||||
return renderLight.hasTransition();
|
||||
}
|
||||
|
||||
bool RenderOrchestrator::retainedUnderlayHasUnsupportedLayer(int32_t boundary) const {
|
||||
return retainedRangeHasUnsupportedLayer(0, boundary);
|
||||
}
|
||||
@@ -1080,6 +1311,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 +1533,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 +1544,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>
|
||||
@@ -127,6 +128,16 @@ public:
|
||||
bool retainedUnderlayHasUnsupportedLayer(int32_t boundary) const;
|
||||
bool retainedRangeUsesPlacement(int32_t begin, int32_t endExclusive) const;
|
||||
bool retainedRangeIsStable(TimePoint, int32_t begin, int32_t endExclusive) const;
|
||||
struct RetainedSegmentSourceState {
|
||||
uint64_t revision = 0;
|
||||
bool ready = false;
|
||||
bool stable = false;
|
||||
const char* reason = "unknown-dependency";
|
||||
};
|
||||
RetainedSegmentSourceState retainedSegmentSourceState(int32_t begin, int32_t endExclusive) const;
|
||||
bool retainedSegmentIsStable(TimePoint, int32_t begin, int32_t endExclusive) const;
|
||||
bool retainedGlobalResourcesReady() const;
|
||||
bool retainedGlobalLightTransition() const;
|
||||
bool retainedRangeHasUnsupportedLayer(int32_t begin, int32_t endExclusive) const;
|
||||
bool continuousRangeSupportsParticleFastPath(int32_t begin, int32_t endExclusive) const;
|
||||
uint64_t retainedRangeContentFingerprint(int32_t begin, int32_t endExclusive) const;
|
||||
@@ -300,6 +311,9 @@ public:
|
||||
const ZoomHistory& getZoomHistory() const { return zoomHistory; }
|
||||
|
||||
private:
|
||||
// Allows the focused teardown test to install a pending tile-backed source.
|
||||
friend struct RenderOrchestratorTeardownTestAccess;
|
||||
|
||||
bool isLoaded() const;
|
||||
bool hasTransitions(TimePoint) const;
|
||||
|
||||
@@ -332,7 +346,15 @@ 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 advanceRetainedSourceRevision(RetainedUnderlayReason, const std::string& sourceId) 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;
|
||||
|
||||
@@ -364,8 +386,21 @@ private:
|
||||
std::string retainedGlyphURL;
|
||||
const void* retainedFontFacesIdentity = nullptr;
|
||||
std::atomic<uint64_t> retainedUnderlayContentRevision{1};
|
||||
RenderSegmentRevisionBook retainedSegmentRevisions;
|
||||
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
|
||||
@@ -374,6 +409,7 @@ private:
|
||||
// reallocation on each frame.
|
||||
std::vector<Immutable<style::LayerProperties>> filteredLayersForSource;
|
||||
RenderLayerReferences orderedLayers;
|
||||
std::vector<StyleLayerDiagnostics> retainedStyleLayers;
|
||||
ResolvedRenderSegmentPlan renderSegmentPlan;
|
||||
bool renderSegmentStyleReady = false;
|
||||
RenderLayerReferences layersNeedPlacement;
|
||||
|
||||
@@ -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();
|
||||
@@ -145,6 +198,13 @@ inline std::string renderSegmentDiagnosticsToJSON(const RenderSegmentDiagnostics
|
||||
writer.Key("build"); writer.Uint64(cache.build);
|
||||
writer.Key("reuse"); writer.Uint64(cache.reuse);
|
||||
writer.Key("invalidation"); writer.Uint64(cache.invalidation);
|
||||
writer.Key("fallbackCount"); writer.Uint64(cache.fallbackCount);
|
||||
writer.Key("finalDecision"); writer.String(cache.finalDecision.c_str());
|
||||
writer.Key("lastFallbackReason"); writer.String(cache.lastFallbackReason.c_str());
|
||||
writer.Key("sourceReady"); writer.Bool(cache.sourceReady);
|
||||
writer.Key("sourceStable"); writer.Bool(cache.sourceStable);
|
||||
writer.Key("supported"); writer.Bool(cache.supported);
|
||||
writer.Key("sourceRevision"); writer.Uint64(cache.sourceRevision);
|
||||
writer.Key("lastInvalidationReason"); writer.String(cache.lastInvalidationReason.c_str());
|
||||
writer.Key("textureCreate"); writer.Uint64(cache.textureCreate);
|
||||
writer.Key("compositeCreate"); writer.Uint64(cache.compositeCreate);
|
||||
|
||||
@@ -71,7 +71,7 @@ RenderSource::RenderSource(Immutable<style::Source::Impl> impl)
|
||||
RenderSource::~RenderSource() = default;
|
||||
|
||||
void RenderSource::setObserver(RenderSourceObserver* observer_) {
|
||||
observer = observer_;
|
||||
observer = observer_ ? observer_ : &nullObserver;
|
||||
}
|
||||
|
||||
void RenderSource::onTileChanged(Tile& tile) {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -551,17 +551,17 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
const auto retainedTopologyRevision = particleFastFrame ? retainedUnderlayTopologyTracker.current()
|
||||
: retainedUnderlayTopologyTracker.observe(retainedTopology);
|
||||
bool legacyInRetainedRange = false;
|
||||
std::vector<bool> legacyInSegment(segmentPlan.segments.size(), false);
|
||||
if (retainedMode) {
|
||||
for (const auto& item : layerRenderItems) {
|
||||
const auto index = static_cast<int32_t>(item.index);
|
||||
for (const auto& segment : segmentPlan.segments) {
|
||||
if (segment.strategy == RenderSegmentStrategy::Retained && index >= segment.beginIndex &&
|
||||
index < segment.endExclusive && item.layer.get().usesLegacyRendering()) {
|
||||
legacyInSegment[segment.segmentIndex] = true;
|
||||
legacyInRetainedRange = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (legacyInRetainedRange) break;
|
||||
}
|
||||
}
|
||||
bool retainedStaticUsesPlacement = false;
|
||||
@@ -601,7 +601,7 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
.has3D = renderTreeParameters.has3D,
|
||||
.unsupportedStaticLayer = retainedUnsupportedStaticLayer,
|
||||
};
|
||||
const auto retainedStaticRevision = retainedUnderlayRevisionTracker.observe(
|
||||
retainedUnderlayRevisionTracker.observe(
|
||||
retainedRevisionState, retainedUnderlayReasonText(retainedContentReason));
|
||||
const bool retainedSupported = retainedMode &&
|
||||
renderTreeParameters.loaded && !renderTreeParameters.has3D &&
|
||||
@@ -636,7 +636,8 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
const auto wantsResources = [](RetainedUnderlayDecision decision) {
|
||||
return decision == RetainedUnderlayDecision::Build || decision == RetainedUnderlayDecision::Reuse;
|
||||
};
|
||||
bool resourcesReady = true;
|
||||
std::vector<RenderOrchestrator::RetainedSegmentSourceState> segmentSourceStates(segmentPlan.segments.size());
|
||||
std::vector<bool> segmentSupportsCache(segmentPlan.segments.size(), false);
|
||||
for (const auto& segment : segmentPlan.segments) {
|
||||
if (segment.strategy != RenderSegmentStrategy::Retained) continue;
|
||||
auto& cache = retainedSegmentCaches[segment.segmentIndex];
|
||||
@@ -646,34 +647,38 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
const bool layerContentChanged = retainedSegmentFingerprints[segment.segmentIndex] != 0 &&
|
||||
retainedSegmentFingerprints[segment.segmentIndex] != fingerprint;
|
||||
retainedSegmentFingerprints[segment.segmentIndex] = fingerprint;
|
||||
const auto segmentContentRevision = retainedStaticRevision ^
|
||||
(fingerprint +
|
||||
0x9e3779b97f4a7c15ULL + (retainedStaticRevision << 6U) + (retainedStaticRevision >> 2U));
|
||||
const auto sourceState = orchestrator.retainedSegmentSourceState(
|
||||
segment.beginIndex, segment.endExclusive);
|
||||
segmentSourceStates[segment.segmentIndex] = sourceState;
|
||||
const auto segmentContentRevision = sourceState.revision ^
|
||||
(fingerprint + 0x9e3779b97f4a7c15ULL + (sourceState.revision << 6U) +
|
||||
(sourceState.revision >> 2U)) ^
|
||||
static_cast<uint64_t>(updateParameters->pixelRatio * 1000000.0f);
|
||||
const bool segmentStable = sourceState.stable && orchestrator.retainedSegmentIsStable(
|
||||
renderTreeParameters.timePoint, segment.beginIndex, segment.endExclusive);
|
||||
const bool segmentSupported = retainedMode && updateParameters->styleLoaded &&
|
||||
updateParameters->spriteLoaded && orchestrator.retainedGlobalResourcesReady() &&
|
||||
updateParameters->mode == MapMode::Continuous && !renderTreeParameters.has3D &&
|
||||
renderTreeParameters.debugOptions == MapDebugOptions::NoDebug && segmentStable &&
|
||||
retainedPlacementStable && !legacyInSegment[segment.segmentIndex] &&
|
||||
!orchestrator.retainedRangeHasUnsupportedLayer(segment.beginIndex, segment.endExclusive);
|
||||
segmentSupportsCache[segment.segmentIndex] = segmentSupported;
|
||||
const char* segmentFallbackReason = !sourceState.ready ? sourceState.reason
|
||||
: !sourceState.stable ? sourceState.reason
|
||||
: !updateParameters->styleLoaded || !updateParameters->spriteLoaded ||
|
||||
!orchestrator.retainedGlobalResourcesReady() ? "style-not-loaded"
|
||||
: !segmentStable ? "static-transition" : retainedFallbackReason;
|
||||
decision = cache->evaluate(segmentKeys[segment.segmentIndex],
|
||||
segmentContentRevision,
|
||||
retainedSupported,
|
||||
retainedFallbackReason,
|
||||
segmentSupported,
|
||||
segmentFallbackReason,
|
||||
layerContentChanged ? "layer-mutation"
|
||||
: retainedUnderlayRevisionTracker.reason().c_str());
|
||||
if (resourcesReady && wantsResources(decision)) {
|
||||
resourcesReady = staticData->shaders && cache->prepareResources(
|
||||
context, *staticData->shaders, *segmentKeys[segment.segmentIndex]);
|
||||
}
|
||||
}
|
||||
if (!resourcesReady) {
|
||||
for (const auto& segment : segmentPlan.segments) {
|
||||
if (segment.strategy != RenderSegmentStrategy::Retained) continue;
|
||||
auto& decision = segmentDecisions[segment.segmentIndex];
|
||||
if (wantsResources(decision)) {
|
||||
retainedSegmentCaches[segment.segmentIndex]->markBuildFailed("resource-or-compositor");
|
||||
decision = RetainedUnderlayDecision::Fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool useRetainedRanges = retainedSupported;
|
||||
for (const auto& segment : segmentPlan.segments) {
|
||||
if (segment.strategy == RenderSegmentStrategy::Retained) {
|
||||
useRetainedRanges = useRetainedRanges && wantsResources(segmentDecisions[segment.segmentIndex]);
|
||||
: "source-or-global-mutation");
|
||||
if (wantsResources(decision) &&
|
||||
(!staticData->shaders || !cache->prepareResources(
|
||||
context, *staticData->shaders, *segmentKeys[segment.segmentIndex]))) {
|
||||
cache->markBuildFailed("resource-or-compositor");
|
||||
decision = RetainedUnderlayDecision::Fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,9 +731,10 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
if (!particleFastFrame) {
|
||||
orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); });
|
||||
}
|
||||
if (useRetainedRanges) {
|
||||
if (retainedMode) {
|
||||
for (const auto& segment : segmentPlan.segments) {
|
||||
if (segment.strategy == RenderSegmentStrategy::Retained) {
|
||||
if (segment.strategy == RenderSegmentStrategy::Retained &&
|
||||
wantsResources(segmentDecisions[segment.segmentIndex])) {
|
||||
retainedSegmentCaches[segment.segmentIndex]->upload(*uploadPass);
|
||||
}
|
||||
}
|
||||
@@ -797,10 +803,10 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
};
|
||||
|
||||
const auto buildRetainedRange = [&](RetainedUnderlayCache& cache,
|
||||
RetainedUnderlayDecision decision,
|
||||
RetainedUnderlayDecision& decision,
|
||||
int32_t begin,
|
||||
int32_t endExclusive) {
|
||||
if (!useRetainedRanges || decision != RetainedUnderlayDecision::Build || begin >= endExclusive) return;
|
||||
if (decision != RetainedUnderlayDecision::Build || begin >= endExclusive) return;
|
||||
bool drawFailed = false;
|
||||
bool globalUniformsBound = false;
|
||||
try {
|
||||
@@ -841,7 +847,7 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
}
|
||||
if (drawFailed) {
|
||||
cache.markBuildFailed("resource-or-compositor");
|
||||
useRetainedRanges = false;
|
||||
decision = RetainedUnderlayDecision::Fallback;
|
||||
} else {
|
||||
cache.markBuildSucceeded();
|
||||
}
|
||||
@@ -878,7 +884,7 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
PaintParameters::depthEpsilon;
|
||||
|
||||
// draw layer groups, opaque pass
|
||||
if (useRetainedRanges && retainedMode) {
|
||||
if (retainedMode) {
|
||||
orchestrator.visitLayerGroupsInRangeReversed(
|
||||
begin, endExclusive,
|
||||
[&](LayerGroupBase& layerGroup, size_t ordinal) {
|
||||
@@ -902,7 +908,7 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
PaintParameters::depthEpsilon;
|
||||
|
||||
// draw layer groups, translucent pass
|
||||
if (useRetainedRanges && retainedMode) {
|
||||
if (retainedMode) {
|
||||
orchestrator.visitLayerGroupsInRange(
|
||||
begin, endExclusive,
|
||||
[&](LayerGroupBase& layerGroup, size_t ordinal) {
|
||||
@@ -929,7 +935,7 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
parameters.currentLayer = i;
|
||||
const LayerRenderItem& layerItem = *it;
|
||||
const RenderItem& item = layerItem;
|
||||
const bool itemInLiveRange = !useRetainedRanges || !retainedMode ||
|
||||
const bool itemInLiveRange = !retainedMode ||
|
||||
(static_cast<int32_t>(layerItem.index) >= begin &&
|
||||
static_cast<int32_t>(layerItem.index) < endExclusive);
|
||||
if (itemInLiveRange && item.hasRenderPass(parameters.pass)) {
|
||||
@@ -964,20 +970,39 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
segment.endExclusive);
|
||||
}
|
||||
}
|
||||
if (retainedRuntimeState) {
|
||||
for (const auto& segment : segmentPlan.segments) {
|
||||
if (segment.strategy != RenderSegmentStrategy::Retained) continue;
|
||||
const auto index = segment.segmentIndex;
|
||||
const auto decision = segmentDecisions[index];
|
||||
const char* decisionText = decision == RetainedUnderlayDecision::Build ? "build" :
|
||||
decision == RetainedUnderlayDecision::Reuse ? "reuse" :
|
||||
decision == RetainedUnderlayDecision::Fallback ? "fallback" : "disabled";
|
||||
const auto& source = segmentSourceStates[index];
|
||||
retainedRuntimeState->recordSegmentExecution(index, source.ready, source.stable,
|
||||
segmentSupportsCache[index], source.revision, decisionText,
|
||||
decision == RetainedUnderlayDecision::Fallback
|
||||
? retainedSegmentCaches[index]->getLastInvalidationReason().c_str() : nullptr);
|
||||
}
|
||||
}
|
||||
commonClearPass();
|
||||
context.bindGlobalUniformBuffers(*parameters.renderPass);
|
||||
if (useRetainedRanges) {
|
||||
for (const auto& segment : segmentPlan.segments) {
|
||||
if (segment.strategy == RenderSegmentStrategy::Retained) {
|
||||
retainedSegmentCaches[segment.segmentIndex]->composite(orchestrator, parameters);
|
||||
} else {
|
||||
drawableOpaquePass(segment.beginIndex, segment.endExclusive);
|
||||
drawableTranslucentPass(segment.beginIndex, segment.endExclusive);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const bool globalLiveFallback = !retainedMode || renderTreeParameters.has3D ||
|
||||
renderTreeParameters.debugOptions != MapDebugOptions::NoDebug ||
|
||||
updateParameters->mode != MapMode::Continuous || !updateParameters->spriteLoaded ||
|
||||
!orchestrator.retainedGlobalResourcesReady() || !retainedPlacementStable ||
|
||||
orchestrator.retainedGlobalLightTransition();
|
||||
if (globalLiveFallback) {
|
||||
drawableOpaquePass(0, segmentPlan.layerCount);
|
||||
drawableTranslucentPass(0, segmentPlan.layerCount);
|
||||
} else {
|
||||
visitRenderSegmentExecution(segmentPlan,
|
||||
[&](uint32_t index) { return wantsResources(segmentDecisions[index]); },
|
||||
[&](uint32_t index) { retainedSegmentCaches[index]->composite(orchestrator, parameters); },
|
||||
[&](int32_t begin, int32_t endExclusive) {
|
||||
drawableOpaquePass(begin, endExclusive);
|
||||
drawableTranslucentPass(begin, endExclusive);
|
||||
});
|
||||
}
|
||||
drawableDebugOverlays();
|
||||
|
||||
|
||||
@@ -42,6 +42,11 @@ RetainedUnderlayReason retainedReason(const char* reason) noexcept {
|
||||
if (value == "style-mutation") return RetainedUnderlayReason::StyleMutation;
|
||||
if (value == "layer-mutation") return RetainedUnderlayReason::LayerMutation;
|
||||
if (value == "source-mutation") return RetainedUnderlayReason::SourceMutation;
|
||||
if (value == "source-or-global-mutation") return RetainedUnderlayReason::SourceOrGlobalMutation;
|
||||
if (value == "source-not-loaded") return RetainedUnderlayReason::SourceNotLoaded;
|
||||
if (value == "source-fading") return RetainedUnderlayReason::SourceFading;
|
||||
if (value == "missing-source") return RetainedUnderlayReason::MissingSource;
|
||||
if (value == "unknown-dependency") return RetainedUnderlayReason::UnknownDependency;
|
||||
if (value == "tile-mutation") return RetainedUnderlayReason::TileMutation;
|
||||
if (value == "image-mutation") return RetainedUnderlayReason::ImageMutation;
|
||||
if (value == "glyph-mutation") return RetainedUnderlayReason::GlyphMutation;
|
||||
@@ -177,6 +182,7 @@ RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<Ret
|
||||
return RetainedUnderlayDecision::Disabled;
|
||||
}
|
||||
if (runtimeState && !runtimeState->isEnabled()) {
|
||||
lastInvalidationReason = "runtime-disabled";
|
||||
++fallbackCount;
|
||||
runtimeState->recordFallback();
|
||||
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
#include <mbgl/renderer/render_segment.hpp>
|
||||
#include <mbgl/renderer/render_segment_plan.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
@@ -43,6 +46,11 @@ enum class RetainedUnderlayReason : uint8_t {
|
||||
StyleMutation,
|
||||
LayerMutation,
|
||||
SourceMutation,
|
||||
SourceOrGlobalMutation,
|
||||
SourceNotLoaded,
|
||||
SourceFading,
|
||||
MissingSource,
|
||||
UnknownDependency,
|
||||
TileMutation,
|
||||
ImageMutation,
|
||||
GlyphMutation,
|
||||
@@ -73,6 +81,11 @@ inline const char* retainedUnderlayReasonText(RetainedUnderlayReason reason) noe
|
||||
case RetainedUnderlayReason::StyleMutation: return "style-mutation";
|
||||
case RetainedUnderlayReason::LayerMutation: return "layer-mutation";
|
||||
case RetainedUnderlayReason::SourceMutation: return "source-mutation";
|
||||
case RetainedUnderlayReason::SourceOrGlobalMutation: return "source-or-global-mutation";
|
||||
case RetainedUnderlayReason::SourceNotLoaded: return "source-not-loaded";
|
||||
case RetainedUnderlayReason::SourceFading: return "source-fading";
|
||||
case RetainedUnderlayReason::MissingSource: return "missing-source";
|
||||
case RetainedUnderlayReason::UnknownDependency: return "unknown-dependency";
|
||||
case RetainedUnderlayReason::TileMutation: return "tile-mutation";
|
||||
case RetainedUnderlayReason::ImageMutation: return "image-mutation";
|
||||
case RetainedUnderlayReason::GlyphMutation: return "glyph-mutation";
|
||||
@@ -224,6 +237,11 @@ public:
|
||||
entry.layers.assign(next.styleLayers.begin() + segment.beginIndex,
|
||||
next.styleLayers.begin() + segment.endExclusive);
|
||||
}
|
||||
const auto dependencies = renderSegmentDependencies(next.styleLayers,
|
||||
segment.beginIndex, segment.endExclusive);
|
||||
entry.dependentSourceIds = dependencies.sourceIds;
|
||||
entry.unknownDependencies = dependencies.globalPrerequisites;
|
||||
if (dependencies.unknown) entry.unknownDependencies.push_back("unknown/source-or-custom");
|
||||
if (segment.strategy == RenderSegmentStrategy::Retained) {
|
||||
++next.retainedSegmentCount;
|
||||
entry.cache.emplace();
|
||||
@@ -240,6 +258,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;
|
||||
@@ -263,6 +312,19 @@ public:
|
||||
entry.cache->cacheState = retainedRangeCacheStateText(state);
|
||||
entry.cache->contentRevision = contentRevision;
|
||||
}
|
||||
void recordSegmentExecution(uint32_t index, bool sourceReady, bool sourceStable,
|
||||
bool supported, uint64_t sourceRevision,
|
||||
const char* decision, const char* fallbackReason) {
|
||||
updateSegmentCache(index, [&](auto& value) {
|
||||
value.sourceReady = sourceReady;
|
||||
value.sourceStable = sourceStable;
|
||||
value.supported = supported;
|
||||
value.sourceRevision = sourceRevision;
|
||||
value.finalDecision = decision;
|
||||
if (fallbackReason) value.lastFallbackReason = fallbackReason;
|
||||
if (std::string_view(decision) == "fallback") ++value.fallbackCount;
|
||||
});
|
||||
}
|
||||
void recordSegmentBuild(uint32_t index) { updateSegmentCache(index, [](auto& value) { ++value.build; }); }
|
||||
void recordSegmentReuse(uint32_t index) { updateSegmentCache(index, [](auto& value) { ++value.reuse; }); }
|
||||
void recordSegmentInvalidation(uint32_t index, RetainedUnderlayReason reason) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,13 +4,29 @@
|
||||
#include <mbgl/renderer/render_orchestrator.hpp>
|
||||
#include <mbgl/renderer/renderer_observer.hpp>
|
||||
#include <mbgl/renderer/retained_underlay_cache.hpp>
|
||||
#include <mbgl/renderer/render_source.hpp>
|
||||
#include <mbgl/renderer/render_source_observer.hpp>
|
||||
#include <mbgl/renderer/tile_render_data.hpp>
|
||||
#include <mbgl/style/sources/geojson_source_impl.hpp>
|
||||
#include <mbgl/tile/tile.hpp>
|
||||
#include <mbgl/tile/tile_observer.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace mbgl;
|
||||
|
||||
namespace mbgl {
|
||||
struct RenderOrchestratorTeardownTestAccess {
|
||||
static void add(RenderOrchestrator& orchestrator, std::unique_ptr<RenderSource> source) {
|
||||
source->setObserver(&orchestrator);
|
||||
orchestrator.renderSources.emplace(source->baseImpl->id, std::move(source));
|
||||
}
|
||||
};
|
||||
} // namespace mbgl
|
||||
|
||||
namespace {
|
||||
|
||||
class TestUniformBuffers final : public gfx::UniformBufferArray {
|
||||
@@ -108,3 +124,128 @@ TEST(RenderOrchestratorError, FailedRangeCannotReuseAndCleanRetryCanRecover) {
|
||||
cache.markBuildSucceeded();
|
||||
EXPECT_EQ(cache.evaluate(key, 1, true), RetainedUnderlayDecision::Reuse);
|
||||
}
|
||||
|
||||
namespace {
|
||||
class TeardownTile final : public Tile {
|
||||
public:
|
||||
TeardownTile(TileObserver* observer_)
|
||||
: Tile(Kind::Geometry, OverscaledTileID{0, 0, {0, 0, 0}}, "step101-source", observer_) {}
|
||||
|
||||
~TeardownTile() override { observer->onTileAction(id, sourceID, TileOperation::Cancelled); }
|
||||
std::unique_ptr<TileRenderData> createRenderData() override { return nullptr; }
|
||||
void cancel() override {}
|
||||
bool layerPropertiesUpdated(const Immutable<style::LayerProperties>&) override { return true; }
|
||||
void emitChanged() { observer->onTileChanged(*this); }
|
||||
void emitError() { observer->onTileError(*this, std::make_exception_ptr(std::runtime_error("tile error"))); }
|
||||
void emitAction() { observer->onTileAction(id, sourceID, TileOperation::Cancelled); }
|
||||
};
|
||||
|
||||
class TeardownSource final : public RenderSource {
|
||||
public:
|
||||
TeardownSource(const TaggedScheduler& scheduler_, std::atomic<int>& deferred_, std::atomic<int>& renderJobs_,
|
||||
std::atomic<int>& detached_)
|
||||
: RenderSource(makeMutable<style::GeoJSONSource::Impl>(
|
||||
"step101-source", style::GeoJSONOptions::defaultOptions())),
|
||||
scheduler(scheduler_), deferred(deferred_), renderJobs(renderJobs_), detached(detached_),
|
||||
tile(std::make_unique<TeardownTile>(static_cast<TileObserver*>(this))) {}
|
||||
|
||||
~TeardownSource() override {
|
||||
if (observer != oldObserver) ++detached;
|
||||
tile.reset(); // A pending tile emits Cancelled synchronously during source destruction.
|
||||
scheduler.schedule([&count = deferred] { ++count; });
|
||||
scheduler.runOnRenderThread([&count = renderJobs] { ++count; });
|
||||
}
|
||||
|
||||
void expectDetachFrom(RenderSourceObserver* observer_) { oldObserver = observer_; }
|
||||
|
||||
void emitAll() {
|
||||
tile->emitChanged();
|
||||
tile->emitError();
|
||||
tile->emitAction();
|
||||
}
|
||||
bool isLoaded() const override { return false; }
|
||||
void update(Immutable<style::Source::Impl>, const std::vector<Immutable<style::LayerProperties>>&,
|
||||
bool, bool, const TileParameters&) override {}
|
||||
std::unique_ptr<RenderItem> createRenderItem() override { return nullptr; }
|
||||
void prepare(const SourcePrepareParameters&) override {}
|
||||
void updateFadingTiles() override {}
|
||||
bool hasFadingTiles() const override { return false; }
|
||||
RenderSourceReadinessStats getReadinessDiagnostics() const override {
|
||||
return {.tileBacked = true,
|
||||
.tileCount = std::nullopt,
|
||||
.incompleteTileCount = std::nullopt,
|
||||
.loadedTileCount = std::nullopt,
|
||||
.hasFadingTiles = false};
|
||||
}
|
||||
std::unordered_map<std::string, std::vector<Feature>> queryRenderedFeatures(
|
||||
const ScreenLineString&, const TransformState&,
|
||||
const std::unordered_map<std::string, const RenderLayer*>&,
|
||||
const RenderedQueryOptions&, const mat4&) const override { return {}; }
|
||||
std::vector<Feature> querySourceFeatures(const SourceQueryOptions&) const override { return {}; }
|
||||
void reduceMemoryUse() override {}
|
||||
void dumpDebugLogs() const override {}
|
||||
|
||||
private:
|
||||
TaggedScheduler scheduler;
|
||||
std::atomic<int>& deferred;
|
||||
std::atomic<int>& renderJobs;
|
||||
std::atomic<int>& detached;
|
||||
RenderSourceObserver* oldObserver = nullptr;
|
||||
std::unique_ptr<TeardownTile> tile;
|
||||
};
|
||||
|
||||
class CountingSourceObserver final : public RenderSourceObserver {
|
||||
public:
|
||||
void onTileChanged(RenderSource&, const OverscaledTileID&) override { ++changed; }
|
||||
void onTileError(RenderSource&, const OverscaledTileID&, std::exception_ptr) override { ++errors; }
|
||||
void onTileAction(RenderSource&, TileOperation, const OverscaledTileID&, const std::string&) override { ++actions; }
|
||||
int changed = 0;
|
||||
int errors = 0;
|
||||
int actions = 0;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST(RenderOrchestratorTeardown, NullObserverAndDetachCoverEveryTileCallback) {
|
||||
TaggedScheduler scheduler{Scheduler::GetBackground(), {}};
|
||||
std::atomic<int> deferred{0};
|
||||
std::atomic<int> renderJobs{0};
|
||||
std::atomic<int> detached{0};
|
||||
auto source = std::make_unique<TeardownSource>(scheduler, deferred, renderJobs, detached);
|
||||
source->emitAll(); // Constructor default NullObserver.
|
||||
|
||||
CountingSourceObserver observer;
|
||||
source->setObserver(&observer);
|
||||
source->expectDetachFrom(&observer);
|
||||
source->emitAll();
|
||||
EXPECT_EQ(observer.changed, 1);
|
||||
EXPECT_EQ(observer.errors, 1);
|
||||
EXPECT_EQ(observer.actions, 1);
|
||||
|
||||
source->setObserver(nullptr);
|
||||
source->emitAll();
|
||||
source.reset(); // Cancelled must not reach the detached observer.
|
||||
EXPECT_EQ(detached.load(), 1);
|
||||
EXPECT_EQ(observer.changed, 1);
|
||||
EXPECT_EQ(observer.errors, 1);
|
||||
EXPECT_EQ(observer.actions, 1);
|
||||
scheduler.waitForEmpty();
|
||||
scheduler.runRenderJobs();
|
||||
}
|
||||
|
||||
TEST(RenderOrchestratorTeardown, PendingTileCancellationAndSecondDrainRepeatSafely) {
|
||||
TaggedScheduler scheduler{Scheduler::GetBackground(), {}};
|
||||
std::atomic<int> deferred{0};
|
||||
std::atomic<int> renderJobs{0};
|
||||
std::atomic<int> detached{0};
|
||||
for (int iteration = 0; iteration < 20; ++iteration) {
|
||||
auto orchestrator = std::make_unique<RenderOrchestrator>(false, scheduler, std::nullopt);
|
||||
auto source = std::make_unique<TeardownSource>(scheduler, deferred, renderJobs, detached);
|
||||
source->expectDetachFrom(orchestrator.get());
|
||||
RenderOrchestratorTeardownTestAccess::add(*orchestrator, std::move(source));
|
||||
orchestrator.reset();
|
||||
EXPECT_EQ(detached.load(), iteration + 1); // Source was detached before its tile cancelled.
|
||||
EXPECT_EQ(deferred.load(), iteration + 1); // Work queued by source destruction was drained.
|
||||
scheduler.runRenderJobs(); // Render jobs have a separate backend-owned queue.
|
||||
EXPECT_EQ(renderJobs.load(), iteration + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +387,8 @@ TEST(RetainedUnderlayRevision, EquivalentIntraFrameRemoveAddDoesNotAdvanceTopolo
|
||||
const RetainedUnderlayTopologySnapshot initial{
|
||||
.boundary = 32,
|
||||
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
|
||||
.continuousEndExclusive = std::nullopt,
|
||||
.staticSuffix = {},
|
||||
};
|
||||
const auto settledRevision = tracker.observe(initial);
|
||||
|
||||
@@ -404,6 +406,8 @@ TEST(RetainedUnderlayRevision, PersistentTopologyChangeAdvancesOnce) {
|
||||
const RetainedUnderlayTopologySnapshot initial{
|
||||
.boundary = 32,
|
||||
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
|
||||
.continuousEndExclusive = std::nullopt,
|
||||
.staticSuffix = {},
|
||||
};
|
||||
const auto initialRevision = tracker.observe(initial);
|
||||
|
||||
@@ -419,6 +423,8 @@ TEST(RetainedUnderlayRevision, OrderingAndIndexChangesAdvanceTopology) {
|
||||
RetainedUnderlayTopologySnapshot topology{
|
||||
.boundary = 32,
|
||||
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
|
||||
.continuousEndExclusive = std::nullopt,
|
||||
.staticSuffix = {},
|
||||
};
|
||||
const auto initialRevision = tracker.observe(topology);
|
||||
|
||||
@@ -934,6 +940,223 @@ 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);
|
||||
state.recordSegmentExecution(0, true, true, true, 17, "reuse", nullptr);
|
||||
|
||||
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",
|
||||
.affectedSegmentIndices = {},
|
||||
.dependencyScope = "none"};
|
||||
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);
|
||||
EXPECT_EQ(second.segments[0].cache->sourceRevision, 17u);
|
||||
EXPECT_EQ(second.segments[0].cache->finalDecision, "reuse");
|
||||
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);
|
||||
EXPECT_NE(json.find("\"sourceRevision\":17"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(RenderSegmentLocality, SourceRevisionsAreScopedAndRecreationUsesNewIdentity) {
|
||||
const std::vector<StyleLayerDiagnostics> layers{
|
||||
{0, "background", "background", true, std::nullopt, std::nullopt},
|
||||
{1, "a0", "fill", true, "a", std::nullopt},
|
||||
{2, "b", "circle", true, "b", std::nullopt},
|
||||
{3, "a1", "line", true, "a", std::nullopt}};
|
||||
const auto r0 = renderSegmentDependencies(layers, 0, 2);
|
||||
const auto r1 = renderSegmentDependencies(layers, 2, 3);
|
||||
const auto r2 = renderSegmentDependencies(layers, 3, 4);
|
||||
const auto sourceLess = renderSegmentDependencies(layers, 0, 1);
|
||||
EXPECT_EQ(r0.sourceIds, std::vector<std::string>({"a"}));
|
||||
EXPECT_EQ(r2.sourceIds, r0.sourceIds);
|
||||
EXPECT_TRUE(sourceLess.sourceIds.empty());
|
||||
RenderSegmentRevisionBook book;
|
||||
book.advanceSource("a");
|
||||
book.advanceSource("b");
|
||||
const auto beforeA = book.revisionFor(r0);
|
||||
const auto beforeB = book.revisionFor(r1);
|
||||
const auto beforeA2 = book.revisionFor(r2);
|
||||
const auto beforeSourceLess = book.revisionFor(sourceLess);
|
||||
book.advanceSource("a");
|
||||
EXPECT_NE(book.revisionFor(r0), beforeA);
|
||||
EXPECT_NE(book.revisionFor(r2), beforeA2);
|
||||
EXPECT_EQ(book.revisionFor(r1), beforeB);
|
||||
EXPECT_EQ(book.revisionFor(sourceLess), beforeSourceLess);
|
||||
const auto beforeRecreate = book.revisionFor(r0);
|
||||
book.advanceSource("a"); // removal
|
||||
book.advanceSource("a"); // recreation with the same ID
|
||||
EXPECT_NE(book.revisionFor(r0), beforeRecreate);
|
||||
book.advanceGlobal(); // image, glyph, style, context or unknown mutation
|
||||
EXPECT_NE(book.revisionFor(r1), beforeB);
|
||||
EXPECT_NE(book.revisionFor(sourceLess), beforeSourceLess);
|
||||
const auto unknown = renderSegmentDependencies(
|
||||
{{0, "custom", "custom", true, std::nullopt, std::nullopt}}, 0, 1);
|
||||
EXPECT_TRUE(unknown.unknown);
|
||||
EXPECT_FALSE(book.revisionFor(unknown));
|
||||
const auto symbol = renderSegmentDependencies(
|
||||
{{0, "label", "symbol", true, "a", std::nullopt}}, 0, 1);
|
||||
EXPECT_FALSE(symbol.unknown);
|
||||
EXPECT_EQ(symbol.sourceIds, std::vector<std::string>({"a"}));
|
||||
EXPECT_EQ(symbol.globalPrerequisites,
|
||||
std::vector<std::string>({"symbol/glyph/image/placement"}));
|
||||
}
|
||||
|
||||
TEST(RenderSegmentLocality, ReadinessAndFadeFollowOnlyDependencies) {
|
||||
const RenderSegmentDependencies r0{{"static"}, {}, false};
|
||||
const RenderSegmentDependencies r1{{"dynamic"}, {}, false};
|
||||
const auto lookup = [](bool dynamicLoaded, bool dynamicFading) {
|
||||
return [=](const std::string& id) -> std::optional<RenderSegmentSourceStatus> {
|
||||
if (id == "static") return RenderSegmentSourceStatus{true, false};
|
||||
if (id == "dynamic") return RenderSegmentSourceStatus{dynamicLoaded, dynamicFading};
|
||||
return std::nullopt;
|
||||
};
|
||||
};
|
||||
EXPECT_TRUE(assessRenderSegmentSources(r0, lookup(false, false)).stable);
|
||||
EXPECT_FALSE(assessRenderSegmentSources(r1, lookup(false, false)).ready);
|
||||
EXPECT_TRUE(assessRenderSegmentSources(r0, lookup(true, true)).stable);
|
||||
const auto fading = assessRenderSegmentSources(r1, lookup(true, true));
|
||||
EXPECT_TRUE(fading.ready);
|
||||
EXPECT_FALSE(fading.stable);
|
||||
EXPECT_STREQ(fading.reason, "source-fading");
|
||||
EXPECT_FALSE(assessRenderSegmentSources({{"missing"}, {}, false}, lookup(true, false)).ready);
|
||||
EXPECT_FALSE(assessRenderSegmentSources({{}, {}, true}, lookup(true, false)).ready);
|
||||
}
|
||||
|
||||
TEST(RenderSegmentLocality, MixedExecutionDrawsEachLiveRangeOnceInStyleOrder) {
|
||||
ResolvedRenderSegmentPlan plan;
|
||||
plan.valid = true;
|
||||
plan.layerCount = 5;
|
||||
plan.segments = {{0, RenderSegmentStrategy::Retained, std::nullopt, 0, 1},
|
||||
{1, RenderSegmentStrategy::Continuous, std::nullopt, 1, 3},
|
||||
{2, RenderSegmentStrategy::Retained, std::nullopt, 3, 5}};
|
||||
std::vector<std::string> events;
|
||||
visitRenderSegmentExecution(plan,
|
||||
[](uint32_t index) { return index == 0; },
|
||||
[&](uint32_t index) { events.push_back("composite:" + std::to_string(index)); },
|
||||
[&](int32_t begin, int32_t end) {
|
||||
for (auto i = begin; i < end; ++i) events.push_back("live:" + std::to_string(i));
|
||||
});
|
||||
EXPECT_EQ(events, (std::vector<std::string>{"composite:0", "live:1", "live:2", "live:3", "live:4"}));
|
||||
events.clear();
|
||||
plan.valid = false;
|
||||
visitRenderSegmentExecution(plan, [](uint32_t) { return true; },
|
||||
[&](uint32_t) { ADD_FAILURE() << "invalid plan composite"; },
|
||||
[&](int32_t begin, int32_t end) {
|
||||
for (auto i = begin; i < end; ++i) events.push_back("live:" + std::to_string(i));
|
||||
});
|
||||
EXPECT_EQ(events.size(), 5u);
|
||||
}
|
||||
|
||||
TEST(RenderSegmentLocality, FailedBuildCannotReuseOldTexture) {
|
||||
RetainedUnderlayCache cache;
|
||||
const auto viewportKey = key();
|
||||
EXPECT_EQ(cache.evaluate(viewportKey, 7, true), RetainedUnderlayDecision::Build);
|
||||
cache.markBuildSucceeded();
|
||||
EXPECT_EQ(cache.evaluate(viewportKey, 7, true), RetainedUnderlayDecision::Reuse);
|
||||
EXPECT_EQ(cache.evaluate(viewportKey, 8, true), RetainedUnderlayDecision::Build);
|
||||
cache.markBuildFailed("resource-or-compositor");
|
||||
EXPECT_FALSE(cache.isValid());
|
||||
EXPECT_EQ(cache.evaluate(viewportKey, 8, true), RetainedUnderlayDecision::Build);
|
||||
}
|
||||
|
||||
TEST(RenderSegmentPlan, StrictJSONRejectsUnknownFieldsStrategyAndInvalidFirstBoundary) {
|
||||
EXPECT_FALSE(parseRenderSegmentPlanJSON(
|
||||
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED","unexpected":true}]})"));
|
||||
|
||||
@@ -99,7 +99,7 @@ unzip -tq "$SOURCE_APK" >/dev/null || fail "APK ZIP integrity check failed."
|
||||
ABI_LIST="$(unzip -Z1 "$SOURCE_APK" | sed -n 's#^lib/\([^/]*\)/.*#\1#p' | sort -u | tr '\n' ' ' | sed 's/ $//')"
|
||||
[[ "$ABI_LIST" == "x86_64" ]] || fail "Unexpected APK ABI set: [$ABI_LIST]"
|
||||
|
||||
LIB_BYTES="$(unzip -l "$SOURCE_APK" 'lib/x86_64/libmaplibre.so' | awk '/lib\/x86_64\/libmaplibre\.so$/ {print $1; exit}')"
|
||||
LIB_BYTES="$(unzip -l "$SOURCE_APK" 'lib/x86_64/libmaplibre.so' | awk '/lib\/x86_64\/libmaplibre\.so$/ {print $1}')"
|
||||
[[ -n "$LIB_BYTES" ]] || fail "lib/x86_64/libmaplibre.so not found in APK."
|
||||
|
||||
APK_BYTES="$(wc -c < "$SOURCE_APK" | tr -d ' ')"
|
||||
|
||||
45
粒子开发记录.md
45
粒子开发记录.md
@@ -1022,3 +1022,48 @@ 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/`。
|
||||
|
||||
### 2026-10-06 STEP-100 SourceScoped RenderSegment 局部失效(CLOSED / PASS)
|
||||
|
||||
- 基于 `c4b481fd2c5b5da20d31f6ae54011f0552d7dbd0`,Codex `gpt-6-sol / high` 实现 source-scoped revision、production/diagnostics 共用 dependency、per-segment loaded/fading/readiness 以及独立 Build/Reuse/Fallback;global loaded/content revision 与 Fast Path gate 保持原兼容语义。
|
||||
- RDC 标准 Android x86_64 OpenGL validation build PASS,APK 61,956,640 bytes,`libmaplibre.so` 39,636,472 bytes,SHA-256 `86e22bf081bf7d3196f0bf0e7c56af51bde01e9f0b01e0bab238cb794273315a`。RDC 同时修复 validation script 在 `pipefail` 下的 `unzip | awk ... exit` SIGPIPE/141,小修不改变产物校验语义。
|
||||
- FZG1-2 T4 R-C-R 约 61.6 秒主窗口:global revision 128→973;R0 build 1→1、invalidation 20→20、reuse 132→4042、source revision 不变;R1 build 5→135、invalidation 26→211。动态 source 持续 `affected=[2]`,核心 locality 硬门槛 PASS。
|
||||
- T2 双 GeoJSON 约 35 秒仍产生 6 个约 203ms loaded false window,但 R0 build/invalidation 不变;T3 unrelated permanently-unready RasterArray 使 global loaded=false 持续 21.468s,但 R0/R1 build=1、invalidation=19 不变,reuse 11→1200,证明无关 source 不再拖垮 retained cache。
|
||||
- T4 运行期间 R1 fallback count 增长而 R0 fallback 不变、reuse 持续增长,实际验证 mixed `R0 Reuse + C Live + R1 Fallback`。未发现 crash/ANR。Fast Path、`singleContinuousSegment`、multi-continuous 语义未改。
|
||||
- Native host GTest 因当前 Intel Mac 缺 `libuv` 环境阻塞;尝试 Homebrew 安装时发现会连带升级 CMake/OpenSSL/Python 且 Intel 无 bottle 支持,已停止,未把环境阻塞虚报 PASS。Android full native build + device runtime 作为本 Step 功能验收事实。Jest 未运行。
|
||||
- Step:`docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md`;完成报告:`docs/报告/STEP-100_SourceScoped_RenderSegment局部失效_Codex完成报告_v1.md`;运行证据在忽略目录 `artifacts/maplibre-particle/android-validation/`。允许进入 V18 unified release。
|
||||
|
||||
### 2026-10-06 STEP-101 RenderOrchestrator 销毁生命周期修复(源码交付,待 RDC 验收)
|
||||
|
||||
- 基于 V18 Final HEAD `d29e84c1a90daeea104b389d28b000c4f017d08d`,按 STEP-101 任务 v2 审计现有差异。已证实 readiness mutex/state 早于 RenderSource 自动析构且 tile/source 销毁可同步回调的生命周期漏洞;与 Android FORTIFY 现场高度吻合,但仍待等价实机闭环确认。
|
||||
- 现有源码差异包含 RenderSource nullptr observer 回退 NullObserver、Orchestrator 析构双 tagged drain、三类 observer detach 与显式 source clear;Android Validation 增加 T1~T4 Activity recreate 场景和 Native reset 配对日志。静态审计发现 teardown 测试 helper 没有注册 Orchestrator observer,本轮仅补正这一行。
|
||||
- render-job 队列独立于 `waitForEmpty()`;OpenGL backend/context 后续清理与真实 Android reset 仍须由 RDC 验收。STEP-100 locality 实现无本次差异,运行态回归待执行。
|
||||
- 本轮仅做源码/任务静态审计与 `git diff --check`;CMake/Ninja、Native GTest、Gradle/APK、ADB/设备、NavSea、Jest、iOS、candidate/release、commit、push、tag、publish 全部 **NOT RUN**。不能写 STEP-101 PASS。
|
||||
- Step:`docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md`;完成报告:`docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md`。V19 未发布;下一步 RDC 按报告命令独立验证。
|
||||
|
||||
### 2026-10-07 STEP-101 源码交付补充
|
||||
|
||||
- 审计 V18 Final HEAD、任务 v2、正常 source erase/clearData、TileCache 延迟释放和独立 render-job 队列;本轮不扩大正常 source 更新语义。
|
||||
- 补齐 C++ 测试对旧 observer 已 detach 的断言;Android 增加 SurfaceView detach、Java/JNI `nativeReset` 与 Native `resetRenderer` 逐轮日志,并补 T3 动态 GeoJSON mutation 标记。
|
||||
- `git diff --check` 通过;Native 编译/GTest、Android Gradle/APK/ADB/设备、STEP-100 locality 运行态、NavSea 和 V19 发布均未执行。结论仍为 PARTIAL,待 RDC 独立验收。
|
||||
|
||||
|
||||
### 2026-10-07 STEP-101 RDC 独立验收 CLOSED / PASS
|
||||
|
||||
- Android x86_64 OpenGL Validation full native build PASS:APK 61,957,678 bytes,libmaplibre 39,637,512 bytes,SHA-256 `14cbd35ed0bc1366389ddea69925901013e3b209ed3575462421a7920c73c721`。
|
||||
- FZG1-2 T1/T3/T4 PASS;T2 完整 20 次真实 Activity recreate / `nativeReset` / `MapRenderer::resetRenderer` PASS,四组 crash buffer 均为空,不再出现 destroyed-mutex SIGABRT。
|
||||
- STEP-100 locality 回归 PASS:global revision 62→894,R0 build 1→1、invalidation 19→19、reuse 12→3890;R1 正常持续 build/invalidate。
|
||||
- Android Native C++ TestRunner 构建 PASS;4 个 RenderSegmentLocality + 2 个 RenderOrchestratorTeardown focused tests 全部 PASS,status=0。TestRunner 临时 fixtures asset 已删除,不纳入 Git。
|
||||
- Mac host GTest 仍受本机 libuv pkg-config 环境阻塞,未安装系统依赖、未虚报 PASS。Jest NOT RUN。
|
||||
- STEP-101 允许关闭;下一步进入 V19 candidate → NavSea 冷启动 Gate → 正式发布。
|
||||
- STEP-101 implementation commit `64cf8f44350eb8ec4b45360db4d3f9505ac4ac0b` 已推送 `origin/particle/main`,远端回读一致;后续只做 docs closeout 与 V19 发行。
|
||||
|
||||
Reference in New Issue
Block a user