diff --git a/docs/40_任务/STEP-102_RenderSegment无Placement段局部复用_Codex任务_v1.md b/docs/40_任务/STEP-102_RenderSegment无Placement段局部复用_Codex任务_v1.md new file mode 100644 index 000000000..9d5b55f6d --- /dev/null +++ b/docs/40_任务/STEP-102_RenderSegment无Placement段局部复用_Codex任务_v1.md @@ -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 冻结边界内,可直接完成代码并停在“等待手工构建/运行验证”。 diff --git a/docs/50_Step记录/STEP-102_RenderSegment无Placement段局部复用_v1.md b/docs/50_Step记录/STEP-102_RenderSegment无Placement段局部复用_v1.md new file mode 100644 index 000000000..971b619fe --- /dev/null +++ b/docs/50_Step记录/STEP-102_RenderSegment无Placement段局部复用_v1.md @@ -0,0 +1,56 @@ +# 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。 diff --git a/docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md b/docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md new file mode 100644 index 000000000..f96d7cf89 --- /dev/null +++ b/docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md @@ -0,0 +1,121 @@ +# 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 状态,再在选定设备上独立运行。下列 `` 需替换为实际 `adb devices` 返回的 serial;每场景单独清日志、force-stop、启动并留存完整 logcat: + +```bash +adb -s install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk +adb -s logcat -c +adb -s shell am force-stop org.maplibre.android.testapp +adb -s shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity --es step102_scenario T1_FREE_SYMBOL +adb -s 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 与发行流程。 diff --git a/include/mbgl/renderer/render_segment.hpp b/include/mbgl/renderer/render_segment.hpp index 566f76f68..06f85de1d 100644 --- a/include/mbgl/renderer/render_segment.hpp +++ b/include/mbgl/renderer/render_segment.hpp @@ -70,6 +70,47 @@ inline RenderSegmentDependencies renderSegmentDependencies(const std::vector& layers, + int32_t begin, + int32_t endExclusive) { + if (begin < 0 || endExclusive < begin || endExclusive > static_cast(layers.size())) return true; + for (int32_t i = begin; i < endExclusive; ++i) { + if (layers[static_cast(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 layers; std::vector dependentSourceIds; std::vector unknownDependencies; + bool placementCapable = false; + bool placementGatePassed = false; + std::string actualExecution{"not-rendered"}; std::optional 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; diff --git a/packages/maplibre-particle-rn/src/types.ts b/packages/maplibre-particle-rn/src/types.ts index 3adfd26de..c56f1f48d 100644 --- a/packages/maplibre-particle-rn/src/types.ts +++ b/packages/maplibre-particle-rn/src/types.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; diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index 38ae8f56b..df1bc9bb9 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -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(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" ) diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step102PlacementFixture.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step102PlacementFixture.kt new file mode 100644 index 000000000..d5b2a2639 --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step102PlacementFixture.kt @@ -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))) +} diff --git a/src/mbgl/renderer/render_orchestrator.cpp b/src/mbgl/renderer/render_orchestrator.cpp index 439c4564c..1ade4e1f3 100644 --- a/src/mbgl/renderer/render_orchestrator.cpp +++ b/src/mbgl/renderer/render_orchestrator.cpp @@ -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); } diff --git a/src/mbgl/renderer/render_orchestrator.hpp b/src/mbgl/renderer/render_orchestrator.hpp index 3eab4fc1b..6b8e04b6c 100644 --- a/src/mbgl/renderer/render_orchestrator.hpp +++ b/src/mbgl/renderer/render_orchestrator.hpp @@ -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; diff --git a/src/mbgl/renderer/render_segment_json.hpp b/src/mbgl/renderer/render_segment_json.hpp index 4239e7150..ffc0a8be3 100644 --- a/src/mbgl/renderer/render_segment_json.hpp +++ b/src/mbgl/renderer/render_segment_json.hpp @@ -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(); diff --git a/src/mbgl/renderer/renderer_impl.cpp b/src/mbgl/renderer/renderer_impl.cpp index 275e774a2..03a07ec33 100644 --- a/src/mbgl/renderer/renderer_impl.cpp +++ b/src/mbgl/renderer/renderer_impl.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #if MLN_RENDER_BACKEND_METAL #include @@ -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 segmentSourceStates(segmentPlan.segments.size()); std::vector segmentSupportsCache(segmentPlan.segments.size(), false); + std::vector 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(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 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 diff --git a/src/mbgl/renderer/retained_underlay_runtime_state.hpp b/src/mbgl/renderer/retained_underlay_runtime_state.hpp index 495371ec4..697254283 100644 --- a/src/mbgl/renderer/retained_underlay_runtime_state.hpp +++ b/src/mbgl/renderer/retained_underlay_runtime_state.hpp @@ -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& placementGates, + const std::vector& actualExecutions) { + std::lock_guard 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) { diff --git a/test/renderer/retained_underlay_cache.test.cpp b/test/renderer/retained_underlay_cache.test.cpp index 8365765fe..67347f0bc 100644 --- a/test/renderer/retained_underlay_cache.test.cpp +++ b/test/renderer/retained_underlay_cache.test.cpp @@ -1157,6 +1157,149 @@ TEST(RenderSegmentLocality, FailedBuildCannotReuseOldTexture) { EXPECT_EQ(cache.evaluate(viewportKey, 8, true), RetainedUnderlayDecision::Build); } +TEST(RenderSegmentPlacementFree, StructuralCapabilityAndConservativeGate) { + const std::vector 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 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{"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 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}]})")); diff --git a/粒子开发记录.md b/粒子开发记录.md index 6fe41d350..f4bec52c5 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.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。