From e70b795098cf0ffefe21becf437337586033b13d Mon Sep 17 00:00:00 2001 From: tei Date: Wed, 23 Sep 2026 11:57:44 +0900 Subject: [PATCH] docs: preserve particle task history and RDC workflow rules --- AGENTS.md | 63 ++ ...mework_Bazel_Codegen阻塞收尾_Codex任务_v1.md | 113 +++ ..._Facade与XCFramework发行基线_Codex任务_v1.md | 801 ++++++++++++++++++ ...Module_Fabric与XCFramework消费_Codex任务_v1.md | 609 +++++++++++++ ...�™态底图缓存PoC与Renderer观测_Codex任务_v1.md | 357 ++++++++ ...连续渲染静态底图缓存与动画快路径优化规划_v1.md | 156 ++++ 6 files changed, 2099 insertions(+) create mode 100644 docs/40_任务/STEP-082_XCFramework_Bazel_Codegen阻塞收尾_Codex任务_v1.md create mode 100644 docs/40_任务/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex任务_v1.md create mode 100644 docs/40_任务/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex任务_v1.md create mode 100644 docs/40_任务/STEP-088_静态底图缓存PoC与Renderer观测_Codex任务_v1.md create mode 100644 docs/40_任务/粒子连续渲染静态底图缓存与动画快路径优化规划_v1.md diff --git a/AGENTS.md b/AGENTS.md index 5f14c4c46..eff1ef1d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,3 +89,66 @@ Codex 的核心角色是“实现者”,不作为其自身修改结果的唯 每一阶段必须有 `docs/50_Step记录/STEP-xxx_*.md`。完成报告固定写入 `docs/报告/STEP-xxx_<主题>_Codex完成报告_vN.md`(仅在任务要求 Codex 完成报告时)。 修改结束必须按当前构建规范验证、更新 Step、追加 `粒子开发记录.md`,并明确所有未完成/阻塞。Git 提交必须 scoped,禁止把既有无关 dirty/untracked 一并提交。 +## 8. Codex 写代码,RDC 执行编译 / 测试 + +本项目固定采用以下闭环,除非用户在当前任务中明确另行授权: + +1. **Codex 负责实现与修复。** + - 读取当前任务、设计、源码与错误信息; + - 修改代码、测试代码、构建脚本和任务要求的文档; + - 分析 RDC 返回的编译、链接、测试、codegen、打包失败; + - 根据真实错误继续修复代码。 + +2. **RDC / 普通 Terminal 负责实际编译与验证。** + - CMake / Ninja / Bazel 编译; + - C++ / OpenGL / Metal 定向测试; + - codegen、链接、打包、artifact 检查; + - 其他耗时构建或机械性验证。 + Codex 不应自行启动、等待或轮询这些长时间命令。 + +3. **固定闭环:** + `Codex 写代码 → Codex 停止并给出精确验证命令 → RDC 执行 → RDC 返回结果 → 若失败,Codex 分析并修复 → RDC 再验证`。 + +4. **失败日志只回传必要信息。** + - RDC 保留完整 stdout/stderr; + - 成功时只返回 exit code、耗时和必要摘要; + - 失败时优先返回首个真实 error、相关 target、前后必要上下文和日志路径; + - 不把完整大型 build log 反复送入 Codex。 + +5. **Codex 禁止行为。** + - 未经用户明确授权,不得自行执行长时间编译、链接、codegen、打包或大型测试; + - 不得创建 background terminal 等待 build; + - 不得高频轮询构建状态; + - 不得为了等待编译而自行切换模型或启动第二个 Codex 实例; + - 不得把“编译未完成”当作理由持续占用高能力模型。 + +6. **RDC 发现问题后的处理。** + - RDC 可以直接处理范围明确、风险低、原因清楚的小问题,包括: + - 构建命令、CMake/Bazel 配置、链接配置、平台条件、测试条件等小型工程问题; + - 单文件或少量文件内、无需重新设计架构的小型源码修正; + - 明确的编译错误、声明/定义不一致、include/宏条件、简单 ABI/平台兼容问题; + - 不改变既定业务语义和总体架构的局部修复。 + - RDC 修完后应立即重新编译/测试验证,不必为了这类小问题启动 Codex。 + - 以下情况才交给 Codex: + - 大规模逻辑修改; + - 跨模块或跨层重构; + - renderer / runtime / API 设计需要重新判断; + - 需要新增较完整功能或大量代码; + - 用户明确指定“这是 Codex 代码任务”。 + - 一旦 RDC 判断修复范围开始扩大、需要重新设计或无法通过少量明确修改解决,应停止继续扩改,把错误证据和现状交给 Codex。 + +7. **用户可见性。** + - 本项目未经用户明确授权,不得使用后台/headless Codex 执行任务; + - Codex 默认运行在用户可见的 Terminal/TUI 中; + - 当前 Mac 上如需启动或恢复 Codex,必须优先使用现有 GNU Screen 会话 `7445.codex`; + - 不得另起隐藏的 Codex 进程或新的不可见会话,除非用户明确授权; + - 用户应能够通过该 screen 会话直接看到 Codex 的输入、输出和当前状态。 + +8. **任务书冲突处理。** + - 若旧任务书写有“Codex 必须执行编译/测试”,但用户未在当前任务明确重新授权,则以本节为准: + Codex 只准备代码与验证命令,RDC 负责实际执行。 + - 只有用户在当前任务中明确要求“由 Codex 执行某项编译/测试”时,Codex 才可执行该项。 + +核心原则: + +> Codex 负责写和改;RDC 负责跑和验;RDC 报错,再交回 Codex 修。 diff --git a/docs/40_任务/STEP-082_XCFramework_Bazel_Codegen阻塞收尾_Codex任务_v1.md b/docs/40_任务/STEP-082_XCFramework_Bazel_Codegen阻塞收尾_Codex任务_v1.md new file mode 100644 index 000000000..af4ee8dde --- /dev/null +++ b/docs/40_任务/STEP-082_XCFramework_Bazel_Codegen阻塞收尾_Codex任务_v1.md @@ -0,0 +1,113 @@ +# STEP-082 XCFramework Bazel Codegen 阻塞收尾 Codex 任务 v1 + +## 1. 当前基线 + +- 项目:`/Users/tei/dev/MapLibreParticleLab` +- 分支:`particle/main` +- 起始 HEAD:`dd9687f5ed6bb9be4b58b73f2efd39f54faef7d6` +- `origin/particle/main` 应与起始 HEAD 一致。 +- STEP-082 当前状态:`IMPLEMENTED / WAITING_MAC_XCFRAMEWORK_VALIDATION`。 +- 本任务仍属于 STEP-082 收尾,不创建 STEP-083,不进入 RN/JS/NavSea。 + +开始前必须读取: +- `docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v1.md` +- `docs/50_Step记录/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_v1.md` +- `bazel/core.bzl` +- `BUILD.bazel` +- `scripts/generate-style-code.mjs` +- `CMakeLists.txt` + +## 2. 已知阻塞与调查结论 + +当前 `bazel build //platform/ios:MapLibre.dynamic` 在 `//:generated_style_code` 阶段失败: +`src/mbgl/style/layers/particle_validation_layer.cpp` 被声明为 generator 输出,但 `scripts/generate-style-code.mjs` 不生成它。 + +当前只读核对显示: +- `particle_validation_layer.cpp` 是仓库中已存在的手写源文件; +- CMake 在 `CMakeLists.txt` 中把它作为普通源文件直接编译; +- `bazel/core.bzl` 目前把它放在 `MLN_GENERATED_STYLE_SOURCE`; +- style generator 只遍历 style spec 中的正式 layer type 并生成对应标准 style layer 文件。 + +这说明高概率是 Bazel 文件分类错误,而不是 generator 缺功能。Codex 必须先自行核对真实代码和 Git 历史后再修改。## 3. 本次唯一目标 + +用最小改动修复 Bazel 对 `particle_validation_layer.cpp` 的源文件分类,使: +1. 它不再作为 `generated_style_code` 的声明输出; +2. 它仍然作为 Core 普通源文件参与 Bazel 构建; +3. 不改变现有 CMake 编译行为; +4. 不改变 Particle Validation Layer、RasterParticle、WeatherColor、QBIN、Metal 或 Runtime 语义。 + +优先方案是:若调查确认该文件确属手写源文件,则从 `MLN_GENERATED_STYLE_SOURCE` 移出并纳入正确的普通 Core source 集合。不得为了通过构建而让 generator 复制、伪造或空生成该文件。 + +## 4. 必做清单 + +- [ ] 核对起始 branch / HEAD / remote / dirty。 +- [ ] 用 Git 历史确认 `particle_validation_layer.cpp` 的来源和预期性质。 +- [ ] 最小修复 Bazel source classification。 +- [ ] `bazel build //:generated_style_code` 通过。 +- [ ] `bazel build //platform/ios:MapLibre.dynamic` 通过。 +- [ ] 验证生成的 `MapLibre.dynamic.xcframework` 中包含 public `MLNParticleFieldRuntime.h`。 +- [ ] 验证 umbrella/public header 链可见 `MLNParticleFieldRuntime`。 +- [ ] 验证 binary 中存在该 facade 的 Objective-C public symbol/class。 +- [ ] 验证 `MLNMapView_Private.h` 没有被导出为 public header。 +- [ ] 若正式 XCFramework 可压缩,则生成临时 zip,记录真实文件大小与 SHA-256;大二进制默认不提交 Git。 +- [ ] 更新 `release/maplibre-particle/ios/release-manifest.json` 中可由本次真实构建确认的字段;URL 尚未发布时保持明确占位,不伪造远端地址。 +- [ ] `git diff --check` PASS。 +- [ ] 更新 STEP-082 Step 记录和 `粒子开发记录.md`。 +- [ ] 生成 `docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v2.md`。 +- [ ] scoped commit、push 到 `origin/particle/main`,记录 Final HEAD。 + +## 5. 严格禁止 + +不得: +- 修改 `scripts/generate-style-code.mjs` 去伪造 particle validation 输出,除非调查证明它本应由 style spec 生成;若出现这种相反证据,先在报告中说明,不要扩大实现。 +- 修改 Particle renderer、Metal shader、QBIN decoder、FieldProfile、BindingProjection、粒子 physics。 +- 修改 `packages/maplibre-particle-rn/`、`integration/react-native/` 或 NavSea。 +- 重做 STEP-081 真机 smoke。 +- 开始 STEP-083。 +- 为通过构建删除或停用 Particle Validation Layer。 +- 使用 `git add -A`、`git add .`、`git reset --hard`、`git clean`。## 6. 用户已有 dirty,必须原样保护 + +以下路径不得修改、暂存、提交: +- `docs/doxygen/doxygen-awesome-css` +- `platform/ios/app/MBXAppDelegate.m` +- `platform/windows/vendor/vcpkg` + +另外,现有未跟踪任务书: +- `docs/40_任务/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex任务_v1.md` +不得为了清理工作树而删除或改写。 + +本任务书自身也不要求提交;提交时只显式 stage 本次实现与正式记录文件。 + +## 7. 构建与验证要求 + +macOS 上使用仓库当前真实工具链。编译输出遵循全局 `$build-output-throttle`。 + +至少执行: +- `bazel build //:generated_style_code` +- `bazel build //platform/ios:MapLibre.dynamic` + +若第二条成功,必须直接检查实际 XCFramework,不允许只根据 Bazel exit code 判定: +- public Headers; +- umbrella import; +- Objective-C class/symbol; +- private header 排除; +- artifact 路径、大小; +- zip SHA-256(若生成 zip)。 + +如果修复第一个 blocker 后暴露新的、与本次 Bazel/iOS packaging 直接相关的小型问题,可以继续最小修复并验证;如果新问题涉及 renderer/Core 语义或较大架构改动,停止并报告,不得扩大范围。 + +## 8. 关闭判定 + +只有以下全部成立,STEP-082 才可改为 `CLOSED / PASS`: +- Bazel generated style target PASS; +- `MapLibre.dynamic` XCFramework build PASS; +- `MLNParticleFieldRuntime.h` 确认存在于 public Headers; +- public ObjC facade symbol/class 确认存在; +- `MLNMapView_Private.h` 未 public export; +- artifact size 与真实 SHA-256 已记录; +- `git diff --check` PASS; +- v2 完成报告、Step 记录、粒子开发记录更新; +- scoped commit 已 push,local HEAD == `origin/particle/main`; +- 用户三处 dirty 和旧任务书保持未触碰。 + +若任一门槛未满足,保持 `IMPLEMENTED / WAITING_MAC_XCFRAMEWORK_VALIDATION` 或明确 BLOCKED,不得写 PASS。 diff --git a/docs/40_任务/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex任务_v1.md b/docs/40_任务/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex任务_v1.md new file mode 100644 index 000000000..817ca078c --- /dev/null +++ b/docs/40_任务/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex任务_v1.md @@ -0,0 +1,801 @@ +# STEP-082 iOS Native FieldRuntime Facade 与 XCFramework 发行基线 — Codex 任务 v1 + +状态:READY FOR IMPLEMENTATION +项目:MapLibreParticleLab / 粒子系统 +目标分支:`particle/main` +任务基线 HEAD:`3e03949d3411c830cfff4bf320cd350f5b6a7527` + +--- + +## 0. 开始前必须确认 + +先完整阅读本任务文件,再开始修改。 + +开始时只读执行并记录: + +```bash +git branch --show-current +git rev-parse HEAD +git rev-parse origin/particle/main +git status --short +``` + +预期: + +```text +branch = particle/main +HEAD = origin/particle/main = 3e03949d3411c830cfff4bf320cd350f5b6a7527 +``` + +已知且必须保持不动的用户无关修改: + +```text +docs/doxygen/doxygen-awesome-css +platform/ios/app/MBXAppDelegate.m +platform/windows/vendor/vcpkg +``` + +若还有新的未知 dirty: + +- 不自动清理; +- 不 reset; +- 不 checkout 覆盖; +- 不纳入本任务 commit; +- 在完成报告中说明。 + +### 执行清单 + +开始实施前建立简短勾选清单,至少包含: + +- [ ] iOS public Native FieldRuntime facade +- [ ] MapView-owned 生命周期 +- [ ] V4 submit / binding / forecast / clear +- [ ] fail-closed / style reload 安全 +- [ ] public header 无 Core 类型泄漏 +- [ ] XCFramework public header / symbol 验证 +- [ ] distribution-ready 元数据 +- [ ] 中文设计/Step/完成报告 +- [ ] git diff --check +- [ ] scoped commit / push +- [ ] 最终逐项对照 + +不得静默跳过;无法完成必须明确写明。 + +--- + +# 1. 本 Step 的定位 + +STEP-081 已正式 `CLOSED / PASS`。 + +本任务禁止重新做: + +- Uniform East +- Uniform North +- Vortex +- QBIN T0 / MID / T1 +- Clear / Clear 后重建 +- STEP081 readback +- 固定绿线 +- Point fallback +- STEP-081 Metal pipeline cache 根因复验 + +STEP-082 是新的产品化前置层: + +```text +RN / Consumer(后续 STEP-083) + ↓ +MapLibre iOS public ObjC API + ↓ +MLNParticleFieldRuntime + ↓ +MLNMapView private implementation seam + ↓ +mbgl::Style / RasterArraySource / QBIN V4 + ↓ +WeatherColor / RasterParticle / Metal +``` + +本 Step 只做到 **公开 Native facade + 正式 XCFramework 导出基线**。 + +不做 RN TurboModule。 + +--- + +# 2. 已确认的当前真实事实 + +实施前只需快速复核,不得重新做大范围架构调查。 + +## 2.1 当前 RN package 是 Android-only Native + +当前: + +```text +packages/maplibre-particle-rn/package.json +version = 13.4.1-particle.5 +``` + +`files` 没有 `ios`。 + +`react-native.config.js` 仅有 Android。 + +这是 STEP-083 的工作,不在本 Step 修改。 + +## 2.2 STEP-081 private seam 不能作为正式 SDK API + +当前: + +```text +platform/ios/src/MLNMapView_Private.h +``` + +暴露: + +```objc +- (mbgl::Map &)mbglMap; +``` + +但该 header 属于: + +```text +MLN_IOS_PRIVATE_HEADERS +``` + +不在 `MapLibre.dynamic.xcframework` 正式 public headers 中。 + +因此本任务必须新增一个正式 public facade,而不是把 `MLNMapView_Private.h` 暴露出去。 + +## 2.3 Particle/Weather 类型继续保持 Core-only + +当前 Darwin LayerManager 已注册: + +```text +RasterParticleLayerFactory +WeatherColorLayerFactory +``` + +为 Core-only layer。 + +不要新建: + +```text +MLNRasterParticleLayer +MLNWeatherColorLayer +MLNRasterArraySource +``` + +RN / Host 仍通过 Style JSON 创建: + +```text +raster-array +weather-color +raster-particle +``` + +Native facade 只负责找到这些对象并提交/控制数据。 + +--- + +# 3. 目标架构 + +## 3.1 新增公开 iOS Runtime facade + +推荐类型: + +```text +MLNParticleFieldRuntime +MLNParticleFieldRuntimeConfiguration +MLNMapView+ParticleFieldRuntime +``` + +如果现有 ObjC 命名规范要求轻微调整,可以调整文件名,但必须在设计文档说明。 + +禁止使用: + +```text +STEP081* +Smoke* +Fixture* +NavSea* +``` + +作为公开 Native 类型名。 + +## 3.2 Public API 绝不能暴露 Core 类型 + +公开 header 只能依赖: + +- Foundation +- MapLibre public Objective-C 类型 + +禁止出现在 public header: + +```text +mbgl::Map +mbgl::Style +mbgl::style::RasterArraySource +std::vector +std::unique_ptr +void * core handle +raw Core pointer +MLNMapView_Private.h +``` + +QBIN payload 使用: + +```objc +NSData * +``` + +而不是文件 URL。 + +“本地文件读取”属于后续 RN bridge 边界,不进入 Native SDK facade。 + +## 3.3 MapView-owned 生命周期 + +目标语义与 Android map-owned `WeatherRuntime` 一致: + +```text +MLNMapView + owns +MLNParticleFieldRuntime +``` + +必须提供等价的: + +- replace/create +- current runtime +- dispose + +行为。 + +建议: + +```objc +- replaceParticleFieldRuntimeWithConfiguration: +- particleFieldRuntime +- disposeParticleFieldRuntime +``` + +可使用 category + associated object,或符合当前 iOS SDK 结构的等价方式。 + +要求: + +1. `MLNParticleFieldRuntime` 不得强引用 MapView 形成循环; +2. MapView 释放后 runtime 不得访问悬空 Core; +3. replace 必须先 dispose 旧 runtime; +4. dispose 幂等; +5. runtime 不保存 `RasterArraySource *` 等跨 style 生命周期 Core pointer; +6. 每次命令通过当前 MapView/current style 重新查找目标 source/layer。 + +--- + +# 4. Configuration + +Configuration 至少包含: + +```text +sourceId +weatherColorLayerId +particleLayerId +``` + +要求: + +- 初始化时字符串非空; +- Runtime 不硬编码 RN package ID; +- RN package 后续仍可使用当前固定值: + - `rn-weather-fields` + - `rn-weather-color` + - `rn-weather-particles` +- Native SDK 保持 Host-neutral。 + +--- + +# 5. attach / replace 时的对象验证 + +创建 runtime 时必须检查当前 style 中: + +```text +sourceId -> RasterArraySource +weatherColorLayerId -> WeatherColor layer +particleLayerId -> RasterParticle layer +``` + +验证必须基于 Core 真实类型,而不是只检查字符串 ID。 + +失败时: + +- 不创建“半可用” runtime; +- 返回明确错误; +- 不创建第二套 source/layer; +- 不偷偷替换 style; +- 不吞掉错误后返回成功。 + +Style 尚未 ready 时也必须明确失败。 + +后续 Host 会在 style ready 后重试/attach。 + +--- + +# 6. STEP-082 必须实现的 QBIN V4 API + +本 Step 只冻结新的 V4 产品主链。 + +至少实现: + +```text +submitEncodedField +setEncodedColorBinding +setEncodedParticleBinding +setEncodedForecastTime +setEncodedForecastPlaybackState +setEncodedForecastPlaybackSpeed +clearEncodedField +clearAllEncodedFields +``` + +## 6.1 submitEncodedField + +输入语义与当前 Android/Core 保持一致: + +```text +fieldKey +fieldID +generation +dataVersion +contractJson +NSData * qbin +``` + +必须继续复用现有: + +```text +parseResolvedFieldProfileContractJson(...) +RasterArraySource::submitEncodedField(...) +``` + +禁止在 ObjC facade 中重新实现 QBIN decoder。 + +必须继续遵守: + +- `generation > 0` +- `dataVersion >= 0` +- ID UTF-8 边界与当前 V4 contract 一致 +- contract JSON 有明确大小上限 +- QBIN transport 有明确上限 +- 空 payload 拒绝 +- invalid contract fail-closed + +建议继续保持当前 64 MiB payload 上限与 256 KiB contract 上限;若 Core 当前权威 guard 已定义同一值,应优先复用,不复制新的魔法数字。 + +## 6.2 结果映射 + +必须提供稳定 ObjC enum / typed result,能够无损表达当前 Core: + +```text +ACCEPTED_INSERTED +ACCEPTED_REPLACED_REVISION +DUPLICATE +REJECTED_DECODE +REJECTED_CONTRACT +REJECTED_INVALID_FIELD_KEY +REJECTED_IDENTITY_CONFLICT +REJECTED_STALE_REVISION +REJECTED_INCOMPATIBLE +REJECTED_CAPACITY +``` + +“source missing / runtime disposed / style not ready”属于 facade/lifecycle 错误,不得伪装成某个 Core submit result。 + +## 6.3 Binding + +Color Binding 必须保持: + +```text +fieldKey +semanticID +groupID +expectedValueClass +expectedUnitID +``` + +支持: + +```text +SCALAR +VELOCITY_COMPONENT +DIRECTION_ANGLE +``` + +Particle Binding 必须保持: + +```text +VECTOR_COMPONENTS +DIRECTION_ANGLE +``` + +inactive selector 仍必须严格为: + +```text +semanticID = 0 +groupID = 0 +``` + +不得按“是否为空”猜模式。 + +## 6.4 Forecast + +保留 Core-owned: + +```text +requested forecast time +play / pause +playback speed +T0 / T1 / mix +boundary reuse +``` + +ObjC runtime 只转发命令。 + +禁止复制 Forecast Clock。 + +## 6.5 Clear + +实现: + +```text +clearEncodedField(fieldKey) +clearAllEncodedFields() +``` + +Clear 后必须触发必要 rerender / resource release 路径,但不得增加 STEP081 临时 readback。 + +--- + +# 7. Renderer / Style 边界 + +本任务不得改变: + +- QBIN V2 格式 +- FieldProfile contract +- BindingProjection +- V4 materialization +- T0/T1 interpolation +- Metal particle physics +- 1 px current-endpoint Metal coverage +- WeatherColor/RasterParticle shader 语义 + +如果 facade 接入暴露出 Core 缺陷: + +- 先停止扩大修改; +- 在完成报告中列出 blocker; +- 不以“顺便重构 renderer”解决。 + +--- + +# 8. Public Header / XCFramework 接线 + +必须把新 public API 正式加入 iOS framework 导出链。 + +至少检查并按真实结构更新: + +```text +platform/ios/bazel/files.bzl +platform/ios/src/Mapbox.h +platform/ios/BUILD.bazel +``` + +若实现文件属于现有 source list 自动包含机制,按真实情况处理,不得重复注册。 + +最终产出的: + +```text +MapLibre.dynamic.xcframework +``` + +必须包含新 public header。 + +同时确认: + +- `MLNMapView_Private.h` 仍然不是 public header; +- 没有把任何 Core header 整包暴露给 Consumer; +- 新 public header 可以单独被 Objective-C Consumer import; +- public symbol 存在于 framework binary。 + +--- + +# 9. Distribution-ready 基线 + +当前 MLRN 11.3.6 iOS 通过 SwiftPM distribution 消费 MapLibre Native。 + +本 Step **不创建远端 Git 仓库,不擅自打 release tag,不发布公网 Swift Package**。 + +但必须在仓库中建立一个最小、可审计的 iOS Particle binary distribution 说明/模板,建议放在: + +```text +release/maplibre-particle/ios/ +``` + +至少包含: + +```text +README.md +Package.swift.template +release-manifest.json +``` + +或与现有 release 目录规范等价的结构。 + +`Package.swift.template` 的目标是后续独立 distribution repo 可以直接使用: + +```text +binaryTarget(name: "MapLibre", url: "...MapLibre.dynamic.xcframework.zip", checksum: "...") +``` + +注意: + +- product/module 名必须保持 `MapLibre`,以满足 MLRN import; +- 不允许再生成第二个不同 module 名并要求 MLRN 同时链接两份 MapLibre; +- URL 可以先用明确占位符; +- checksum 必须在真实 zip 生成后记录真实值; +- 若当前 Step 尚不能生成正式 zip,则 manifest 明确标记 `NOT_BUILT`,禁止伪造 hash。 + +建议 external distribution version 独立采用语义版本,例如: + +```text +6.28.0-particle.1 +``` + +但不要直接修改 `platform/ios/VERSION=6.28.0`,除非通过构建系统审查确认不会破坏 upstream-compatible framework version。 + +本 Step 的核心是建立“MapLibre module 不变 + 自定义 binary distribution version 独立管理”的边界。 + +--- + +# 10. MLRN 11.3.6 后续兼容性记录 + +STEP-082 不写 RN module,但设计文档必须记录下一 Step 已确认的 iOS 视图链: + +```text +reactTag + -> React View Registry + -> Fabric ComponentView + -> contentView + -> MLNMapView + -> public MLNParticleFieldRuntime +``` + +同时记录: + +- MLRN 11.3.6 是当前 peer dependency; +- 后续 STEP-083 不建立第二套 Map registry; +- 不依赖 MLRN private ObjC header 作为长期 API; +- 后续通过 `$MLRN_SPM_SPEC` 让 MLRN 与 Particle Bridge 使用同一份自定义 `MapLibre` binary。 + +不要在本 Step 修改: + +```text +packages/maplibre-particle-rn/ +integration/react-native/ +NavSea +``` + +除非仅为文档引用且任务确实需要;默认都不改。 + +--- + +# 11. 测试与验证 + +## 11.1 必做静态检查 + +必须执行: + +```text +git diff --check +``` + +并检查: + +1. public header 没有 `mbgl::`; +2. public header 没有 STL 类型; +3. `MLNMapView_Private.h` 未进入 public header list; +4. Core-only layer/source wrapper 没有被新增; +5. Android/RN 文件没有变化; +6. STEP-081 临时诊断代码没有恢复。 + +## 11.2 Objective-C / Objective-C++ 编译 + +如果执行环境是 macOS: + +- 使用当前仓库真实 iOS build 工具链; +- 编译输出遵循全局 `$build-output-throttle`; +- 构建包含新 facade 的 iPhoneOS arm64 或正式 XCFramework target; +- 不需要重新做 STEP-081 真机视觉 smoke。 + +如果执行环境不是 macOS: + +- 不得伪造 iOS build PASS; +- 完成源码/构建图静态接线; +- 完成报告标记 `WAITING_MAC_XCFRAMEWORK_VALIDATION`。 + +## 11.3 XCFramework 验证 + +如果成功生成 XCFramework,必须验证: + +```text +Headers 中存在新 public header +Mapbox.h 可导入该 API +framework binary 中存在对应 public ObjC symbol +MLNMapView_Private.h 不在导出 Headers +``` + +并记录 zip: + +```text +文件名 +size +SHA-256 +``` + +可将临时大产物保留在 build/release 临时目录,但是否提交大二进制必须遵守仓库现有规则;默认不要把 XCFramework zip 直接提交进 Git。 + +## 11.4 本 Step 不做 + +禁止: + +- Android Gradle +- APK +- ADB +- Android 设备 +- STEP-081 iPad 视觉复测 +- RN iOS build +- NavSea build +- provider/network 测试 + +--- + +# 12. 文档输出 + +所有文档使用中文。 + +必须新增/更新: + +```text +docs/30_设计/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行设计_v1.md +docs/50_Step记录/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_v1.md +粒子开发记录.md +``` + +Codex 完成报告必须固定生成到: + +```text +docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v1.md +``` + +若同名报告已存在,递增 v2/v3,不覆盖旧报告。 + +完成报告至少写明: + +- 起始 HEAD +- 结束 HEAD +- 修改文件 +- 新 public API +- 生命周期模型 +- V4 API 覆盖 +- public/private header 检查 +- XCFramework build 结果 +- artifact/hash(若有) +- 未完成/阻塞 +- Git commit hash +- commit message +- push result +- remote branch +- Final HEAD +- 无关 dirty 未触碰确认 + +--- + +# 13. Git 边界 + +只提交本 STEP 相关修改。 + +禁止: + +```text +git add -A +git add . +git reset --hard +git checkout -- <用户无关文件> +git clean +``` + +必须显式按文件/目录暂存。 + +提交前再次检查: + +```bash +git status --short +git diff --check +git diff --cached --stat +git diff --cached +``` + +建议实现提交 message: + +```text +feat: add ios particle field runtime facade +``` + +若文档收尾需要第二个 scoped commit,可使用: + +```text +docs: record STEP-082 ios runtime distribution baseline +``` + +提交后: + +```bash +git push origin particle/main +``` + +若 push 失败,不得宣称成功。 + +--- + +# 14. STEP-082 关闭门槛 + +只有同时满足以下条件,才可判定本 Step `CLOSED / PASS`: + +- [ ] public iOS FieldRuntime facade 已实现; +- [ ] MapView-owned lifecycle 已实现; +- [ ] V4 submit/binding/forecast/clear 已接通现有 Core; +- [ ] public API 无 Core/STL 类型泄漏; +- [ ] style reload / source missing fail-closed; +- [ ] `MLNMapView_Private.h` 仍为 private; +- [ ] XCFramework public header 接线完成; +- [ ] macOS 环境下 XCFramework/arm64 compile 已通过; +- [ ] 若产出 zip,SHA-256 已记录; +- [ ] distribution-ready 模板/manifest 已建立; +- [ ] 没有修改 RN/Android/NavSea; +- [ ] 没有重新验证 STEP-081; +- [ ] 3 处用户无关 dirty 未触碰; +- [ ] `git diff --check` PASS; +- [ ] 中文 Step 记录与完成报告已更新; +- [ ] scoped commit 已完成; +- [ ] push `origin/particle/main` 成功; +- [ ] Final HEAD 已记录。 + +如果 macOS/XCFramework 构建条件不可用: + +```text +IMPLEMENTED / WAITING_MAC_XCFRAMEWORK_VALIDATION +``` + +不得写 `CLOSED / PASS`。 + +--- + +# 15. 下一 Step 边界 + +STEP-082 通过后,下一任务才是: + +```text +STEP-083 +iOS RN TurboModule + Fabric reactTag/MLNMapView Bridge + Custom MapLibre SwiftPM Consumer +``` + +STEP-083 预计负责: + +```text +packages/maplibre-particle-rn/ios +Podspec / codegen +React View Registry +contentView -> MLNMapView +本地 QBIN file:// -> NSData +调用 STEP-082 public facade +$MLRN_SPM_SPEC 指向自定义 MapLibre binary distribution +RN Test Host iOS +V4 T0 / MID / T1 / Clear +``` + +本 STEP 不提前进入该范围。 diff --git a/docs/40_任务/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex任务_v1.md b/docs/40_任务/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex任务_v1.md new file mode 100644 index 000000000..bb095c25e --- /dev/null +++ b/docs/40_任务/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex任务_v1.md @@ -0,0 +1,609 @@ +# STEP-083 iOS RN 统一 FieldRuntime / TurboModule / Fabric 与 XCFramework 消费 Codex 任务 v1 + +## 0. 任务定位 + +本任务是 STEP-082 之后的一个完整实现批次,不再拆成多个很小的 Step。 + +目标是让当前已经在 Android 使用的同一套 React Native / JS FieldRuntime 公共 API,在 iOS 上获得真实 native 实现,并通过 React Native Fabric 的 reactTag 找到当前 MLRN MapView,最终调用 STEP-082 已冻结并验证的 iOS Native FieldRuntime / MapLibre Core。 + +完成后应形成: + +JS/TS FieldRuntime +→ 同一 NativeWeatherRuntimeBridge TurboModule ABI +→ iOS ObjC++ TurboModule +→ 当前 Fabric / MLRN MapView +→ public MLNMapView / MLNParticleFieldRuntime +→ Shared Core / Renderer / Metal + +不得建立 iOS 第二套 JS API、第二套 Map registry、第二套 QBIN/Field contract。 + +--- + +## 1. 正式跨项目依据 + +开始前必须以以下正式约束为准,不以聊天记忆替代: + +- Integration repo HEAD:`39ad8415fbdb76c1822460bb09f3a3f35fe141e0` +- Contract:`C-001` +- `contracts/PARTICLE_NAVSEA_BOUNDARY.md` +- `rfc/RFC-001_RN公共API与接入基线.md` +- `coordination/TEAM_MODEL.md` +- `WORKFLOW.md` + +RFC-001 已接受的关键事实: + +1. 当前公共 RN 包为 `@navsea/maplibre-particle-rn@13.4.1-particle.5`。 +2. Map / Camera 继续由 `@maplibre/maplibre-react-native@11.3.6` 提供,Particle 包不得转导出或复制 MapView。 +3. Particle 是纯 Field Renderer 控制面,不负责 provider、HTTP、缓存、Route/ETA 或 NavSea 业务。 +4. Android 当前已有 RN bridge + Native + QBIN V4;iOS RN 在本任务前未实现。 +5. Android / iOS 必须共享同一 Shared Core 与同一公共 RN 语义;不得平台分叉。 + +Integration 的 `PARTICLE_IOS_STATUS.md` / compatibility 在本任务创建时仍保留 STEP-082 验证前的旧状态,但 Particle 仓库实际 STEP-082 已于当前 HEAD 正式 CLOSED / PASS。不得因为 Integration 状态文件滞后而重新打开 STEP-082。 + +--- + +## 2. 起始 Git 基线 + +仓库: + +`/Users/tei/dev/MapLibreParticleLab` + +分支: + +`particle/main` + +起始 HEAD / origin: + +`44af5e68b8569d90c26e37c2a5ae5493a5b485eb` + +STEP-082 关键提交: + +- implementation blocker close: + `136ed726cf749c24b3673c046581a38ef48be390` +- STEP-082 final closeout: + `44af5e68b8569d90c26e37c2a5ae5493a5b485eb` + +STEP-082 已验证发行产物: + +- `bazel-bin/platform/ios/MapLibre.dynamic.xcframework.zip` +- distributionVersion:`6.28.0-particle.1` +- module:`MapLibre` +- size:`24639200` bytes +- SHA-256: + `64faa174c5c0dc0a2da3e289f0beaf2dedf7298665e3345f39836729b668e239` +- device:arm64 +- simulator:arm64 + x86_64 +- public `MLNParticleFieldRuntime.h`:两个 slice 均存在 +- `Mapbox.h`:两个 slice 均导入该 header +- `MLNMapView_Private.h`:public export count = 0 +- ObjC class symbols:device/simulator 均存在 + +必须先阅读: + +- `docs/50_Step记录/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_v1.md` +- `docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v2.md` +- `release/maplibre-particle/ios/release-manifest.json` +- `platform/ios/src/MLNParticleFieldRuntime.h` +- `platform/ios/src/MLNParticleFieldRuntime.mm` + +--- + +## 3. 当前 RN 公共 API 是冻结基线 + +以下文件是本任务必须兼容的公共面: + +- `packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts` +- `packages/maplibre-particle-rn/src/FieldRuntime.ts` +- `packages/maplibre-particle-rn/src/types.ts` +- `packages/maplibre-particle-rn/src/EncodedFieldGuards.ts` + +当前 Android reference implementation: + +- `packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt` +- `WeatherRuntimeBridgePackage.kt` +- 当前 JNI / WeatherRuntime Android 实现与 guard/error 语义 + +### 3.1 硬性一致性要求 + +本任务原则上不得改变: + +- TurboModule 名称:`WeatherRuntimeBridge` +- `NativeWeatherRuntimeBridge.ts` 方法名 +- 参数顺序与 primitive ABI +- TypeScript public `FieldRuntime` 方法名 +- `types.ts` public union / enum 字符串 +- V4 submit result 字符串 +- V3 submit result 字符串 +- `CommandResult = ACCEPTED | REJECTED` +- JS 层 encoded guard 语义 +- V4 contract JSON field names +- inactive selector `(0,0)` ABI +- `reactTag` 作为 MapView 入口的调用方式 + +不得让 NavSea / Test Host 写: + +`Platform.OS === "ios" ? iOS专用API : AndroidAPI` + +也不得新增第二套 iOS JS module。 + +### 3.2 JS 文案允许修正 + +`FieldRuntime.ts` 当前 `nativeBridge()` 的错误文本仍写死 “requires the Android WeatherRuntimeBridge native module”。 + +iOS 模块完成后必须改成平台中性的错误文本,但不得改变 API 类型或成功路径。 + +--- + +## 4. 第一阶段:真实代码调查,禁止凭猜测实现 + +在写 iOS bridge 前必须完成一次短而完整的只读调查,并在 v1 完成报告中记录结果。 + +### 4.1 调查当前 MLRN 11.3.6 + +必须取得并检查 `@maplibre/maplibre-react-native@11.3.6` 的真实 iOS source / podspec / codegen 配置。 + +可通过当前 Test Host 安装依赖、npm pack 或其他不会改变产品版本锁的方式取得源码。 + +必须确认: + +1. Fabric 下 MapView ComponentView 的实际 ObjC/ObjC++ 类名。 +2. `reactTag` 在 RN 0.85.2 / Fabric 中如何从当前 surface/UIManager 安全 resolve ComponentView。 +3. MLRN ComponentView 内实际持有/暴露的 content view 类型。 +4. 如何从该 content view 获得真正的 public `MLNMapView`。 +5. 哪些接口是 MLRN public/稳定 seam,哪些是私有实现。 +6. MLRN 11.3.6 的 MapLibre iOS binary dependency 方式。 +7. 当前版本是否支持 `$MLRN_SPM_SPEC` 或等价官方/仓库已有 override;必须以真实 podspec/source 为准,不得凭记忆发明变量。 + +### 4.2 调查 RN 0.85.2 TurboModule iOS codegen + +必须检查当前 RN 0.85.2 codegen 生成的 iOS Spec / base class pattern,确认: + +- ObjC++ module class声明/继承方式; +- Promise resolve/reject ABI; +- module provider / codegen registration; +- 新架构 Fabric / TurboModule 的实际编译入口; +- package podspec 需要包含哪些 generated files / dependency。 + +### 4.3 做一张 parity matrix + +在完成报告中列出当前 `NativeWeatherRuntimeBridge.ts` 每一个方法: + +- Android 当前实现 +- iOS 可映射到哪个 public Native/Core seam +- 是否需要扩展 `MLNParticleFieldRuntime` +- 最终 parity 状态 + +不得只实现 Wind V4 后声称“接口一致”。 + +--- + +## 5. 第二阶段:iOS TurboModule 与 Fabric reactTag → MLNMapView + +### 5.1 新增 iOS package 实现 + +在 `packages/maplibre-particle-rn/` 中增加正式 iOS native implementation,并补齐 RN package iOS autolinking 所需文件。 + +预期包括但不限于: + +- iOS ObjC++ TurboModule implementation +- 必要的 package podspec / codegen 接线 +- `package.json files` 纳入 iOS / podspec +- `react-native.config.js` 正确支持 iOS(若当前 RN autolinking 实际需要) +- README / CHANGELOG 更新 + +实际文件名必须遵循当前 RN 0.85.2 codegen 生成基类和模块注册要求,不得照旧版 RN 模板硬套。 + +### 5.2 reactTag 规则 + +每一个需要 Map 的 native command 都必须: + +1. 验证 reactTag 为正整数; +2. 在 Main/UI thread 执行 view lookup; +3. 从当前 Fabric surface/UIManager 重新 resolve reactTag; +4. 确认它确实是当前 MLRN MapView ComponentView/content view; +5. 获得当前真实 `MLNMapView`; +6. 再调用 MapView-owned Particle runtime。 + +禁止: + +- 缓存 reactTag → map 指针; +- 建立全局/静态 Map registry; +- 缓存跨 style / surface 生命周期的 `MLNMapView *`; +- 通过 private Core 指针绕过 public MLN facade; +- 用 Test App private seam 作为 RN 正式 API。 + +### 5.3 Map 生命周期 + +`attach` 必须对应 Android 现有语义: + +- 当前 Map 存在; +- style ready; +- 配置要求的 `raster-array` source / `weather-color` / `raster-particle` 已存在并类型正确; +- attach/re-attach 的 replace/dispose 语义稳定; +- runtime 由 MapView 持有; +- bridge 不持有第二份 runtime owner。 + +`detach`: + +- 对当前 MapView 的 runtime 执行 dispose; +- 不销毁 MapView; +- 不缓存 stale runtime。 + +后续每条 command 都必须针对当前 MapView/current runtime执行,style reload 后按 Native facade fail-closed。 + +--- + +## 6. 第三阶段:Android ↔ iOS 公共行为完整对齐 + +### 6.1 V4 必须完整实现 + +至少包括: + +- `submitEncodedField` +- `setEncodedColorBinding` +- `setEncodedParticleBinding` +- `setEncodedForecastTime` +- `setEncodedForecastPlaybackState` +- `setEncodedForecastPlaybackSpeed` +- `clearEncodedField` +- `clearAllEncodedFields` + +QBIN 必须从本地绝对 path / `file://` URI读取为 bytes,再交给 `MLNParticleFieldRuntime`;JS 不解码 QBIN。 + +文件 guard 必须与 Android 当前 contract 等价: + +- local file only; +- readable regular file; +- 1..64 MiB; +- read 时防文件长度变化; +- contract JSON 上限与 encoded ID UTF-8 byte guard保持一致; +- 不使用 Base64 / JS number[]。 + +### 6.2 V3 与当前公共控制面也必须审计并实现 parity + +当前 RN public Spec 不只有 V4。 + +必须逐项处理: + +- `submitWindField` +- `submitField` +- `clearWindFields` +- `clearFields` +- `getState` +- `setFieldValueSource` +- `setParticleAdvectionScale` +- `setWeatherEnabled` +- `setWeatherColorVisible` +- `setParticleVisible` +- `setTargetFps` +- `setForecastTime` +- `setPlaybackState` +- `setPlaybackSpeed` +- `setWeatherColorStops` +- `setParticleColor` +- `setTrailColor` +- `setParticleCount` +- `setParticleLineWidth` +- `setParticleLength` +- `setParticleOpacity` +- `setTrailOpacity` +- `setTrailFade` +- `setWeatherColorOpacity` + +原因:RFC-001 当前 NavSea 事实仍包含 Wind V4 + Wave/Rain/Tide V3;不能靠 iOS-specific JS branch规避。 + +### 6.3 如果 082 facade 覆盖不足 + +当前 `MLNParticleFieldRuntime` 主要冻结 V4 submit/binding/forecast/clear。 + +如 parity audit 证明 V3 / state / visual control 在 iOS 已有 Shared Core 能力但没有 public ObjC facade: + +- 允许在本 STEP-083 内最小扩展 `MLNParticleFieldRuntime.h/.mm`; +- 仅做现有 Core 能力的薄 public facade; +- Android API/Shared Core contract不改变; +- 不把 provider、NavSea、RN 类型写进 Core; +- 仍保持每次 command current-style lookup 与 fail-closed; +- public header 不泄漏 C++/STL/private Core 类型。 + +如某个现有 RN public method 在 iOS 根本没有对应 Shared Core 语义,且实现它必须新增/改变跨平台公共协议: + +**停止扩大该部分并在报告中明确 BLOCKED。不得:** + +- 返回假的 ACCEPTED; +- 写 no-op stub; +- iOS 永久返回 REJECTED 却仍宣称 parity; +- 在 JS 用 Platform.OS 绕过; +- 私自创建 iOS-only public protocol。 + +需要新的跨平台语义时必须回到 Integration RFC/Contract。 + +--- + +## 7. Error / Promise 语义对齐 + +以 Android `WeatherRuntimeBridgeModule.kt` 为 reference。 + +iOS 必须尽量保持相同 reject code 和触发条件: + +- `INVALID_ARGUMENT` +- `MAP_VIEW_NOT_FOUND` +- `MAP_NOT_READY` +- `STYLE_NOT_READY` +- `RUNTIME_DISPOSED` +- `SOURCE_MISSING` +- `NATIVE_REJECTED` +- 以及当前 attach / weather-object readiness 已有的稳定错误码 + +Core V4 submit rejection 仍作为 **resolved `EncodedFieldSubmitResult`** 返回,不得错误地全部转成 Promise reject。 + +Facade/lifecycle/file/ABI 错误使用 Promise reject。 + +同类输入在 Android 与 iOS 的成功/reject类别不得无理由分叉。 + +--- + +## 8. XCFramework / MapLibre 单实例消费 + +这是本任务的关键发行门槛之一。 + +### 8.1 只能有一个 MapLibre.framework + +MLRN 与 Particle iOS bridge 必须消费 **同一个** STEP-082 custom MapLibre module/product。 + +禁止: + +- MLRN 拉官方 MapLibre,而 Particle bridge再静态/动态链接另一份 custom MapLibre; +- App 中出现两个 `MapLibre.framework`; +- 两份 ObjC class runtime; +- 私自 rename module 绕开 duplicate symbol。 + +### 8.2 使用真实 MLRN 11.3.6 支持的 override + +先调查 MLRN 11.3.6 podspec / SPM consumer。 + +如果真实支持 `MLRN_SPM_SPEC`,按其实际格式接 STEP-082 custom distribution。 + +如果实际版本使用其他机制,按真实机制实现,并在报告记录。 + +不能凭聊天记忆写一个未被 MLRN 读取的环境变量。 + +### 8.3 本地验证允许使用 STEP-082 artifact + +当前 artifact 尚未远端发布,manifest URL 为 `LOCAL_BUILD`。 + +本任务测试阶段允许用本地 `MapLibre.dynamic.xcframework.zip` / 解包后的 XCFramework 做可重复的 local consumer 验证。 + +不得伪造远端 URL。 + +远端正式发布可以后续单独处理;但本 STEP 必须证明 iOS RN/Test Host 能消费同一 custom MapLibre binary。 + +--- + +## 9. RN package 版本规则 + +本任务首先完成实现与验证,不为了“有 iOS 文件”立即发布新 npm。 + +当前 `13.4.1-particle.5` 与 Android Native `.5` 是 RFC-001 正式发布基线。 + +除非任务过程中发现已有正式 release 流程明确要求同步 bump,否则: + +- 不擅自发布 `.6`; +- 不让 RN package version 与 Android Native strict lock失配; +- 可以在工作树/Test Host 用 source package验证 iOS。 + +最终若需要新跨平台发行版本,在 STEP-083 验证通过后单独记录 release decision。 + +--- + +## 10. Test Host:同一 JS 代码验证 Android/iOS + +优先复用: + +`integration/react-native/MapLibreParticleRNTestHost/` + +当前它没有生成 iOS 工程。 + +本任务应让它具备 iOS validation 能力,但不要复制第二份业务 App。 + +### 10.1 JS 层不得另写 iOS 页面 + +同一个 `App.tsx` / `FieldRuntime` 调用路径用于 Android 与 iOS。 + +允许平台专属的 build/config/native files,不允许平台专属业务调用。 + +### 10.2 iOS 最小验收内容 + +至少准备并尽可能实际执行: + +1. dependency install; +2. RN codegen; +3. TypeScript typecheck; +4. iOS project/prebuild/pods 或实际当前工具链对应生成流程; +5. simulator 或 device Debug build; +6. module link/launch smoke; +7. Map reactTag attach; +8. V4 Wind Golden T0/T1 submit; +9. color/particle binding; +10. forecast time midpoint; +11. PLAY/PAUSE/speed; +12. clear selected / clear all; +13. detach/re-attach; +14. style/source not ready fail-closed; +15. 验证没有 duplicate MapLibre framework/class。 + +如真实运行需要用户手工设备操作,Codex必须把代码、测试入口和明确验收步骤准备完整;不得把未运行项目写 PASS。 + +--- + +## 11. 允许修改范围 + +主要允许: + +- `packages/maplibre-particle-rn/src/`(仅为平台中性错误文本/必要 codegen 修正;不得分叉 public API) +- `packages/maplibre-particle-rn/ios/` 新增 +- `packages/maplibre-particle-rn/*.podspec` / package/autolinking配置 +- `packages/maplibre-particle-rn/package.json`(仅 package files/codegen/build必要项;默认不 bump version) +- `packages/maplibre-particle-rn/react-native.config.js` +- `packages/maplibre-particle-rn/README.md` +- `packages/maplibre-particle-rn/CHANGELOG.md` +- `integration/react-native/MapLibreParticleRNTestHost/` 的 iOS build/config/test入口 +- 如 parity 必需:`platform/ios/src/MLNParticleFieldRuntime.h/.mm` 的薄 facade 扩展 +- 必要 iOS Bazel/public header build graph,前提是不改变 Shared Core contract +- STEP-083 设计/记录/报告文件 + +不得修改: + +- NavSea 仓库 +- Android public RN API签名 +- Android Kotlin/JNI行为,除非只是无语义的测试/编译兼容且报告说明;默认不改 +- QBIN/FieldProfile/BindingProjection contract +- Renderer/Metal particle physics +- provider/network/cache/Route/ETA +- STEP-081/082 已关闭事实 +- unrelated submodule / user dirty + +--- + +## 12. 必须保护的现有 dirty / untracked + +以下必须原样保护,不得暂存或提交: + +- `docs/doxygen/doxygen-awesome-css` +- `platform/ios/app/MBXAppDelegate.m` +- `platform/windows/vendor/vcpkg` + +现有未跟踪任务书也不得删除、改写、提交: + +- `docs/40_任务/STEP-082_XCFramework_Bazel_Codegen阻塞收尾_Codex任务_v1.md` +- `docs/40_任务/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex任务_v1.md` + +本 STEP-083 任务书自身默认也不要求提交。 + +禁止: + +- `git add -A` +- `git add .` +- `git reset --hard` +- `git clean` + +只允许 scoped stage。 + +--- + +## 13. 验证要求 + +### 13.1 静态/代码 + +至少: + +- package TypeScript typecheck +- RN Codegen 成功生成 iOS/Android Spec,且 Android ABI未意外变化 +- iOS ObjC++ compile / Xcode build能证明 module与 generated spec匹配 +- podspec lint/解析或等价 CocoaPods 集成检查 +- `git diff --check` +- package tarball 内容检查:必须包含 iOS native files / podspec,不丢现有 Android/src + +编译输出遵循全局 `$build-output-throttle`。 + +### 13.2 公共 API parity + +完成报告必须逐项证明: + +- public TS API无 iOS新增/删减 +- Native Spec无平台分叉 +- Android既有 API仍可 codegen +- iOS module实现同一 Spec +- 关键 error code/submit result映射一致 +- Test Host JS 不需要 Platform.OS分支 + +### 13.3 MapLibre binary identity + +必须检查最终 iOS build graph: + +- 只解析一个 MapLibre module/product; +- 实际链接的是 STEP-082 custom binary; +- `MLNParticleFieldRuntime` symbol可见; +- 无 duplicate framework/class warning; +- MLRN 与 Particle bridge看到同一 ObjC runtime class。 + +--- + +## 14. 执行策略 + +这是复杂任务,可以在一个 STEP 内分“调查 → 实现 → 构建/验证 → 收尾”四个阶段,但不要人为拆成 STEP-083A/B/C/D。 + +遇到小型编译/Pod/codegen/headers问题,Codex继续最小修复并验证。 + +只有遇到以下情况才停止: + +1. 需要改变当前 RN public API; +2. 需要改变 C-001 Shared Core contract; +3. 需要新增跨平台 semantic/error/lifecycle协议; +4. MLRN 11.3.6 无法在不双链接 MapLibre 的情况下消费 custom binary,需要新的架构决定。 + +停止时写明确 blocker,不得用 stub/Platform.OS/第二 registry绕过。 + +--- + +## 15. Git 与文档输出 + +实质进展后更新: + +- `粒子开发记录.md` +- 新建: + `docs/50_Step记录/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_v1.md` + +Codex完成报告固定生成: + +`docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v1.md` + +报告至少写: + +- 调查结论 +- parity matrix +- MLRN 11.3.6 Fabric lookup真实路径 +- RN 0.85.2 TurboModule registration真实路径 +- package/podspec改动 +- Native facade扩展(如有) +- custom MapLibre single-binary消费方式 +- typecheck/codegen/build/test结果 +- 未实际运行项 +- user dirty保护情况 +- commit hash / message +- push result +- remote branch +- Final HEAD + +在合适检查点做 scoped commit;完成范围才提交,不要带入 unrelated dirty。 + +最终 push: + +`origin/particle/main` + +--- + +## 16. CLOSED / PASS 门槛 + +只有全部成立才能标记 STEP-083 `CLOSED / PASS`: + +- iOS native module 实现现有同一 `NativeWeatherRuntimeBridge` Spec; +- JS/TS public API 不分叉; +- reactTag → 当前 Fabric/MLRN view → public MLNMapView 链真实成立; +- bridge 不缓存 Map/Core pointer,不建立第二 registry; +- V4 入口完整; +- V3 + 当前公共控制面 parity audit 完成,所有方法真实实现或已由正式跨平台决定处理; +- iOS package autolinking/codegen可用; +- MLRN 与 bridge只消费一个 STEP-082 custom MapLibre; +- iOS compile/link成功; +- Test Host 至少完成真实 module/link/attach smoke;QBIN/Field功能未实际运行的项目不得伪报 PASS; +- `git diff --check` PASS; +- 完成报告与 Step记录更新; +- scoped commit/push成功; +- local HEAD == `origin/particle/main`; +- 三处用户 dirty和历史任务书未触碰。 + +若代码完整但真实 iOS runtime/device验收尚未完成,应写: + +`IMPLEMENTED / WAITING_IOS_VALIDATION` + +不得提前写 CLOSED/PASS。 diff --git a/docs/40_任务/STEP-088_静态底图缓存PoC与Renderer观测_Codex任务_v1.md b/docs/40_任务/STEP-088_静态底图缓存PoC与Renderer观测_Codex任务_v1.md new file mode 100644 index 000000000..ebb667032 --- /dev/null +++ b/docs/40_任务/STEP-088_静态底图缓存PoC与Renderer观测_Codex任务_v1.md @@ -0,0 +1,357 @@ +# STEP-088 静态底图缓存 PoC 与 Renderer 可观测性 Codex 任务 v1 + +## 1. 任务目标 +基于最新 origin/particle/main 实际代码,实现第一阶段 Retained Static Underlay PoC。 + +核心目标: +- Camera 固定、Style/Tile 稳定、存在 WeatherColor/Particle 时,把动态层之前连续普通 LayerGroup 前缀 render 到 offscreen texture。 +- 后续 Particle Tick 继续走当前 onUpdate/createRenderTree,但不再重复 draw 静态前缀,而复用 cached underlay texture。 +- Particle simulation/render 继续按 cadence 正常。 +- 增加 Renderer 可观测性。 +- 本任务只执行 STEP-088,不得执行 STEP-089/090。 + +## 2. 执行基线 +直接在主仓库: +/Users/tei/dev/MapLibreParticleLab + +目标主线: +origin/particle/main + +执行前: +1. git fetch origin particle/main。 +2. 确认本地 HEAD、origin/particle/main、git status。 +3. 不 reset/clean,不覆盖用户既有未提交修改。 +4. 如存在无关本地修改,只修改、stage、commit STEP-088 相关文件。 +5. 以当前实际代码为准,不得凭旧版本猜接口。 + +必须阅读: +- docs/40_任务/粒子连续渲染静态底图缓存与动画快路径优化规划_v1.md +- 本任务书 +- docs/20_调查/STEP-086_Particle连续渲染唤醒MapLibre整图成本调查_v1.md +- docs/00_项目总则/ +- docs/00_项目规范/ +- 根目录 AGENTS.md(如存在) + +## 3. 开工前只读确认 +必须确认并在完成报告记录: +- RenderRasterParticleLayer::needsRepaint() +- RenderWeatherColorLayer::needsRepaint() +- Renderer::render() +- RenderOrchestrator::createRenderTree() +- RenderOrchestrator::hasTransitions() +- renderer_impl.cpp 的 updateLayers/processChanges +- LayerGroup tweaker/upload +- visitRenderTargets +- main clear +- opaque/translucent pass +- present +- RenderTarget / OffscreenTexture +- Heatmap/Hillshade offscreen + texture composite 样板 + +涉及文件至少: +- src/mbgl/renderer/layers/render_particle_validation_layer.cpp +- src/mbgl/renderer/layers/render_weather_color_layer.cpp +- src/mbgl/renderer/renderer.cpp +- src/mbgl/renderer/render_orchestrator.cpp/.hpp +- src/mbgl/renderer/renderer_impl.cpp/.hpp +- include/mbgl/renderer/render_target.hpp +- src/mbgl/renderer/render_target.cpp +- src/mbgl/renderer/layers/render_heatmap_layer.cpp +- src/mbgl/renderer/layers/render_hillshade_layer.cpp + +## 4. 修改一:retained dynamic boundary +重点修改: +- src/mbgl/renderer/render_layer.hpp +- src/mbgl/renderer/layers/render_weather_color_layer.hpp +- src/mbgl/renderer/layers/render_raster_particle_layer.hpp + +增加仅 Renderer 内部使用的虚函数,语义类似: +virtual bool startsRetainedDynamicRange() const { return false; } + +WeatherColor 与 RasterParticle override=true。 + +规则: +- 两者同时存在取最小实际 layerIndex。 +- only Particle 使用 Particle index。 +- 无 candidate 时完全走原 Renderer。 +- 禁止根据 React layer ID/name 字符串硬编码。 +- 禁止让 NavSea/Host 传 boundary。 +- 不新增 public API。 + +## 5. 修改二:RenderOrchestrator LayerGroup 分段访问 +重点修改: +- src/mbgl/renderer/render_orchestrator.hpp +- src/mbgl/renderer/render_orchestrator.cpp + +必要时小改: +- include/mbgl/renderer/layer_group.hpp +- src/mbgl/renderer/layer_group.cpp + +需要: +- 查询 retained dynamic boundary。 +- 遍历 boundary 之前 groups。 +- 遍历 boundary 及之后 groups。 +- forward/reverse 都支持。 +- callback 能获得完整原始 LayerGroup 序列 ordinal。 + +可采用 before/from visitor 或更通用 indexed/range visitor。 + +关键: +parameters.currentLayer、depthRangeSize、opaque/translucent ordering 必须与完整遍历等价。 +不得让 dynamic subset 从 0 重新编号导致 depth/transparency 变化。 + +## 6. 修改三:Renderer-owned RetainedUnderlayCache +优先新增: +- src/mbgl/renderer/retained_underlay_cache.hpp +- src/mbgl/renderer/retained_underlay_cache.cpp + +并在 CMakeLists.txt 真实 source list 注册。 + +至少维护: +- offscreen renderable / texture +- viewport size +- boundary +- valid +- buildCount +- reuseCount +- fallbackCount +- last invalidation reason + +不要修改 Heatmap/Hillshade RenderTarget 原有语义。 + +STEP-088 至少保证: +- resource 创建失败 -> full-render fallback +- viewport resize -> invalid +- boundary change -> invalid +- has3D -> fallback +- Debug Overdraw 等明显不兼容模式 -> fallback +- context 不可用 -> 不复用 +- Camera fingerprint 连续相同、style loaded、2D、boundary stable 才允许 reuse + +任何不能证明安全的状态都 full render。 + +## 7. 修改四:Renderer::Impl 分离 Static Underlay / Dynamic Range +重点: +- src/mbgl/renderer/renderer_impl.hpp +- src/mbgl/renderer/renderer_impl.cpp + +STEP-088 必须保留: +- Renderer::render -> createRenderTree +- source update/prepare +- layer prepare +- updateLayers +- LayerGroup tweaker/upload + +只修改真正 draw 阶段。 + +目标流程: +1. existing independent RenderTargets。 +2. cache invalid 时,boundary 前 LayerGroups render 到 offscreen 并 mark valid。 +3. main clear。 +4. composite cached underlay texture。 +5. boundary 及之后 opaque/translucent 正常 render。 +6. legacy items 按安全规则。 +7. debug。 +8. present。 + +Underlay offscreen 优先透明 clear。 +main framebuffer 保持原 backgroundColor/depth/stencil 语义。 +必须确认 background layer optimized-as-color 路径。 + +## 8. Legacy RenderItems +renderer_impl.cpp translucent pass 仍有 legacy layerRenderItems。 + +必须确认是否有 boundary 以下 legacy item。 +- 无法安全按 layerIndex 切分时不纳入 cache。 +- 可能破坏 ordering 时 fallback full render。 +- 不得静默改变顺序。 + +## 9. 修改五:cached texture compositor +不要正式复用 LocationIndicatorTexturedShader。 + +建议新增: +RetainedUnderlayCompositeShader + +职责: +- viewport/fullscreen quad +- sample cached RGBA +- 正确 premultiplied alpha +- 不做 color ramp +- 不做 map transform +- 不做 Particle 业务 + +按当前真实 shader 注册规则检查/修改至少: +- include/mbgl/shaders/shader_source.hpp +- src/mbgl/shaders/shader_source.cpp +- include/mbgl/shaders/shader_defines.hpp +- include/mbgl/shaders/shader_manifest.hpp +- include/mbgl/shaders/gl/shader_info.hpp +- src/mbgl/shaders/gl/shader_info.cpp +- src/mbgl/gl/renderer_backend.cpp +- src/mbgl/mtl/renderer_backend.cpp +- cmake/opengl.cmake +- cmake/metal.cmake +- 新增 GL/Metal compositor shader 文件 + +如当前 shader codegen 要求生成文件,按真实 codegen 走。 +Android OpenGL 与 iOS Metal 语义都要完整。 + +## 10. Renderer Debug Counters +修改: +- include/mbgl/particle/renderer_debug_counters.hpp + +至少新增: +- FullRendererFrame +- RetainedUnderlayBuild +- RetainedUnderlayReuse +- RetainedUnderlayFallback +- RetainedUnderlayInvalidation +- RetainedUnderlayStaticGroupDraw +- RetainedDynamicGroupDraw + +要求: +- NDEBUG 下 no-op +- 不增加 Host API +- 不每帧刷大量日志 + +允许有限首次日志:cache enabled/build/fallback reason。 + +## 11. Native 定向测试 +不新增或运行 Jest。 + +至少覆盖: +Boundary: +- 无 Weather/Particle -> disabled +- only Particle +- Weather + Particle 不同顺序 + +LayerGroup range: +- before/from +- forward/reverse +- original ordinal + +Cache: +- first build +- reuse +- resize invalid +- boundary change invalid +- unsupported 3D fallback + +Failure: +- compositor/resource 创建失败 -> safe fallback/no crash + +测试放当前 renderer test 真实结构,不新造孤立测试框架。 + +## 12. Mac 编译与验证 + +本任务采用仓库 AGENTS.md 的固定闭环: + +`Codex 写代码 → RDC 执行编译/测试 → 失败则把错误证据交回 Codex 修复 → RDC 再验证` + +Codex 本轮负责: +- 完成 Renderer / OpenGL / Metal 相关源码与测试代码实现; +- 给出 RDC 需要执行的精确命令; +- 不自行启动、等待或轮询长时间编译/测试; +- RDC 返回失败日志后,继续分析并修复代码。 + +由 RDC / 普通 Terminal 实际执行: +- 编译受影响 Renderer C++ target; +- 编译 OpenGL compositor shader 相关 target; +- 编译 Metal compositor shader 相关 target; +- 运行直接相关 C++ 定向测试; +- git diff --check。 + +RDC 返回结果时: +- 成功:返回 exit code、耗时、target 摘要; +- 失败:返回首个真实 error、相关 target、必要上下文和完整日志路径; +- 不把完整大型 build log 反复送入 Codex。 + +明确禁止 Codex 执行: +- CMake / Ninja / Bazel 长时间编译; +- OpenGL / Metal 长时间构建等待; +- Android Gradle build; +- AAR/APK build/install; +- ADB; +- Android App 启动/清数据; +- Android 真机/虚拟机; +- Jest。 + +Android 最终由用户手工验证。 + +## 13. PoC 验收证据 +理想 fixed Camera + Particle ~30 FPS: +- RetainedUnderlayBuild ≈ 1 +- RetainedUnderlayReuse 持续增长 +- ParticleSimulationAdvance 持续增长 +- RetainedUnderlayStaticGroupDraw 建 cache 后不随 Particle FPS 增长 +- RetainedDynamicGroupDraw 持续增长 + +如 Mac 无法跑完整 Particle runtime: +- 完成 Native 定向/静态验证 +- 给出用户最少 Android 手工验证项 +- 最终状态必须:IMPLEMENTED / WAITING FOR ANDROID VALIDATION +- 不得写 CLOSED / PASS + +## 14. 本任务明确不做 +- production 全部 tile/style/placement invalidation +- Map::Impl::onUpdate fast path +- 跳过 createRenderTree +- 跳过 source->update +- 跳过 TilePyramid::update +- 跳过普通 prepare +- 新 Host API +- NavSea React 修改 +- AIS 专项优化 +- 通用任意 static/dynamic RenderGraph +- STEP-089 +- STEP-090 + +## 15. 文档输出 +必须生成: +- docs/50_Step记录/STEP-088_静态底图缓存PoC与Renderer观测_v1.md +- docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v1.md + +如同主题已有,递增版本,不覆盖旧报告。 + +Step 记录至少写: +- 当前问题 +- 实现结构 +- boundary +- underlay cache flow +- PoC 限制 +- 用户 Android 手工验证项 + +完成报告必须写: +- 起始 HEAD / 最终 HEAD +- 修改文件 +- boundary 实现位置 +- LayerGroup 分段实现位置 +- cache/compositor 实现位置 +- debug counters +- 测试/编译结果 +- Android 未执行项 +- fallback +- 是否满足进入 STEP-089 条件 +- final commit hash +- commit message +- push result +- remote branch +- Final HEAD + +## 16. Git 收尾 +完成后: +1. 只 stage/commit STEP-088 相关文件。 +2. 不提交当前主仓库既有无关 dirty/untracked 内容。 +3. commit message 建议:feat: add retained underlay render cache poc +4. push 到 origin/particle/main。 +5. push 前 fetch;远端前进时安全整合本任务。 +6. push 失败不得声称完成。 +7. 最终确认任务提交已在 origin/particle/main。 + +## 17. 最终响应 +直接逐项报告: +- 已完成 +- 未完成 +- 无法验证 + +不要再输出实施计划。 diff --git a/docs/40_任务/粒子连续渲染静态底图缓存与动画快路径优化规划_v1.md b/docs/40_任务/粒子连续渲染静态底图缓存与动画快路径优化规划_v1.md new file mode 100644 index 000000000..113c409ad --- /dev/null +++ b/docs/40_任务/粒子连续渲染静态底图缓存与动画快路径优化规划_v1.md @@ -0,0 +1,156 @@ +# MapLibreParticleLab 粒子连续渲染静态底图缓存与动画快路径优化规划 v1 + +## 1. 目标 +STEP-086 已确认:Particle / WeatherColor 的 needsRepaint 会通过全局 repaint 让完整 MapLibre scene 连续出帧。React 不会每帧重新提交 PBF,但 Native Renderer 会重复进入 source/layer orchestration,并重新提交静态 PBF/fill/line/symbol draw。 + +本规划目标: +- 地图真实变化时仍走 Full Frame。 +- Camera、Style、Tile 稳定且只有 WeatherColor/Particle 动画时,复用 Static Underlay。 +- NavSea / React 不参与 Renderer cache 管理。 +- 不改变 NavSea ↔ Particle C-001 公共边界。 + +## 2. 当前关键代码 +必须以当前 origin/particle/main 实际代码为准,重点包括: +- src/mbgl/renderer/layers/render_particle_validation_layer.cpp +- src/mbgl/renderer/layers/render_weather_color_layer.cpp +- src/mbgl/renderer/render_orchestrator.cpp/.hpp +- src/mbgl/renderer/renderer.cpp +- src/mbgl/renderer/renderer_impl.cpp/.hpp +- src/mbgl/map/map_impl.cpp +- include/mbgl/renderer/render_target.hpp +- src/mbgl/renderer/render_target.cpp +- src/mbgl/renderer/layers/render_heatmap_layer.cpp +- src/mbgl/renderer/layers/render_hillshade_layer.cpp + +## 3. 核心结构 +不增加永久 public static/dynamic layer flag。采用 Renderer 内部 retained dynamic boundary: +- WeatherColor 是 boundary candidate。 +- RasterParticle 是 boundary candidate。 +- 同时存在时取最小 layerIndex。 +- boundary 之前连续 LayerGroup 前缀作为 Static Underlay。 +- boundary 及之后继续实时绘制。 +- 没有 boundary 时完全保持原 Renderer。 + +Layer 顺序、parameters.currentLayer、depthRangeSize、opaque/translucent 原始 ordinal 必须保持,不允许对子集从 0 重新编号而改变透明/depth 语义。 + +## 4. Static Underlay Cache +建议 Renderer-owned helper: +- src/mbgl/renderer/retained_underlay_cache.hpp +- src/mbgl/renderer/retained_underlay_cache.cpp + +职责: +- offscreen renderable / texture +- viewport size +- boundary +- valid/invalid +- invalidation reason +- build/reuse/fallback 计数 +- compositor resources + +不改变 Heatmap/Hillshade RenderTarget 现有行为;复用现有 gfx::OffscreenTexture / RenderTarget / Drawable / shader abstraction。 + +## 5. Texture Composite +使用独立语义的 RetainedUnderlayCompositeShader: +- viewport/fullscreen quad +- sample cached RGBA texture +- 正确 premultiplied alpha +- 不做 map transform / color ramp / Particle 业务 + +不要长期复用 LocationIndicatorTexturedShader。 + +## 6. STEP-088 +先只消除静态地图重复 GPU draw,仍保留: +- Map::Impl::onUpdate +- UpdateParameters +- createRenderTree +- source update / TilePyramid update +- source/layer prepare +- updateLayers +- LayerGroup tweaker/upload + +Render 阶段: +1. cache invalid:boundary 前 LayerGroups render 到 offscreen。 +2. cache valid:跳过 boundary 前静态 draw。 +3. main framebuffer clear。 +4. composite cached underlay。 +5. boundary 及之后 WeatherColor/Particle/动态 overlay 正常绘制。 +6. present。 + +## 7. STEP-089 +在 STEP-088 实机通过后再做 production invalidation: +- Camera center/zoom/bearing/pitch +- viewport/pixelRatio +- style/layer add/remove/reorder/change +- source/tile/bucket/drawable 变化 +- sprite/image/glyph +- symbol placement/fade +- context/surface recreation +- 3D/terrain/custom/debug fallback +- OpenGL/Metal 一致生命周期 + +## 8. STEP-090 条件步骤 +只有 STEP-089 后真实 NavSea profile 仍证明主要 CPU 落在: +- Map::Impl::onUpdate +- createRenderTree +- Vector/PBF source update +- TilePyramid update +- source/layer prepare +才进入 Animation-only fast path。 + +如果 STEP-089 已满足性能目标,不执行 STEP-090。 + +## 9. STEP-088 安全边界 +PoC reuse 至少要求: +- 2D +- Camera fingerprint 连续一致 +- viewport 不变 +- style loaded +- boundary 不变 +- context/resource 正常 + +以下至少 fallback full render: +- has3D +- Debug Overdraw +- cache texture/compositor 创建失败 +- viewport resize +- boundary change +- context 不可用 +- legacy RenderItem ordering 无法证明安全 +- 其他不能证明 cache 正确的状态 + +原则:不确定就 full render,不用 stale cache 换性能。 + +## 10. 观测 +复用 include/mbgl/particle/renderer_debug_counters.hpp,建议增加: +- FullRendererFrame +- RetainedUnderlayBuild +- RetainedUnderlayReuse +- RetainedUnderlayFallback +- RetainedUnderlayInvalidation +- RetainedUnderlayStaticGroupDraw +- RetainedDynamicGroupDraw + +固定 Camera + Particle 约 30 FPS 时,理想: +- RetainedUnderlayBuild ≈ 1 +- RetainedUnderlayReuse 持续增长 +- ParticleSimulationAdvance 持续增长 +- StaticGroupDraw 建 cache 后不随 Particle FPS 增长 +- DynamicGroupDraw 持续增长 + +## 11. 不允许 +- React RAF 驱动 +- Host renderFastFrame/cacheMapNow/invalidateBasemap API +- NavSea 手工管理 renderer cache +- 修改 C-001 +- 永久 public static/dynamic flag +- 改变 Style layer ordering +- STEP-088 就跳过 source/update/prepare +- Android/iOS 不同 renderer semantics +- stale cache + +## 12. 执行顺序 +1. STEP-088:PoC + observability +2. 用户 Android/NavSea 真机验证 +3. STEP-089:production invalidation/fallback +4. 用户再次 profile +5. 只有 CPU orchestration 仍是瓶颈时才 STEP-090