Compare commits
4 Commits
6df21dac38
...
630ec65768
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
630ec65768 | ||
|
|
16a5d15f16 | ||
|
|
d3bf98bc55 | ||
|
|
0ce551ad37 |
262
docs/40_任务/STEP-102_RenderSegment无Placement段局部复用_Codex任务_v1.md
Normal file
262
docs/40_任务/STEP-102_RenderSegment无Placement段局部复用_Codex任务_v1.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# STEP-102 RenderSegment 无 Placement 段局部复用 Codex 任务 v1
|
||||
|
||||
## 1. Step 编号和目的
|
||||
|
||||
STEP-102:修复 full CPU 多 Retained Segment 下 placement aggregate gate 的不必要扩散,使**结构上不具备 placement 能力**的 Retained Segment 能在其他 placement-bearing Segment fallback/live 时继续独立 composite/reuse。
|
||||
|
||||
本 Step 的设计依据:
|
||||
`docs/20_调查/RenderSegment多Retained段全局Placement门槛_粒子管理员审查说明_v3.md`。
|
||||
|
||||
## 2. 当前已冻结基线
|
||||
|
||||
- 仓库:`/Users/tei/dev/MapLibreParticleLab-step102`
|
||||
- branch:`step102-placement-free-retained-locality`
|
||||
- start HEAD:`6df21dac38f072c5ad35b8dfc7ec5217509602bc`
|
||||
- upstream base:MapLibre Native core 既有冻结基线,不升级
|
||||
- V19/V20 release worktree:冻结,只读,不修改
|
||||
- STEP-100 source-scoped locality:必须保持
|
||||
- STEP-101 teardown 修复:必须保持
|
||||
- Fast Path:本 Step 不修改
|
||||
- C-001:不升级
|
||||
|
||||
## 3. 本 Step 目标
|
||||
|
||||
必须实现:
|
||||
1. 增加结构性 placement-capable range 查询;
|
||||
2. full CPU 每个 Retained Segment 独立计算 placement gate;
|
||||
3. placement-free Retained 不再被其他 Segment 的 placementChanged/symbolFadeChange 纯 aggregate gate 拖入 fallback;
|
||||
4. placement-bearing Retained 继续使用现有保守 placement gate;
|
||||
5. 从 full CPU `globalLiveFallback` 移除纯 aggregate placement 条件;
|
||||
6. 保持真正全局 fail-closed 条件;
|
||||
7. diagnostics 增加结构 placement、per-segment gate、实际 composite/live 与全局 main-buffer execution 信息;
|
||||
8. RN diagnostics TS 类型做 additive 扩展;
|
||||
9. 增加对应 C++ 测试源码与 Android Validation 场景源码;
|
||||
10. 不改变 Style Z-order、mixed executor、C-001 和 RN method surface。
|
||||
|
||||
## 4. 非目标 / 禁止事项
|
||||
|
||||
禁止:
|
||||
- placement-bearing Segment 之间的局部碰撞缓存;
|
||||
- per-segment collision graph / symbol fade / placement fingerprint;
|
||||
- Fast Path 改造;
|
||||
- multi-continuous Fast Path;
|
||||
- 重写 `visitRenderSegmentExecution()`;
|
||||
- 修改 NavSea;
|
||||
- 新增 NavSea 私有 Native API;
|
||||
- 修改 C-001;
|
||||
- 修改 V19/V20 release worktree/tag;
|
||||
- 升级 MapLibre、NDK、Gradle、AGP、Java、Node 或其他工具链;
|
||||
- 为通过测试降低 fail-closed 门槛;
|
||||
- Android Gradle/build、APK/AAR build、ADB、设备操作;
|
||||
- Jest。用户未要求 Jest,本任务不得新增或执行 Jest。
|
||||
|
||||
## 5. 允许修改范围
|
||||
|
||||
生产代码优先限制在:
|
||||
- `src/mbgl/renderer/render_orchestrator.hpp`
|
||||
- `src/mbgl/renderer/render_orchestrator.cpp`
|
||||
- `src/mbgl/renderer/renderer_impl.cpp`
|
||||
- `include/mbgl/renderer/render_segment.hpp`
|
||||
- `src/mbgl/renderer/retained_underlay_runtime_state.hpp`
|
||||
- `src/mbgl/renderer/render_segment_json.hpp`
|
||||
- `packages/maplibre-particle-rn/src/types.ts`
|
||||
|
||||
测试/Validation 允许:
|
||||
- `test/renderer/retained_underlay_cache.test.cpp`
|
||||
- 与现有 Particle Validation 场景直接相关的最小 Android Test App 文件
|
||||
|
||||
文档允许:
|
||||
- 本任务书
|
||||
- `docs/50_Step记录/STEP-102_RenderSegment无Placement段局部复用_v1.md`
|
||||
- `docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md`
|
||||
- `粒子开发记录.md`
|
||||
- v3 审查说明只读,除非发现明确事实错误;若需改,停止并先报告
|
||||
|
||||
如果实现必须扩大生产代码范围,先停止并报告原因,不得自行扩张架构。
|
||||
|
||||
## 6. 禁止修改范围
|
||||
|
||||
- NavSea 仓库
|
||||
- `navsea-particle-integration` contract/RFC/ADR
|
||||
- Fast Path 逻辑、`tryRenderParticleFastFrame()`、`singleContinuousSegment()` 语义
|
||||
- `retainedRangeUsesPlacement()` 现有动态语义
|
||||
- mixed executor 算法
|
||||
- Source `isLoaded()` 定义
|
||||
- STEP-100 source revision/locality 语义
|
||||
- STEP-101 teardown 生命周期语义
|
||||
- renderer/backend 架构
|
||||
- release/version/tag/publish 坐标
|
||||
|
||||
## 7. 实施要求
|
||||
|
||||
### 7.1 结构性 placement-capable 查询
|
||||
|
||||
新增 full CPU retained 专用查询,必须基于 layer 结构,而不是当前帧 placementData。
|
||||
|
||||
最小保守语义:
|
||||
- range 中存在 `symbol` layer → placement-capable
|
||||
- 无 `symbol` layer → placement-free
|
||||
- custom/custom-drawable 继续由 unsupported gate 处理
|
||||
|
||||
不得用 `needsPlacement()==false` 证明 placement-free。
|
||||
|
||||
### 7.2 per-segment placement gate
|
||||
|
||||
每个 Retained Segment 至少形成两个明确事实:
|
||||
- structural placement capability
|
||||
- current full-CPU placement gate result
|
||||
|
||||
placement-free Segment 不受纯 global placement gate 阻塞。
|
||||
placement-bearing Segment 本 Step 继续沿用旧保守 gate,不承诺稳定后 reuse。
|
||||
|
||||
变量/字段命名不得把 `symbolFadeChange==0` 错称为严格 placement stable。
|
||||
|
||||
### 7.3 full CPU mixed execution
|
||||
|
||||
`segmentSupported` 改为使用对应 Segment placement gate。
|
||||
|
||||
`globalLiveFallback` 仅移除纯 aggregate placement 条件,其他真正 global 条件继续保留。
|
||||
|
||||
不得重写 `visitRenderSegmentExecution()`。
|
||||
|
||||
### 7.4 fallback reason
|
||||
|
||||
`static-placement` 必须按 Segment 自身 placement capability/gate 判定,不能从 aggregate reason 无差别传播。
|
||||
|
||||
### 7.5 diagnostics
|
||||
|
||||
每个 Segment 最少新增等价语义:
|
||||
- `placementCapable`
|
||||
- `placementGatePassed`
|
||||
- `actualExecution = composite | live`
|
||||
|
||||
全局最少新增等价语义:
|
||||
- `placementChanged`
|
||||
- `symbolFadeChange`
|
||||
- `placementTransitionActive`
|
||||
- `crossSourceCollisions`
|
||||
- `mainBufferExecutionMode = mixed | global-live`
|
||||
- `globalLiveFallbackReason`
|
||||
|
||||
`actualExecution` 必须反映本帧 main-buffer 最终实际执行,不能根据 cache finalDecision 推导。
|
||||
Diagnostics 查询必须无副作用、不得推进 revision。
|
||||
|
||||
### 7.6 RN 类型
|
||||
|
||||
只扩展现有 diagnostics 返回类型,不新增 method,不改变 `setRenderSegmentPlan()` 输入 contract。
|
||||
Android/iOS Shared Core JSON 语义保持一致。
|
||||
|
||||
## 8. 构建要求
|
||||
|
||||
Codex 本阶段**不得执行任何 Android 编译/Gradle/APK/AAR build**,也不得启动长时间 CMake/Ninja/Bazel 构建。
|
||||
|
||||
Codex 完成代码后只需提供精确验证命令,由后续 RDC/用户按规则执行。其中 Android 编译由用户手工执行。
|
||||
|
||||
Codex 可执行的机械静态检查仅限:
|
||||
- `git diff --check`
|
||||
- 只读 grep/rg/git diff/status
|
||||
- 不产生构建产物的格式/静态检查(若仓库已有且无需安装依赖)
|
||||
|
||||
不得安装或升级系统依赖来制造测试 PASS。
|
||||
|
||||
## 9. 测试要求
|
||||
|
||||
Codex 必须新增/补强测试源码,但本阶段不执行大型测试。
|
||||
|
||||
至少覆盖:
|
||||
1. placement-free R0 + placement-bearing R1:R0 可 composite,R1 live;
|
||||
2. 含 symbol 但当前 placementData 为空:仍判定 placementCapable=true;
|
||||
3. 两个 placement-free Retained:持续 reuse/composite;
|
||||
4. source-local fading/mutation 回归不误伤 R0;
|
||||
5. placement-bearing 在 transition disabled / `symbolFadeChange=1` 等状态仍不被本 Step 放宽;
|
||||
6. 真正 global gate 继续 global-live/fail-closed;
|
||||
7. topology 变化释放旧 resource;
|
||||
8. cache decision=reuse 但 global-live 时,diagnostics 的 actualExecution 必须为 live;
|
||||
9. diagnostics JSON/TS 字段语义一致且查询无副作用。
|
||||
|
||||
Android Validation 场景应能在后续手工构建后明确输出 STEP-102 验收所需字段,不依赖 NavSea。
|
||||
|
||||
## 10. 验收标准
|
||||
|
||||
代码审计门槛:
|
||||
- [ ] 新增结构性 placement-capable 查询
|
||||
- [ ] 未改变 `retainedRangeUsesPlacement()` 旧语义
|
||||
- [ ] full CPU per-segment placement gate 生效
|
||||
- [ ] placement-bearing 继续保守
|
||||
- [ ] `globalLiveFallback` 只移除纯 aggregate placement 条件
|
||||
- [ ] 真正 global gate 保留
|
||||
- [ ] mixed executor 未重写
|
||||
- [ ] Fast Path 无语义修改
|
||||
- [ ] per-segment fallback reason 正确
|
||||
- [ ] diagnostics 有 actualExecution 与 main-buffer mode/fallback reason
|
||||
- [ ] RN method surface 不变
|
||||
- [ ] C-001 不变
|
||||
- [ ] 测试源码覆盖设计矩阵
|
||||
- [ ] `git diff --check` PASS
|
||||
|
||||
运行态 PASS 不在 Codex 阶段宣称。Android 手工编译与真机验证未完成前,STEP-102 状态只能写“代码完成 / 等待验证”或同等非 PASS 状态。
|
||||
|
||||
## 11. Git 要求
|
||||
|
||||
- 工作分支:`step102-placement-free-retained-locality`
|
||||
- 禁止修改 particle/main 工作区
|
||||
- 禁止 `git add -A`、`git add .`、`git reset --hard`、`git clean`
|
||||
- 只 scoped staging 本任务文件
|
||||
- 不提交无关 dirty/untracked 文件
|
||||
- Codex 可在实现完成、静态检查通过后创建 scoped implementation commit,但**不得 merge 到 particle/main、不得 push release 分支/tag、不得发布制品**
|
||||
- 是否 push/merge 由后续独立验收决定
|
||||
|
||||
## 12. 必做输出
|
||||
|
||||
Codex 完成后必须交付:
|
||||
1. 生产代码修改;
|
||||
2. C++ 测试源码;
|
||||
3. Android Validation STEP-102 场景源码/日志字段;
|
||||
4. `docs/50_Step记录/STEP-102_RenderSegment无Placement段局部复用_v1.md`;
|
||||
5. `docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md`;
|
||||
6. 追加 `粒子开发记录.md`;
|
||||
7. 精确列出后续验证命令;
|
||||
8. 明确列出所有 NOT RUN / BLOCKED 项;
|
||||
9. Git diff/status/commit(若创建)事实。
|
||||
|
||||
## 13. 执行报告完整文件名
|
||||
|
||||
固定:
|
||||
`docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md`
|
||||
|
||||
同主题后续报告只递增 `_v2/_v3`,不得另造近义文件名。
|
||||
|
||||
## 14. 执行报告固定内容
|
||||
|
||||
至少包含:
|
||||
1. 执行结论:只能按实际写代码完成/等待验证、PARTIAL、BLOCKED 或 FAIL;未运行 Android 不得写运行态 PASS
|
||||
2. Git 基线:branch/start HEAD/end HEAD/status
|
||||
3. 实际修改文件
|
||||
4. 新增文件
|
||||
5. 实现内容逐条对照任务书
|
||||
6. 与 v3 设计差异
|
||||
7. Fast Path / mixed executor / C-001 未修改审计
|
||||
8. diagnostics 新字段与语义
|
||||
9. 测试源码覆盖矩阵
|
||||
10. 实际执行的静态检查
|
||||
11. 未执行的 build/test/Android/设备项目
|
||||
12. 用户手工 Android 构建命令
|
||||
13. 后续 RDC/真机验收命令与硬门槛
|
||||
14. 已知风险与未完成事项
|
||||
15. Git commit 信息(若有)
|
||||
|
||||
## 15. 阻塞与停止条件
|
||||
|
||||
遇到以下任一情况必须停止扩大修改并报告:
|
||||
- 结构性 placement capability 不能仅靠既有 layer type 安全判定,需要新增更大公共抽象;
|
||||
- 必须修改 Fast Path 才能完成本任务;
|
||||
- 必须重写 mixed executor;
|
||||
- 必须改变 collision/Placement 核心算法;
|
||||
- 必须修改 C-001 / RN method surface;
|
||||
- 必须修改 NavSea;
|
||||
- 必须扩大到 renderer/backend 架构重构;
|
||||
- 发现 global revision 仍有未预期的 placement 推进,需要新设计;
|
||||
- 需要升级工具链/依赖;
|
||||
- 需要删除或弱化既有测试/安全门槛。
|
||||
|
||||
若实现范围保持在 v3 冻结边界内,可直接完成代码并停在“等待手工构建/运行验证”。
|
||||
60
docs/50_Step记录/STEP-102_RenderSegment无Placement段局部复用_v1.md
Normal file
60
docs/50_Step记录/STEP-102_RenderSegment无Placement段局部复用_v1.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# STEP-102 RenderSegment 无 Placement 段局部复用记录 v1
|
||||
|
||||
状态:**进行中(代码完成,等待独立构建与运行验证)**
|
||||
日期:2026-10-08
|
||||
|
||||
## 目标与基线
|
||||
|
||||
- 工作区:`/Users/tei/dev/MapLibreParticleLab-step102`
|
||||
- 分支:`step102-placement-free-retained-locality`
|
||||
- 起点 HEAD:`6df21dac38f072c5ad35b8dfc7ec5217509602bc`
|
||||
- 工作合同:`docs/40_任务/STEP-102_RenderSegment无Placement段局部复用_Codex任务_v1.md`
|
||||
- 设计依据:`docs/20_调查/RenderSegment多Retained段全局Placement门槛_粒子管理员审查说明_v3.md`
|
||||
- 发布坐标:V19/V20 工作区与发行内容冻结;本 Step 不创建版本。
|
||||
|
||||
目标是让结构上没有 symbol layer 的 Retained Segment 在其他段 placement 活跃时继续独立缓存并经 main-buffer composite。
|
||||
|
||||
## 必做项
|
||||
|
||||
- [x] 基于 style layer type 查询结构性 placement capability;不使用当前帧 `needsPlacement()` 证明 placement-free。
|
||||
- [x] full CPU 每段计算 placement gate;含 symbol 的段仍受 changed、fade 与 transition 保守门槛约束。
|
||||
- [x] `segmentSupported` 和 fallback reason 按段生成;full CPU global-live 只移除纯 aggregate placement 条件。
|
||||
- [x] 保留 3D、debug、MapMode、sprite/image、light 等真实全局门槛和既有 revision/key 失效。
|
||||
- [x] diagnostics 增加结构、逐段 gate、实际 main-buffer 执行及全局 placement/execution 字段;RN TS 只增加诊断字段。
|
||||
- [x] 新增 C++ 源码测试与 Android STEP-102 Validation fixture/日志场景。
|
||||
- [x] 静态核对 Fast Path、mixed executor、C-001、RN method surface 未修改。
|
||||
- [ ] C++ 编译与定向测试、Android 用户手工构建、设备场景、iOS/Metal 验证:尚未执行。
|
||||
|
||||
## 允许与禁止范围
|
||||
|
||||
代码修改仅在任务书允许的 renderer、RN diagnostics 类型、`retained_underlay_cache.test.cpp` 和 Android Particle Validation 文件。未修改 `visitRenderSegmentExecution()`、`tryRenderParticleFastFrame()`、`retainedRangeUsesPlacement()`、Placement 算法、NavSea、C-001、发布文件或总则。既有未跟踪 v2/v3 审查说明及任务书保持原样。
|
||||
|
||||
## 构建、测试与当前结果
|
||||
|
||||
只读源码/diff 审计及 `git diff --check` 已执行,后者通过;新增未跟踪文件另用 `git diff --no-index --check` 检查,无空白错误(差异本身使退出码为 1)。CMake/Ninja/Bazel、GTest、Android Gradle、APK/AAR、ADB、设备、Jest、NavSea、发布全部 **NOT RUN**。静态检查不构成运行态 PASS。
|
||||
|
||||
## 必做输出
|
||||
|
||||
- STEP-102 生产代码、C++ 测试源码、Android Validation 场景源码。
|
||||
- `docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md`。
|
||||
- 本记录与 `粒子开发记录.md` 追加。
|
||||
- 报告中列出精确后续验证命令、硬门槛、NOT RUN 与剩余风险。
|
||||
|
||||
## 阻塞与下一步
|
||||
|
||||
目前没有已观察到的编译或运行失败;真正结果仍未知。由 RDC/用户按报告完成 Native focused tests、用户手工 Android APK 构建和 T1/T2 设备场景,核对 R0/R1 的 `placementCapable`、`placementGatePassed`、`actualExecution` 与 build/reuse/invalidation。构建或设备失败时保留首个真实错误与日志,再按任务边界修复;本 Step 未进入后续发行阶段。
|
||||
|
||||
## RDC 独立验证补记(2026-10-08)
|
||||
|
||||
- 从现有工作树、任务书、v3 审查依据和项目总则恢复审查;原 Codex 阶段的 NOT RUN 记录保留,以下为新增 RDC 事实。
|
||||
- `git diff --check`:退出码 0,PASS;无关的 `platform/windows/vendor/vcpkg` 子模块状态变化不纳入本任务。
|
||||
- 以 AppleClang 对 `test/renderer/retained_underlay_cache.test.cpp` 执行 `-std=c++20 -fsyntax-only`:退出码 0,语法检查 PASS;不是 GTest 运行结果。
|
||||
- 本地临时独立 C++ helper smoke 测试:结构性 symbol 判定、placement gate、全局 live reason 共 10 个断言,10/10 PASS;测试文件位于 `/tmp/step102-placement-smoke.cpp`,不是正式 GTest。
|
||||
- Mac CMake `cmake --preset macos-metal`:配置退出码 1,阻塞于缺少 `libuv`;未进入 GTest 编译或运行,未安装/升级依赖。日志 `/tmp/step102-cmake-config.log`。
|
||||
- 额外审查并修复 Android STEP-102 Validation 切换入口:进入该场景时停止 STEP-093 诊断轮询、清除 STEP-093/097 标志,避免旧场景轮询污染 STEP-102 证据。
|
||||
- Metal Bazel Core 构建 `bazel build -c opt --//:renderer=metal //:mbgl-core` 已完成:退出码 0,642/642 actions,耗时 875.118s,产物 `bazel-bin/libmbgl-core.a`,构建 PASS;日志 `/tmp/step102-bazel-core.log`。该 PASS 仅表示 Native Core 编译,不代表 GTest/Android/运行态 PASS。
|
||||
- Android Gradle/APK/ADB 与设备验证依任务规则均未执行;STEP-102 继续保持“进行中 / 待 Android 实际验收”,不得标记 CLOSED / PASS。
|
||||
|
||||
## V21 发布决策(2026-10-08)
|
||||
|
||||
用户明确接受未执行正式 GTest、Android T1/T2、iOS/Metal 运行态验证的风险,要求 STEP-102 经源码审查即纳入 V21 发行。代码审查未发现明确阻断项;先前 Metal Core 642/642 编译通过。本步骤依然不属于运行态验收 PASS。发布须完整执行 `prepare → build → publish → finalize`;发布记录应明确验收豁免。
|
||||
129
docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md
Normal file
129
docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# STEP-102 RenderSegment 无 Placement 段局部复用 Codex 完成报告 v1
|
||||
|
||||
## 1. 执行结论
|
||||
|
||||
**代码完成,等待独立构建与运行验证;STEP-102 尚非 PASS。** 已按任务书实现 full CPU placement-free Retained Segment 局部复用、逐段 fallback reason 和实际 main-buffer 执行诊断。C++ 与 Android 测试均只交付源码,未运行。没有编译、设备或视觉结果,不能据此宣称无 stale、Z-order 错乱或 crash。
|
||||
|
||||
## 2. Git 基线与工作树
|
||||
|
||||
- 工作区:`/Users/tei/dev/MapLibreParticleLab-step102`
|
||||
- 当前机器:macOS 15.7.7 / Darwin x86_64;未连接或操作设备。
|
||||
- 分支:`step102-placement-free-retained-locality`
|
||||
- start HEAD:`6df21dac38f072c5ad35b8dfc7ec5217509602bc`
|
||||
- end HEAD:仍为 `6df21dac38f072c5ad35b8dfc7ec5217509602bc`;本阶段未 commit、push、merge。
|
||||
- 子模块:`git submodule status` 均以 `-` 开头,本 worktree 未初始化;未更改。
|
||||
- 工作树:仅下列任务文件有本阶段差异;任务开始时已经未跟踪的 v2/v3 审查说明及 STEP-102 任务书未修改、未暂存。
|
||||
- V19/V20 release worktree、`particle/main`、tag 与发布坐标未修改;无新 artifact。
|
||||
|
||||
## 3. 实际修改与新增文件
|
||||
|
||||
生产代码:
|
||||
|
||||
1. `include/mbgl/renderer/render_segment.hpp`:结构查询、placement gate、全局 live reason 辅助函数及 diagnostics 字段。
|
||||
2. `src/mbgl/renderer/render_orchestrator.hpp`、`src/mbgl/renderer/render_orchestrator.cpp`:对 full CPU 提供结构查询和 placement transition 查询;旧动态查询保持原样。
|
||||
3. `src/mbgl/renderer/renderer_impl.cpp`:逐段 gate/fallback、全局 live reason、实际执行记录。
|
||||
4. `src/mbgl/renderer/retained_underlay_runtime_state.hpp`、`src/mbgl/renderer/render_segment_json.hpp`:只读快照与 JSON 字段。
|
||||
5. `packages/maplibre-particle-rn/src/types.ts`:现有 diagnostics 返回类型 additive 扩展。
|
||||
|
||||
测试/Validation:
|
||||
|
||||
6. `test/renderer/retained_underlay_cache.test.cpp`:新增 `RenderSegmentPlacementFree.*` C++ 测试源码。
|
||||
7. `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt`:STEP-102 场景菜单、Intent 入口、轮询及审计日志。
|
||||
8. 新增 `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step102PlacementFixture.kt`:离线 R-C-R symbol/placement-free 两组 fixture。
|
||||
|
||||
文档:新增 `docs/50_Step记录/STEP-102_RenderSegment无Placement段局部复用_v1.md`、本报告;追加 `粒子开发记录.md`。
|
||||
|
||||
## 4. 实现与任务书逐项对照
|
||||
|
||||
- **结构能力**:扫描同一 style-layer 元数据中的 `layerType == "symbol"`;越界 range 返回 placement-capable 以 fail-closed。`retainedRangeUsesPlacement()` 的 `needsPlacement()` 动态语义未变。custom/custom-drawable 仍由原 unsupported gate 处理。
|
||||
- **逐段 gate**:placement-free 返回通过;placement-bearing 要求 `!placementChanged && symbolFadeChange == 0 && !placementTransitionActive`。`symbolFadeChange==0` 只是沿用保守 gate 的条件,不命名为严格 stable。transition disabled 且 fade=1 仍不能让含 symbol 段复用。
|
||||
- **逐段 fallback**:优先明确 source、global resource、placement、layer transition、legacy/unsupported 等本段理由。`static-placement` 只会由本段 gate 失败产生;不再拷贝 aggregate placement/transition 理由给邻段。
|
||||
- **full CPU main-buffer**:全局 live 条件只删掉 aggregate `!retainedPlacementStable`,其他既有 3D、debug、非 Continuous MapMode、sprite、global resources、light transition 条件保留。原 `visitRenderSegmentExecution()` 和 Style 顺序不变。camera/viewport/pixelRatio、topology、content/global revision、resource/compositor failure 的原有失效路径未放宽。
|
||||
- **兼容/安全边界**:aggregate `retainedStaticUsesPlacement`、`retainedPlacementStable`、prepared-tree/Fast Path eligibility 与 revision 观察继续原用;含 symbol 的 retained 仍 fail-closed;C-001、RN method surface、`setRenderSegmentPlan()` 输入未变。
|
||||
|
||||
与 v3 审查设计无架构差异。逐段 gate 额外显式查询当前 placement transition,属于对 placement-bearing 的更保守判定;原 `retainedSegmentIsStable()` 内仍保留既有 transition 检查。测试和 Android 场景均不修改 Placement 核心算法。
|
||||
|
||||
## 5. diagnostics 字段与真实执行语义
|
||||
|
||||
每段新增 `placementCapable`、`placementGatePassed`、`actualExecution`。前者来自 style-layer 结构,第二项表示本帧 full CPU placement cache gate;Continuous 段的 gate 为非阻塞值。`actualExecution` 在 main-buffer 全局 live 分支或 mixed executor 的 composite 回调实际发生后记录。即使 `cache.finalDecision=reuse`,全局门槛触发时仍记录 `actualExecution=live`。
|
||||
|
||||
全局新增 `placementChanged`、`symbolFadeChange`、`placementTransitionActive`、`crossSourceCollisions`、`mainBufferExecutionMode`、`globalLiveFallbackReason`。`mixed` 表示进入 Style 顺序的 mixed executor,可以包含某些 Retained live;`global-live` 表示整帧 live 分支。尚未渲染时 `mainBufferExecutionMode/actualExecution=not-rendered`。查询只拷贝已记录快照,不推进 revision;Android/iOS Shared Core 使用同一 JSON writer,RN TS 只补类型。
|
||||
|
||||
## 6. 测试源码覆盖矩阵
|
||||
|
||||
| 合同场景 | 新增/现有测试源码 | 尚待运行证明 |
|
||||
| --- | --- | --- |
|
||||
| A:R0 free、C live、R1 symbol live;Style 顺序 | `RenderSegmentPlacementFree.MixedExecutionKeepsFreeRangesInStyleOrder` | Android T1 真实 GPU composite/live 与视觉 Z-order |
|
||||
| B:symbol 当前 placementData 为空仍 capable | `StructuralCapabilityAndConservativeGate` 直接输入 symbol style 结构,不依赖 placementData | Android 空 bucket/placementData 条件仍需设备审计 |
|
||||
| C:两个 free Retained 复用 | `MixedExecutionKeepsFreeRangesInStyleOrder` 中 R0/R2;Android T2 | 60 秒 build/reuse/invalidation 窗口 |
|
||||
| D:source-local unloaded/fading/mutation | 既有 `RenderSegmentLocality.ReadinessAndFadeFollowOnlyDependencies` 与本轮两 free cache 断言;Android T1 动态 symbol source | 设备 R0 计数不误增 |
|
||||
| E:bearing 保守 gate | `StructuralCapabilityAndConservativeGate` changed/fade=1/transition 活跃组合 | transition disabled 场景的 GPU 行为 |
|
||||
| F:真实全局门槛 | `RealGlobalGatesStillForceGlobalLive`;既有 revision/cache tests | style/context/image/glyph/light 等设备失效 |
|
||||
| G:拓扑与 key | `TopologyAndCameraChangesCannotReuseOldRetainedKey`;既有 `OldCacheReleaseCannotChargeReplacementAtSameIndex` | 真正 GPU resource 释放及无 stale texture |
|
||||
| H:reuse 但 global-live | `DiagnosticsRecordActualMainBufferAndDoNotMutateOnRead` | 设备日志中 cache/actual 分离 |
|
||||
|
||||
测试源码只验证可隔离的策略、cache 决策、diagnostics 和 mixed traversal;无法单凭这些单元测试替代真实 renderer/GPU 场景。
|
||||
|
||||
## 7. Android Validation 场景
|
||||
|
||||
Test App 的 STEP-093 菜单新增 `STEP-102 PLACEMENT LOCALITY`,也可用 Intent `step102_scenario` 启动:
|
||||
|
||||
- `T1_FREE_SYMBOL`:R0 background/fill(无 symbol)→ C0 持续粒子 → R1 symbol;每 500ms 更新 R1 GeoJSON point,让 placement/source 活跃。
|
||||
- `T2_TWO_FREE`:相同 R0/C0,R1 改为 fill;两个 Retained 均结构上 placement-free。
|
||||
|
||||
每个 `STEP102_SAMPLE` 记录固定 camera、placement 四个全局字段、main-buffer mode/reason、每段 capability/gate/actual、cache decision、build/reuse/invalidation/fallback。tick 8/130 打窗口标记。日志字段用于验收,不在 App 中自行宣告 PASS。
|
||||
|
||||
## 8. 实际检查与 NOT RUN
|
||||
|
||||
- 已执行:`git status --short`、`git diff`/`rg` 只读审计;`git diff --check` **PASS**。三个新增未跟踪文件另以 `git diff --no-index --check /dev/null <文件>` 检查,无空白错误;该命令因有差异正常退出 1。
|
||||
- **NOT RUN**:CMake/Ninja/Bazel configure/build/link、C++ GTest、Android Native TestRunner、Android Gradle、APK/AAR、codegen、Jest、ADB、设备/截图/logcat、iOS/Metal、NavSea、发布、commit、push。
|
||||
- 无 APK/AAR、大小、SHA-256、运行日志或最终性能结果。没有编译失败或测试失败的事实;缺少编译/运行证据。
|
||||
|
||||
## 9. 后续精确验证命令与硬门槛
|
||||
|
||||
先由 RDC 在本工作区运行 host 定向测试;若当前 Mac `libuv` 环境仍阻塞,应记录首个真实 configure 错误,而非改动依赖:
|
||||
|
||||
```bash
|
||||
cmake --preset macos-metal
|
||||
cmake --build build-macos-metal --target mbgl-test-runner --parallel 3
|
||||
build-macos-metal/mbgl-test-runner --gtest_filter='RenderSegmentPlacementFree.*:RenderSegmentLocality.*:RenderSegmentDiagnostics.*'
|
||||
```
|
||||
|
||||
**Android Validation APK 只由用户手工构建**,使用项目固定入口:
|
||||
|
||||
```bash
|
||||
cd /Users/tei/dev/MapLibreParticleLab-step102
|
||||
./tools/particle_android_validation/build_validation_apk.sh
|
||||
```
|
||||
|
||||
构建成功后由 RDC/用户核对 APK ZIP、ABI、`libmaplibre.so`、SHA-256、branch/commit/dirty 状态,再在选定设备上独立运行。下列 `<SERIAL>` 需替换为实际 `adb devices` 返回的 serial;每场景单独清日志、force-stop、启动并留存完整 logcat:
|
||||
|
||||
```bash
|
||||
adb -s <SERIAL> install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk
|
||||
adb -s <SERIAL> logcat -c
|
||||
adb -s <SERIAL> shell am force-stop org.maplibre.android.testapp
|
||||
adb -s <SERIAL> shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity --es step102_scenario T1_FREE_SYMBOL
|
||||
adb -s <SERIAL> logcat -v threadtime -d > /tmp/step102-t1-logcat.txt
|
||||
rg 'STEP102_(SCENARIO_START|STYLE_READY|SAMPLE|WINDOW_MARKER)|Fatal signal|ANR' /tmp/step102-t1-logcat.txt
|
||||
```
|
||||
|
||||
对 `T2_TWO_FREE` 重复 `logcat -c`、force-stop、启动、保存到 `/tmp/step102-t2-logcat.txt`。每组至少等待 tick 130(约 65 秒)后取最终日志。T1 硬门槛:固定 camera、`mainBufferExecutionMode=mixed` 的稳定窗口中 R0 `placementCapable=false`、`actualExecution=composite`、buildDelta=0、invalidationDelta=0、reuseDelta>0;R1 `placementCapable=true`,placement gate 失败期间 `actualExecution=live`,C0 live,Style Z-order 正确,无 stale/crash/ANR。T2 两个 Retained 均无 symbol,build 一次后持续 reuse/composite。若窗口内出现真实 global mutation,应结合 `globalLiveFallbackReason` 与 lastMutation 重新选择稳定窗口,不把合法 global invalidation 算作误伤。
|
||||
|
||||
还需独立做 STEP-100 source locality 与 STEP-101 teardown 回归,以及 global light/style/context/camera、topology 变动资源释放验证。iOS/Metal 只可在相同源码提交的 Mac/iOS 验收后声明通过。本阶段均未执行。
|
||||
|
||||
## 10. 风险、未完成事项与交接
|
||||
|
||||
- Android symbol glyph asset 与 placement 活跃窗口尚未由构建/设备核实;T1 若未出现 placement gate 失败,不构成主场景验收。
|
||||
- 现有 C++ topology 测试验证 key/revision 无旧 cache reuse,不能证明真实 GPU 资源已释放;需设备/后端证据。
|
||||
- `placementChanged` 与 `symbolFadeChange` 在 Fast Path 帧来自既有 prepared render tree;本 Step 没有改变 Fast Path 判定或语义。主验收只使用 `fullCpuFrame` 增长的 full CPU 窗口。
|
||||
- 构建或运行失败应保留现场、回传首个真实错误及必要上下文;不得放宽测试或安全门槛。没有触及任务书要求 BLOCKED 的架构扩张条件。
|
||||
|
||||
本阶段未创建 Git commit,故无 implementation commit SHA;也未执行 merge、push 或远端 HEAD 回读。后续独立验收决定 scoped commit/merge 与发行流程。
|
||||
|
||||
## 11. V21 发行风险豁免记录(2026-10-08)
|
||||
|
||||
- 用户明确要求:V21 必须包含 STEP-102;本次发行不以正式 GTest、Android T1/T2 实机回归、iOS/Metal 运行态验收为前置条件,改由代码审查后执行完整发布流水线。
|
||||
- 源码审查基线:`0ce551ad3791a21dbd8507e6cf8c607c270b6eee`。已检查结构性 symbol/Placement 分类、逐段 gate、真实全局 live 门槛、main-buffer 顺序、诊断数据和 Android Validation 场景;`git diff --check` 通过,未发现明确阻断发布的源码问题。
|
||||
- 已有构建证据仅限 STEP-102 Metal Core 编译成功(Bazel 642/642),以及临时 helper smoke 10/10。正式 C++ GTest 受 `libuv` 依赖缺失阻塞,Android 与 iOS 运行态验收没有执行。
|
||||
- **验收豁免不等于测试 PASS**:STEP-102 不标记 `CLOSED / PASS`,而按“源码审查后接受未验收风险发行 V21”记录。不能声称已经证明运行态无陈旧纹理、Z-order 错乱或崩溃。
|
||||
- V21 Pipeline 的 prepare、build、publish、finalize 仍必须全部执行并以真实返回值、artifact 校验和公开回读为准;构建或发行阶段失败不可豁免。
|
||||
@@ -70,6 +70,47 @@ inline RenderSegmentDependencies renderSegmentDependencies(const std::vector<Sty
|
||||
return result;
|
||||
}
|
||||
|
||||
// Placement capability is a property of the style layer, not of this frame's
|
||||
// buckets or placementData. Invalid ranges fail closed.
|
||||
inline bool renderSegmentHasPlacementCapableLayer(const std::vector<StyleLayerDiagnostics>& layers,
|
||||
int32_t begin,
|
||||
int32_t endExclusive) {
|
||||
if (begin < 0 || endExclusive < begin || endExclusive > static_cast<int32_t>(layers.size())) return true;
|
||||
for (int32_t i = begin; i < endExclusive; ++i) {
|
||||
if (layers[static_cast<std::size_t>(i)].layerType == "symbol") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool renderSegmentPlacementGatePassed(bool placementCapable,
|
||||
bool placementChanged,
|
||||
float symbolFadeChange,
|
||||
bool placementTransitionActive) noexcept {
|
||||
return !placementCapable ||
|
||||
(!placementChanged && symbolFadeChange == 0.0f && !placementTransitionActive);
|
||||
}
|
||||
|
||||
struct RenderSegmentGlobalLiveGates {
|
||||
bool retainedMode = false;
|
||||
bool has3D = false;
|
||||
bool debugOptions = false;
|
||||
bool continuousMode = false;
|
||||
bool spriteLoaded = false;
|
||||
bool globalResourcesReady = false;
|
||||
bool globalLightTransition = false;
|
||||
};
|
||||
|
||||
inline const char* renderSegmentGlobalLiveFallbackReason(const RenderSegmentGlobalLiveGates& gates) noexcept {
|
||||
if (!gates.retainedMode) return "no-boundary";
|
||||
if (gates.has3D) return "3d";
|
||||
if (gates.debugOptions) return "debug-options";
|
||||
if (!gates.continuousMode) return "map-mode";
|
||||
if (!gates.spriteLoaded) return "sprite-not-loaded";
|
||||
if (!gates.globalResourcesReady) return "global-resource-not-ready";
|
||||
if (gates.globalLightTransition) return "global-light-transition";
|
||||
return "none";
|
||||
}
|
||||
|
||||
class RenderSegmentRevisionBook {
|
||||
public:
|
||||
void advanceGlobal() noexcept { ++globalEpoch; }
|
||||
@@ -190,6 +231,9 @@ struct RenderSegmentDiagnosticsEntry {
|
||||
std::vector<StyleLayerDiagnostics> layers;
|
||||
std::vector<std::string> dependentSourceIds;
|
||||
std::vector<std::string> unknownDependencies;
|
||||
bool placementCapable = false;
|
||||
bool placementGatePassed = false;
|
||||
std::string actualExecution{"not-rendered"};
|
||||
std::optional<RenderSegmentCacheDiagnostics> cache;
|
||||
};
|
||||
|
||||
@@ -214,6 +258,12 @@ struct RenderSegmentDiagnostics {
|
||||
uint64_t fastPathHit = 0;
|
||||
uint64_t fastPathMiss = 0;
|
||||
uint64_t fullCpuFrame = 0;
|
||||
bool placementChanged = false;
|
||||
float symbolFadeChange = 0.0f;
|
||||
bool placementTransitionActive = false;
|
||||
bool crossSourceCollisions = true;
|
||||
std::string mainBufferExecutionMode{"not-rendered"};
|
||||
std::string globalLiveFallbackReason{"none"};
|
||||
bool styleLoaded = false;
|
||||
bool imageManagerLoaded = false;
|
||||
bool renderOrchestratorLoaded = false;
|
||||
|
||||
@@ -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.20")\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.21")\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,9 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 13.4.1-particle.21(Android / iOS 统一发行)
|
||||
|
||||
- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;共享 Particle revision 21。
|
||||
## 13.4.1-particle.20(Android / iOS 统一发行)
|
||||
|
||||
- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;共享 Particle revision 20。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# @navsea/maplibre-particle-rn
|
||||
|
||||
当前版本:`13.4.1-particle.20`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.20`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。
|
||||
当前版本:`13.4.1-particle.21`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.21`。本包是 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.20.tgz
|
||||
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.21.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.20
|
||||
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.21
|
||||
```
|
||||
|
||||
Expo plugin 与 `android/build.gradle` 必须指向同一 `.20`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。
|
||||
Expo plugin 与 `android/build.gradle` 必须指向同一 `.21`。正常 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.20"
|
||||
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.21"
|
||||
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.20")\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.21")\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.20",
|
||||
"version": "13.4.1-particle.21",
|
||||
"description": "NavSea MapLibre Particle React Native pure Field Renderer API.",
|
||||
"license": "BSD-2-Clause",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -91,6 +91,9 @@ export type RetainedRenderSegmentDiagnostics = Readonly<{
|
||||
layers: readonly RenderSegmentLayerDiagnostics[];
|
||||
dependentSourceIds: readonly string[];
|
||||
unknownDependencies: readonly string[];
|
||||
placementCapable: boolean;
|
||||
placementGatePassed: boolean;
|
||||
actualExecution: "composite" | "live" | "not-rendered";
|
||||
cache: Readonly<{
|
||||
cacheState: string;
|
||||
contentRevision: number;
|
||||
@@ -122,6 +125,9 @@ export type ContinuousRenderSegmentDiagnostics = Readonly<{
|
||||
layers: readonly RenderSegmentLayerDiagnostics[];
|
||||
dependentSourceIds: readonly string[];
|
||||
unknownDependencies: readonly string[];
|
||||
placementCapable: boolean;
|
||||
placementGatePassed: boolean;
|
||||
actualExecution: "composite" | "live" | "not-rendered";
|
||||
cache: null;
|
||||
}>;
|
||||
export type RenderSourceReadinessDiagnostics = Readonly<{
|
||||
@@ -170,6 +176,12 @@ export type RenderSegmentDiagnostics = Readonly<{
|
||||
fastPathHit: number;
|
||||
fastPathMiss: number;
|
||||
fullCpuFrame: number;
|
||||
placementChanged: boolean;
|
||||
symbolFadeChange: number;
|
||||
placementTransitionActive: boolean;
|
||||
crossSourceCollisions: boolean;
|
||||
mainBufferExecutionMode: "mixed" | "global-live" | "not-rendered";
|
||||
globalLiveFallbackReason: string;
|
||||
styleLoaded: boolean;
|
||||
imageManagerLoaded: boolean;
|
||||
renderOrchestratorLoaded: boolean;
|
||||
|
||||
@@ -63,6 +63,56 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private var step097Topology: Step097StencilFixture.Topology? = null
|
||||
private var step099Scenario: Step099ReadinessFixture.Scenario? = null
|
||||
private var step101Scenario: String? = null
|
||||
private var step102Scenario: Step102PlacementFixture.Scenario? = null
|
||||
private var step102Tick = 0L
|
||||
private val step102Handler = Handler(Looper.getMainLooper())
|
||||
private val step102Poll = object : Runnable {
|
||||
override fun run() {
|
||||
val scenario = step102Scenario ?: return
|
||||
if (this@ParticleCoreLayerValidationActivity::maplibreMap.isInitialized && styleReady) {
|
||||
++step102Tick
|
||||
if (scenario == Step102PlacementFixture.Scenario.T1_FREE_SYMBOL) {
|
||||
val source = maplibreMap.style?.getSourceAs<GeoJsonSource>(Step102PlacementFixture.SYMBOL_SOURCE)
|
||||
if (source != null) {
|
||||
val offset = (step102Tick % 20L) * 0.08
|
||||
source.setGeoJson(Feature.fromGeometry(Point.fromLngLat(135.0 + offset, 28.0)))
|
||||
}
|
||||
}
|
||||
val snapshot = JSONObject(maplibreMap.getRenderSegmentDiagnostics())
|
||||
val segments = snapshot.optJSONArray("segments")
|
||||
val ranges = buildList {
|
||||
for (index in 0 until (segments?.length() ?: 0)) {
|
||||
val segment = segments?.optJSONObject(index) ?: continue
|
||||
val cache = segment.optJSONObject("cache")
|
||||
add("$index:${segment.optString("strategy")}" +
|
||||
" capable=${segment.optBoolean("placementCapable")}" +
|
||||
" gate=${segment.optBoolean("placementGatePassed")}" +
|
||||
" actual=${segment.optString("actualExecution")}" +
|
||||
" decision=${cache?.optString("finalDecision") ?: "-"}" +
|
||||
" build=${cache?.optLong("build") ?: 0}" +
|
||||
" reuse=${cache?.optLong("reuse") ?: 0}" +
|
||||
" invalidation=${cache?.optLong("invalidation") ?: 0}" +
|
||||
" fallback=${cache?.optString("lastFallbackReason") ?: "-"}")
|
||||
}
|
||||
}
|
||||
val message = "scenario=$scenario tick=$step102Tick camera=fixed " +
|
||||
"placementChanged=${snapshot.optBoolean("placementChanged")} " +
|
||||
"symbolFadeChange=${snapshot.optDouble("symbolFadeChange")} " +
|
||||
"placementTransitionActive=${snapshot.optBoolean("placementTransitionActive")} " +
|
||||
"crossSourceCollisions=${snapshot.optBoolean("crossSourceCollisions")} " +
|
||||
"mainBufferExecutionMode=${snapshot.optString("mainBufferExecutionMode")} " +
|
||||
"globalLiveFallbackReason=${snapshot.optString("globalLiveFallbackReason")} " +
|
||||
"fullCpuFrame=${snapshot.optLong("fullCpuFrame")} ranges=${ranges.joinToString(";")}"
|
||||
diagnostics.text = "STEP-102 $message"
|
||||
Log.i(TAG, "STEP102_SAMPLE $message")
|
||||
if (step102Tick == 8L || step102Tick == 130L) {
|
||||
Log.i(TAG, "STEP102_WINDOW_MARKER phase=${if (step102Tick == 8L) "baseline" else "final"} " +
|
||||
"scenario=$scenario tick=$step102Tick")
|
||||
}
|
||||
}
|
||||
if (step102Scenario != null) step102Handler.postDelayed(this, 500L)
|
||||
}
|
||||
}
|
||||
private var step101Completed = 0
|
||||
private var step101DynamicTick = 0L
|
||||
private var step101ReadySinceMs = 0L
|
||||
@@ -268,6 +318,9 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
intent.getStringExtra("step101_scenario")?.takeIf { it in STEP101_SCENARIOS }
|
||||
}
|
||||
step101Completed = savedInstanceState?.getInt(STATE_STEP101_COMPLETED, 0) ?: 0
|
||||
step102Scenario = (savedInstanceState?.getString(STATE_STEP102_SCENARIO)
|
||||
?: intent.getStringExtra("step102_scenario"))
|
||||
?.let { value -> Step102PlacementFixture.Scenario.values().firstOrNull { it.name == value } }
|
||||
step093Topology = savedInstanceState?.getString(STATE_STEP093_TOPOLOGY)
|
||||
?.let { value -> Step093Topology.values().firstOrNull { it.name == value } }
|
||||
?: Step093Topology.BOTTOM_CONTINUOUS_TOP
|
||||
@@ -306,7 +359,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
updateNavSeaPbfButtonState()
|
||||
updatePbfModeButtonState()
|
||||
updateRetainedCacheButtonState()
|
||||
if (step101Scenario != null) loadStep101Style()
|
||||
if (step102Scenario != null) loadStep102Style()
|
||||
else if (step101Scenario != null) loadStep101Style()
|
||||
else if (step097Topology != null) loadStep097StencilStyle()
|
||||
else if (step093Mode) loadStep093Style(configureBeforeStyle = false)
|
||||
else loadValidationStyle(active = active, enabled = enabled, reason = "INITIAL")
|
||||
@@ -613,6 +667,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
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")
|
||||
menu.add(0, 19, 19, "STEP-102 PLACEMENT LOCALITY")
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId])
|
||||
@@ -625,6 +680,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
16 -> showStep097StencilMenu(anchor)
|
||||
17 -> showStep099Menu(anchor)
|
||||
18 -> showStep101Menu(anchor)
|
||||
19 -> showStep102Menu(anchor)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -685,8 +741,57 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showStep102Menu(anchor: android.view.View) {
|
||||
PopupMenu(this, anchor).apply {
|
||||
Step102PlacementFixture.Scenario.values().forEachIndexed { index, scenario ->
|
||||
menu.add(0, index, index, scenario.name)
|
||||
}
|
||||
setOnMenuItemClickListener { item ->
|
||||
val scenario = Step102PlacementFixture.Scenario.values().getOrNull(item.itemId)
|
||||
?: return@setOnMenuItemClickListener false
|
||||
step102Scenario = scenario
|
||||
loadStep102Style()
|
||||
true
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadStep102Style() {
|
||||
val scenario = step102Scenario ?: return
|
||||
stopStep093DiagnosticsPolling()
|
||||
step093Mode = false
|
||||
step097Topology = null
|
||||
step101Scenario = null
|
||||
step101Handler.removeCallbacksAndMessages(null)
|
||||
stopStep099Scenario()
|
||||
step102Handler.removeCallbacksAndMessages(null)
|
||||
step102Tick = 0L
|
||||
retainedCacheEnabled = true
|
||||
maplibreMap.setRetainedUnderlayEnabled(true)
|
||||
updateRetainedCacheButtonState()
|
||||
val accepted = maplibreMap.setRenderSegmentPlan(Step102PlacementFixture.planJson())
|
||||
styleReady = false
|
||||
val epoch = ++styleLoadEpoch
|
||||
Log.i(TAG, "STEP102_SCENARIO_START scenario=$scenario planAccepted=$accepted")
|
||||
maplibreMap.setStyle(Style.Builder().fromJson(Step102PlacementFixture.styleJson(scenario))) {
|
||||
if (step102Scenario != 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, "STEP102_STYLE_READY scenario=$scenario")
|
||||
step102Handler.post(step102Poll)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopStep102Scenario() {
|
||||
step102Scenario = null
|
||||
step102Handler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
private fun loadStep101Style() {
|
||||
val scenario = step101Scenario ?: return
|
||||
stopStep102Scenario()
|
||||
stopStep099Scenario()
|
||||
step101Handler.removeCallbacksAndMessages(null)
|
||||
step101ReadySinceMs = 0L
|
||||
@@ -711,6 +816,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun applyStep099Scenario(scenario: Step099ReadinessFixture.Scenario) {
|
||||
stopStep102Scenario()
|
||||
stopStep093DiagnosticsPolling()
|
||||
step093Mode = false
|
||||
step097Topology = null
|
||||
@@ -834,6 +940,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun applyStep097StencilScene(topology: Step097StencilFixture.Topology) {
|
||||
stopStep102Scenario()
|
||||
stopStep099Scenario()
|
||||
leaveRealWeatherMode()
|
||||
step097Topology = topology
|
||||
@@ -973,6 +1080,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
variant: Step093Variant = Step093Variant.DEFAULT,
|
||||
configureBeforeStyle: Boolean = false
|
||||
) {
|
||||
stopStep102Scenario()
|
||||
stopStep099Scenario()
|
||||
leaveRealWeatherMode()
|
||||
step093Mode = true
|
||||
@@ -1493,6 +1601,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") {
|
||||
stopStep102Scenario()
|
||||
if (step099Scenario != null) stopStep099Scenario()
|
||||
step097V4GoldenReady = false
|
||||
if (pureParticleMode) {
|
||||
@@ -2279,6 +2388,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
super.onStop()
|
||||
}
|
||||
override fun onDestroy() {
|
||||
step102Handler.removeCallbacksAndMessages(null)
|
||||
step101Handler.removeCallbacksAndMessages(null)
|
||||
if (step101Scenario != null) Log.i(TAG, "STEP101_MAPVIEW_DESTROY_ENTER scenario=$step101Scenario completed=$step101Completed")
|
||||
step093Handler.removeCallbacks(step093DiagnosticsPoll)
|
||||
@@ -2317,6 +2427,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
outState.putString(STATE_STEP097_TOPOLOGY, step097Topology?.name)
|
||||
outState.putString(STATE_STEP101_SCENARIO, step101Scenario)
|
||||
outState.putInt(STATE_STEP101_COMPLETED, step101Completed)
|
||||
outState.putString(STATE_STEP102_SCENARIO, step102Scenario?.name)
|
||||
outState.putString(STATE_STEP093_TOPOLOGY, step093Topology.name)
|
||||
outState.putString(STATE_STEP093_VARIANT, step093Variant.name)
|
||||
super.onSaveInstanceState(outState)
|
||||
@@ -2356,6 +2467,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
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 const val STATE_STEP102_SCENARIO = "particle_validation.step102_scenario"
|
||||
private val STEP101_SCENARIOS = listOf(
|
||||
"T1_RESET_ONCE", "T2_RESET_LOOP_20", "T3_RESET_WITH_DYNAMIC_GEOJSON", "T4_RESET_WITH_DIAGNOSTICS"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.maplibre.android.testapp.activity.particle
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
/** Offline STEP-102 R-C-R fixture; only the upper retained range changes placement capability. */
|
||||
internal object Step102PlacementFixture {
|
||||
enum class Scenario { T1_FREE_SYMBOL, T2_TWO_FREE }
|
||||
|
||||
const val STATIC_SOURCE = "step102-static"
|
||||
const val SYMBOL_SOURCE = "step102-symbol"
|
||||
private const val PARTICLE_SOURCE = "step102-field"
|
||||
private const val PARTICLE_LAYER = "step102-particle"
|
||||
private const val UPPER_LAYER = "step102-upper"
|
||||
|
||||
fun styleJson(scenario: Scenario): String {
|
||||
val sources = JSONObject()
|
||||
.put(STATIC_SOURCE, geoJsonSource(
|
||||
"""{"type":"Polygon","coordinates":[[[112,0],[158,0],[158,56],[112,56],[112,0]]]}"""))
|
||||
.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"))
|
||||
if (scenario == Scenario.T1_FREE_SYMBOL) {
|
||||
sources.put(SYMBOL_SOURCE, geoJsonSource("""{"type":"Point","coordinates":[135.0,28.0]}"""))
|
||||
}
|
||||
val layers = JSONArray()
|
||||
.put(JSONObject().put("id", "step102-background").put("type", "background")
|
||||
.put("paint", JSONObject().put("background-color", "#0b2942")))
|
||||
.put(JSONObject().put("id", "step102-static-fill").put("type", "fill")
|
||||
.put("source", STATIC_SOURCE)
|
||||
.put("paint", JSONObject().put("fill-color", "#176b87").put("fill-opacity", 0.58)))
|
||||
.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.T1_FREE_SYMBOL) {
|
||||
layers.put(JSONObject().put("id", UPPER_LAYER).put("type", "symbol")
|
||||
.put("source", SYMBOL_SOURCE)
|
||||
.put("layout", JSONObject().put("text-field", "STEP-102")
|
||||
.put("text-font", JSONArray().put("DIN Offc Pro Regular").put("Arial Unicode MS Regular"))
|
||||
.put("text-size", 20))
|
||||
.put("paint", JSONObject().put("text-color", "#ffcc00")))
|
||||
} else {
|
||||
layers.put(JSONObject().put("id", UPPER_LAYER).put("type", "fill")
|
||||
.put("source", STATIC_SOURCE)
|
||||
.put("paint", JSONObject().put("fill-color", "#d3a640").put("fill-opacity", 0.38)))
|
||||
}
|
||||
return JSONObject().put("version", 8).put("name", "STEP-102 Placement Locality")
|
||||
.put("glyphs", "asset://fonts/{fontstack}/{range}.pbf")
|
||||
.put("sources", sources).put("layers", layers).toString()
|
||||
}
|
||||
|
||||
fun planJson(): String = JSONObject().put("schemaVersion", 1)
|
||||
.put("segments", JSONArray()
|
||||
.put(JSONObject().put("strategy", "RETAINED"))
|
||||
.put(JSONObject().put("strategy", "CONTINUOUS").put("startLayerId", PARTICLE_LAYER))
|
||||
.put(JSONObject().put("strategy", "RETAINED").put("startLayerId", UPPER_LAYER)))
|
||||
.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.20
|
||||
13.4.1-particle.21
|
||||
|
||||
@@ -9,8 +9,8 @@ let package = Package(
|
||||
targets: [
|
||||
.binaryTarget(
|
||||
name: "MapLibre",
|
||||
url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.20/MapLibre.dynamic.xcframework.zip",
|
||||
checksum: "f31468a13356e7a3be10ab5308af65974f4ce2a87573ab7e699c5a1086c78ccd"
|
||||
url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.21/MapLibre.dynamic.xcframework.zip",
|
||||
checksum: "fd78ed8f504c3d0ee969954f44501719b94e699aa5b160de849e712b32d24f72"
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# MapLibre Particle iOS 发行
|
||||
|
||||
当前正式发行:6.28.0-particle.20
|
||||
当前正式发行:6.28.0-particle.21
|
||||
|
||||
- Renderer:Metal
|
||||
- Product / Module:MapLibre
|
||||
- Binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.20/MapLibre.dynamic.xcframework.zip
|
||||
- SHA-256:f31468a13356e7a3be10ab5308af65974f4ce2a87573ab7e699c5a1086c78ccd
|
||||
- Binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.21/MapLibre.dynamic.xcframework.zip
|
||||
- SHA-256:fd78ed8f504c3d0ee969954f44501719b94e699aa5b160de849e712b32d24f72
|
||||
- SwiftPM:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git
|
||||
- Exact Version:6.28.0-particle.20
|
||||
- Source Commit:a4c6047d7092523ad8aa6c14cb2b2246f4a88b8d
|
||||
- Exact Version:6.28.0-particle.21
|
||||
- Source Commit:16a5d15f164dc93d220e3fac62de66a9c9527ccc
|
||||
|
||||
构建命令固定为:
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"status": "PUBLISHED_PUBLIC_READBACK_PASS",
|
||||
"distributionVersion": "6.28.0-particle.20",
|
||||
"distributionVersion": "6.28.0-particle.21",
|
||||
"moduleName": "MapLibre",
|
||||
"compilationMode": "opt",
|
||||
"artifact": {
|
||||
"fileName": "MapLibre.dynamic.xcframework.zip",
|
||||
"sizeBytes": 4119687,
|
||||
"sha256": "f31468a13356e7a3be10ab5308af65974f4ce2a87573ab7e699c5a1086c78ccd",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.20/MapLibre.dynamic.xcframework.zip"
|
||||
"sizeBytes": 4121573,
|
||||
"sha256": "fd78ed8f504c3d0ee969954f44501719b94e699aa5b160de849e712b32d24f72",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.21/MapLibre.dynamic.xcframework.zip"
|
||||
},
|
||||
"sourceCommit": "a4c6047d7092523ad8aa6c14cb2b2246f4a88b8d",
|
||||
"sourceCommit": "16a5d15f164dc93d220e3fac62de66a9c9527ccc",
|
||||
"swiftPackage": {
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git",
|
||||
"requirement": "exactVersion",
|
||||
"version": "6.28.0-particle.20",
|
||||
"version": "6.28.0-particle.21",
|
||||
"productName": "MapLibre"
|
||||
},
|
||||
"notes": "Metal renderer optimized release built with bazel build -c opt --//:renderer=metal //platform/ios:MapLibre.dynamic"
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"releaseState": "PUBLISHED_PUBLIC_READBACK_PASS",
|
||||
"particleRevision": 20,
|
||||
"sourceCommit": "a4c6047d7092523ad8aa6c14cb2b2246f4a88b8d",
|
||||
"generatedAtUtc": "2026-10-07T12:35:15.522160+00:00",
|
||||
"particleRevision": 21,
|
||||
"sourceCommit": "16a5d15f164dc93d220e3fac62de66a9c9527ccc",
|
||||
"generatedAtUtc": "2026-10-08T04:32:42.009872+00:00",
|
||||
"reactNative": {
|
||||
"name": "@navsea/maplibre-particle-rn",
|
||||
"version": "13.4.1-particle.20",
|
||||
"version": "13.4.1-particle.21",
|
||||
"artifact": {
|
||||
"fileName": "navsea-maplibre-particle-rn-13.4.1-particle.20.tgz",
|
||||
"sizeBytes": 28827,
|
||||
"sha256": "fdc19f62cc2ab3c5bd521faa202d05ca69b17057c1afb5d8ce5e15722cc13c58",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.20.tgz"
|
||||
"fileName": "navsea-maplibre-particle-rn-13.4.1-particle.21.tgz",
|
||||
"sizeBytes": 28965,
|
||||
"sha256": "8862dda5ad03727967465edea42053c71e59485e45afe7b99ed153d5c0a27eb5",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.21.tgz"
|
||||
},
|
||||
"mapLibreReactNative": "11.3.6"
|
||||
},
|
||||
"android": {
|
||||
"version": "13.4.1-particle.20",
|
||||
"coordinate": "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.20",
|
||||
"version": "13.4.1-particle.21",
|
||||
"coordinate": "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.21",
|
||||
"renderer": "opengl",
|
||||
"artifact": {
|
||||
"fileName": "android-sdk-particle-opengl-13.4.1-particle.20.aar",
|
||||
"sizeBytes": 170731109,
|
||||
"sha256": "213924d5970a83ce953b7a2be908cfb72419127e8d10cb36949681f7dac16844",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.20/android-sdk-particle-opengl-13.4.1-particle.20.aar"
|
||||
"fileName": "android-sdk-particle-opengl-13.4.1-particle.21.aar",
|
||||
"sizeBytes": 170777014,
|
||||
"sha256": "59248c9a80aafa03da202a2666cfefe919090a16ae21d425370fcf8b39d0898c",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.21/android-sdk-particle-opengl-13.4.1-particle.21.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.20",
|
||||
"distributionVersion": "6.28.0-particle.21",
|
||||
"renderer": "metal",
|
||||
"compilationMode": "opt",
|
||||
"moduleName": "MapLibre",
|
||||
"artifact": {
|
||||
"fileName": "MapLibre.dynamic.xcframework.zip",
|
||||
"sizeBytes": 4119687,
|
||||
"sha256": "f31468a13356e7a3be10ab5308af65974f4ce2a87573ab7e699c5a1086c78ccd",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.20/MapLibre.dynamic.xcframework.zip"
|
||||
"sizeBytes": 4121573,
|
||||
"sha256": "fd78ed8f504c3d0ee969954f44501719b94e699aa5b160de849e712b32d24f72",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.21/MapLibre.dynamic.xcframework.zip"
|
||||
},
|
||||
"swiftPackage": {
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git",
|
||||
"requirement": "exactVersion",
|
||||
"version": "6.28.0-particle.20",
|
||||
"version": "6.28.0-particle.21",
|
||||
"productName": "MapLibre"
|
||||
},
|
||||
"xcode": "Xcode 26.3 Build version 17C529 ",
|
||||
|
||||
@@ -1140,6 +1140,14 @@ bool RenderOrchestrator::retainedRangeUsesPlacement(int32_t begin, int32_t endEx
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RenderOrchestrator::retainedRangeHasPlacementCapableLayer(int32_t begin, int32_t endExclusive) const {
|
||||
return renderSegmentHasPlacementCapableLayer(retainedStyleLayers, begin, endExclusive);
|
||||
}
|
||||
|
||||
bool RenderOrchestrator::retainedPlacementTransitionActive(TimePoint timePoint) const {
|
||||
return placementController.hasTransitions(timePoint);
|
||||
}
|
||||
|
||||
bool RenderOrchestrator::retainedUnderlayIsStable(TimePoint timePoint, int32_t boundary) const {
|
||||
return retainedRangeIsStable(timePoint, 0, boundary);
|
||||
}
|
||||
|
||||
@@ -127,6 +127,8 @@ public:
|
||||
bool retainedUnderlayIsStable(TimePoint, int32_t boundary) const;
|
||||
bool retainedUnderlayHasUnsupportedLayer(int32_t boundary) const;
|
||||
bool retainedRangeUsesPlacement(int32_t begin, int32_t endExclusive) const;
|
||||
bool retainedRangeHasPlacementCapableLayer(int32_t begin, int32_t endExclusive) const;
|
||||
bool retainedPlacementTransitionActive(TimePoint timePoint) const;
|
||||
bool retainedRangeIsStable(TimePoint, int32_t begin, int32_t endExclusive) const;
|
||||
struct RetainedSegmentSourceState {
|
||||
uint64_t revision = 0;
|
||||
|
||||
@@ -131,6 +131,12 @@ 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("placementChanged"); writer.Bool(snapshot.placementChanged);
|
||||
writer.Key("symbolFadeChange"); writer.Double(snapshot.symbolFadeChange);
|
||||
writer.Key("placementTransitionActive"); writer.Bool(snapshot.placementTransitionActive);
|
||||
writer.Key("crossSourceCollisions"); writer.Bool(snapshot.crossSourceCollisions);
|
||||
writer.Key("mainBufferExecutionMode"); writer.String(snapshot.mainBufferExecutionMode.c_str());
|
||||
writer.Key("globalLiveFallbackReason"); writer.String(snapshot.globalLiveFallbackReason.c_str());
|
||||
writer.Key("styleLoaded"); writer.Bool(snapshot.styleLoaded);
|
||||
writer.Key("imageManagerLoaded"); writer.Bool(snapshot.imageManagerLoaded);
|
||||
writer.Key("renderOrchestratorLoaded"); writer.Bool(snapshot.renderOrchestratorLoaded);
|
||||
@@ -187,6 +193,9 @@ inline std::string renderSegmentDiagnosticsToJSON(const RenderSegmentDiagnostics
|
||||
writer.EndArray();
|
||||
writeStringArray(writer, "dependentSourceIds", segment.dependentSourceIds);
|
||||
writeStringArray(writer, "unknownDependencies", segment.unknownDependencies);
|
||||
writer.Key("placementCapable"); writer.Bool(segment.placementCapable);
|
||||
writer.Key("placementGatePassed"); writer.Bool(segment.placementGatePassed);
|
||||
writer.Key("actualExecution"); writer.String(segment.actualExecution.c_str());
|
||||
writer.Key("cache");
|
||||
if (!segment.cache) {
|
||||
writer.Null();
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <mbgl/renderer/render_target.hpp>
|
||||
#include <mbgl/renderer/retained_underlay_cache.hpp>
|
||||
#include <mbgl/particle/renderer_debug_counters.hpp>
|
||||
#include <string_view>
|
||||
|
||||
#if MLN_RENDER_BACKEND_METAL
|
||||
#include <mbgl/mtl/renderer_backend.hpp>
|
||||
@@ -579,6 +580,8 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
const bool retainedPlacementStable =
|
||||
!retainedStaticUsesPlacement ||
|
||||
(!renderTreeParameters.placementChanged && renderTreeParameters.symbolFadeChange == 0.0f);
|
||||
const bool placementTransitionActive = orchestrator.retainedPlacementTransitionActive(
|
||||
renderTreeParameters.timePoint);
|
||||
const auto retainedContentRevision = orchestrator.getRetainedUnderlayContentRevision();
|
||||
const auto retainedContentReason = orchestrator.getRetainedUnderlayContentReason();
|
||||
const RetainedUnderlayRevisionState retainedRevisionState{
|
||||
@@ -615,29 +618,12 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
if (retainedRuntimeState) retainedRuntimeState->recordFastPathEligibility(
|
||||
retainedSupported && fastContinuousSupported);
|
||||
if (!particleFastFrame) particlePreparedSceneSupported = retainedSupported && fastContinuousSupported;
|
||||
const char* retainedFallbackReason = nullptr;
|
||||
if (!retainedMode) {
|
||||
retainedFallbackReason = "no-boundary";
|
||||
} else if (!renderTreeParameters.loaded) {
|
||||
retainedFallbackReason = "style-not-loaded";
|
||||
} else if (renderTreeParameters.has3D) {
|
||||
retainedFallbackReason = "3d";
|
||||
} else if (renderTreeParameters.debugOptions != MapDebugOptions::NoDebug) {
|
||||
retainedFallbackReason = "debug-options";
|
||||
} else if (!retainedStaticStable) {
|
||||
retainedFallbackReason = "static-transition";
|
||||
} else if (!retainedPlacementStable) {
|
||||
retainedFallbackReason = "static-placement";
|
||||
} else if (legacyInRetainedRange) {
|
||||
retainedFallbackReason = "legacy-ordering";
|
||||
} else if (retainedUnsupportedStaticLayer) {
|
||||
retainedFallbackReason = "unsupported";
|
||||
}
|
||||
const auto wantsResources = [](RetainedUnderlayDecision decision) {
|
||||
return decision == RetainedUnderlayDecision::Build || decision == RetainedUnderlayDecision::Reuse;
|
||||
};
|
||||
std::vector<RenderOrchestrator::RetainedSegmentSourceState> segmentSourceStates(segmentPlan.segments.size());
|
||||
std::vector<bool> segmentSupportsCache(segmentPlan.segments.size(), false);
|
||||
std::vector<bool> segmentPlacementGates(segmentPlan.segments.size(), true);
|
||||
for (const auto& segment : segmentPlan.segments) {
|
||||
if (segment.strategy != RenderSegmentStrategy::Retained) continue;
|
||||
auto& cache = retainedSegmentCaches[segment.segmentIndex];
|
||||
@@ -656,18 +642,32 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
static_cast<uint64_t>(updateParameters->pixelRatio * 1000000.0f);
|
||||
const bool segmentStable = sourceState.stable && orchestrator.retainedSegmentIsStable(
|
||||
renderTreeParameters.timePoint, segment.beginIndex, segment.endExclusive);
|
||||
const bool placementCapable = orchestrator.retainedRangeHasPlacementCapableLayer(
|
||||
segment.beginIndex, segment.endExclusive);
|
||||
const bool segmentPlacementGatePassed = renderSegmentPlacementGatePassed(
|
||||
placementCapable, renderTreeParameters.placementChanged,
|
||||
renderTreeParameters.symbolFadeChange, placementTransitionActive);
|
||||
segmentPlacementGates[segment.segmentIndex] = segmentPlacementGatePassed;
|
||||
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] &&
|
||||
segmentPlacementGatePassed && !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;
|
||||
: !segmentPlacementGatePassed ? "static-placement"
|
||||
: !segmentStable ? "static-transition"
|
||||
: legacyInSegment[segment.segmentIndex] ? "legacy-ordering"
|
||||
: orchestrator.retainedRangeHasUnsupportedLayer(segment.beginIndex, segment.endExclusive) ? "unsupported"
|
||||
: !retainedMode ? "no-boundary"
|
||||
: renderTreeParameters.has3D ? "3d"
|
||||
: renderTreeParameters.debugOptions != MapDebugOptions::NoDebug ? "debug-options"
|
||||
: updateParameters->mode != MapMode::Continuous ? "map-mode"
|
||||
: nullptr;
|
||||
decision = cache->evaluate(segmentKeys[segment.segmentIndex],
|
||||
segmentContentRevision,
|
||||
segmentSupported,
|
||||
@@ -987,23 +987,37 @@ void Renderer::Impl::render(const RenderTree& renderTree,
|
||||
}
|
||||
commonClearPass();
|
||||
context.bindGlobalUniformBuffers(*parameters.renderPass);
|
||||
const bool globalLiveFallback = !retainedMode || renderTreeParameters.has3D ||
|
||||
renderTreeParameters.debugOptions != MapDebugOptions::NoDebug ||
|
||||
updateParameters->mode != MapMode::Continuous || !updateParameters->spriteLoaded ||
|
||||
!orchestrator.retainedGlobalResourcesReady() || !retainedPlacementStable ||
|
||||
orchestrator.retainedGlobalLightTransition();
|
||||
const char* globalLiveFallbackReason = renderSegmentGlobalLiveFallbackReason({
|
||||
retainedMode,
|
||||
renderTreeParameters.has3D,
|
||||
renderTreeParameters.debugOptions != MapDebugOptions::NoDebug,
|
||||
updateParameters->mode == MapMode::Continuous,
|
||||
updateParameters->spriteLoaded,
|
||||
orchestrator.retainedGlobalResourcesReady(),
|
||||
orchestrator.retainedGlobalLightTransition(),
|
||||
});
|
||||
const bool globalLiveFallback = std::string_view(globalLiveFallbackReason) != "none";
|
||||
std::vector<std::string> actualExecutions(segmentPlan.segments.size(), "live");
|
||||
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); },
|
||||
[&](uint32_t index) {
|
||||
retainedSegmentCaches[index]->composite(orchestrator, parameters);
|
||||
actualExecutions[index] = "composite";
|
||||
},
|
||||
[&](int32_t begin, int32_t endExclusive) {
|
||||
drawableOpaquePass(begin, endExclusive);
|
||||
drawableTranslucentPass(begin, endExclusive);
|
||||
});
|
||||
}
|
||||
if (retainedRuntimeState) retainedRuntimeState->recordMainBufferExecution(
|
||||
segmentSnapshot.topologyRevision, renderTreeParameters.placementChanged,
|
||||
renderTreeParameters.symbolFadeChange, placementTransitionActive,
|
||||
updateParameters->crossSourceCollisions, globalLiveFallback ? "global-live" : "mixed",
|
||||
globalLiveFallbackReason, segmentPlacementGates, actualExecutions);
|
||||
drawableDebugOverlays();
|
||||
|
||||
// Give the layers a chance to do cleanup
|
||||
|
||||
@@ -242,6 +242,8 @@ public:
|
||||
entry.dependentSourceIds = dependencies.sourceIds;
|
||||
entry.unknownDependencies = dependencies.globalPrerequisites;
|
||||
if (dependencies.unknown) entry.unknownDependencies.push_back("unknown/source-or-custom");
|
||||
entry.placementCapable = renderSegmentHasPlacementCapableLayer(
|
||||
next.styleLayers, segment.beginIndex, segment.endExclusive);
|
||||
if (segment.strategy == RenderSegmentStrategy::Retained) {
|
||||
++next.retainedSegmentCount;
|
||||
entry.cache.emplace();
|
||||
@@ -325,6 +327,31 @@ public:
|
||||
if (std::string_view(decision) == "fallback") ++value.fallbackCount;
|
||||
});
|
||||
}
|
||||
void recordMainBufferExecution(uint64_t topologyRevision,
|
||||
bool placementChanged,
|
||||
float symbolFadeChange,
|
||||
bool placementTransitionActive,
|
||||
bool crossSourceCollisions,
|
||||
const char* mode,
|
||||
const char* globalLiveFallbackReason,
|
||||
const std::vector<bool>& placementGates,
|
||||
const std::vector<std::string>& actualExecutions) {
|
||||
std::lock_guard<std::mutex> lock(rangeMutex);
|
||||
if (topologyRevision != renderSegmentTopologyRevision ||
|
||||
placementGates.size() != renderSegmentDiagnostics.segments.size() ||
|
||||
actualExecutions.size() != renderSegmentDiagnostics.segments.size()) return;
|
||||
auto& diagnostics = renderSegmentDiagnostics;
|
||||
diagnostics.placementChanged = placementChanged;
|
||||
diagnostics.symbolFadeChange = symbolFadeChange;
|
||||
diagnostics.placementTransitionActive = placementTransitionActive;
|
||||
diagnostics.crossSourceCollisions = crossSourceCollisions;
|
||||
diagnostics.mainBufferExecutionMode = mode;
|
||||
diagnostics.globalLiveFallbackReason = globalLiveFallbackReason;
|
||||
for (std::size_t i = 0; i < diagnostics.segments.size(); ++i) {
|
||||
diagnostics.segments[i].placementGatePassed = placementGates[i];
|
||||
diagnostics.segments[i].actualExecution = actualExecutions[i];
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -1157,6 +1157,149 @@ TEST(RenderSegmentLocality, FailedBuildCannotReuseOldTexture) {
|
||||
EXPECT_EQ(cache.evaluate(viewportKey, 8, true), RetainedUnderlayDecision::Build);
|
||||
}
|
||||
|
||||
TEST(RenderSegmentPlacementFree, StructuralCapabilityAndConservativeGate) {
|
||||
const std::vector<StyleLayerDiagnostics> layers{
|
||||
{0, "background", "background", true, std::nullopt, std::nullopt},
|
||||
{1, "particle", "raster-particle", true, "field", std::nullopt},
|
||||
{2, "label", "symbol", true, "chart", std::nullopt}};
|
||||
EXPECT_FALSE(renderSegmentHasPlacementCapableLayer(layers, 0, 1));
|
||||
EXPECT_TRUE(renderSegmentHasPlacementCapableLayer(layers, 2, 3));
|
||||
// The symbol's current placementData may be empty; style structure still wins.
|
||||
EXPECT_TRUE(renderSegmentHasPlacementCapableLayer(layers, 2, 3));
|
||||
EXPECT_TRUE(renderSegmentHasPlacementCapableLayer(layers, -1, 1));
|
||||
EXPECT_TRUE(renderSegmentPlacementGatePassed(false, true, 1.0f, true));
|
||||
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, true, 0.0f, false));
|
||||
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false, 1.0f, false));
|
||||
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false, 0.0f, true));
|
||||
EXPECT_TRUE(renderSegmentPlacementGatePassed(true, false, 0.0f, false));
|
||||
}
|
||||
|
||||
TEST(RenderSegmentPlacementFree, MixedExecutionKeepsFreeRangesInStyleOrder) {
|
||||
ResolvedRenderSegmentPlan plan;
|
||||
plan.valid = true;
|
||||
plan.layerCount = 5;
|
||||
plan.segments = {{0, RenderSegmentStrategy::Retained, std::nullopt, 0, 1},
|
||||
{1, RenderSegmentStrategy::Continuous, std::nullopt, 1, 2},
|
||||
{2, RenderSegmentStrategy::Retained, std::nullopt, 2, 3},
|
||||
{3, RenderSegmentStrategy::Continuous, std::nullopt, 3, 4},
|
||||
{4, RenderSegmentStrategy::Retained, std::nullopt, 4, 5}};
|
||||
RetainedUnderlayCache r0, r1, r2;
|
||||
const auto k0 = key({640, 480}, 1);
|
||||
const auto k1 = key({640, 480}, 3);
|
||||
const auto k2 = key({640, 480}, 5);
|
||||
EXPECT_EQ(r0.evaluate(k0, 1, true), RetainedUnderlayDecision::Build);
|
||||
EXPECT_EQ(r1.evaluate(k1, 1, true), RetainedUnderlayDecision::Build);
|
||||
EXPECT_EQ(r2.evaluate(k2, 1, true), RetainedUnderlayDecision::Build);
|
||||
r0.markBuildSucceeded(); r1.markBuildSucceeded(); r2.markBuildSucceeded();
|
||||
const bool freeGate = renderSegmentPlacementGatePassed(false, true, 1.0f, true);
|
||||
const bool symbolGate = renderSegmentPlacementGatePassed(true, true, 1.0f, true);
|
||||
EXPECT_EQ(r0.evaluate(k0, 1, freeGate), RetainedUnderlayDecision::Reuse);
|
||||
EXPECT_EQ(r1.evaluate(k1, 1, symbolGate, "static-placement"), RetainedUnderlayDecision::Fallback);
|
||||
EXPECT_EQ(r1.getLastInvalidationReason(), "static-placement");
|
||||
EXPECT_EQ(r2.evaluate(k2, 1, freeGate), RetainedUnderlayDecision::Reuse);
|
||||
EXPECT_NE(r0.getLastInvalidationReason(), "static-placement");
|
||||
EXPECT_NE(r2.getLastInvalidationReason(), "static-placement");
|
||||
std::vector<std::string> events;
|
||||
visitRenderSegmentExecution(plan,
|
||||
[](uint32_t index) { return index == 0 || index == 4; },
|
||||
[&](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", "composite:4"}));
|
||||
// Source-local mutation/fade in R1 leaves both independent retained caches reusable.
|
||||
EXPECT_EQ(r0.evaluate(k0, 1, freeGate), RetainedUnderlayDecision::Reuse);
|
||||
EXPECT_EQ(r2.evaluate(k2, 1, freeGate), RetainedUnderlayDecision::Reuse);
|
||||
}
|
||||
|
||||
TEST(RenderSegmentPlacementFree, RealGlobalGatesStillForceGlobalLive) {
|
||||
RenderSegmentGlobalLiveGates gates{true, false, false, true, true, true, false};
|
||||
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "none");
|
||||
gates.has3D = true;
|
||||
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "3d");
|
||||
gates.has3D = false; gates.debugOptions = true;
|
||||
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "debug-options");
|
||||
gates.debugOptions = false; gates.continuousMode = false;
|
||||
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "map-mode");
|
||||
gates.continuousMode = true; gates.spriteLoaded = false;
|
||||
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "sprite-not-loaded");
|
||||
gates.spriteLoaded = true; gates.globalResourcesReady = false;
|
||||
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "global-resource-not-ready");
|
||||
gates.globalResourcesReady = true; gates.globalLightTransition = true;
|
||||
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "global-light-transition");
|
||||
gates.globalLightTransition = false; gates.retainedMode = false;
|
||||
EXPECT_STREQ(renderSegmentGlobalLiveFallbackReason(gates), "no-boundary");
|
||||
}
|
||||
|
||||
TEST(RenderSegmentPlacementFree, TopologyAndCameraChangesCannotReuseOldRetainedKey) {
|
||||
RetainedUnderlayTopologyTracker tracker;
|
||||
RetainedUnderlayTopologySnapshot topology;
|
||||
topology.staticPrefix = {{0, "RETAINED:1", 0}, {1, "CONTINUOUS:2", 1},
|
||||
{2, "RETAINED:3", 0}, {3, "CONTINUOUS:4", 1}};
|
||||
const auto firstRevision = tracker.observe(topology);
|
||||
RetainedUnderlayCache cache;
|
||||
const auto firstKey = key({640, 480}, 3);
|
||||
EXPECT_EQ(cache.evaluate(firstKey, firstRevision, true), RetainedUnderlayDecision::Build);
|
||||
cache.markBuildSucceeded();
|
||||
EXPECT_EQ(cache.evaluate(firstKey, firstRevision, true), RetainedUnderlayDecision::Reuse);
|
||||
topology.staticPrefix.resize(2); // R-C-R-C becomes R-C.
|
||||
EXPECT_GT(tracker.observe(topology), firstRevision);
|
||||
EXPECT_EQ(cache.evaluate(firstKey, tracker.current(), true, nullptr, "layer-group-mutation"),
|
||||
RetainedUnderlayDecision::Build);
|
||||
EXPECT_FALSE(cache.isValid());
|
||||
cache.markBuildSucceeded();
|
||||
EXPECT_EQ(cache.evaluate(key({640, 480}, 4), tracker.current(), true), RetainedUnderlayDecision::Build);
|
||||
EXPECT_FALSE(cache.isValid());
|
||||
}
|
||||
|
||||
TEST(RenderSegmentPlacementFree, DiagnosticsRecordActualMainBufferAndDoNotMutateOnRead) {
|
||||
RetainedUnderlayRuntimeState state;
|
||||
RenderSegmentPlan configured;
|
||||
configured.segments = {{RenderSegmentStrategy::Retained, std::nullopt},
|
||||
{RenderSegmentStrategy::Continuous, "particle"},
|
||||
{RenderSegmentStrategy::Retained, "label"}};
|
||||
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
|
||||
const std::vector<std::string> ids{"background", "particle", "label"};
|
||||
const auto plan = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), ids, true);
|
||||
ASSERT_TRUE(plan.valid);
|
||||
state.recordRenderSegmentResolution(plan, {
|
||||
{0, "background", "background", true, std::nullopt, std::nullopt},
|
||||
{1, "particle", "raster-particle", true, "field", std::nullopt},
|
||||
{2, "label", "symbol", true, "chart", std::nullopt}});
|
||||
auto snapshot = state.renderSegmentSnapshot();
|
||||
ASSERT_EQ(snapshot.segments.size(), 3u);
|
||||
EXPECT_FALSE(snapshot.segments[0].placementCapable);
|
||||
EXPECT_TRUE(snapshot.segments[2].placementCapable);
|
||||
state.recordSegmentExecution(0, true, true, true, 7, "reuse", nullptr);
|
||||
state.recordMainBufferExecution(snapshot.topologyRevision, true, 1.0f, true, true,
|
||||
"global-live", "global-light-transition",
|
||||
{true, true, false}, {"live", "live", "live"});
|
||||
snapshot = state.renderSegmentSnapshot();
|
||||
ASSERT_TRUE(snapshot.segments[0].cache);
|
||||
EXPECT_EQ(snapshot.segments[0].cache->finalDecision, "reuse");
|
||||
EXPECT_EQ(snapshot.segments[0].actualExecution, "live");
|
||||
EXPECT_EQ(snapshot.mainBufferExecutionMode, "global-live");
|
||||
EXPECT_EQ(snapshot.globalLiveFallbackReason, "global-light-transition");
|
||||
EXPECT_TRUE(snapshot.placementChanged);
|
||||
EXPECT_FLOAT_EQ(snapshot.symbolFadeChange, 1.0f);
|
||||
EXPECT_TRUE(snapshot.placementTransitionActive);
|
||||
EXPECT_TRUE(snapshot.crossSourceCollisions);
|
||||
state.recordMainBufferExecution(snapshot.topologyRevision, true, 1.0f, true, true,
|
||||
"mixed", "none", {true, true, false},
|
||||
{"composite", "live", "live"});
|
||||
const auto mixed = state.renderSegmentSnapshot();
|
||||
EXPECT_EQ(mixed.segments[0].actualExecution, "composite");
|
||||
EXPECT_EQ(mixed.segments[2].actualExecution, "live");
|
||||
const auto json = renderSegmentDiagnosticsToJSON(mixed);
|
||||
EXPECT_EQ(renderSegmentDiagnosticsToJSON(state.renderSegmentSnapshot()), json);
|
||||
EXPECT_NE(json.find("\"placementCapable\":true"), std::string::npos);
|
||||
EXPECT_NE(json.find("\"placementGatePassed\":false"), std::string::npos);
|
||||
EXPECT_NE(json.find("\"actualExecution\":\"composite\""), std::string::npos);
|
||||
EXPECT_NE(json.find("\"mainBufferExecutionMode\":\"mixed\""), std::string::npos);
|
||||
EXPECT_EQ(state.renderSegmentSnapshot().topologyRevision, mixed.topologyRevision);
|
||||
}
|
||||
|
||||
TEST(RenderSegmentPlan, StrictJSONRejectsUnknownFieldsStrategyAndInvalidFirstBoundary) {
|
||||
EXPECT_FALSE(parseRenderSegmentPlanJSON(
|
||||
R"({"schemaVersion":1,"segments":[{"strategy":"RETAINED","unexpected":true}]})"));
|
||||
|
||||
14
粒子开发记录.md
14
粒子开发记录.md
@@ -1067,3 +1067,17 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
|
||||
- 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 发行。
|
||||
|
||||
### 2026-10-08 STEP-102 RenderSegment 无 Placement 段局部复用(代码完成,待验证)
|
||||
|
||||
- 基于 `6df21dac38f072c5ad35b8dfc7ec5217509602bc` 的独立 `step102-placement-free-retained-locality` worktree,实现结构性 symbol placement capability、full CPU per-segment gate、逐段 fallback reason,以及仅从全局 live 条件移除纯 aggregate placement gate。旧动态 `retainedRangeUsesPlacement()`、Fast Path、mixed executor、C-001 与 RN method surface 保持原语义。
|
||||
- diagnostics 新增 `placementCapable`、`placementGatePassed`、`actualExecution`、全局 placement 状态、`mainBufferExecutionMode` 与 `globalLiveFallbackReason`;actual 在 main-buffer 分支执行后记录,允许 cache=reuse 但实际 live 的事实被看见。C++ 测试源码和 Android T1 symbol/T2 two-free 离线场景已交付。
|
||||
- 只做只读审计与 `git diff --check` 静态检查;CMake/Ninja、C++ 测试、Android Gradle/APK、ADB/设备、Jest、iOS/Metal、NavSea、发布、commit/push 均未执行。STEP-102 不记运行态 PASS;用户手工 Android 构建与后续独立验收命令见 `docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md`。
|
||||
|
||||
### 2026-10-08 STEP-102 RDC 独立验证补记
|
||||
|
||||
- 在独立工作树 `step102-placement-free-retained-locality` 核查了结构性 placement 判断、per-segment gate、实际 main-buffer diagnostics 与 RN 类型边界,未改 Fast Path、C-001、V20 发布源码。
|
||||
- AppleClang 对新增 C++ GTest 源码 `-fsyntax-only` PASS;独立头文件 smoke 测试 10/10 PASS;`git diff --check` PASS。
|
||||
- Metal `bazel build -c opt --//:renderer=metal //:mbgl-core` 构建 PASS(642/642,875.118s,产物 `bazel-bin/libmbgl-core.a`)。
|
||||
- Mac CMake/GTest 配置遇缺失 `libuv`,未安装依赖,正式 GTest NOT RUN。
|
||||
- 修复 STEP-102 Android Validation 入口切换时残留 STEP-093/097 状态与轮询的问题。Android APK 编译、T1/T2 真机及 STEP-100/101 回归仍待手工构建与运行;STEP-102 维持“进行中”,尚非 CLOSED/PASS。
|
||||
|
||||
Reference in New Issue
Block a user