diff --git a/.gitignore b/.gitignore index dac847875..1e3aea8d2 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ /platform/android/build /platform/android/MapLibreAndroid/src/main/assets/sdk_versions/com.mapbox.mapboxsdk /platform/android/MapLibreAndroid/android-binaries-*.zip +/artifacts/maplibre-particle/ *.code-workspace local.properties diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..eff1ef1d5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,154 @@ +# MapLibreParticleLab — Codex / ChatGPT RDC 总规则 + +本文件是 Codex 与 ChatGPT Remote Desktop Commander(RDC)进入本仓库后的第一入口。 + +## 1. 开始工作前必须读取 + +任何修改、构建、Git、发布、同步或设备操作前,必须先读取: + +1. 根目录 `AGENTS.md`(本文件); +2. 根目录 `PARTICLE_BASELINE.md`; +3. `docs/00_项目规范/`(包括官方基线来源); +4. `docs/00_项目总则/README.md`; +5. `docs/00_项目总则/` 中 README 标记的全部当前有效总则; +6. `docs/00_项目总则/当前基线与开发路线_v2.md`(或 README 指定的后续当前版本); +7. 当前 Step / 当前任务,以及与本任务直接相关的当前设计、测试或发布文档。 + +不得只依赖聊天记忆、旧窗口摘要或历史 Step 直接修改仓库。 + +## 2. RDC 启动规则 + +ChatGPT 使用 RDC 对本仓库进行实际操作时,必须把上面的阅读步骤作为固定启动流程。RDC 本身不会自动读取 `AGENTS.md`,因此由 ChatGPT 在第一次写文件、执行构建、Git、发布或设备命令前主动完成。 + +开始实际修改前至少确认: + +- 当前机器/设备; +- 仓库绝对路径; +- 当前 branch; +- 当前 HEAD; +- `git status`; +- 与任务有关的当前版本号/发布坐标。 + +## 3. 冲突与事实优先级 + +若文档与代码/构建事实冲突,不得自行猜测或用旧规则覆盖当前实现。按以下顺序处理: + +1. Git 当前代码与实际构建/运行结果 = 实现事实; +2. 当前基线与项目总则 = 设计与流程意图; +3. 当前 Step/任务 = 本轮工作合同; +4. 历史 Step/旧验收文档 = 历史证据,不自动代表当前流程。 + +发现冲突时先报告并限定修改范围;除非任务明确要求,不重写历史文档来伪装一致。 + +## 4. 项目固定边界 + +项目目标:在 MapLibre Native Core 中 clean-room 独立实现 Raster/Vector Field Particle,支持 Android OpenGL/Vulkan 与后续 iOS Metal。 + +固定边界:正式实现不使用 React Native/WebView/外部 Overlay;粒子模块高度隔离;官方原文件只做薄接入;Android/iOS 共用 C++ Core;WeatherColorLayer 与 RasterParticleLayer 分离;Wind=GLOBAL;Wave/Tide/Current=OCEAN_ONLY 且共用 Land/Ocean Mask;不得复制 Mapbox 新源码或 Windy APK 内代码。 + +Host/NavSea 负责 provider、HTTP、manifest、下载、缓存和业务调度;MapLibre Native 从宿主提交的 canonical/encoded field 开始负责校验、解码、Runtime Field Window 与 GPU 渲染。 + +## 5. Codex 与 RDC 的职责分工 + +### Codex 主要负责 + +- 大规模源码修改与跨文件重构; +- 新功能实现、接口调整、批量工程化修改; +- 按 Codex 任务书完成明确范围内的代码与文档变更; +- 需要较长连续编辑、依赖全仓上下文的实现工作。 + +Codex 的核心角色是“实现者”,不作为其自身修改结果的唯一最终验收者。 + +### RDC 主要负责 + +- 编译、链接、codegen、运行测试程序与测试命令; +- AAR/APK/npm tgz 等 artifact 的构建、检查、发布与下载; +- 公网发布后的回读、SHA-256/版本/文件内容验证; +- Win11、Linux、模拟器、ADB、Metro、安装、启动和运行态检查; +- 日志、进程、端口、文件系统、Git 状态与构建产物审计; +- 对 Codex 完成结果做独立代码/配置/文档/构建审计; +- 小范围、事实明确、风险可控的修正。 + +## 6. Codex → RDC 固定切换流程 + +默认工作流: + +1. ChatGPT/用户先明确设计边界与任务目标; +2. 需要大规模实现时交给 Codex; +3. Codex 完成任务书范围后停止,不继续承担最终环境验收; +4. 切换到 RDC,重新读取 `AGENTS.md`、当前基线、Codex 交付文件和 Git diff; +5. RDC 做独立审计,并负责实际编译、测试、artifact、发布、下载、安装、Metro/ADB/模拟器和日志验证; +6. 若审计发现小型明确问题,可由 RDC 直接修正并重新验证; +7. 若发现架构性问题、跨模块大改或修改范围明显扩大,停止 RDC 扩改,重新生成/调整 Codex 任务再进入实现; +8. 最终以 RDC 实际环境验证结果 + Git 当前代码作为验收事实。 + +禁止默认让 Codex 同时承担“大规模实现 + 唯一审计 + 唯一最终环境验证”三种角色。 + +## 7. 记录与收尾 + +每一阶段必须有 `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/CMakeLists.txt b/CMakeLists.txt index e71097937..26fc37904 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,6 +180,12 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mln/gfx/uniform_buffer.cpp ${PROJECT_SOURCE_DIR}/src/mln/gfx/vertex_attribute.cpp ${PROJECT_SOURCE_DIR}/src/mln/renderer/render_target.cpp + ${PROJECT_SOURCE_DIR}/src/mln/renderer/particle_fast_path.hpp + ${PROJECT_SOURCE_DIR}/src/mln/renderer/retained_underlay_cache.cpp + ${PROJECT_SOURCE_DIR}/src/mln/renderer/retained_underlay_cache.hpp + ${PROJECT_SOURCE_DIR}/src/mln/renderer/retained_underlay_runtime_state.hpp + ${PROJECT_SOURCE_DIR}/src/mln/renderer/render_segment_plan.hpp + ${PROJECT_SOURCE_DIR}/src/mln/renderer/render_segment_json.hpp ${PROJECT_SOURCE_DIR}/src/mln/renderer/change_request.cpp ${PROJECT_SOURCE_DIR}/src/mln/renderer/layer_group.cpp ${PROJECT_SOURCE_DIR}/src/mln/renderer/tile_layer_group.cpp @@ -210,10 +216,32 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/symbol_layer_tweaker.hpp ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/collision_layer_tweaker.cpp ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/collision_layer_tweaker.hpp + ${PROJECT_SOURCE_DIR}/src/mln/particle/field/offline_fixture_particle_adapter.cpp + ${PROJECT_SOURCE_DIR}/src/mln/particle/field/offline_fixture_particle_adapter.hpp + ${PROJECT_SOURCE_DIR}/src/mln/particle/field/field_data_adapter.cpp + ${PROJECT_SOURCE_DIR}/src/mln/particle/field/field_runtime.cpp + ${PROJECT_SOURCE_DIR}/src/mln/particle/field/field_binding.cpp + ${PROJECT_SOURCE_DIR}/src/mln/particle/field/field_projection_materialization.cpp + ${PROJECT_SOURCE_DIR}/src/mln/particle/field/field_profile_validator.cpp + ${PROJECT_SOURCE_DIR}/src/mln/particle/field/field_profile_contract_json.cpp + ${PROJECT_SOURCE_DIR}/src/mln/particle/ingress/qbin_v2_decoder.cpp + ${PROJECT_SOURCE_DIR}/src/mln/particle/ingress/qbin_v3_decoder.cpp + ${PROJECT_SOURCE_DIR}/src/mln/particle/field/local_particle_field_feed.cpp ${PROJECT_SOURCE_DIR}/src/mln/shaders/shader_program_base.cpp ) list(APPEND INCLUDE_FILES + ${PROJECT_SOURCE_DIR}/include/mln/particle/field/field_data_adapter.hpp + ${PROJECT_SOURCE_DIR}/include/mln/particle/field/field_profile_contract.hpp + ${PROJECT_SOURCE_DIR}/include/mln/particle/field/field_profile_validator.hpp + ${PROJECT_SOURCE_DIR}/include/mln/particle/field/field_profile_contract_json.hpp + ${PROJECT_SOURCE_DIR}/include/mln/particle/field/field_runtime.hpp + ${PROJECT_SOURCE_DIR}/include/mln/particle/field/field_binding.hpp + ${PROJECT_SOURCE_DIR}/include/mln/particle/renderer_debug_counters.hpp + ${PROJECT_SOURCE_DIR}/include/mln/particle/ingress/qbin_v2_decoder.hpp + ${PROJECT_SOURCE_DIR}/include/mln/particle/ingress/qbin_v3_decoder.hpp + ${PROJECT_SOURCE_DIR}/include/mln/particle/ingress/encoded_field_transport.hpp + ${PROJECT_SOURCE_DIR}/include/mln/particle/field/local_particle_field_feed.hpp ${PROJECT_SOURCE_DIR}/include/mln/actor/actor_ref.hpp ${PROJECT_SOURCE_DIR}/include/mln/actor/actor.hpp ${PROJECT_SOURCE_DIR}/include/mln/actor/aspiring_actor.hpp @@ -246,6 +274,9 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mln/gfx/types.hpp ${PROJECT_SOURCE_DIR}/include/mln/gfx/vertex_buffer.hpp ${PROJECT_SOURCE_DIR}/include/mln/layermanager/background_layer_factory.hpp + ${PROJECT_SOURCE_DIR}/include/mln/layermanager/particle_validation_layer_factory.hpp + ${PROJECT_SOURCE_DIR}/include/mln/layermanager/raster_particle_layer_factory.hpp + ${PROJECT_SOURCE_DIR}/include/mln/layermanager/weather_color_layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mln/layermanager/circle_layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mln/layermanager/custom_layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mln/layermanager/fill_extrusion_layer_factory.hpp @@ -280,6 +311,8 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mln/renderer/renderer_observer.hpp ${PROJECT_SOURCE_DIR}/include/mln/renderer/renderer_state.hpp ${PROJECT_SOURCE_DIR}/include/mln/renderer/renderer.hpp + ${PROJECT_SOURCE_DIR}/include/mln/renderer/retained_underlay.hpp + ${PROJECT_SOURCE_DIR}/include/mln/renderer/render_segment.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/program_parameters.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/shader_source.hpp ${PROJECT_SOURCE_DIR}/include/mln/storage/database_file_source.hpp @@ -357,6 +390,10 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mln/style/layer_properties.hpp ${PROJECT_SOURCE_DIR}/include/mln/style/layer.hpp ${PROJECT_SOURCE_DIR}/include/mln/style/layers/background_layer.hpp + ${PROJECT_SOURCE_DIR}/include/mln/style/layers/particle_validation_layer.hpp + ${PROJECT_SOURCE_DIR}/include/mln/style/layers/raster_particle_layer.hpp + ${PROJECT_SOURCE_DIR}/include/mln/style/layers/weather_color_layer.hpp + ${PROJECT_SOURCE_DIR}/include/mln/style/sources/raster_array_source.hpp ${PROJECT_SOURCE_DIR}/include/mln/style/layers/circle_layer.hpp ${PROJECT_SOURCE_DIR}/include/mln/style/layers/custom_layer_host.hpp ${PROJECT_SOURCE_DIR}/include/mln/style/layers/custom_layer_init_parameters.hpp @@ -503,6 +540,9 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mln/gfx/upload_pass.hpp ${PROJECT_SOURCE_DIR}/src/mln/gfx/vertex_vector.hpp ${PROJECT_SOURCE_DIR}/src/mln/layermanager/background_layer_factory.cpp + ${PROJECT_SOURCE_DIR}/src/mln/layermanager/particle_validation_layer_factory.cpp + ${PROJECT_SOURCE_DIR}/src/mln/layermanager/raster_particle_layer_factory.cpp + ${PROJECT_SOURCE_DIR}/src/mln/layermanager/weather_color_layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mln/layermanager/circle_layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mln/layermanager/fill_extrusion_layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mln/layermanager/fill_layer_factory.cpp @@ -580,6 +620,11 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mln/renderer/image_manager_observer.hpp ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/render_background_layer.cpp ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/render_background_layer.hpp + ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/render_particle_validation_layer.cpp + ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/render_particle_validation_layer.hpp + ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/render_raster_particle_layer.hpp + ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/render_weather_color_layer.cpp + ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/render_weather_color_layer.hpp ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/render_circle_layer.cpp ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/render_circle_layer.hpp ${PROJECT_SOURCE_DIR}/src/mln/renderer/layers/render_fill_extrusion_layer.cpp @@ -755,6 +800,13 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mln/style/layer_impl.hpp ${PROJECT_SOURCE_DIR}/src/mln/style/layer_observer.hpp ${PROJECT_SOURCE_DIR}/src/mln/style/layers/background_layer.cpp + ${PROJECT_SOURCE_DIR}/src/mln/style/layers/particle_validation_layer.cpp + ${PROJECT_SOURCE_DIR}/src/mln/style/layers/particle_validation_layer_impl.hpp + ${PROJECT_SOURCE_DIR}/src/mln/style/layers/particle_render_layer_properties.hpp + ${PROJECT_SOURCE_DIR}/src/mln/style/layers/raster_particle_layer.cpp + ${PROJECT_SOURCE_DIR}/src/mln/style/layers/raster_particle_layer_impl.hpp + ${PROJECT_SOURCE_DIR}/src/mln/style/layers/weather_color_layer.cpp + ${PROJECT_SOURCE_DIR}/src/mln/style/layers/weather_color_layer_impl.hpp ${PROJECT_SOURCE_DIR}/src/mln/style/layers/background_layer_impl.cpp ${PROJECT_SOURCE_DIR}/src/mln/style/layers/background_layer_impl.hpp ${PROJECT_SOURCE_DIR}/src/mln/style/layers/background_layer_properties.cpp @@ -835,6 +887,8 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mln/style/sources/raster_dem_source.cpp ${PROJECT_SOURCE_DIR}/src/mln/style/sources/tile_source.cpp ${PROJECT_SOURCE_DIR}/src/mln/style/sources/raster_source.cpp + ${PROJECT_SOURCE_DIR}/src/mln/style/sources/raster_array_source.cpp + ${PROJECT_SOURCE_DIR}/src/mln/style/sources/raster_array_source_impl.hpp ${PROJECT_SOURCE_DIR}/src/mln/style/sources/tile_source_impl.cpp ${PROJECT_SOURCE_DIR}/src/mln/style/sources/tile_source_impl.hpp ${PROJECT_SOURCE_DIR}/src/mln/style/sources/vector_source.cpp diff --git a/PARTICLE_BASELINE.md b/PARTICLE_BASELINE.md new file mode 100644 index 000000000..856f4eab0 --- /dev/null +++ b/PARTICLE_BASELINE.md @@ -0,0 +1,17 @@ +# MapLibreParticleLab 基线记录 + +- 初始化日期:2026-08-27 +- upstream:https://github.com/maplibre/maplibre-native.git +- origin:https://git.eyaku.jp/tei/MapLibreParticleLab +- Android:android-v13.4.1 +- iOS:ios-v6.28.0 +- 共同 core:18545ca10d296704b2cba675b182ff2c8da24588 +- 分支:particle/main +- Ubuntu:Ubuntu 24.04.4 LTS +- 架构:x86_64 +- Java:17 +- Android API:34 +- NDK:28.2.13676358 +- 默认并行度:3 +- upstream push:DISABLED +- git rerere:true diff --git a/bazel/core.bzl b/bazel/core.bzl index a9b8c7a06..98e9ee59f 100644 --- a/bazel/core.bzl +++ b/bazel/core.bzl @@ -99,6 +99,8 @@ MLN_GENERATED_OPENGL_SHADER_HEADERS = [ "include/mln/shaders/gl/fill_extrusion_pattern_instanced.hpp", "include/mln/shaders/gl/heatmap.hpp", "include/mln/shaders/gl/heatmap_texture.hpp", + "include/mln/shaders/gl/particle_gpu_validation.hpp", + "include/mln/shaders/gl/retained_underlay_composite.hpp", "include/mln/shaders/gl/hillshade_prepare.hpp", "include/mln/shaders/gl/hillshade.hpp", "include/mln/shaders/gl/color_relief.hpp", @@ -162,6 +164,9 @@ MLN_CORE_SOURCE = [ "src/mln/gfx/upload_pass.hpp", "src/mln/gfx/vertex_vector.hpp", "src/mln/layermanager/background_layer_factory.cpp", + "src/mln/layermanager/particle_validation_layer_factory.cpp", + "src/mln/layermanager/raster_particle_layer_factory.cpp", + "src/mln/layermanager/weather_color_layer_factory.cpp", "src/mln/layermanager/circle_layer_factory.cpp", "src/mln/layermanager/custom_layer_factory.cpp", "src/mln/layermanager/fill_extrusion_layer_factory.cpp", @@ -201,6 +206,17 @@ MLN_CORE_SOURCE = [ "src/mln/map/zoom_history.hpp", "src/mln/math/log2.cpp", "src/mln/platform/settings.cpp", + "src/mln/particle/field/offline_fixture_particle_adapter.cpp", + "src/mln/particle/field/offline_fixture_particle_adapter.hpp", + "src/mln/particle/field/field_data_adapter.cpp", + "src/mln/particle/field/field_runtime.cpp", + "src/mln/particle/field/field_binding.cpp", + "src/mln/particle/field/field_projection_materialization.cpp", + "src/mln/particle/field/field_profile_validator.cpp", + "src/mln/particle/field/field_profile_contract_json.cpp", + "src/mln/particle/ingress/qbin_v2_decoder.cpp", + "src/mln/particle/ingress/qbin_v3_decoder.cpp", + "src/mln/particle/field/local_particle_field_feed.cpp", "src/mln/renderer/backend_scope.cpp", "src/mln/renderer/bucket.hpp", "src/mln/renderer/bucket_parameters.cpp", @@ -233,6 +249,11 @@ MLN_CORE_SOURCE = [ "src/mln/renderer/image_manager_observer.hpp", "src/mln/renderer/layers/render_background_layer.cpp", "src/mln/renderer/layers/render_background_layer.hpp", + "src/mln/renderer/layers/render_particle_validation_layer.cpp", + "src/mln/renderer/layers/render_particle_validation_layer.hpp", + "src/mln/renderer/layers/render_raster_particle_layer.hpp", + "src/mln/renderer/layers/render_weather_color_layer.cpp", + "src/mln/renderer/layers/render_weather_color_layer.hpp", "src/mln/renderer/layers/render_circle_layer.cpp", "src/mln/renderer/layers/render_circle_layer.hpp", "src/mln/renderer/layers/render_custom_layer.cpp", @@ -259,6 +280,8 @@ MLN_CORE_SOURCE = [ "src/mln/renderer/paint_parameters.hpp", "src/mln/renderer/paint_property_binder.hpp", "src/mln/renderer/paint_property_statistics.hpp", + "src/mln/renderer/particle_viewport_reseed.hpp", + "src/mln/renderer/particle_fast_path.hpp", "src/mln/renderer/pattern_atlas.cpp", "src/mln/renderer/pattern_atlas.hpp", "src/mln/renderer/possibly_evaluated_property_value.hpp", @@ -270,6 +293,9 @@ MLN_CORE_SOURCE = [ "src/mln/renderer/render_light.hpp", "src/mln/renderer/render_orchestrator.cpp", "src/mln/renderer/render_orchestrator.hpp", + "src/mln/renderer/continuous_layer_range.hpp", + "src/mln/renderer/render_segment_plan.hpp", + "src/mln/renderer/render_segment_json.hpp", "src/mln/renderer/render_pass.hpp", "src/mln/renderer/render_source.cpp", "src/mln/renderer/render_source.hpp", @@ -283,6 +309,9 @@ MLN_CORE_SOURCE = [ "src/mln/renderer/renderer_impl.cpp", "src/mln/renderer/renderer_impl.hpp", "src/mln/renderer/renderer_state.cpp", + "src/mln/renderer/retained_underlay_cache.cpp", + "src/mln/renderer/retained_underlay_cache.hpp", + "src/mln/renderer/retained_underlay_runtime_state.hpp", "src/mln/renderer/sources/render_custom_geometry_source.cpp", "src/mln/renderer/sources/render_custom_geometry_source.hpp", "src/mln/renderer/sources/render_custom_vector_source.cpp", @@ -295,6 +324,7 @@ MLN_CORE_SOURCE = [ "src/mln/renderer/sources/render_raster_dem_source.hpp", "src/mln/renderer/sources/render_raster_source.cpp", "src/mln/renderer/sources/render_raster_source.hpp", + "src/mln/renderer/sources/render_raster_array_source.hpp", "src/mln/renderer/sources/render_tile_source.cpp", "src/mln/renderer/sources/render_tile_source.hpp", "src/mln/renderer/sources/render_vector_source.cpp", @@ -412,6 +442,13 @@ MLN_CORE_SOURCE = [ "src/mln/style/layer_observer.hpp", "src/mln/style/layers/background_layer_impl.cpp", "src/mln/style/layers/background_layer_impl.hpp", + "src/mln/style/layers/particle_validation_layer.cpp", + "src/mln/style/layers/particle_validation_layer_impl.hpp", + "src/mln/style/layers/particle_render_layer_properties.hpp", + "src/mln/style/layers/raster_particle_layer.cpp", + "src/mln/style/layers/raster_particle_layer_impl.hpp", + "src/mln/style/layers/weather_color_layer.cpp", + "src/mln/style/layers/weather_color_layer_impl.hpp", "src/mln/style/layers/circle_layer_impl.cpp", "src/mln/style/layers/circle_layer_impl.hpp", "src/mln/style/layers/custom_layer.cpp", @@ -466,6 +503,8 @@ MLN_CORE_SOURCE = [ "src/mln/style/sources/raster_dem_source.cpp", "src/mln/style/sources/tile_source.cpp", "src/mln/style/sources/raster_source.cpp", + "src/mln/style/sources/raster_array_source.cpp", + "src/mln/style/sources/raster_array_source_impl.hpp", "src/mln/style/sources/tile_source_impl.cpp", "src/mln/style/sources/tile_source_impl.hpp", "src/mln/style/sources/vector_source.cpp", @@ -636,6 +675,20 @@ MLN_CORE_SOURCE = [ }) MLN_CORE_HEADERS = [ + "include/mln/particle/field/field_data_adapter.hpp", + "include/mln/particle/field/field_profile_contract.hpp", + "include/mln/particle/field/field_profile_validator.hpp", + "include/mln/particle/field/field_profile_contract_json.hpp", + "include/mln/particle/field/field_projection_materialization.hpp", + "include/mln/particle/field/field_runtime.hpp", + "include/mln/particle/field/particle_gpu_math.hpp", + "include/mln/particle/field/field_binding.hpp", + "include/mln/particle/renderer_debug_counters.hpp", + "include/mln/renderer/layers/weather_color_texture_lifecycle.hpp", + "include/mln/particle/ingress/qbin_v2_decoder.hpp", + "include/mln/particle/ingress/qbin_v3_decoder.hpp", + "include/mln/particle/ingress/encoded_field_transport.hpp", + "include/mln/particle/field/local_particle_field_feed.hpp", "include/mln/gfx/command_encoder.hpp", "include/mln/gfx/debug_group.hpp", "include/mln/gfx/context.hpp", @@ -673,6 +726,9 @@ MLN_CORE_HEADERS = [ "include/mln/i18n/collator.hpp", "include/mln/i18n/number_format.hpp", "include/mln/layermanager/background_layer_factory.hpp", + "include/mln/layermanager/particle_validation_layer_factory.hpp", + "include/mln/layermanager/raster_particle_layer_factory.hpp", + "include/mln/layermanager/weather_color_layer_factory.hpp", "include/mln/layermanager/circle_layer_factory.hpp", "include/mln/layermanager/custom_layer_factory.hpp", "include/mln/layermanager/fill_extrusion_layer_factory.hpp", @@ -686,6 +742,11 @@ MLN_CORE_HEADERS = [ "include/mln/layermanager/location_indicator_layer_factory.hpp", "include/mln/layermanager/raster_layer_factory.hpp", "include/mln/layermanager/symbol_layer_factory.hpp", + "include/mln/style/layers/particle_validation_layer.hpp", + "include/mln/style/layers/raster_particle_layer.hpp", + "include/mln/style/layers/weather_color_layer.hpp", + "include/mln/style/layers/weather_color_stop.hpp", + "include/mln/style/sources/raster_array_source.hpp", "include/mln/map/bound_options.hpp", "include/mln/map/camera.hpp", "include/mln/map/change.hpp", @@ -708,6 +769,8 @@ MLN_CORE_HEADERS = [ "include/mln/renderer/renderer_frontend.hpp", "include/mln/renderer/renderer_observer.hpp", "include/mln/renderer/renderer_state.hpp", + "include/mln/renderer/retained_underlay.hpp", + "include/mln/renderer/render_segment.hpp", "include/mln/shaders/program_parameters.hpp", "include/mln/storage/database_file_source.hpp", "include/mln/storage/file_source.hpp", @@ -1021,6 +1084,7 @@ MLN_DRAWABLES_HEADERS = [ "include/mln/shaders/fill_extrusion_layer_ubo.hpp", "include/mln/shaders/heatmap_layer_ubo.hpp", "include/mln/shaders/heatmap_texture_layer_ubo.hpp", + "include/mln/shaders/particle_gpu_validation_ubo.hpp", "include/mln/shaders/hillshade_layer_ubo.hpp", "include/mln/shaders/color_relief_layer_ubo.hpp", "include/mln/shaders/hillshade_prepare_layer_ubo.hpp", @@ -1102,6 +1166,8 @@ MLN_DRAWABLES_MTL_SOURCE = [ "src/mln/shaders/mtl/color_relief.cpp", "src/mln/shaders/mtl/line.cpp", "src/mln/shaders/mtl/location_indicator.cpp", + "src/mln/shaders/mtl/particle_gpu_validation.cpp", + "src/mln/shaders/mtl/retained_underlay_composite.cpp", "src/mln/shaders/mtl/raster.cpp", "src/mln/shaders/mtl/symbol.cpp", "src/mln/shaders/mtl/widevector.cpp", @@ -1145,6 +1211,8 @@ MLN_DRAWABLES_MTL_HEADERS = [ "include/mln/shaders/mtl/color_relief.hpp", "include/mln/shaders/mtl/line.hpp", "include/mln/shaders/mtl/location_indicator.hpp", + "include/mln/shaders/mtl/particle_gpu_validation.hpp", + "include/mln/shaders/mtl/retained_underlay_composite.hpp", "include/mln/shaders/mtl/raster.hpp", "include/mln/shaders/mtl/shader_group.hpp", "include/mln/shaders/mtl/shader_program.hpp", diff --git a/cmake/metal.cmake b/cmake/metal.cmake index 03338974a..5fab13092 100644 --- a/cmake/metal.cmake +++ b/cmake/metal.cmake @@ -47,6 +47,8 @@ list(APPEND ${PROJECT_SOURCE_DIR}/include/mln/shaders/mtl/color_relief.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/mtl/line.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/mtl/location_indicator.hpp + ${PROJECT_SOURCE_DIR}/include/mln/shaders/mtl/particle_gpu_validation.hpp + ${PROJECT_SOURCE_DIR}/include/mln/shaders/mtl/retained_underlay_composite.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/mtl/raster.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/mtl/shader_group.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/mtl/shader_program.hpp @@ -90,6 +92,8 @@ list(APPEND ${PROJECT_SOURCE_DIR}/src/mln/shaders/mtl/color_relief.cpp ${PROJECT_SOURCE_DIR}/src/mln/shaders/mtl/line.cpp ${PROJECT_SOURCE_DIR}/src/mln/shaders/mtl/location_indicator.cpp + ${PROJECT_SOURCE_DIR}/src/mln/shaders/mtl/particle_gpu_validation.cpp + ${PROJECT_SOURCE_DIR}/src/mln/shaders/mtl/retained_underlay_composite.cpp ${PROJECT_SOURCE_DIR}/src/mln/shaders/mtl/raster.cpp ${PROJECT_SOURCE_DIR}/src/mln/shaders/mtl/symbol.cpp ${PROJECT_SOURCE_DIR}/src/mln/shaders/mtl/widevector.cpp diff --git a/cmake/opengl.cmake b/cmake/opengl.cmake index 4b2cfe008..256c8108f 100644 --- a/cmake/opengl.cmake +++ b/cmake/opengl.cmake @@ -37,6 +37,8 @@ list(APPEND ${PROJECT_SOURCE_DIR}/include/mln/shaders/gl/fill_extrusion_pattern_instanced.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/gl/heatmap.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/gl/heatmap_texture.hpp + ${PROJECT_SOURCE_DIR}/include/mln/shaders/gl/particle_gpu_validation.hpp + ${PROJECT_SOURCE_DIR}/include/mln/shaders/gl/retained_underlay_composite.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/gl/hillshade_prepare.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/gl/hillshade.hpp ${PROJECT_SOURCE_DIR}/include/mln/shaders/gl/color_relief.hpp diff --git a/docs/00_项目总则/README.md b/docs/00_项目总则/README.md new file mode 100644 index 000000000..a6fbbab0c --- /dev/null +++ b/docs/00_项目总则/README.md @@ -0,0 +1,34 @@ +# MapLibreParticleLab 项目总则目录 + +## 定位 +`docs/00_项目总则/` 是本项目长期规则层(Source of Truth)。保存经用户与 GPT 确认的长期设计决策、开发环境、系统构建规范、Codex 协作规则、Step/执行报告规范,以及当前正式基线与路线。普通 Step 默认禁止修改本目录。 + +## 当前有效文件 +1. `粒子系统项目规则与设计决策_v1.md` +2. `开发环境与系统构建规范_v1.md` +3. `Step指令书与执行报告规范_v1.md` +4. `当前基线与开发路线_v2.md`(当前) + +`当前基线与开发路线_v1.md` 保留为 2026-08-29 历史冻结,不再作为当前构建/发布依据。 + +## 阅读顺序 +`AGENTS.md` → 本目录 README → 四份当前有效总则 → 当前 Step 指令书 → 上一 Step 执行报告 → 其他阶段文档。 + +## 维护职责 +- GPT:参考“粒子系统”GPT 项目讨论,提出总则更新。 +- 用户:确认或否决长期规则。 +- Codex:普通 Step 只读;只有明确授权的总则更新任务才可修改。 + +## 冲突处理 +若 Step 与总则冲突,Codex 停止冲突部分,在执行报告记录,不得自行改总则。 + +## 版本规则 +同主题仅递增 `_v1`、`_v2`、`_v3`;不使用“最终版/最新版/final/new/修正版”。 + +## 大阶段目录 +建议:`10_架构调查/`、`20_测试App/`、`30_粒子核心/`、`40_天气数据链/`、`50_渲染与视觉/`、`60_性能与后端/`、`70_NavSea集成/`、`80_iOS与Metal/`、`90_升级与维护/`。进入新大阶段前,GPT 先告诉用户目录名、目的、预计 Step 范围。 + +## 换窗口/换会话恢复 +1. 读本目录全部当前有效总则;2. 读当前基线;3. 读上一 Step 执行报告;4. 检查 Git branch/commit/working tree/submodule;5. 再继续下一 Step。 + +分层:GPT 项目讨论=设计过程;项目总则=冻结规则;Step 指令书=工作合同;执行报告=实际事实;Git=最终代码事实。 diff --git a/docs/00_项目总则/Step指令书与执行报告规范_v1.md b/docs/00_项目总则/Step指令书与执行报告规范_v1.md new file mode 100644 index 000000000..b30707ef8 --- /dev/null +++ b/docs/00_项目总则/Step指令书与执行报告规范_v1.md @@ -0,0 +1,55 @@ +# Step 指令书与执行报告规范 v1 + +## 1. 命名 +指令书:`step-xxxx-目的内容.md`。 +报告:`step-xxxx-目的内容-执行报告.md`。 +统一使用“执行报告”。 + +## 2. 同目录 +指令书与报告放同一个大阶段目的目录。 + +## 3. 指令书固定章节 +1. Step 编号和目的 +2. 当前已冻结基线 +3. 本 Step 目标 +4. 非目标/禁止事项 +5. 允许修改范围 +6. 禁止修改范围 +7. 实施要求 +8. 构建要求 +9. 测试要求 +10. 验收标准 +11. Git 要求 +12. 必做输出 +13. 执行报告完整文件名 +14. 执行报告固定内容 +15. 阻塞与停止条件 + +## 4. Codex 前置必读 +AGENTS.md → 本目录 README → 三份长期总则 → 当前基线与路线 → 本阶段已有文档 → 上一 Step 执行报告。 + +## 5. Jest 特殊规则 +GPT 生成任何正式 Step 指令书之前必须先询问用户是否加入 Jest。未经明确确认不得写入。若加入,先确认测试力度、允许修复范围、是否可改生产代码、最大修复轮数、停止条件。 + +## 6. 执行报告固定结构 +1. 执行结论:PASS/PARTIAL/BLOCKED/FAIL +2. Git 基线:branch/start commit/end commit/working tree/submodule +3. 实际修改文件 +4. 新增文件 +5. 实际实现内容 +6. 与指令书差异(无也写“无”) +7. 构建记录:command/renderer/build type/JOBS/result/关键日志/耗时 +8. 测试记录 +9. 生成产物:文件名/路径/大小/SHA256/Renderer/Build Type/ABI +10. 已知问题 +11. 未完成事项 +12. 下一 Step 建议(只建议,不自行创建) + +## 7. 报告必须反映事实 +禁止未执行测试写 PASS、计划写成已完成、隐藏失败、用“应该正常”替代结果、把 Win11 人工验证写成 Linux Codex 已完成。若只完成 Linux APK build,必须明确“Win11 Android Simulator 人工验证待用户执行”。 + +## 8. 总则修改 +普通 Step 禁止修改 `docs/00_项目总则/`。如认为需调整,只在报告提出建议。 + +## 9. GPT 写新 Step 的依据 +必须参考:“粒子系统”GPT 项目既有设计、项目总则、当前基线、上一 Step 报告、当前 Git 事实、用户最新确认。 diff --git a/docs/00_项目总则/开发环境与系统构建规范_v1.md b/docs/00_项目总则/开发环境与系统构建规范_v1.md new file mode 100644 index 000000000..24a65c873 --- /dev/null +++ b/docs/00_项目总则/开发环境与系统构建规范_v1.md @@ -0,0 +1,57 @@ +# 开发环境与系统构建规范 v1 + +## 1. 正式基线 +源码:`/mnt/sda1/MapLibreParticleLab`;稳定分支:`particle/main`。Linux:Ubuntu 24.04 LTS、x86_64、4 CPU threads、约 8GB RAM、默认 `JOBS=3`。Win11:NavSea、Android 模拟器、Particle Test APK 运行、NavSea Android 手工编译。 + +## 2. Step 开始前 +记录:`git branch --show-current`、`git rev-parse HEAD`、`git status --short`、`git submodule status`。 + +## 3. Git 远端 +origin=`ssh://git@192.168.200.230:2222/tei/MapLibreParticleLab.git`;upstream=`https://github.com/maplibre/maplibre-native.git`,只读,push 禁用;保留 `git rerere`。 + +## 4. Step branch +建议 `step/0001-...`。流程:particle/main → Step branch → Codex → build/test/report → GPT/用户审查 → PASS → merge particle/main → push → freeze。 + +## 5. 构建磁盘 +大型 build 必须使用 Linux 本地 SSD;禁止 NAS/SMB/NFS 作为 build root。NAS 只做备份、APK/AAR 中转、归档。 + +## 6. 环境入口 +构建前 `source /root/.maplibre-particle-env`。Codex 不得自行升级 Node/Java/SDK/NDK/Gradle/AGP/CMake/MapLibre baseline,除非 Step 明确要求。 + +## 7. 已验证环境 +Linux Native OpenGL PASS;mbgl-render PASS;mbgl-render-test-runner PASS;Android OpenGL Debug AAR PASS;Android Vulkan Debug AAR PASS;Android OpenGL Release AAR PASS。SDK=`/root/Android/Sdk`;NDK=`28.2.13676358`。 + +## 8. Development Build +默认 Debug + OpenGL + 增量 + Particle Test App。小改不强制 clean/Release/Vulkan/NavSea/全量回归。 + +## 9. Gate Build +关键 Step 才做 Release(按需)、明确 Git commit、完整 build、APK/AAR、Renderer、ABI、大小、SHA256、执行报告、必要时 Vulkan/NavSea。 + +## 10. OpenGL/Vulkan +OpenGL 是主开发 backend;Vulkan 在专门 Step 做正式回归。核心必须遵循 MapLibre gfx 抽象。 + +## 11. Test App 日常链 +MapLibre Native → Particle Test APK → 复制到 Win11 → Android Simulator。NavSea 不参与日常粒子调试。 + +## 12. 产物 +建议统一 `dist/android/`。示例:`particle-test-step-0005-opengl-debug.apk`、`particle-test-step-0009-opengl-release.apk`、`particle-test-step-0014-vulkan-debug.apk`、`maplibre-particle-step-0016-opengl-release.aar`。Gate 产物记录 SHA256。 + +## 13. Linux/Win11 边界 +Linux Codex 默认责任到 APK/AAR build PASS、产物存在、大小、SHA256、执行报告。用户负责复制到 Win11、安装模拟器、人工视觉/交互验证。 + +## 14. NavSea Android +继续由用户在 Win11 手工编译;Linux Codex 不代替用户编译 NavSea。 + +## 15. Gate A/B/C +Gate A:maplibre-react-native 11.3.6 + 官方 Native 13.2.0,PASS。 +Gate B:11.3.6 + 官方 Native 13.4.1,PASS。 +Gate C:11.3.6 + Linux 自编译 Native 13.4.1 OpenGL Release,PASS。 + +## 16. 当前 Release AAR +`MapLibreAndroid-opengl-release.aar`,约 156MB,SHA256=`5c67d2751fafe211a0df4b82deded09ab1c07cf6ca0f4a12a880283287f85e5c`,ABI:arm64-v8a/armeabi-v7a/x86/x86_64。 + +## 17. 构建失败 +只修当前 Step 允许范围;若必须升级工具链/MapLibre、大改 gfx、修改 NavSea/冻结接口、删除测试或降低门槛,STOP 并报告 BLOCKED。 + +## 18. 禁止项 +普通 Step 禁止自行升级 MapLibre/AGP/Gradle/NDK/Java/Node,禁止修改 NavSea/冻结接口、删除测试、降低门槛、用 CPU 粒子冒充正式 GPU 实现、修改 `docs/00_项目总则/`。 diff --git a/docs/00_项目总则/当前基线与开发路线_v1.md b/docs/00_项目总则/当前基线与开发路线_v1.md new file mode 100644 index 000000000..06791d053 --- /dev/null +++ b/docs/00_项目总则/当前基线与开发路线_v1.md @@ -0,0 +1,42 @@ +# 当前基线与开发路线 v1 + +> **历史冻结:2026-08-29。** 当前基线已由 `当前基线与开发路线_v2.md` 取代;本文仅保留当时事实,不用于当前 Android/RN 构建。 + + +## 1. 冻结日期 +2026-08-29 + +## 2. Git +正式分支:`particle/main`。STEP-000 冻结提交:`ea5b8f9d7fc53204a6d7fa088e9cf89be765b5ac`,说明 `chore: freeze MapLibreParticleLab step 0 baseline`。origin=`ssh://git@192.168.200.230:2222/tei/MapLibreParticleLab.git`。upstream=`https://github.com/maplibre/maplibre-native.git`。 + +## 3. MapLibre Native +正式粒子开发 baseline:Android 13.4.1。OpenGL 主开发;Vulkan/Metal 后续。 + +## 4. Linux 构建状态 +Linux Native OpenGL PASS;mbgl-render PASS;mbgl-render-test-runner PASS;Android OpenGL Debug AAR PASS;Android Vulkan Debug AAR PASS;Android OpenGL Release AAR PASS。SDK=`/root/Android/Sdk`;NDK=`28.2.13676358`;JOBS=3。 + +## 5. Release AAR +`MapLibreAndroid-opengl-release.aar`,约 156MB,SHA256=`5c67d2751fafe211a0df4b82deded09ab1c07cf6ca0f4a12a880283287f85e5c`,ABI:arm64-v8a/armeabi-v7a/x86/x86_64。 + +## 6. NavSea +Win11;React Native 0.85.2;New Architecture=true;`@maplibre/maplibre-react-native@11.3.6`。 + +## 7. Gate +Gate A:11.3.6 + 官方 Native 13.2.0,PASS。 +Gate B:11.3.6 + 官方 Native 13.4.1,PASS。 +Gate C:11.3.6 + Linux 自编译 Native 13.4.1 OpenGL Release,PASS。 + +## 8. 固定分工 +Linux:MapLibre Native/Particle/Test App/APK/AAR/Fixture/Native Debug/Git。Win11:Android Simulator/Particle Test APK/NavSea/NavSea Android 编译/产品 Gate。NavSea 不作为日常 Particle 宿主。 + +## 9. 下一大阶段 +建议 `10_架构调查/`。正式建立前 GPT 先向用户说明该阶段目的与预计 Step 范围。 + +## 10. 下一 Step +`step-0001-MapLibre粒子接入点调查.md`。只读调查 Style Layer、Source/Tile/Bucket、RenderLayer/RenderPass、gfx、Shader、Texture/RenderTarget、OpenGL/Vulkan/Metal、Raster/Heatmap/Hillshade/CustomLayer 等;不实现正式粒子。 + +## 11. 近期路线 +STEP-0001 接入点调查;0002 Android Particle Test App;0003 Fixture;0004 RasterParticleLayer 骨架;0005 最小 GPU Particle;0006 Camera/Projection;0007 Particle State 冻结;0008 Velocity/RasterArray/Tile;0009 Trail;0010 生命周期与视觉;0011 WeatherColorLayer;0012 Land/Ocean Mask;0013 OpenGL 性能;0014 Vulkan;0015 Test App 第一阶段冻结;0016 NavSea Particle Gate;0017 NavSea API/Adapter;0018 Android 产品回归;0019 iOS/Metal;0020 NavSea iOS;0021 upstream/长期维护。 + +## 12. 换窗口恢复 +读项目总则 → 当前基线 → 上一 Step 报告 → 检查 Git → 继续下一 Step。 diff --git a/docs/00_项目总则/当前基线与开发路线_v2.md b/docs/00_项目总则/当前基线与开发路线_v2.md new file mode 100644 index 000000000..dbb5a4225 --- /dev/null +++ b/docs/00_项目总则/当前基线与开发路线_v2.md @@ -0,0 +1,41 @@ +# 当前基线与开发路线 v2 + +## 1. 冻结日期 +2026-09-14。当前正式分支为 `particle/main`,本文对应 QBIN2 V4 Android/RN Test Host 已完成真机/模拟器人工验证后的基线。 + +## 2. 当前 Git 基线 +当前整理起点 HEAD:`de8d230b87345c2538c71318681119cdecb2762a`。正式远端为 `origin/particle/main`。历史 STEP 文档用于追溯当时事实,不再自动代表当前构建方式。 + +## 3. 当前 Android/RN 版本 +- Native OpenGL AAR:`cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3`。 +- RN package:`@navsea/maplibre-particle-rn@13.4.1-particle.3`。 +- MLRN:`@maplibre/maplibre-react-native@11.3.6`。 +- React Native Test Host:RN `0.85.2`、New Architecture。 +- Native/RN 版本必须同为 `.3`;不得再使用 `.2` 作为当前消费坐标。 + +## 4. 当前数据与渲染主链 +正式新增链路为 QBIN V4:宿主下载/缓存 raw QBIN,本地文件通过 `FieldRuntime.submitEncodedField(...)` 进入 Android Bridge/JNI,再由 `RasterArraySource::submitEncodedField(...)` 与 `QbinV2Decoder::decode(...)` 解码。MapLibre 不负责 HTTP、provider、manifest、缓存或业务调度。 + +旧 V3 canonical `submitField(...)` 仍保留用于历史兼容和 Test Host 对照,不是 NavSea 新接入的推荐主链。 + +## 5. 当前 V4 公共控制面 +NavSea/Consumer 新代码优先使用:`submitEncodedField`、`setEncodedColorBinding`、`setEncodedParticleBinding`、`setEncodedForecastTime`、`setEncodedForecastPlaybackState`、`setEncodedForecastPlaybackSpeed`、`clearEncodedField`。运行时继续由 Core 拥有 Field Window、Forecast Clock、T0/T1/mix 与 boundary reuse。 + +## 6. 当前发布来源 +`https://pbf.jpcn.ccwu.cc/` 映射 Linux `/mnt/nas/pbf`。Native 正常发布到 `/mnt/nas/pbf/maven/`,RN tgz 正常发布到 `/mnt/nas/pbf/npm/`;Windows 11 正常验证必须从公网 npm/Maven 消费,不以 Windows Maven Local 或 LAN AAR 复制作为正式路径。 + +当前已验证 AAR SHA-256:`9ef03eb0d0c03457adc04847cfa69fb67d85191daecd64d3b76db2ad8cc688cd`。当前 `.3` RN tgz 在 2026-09-14 Kotlin 编译修正后重新发布;今后公开版本内容变化必须升版本,不再覆盖同版本。 + +## 7. 当前 Test Host +正式源码:`integration/react-native/MapLibreParticleRNTestHost/`。V4 Golden fixtures:Wind/Wave/Current/Rain 各 T0/T1,共 8 个 `.qbin`。Test Host 同时保留 V3 控制用于对照,但 V4 验收必须通过 `submitEncodedField`。 + +Windows 推荐工作目录必须是普通子目录,例如 `D:\React\MapLibreParticleRNTestHost-V4`,不要在盘符根目录执行 Expo prebuild。Metro 监听 `8091`,Debug App 访问设备 `localhost:8081`,固定使用 `adb -s 192.168.1.146 reverse tcp:8081 tcp:8091`。 + +## 8. 当前权威操作文档 +Android AAR 公网发布、Win11 Test Host 构建、Metro/ADB 与 V4 验收以 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md` 为准。`tools/particle_android_pipeline/README.md` 已明确 v1 本地 handoff 工具只用于历史/离线兼容。 + +## 9. 历史文档边界 +STEP-060~072 中的 `Maven Local`、`13.4.1-particle.1/.2`、旧 `D:\V308`、旧 reverse 组合等内容保留为历史验收证据,不得直接用于当前 `.3` 构建。需要当前命令时先读本文件和 STEP-080 v2。 + +## 10. 下一阶段 +当前 Native/RN QBIN V4 Android 基线可用于 NavSea 最小正式接入。建议先做 Wind 单纵向切片:NavSea Weather/Cache 层准备本地 QBIN → V4 submit/binding/time → 地图显示/clear;通过后再扩展 Wave/Current/Rain。NavSea 不解析 QBIN,也不把网络 URL 交给 MapLibre。 diff --git a/docs/00_项目总则/粒子系统项目规则与设计决策_v1.md b/docs/00_项目总则/粒子系统项目规则与设计决策_v1.md new file mode 100644 index 000000000..bcb620740 --- /dev/null +++ b/docs/00_项目总则/粒子系统项目规则与设计决策_v1.md @@ -0,0 +1,55 @@ +# 粒子系统项目规则与设计决策 v1 + +## DEC-001 Linux 与 Win11 职责分离 +Linux 是 MapLibre Native / Particle Renderer 的主要开发和构建环境,正式源码 `/mnt/sda1/MapLibreParticleLab`。Linux 负责 Native、Particle、Test App、Fixture、AAR/APK、OpenGL/Vulkan、Native 调试、性能与 Git。Win11 负责 NavSea、NavSea Android 编译、Android 模拟器、Particle Test APK 运行和产品 Gate。 + +## DEC-002 独立 Particle Test App +必须建立长期维护的独立测试 App。它不是 Demo,而是粒子 Renderer 的开发、调试、回归和性能平台。日常粒子开发不直接使用 NavSea。Android 第一阶段优先原生 Test App。 + +## DEC-003 NavSea 只用于关键产品 Gate +NavSea 是最终产品,不作为日常粒子调试宿主。只在自编译等价验证、关键 Particle Gate、真实 Layer Stack、Weather/Mask、产品级回归和最终集成时使用。 + +## DEC-004 OpenGL 为 Android 主开发 Backend +初期主开发 OpenGL;架构必须服从 MapLibre `gfx` 抽象,不能锁死 OpenGL。 + +## DEC-005 Vulkan / Metal 不是后补重写 +Particle State、Velocity、Tile、生命周期、Style/RenderLayer、Coverage、Color Ramp、Mask 尽量共享;后端差异主要放在 Shader、Texture、RenderTarget、RenderPass、gfx backend。iOS/Metal 以后单独设计测试流程。 + +## DEC-006 WeatherColorLayer 与 RasterParticleLayer 分离 +定量天气色场和粒子流动分为 `WeatherColorLayer` 与 `RasterParticleLayer`。 + +## DEC-007 天气色场不靠低透明度看底图 +天气定量颜色保持准确;岸线、等深线、航标、航道、文字和导航对象绘制在天气层上方。 + +## DEC-008 Coverage +Wind=`GLOBAL`;Wave/Tide/Current/Sea Temperature 等=`OCEAN_ONLY`。 + +## DEC-009 统一 Land/Ocean Mask +所有 OCEAN_ONLY 字段共用统一 Mask;海岸线语义尽量与 NavSea/MapLibre 海图一致,不依赖天气模型 nodata。 + +## DEC-010 正式目标是 GPU Particle Engine +核心至少包括 Velocity Texture、Particle State Texture A/B、Ping-Pong、Update Pass、Draw Pass、Trail Render Target、Color Ramp。CPU 每粒子循环不能作为正式主实现。 + +## DEC-011 Camera 正确后再冻结 Particle State Model +先验证 Pan/Zoom/Rotate/Pitch/Resize/Camera Animation,再冻结状态格式。 + +## DEC-012 Fixture 优先 +至少:uniform-east/north/west/south、vortex-clockwise/counterclockwise、shear、divergence、convergence、coastal-mask。真实天气服务器不是早期 Renderer 调试前置条件。 + +## DEC-013 thin-hook 原则 +目标约 95% 粒子代码放新增文件/独立模块;MapLibre 官方现有文件只做必要 Layer/Source/RenderLayer/Shader/Build/Backend 薄接入,降低 upstream merge 冲突。 + +## DEC-014 普通 Step 禁止修改项目总则 +需要改变长期规则时,只在执行报告提出建议。 + +## DEC-015 GPT“粒子系统”项目是设计背景来源 +GPT 写设计和指令书时要参考该项目既有讨论;正式权威规则以 Git 仓库本目录为准。 + +## DEC-016 大阶段目录按目的建立 +使用两位数字前缀;进入新阶段前 GPT 先告知用户目录目的和预计 Step 范围。 + +## DEC-017 Step 指令书与执行报告同目录 +`step-xxxx-目的内容.md` 与 `step-xxxx-目的内容-执行报告.md` 放同一大阶段目的目录。 + +## DEC-018 报告由指令书预先定义 +指令书必须事先写清报告完整路径、文件名、章节、Git 信息、构建信息、APK/AAR、大小、SHA256、PASS/PARTIAL/BLOCKED/FAIL。不得执行完后临时追加未定义报告。 diff --git a/docs/00_项目规范/00_文档目录与命名规范_v1.md b/docs/00_项目规范/00_文档目录与命名规范_v1.md new file mode 100644 index 000000000..fa92de886 --- /dev/null +++ b/docs/00_项目规范/00_文档目录与命名规范_v1.md @@ -0,0 +1,3 @@ +# 文档目录与命名规范 v1 + +正式目录:`00_项目规范/10_架构/20_调查/30_设计/40_任务/50_Step记录/60_测试/70_发布/报告`。正式说明文档文件名和正文使用中文;代码/API/命令保留英文。同主题只用 `_v1/_v2/_v3` 递增,不得用“最新版/修正版/final”制造近义文件。调查=`<主题>调查_vN.md`,设计=`<主题>设计_vN.md`,任务=`STEP-xxx_<主题>_Codex任务_vN.md`,Step=`STEP-xxx_<主题>_vN.md`,完成报告=`STEP-xxx_<主题>_Codex完成报告_vN.md`。完成报告只放 `docs/报告/`。 diff --git a/docs/00_项目规范/01_Codex协作规范_v1.md b/docs/00_项目规范/01_Codex协作规范_v1.md new file mode 100644 index 000000000..e8c963352 --- /dev/null +++ b/docs/00_项目规范/01_Codex协作规范_v1.md @@ -0,0 +1,3 @@ +# Codex 协作规范 v1 + +标准流程:只读调查 → 设计 → Step → 实现 → 构建/测试 → 完成报告 → Step 更新。没有调查结论不得凭猜测大改 Core。每个任务明确目标、必做项、允许/禁止范围、构建测试、必做输出、停止条件。无法完成必须明确阻塞,不伪造 PASS。默认禁止 force push、push upstream、改写官方 tag、删除历史 Step/报告、无授权扩大重构。 diff --git a/docs/00_项目规范/02_构建与验证规范_v1.md b/docs/00_项目规范/02_构建与验证规范_v1.md new file mode 100644 index 000000000..33074b46d --- /dev/null +++ b/docs/00_项目规范/02_构建与验证规范_v1.md @@ -0,0 +1,3 @@ +# 构建与验证规范 v1 + +主环境 Ubuntu 24.04 x86_64,默认并行度 3。Linux 优先官方 CMake/Make 路线,至少保证 OpenGL build、mbgl-render、mbgl-render-test-runner。Android 官方路线:Java17、.nvmrc Node、npm install --ignore-scripts;`cd platform/android && RENDERER=opengl BUILDTYPE=Debug make apackage` 与 `RENDERER=vulkan BUILDTYPE=Debug make apackage`。涉及 renderer/backend 必须双 backend 验证。Linux 不得宣称 iOS/Metal PASS,必须 Mac 同 commit 验证。基础回归只用离线 fixture。GPU 阶段记录粒子数、FPS/frame time、backend 与 pan/zoom/rotate/pitch。 diff --git a/docs/00_项目规范/03_Step阶段记录规范_v1.md b/docs/00_项目规范/03_Step阶段记录规范_v1.md new file mode 100644 index 000000000..fb8fcaf95 --- /dev/null +++ b/docs/00_项目规范/03_Step阶段记录规范_v1.md @@ -0,0 +1,3 @@ +# Step 阶段记录规范 v1 + +Step 连续编号 STEP-000/001/002,不跳号。状态只用:计划/进行中/阻塞/完成/冻结。每个 Step 至少有:状态、目标、基线、必做项勾选、允许修改、禁止修改、构建测试、必做输出、当前结果、阻塞、下一步。执行结束必须更新 Step、追加粒子开发记录、生成 docs/报告 完成报告;未完成项不得勾选。 diff --git a/docs/00_项目规范/04_Git与上游同步规范_v1.md b/docs/00_项目规范/04_Git与上游同步规范_v1.md new file mode 100644 index 000000000..bcf4a0fe3 --- /dev/null +++ b/docs/00_项目规范/04_Git与上游同步规范_v1.md @@ -0,0 +1,3 @@ +# Git 与上游同步规范 v1 + +upstream=MapLibre GitHub,仅 fetch,push=DISABLED;origin=用户 Gitea。日常分支 particle/main。提交小而单一。上游升级使用 upgrade/ 基于官方新 release 重放 Particle commits,完整回归后更新 PARTICLE_BASELINE.md;不得直接把 upstream/main 混进 particle/main。必须启用 git rerere。 diff --git a/docs/00_项目规范/05_粒子架构原则_v1.md b/docs/00_项目规范/05_粒子架构原则_v1.md new file mode 100644 index 000000000..5ef28b5b2 --- /dev/null +++ b/docs/00_项目规范/05_粒子架构原则_v1.md @@ -0,0 +1,3 @@ +# 粒子架构原则 v1 + +目标链:RasterArraySource → RasterArrayTile/Velocity Field → WeatherColorLayer + RasterParticleLayer → RenderRasterParticleLayer → Particle State A/B + GPU Update/Draw/Trail → MapLibre gfx/backend → OpenGL/Vulkan/Metal。WeatherColor 表达定量颜色,Particle 表达方向/运动,默认不得靠降低 WeatherColor opacity 才看导航信息。Wind=GLOBAL;Wave/Tide/Current=OCEAN_ONLY 共用 Land/Ocean Mask。Particle State 格式/坐标系/生命周期/跨 tile 必须单独设计后冻结。 diff --git a/docs/00_项目规范/06_官方基线来源_v1.md b/docs/00_项目规范/06_官方基线来源_v1.md new file mode 100644 index 000000000..e1b5b9c80 --- /dev/null +++ b/docs/00_项目规范/06_官方基线来源_v1.md @@ -0,0 +1,18 @@ +# 官方基线来源 v1 + +本项目初始化版本依据 MapLibre 官方资料固定: + +- MapLibre Native releases:`https://github.com/maplibre/maplibre-native/releases` +- Linux 开发文档:`https://maplibre.org/maplibre-native/docs/book/platforms/linux/` +- Render Tests:`https://maplibre.org/maplibre-native/docs/book/render-tests.html` +- Android release CI:MapLibre Native `.github/workflows/android-release.yml` +- Android `Versions.kt`:当前 core `18545ca` 的 `ndkVersion = 28.2.13676358`,`cmakeVersion = 3.24.0+`。 + +当前冻结: + +- Android `android-v13.4.1` → core `18545ca` +- iOS `ios-v6.28.0` → core `18545ca` +- Android compileSdk = 34 +- Java = 17 + +初始化脚本仍会运行时验证 Android/iOS tag 是否继续指向同一 core,避免只依赖本文档。 diff --git a/docs/00_项目规范/07_Android粒子Validation_APK构建规范_v1.md b/docs/00_项目规范/07_Android粒子Validation_APK构建规范_v1.md new file mode 100644 index 000000000..10b3b5307 --- /dev/null +++ b/docs/00_项目规范/07_Android粒子Validation_APK构建规范_v1.md @@ -0,0 +1,68 @@ +# Android 粒子 Validation APK 构建规范 v1 + +## 目的 + +固定 `MapLibreAndroidTestApp` 粒子验证 APK 的制作流程,避免每次重新调查 Gradle 参数、Java/Android SDK 环境、native strip 与 APK 体积异常。 + +权威入口: + +```bash +./tools/particle_android_validation/build_validation_apk.sh +``` + +除排错外,不再手工拼接 Validation APK 的 Gradle 命令。 + +## 固定构建契约 + +```text +App: MapLibreAndroidTestApp +Renderer: OpenGL +BuildType: Debug +ABI: x86_64 +Native debug symbols: strip +Gradle console: plain +``` + +核心参数固定为: + +```text +:MapLibreAndroidTestApp:assembleOpenglDebug +-Pmaplibre.abis=x86_64 +-Pmaplibre.validation.stripNativeDebugSymbols=true +--console=plain +``` + +## 必须执行的防错逻辑 + +1. 构建前删除目标 APK 文件,但不做全量 Gradle clean。 +2. Java 必须为 17;Android SDK 必须可解析。 +3. 构建失败时不得复用旧 APK。 +4. 构建成功后必须校验 APK ZIP 完整性。 +5. APK 内 native ABI 必须只有 `x86_64`。 +6. 必须包含 `lib/x86_64/libmaplibre.so`。 +7. 默认 `libmaplibre.so` 不得超过 80 MiB。 +8. 默认 APK 不得超过 100 MiB。 +9. 必须记录 SHA-256、commit、branch、dirty 状态和完整 Gradle 日志。 + +STEP-090/091 的正常 APK 参考体积约 57 MB。数百 MB 通常表示未 strip native debug symbols;约 95 MB 且 ZIP 逻辑内容仍约 60 MB,通常表示同一路径增量打包残留旧 ZIP 尾部数据。 + +## 标准产物目录 + +```text +artifacts/maplibre-particle/android-validation/ +├── MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk +├── build-info.txt +└── build.log +``` + +该目录为本地验证产物目录,不进入 Git。 + +## 边界 + +本 Pipeline 只负责构建和 APK 自验,不负责 ADB 安装、Activity 启动、设备交互或 STEP 业务场景验收。 + +Android 上机验证仍由后续独立步骤执行。构建成功不等于 STEP 场景已通过。 + +## 变更规则 + +若未来测试 App、ABI、Renderer、strip 策略或产物门槛发生正式变化,应先修改 Pipeline 脚本与本规范,再使用新流程;不得只在聊天或临时命令中形成新的事实基线。 diff --git a/docs/10_架构/20_粒子系统总体架构设计_v1.md b/docs/10_架构/20_粒子系统总体架构设计_v1.md new file mode 100644 index 000000000..0b560ca7b --- /dev/null +++ b/docs/10_架构/20_粒子系统总体架构设计_v1.md @@ -0,0 +1,773 @@ +# MapLibreParticleLab 粒子系统总体架构设计 v1 + +设计日期:2026-08-29 + +基线分支:`particle/main` + +基线 Core:`18545ca10d296704b2cba675b182ff2c8da24588` + +性质:正式总体架构设计,不包含实现 + +## 1. 设计输入与适用范围 + +本设计以项目总则、项目规范、`PARTICLE_BASELINE.md`、`docs/20_调查/10_架构调查报告_v1.md` 和当前 MapLibre Native 源码为事实基础。当前仓库不是 React Native 工程,因此架构从 Native Core、Android/iOS 平台薄接入和 MapLibre gfx 开始,不定义不存在的 JS 每帧运行时。 + +现有 `docs/10_架构/MapLibre粒子总体架构_v1.md` 只给出了 Layer 顺序和 GPU 组件候选,与本设计没有冲突。本文件把这些候选收敛为正式模块、接口和生命周期;后续以本文件作为 `30_最小技术验证` 和实现任务的主要架构输入。 + +固定长期边界继续有效:clean-room 独立实现;正式实现不用 React Native/WebView/外部 Overlay;Android/iOS 共用 C++ Core;OpenGL 为 Android 主开发 backend;WeatherColorLayer 与 RasterParticleLayer 分离;Wind=`GLOBAL`;Wave/Tide/Current=`OCEAN_ONLY` 并共用 Land/Ocean Mask;第一阶段只用离线 fixture。 + +## 2. 核心决策摘要 + +1. **唯一正式主路线**:新增独立 Core `RasterParticleLayer`、`RenderRasterParticleLayer` 和隔离的 `mbgl::particle` 模块,进入 Style/RenderTree,通过 MapLibre gfx 执行 GPU simulation、trail 和最终合成。 +2. `CustomLayerHost` 与 `CustomDrawableLayer` 不作为正式粒子引擎宿主;需要时只可用于一次性技术探针,不能进入公共 API、产品依赖或长期双轨。 +3. 新增 `RasterArraySource` 作为 Field 数据边界;WeatherColorLayer 与 RasterParticleLayer 可引用同一 Field snapshot 和 Mask,但拥有不同 RenderLayer、GPU 状态和生命周期。 +4. 每帧由 MapLibre render loop 驱动;活动粒子通过 Core render state 持续声明 `needsRepaint`,不建立第二套 UI/JS 动画循环。 +5. Camera/Matrix 只从当前 render frame 的 `PaintParameters/TransformParameters/TransformState` 读取。 +6. 第一版 GPU simulation 使用跨 backend 的 render-pass ping-pong,不以 compute 为前置;compute 仅作为后续 capability-gated 优化。 +7. 第一阶段正式视觉范围包含粒子、颜色映射和屏幕空间 trail;最小验证可先无 trail 验证状态更新,但在第一阶段冻结前必须通过 trail 验证。 +8. 第一阶段实现/日常验证范围是 Android OpenGL;Android Vulkan 在第一阶段冻结前做 gfx 兼容 gate;iOS OpenGL/Metal 保留相同 Core contract,在独立后续阶段验证。 +9. 下一阶段选择 **B:进入 `30_最小技术验证`**,不直接进入正式实现。 + +## 3. 候选路线比较与选择 + +| 候选 | 优点 | 不作为正式主路线的原因 | 定位 | +|---|---|---|---| +| `CustomLayerHost/RenderCustomLayer` | 已有同帧 transform 和 backend callback;示例可运行 | Android 使用 raw host 指针且 API 标记 experimental;平台 context/encoder 不对称;公共生命周期容易平台化 | 只允许一次性验证,不进入正式 API | +| `CustomDrawableLayer` | 已使用 gfx DrawableBuilder,跨 backend 程度较高 | 当前接口偏向线、面、符号、几何 drawable;不天然表达 velocity、state ping-pong、trail target 和 source contract | 作为 gfx/Drawable 设计参考 | +| 独立 Core particle layer | 可进入 Style、RenderTree、layer order、repaint、context lifecycle;可隔离数据与 GPU 资源;平台只需薄 wrapper | 需要新增 Layer/Source/RenderLayer、shader 注册和少量 RenderTarget 调度薄接入 | **正式唯一主路线** | +| MapView Overlay/独立 renderer | 开发表面上独立 | 与地图 frame、matrix、depth/layer order、context 和生命周期不同步;违反固定规则 | 拒绝 | + +正式路线的退出条件不是“以后再决定”:从第一个可保留实现开始,产品 API 和生产资源均必须归属独立 Core layer。若技术验证临时使用 CustomLayer,验证结束后仅保留测试结论;正式 `RasterParticleLayer` 可渲染第一张 fixture 且通过 lifecycle gate 时,任何 CustomLayer 粒子探针不得继续作为产品路径。 + +主路线风险是现有通用 RenderTarget/LayerGroup 是否足以表达多 offscreen pass、跨 backend 的资源状态转换,以及新增 Layer 对 `needsRepaint` 的最小接入是否能够保持 thin-hook。风险不通过平台分叉规避,而由第 26 节的独立 RenderLayer、State/Trail ping-pong 和 context recreation 验证决定;若验证失败,只允许调整粒子专用 gfx 抽象或必要薄 hook,不回退到 Overlay、JS loop 或两套平台引擎。 + +## 4. 粒子系统总体架构图 + +```text +Host Native API + ├─ RasterArraySource ───────────────┐ + ├─ WeatherColorLayer ───────┐ │ + └─ RasterParticleLayer ─────┼───────┘ + ▼ + Style immutable snapshot + ▼ + RenderTree / RenderOrchestrator / layer order + ┌────────────┴────────────┐ + ▼ ▼ + RenderWeatherColorLayer RenderRasterParticleLayer + │ + mbgl::particle Core + Field / Simulation / Mask / GPU / Controller + │ + gfx Texture/Buffer/RenderTarget/Drawable/Shader + │ + OpenGL / Vulkan / Metal backend → GPU +``` + +正式 Layer 顺序由宿主 Style 控制,推荐: + +```text +Base Map + → WeatherColorLayer + → RasterParticleLayer + → Navigation Reference / coast / labels + → AIS / Route / Ownship / Alarm +``` + +WeatherColor 保持定量色场;导航信息通过更高 Style Layer 显示,不以降低天气层透明度作为默认策略。 + +## 5. Core 模块边界与建议目录 + +```text +include/mbgl/style/sources/ + └─ raster_array_source.hpp 公共 Field Source API +include/mbgl/style/layers/ + ├─ weather_color_layer.hpp 定量色场 Layer API + └─ raster_particle_layer.hpp 粒子 Layer API + +src/mbgl/particle/ + ├─ field/ Field descriptor/snapshot/validation + ├─ data_adapter/ fixture 与未来数据适配 + ├─ simulation/ 状态语义、时间步、respawn + ├─ render/ pass 描述、frame parameters + ├─ gpu/ velocity/state/trail/mask resources + ├─ mask/ Land/Ocean Mask snapshot/cache/contract + └─ controller/ 状态机、revision、repaint、错误 + +src/mbgl/style/sources/ RasterArraySource Impl +src/mbgl/style/layers/ 两个 Layer Impl/properties +src/mbgl/renderer/sources/ RenderRasterArraySource +src/mbgl/renderer/layers/ + ├─ render_weather_color_layer.* + └─ render_raster_particle_layer.* +src/mbgl/layermanager/ 两个 LayerFactory 的薄注册 +shaders/particle/ update/draw/trail/composite shader contract +platform/android/ 类型安全 Java/Kotlin + JNI 薄 wrapper +platform/darwin/ ObjC/Swift Source/Layer peer 薄 wrapper +``` + +目标仍是约 95% 粒子代码位于新增文件。允许修改的官方既有文件只限 Layer/Source factory 注册、shader/build 清单、RenderTarget/repaint 所需的最小 hook;不得把算法写入 Map、RasterLayer、Camera、MapView 或 backend 主文件。 + +### 5.1 Particle Field + +`ParticleFieldDescriptor` 描述:field id/type、revision、语义单位、bounds、CRS、grid registration、尺寸、U/V、NoData、时间片、display domain 和数据版本。`ParticleFieldSnapshot` 是 descriptor 与只读数据的不可变组合。 + +Field 数据进入 Core 后统一为:U 为正东、V 为正北、canonical simulation unit 为米/秒。原始数据单位转换属于 Data Adapter,不进入每帧 shader 的业务分支。 + +### 5.2 Particle Data Adapter + +第一阶段 `OfflineFixtureParticleAdapter` 负责读取、校验、字节序转换并产生 immutable snapshot。未来真实天气协议必须通过新的 adapter 转成同一 snapshot,Renderer 不认识 URL、HTTP、GRIB、厂商字段名或 Server 协议。 + +### 5.3 Particle Simulation + +Core 定义状态语义、固定/受限 timestep、field sample、NoData、mask、respawn 和 deterministic seed。正式每粒子更新在 GPU;CPU 只负责 fixture 解码、验证、测试参考值和资源描述,不允许每帧 CPU 粒子循环冒充正式实现。 + +### 5.4 Particle Render + +负责 simulation pass、particle draw、trail fade/accumulation、最终 translucent composite、viewport 和当前 frame transform。它不拥有天气色场 RenderLayer。 + +### 5.5 Particle GPU Resources + +负责 velocity texture、state A/B、trail A/B、mask texture、uniform、shader set、context generation、创建/释放/重建。公共逻辑只使用 gfx 抽象;backend-specific 格式映射、barrier/encoder 细节留在 gfx/backend。 + +### 5.6 Particle Mask + +Mask 是可共享的 immutable snapshot 与 GPU resource,不属于某一个 RasterParticleLayer。Wave/Tide/Current 和未来 OCEAN_ONLY field 通过 mask id/revision 引用同一资源。 + +### 5.7 Particle Controller / Layer State + +Controller 只管理 enable、source/mask revision、map/data/GPU readiness、状态机、repaint 和错误;UI/宿主不得持有或销毁 GPU texture。 + +## 6. 与 Style、RenderTree 和 gfx 的关系 + +- `RasterArraySource` 进入 Style Source 系统,负责提供 immutable Field snapshot,不直接绘制。 +- `WeatherColorLayer` 和 `RasterParticleLayer` 分别引用 source id,进入 Style Layer、LayerFactory、RenderLayer 和 RenderTree。 +- `RenderRasterParticleLayer` 最终合成属于 `Translucent` pass,按 Style layer index 与其他地图内容排序。 +- simulation 与 trail 使用独立 offscreen render targets,在主 buffer 合成前完成;最终 composite drawable 仍在该 Layer 的 translucent 位置绘制。 +- Layer 活动且可见时参与 `needsRepaint`;首次 enable、数据/选项变化和恢复时调用一次现有 invalidate/`triggerRepaint` 路径唤醒渲染。 +- 每帧 transform 直接来自当前 `PaintParameters`,不缓存 UI 线程推测值。 + +### 6.1 复用的 gfx 能力 + +| 现有能力 | 用途 | +|---|---| +| `Texture2D` | velocity、state、mask、trail texture | +| `DynamicTexture` | 仅在技术验证证明适合局部 field 更新时使用;不是默认 state 资源 | +| Buffer/UniformBuffer | quad/point geometry、frame/field/options uniform | +| `RenderTarget` | simulation output、trail accumulation | +| `DrawableBuilder` | 最终 composite 与可由通用 drawable 表达的 draw geometry | +| `DrawableTweaker` | 同帧 transform/options uniform 更新 | +| Shader registry | 后端 shader 注册与查询 | +| UploadPass | field/mask/静态资源低频上传 | +| CommandEncoder | pass encoding/present,由 render thread 和 gfx 管理 | + +新增粒子专用抽象仅描述语义:`ParticleVelocityResource`、`ParticleStateResources`、`ParticleTrailResources`、`ParticleMaskResource`、`ParticleShaderSet`、`ParticleFrameParameters`。这些类型不能暴露 GL/Vulkan/Metal handle。 + +## 7. WeatherColorLayer 与 RasterParticleLayer + +| 边界 | WeatherColorLayer | RasterParticleLayer | +|---|---|---| +| 目的 | 定量连续色场 | 方向、运动、动态流线 | +| RenderLayer | 独立 `RenderWeatherColorLayer` | 独立 `RenderRasterParticleLayer` | +| GPU 状态 | color ramp/field sampling 资源 | state A/B、simulation、trail、particle draw | +| 动画 | 不承担粒子动画 | 由 MapLibre frame loop 驱动 | +| 可共享 | RasterArraySource snapshot、grid metadata、time slice、Mask snapshot/cache | 同左 | +| 不共享 | RenderLayer、粒子 state、trail target、动画状态 | 同左 | + +二者不是同一 Layer 的模式开关。删除或禁用粒子 Layer 不影响 WeatherColor;更换天气颜色样式不重置 Particle State,除非 Field revision、Mask 或影响 simulation 的参数同时改变。 + +## 8. Field 与显示域模型 + +`FieldType` 第一版至少包含 `WIND`、`WAVE`、`TIDE`、`CURRENT`。`DisplayDomain` 只有 `GLOBAL` 和 `OCEAN_ONLY`。 + +规则由 Core validation 强制执行: + +- WIND → `GLOBAL`,陆地与海洋均可显示,不依赖 Land/Ocean Mask。 +- WAVE/TIDE/CURRENT → `OCEAN_ONLY`,必须有有效 Mask;mask 缺失时 fail closed,不在陆地或未知区继续显示。 +- 未来新增 OCEAN_ONLY field 复用同一 Mask contract,不创建 field-specific mask engine。 + +Field snapshot 的 revision 单调变化。只改变 UI 可视属性不生成 Field revision;U/V、bounds、grid、time slice、NoData 或数据版本变化必须生成新 revision。 + +## 9. 第一阶段离线 fixture contract v1 + +### 9.1 目录与文件 + +每个 fixture 目录固定包含: + +```text +particle-fixtures// +├─ metadata.json +├─ field.f32 +└─ mask.u8 仅 OCEAN_ONLY/Mask 场景必需 +``` + +`metadata.json` 最小结构: + +```json +{ + "schema": "maplibre-particle-fixture/v1", + "fixtureId": "uniform-east", + "fieldType": "WIND", + "displayDomain": "GLOBAL", + "dataVersion": 1, + "timeIndex": 0, + "timestamp": null, + "crs": "EPSG:4326", + "bounds": { "west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0 }, + "grid": { + "width": 64, + "height": 32, + "registration": "node", + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "vector": { + "uPositive": "east", + "vPositive": "north", + "unit": "m/s", + "layout": "interleaved-uv", + "encoding": "float32-le", + "noData": -9999.0, + "file": "field.f32" + }, + "mask": { "mode": "none" }, + "integrity": { + "fieldByteLength": 16384, + "fieldSha256": "" + } +} +``` + +实际 width/height 和 checksum 由 fixture 生成任务填写,上例数值只展示 contract,不冻结具体场景分辨率。 + +### 9.2 Binary 规则 + +- `field.f32`:little-endian IEEE-754 float32,row-major,按 `u0,v0,u1,v1...` 交错;文件长度必须等于 `width × height × 2 × 4`。 +- 任一 cell 的 U 或 V 等于 `noData`,整个 cell 无效;不依赖 NaN payload 表达 NoData。 +- adapter 校验有限值、长度、checksum、bounds、grid 和 domain 后,转换为 canonical immutable Field。 +- `mask.u8`:row-major、与 metadata 指定 grid 对齐;`0=LAND`、`255=OCEAN`,`1..254` 为未来 coverage/shore antialias 保留,v1 validator 不接受未声明的中间值。 +- Mask 文件长度必须等于 mask width × height,并记录 byte length/SHA256。 +- v1 fixture 的 `bounds.west < bounds.east`,单文件不跨反经线;全球使用 `[-180,180]`。运行时 Field/Mask 模型仍支持以 unwrapped longitude interval 表达未来反经线数据。 +- `timeIndex` 必须存在;有真实时间语义时同时填写 UTC ISO-8601 `timestamp`,确定性 synthetic fixture 可令其为 `null`。 + +### 9.3 Mask metadata + +OCEAN_ONLY 场景把 `mask` 写为: + +```json +{ + "mode": "embedded", + "maskId": "coast-v1", + "dataVersion": 1, + "file": "mask.u8", + "encoding": "uint8-land-ocean", + "width": 64, + "height": 32, + "registration": "node", + "origin": "north-west", + "noDataPolicy": "land" +} +``` + +### 9.4 预留 fixture 覆盖 + +| fixture | 合同目标 | +|---|---| +| `uniform-east` | WIND/GLOBAL,常量 +U,验证东西向和平移/wrap | +| `uniform-north` | WIND/GLOBAL,常量 +V,验证纬向和 projection | +| `vortex` | WIND/GLOBAL 或明确区域 bounds,验证旋转方向和采样连续性 | +| `shear` | WIND/GLOBAL 或区域 bounds,验证速度梯度和 timestep | +| `coastal-mask` | CURRENT/OCEAN_ONLY,必须含 mask,验证海岸裁剪和 fail-closed | + +后续 fixture 可增加 west/south、双向 vortex、divergence/convergence,但不改变 v1 contract。 + +## 10. Offline Fixture 到 GPU 数据流 + +```text +Offline Fixture files + → Worker: read / checksum / decode / validate + → ParticleFieldSnapshot(shared immutable, revision N) + → Map/Style thread: RasterArraySource Impl snapshot + → RendererFrontend immutable UpdateParameters + → Render thread: compare revision + → UploadPass: velocity texture + optional mask texture + → ParticleState A/B initialization + → Simulation pass + → Particle draw + Trail A/B + → RasterParticleLayer translucent composite + → MapLibre main render pass → GPU +``` + +### 10.1 低频步骤 + +fixture I/O、checksum、decode、unit normalization、Field validation、CPU snapshot 创建、velocity/mask upload、shader/resource 创建。仅在 source revision、mask revision、context generation、viewport target size或影响资源布局的选项变化时执行。 + +### 10.2 每帧步骤 + +读取当前 frame transform;计算受限 timestep;state A→B 或 B→A simulation;particle draw;trail fade/accumulate;final composite;更新 rendering stats;在仍活动时声明下一帧 repaint。 + +### 10.3 Camera 变化时 + +更新 transform、pixel ratio、viewport、visible field intersection、world-wrap selection 等 uniform。v1 不重建 velocity texture,不重新解析 Field,不重建 Particle State;viewport resize 只重建/清空 trail target。任何 pan/zoom/bearing/pitch/resize 的有效变化清空旧 screen-space trail,避免错误残影;后续可验证 trail reprojection,但不属于 v1。 + +## 11. 线程模型 + +```text +Map/UI thread + ├─ 创建/删除 Source、Layer + ├─ 修改业务 options、enable、source/mask reference + └─ 发布 immutable Impl/snapshot revision + │ + ▼ +Worker thread + ├─ fixture I/O、decode、validate、unit normalize + └─ 产出 shared_ptr + │ ownership/immutable snapshot + ▼ +Render thread + ├─ snapshot revision diff + ├─ GPU resource create/upload/release/recreate + ├─ 当前 frame Camera/Matrix + └─ encode simulation/trail/draw/composite + ▼ +GPU + └─ velocity sample、particle update、trail、rasterization +``` + +- Field 解码:Style 所属 worker/thread pool 或独立受控 worker;不得在 render frame 内 decode。 +- CPU 插值:只用于 validator、测试参考或查询 API;正式每帧 field sampling 在 GPU。 +- GPU resource 创建/释放:持有有效 `gfx::BackendScope` 的 render thread。 +- 每帧 simulation:render thread 编码,GPU 执行。 +- Camera Matrix:当前 `PaintParameters.transformParams/state`。 +- Data 交接:`shared_ptr`/MapLibre Immutable snapshot + monotonic revision;render thread 原子替换当前引用后再上传。 +- 避免复制:decode 只产生一次 owning buffer;source/render snapshot 共享 const ownership;只在 GPU revision 变化时上传,不通过 Java/ObjC/JS 每帧传大数组。 +- context loss:平台/backend 通知 Core;render thread 丢弃 GPU generation,CPU snapshot 保留;新 context 下 render thread 重建。 + +## 12. Camera / Matrix 同帧同步 + +```text +Map camera mutation + → TransformState snapshot in UpdateParameters + → RenderTree TransformParameters + → PaintParameters for current frame + ├─ state: center/zoom/bearing/pitch/size/frustum + ├─ proj/aligned/near-clipped matrix + └─ pixel ratio/frame count/time + → ParticleFrameParameters + → simulation visible domain + particle draw/composite +``` + +规则: + +- pan/zoom/bearing/pitch 全部使用当前 render frame snapshot;不接受 UI/JS 另行推算的 Camera。 +- position state 存储与 Camera 无关的 Field-normalized/geographic 语义位置;绘制时转为 canonical Web Mercator/world coordinate,再使用当前 frame matrix。 +- resize/pixel ratio 改变更新 viewport uniform,并重建 trail target;state 和 velocity texture保持。 +- GLOBAL field 的 longitude 在 canonical interval 内 wrap;绘制时选择距离当前 map center 最近的 unwrapped world copy。 +- 区域 Field 以 descriptor 的 unwrapped longitude interval 判断范围;v1 fixture 不跨反经线,运行时 contract 保留跨反经线能力。 +- projection/unprojection 只复用 MapLibre Core utility/transform,不在平台层复制公式。 +- v1 Camera 任意有效变化清空 screen-space trail;粒子位置继续保留并以新 transform 绘制。 + +## 13. 帧驱动与动画时钟 + +唯一 frame driver 是 MapLibre render loop。 + +1. enable 或资源 ready 时,Controller 通过现有 Core invalidate/`triggerRepaint` 路径唤醒第一帧。 +2. `RENDERING` 且可见、Field/GPU ready 时,Particle Render 在当前 RenderTree 中声明 `needsRepaint=true`,使 MapLibre 请求下一帧。 +3. 地图静止但粒子活动时,以上机制持续;不依赖 camera transition。 +4. disable、Layer 不可见、App background、View 不可见、Field 不可用、ERROR 或 layer removal 时停止额外 repaint。 +5. App foreground/context restore 后,只有资源重新 READY 且 layer enabled/visible 才恢复。 +6. simulation time 使用 monotonic render time;采用固定逻辑步长加有界 accumulator,过长 pause 后不追赶所有丢失步,避免恢复瞬间爆炸。 + +平台 `requestRender`/CADisplayLink 只是现有 MapLibre frontend 的落地机制,不形成第二套粒子时钟。 + +## 14. GPU 粒子状态模型 + +### 14.1 冻结的逻辑状态 + +每个 particle 逻辑上至少包含: + +- `position`:Field-normalized 2D position,可无损映射到 descriptor bounds;与屏幕 Camera 无关。 +- `age`:秒或归一化寿命语义。 +- `seed`:稳定随机/respawn 输入。 +- `alive/valid`:可由 age/特殊值派生,不要求独立 channel。 + +Exact texture format、channel packing 和最终最大 particle count **不在本阶段冻结**;它们必须在 Camera correctness 和 backend capability 验证后选择。 + +### 14.2 Ping-pong 与 simulation + +- `Particle State A/B` 同尺寸,当前帧只读 current、只写 next,帧末交换。 +- texture texel count 至少覆盖 particle count;多余 texel 标记 inactive。 +- velocity texture 保存 canonical U/V 与 validity;shader 按 descriptor 将物理速度转换为 Field-normalized displacement。 +- timestep 使用受限固定步长;speed factor 是业务参数,不能改变原始 Field snapshot。 +- Field bounds 外、NoData、age 到期或 OCEAN_ONLY mask 非 ocean 时执行 deterministic respawn。 +- respawn domain 优先取 Field bounds 与当前可见 geographic bounds 的有效交集;交集为空时不绘制且不忙循环重建资源。 +- viewport clipping 只影响 draw,不以离开屏幕立即销毁 state;region/domain 无效时才 respawn。 + +### 14.3 为什么第一阶段不用 compute 作为前置 + +当前目标同时覆盖 Android OpenGL/Vulkan 与后续 iOS Metal,而现有跨 backend 最稳定的共同语义是 render pass、texture、render target 和 shader registry。第一阶段以 full-screen/texture render-pass simulation 验证主架构,避免先冻结 Vulkan/Metal compute pipeline、workgroup 和 barrier contract。 + +后续 compute 只能作为 `ParticleSimulationBackend` 的 capability-gated 实现;逻辑 state、Field、Mask、Controller 和公共 API 不变。无 compute 时继续使用 render-pass simulation,不回退到正式 CPU 每粒子循环。 + +## 15. Trail 架构 + +第一阶段正式视觉范围包含 trail,但最小验证顺序允许先验证无 trail 粒子点/短线,再增加 trail gate。 + +```text +Trail Current texture + → Fade pass → Trail Next + → Particle draw blend into Trail Next + → Composite Trail Next at RasterParticleLayer position + → swap Current/Next +``` + +- 使用 screen-space Trail A/B,避免依赖所有 backend 对同一 attachment 同时读写的行为。 +- accumulation/fade 与 particle draw 是独立 pass;最终 composite 使用 premultiplied alpha contract。 +- resize/pixel ratio/render target scale 变化:重建并 clear A/B。 +- pan/zoom/bearing/pitch/frustum 有效变化:v1 clear A/B,不错误复用旧屏幕轨迹。 +- Field/mask revision、simulation reset、disable、style reload、context loss、App background、ERROR、layer removal:clear 或释放。 +- opacity/trail fade 的纯样式变化可从下一帧生效;是否清空由 property 语义明确,改变 fade 不强制清空,改变坐标/particle size 可清空。 +- context loss 后不恢复旧 trail 内容,只重建空 target。 + +## 16. Land/Ocean Mask 设计 + +### 16.1 数据与资源 + +Mask 数据属于独立 `ParticleMask` 模块。CPU 层是 `LandOceanMaskSnapshot`:mask id/revision、CRS、bounds、grid、registration、coverage policy 和 immutable byte data。GPU 层是按 `(maskId, revision, backend context generation)` 缓存的 texture,可被多个 OCEAN_ONLY Layer 共享。 + +第一阶段使用 fixture `mask.u8` GPU texture;CPU geometry 只允许作为未来 mask 生产/测试输入,不在每帧做 point-in-polygon。未来可扩展 tiled/pyramid mask,但保持同一 snapshot/sample contract。 + +### 16.2 坐标、分辨率和采样 + +- CRS 为 EPSG:4326,bounds/grid registration 与 Field contract 相同。 +- fixture v1 mask 默认与对应 Field 同 bounds/width/height;架构允许独立分辨率,shader 使用各自 transform。 +- binary 只接受 LAND/OCEAN;未知/NoData 对 OCEAN_ONLY 按 LAND 处理。 +- v1 采用 nearest/明确阈值采样,避免线性过滤把海值渗入陆地。未来 coverage mask 可显式声明 coverage encoding 后再允许平滑。 +- zoom 不触发 mask 重建;未来 pyramid 只在 LOD 变化时切换 resource revision。 +- bearing/pitch 不改变 mask sample:particle 在 geographic/Field space 采样,再与地图使用同帧 transform 绘制。 +- GLOBAL Wind 完全绕过 mask branch/resource requirement。 + +### 16.3 wrap、反经线与海岸 artifact + +- 全球 mask longitude 周期 repeat;区域 mask 使用 unwrapped interval。 +- 反经线数据拆分或使用 unwrapped bounds,不能用 `west > east` 的模糊单文件语义。 +- 海岸 artifact 风险包括 mask 分辨率不足、Field 与 Mask registration 不同、nearest 锯齿、trail 残留和海图海岸语义差异。 +- v1 通过严格 metadata validation、nearest sample、未知即陆地、Camera 变化清 trail 和 coastal fixture 控制;最终生产 Mask 来源/分辨率留待后续数据设计与 NavSea 语义 Gate。 + +### 16.4 更新、缓存和恢复 + +- 仅 mask id/revision/context generation 改变时重新 upload;Camera 和每帧 animation 不重建。 +- CPU snapshot 可跨 context 保留;GPU texture 由 render thread 在新 context 重建。 +- mask 创建失败时 OCEAN_ONLY layer 进入 ERROR/不绘制,不能降级为 GLOBAL。 + +## 17. Land/Ocean Mask 共用图 + +```text +LandOceanMaskSnapshot(maskId, revision) + │ + ▼ +Shared ParticleMaskCache + ├─ CPU immutable bitmap + └─ GPU mask texture per context generation + │ + ┌──────┼─────────┬──────────────┐ + ▼ ▼ ▼ ▼ + Wave Tide Current Future OCEAN_ONLY + +Wind/GLOBAL ─────────────────────→ bypass Mask +``` + +## 18. 生命周期状态机 + +```text +DISABLED + └─ enable → WAITING_MAP +WAITING_MAP + ├─ map/render context ready, no field → WAITING_FIELD + └─ disable/remove/destroy → DISABLED +WAITING_FIELD + ├─ valid field snapshot → PREPARING + └─ invalid field → ERROR +PREPARING + ├─ GPU resources ready → READY + ├─ context lost → CONTEXT_LOST + └─ resource/shader failure → ERROR +READY + ├─ enabled + visible + foreground → RENDERING + ├─ background/invisible → PAUSED + └─ new field/mask revision → PREPARING +RENDERING + ├─ background/invisible → PAUSED + ├─ context lost → CONTEXT_LOST + ├─ new resource revision → PREPARING + ├─ disable/remove → DISABLED + └─ failure → ERROR +PAUSED + ├─ foreground/visible, resources valid → READY → RENDERING + ├─ context lost → CONTEXT_LOST + └─ disable/remove/destroy → DISABLED +CONTEXT_LOST + ├─ new context + CPU snapshot valid → PREPARING + └─ disable/remove/destroy → DISABLED +ERROR + ├─ valid new field/options/manual retry/context generation → PREPARING + └─ disable/remove/destroy → DISABLED +``` + +责任: + +- Map/Style thread 管理 Layer/Source 存在性、enable 和 immutable revision。 +- Worker 管理 decode job 的取消和 CPU snapshot。 +- Render thread 管理所有 GPU resource 和 context generation。 +- GPU 不拥有跨 Map 生命周期的资源责任。 +- Style reload 删除旧 RenderLayer 时先停止 repaint,再释放 GPU;同 id 新 layer 按新实例从 WAITING_FIELD/PREPARING 开始。 +- Map destroy/layer removal 是终止边界:取消 worker callback、断开 observer、释放 render resources,不自动复活。 + +## 19. 每帧渲染链图 + +```text +MapLibre requests frame + → RenderTree prepared with current TransformState + → RenderRasterParticleLayer checks state/revisions + → Upload only if Field/Mask/resource revision changed + → Simulation pass: State Current → State Next + → Trail fade: Trail Current → Trail Next + → Draw particles into Trail Next (Mask/domain applied) + → Main translucent pass: composite Trail Next by layer index + → present + → active? needsRepaint=true : stop extra repaint +``` + +## 20. Android 薄接入 + +```text +Kotlin/Java Source + Layer API + → typed JNI peer (no raw host pointer) + → Core RasterArraySource / RasterParticleLayer + → RenderRasterParticleLayer / mbgl::particle / gfx + → Android OpenGL or Vulkan backend + → existing MapRenderer requestRender + Surface lifecycle +``` + +- Java/Kotlin 只表达 source/layer/options/status;不接收 texture id、不执行每帧 simulation、不持有 GPU resources。 +- 正式接口需要类型安全 JNI wrapper;禁止把 `CustomLayer(String,long)` 的 raw `long CustomLayerHost` 作为粒子 API。 +- SurfaceView/TextureView 的 surface/context recreation 统一转为 Core context generation/lifecycle;不在两种 View 内分别写 particle engine。 +- OpenGL/Vulkan 差异留在 gfx shader/resource implementation;Core logical resources 和 pass graph 相同。 +- repaint 复用 `Map::triggerRepaint` → `AndroidRendererFrontend` → `MapRenderer.requestRender`,平台不增加粒子 timer。 +- 第一阶段 Android OpenGL 完成日常开发;Vulkan 在冻结前验证 state/trail/mask/context-loss,不另写业务逻辑。 + +## 21. iOS 薄接入 + +```text +ObjC/Swift Source + Layer peer + → Core RasterArraySource / RasterParticleLayer + → RenderRasterParticleLayer / mbgl::particle / gfx + → OpenGL or Metal backend + → existing MLNMapView RendererFrontend / CADisplayLink / draw callback +``` + +- `MLNCustomStyleLayer`、`MLNCustomDrawableStyleLayer` 和 `MLNBackendResource` 是已确认能力与验证参考,不是正式粒子引擎所有者。 +- 正式 iOS API 应是 Core Source/Layer 的 ObjC peer;ObjC/Swift 不保存 particle state、不编写第二套 simulation。 +- CADisplayLink 继续驱动 MapLibre view;粒子通过 Core `needsRepaint` 使 MapLibre 保持活动,不添加独立 display link。 +- Metal/OpenGL resource 与 encoder 只在 gfx/backend 内可见;平台 wrapper 不暴露给业务 API。 +- iOS 最终 backend 和设备能力在后续专项验证;不能据当前源码宣称 Metal/OpenGL 已通过粒子 gate。 + +## 22. 最小公共 API 设计 + +以下为语义 API,不冻结具体 C++/Java/ObjC 方法签名: + +```text +RasterArraySource(id) + setField(ParticleFieldSnapshot) + clearField() + getRevision()/getDescriptor() + +RasterParticleLayer(id, sourceId) + setEnabled(bool) + setDisplayDomain(DisplayDomain) 受 FieldType 固定规则校验 + setParticleOptions(ParticleOptions) + setMaskId(optional maskId) OCEAN_ONLY 必需 + getParticleOptions() + getStatus()/getLastError() + +Style + add/remove RasterArraySource + add/remove RasterParticleLayer + observe ParticleStatus changes 低频状态,不是每帧 telemetry +``` + +数据更新通过 Source revision,而不是把 field 数组设为 Layer paint property。公共 API 禁止 GL texture id、VkImage、MTLTexture、command encoder、backend workgroup、raw native pointer。 + +Layer display domain 必须符合业务规则;例如尝试把 CURRENT 改为 GLOBAL 返回 validation error,不静默放宽。 + +## 23. 配置分层 + +### 23.1 稳定业务参数 + +- particle count(目标值,受 capability/memory clamp) +- speed factor +- lifetime +- opacity +- particle size/line width +- trail enabled +- trail fade +- color ramp/reference +- enabled/visibility + +业务参数具有 backend-independent 范围、默认值和 validation;具体默认值在最小验证后冻结。 + +### 23.2 内部 backend 参数 + +- state/velocity/trail texture format +- buffer alignment、binding index +- render target channel type/scale +- shader variant/defines +- compute workgroup/barrier(未来) +- upload staging、sampler/filter 细节 + +这些由 capability profile 与 gfx implementation 决定,不能出现在产品业务 API 或 fixture contract。 + +## 24. 错误与降级策略 + +| 情况 | 行为 | 重试 | +|---|---|---| +| metadata/binary/checksum/Field 无效 | Source 拒绝新 revision;Layer 保留最后一个有效 snapshot或进入 WAITING_FIELD/ERROR | 仅新数据 revision | +| Field 与 viewport 无交集 | READY 但不绘制、不持续空忙 repaint | Camera/Field 改变时恢复 | +| OCEAN_ONLY Mask 缺失/无效 | fail closed,Layer ERROR;地图继续 | 有效 mask revision | +| GPU resource 创建失败 | 释放本次部分资源,Layer ERROR;其他 Layer 不受影响 | 一次受控低质量 profile 可选,否则人工/新配置 | +| backend 不支持必要 render target/format | Layer ERROR/UNSUPPORTED;不回退 CPU 正式粒子 | backend capability 变化或实现更新 | +| context loss | 停止 repaint,丢弃 GPU generation,保留 CPU snapshot,进入 CONTEXT_LOST | 新 context 自动 PREPARING | +| shader 创建失败 | 释放 particle shader/resource,Layer ERROR | 新 context/shader revision 时一次 | +| memory pressure | 先释放 trail、闲置 cache;允许按内部 profile 降低 trail scale/particle count并记录状态 | 有界一次;仍失败则 ERROR | +| Layer removal/Map destroy | 取消任务、停止 callback/repaint、释放 GPU/CPU ownership | 不自动重试 | + +任何错误不得使 MapLibre Map、其他 Style Layer 或平台 View 崩溃。自动重试必须按 revision/context generation 去重,禁止每帧重试形成死循环。 + +## 25. 性能预算与后续测量 + +以下是设计目标,不是已验证结论。正式 PASS 必须绑定具体设备、分辨率、backend、build type、fixture 和 particle count。 + +### 25.1 第一阶段 Gate 指标 + +| 指标 | 设计目标/测量档位 | +|---|---| +| particle count | 16K correctness、32K target、64K stress;最终默认/最大值待验证 | +| FPS/frame time | 声明设备上 32K 的目标下限 30 FPS/p95 ≤33.3ms;不得写成已达到 | +| Particle CPU time | 32K target p95 ≤3ms/frame,不含 MapLibre 基础渲染 | +| Particle GPU time | 32K target p95 ≤12ms/frame,分别记录 simulation/trail/composite | +| Particle GPU memory | 1080p、32K target ≤64MiB,含 state/trail/velocity/mask,不含 base map | +| Field upload | 1024×1024 U/V target ≤200ms,且只在 revision 变化时发生 | +| Context recreation | target ≤1s 恢复可绘制;旧 trail 不要求恢复 | +| correctness | pan/zoom/bearing/pitch/resize/wrap/mask/context loss 无崩溃、无明显错位 | + +若没有声明的 baseline 设备,只能报告测量值,不能判定性能 PASS。第一阶段必须提供 CPU/GPU time、资源字节数、upload/recreate time 的 instrumentation;60 FPS、最终最大粒子数、最佳格式和厂商优化属于后续优化。 + +## 26. 后续最小技术验证清单 + +| 优先级 | 验证 | 是否阻塞正式实现 | 可能改变什么 | 最小方式 | +|---|---|---|---|---| +| 1 | 独立 Core RenderLayer 在 Android OpenGL 中进入正确 Style order、读取同帧 transform并持续/停止 repaint | 是 | 主入口与 frame driver | 仅一层、单 fixture、固定少量 state 的 RenderTree probe;不做完整视觉 | +| 2 | gfx render-pass State A/B + Trail A/B 在 Android OpenGL/Vulkan 的创建、采样、交换、barrier和 context recreation | 是 | render-pass simulation 是否可作为共同基线、资源抽象边界 | 最小 ping-pong pattern + trail target,Surface recreate 后重建;记录两 backend | +| 3 | Camera/wrap/Mask contract | 是 | position semantic、trail clear 和 Mask sampling | uniform-east/north + coastal-mask,覆盖 pan/zoom/bearing/pitch/resize/反经线邻近视图 | +| 4 | iOS Core layer 薄 peer 与 Metal/OpenGL 同帧 gfx 资源可达性 | 不阻塞 Android 第一阶段;阻塞 iOS 实现 | iOS wrapper/backend adapter 细节,不得改变 Core engine | Mac/iOS 同 commit 的最小 Source/Layer/render-target probe | + +Android raw `CustomLayerHost` 不再是正式路线的阻塞项,不专门扩展产品 API;若独立 Layer probe 需要对照,可运行现有 CustomLayer 示例,但其结果只作诊断。 + +## 27. 明确不采用的架构 + +| 不采用方案 | 原因 | +|---|---| +| React Native/JS 每帧粒子 | 当前仓库无 RN runtime;高频大数组/Camera/frame 跨桥;无法与 render frame 严格同步 | +| MapView 外部 Overlay | 独立 surface/frame/matrix/layer order;Camera/pitch/context lifecycle 易错;违反固定边界 | +| 每种海洋场各自一套 Mask | 数据重复、海岸语义分叉、缓存和 artifact 不一致;违反统一 Mask 决策 | +| 直接改造 RasterLayer 承担粒子 | 普通 RasterLayer 是静态 raster semantics;会污染官方层并混合数据、simulation、trail | +| Android/iOS 各写一套引擎 | 状态、Field、Mask、生命周期和算法会分叉;违反共享 C++ Core | +| CustomLayer 作为长期产品主宿主 | Android raw host 与 iOS context API 不对称,生命周期/API 难以稳定;不利于 Source/Layer/RenderTree 正式集成 | +| 每帧重建 Field texture | 造成 upload、allocation、driver 和内存抖动;Field 应按 revision 缓存 | +| 每帧大数组跨 Java/ObjC/JS | 复制、序列化和 GC/ARC 压力;只允许低频 immutable snapshot/revision | +| backend-specific GPU API 暴露给业务 | 破坏跨平台、测试和升级边界;handle 只能在 gfx/backend 内部 | +| 正式 CPU 每粒子循环 | 不满足 GPU Particle Engine 目标;CPU 只做 decode/validation/reference test | +| compute-only 第一版 | 当前跨 backend capability 未验证,会过早冻结 workgroup/barrier;先用 render-pass simulation | + +## 28. Camera / Matrix 同步图 + +```text +UI/Native camera API + → mbgl::Map / TransformState + → immutable UpdateParameters + → RenderTree TransformParameters + → current PaintParameters + └→ ParticleFrameParameters + ├→ visible Field intersection / world copy + ├→ GPU simulation uniforms + └→ draw/composite matrix + +Independent UI/JS camera guess ─X→ Particle Renderer +``` + +## 29. Core 模块边界图 + +```text +Style API + ├─ RasterArraySource ─→ ParticleFieldSnapshot + ├─ WeatherColorLayer ─→ RenderWeatherColorLayer + └─ RasterParticleLayer + │ + ▼ +RenderRasterParticleLayer ← current PaintParameters + │ + ▼ +mbgl::particle + ├─ DataAdapter → Field + ├─ Controller → lifecycle/repaint/revision + ├─ Simulation → logical state/timestep/respawn + ├─ Mask → shared domain filter + ├─ GPUResources → velocity/state/trail/uniform + └─ Render → offscreen passes/composite + │ + ▼ +gfx/backend only +``` + +## 30. 架构冻结项 + +以下内容从本设计起冻结,后续普通实现 Step 不得自行改变: + +1. 主路线是独立 Core `RasterParticleLayer/RenderRasterParticleLayer`,进入 Style/RenderTree/gfx;CustomLayer/CustomDrawable 不是正式宿主。 +2. Android/iOS 共用 C++ `mbgl::particle` Field、Simulation、Render、GPU resource contract、Mask、Controller 和状态机;平台只有类型 peer、backend lifecycle 与 capability 薄接入。 +3. `RasterArraySource` 是 Field 数据边界;Field snapshot immutable、revisioned、canonical U-east/V-north/SI unit。 +4. WeatherColorLayer 与 RasterParticleLayer 是不同 Layer/RenderLayer/GPU 状态/动画生命周期,只共享数据 snapshot、grid/time metadata 和 Mask。 +5. Wind=`GLOBAL`;Wave/Tide/Current=`OCEAN_ONLY`;OCEAN_ONLY 统一共享 Land/Ocean Mask,缺失时 fail closed。 +6. fixture contract v1 使用 `metadata.json + field.f32 + optional/required mask.u8`、float32 little-endian interleaved UV、uint8 mask、checksum 和明确 grid/bounds/NoData。 +7. frame driver 是 MapLibre render loop;active layer 贡献 `needsRepaint`,没有独立 UI/JS 主时钟。 +8. Camera/Matrix 来自同一 render frame 的 Paint/Transform parameters;position state 与 Camera 无关。 +9. GPU simulation 的第一共同基线是 render-pass State A/B ping-pong;正式 CPU 每粒子循环和 compute-only 第一版均拒绝。 +10. 第一阶段正式视觉包含粒子、颜色映射和 screen-space Trail A/B;Camera 变化清 trail,不做 v1 trail reprojection。 +11. GPU resource 只由 render thread/gfx 创建;context loss 保留 CPU snapshot、丢弃 GPU generation并重建空 trail。 +12. Android 第一阶段主验证 OpenGL,冻结前必须做 Vulkan gfx gate;iOS OpenGL/Metal 使用同一 Core contract,后续 Mac/iOS 专项验证。 +13. 公共 API 不暴露 backend handle、raw host pointer或每帧大数组;数据更新通过 Source immutable revision。 +14. 下一阶段只执行第 26 节四项最小技术验证,不重新讨论总体架构。 + +## 31. 本阶段不得冻结的内容 + +- 已达到的 FPS、最终最大/default particle count。 +- 最佳 state/velocity/trail texture format 和 channel packing。 +- Vulkan/Metal compute workgroup、barrier 和厂商优化。 +- 最终生产 Mask 来源、分辨率与 NavSea 海岸语义。 +- 真实天气 Server/GRIB/厂商数据格式。 +- NavSea/RN 最终 Host API 和产品桥接。 +- 最终生产设备能力矩阵、iOS 最终 backend。 +- Camera 验证前的最终 Particle State 物理 packing。 + +这些内容只能由后续最小技术验证、数据设计和产品 Gate 冻结。 + +## 32. 下一阶段判定 + +选择:**B,进入 `30_最小技术验证`。** + +原因:独立 Core layer、render-pass ping-pong/trail 在 OpenGL/Vulkan 的实际资源行为、context recreation、同帧 Camera/wrap/Mask correctness 和 iOS backend 薄接入仍缺运行时证据。这些未知项会影响实现细节或架构可行性,不能直接进入正式实现;但它们已被压缩为四项最小验证,不需要重新开展总体架构讨论。 + +## 33. 本次设计边界 + +本阶段未创建 Source、Layer、Renderer、Shader、GPU resource、CustomLayerHost、JNI/ObjC 类或 fixture 数据;未修改生产源码、构建配置、依赖、lock、Step 或项目总则;未运行会写工程状态的构建/测试。 diff --git a/docs/10_架构/MapLibre粒子总体架构_v1.md b/docs/10_架构/MapLibre粒子总体架构_v1.md new file mode 100644 index 000000000..c840844a0 --- /dev/null +++ b/docs/10_架构/MapLibre粒子总体架构_v1.md @@ -0,0 +1,3 @@ +# MapLibre 粒子总体架构 v1 + +Layer:Base Map → WeatherColorLayer → RasterParticleLayer → Navigation Reference → AIS/Route/Ownship/Alarm。GPU 核心候选:Velocity Texture、Particle State A/B、Update、Draw、Trail Render Target、Color Ramp。 diff --git a/docs/20_调查/10_架构调查报告_v1.md b/docs/20_调查/10_架构调查报告_v1.md new file mode 100644 index 000000000..b0a888f08 --- /dev/null +++ b/docs/20_调查/10_架构调查报告_v1.md @@ -0,0 +1,391 @@ +# MapLibreParticleLab 架构调查报告 v1 + +调查日期:2026-08-29 + +调查分支:`particle/main` +调查性质:只读架构调查,不是粒子方案设计 + +## 1. 调查环境 + +- 仓库:`/mnt/sda1/MapLibreParticleLab`,根目录包含 `PARTICLE_BASELINE.md`、`include/`、`src/`、`platform/android/`、`platform/ios/`、`shaders/`、`particle-fixtures/` 和 `docs/`,确认是 MapLibreParticleLab。 +- 主机基线:Ubuntu 24.04 x86_64;项目基线记录的 Java 17、Android API 34、NDK 28.2.13676358、构建并行度 3 已读取自 `PARTICLE_BASELINE.md` 与 `docs/00_项目规范/06_官方基线来源_v1.md`。 +- 本次只执行文件搜索、源码阅读、配置阅读和 Git 查询;未运行构建、测试、依赖安装或会生成产物的命令。 + +## 2. Git、基线和规则读取 + +- 分支:`particle/main`。 +- HEAD:`127de2271480000fcc8f390d81cf446e98757800`,最近提交主题为 `docs: freeze particle project governance v1`,时间为 2026-08-29 11:13:49 +08:00。 +- 远端:`origin` 为本地 Gitea SSH 地址;`upstream` 为 MapLibre Native GitHub,只启用 fetch,push 显式为 `DISABLED`。 +- 工作区:调查开始时无已跟踪修改;有一个未跟踪的当前任务书 `docs/40_任务/10_架构调查指令书_v1.md`。因此“工作区干净”检查结果为:`未完成`,原因是任务书本身在当前工作区未跟踪,非本次生成。 +- 当前基线:Android `android-v13.4.1`、iOS `ios-v6.28.0`、共用 Core `18545ca10d296704b2cba675b182ff2c8da24588`。实际版本文件为 `platform/android/VERSION` 和 `platform/ios/VERSION`。 +- 已完整读取:根目录 `AGENTS.md`;`PARTICLE_BASELINE.md`;`docs/00_项目总则/` 下当前四份总则;`docs/00_项目规范/` 下当前六份规范及官方基线来源;当前 `STEP-000` 记录和报告;`docs/40_任务/README.md`;本任务书;调查、Step、报告 README;现有 `docs/10_架构/MapLibre粒子总体架构_v1.md`;`粒子开发记录.md`。 +- 本次未修改 `docs/00_项目总则/`、`docs/00_项目规范/`、生产代码、实验源码、构建配置或依赖。 + +## 3. 当前项目整体架构 + +### 3.1 工程形态 + +当前仓库不是 React Native、Expo 或 Bare RN 产品工程。根 `package.json` 的名称是 `maplibre-native`,脚本只有样式规范复制;`tsconfig.json` 只包含 `.github/scripts/*.ts`、`scripts/**/*.mjs` 和声明文件,并设置 `noEmit`。仓库没有业务页面、React 组件、`react-native`/Expo 依赖、Metro 配置或 Babel 配置。 + +因此任务书中的“业务/页面 → RN Map 组件 → RN 封装”在当前仓库不存在,不能据此补画一条虚构调用链。这里的实际入口是各平台 MapLibre SDK 示例、测试应用或直接使用 Native C++ API 的宿主。 + +### 3.2 主要目录 + +```text +MapLibreParticleLab/ +├── include/mbgl/ Core 对外 C++ API +├── src/mbgl/ Core 实现:Map、Style、Source、Render、gfx、线程 +├── platform/android/ Android SDK、JNI、Java/Kotlin View、C++ backend、测试 App +├── platform/ios/ iOS SDK、MLNMapView、OpenGL/Metal backend、示例和测试 +├── platform/darwin/ Darwin 公共绑定、CustomStyleLayer 和 CustomDrawableLayer +├── platform/node/ Node binding 与 Node 测试,不是 RN UI 宿主 +├── shaders/ MapLibre 内置 GLSL/生成清单;无粒子 shader +├── particle-fixtures/ 第一阶段离线粒子场/Mask fixture 占位目录 +├── render-test/ test/ 渲染测试、API 测试、fixture 和服务器测试 +├── scripts/ 样式代码、设备农场和工程脚本 +├── docs/ 总则、架构、任务、调查、Step、报告 +├── vendor/ 第三方源码子模块 +└── CMakeLists.txt、Makefile、bazel/、platform 构建文件 +``` + +### 3.3 入口和边界 + +- Core 入口是 `mbgl::Map`(`include/mbgl/map/map.hpp`),通过构造时注入 `RendererFrontend` 和 `MapObserver`。 +- Android 入口是 `MapView`(`platform/android/MapLibreAndroid/src/main/java/.../MapView.java`),内部创建 `MapRenderer`、`NativeMapView` 和 Java/C++ JNI peer。 +- iOS 入口是 `MLNMapView : UIView`(`platform/ios/src/MLNMapView.h`),内部由 `MLNMapViewImpl` 连接 Core `Map` 和 OpenGL/Metal View。 +- 没有项目自有 `assets/` 业务资源目录;`test/fixtures` 是 MapLibre 测试资源,`particle-fixtures` 目前仅包含 README 和五个 `.gitkeep`。 +- 现有 `build-linux-opengl`、`build-particle` 等是工作区中的构建目录,不视为源码模块。 + +## 4. 依赖与版本 + +| 类别 | 当前可确认内容 | 证据/结论 | +|---|---|---| +| JavaScript 直接依赖 | `@maplibre/maplibre-gl-style-spec`、`simple-git`、`minimatch`、`npm-run-all`、GitHub Actions 等 | 根 `package.json`;不是地图 UI 依赖 | +| JavaScript 开发依赖 | TypeScript、EJS、Express、AWS SDK、MVT/style fixtures 等 | 用于脚本、测试、设备农场和代码生成 | +| RN/React/Expo | 未找到 `react`、`react-native`、Expo、RN MapLibre 包 | 当前仓库没有 RN 产品层 | +| Skia/Reanimated/WebGL JS | 未找到项目直接依赖 | 不能假定存在 | +| Android SDK | `platform/android/VERSION=13.4.1`;`compileSdk=34`、`minSdk=23`;OpenGL/Vulkan/WebGPU flavor;Java SDK 直接依赖 GeoJSON、gestures 等 | `platform/android/MapLibreAndroid/build.gradle.kts` | +| Android Core | `libmaplibre` 由本仓库 C++ Core 构建并通过 JNI/Prefab 接入,不是另一个 RN 包 | Android CMake、JNI 和 Prefab 配置 | +| iOS SDK | `platform/ios/VERSION=6.28.0`;Podspec 通过 `ios-v#{version}` release xcframework;iOS deployment target 12 | `platform/ios/MapLibre.podspec` | +| iOS backend | 当前源码含 OpenGL、Metal、WebGPU 条件路径;当前项目基线将 OpenGL 作为主线、Metal 作为后续验证方向 | `MLNMapView+Impl.mm` 和各 backend 文件 | +| lock 文件 | 根 `package-lock.json` lockfile v3、`pnpm-lock.yaml` lockfile v9;锁定 TypeScript 5.9.3 等脚本依赖 | 未安装/更新 | + +需要区分:`package.json` 的版本 `5.4.0` 是 Native 仓库/脚本包版本;Android/iOS SDK 版本分别由各平台 `VERSION` 决定,不能把三者合并为一个 RN 版本。 + +## 5. MapLibre 实际接入链 + +当前无 RN 绑定,因此实际链路分为 Android SDK、iOS SDK 和直接 Core 三条: + +```text +Android Activity/Test App + → MapView(FrameLayout) + → MapRenderer + NativeMapView(Java JNI peer) + → AndroidRendererFrontend + → mbgl::Map / mbgl::Style + → RendererFrontend::update(UpdateParameters) + → Android MapRenderer render thread + → mbgl::Renderer + → RenderOrchestrator / RenderTree / RenderLayer / gfx::Drawable + → Android OpenGL 或 Vulkan RendererBackend + → Surface/TextureView → GPU +``` + +```text +iOS App/Example + → MLNMapView(UIView) + → MLNMapViewImpl(MapObserver adapter) + → mbgl::Map + RendererFrontend + → RenderOrchestrator / RenderTree / RenderLayer / gfx + → MLNMapViewOpenGLImpl(GLKView) 或 MLNMapViewMetalImpl(MTKView) + → EAGL/CAMetalLayer → GPU +``` + +```text +直接 Core 宿主 + → mbgl::Map(RendererFrontend, MapObserver) + → Map::Impl::onUpdate() + → Immutable Style/Source/Layer snapshot + → Renderer::render() +``` + +当前能够确认的对象:MapView、Camera、Style、RasterSource、ImageSource、Vector/GeoJSON/CustomGeometrySource、Layer、MapObserver 事件和 CustomLayer。当前仓库没有 RN `ShapeSource` 组件层,也没有 RN `onPress` API;Android/iOS Native SDK 的点击、相机和样式 API 需分别通过各自 SDK 查询,不能移植成当前 RN 事实。 + +## 6. MapLibre 生命周期 + +### 6.1 Core 生命周期 + +`Map::Impl::onUpdate()` 在连续模式下读取当前时间、更新 transition,构造包含 `TransformState`、style/source/layer immutable snapshot、图片、字体和 FileSource 的 `UpdateParameters`,再交给 `RendererFrontend::update()`。`Map::triggerRepaint()` 也只是触发同一更新路径。 + +`Renderer::render()` 创建并准备 `RenderTree`,`Renderer::Impl::render()` 依次执行:开始 frame、上传 source/layer 资源、更新 layer group、运行 tweaker、上传 drawable/render target、可选 3D pass、render target、清屏、opaque pass、translucent pass、debug pass、postRender、present、结束 frame,并回调 `onDidFinishRenderingFrame`。 + +### 6.2 Android + +- `MapView.initializeDrawingSurface()` 创建 `MapRenderer` 和 `NativeMapView`;Surface 创建后回到 UI 线程初始化 `MapLibreMap` 并调用 `onStart()`。 +- `MapView.onStart/onResume/onPause/onStop/onDestroy` 分别驱动 Java map/renderer;SurfaceView detach 会触发 native reset,Surface 销毁/重建可能导致 backend 和 `Renderer` 重建。 +- Java `MapRenderer` 的 `onDrawFrame()` 调用 JNI `nativeRender()`;SurfaceView 由 `MapLibreSurfaceView` 的 render thread 请求 dirty/continuous frame,TextureView 由 `TextureViewRenderThread` 的事件队列和 request flag 驱动。 +- `AndroidRendererFrontend::update()` 保存最新 `UpdateParameters`,异步任务把它交给 `MapRenderer` 并调用 Java `requestRender()`。C++ render thread 只持有一次 render 所需的参数快照。 +- Native MapObserver 事件由 JNI 转回 Java `MapChangeReceiver`,包括 camera will/is/did、地图/样式加载、render frame start/end、idle、source 和 shader 事件。 + +### 6.3 iOS + +- `MLNMapView` 创建 Core `Map` 和 `MLNMapViewImpl`;实现通过编译宏选择 OpenGL、Metal 或 WebGPU backend。 +- 显示刷新由 `CADisplayLink` 调用 `updateFromDisplayLink:`;当 `needsDisplayRefresh` 或 pending completion 存在时,调用 backend View 的 `setNeedsDisplay`。 +- `renderSync` 清除刷新标记后调用 `_rendererFrontend->render()`;渲染 View delegate 的 `drawInRect:`/`drawInMTKView:` 再调用 `MLNMapViewImpl::render()`,形成地图自己的渲染帧。 +- View 不可见、进入后台、销毁或恢复时,DisplayLink、drawable、backend 资源和 dormant 状态分别处理;具体资源重建依赖 backend 实现。 +- iOS delegate 可获得相机、地图/样式加载、render frame start/end、idle 等事件;CustomStyleLayer 的 `drawInMapView:withContext:` 在 MapLibre 的自定义 layer 绘制时调用。 + +### 6.4 TS/RN 层与内部事件 + +当前仓库没有 TS/RN 层,所以“TS/RN 直接获得的生命周期事件”:`未确认/不适用`,原因是该层不存在。Native SDK 可观察的事件如上;只存在于 Core/Renderer 的 `UpdateParameters`、RenderTree、LayerGroup、GPU command encoder 的内部状态,当前没有 JS 层封装。 + +## 7. 自定义渲染扩展点分类 + +### A:当前项目已经直接使用 + +- Core `style::CustomLayer` + `CustomLayerHost` + `RenderCustomLayer`:现有 Android Test App 的 `CustomLayerActivity` 和 C++ `ExampleCustomLayer`/`ExampleVulkanCustomLayer` 实际使用。 +- Darwin `MLNCustomStyleLayer`:iOS/Swift 示例实现 `drawInMapView:withContext:`,获得同一 frame 的 projection matrix;Metal 示例使用当前 render encoder。 +- Darwin `MLNCustomDrawableStyleLayer`/Core `CustomDrawableLayer`:示例 `ExampleCustomDrawableStyleLayer` 和 UI 测试实际使用,属于 Drawable/Builder 路径而不是 raw GL callback。 +- 内置 `RenderLayer`、`LayerGroup`、`DrawableBuilder`、`DrawableTweaker`、shader manifest、texture/buffer/render target:是当前地图本身的生产渲染链,粒子尚未接入。 + +### B:当前依赖已暴露,但目标粒子业务尚未使用 + +- Android Java `CustomLayer(String id, long host)` 暴露了 native host 指针入口,但 Java API 明确标注 experimental/do not use;当前只在 SDK test app 示例使用,粒子业务未使用。 +- Android `CustomGeometrySource` 和 iOS `MLNComputedShapeSource` 已暴露,能提供按 tile 的 GeoJSON/几何数据;当前没有粒子/格点数据实现。 +- Core `gfx::Context` 已有 `createTexture2D`、`createDynamicTexture`、`createRenderTarget`、uniform buffer、`createDrawableBuilder`、generic shader 和 upload pass 接口;这些是现有通用基础设施,不等于粒子专用 API。 + +### C:Native Core 支持,但当前高层封装未统一暴露 + +- `CustomLayerHost::render()` 可拿到 width/height、center、zoom、bearing、pitch、FOV、projection matrix 和 near-clipped matrix;Vulkan/Metal 还有 backend-specific render parameters。Android 高层 Java host 没有一个类型安全的 `CustomLayerRenderParameters` API,应用级可直接使用范围未确认。 +- Core `RenderLayer`、`RenderOrchestrator`、`LayerManager`、`RendererObserver` 和 gfx backend 内部 hook 可承载 native 粒子实现,但它们是 Core 内部边界,不是当前 RN/业务 API。 +- iOS `MLNBackendResource` 可暴露 Metal view/device/render pass/command buffer;这是 iOS Native API,当前仓库没有 RN 统一桥接。 + +### D:当前代码无法确认 + +- React Native UI/Main Thread、JS bridge、TurboModule/Fabric、RN `requestAnimationFrame` 或 Reanimated frame callback:当前项目没有 RN host,无法确认。 +- 任何外部 Overlay 与 MapLibre View 的同步质量:当前正式边界禁止外部 Overlay,仓库也没有粒子 Overlay 实现,因此没有实测数据。 +- 未实现的粒子 shader、GPU ping-pong state、trail render target、velocity texture 的具体格式和生命周期:当前只有架构草图,没有实现。 + +## 8. Android 渲染架构 + +- 原生 View:`MapView` 是 `FrameLayout`,将 SurfaceView 或 TextureView renderer 放在 child index 0;不是 RN `View`。 +- renderer:Java `MapRenderer` 仅负责生命周期、JNI 和调度;C++ `MapRenderer` 持有 Android backend、Core `Renderer`、`ActorRef`、update mutex 和 renderer observer。 +- backend:Gradle flavor 通过 `-DMLN_WITH_OPENGL=ON` 或 `-DMLN_WITH_VULKAN=ON` 构建;C++ `AndroidGLRendererBackend` 和 `AndroidVulkanRendererBackend` 分别管理 EGL/GL 或 Vulkan surface/swapchain/device。 +- 帧驱动:SurfaceView 默认可以 WHEN_DIRTY,CONTINUOUS 用于连续渲染/benchmark;TextureView 自有 render thread。Core update 通过 `AndroidRendererFrontend` 调用 `requestRender`。 +- 相机:Java `MapLibreMap`/`Transform` 调 JNI,NativeMapView 调 Core `jumpTo/easeTo/flyTo`、坐标投影和反投影;完整 matrix 仍在 Core Transform/Paint/CustomLayer 参数中。 +- 自定义入口:Core CustomLayer 和 Android wrapper/test example 明确存在;但 Java host 是原始 `long`,生命周期、指针归属、context loss 和参数传递要求高,不能当作稳定业务接口。 +- Native Module:未找到 RN NativeModule/TurboModule/Fabric。Android 的 `NativeMapView` 是 MapLibre Java/C++ peer/JNI,不是 RN module;当前不存在“Native Module 接触 RN MapView 实例”的事实。 + +## 9. iOS 渲染架构 + +- 原生 View:`MLNMapView` 继承 `UIView`;内部由 `MLNMapViewImpl` 将 Core MapObserver 与 ObjC delegate 接起来。 +- OpenGL:`MLNMapViewOpenGLImpl` 持有 `gl::RendererBackend`、Renderable 和 `GLKView`/EAGL context;GLKView delegate 的绘制回调进入 Core render。 +- Metal:`MLNMapViewMetalImpl` 持有 Metal backend、`MTKView`、command queue/buffer 和 render pass descriptor;`drawInMTKView:` 调 `impl->render()`,Core 负责 command encoding/present。 +- Camera/transform:Core `TransformState` 生成 `TransformParameters` 的 projection/aligned/near-clipped matrices;`MLNCustomStyleLayer` 将这些和中心、zoom、方向、pitch、FOV 传给 ObjC drawing context。 +- 自定义接口:`MLNCustomStyleLayer` 是可确认的 Native custom layer;Metal 下可以使用当前 encoder,OpenGL 下存在 deprecated EAGL context API。`MLNCustomDrawableStyleLayer` 是另一条 Drawable/Builder 接口。 +- Native 对象范围:iOS adapter 能访问 Core Map、RendererFrontend 和 backend resource;公开的业务层主要通过 `MLNMapView`、`MLNStyle`、`MLNStyleLayer` 及 delegate 访问。没有 RN 连接层,RN 侧范围为 `未确认`。 +- Android/iOS 差异:Android 有明确 OpenGL/Vulkan flavor 和实验性 raw host;iOS 示例有成熟的 ObjC/Swift CustomStyleLayer 与 Metal encoder 上下文。两者虽共享 Core CustomLayer 接口,但高层资源和生命周期 API 不一致。 + +## 10. JS/UI/Native/Worker/Render/GPU 边界 + +```text +[不存在的 JS/RN 业务层] + │ 当前仓库无桥接事实 + ▼ +Android UI thread / iOS main thread + ├─ Native MapView、Camera、Style API + ├─ Core Map 所在线程(由宿主创建线程决定) + └─ MapObserver 回调 + ▼ immutable UpdateParameters / mailbox / async task +MapLibre Worker/FileSource + ├─ Style/tile 解析、布局、bucket/buffer 计算 + └─ FileSource 网络/缓存/离线 I/O + ▼ +MapLibre Render thread(Android 明确独立;iOS 当前 render delegate 路径需遵循宿主主线程/渲染上下文约束) + → Renderer → RenderTree → LayerGroup/Drawable → gfx Context/CommandEncoder + → OpenGL/Vulkan/Metal backend → GPU +``` + +- 高频数据若走不存在的 JS bridge,当前没有事实可测;按现有边界,不能将每帧大数组交给 Java/ObjC API 作为已支持能力。 +- Android 的 `UpdateParameters` 在 frontend 与 render peer 之间异步保存并加锁,渲染使用最新快照;Java `requestRender` 只触发帧,不传粒子数据。 +- iOS `setNeedsRerender` 只设置刷新标记,`CADisplayLink`/UIView backend 再进入 render;这不是 JS 每帧回调。 +- Native 每帧回调存在:Core `MapObserver`/`RendererObserver` 的 render frame 事件,以及 CustomLayerHost render callback;它们位于地图渲染链内部。 +- GPU 资源直接更新位置存在于 Core `gfx::Context`、UploadPass、Drawable、Texture、RenderTarget 和 backend command encoder;当前没有对业务开放的跨平台粒子资源接口。 + +## 11. Camera、Matrix 与坐标转换能力 + +| 信息 | JS/RN | Native SDK | Core 内部/CustomLayer | 当前结论 | +|---|---|---|---|---| +| center/zoom/bearing/pitch | 当前无 RN host,未确认 | Android `MapLibreMap`、iOS `MLNMapView`/delegate 可访问部分状态 | `TransformState` 全部存在,CustomLayer 参数传出 | 可由 Native/Core 获取 | +| bounds/viewport | 无 RN host | SDK 有尺寸和相机/投影 API | `TransformState::getSize`、frustum offset、tile cover | Core 信息完整 | +| screen ↔ geographic | 无 RN host | Android NativeMapView 和 iOS 公共 API 有相应能力 | `latLngToScreenCoordinate`、`screenCoordinateToLatLng` | 已确认 Core 能力 | +| projection/unprojection | 未确认 | 高层不统一暴露 raw matrix | Core `getProjMatrix`、projection/inverse matrices、camera utility | 内部可用 | +| view/transform matrix | 未确认 | iOS CustomStyleLayer 以 projection context 形式收到;Android Java wrapper 未提供同等类型 API | `TransformState::matrixFor`、`TransformParameters`、`PaintParameters` | 跨平台高层不一致 | + +证据:`TransformParameters` 保存 `projMatrix`、`alignedProjMatrix`、`nearClippedProjMatrix`;`CustomLayerRenderParameters` 从同一 `PaintParameters.state` 读取中心、zoom、bearing、pitch、FOV 和 projection matrix;`PaintParameters::matrixForTile` 负责 tile matrix。 + +严格同步结论:在 Core 的同一渲染帧内,CustomLayer callback 可以获得地图当前 transform,因此“Core 内部同帧使用地图矩阵”已确认;但“Android/iOS 业务高层均能以统一公开 API 获取并接入粒子”未确认,当前不能宣称跨平台正式实现已具备严格同步能力。 + +## 12. 帧驱动能力 + +已确认:Android SurfaceView/TextureView render loop、Android `requestRender`/CONTINUOUS、iOS CADisplayLink、GLKView/MTKView draw callback、MapLibre `triggerRepaint`、Core CustomLayer render callback。 + +未确认/不适用:当前没有 RN `requestAnimationFrame`、RN animation frame、Reanimated frame callback 或 JS 每帧回调。 + +地图 Renderer 的帧驱动不是独立于地图的粒子动画时钟:Core `Map::Impl::onDidFinishRenderingFrame` 在 `needsRepaint` 或 transition 时重新 `onUpdate()`;平台再请求下一帧。若未来另建独立动画循环,现有代码无法保证它与相机快照、drawable context、present 时刻一致,可能产生一帧延迟、不同步或重复渲染;这里只记录风险,不作方案选择。 + +## 13. 当前数据与粒子相关实验代码 + +- `particle-fixtures/README.md` 明确第一阶段禁止真实天气服务器,具体数据格式在设计冻结后确定;`coastal-mask/`、`shear/`、`uniform-east/`、`uniform-north/`、`vortex/` 目前均只有 `.gitkeep`。 +- 全仓库排除 vendor、node_modules、build 后搜索 `particle/wind/current/wave/vector field/rasterarray/velocity`,没有发现粒子运行时、速度场解析、粒子 shader、GPU ping-pong 或 trail 实现。 +- 现有 `raster` 文件是 MapLibre 常规 RasterSource/RasterLayer 测试和内置 raster shader;`interpolate` 多数是 Style Spec/通用工具,不是向量场插值实现。 +- `shaders/` 有 raster、heatmap、hillshade、fill、line、symbol、custom geometry 等现有地图 shader,没有 `particle`、`velocity` 或 trail shader。 +- Android/iOS custom layer 示例是图形 API 示例/测试用途,不是粒子基础代码;应分类为“仅实验/示例用途”,不能直接判定为最终可复用粒子实现。 + +## 14. 海洋/气象格点数据可经过的现有层 + +| 阶段 | 当前仓库事实 | +|---|---| +| Fetch | Core `FileSource` 支持样式、tile、资源和缓存;没有粒子气象数据协议或 fixture loader | +| ArrayBuffer/TypedArray | JS 侧只有 Node/脚本和测试用途;未发现粒子 ArrayBuffer/TypedArray 模型 | +| 二进制解码 | Core 有 tile、PBF、图片等既有解码;没有速度场/海洋格点解码器 | +| Grid 数据模型 | 没有项目粒子 Grid/经纬度格点类型;`grid_index` 等是通用地图工具或测试 | +| 插值 | 有通用数值/Style 插值工具;没有已确认的向量场双线性/球面插值模块 | +| 内存结构 | Core 可用 immutable/vector/CPU buffer;没有粒子数据生命周期模型 | +| GPU upload | gfx Context、UploadPass、Texture2D、UniformBuffer、RenderTarget、Drawable 已存在 | +| Shader uniform | 内置 shader 与 global/layer uniform 已存在;没有粒子 uniform contract | +| 缓存/生命周期 | FileSource、tile cache、style snapshot 和 gfx resource 生命周期已存在;粒子数据生命周期未实现 | + +可复用基础设施只能确认到通用地图层:FileSource/离线 fixture 测试体系、immutable snapshot、thread pool、gfx Context/Texture/Buffer/RenderTarget/Drawable、shader registry 和 frame observer。未来粒子数据格式、解析、插值、上传策略均未确定。 + +## 15. 性能与线程风险 + +| 风险 | 等级 | 当前证据与边界 | +|---|---|---| +| JS 高频循环/每帧 JS→Native | 尚无法判断 | 当前无 JS/RN 宿主;若未来新增,仓库没有现成桥接基准 | +| 大数组/JSON/ArrayBuffer 跨桥复制 | 高 | 当前 MapLibre update 是 immutable snapshot/异步传递,未提供粒子大数组桥接;跨平台高层接口若采用逐帧对象传输会形成风险 | +| Native→GPU 重复复制 | 中 | gfx upload/update 能力存在,但粒子资源尚未定义,复制次数无法实测 | +| Texture 高频重建 | 中 | Context 有 Texture2D/DynamicTexture;RenderCustomLayer 示例未提供粒子资源复用策略 | +| Camera 变化触发大规模重算 | 中 | Map update 会重新计算 RenderTree、tile/layer evaluation;未来粒子若绑定 camera 需避免把每帧状态误当 style/source 变更 | +| React re-render | 尚无法判断 | 没有 React/RN 工程 | +| 外部 Overlay 不同步 | 低(当前)/架构禁止 | 当前没有外部粒子 Overlay;正式规则也禁止该路径 | +| Android/iOS 实现分叉 | 高 | 同 Core 但高层 CustomLayer、context/encoder、线程和 surface 生命周期不同 | +| GPU context/surface loss | 高 | Android `onSurfaceCreated` 会 mark context lost 并重建 backend/Renderer;CustomLayer 有 contextLost;iOS 后台/视图重建也涉及资源生命周期 | +| MapView 销毁后资源泄漏 | 中 | Core/RenderCustomLayer 有 deinitialize/contextLost 路径,raw `long` host 和应用归属仍需后续设计验证 | +| Render pass 顺序/状态污染 | 高 | CustomLayer 在 translucent pass;raw backend callback 需要遵守当前 render pass 和状态恢复约束 | + +## 16. 当前架构图 + +### 16.1 App / MapLibre 总体调用图 + +```text +Android 示例或 iOS 示例 + └→ 平台 MapView + └→ 平台 RendererFrontend / MapObserver adapter + └→ mbgl::Map + ├→ Style / Source / Layer snapshot + ├→ Worker + FileSource + └→ Renderer → RenderTree → gfx → 平台 GPU backend +``` + +### 16.2 JS → Native → MapLibre 调用链 + +```text +JS/RN 业务入口:不存在于当前仓库 + └→ RN MapLibre binding:不存在于当前仓库 + └→ Android Java MapView 或 iOS MLNMapView:由外部宿主提供 + └→ JNI/ObjC++ adapter + └→ mbgl::Map → RendererFrontend → Renderer → GPU +``` + +### 16.3 Android + +```text +UI thread: MapView / MapLibreMap / NativeMapView JNI + └→ AndroidRendererFrontend + AsyncTask/mailbox +Render thread: MapRenderer → Renderer → RenderOrchestrator + └→ OpenGL EGL 或 Vulkan device/swapchain + └→ SurfaceView/TextureView → GPU +Workers/FileSource: tile/style parsing、layout、I/O +``` + +### 16.4 iOS + +```text +main/UI: MLNMapView(UIView) + CADisplayLink + └→ MLNMapViewImpl(MapObserver) + RendererFrontend +draw callback: GLKView 或 MTKView + └→ mbgl::Renderer → RenderTree → gfx command encoder + └→ EAGL/OpenGL 或 Metal command buffer → GPU +``` + +### 16.5 数据与渲染边界 + +```text +FileSource / tile / offline fixture + → immutable Source/Layer/UpdateParameters + → RenderTree / RenderSource / RenderLayer + → gfx Texture/Buffer/Drawable/RenderTarget + → backend command encoding + → GPU + +粒子 field / particle state / trail:当前不存在;未来位置为 [未确认] +``` + +## 17. 关键结论分类 + +### 17.1 已确认事实 + +1. 当前仓库是 MapLibre Native monorepo,不是 RN app。 +2. Android 13.4.1、iOS 6.28.0 和 Core commit 已从版本文件/基线确认。 +3. Core 的真实主链是 Map → RendererFrontend → UpdateParameters → Renderer → RenderTree/Orchestrator → Layer/Drawable → gfx backend。 +4. Android 有 OpenGL/Vulkan flavor、独立 renderer thread、JNI Map peer 和实验性 CustomLayer。 +5. iOS 有 `MLNMapView`、CADisplayLink、OpenGL/Metal backend、Native CustomStyleLayer 和 Drawable 示例。 +6. Core CustomLayer render 参数包含相机和 projection 信息;CustomLayer callback 在地图 render pass 内执行。 +7. 现有 fixture 只有目录占位;没有粒子运行时、格点模型、粒子 shader 或速度场解析器。 +8. Core 已有可复用的 immutable snapshot、worker/FileSource、gfx texture/buffer/render target/drawable/upload/shader registry 基础设施。 + +### 17.2 当前限制 + +- 没有 RN 页面、RN binding、Metro/Babel、NativeModule/TurboModule/Fabric,因此不能从本仓库确认 RN 到 MapView 的调用和生命周期。 +- Android CustomLayer 的 Java API 使用 raw native host 指针且标记 experimental;与 iOS CustomStyleLayer 的上下文能力不对称。 +- 正式粒子数据格式、内存模型、插值、GPU state、trail 和跨平台资源生命周期尚不存在。 +- 不可把现有 RasterSource 直接解释为 Raster/Vector Field source;它当前只处理常规 raster tile/image。 +- 本次未进行设备运行时验证,不能把静态可见接口写成已验证的帧率、延迟或 context-loss 行为。 + +### 17.3 候选扩展点(不作最终选择) + +1. Core `CustomLayerHost`/`RenderCustomLayer` 的同帧 callback。 +2. Core `CustomDrawableLayer` + DrawableBuilder/Tweaker/gfx resource。 +3. 新的隔离 Core particle render layer,与现有 RasterLayer/WeatherColorLayer 分离。 +4. 现有 Source/Tile/FileSource/fixture 管线的薄接入,用于离线数据而非直接改造 RasterSource。 +5. 平台薄接入:Android 复用 renderer surface/context 生命周期,iOS 复用 CustomStyleLayer/Metal 或 Core adapter 的同帧 transform。 + +这些只是调查候选;本报告不决定 CPU/GPU、CustomLayer 最终是否采用、OpenGL/Vulkan/Metal 路线、数据纹理格式、shader 架构或线程模型。 + +### 17.4 未确认问题 + +| 问题 | 当前无法确认原因 | 是否阻塞 `20_架构设计` | 最小确认方式 | +|---|---|---|---| +| 实际目标产品的 RN 宿主和 binding 版本 | 不在当前仓库 | 否;当前阶段可先按 Native Core 事实设计边界 | 后续由产品宿主提供 package/native 工程清单 | +| Android 业务侧能否稳定创建并持有 CustomLayerHost | 当前只有 raw `long` 示例和 JNI peer | 否,但影响平台薄接入细节 | 单独最小 Android 示例运行验证,不改本阶段代码 | +| iOS 当前正式发布构建究竟启用 OpenGL 还是 Metal | 源码同时支持,构建宏由外部工程决定 | 否;设计需保留 backend 分支 | 读取目标 Pod/Xcode 构建设置或做最小设备验证 | +| Core render thread 与 iOS app main thread 的确切调度策略 | 静态源码能确认 DisplayLink/MTKView callback,不能确认目标宿主调度 | 否 | 目标 iOS scheme 的运行时线程标记/最小 sample | +| 粒子目标场的具体文件格式、范围、时间步长 | fixture 只有占位目录且设计尚未冻结 | 否;属于后续数据设计输入 | 由设计阶段冻结离线 fixture contract | +| 目标设备的 GPU 特性、纹理格式和可用 compute 能力 | 当前没有设备矩阵或运行时探测结果 | 否;可在设计中标注 capability gate | Android/iOS 设备专项最小验证 | +| 旋转、pitch、wrap、frustum 下粒子坐标的最终视觉约束 | Core matrix 可用,但粒子规则尚未定义 | 否 | 进入 `20_架构设计` 后以 fixture 和截图测试确认 | + +## 18. 是否追加调查、是否进入 20_架构设计 + +选择:**A,可以直接进入 `20_架构设计`。** + +理由:本次已确认项目实际形态、Core 调用链、Android/iOS render lifecycle、CustomLayer/gfx 位置、相机矩阵来源、frame driver、现有数据基础设施和主要风险。未确认项主要是外部目标宿主、运行时设备和未来数据 contract,不满足“必须先追加一个调查任务”的必要条件;它们可作为 `20_架构设计` 的约束和验证清单。 + +下一阶段必须继续遵守:不复制 Mapbox 新源码或 Windy APK 代码;粒子模块高度隔离;WeatherColorLayer 与 RasterParticleLayer 分离;Wind=GLOBAL;Wave/Tide/Current=OCEAN_ONLY 并共用 Land/Ocean Mask;第一阶段只使用离线 fixture;平台只做薄接入。 + +## 19. 本次未做和边界声明 + +- 未创建粒子实现、shader、CustomLayer 实现、Native Module 或架构重构。 +- 未升级/安装 npm、Gradle、Pod 或其他依赖。 +- 未修改 Android/iOS 原生工程、Gradle/Podfile/Metro/Babel/TypeScript 配置。 +- 未运行构建和测试,因为任务明确为只读调查,且构建可能写入产物;历史基线构建 PASS 仅引用已有基线记录,不作为本次重新验证结果。 +- 按用户前置要求,未自行创建或改写 Step 文件;现有 `docs/50_Step记录/STEP-000_完整环境初始化_v1.md` 的文件内容仍为“进行中”,本报告不改动其历史状态。 diff --git a/docs/20_调查/Particle_V13_严格代码审查报告_v1.md b/docs/20_调查/Particle_V13_严格代码审查报告_v1.md new file mode 100644 index 000000000..198c01b8c --- /dev/null +++ b/docs/20_调查/Particle_V13_严格代码审查报告_v1.md @@ -0,0 +1,104 @@ +# Particle V13 严格代码审查报告 + +日期:2026-09-25(Asia/Tokyo) +审查性质:Mac 仓库只读代码审查、历史任务/报告交叉核对、定向原生测试复跑。未修改业务源码、未执行设备 GPU runtime 或 profiler。 + +## 1. 审查基线和证据分级 + +- Mac:`/Users/tei/dev/MapLibreParticleLab`;分支:`particle/main`。 +- HEAD 与 `origin/particle/main` 一致:`ee0b89e711bf01335b3aa71d36def785cd97fec3`;工作树 clean。 +- V13 功能提交:`016fd1fff6a33cf795c10d4f5c5413d308383677`。发布版本:`13.4.1-particle.13`。 +- Integration 主线:`39ad8415fbdb76c1822460bb09f3a3f35fe141e0`。 +- 参照:`docs/40_任务/STEP-092...`、`STEP-093...`、`STEP-094...` 任务;对应 `docs/报告/` 与 `docs/50_Step记录/`;STEP-088/089/090 性能历史报告。 +- 本次独立复跑:Render Segment/Retained/Fast Path 43/43 PASS;Field/QBIN/Viewport Reseed 69/69 PASS;V13 RN package TypeScript PASS。 +- 当前 RN Test Host `npm run typecheck` **失败(exit 2)**:TS2307 无法解析 `@navsea/maplibre-particle-rn`,伴随 TS7006。Host package.json 仍指向 `.10`。此前 Codex 使用临时工作树依赖通过,不等于当前默认安装可重复验证。 +- STEP-094 报告记录 OpenGL、Metal 构建、Codegen、Android Validation APK 通过;**Android/iOS V13 runtime、GPU frame/visual、功耗与长时间运行仍 NOT RUN**。此报告不将历史 Android 性能测量冒充 V13 性能。 + +## 2. 审查结论 + +没有在已复跑的纯数据结构/协议测试中发现失败;但 V13 在高频渲染、Metal 多段重建、GPU 资源分配和真实运行态验收上存在明确风险。不能由 43 项纯 Native 测试和编译 PASS 推断任意 R/C 交错的 GPU 输出、跨平台性能已经验收。 + +### F01 — P1:完整 Diagnostics 深复制进入每帧热路径(源码确定) + +证据:`src/mbgl/renderer/renderer_impl.cpp:206-208,500-505`;`src/mbgl/renderer/retained_underlay_runtime_state.hpp:243-254`。 + +`tryRenderParticleFastFrame()` 和 `render()` 调用 `renderSegmentSnapshot()`;该函数在互斥锁内复制完整 `RenderSegmentDiagnostics`(含 `styleLayers` 与每个 Segment 的重复 `layers`/字符串),再补几个状态字段。一次 fast frame 可能复制两次。与 pull-based 诊断的设计意图不一致。 + +修复:增加只读的轻量 hot-path 状态(enabled/configRevision/topologyRevision 等);完整快照只在外部 pull 时构造;快照构建/复制次数做计数,稳定 30/60 FPS 应接近零。 + +### F02 — P1:Metal Retained 构建逐段 GPU 同步等待(源码确定,运行影响待 profile) + +证据:`renderer_impl.cpp:790-824`;`retained_underlay_cache.cpp:316-322`;`src/mbgl/mtl/command_encoder.cpp:29-31`;`src/mbgl/mtl/offscreen_texture.cpp:86-91`。 + +`finishBuild()->present(*offscreenTexture)->swap()` 内部 `commit()` 后 `waitUntilCompleted()`。一帧多个 Retained 段失效时,可能产生多次串行 CPU/GPU 同步。使用 Metal System Trace / GPU Capture 测量 `waitUntilCompleted` 时间和 command-buffer 数量,再设计保持依赖顺序的异步提交/跨 pass 协调;不可仅删除 wait 而不确保纹理可读。 + +### F03 — P1:缓存内容失效与 GPU 资源销毁耦合(源码确定) + +证据:`retained_underlay_cache.cpp:203-209,241-255,364-374`。 + +`revisionChanged` 使 `invalidate()` 与 `releaseResources()` 一起发生,即使 viewport 未变、GPU context 有效,重建时仍需重新创建纹理和 composite group。普通 camera、source/tile、content revision 在多 Retained 段下会放大分配/销毁和相关同步。 + +修复:分离 `contentDirty` 与 `resourceInvalid`,同尺寸同 context 保留 allocation、重新 clear/build;context loss、尺寸不兼容、显式关闭/释放时才销毁。用 peak GPU bytes、textureCreate、resourceRelease、每帧 build/reuse 证明收益与正确性。 + +### F04 — P1:多缓存没有 GPU 内存预算(源码确定,OOM 影响待量化) + +证据:`renderer_impl.cpp:506-524`;`retained_underlay_cache.cpp:241-251`;`mtl/offscreen_texture.cpp:20-55`;`gl/offscreen_texture.cpp:18-35`。 + +每个 Retained Segment 均拥有完整 viewport 的 RGBA + depth/stencil 离屏资源;总成本约随 retainedCount×width×height 增长。相邻 R-R 也各自独立,符合契约,但高像素 iPad 与多段场景缺乏实际 bytes/peak/budget 观测。先增加预算与失败时的安全 full-render fallback,切勿通过强制合并相邻 R-R 破坏 V13 契约。 + +### F05 — P1:V13 Test Host 默认安装不可复现(已复现失败) + +证据:`integration/react-native/MapLibreParticleRNTestHost/package.json` 指向 `.10`;当前 `npm run typecheck` exit 2;报告也明确临时本地替换后已恢复旧依赖。 + +修复:将 Test Host 依赖和 lockfile 与已发布 `.13` 精确对应,重新安装并验证 V13 typecheck/codegen/iOS & Android bridge/真实运行;测试日志注明源包及 Native 精确版本。不得将临时链接环境结果写成默认环境 PASS。 + +### F06 — P1:多 Continuous 的 Particle Fast Path 固定 fallback(明确设计限制) + +证据:`renderer_impl.cpp:45-57,198-287` 和 `particle_fast_path.hpp:110-140`,STEP-094 设计与报告。 + +N 个 Continuous 中只支持恰好一个的 Fast Path;R-C-R-C-R 场景总走 Full CPU。STEP-090 历史 A/B 场景 RenderThread 约为 fast ON 21.7% / OFF 97.0%,只是历史参考,不能直接视作 V13 数据。先采集 V13 多 C 的 CPU/GPU 帧时间与频率,再设计可组合的 multi-C fast path;绝不可为性能偷偷移动 Style Layer 或合并相邻段。 + +### F07 — P2:范围访问反复计算线性 ordinal(源码确定) + +证据:`src/mbgl/renderer/render_orchestrator.hpp:156-180`,`std::distance(multimap.begin(), lower_bound(...))`。每个 Segment 的 opaque/translucent、build、upload/tweaker 路径可重复触发定位。 + +修复:在 LayerGroup topology 变化时预计算有序索引/ordinal;高频 render 只用稳定区间和 ordinal,避免多段场景重复 O(N) 前缀遍历。 + +### F08 — P2:多段失效粒度仍过宽(明确设计限制) + +证据:`renderer_impl.cpp:565-646`;`render_orchestrator.cpp:156-158,1005-1018`。Layer Impl fingerprint 可局部失效;source/tile/image/glyph/placement 与聚合 `retainedSupported` 仍可能使全部 R fallback/build。应先用 `sourceId` 与 Segment 关联建立有证据的保守依赖关系,无法归属继续全局失效,不得牺牲安全。 + +### F09 — P2:Diagnostics 资源指标可能发生归属偏差(源码路径风险) + +证据:`retained_underlay_runtime_state.hpp:173-240` 更新新 topology/entry;`renderer_impl.cpp:506-524` 随后 `clear()` 旧 caches;旧对象析构在 `retained_underlay_cache.cpp:364-374` 用旧 segmentIndex 报 release。不同 topology 的相同 index 可能指向新段,旧资源的释放计数可能归于新段或被 Continuous cache=null 丢弃。建议使用 cache generation/unique ID 归属,或在更换 diagnostics topology 前记录旧 generation 的 release;聚合 totals 与 per-generation 分离。 + +### F10 — P2:发布与协调状态资料漂移(核对事实) + +Integration 的 `status/`、`compatibility/MATRIX.md` 仍描述 `.5` / iOS XCFramework 等旧状态,Mac 粒子仓库已发布 `.13`。应由粒子负责人维护正式兼容矩阵,区分“Particle 已发布”和“NavSea 已接入/验证”,不得仅根据旧矩阵断言 V13 不存在或已在 NavSea 生效。 + +## 3. 正确性与验证缺口 + +以下不是已证实的渲染 bug,而是必须在设备上验证的风险: + +- V13 segmented pass 改变了 opaque/translucent、offscreen composite 的执行拆分;纯 resolver GTest 不覆盖 GPU blending/depth/stencil 与 Metal/OpenGL 的最终像素。 +- 必须覆盖 R-C-R、C-R-R、R-R-C、R-C-R-C-R、相邻 R-R 独立 cache、全 R、全 C,特别是透明航标/半透明颜色场与动态粒子叠加。 +- hidden boundary、invalid/recovery、Style layer insert/remove/reorder、Resize/Rotate/Pan/Zoom/Pitch、Context loss、Cache ON/OFF、Source/Tile mutation、切换 Plan 后的下一帧,均需要截图/帧采样和资源 counter 交叉验收。 +- STEP-093/094 报告已明确运行态 NOT RUN,因此不以原生编译 PASS 代替此项。 + +## 4. 推荐执行顺序(分批,降低反复安装成本) + +A. **无需设备的修复**:先处理 F01 快照深复制、F05 Test Host 精确版本与复现、F07 ordinal、F09 诊断归属;增加纯 C++ 定向回归。每阶段只做对应范围,不把源文件无关改动混入。 + +B. **Metal 资源修复**:合并 F02/F03/F04,统一处理 GPU 对象分配与同步、资源预算;OpenGL/Metal 静态编译和 Native regression 先行;再一次集中到 iPhone/iPad 采集 GPU Frame Capture。 + +C. **正式运行态验收**:集中执行 topology、alpha/depth、invalid/recovery、30/60 FPS、稳定/变更、内存峰值、30 分钟以上长稳态;记录相同设备、相同 Style、相同视口的 V12/V13 或 V13 ON/OFF 成对指标,不外推历史数字。 + +D. **后续性能架构**:在基础正确性通过后,再研究多 Continuous Fast Path 和 source-to-segment 精确依赖。保持 Style Z-order 唯一顺序源、不合并同策略相邻段、不引入业务频率分类。 + +## 5. 最终判定边界 + +- 纯 Native tests:43/43 + 69/69 PASS;RN V13 package TS PASS。 +- Test Host 默认环境:FAIL(缺失模块 / `.10` 引用)。 +- 源码性能问题:F01/F02/F03/F07 的触发调用链得到静态确认;真实节省量仍需设备测量。 +- V13 Android/iOS GPU runtime、完整画面、内存和长时间性能:本次 NOT RUN;不声称生产级全量 PASS。 +- Mac Git:HEAD 与 origin 一致,工作树 clean;未修改粒子源码。 diff --git a/docs/20_调查/README.md b/docs/20_调查/README.md new file mode 100644 index 000000000..c72133999 --- /dev/null +++ b/docs/20_调查/README.md @@ -0,0 +1,3 @@ +# 调查目录 + +第一份建议 `MapLibre粒子接入点调查_v1.md`,只读调查 Style/Source/Tile/RenderLayer/gfx/ShaderRegistry/OpenGL/Vulkan/Metal。 diff --git a/docs/20_调查/RenderSegment多Retained段全局Placement门槛_粒子管理员审查说明_v1.md b/docs/20_调查/RenderSegment多Retained段全局Placement门槛_粒子管理员审查说明_v1.md new file mode 100644 index 000000000..4e0f3f520 --- /dev/null +++ b/docs/20_调查/RenderSegment多Retained段全局Placement门槛_粒子管理员审查说明_v1.md @@ -0,0 +1,436 @@ +# RenderSegment 多 Retained 段全局 Placement 门槛——粒子管理员审查说明 v1 + +日期:2026-10-07 +性质:调查结论 / 修改目标 / 管理员审查输入,不是直接实施任务书。 +目标读者:Particle Shared Core / Renderer 维护者。 + +## 1. 本文要解决的问题 + +NavSea 当前已经能通过公开 `RendererRuntime.setRenderSegmentPlan()` 配置任意连续的 `RETAINED / CONTINUOUS` Segment,且 `.19 / .20` 的 RenderSegment 实现已经具备 source-local revision / readiness / cache diagnostics 等基础。 + +但 NavSea 真实运行验证发现:当存在多个 Retained Segment 时,一个与某 Retained Segment 无直接关系的全局 placement/fade 状态,仍可能把所有 Retained Segment 同时打回 fallback,导致多 Segment 不能发挥设计目标。 + +本文希望 Particle 管理员审查并决定:怎样把剩余的 placement/global fallback 门槛收敛到正确粒度,同时保持 fail-closed、Style Z-order、跨平台语义和 C-001 边界不变。 + +## 2. 正式边界 + +跨项目 Contract 仍为 `C-001`。 + +本问题属于 Particle Renderer 内部实现优化,目标上不要求新增 NavSea 私有 Native 通道,也不要求 Particle 理解 AIS、Route、Weather、Alarm 等 NavSea 业务语义。 + +NavSea 仍只负责: +- 按 Style Z-order 选择 Segment 边界; +- 通过稳定公开 API 提交 RenderSegmentPlan; +- 读取 diagnostics; +- 决定天气等业务能力何时显示。 + +Particle 仍负责: +- Segment resolve / cache / invalidation / renderer execution; +- placement / transition / fading / source dependency 的正确性; +- GPU resource 和 stale-cache 安全; +- Android / iOS 共用的 Shared Core 语义。 + +本修改若不改变公共 API、生命周期、错误语义或双方职责,不需要升级 Contract。 + +## 3. 当前 Mac 基线需要管理员先确认 + +本次只读检查发现: + +- `/Users/tei/dev/MapLibreParticleLab` + - branch: `particle/main` + - HEAD: `750c14f52708d7dc93d8eb6ec2b21112185afd5c` +- `/Users/tei/dev/MapLibreParticleLab-release-v19` + - branch: `release/particle.19` + - HEAD: `6b48f905d2d17a8271b6ff883e682db7bc45dcd3` +- `/Users/tei/dev/MapLibreParticleLab-release-v20` + - branch: `release/particle.20` + - HEAD: `53a844e81849f254ba2d169c2d875dbd862cc7d5` +- `/Users/tei/dev/MapLibreParticleLab-step101` + - branch: `step101-render-orchestrator-teardown` + +NavSea 当前实际消费的是 `.19` 行为;本次检查确认 `release/particle.19` 与 `release/particle.20` 在本文相关 placement gate 代码上相同。 + +注意:当前 `particle/main` 与 `release/particle.19/.20` 的 RenderSegment 执行细节存在差异,因此管理员开始修改前必须先明确正式修复基线,不要把 main 与 release 分支默认视为完全一致。 + +## 4. NavSea 当前为什么需要多 Segment + +NavSea 当前为了避免旧的 retained fallback,采用保守的两段方案: + +```text +R0 [0,31) RETAINED +C0 [31,114) CONTINUOUS +``` + +C0 从 `rn-weather-color` 开始一直覆盖到 Style 尾部。 + +真实 diagnostics 已确认该 Continuous 尾段包含天气之后的大量运行时对象,包括 AIS / ownship / navigation / alarm 等 source/layer。 + +这能保护 R0 底图复用,但代价是:天气粒子需要持续 repaint 时,天气以上整个 Style 尾部都留在 Continuous 区域。 + +RenderSegment 的原始设计目标本来就是允许: + +```text +R0 → C0 → R1 → C1 → R2 ... +``` + +并严格保持 Style Z-order,不把业务语义写进 Renderer。 + +## 5. NavSea 真实多段实验 A + +第一次临时配置: + +```text +R0 [0,31) +C0 [31,79) +R1 [79,98) +C1 [98,114) +``` + +Native 接受该配置: + +```text +configured=true +valid=true +segmentCount=4 +retainedSegmentCount=2 +continuousSegmentCount=2 +coverageComplete=true +fallback=false +``` + +解析结果: + +- R0: `background` → `place-labels-land-ja` +- C0: `rn-weather-color` → `ownshipRangeLabelLayer` +- R1: `routeLineActiveShadowLayer` → `planSimBoatLabelLayer` +- C1: `crossCursorGotoLineLayer` → `chartDangerScanCenterLineLayer` + +R1 当时依赖: +- `navigationSource` +- `ownshipTrackSource` +- `planSimRouteSource` + +结果 R1 为 `source-fading` fallback,`reuse=0`。 + +这个结果本身还不能证明 placement 问题,因为 R1 确实包含活动 source。 + +## 6. NavSea 真实多段实验 B:把 R1 收窄到 idle PlanSim + +第二次把 R1 收窄为只有 PlanSim Route 的 10 个 layer: + +```text +R0 [0,31) +C0 [31,88) +R1 [88,98) +C1 [98,114) +``` + +R1: +- start: `planSimRouteShadowLayer` +- end: `planSimBoatLabelLayer` +- dependent source: 仅 `planSimRouteSource` +- 当时 PlanSim 为 idle。 + +5 秒早期诊断仍有 `source-fading`,所以继续等待到约 20 秒稳定态。 + +20 秒稳定态关键结果: + +```text +fadingSourceIds = [] +``` + +R0: +```text +sourceReady=true +sourceStable=true +cacheState=fallback +build=0 +reuse=0 +invalidation=306 +lastInvalidationReason=static-placement +finalDecision=fallback +``` + +R1: +```text +sourceReady=true +sourceStable=true +cacheState=fallback +build=1 +reuse=0 +invalidation=306 +lastInvalidationReason=static-placement +finalDecision=fallback +``` + +这说明稳定 source 已经不是阻塞因素;两个 Retained Segment 被同一个 `static-placement` 门槛同步打回 fallback。 + +## 7. release/particle.19 / .20 当前代码事实 + +### 7.1 per-segment source locality 已经存在 + +`.19 / .20` 在 `renderer_impl.cpp` 中会对每个 Retained Segment 单独计算: + +- `retainedSegmentSourceState(begin,end)` +- source-local revision +- source `ready / stable` +- `retainedSegmentIsStable(begin,end)` +- `segmentSupportsCache[]` +- 每个 cache 独立 `evaluate()` +- 每段独立 diagnostics / finalDecision + +`render_orchestrator.cpp` 中 `retainedSegmentSourceState()` 已通过 RenderSegment dependency 只检查该 Segment 相关 source 的 loaded / fading 状态。 + +这部分方向是正确的,不应回退到全局 source invalidation。 + +### 7.2 剩余 placement gate 仍是全局聚合 + +`.19 / .20` 的 `renderer_impl.cpp` 仍先对全部 Retained Segment 聚合: + +```cpp +bool retainedStaticUsesPlacement = false; +... +retainedStaticUsesPlacement = + retainedStaticUsesPlacement || + orchestrator.retainedRangeUsesPlacement(...); + +const bool retainedPlacementStable = + !retainedStaticUsesPlacement || + (!renderTreeParameters.placementChanged && + renderTreeParameters.symbolFadeChange == 0.0f); +``` + +然后每个 Segment 的 `segmentSupported` 又共同依赖同一个 `retainedPlacementStable`。 + +因此只要任一 Retained Segment 使用 placement,并且当前全局 frame 有 `placementChanged / symbolFadeChange`,所有 Retained Segment 都可能失去 cache eligibility。 + +### 7.3 full CPU mixed execution 已有基础,但仍有 globalLiveFallback + +`.19 / .20` 已有 `visitRenderSegmentExecution(...)`,理论上可以: + +```text +R0 reuse +C0 live +R1 fallback/live +C1 live +R2 reuse +``` + +但是进入该混合执行前仍有: + +```cpp +const bool globalLiveFallback = + ... + !retainedPlacementStable || + orchestrator.retainedGlobalLightTransition(); +``` + +`!retainedPlacementStable` 会直接导致: + +```text +drawableOpaquePass(0, layerCount) +drawableTranslucentPass(0, layerCount) +``` + +即整帧退回全 live,绕过 per-segment mixed execution。 + +这与 NavSea 实验中 R0/R1 同时出现 `static-placement` fallback 的现象一致。 + +## 8. 希望管理员审查的核心修改目标 + +目标不是简单“关闭 placement 检查”。 + +目标是: + +> 当 placement / symbol fade 只影响某些 Segment 时,不应让与其无关、且自身内容/依赖/相机均稳定的 Retained Segment 一起失效。 + +期望行为示例: + +```text +R0:不含 placement layer,source stable +C0:Weather continuous +R1:含 placement 或当前 placement unstable +C1:AIS / Alarm continuous +R2:不含 placement layer,source stable + +期望: +R0 = reuse +C0 = live +R1 = fallback/live +C1 = live +R2 = reuse +``` + +而不是: + +```text +R1 placement unstable +→ global retainedPlacementStable=false +→ R0/R1/R2 全部失效 +→ 整帧 full live +``` + +## 9. 建议管理员重点评估的实现方向 + +以下是审查建议,不是强制实现方案。 + +### 9.1 先把 placement eligibility 变成 per-retained-segment + +对于每个 Retained Segment 分别判断: + +- 该 Segment 是否实际 `usesPlacement`; +- 该 Segment 的 layer transition / needsRepaint; +- 该 Segment 的 source readiness / fading; +- placement transition 是否确实会改变该 Segment 的最终像素; +- 是否存在必须全局 fail-closed 的 placement 共享状态。 + +至少对于 **完全不含 placement layer 的 Retained Segment**,应评估是否有理由继续受全局 `placementChanged / symbolFadeChange` 阻塞。 + +### 9.2 审查 `globalLiveFallback` 中的 `!retainedPlacementStable` + +建议确认是否可以: + +- 从 globalLiveFallback 移除纯 placement 条件; +- 让 placement 结果进入每段 `segmentSupported / decision`; +- 真正使用现有 `visitRenderSegmentExecution()` 做 mixed retained/live。 + +真正全局的条件仍可保持全局,例如: +- 3D; +- Debug 模式; +- MapMode 不支持; +- sprite/image/global resource 未就绪; +- global light transition(若确实会改变所有段); +- context/resource 失效; +- 无法证明安全的全局状态。 + +### 9.3 不要破坏已有 source-local 机制 + +`.19/.20` 已有 `retainedSegmentSourceState()` 与 dependency-scoped revision/readiness。 + +目标应是在此基础上继续收窄 placement/global gate,而不是重新把 source/tile mutation 退回“所有 Retained 一起 invalidate”。 + +### 9.4 placement 本身若无法简单 local 化,应明确分层 + +管理员需要判断 MapLibre placement 是否存在以下区别: + +1. Segment 完全不使用 placement:理论上是否可忽略全局 placementChanged? +2. Segment 使用 symbol placement,但其相关 source/layer 没变化:是否仍必须 invalidate? +3. cross-source collision / shared collision index 是否会让某 Segment 的符号像素受其他 Segment placement 影响? +4. `symbolFadeChange` 是全局时间量还是可以按 dependency/range 推导? +5. 是否需要一个 per-segment placement revision/fingerprint,而不是 bool? + +如果 2/3 无法安全局部化,也至少应优先解决第 1 类,让“无 placement Retained Segment”不被无关 placement 拖死。 + +## 10. 不能牺牲的正确性约束 + +任何优化都必须保持: + +- Style Z-order 是唯一顺序事实; +- Segment 不重新编号 Layer 的全局 ordinal; +- opaque/translucent/depth 语义不变; +- retained cache 不得显示 stale 像素; +- Camera / viewport / pixelRatio 变化仍正确 invalidation; +- Style reload / topology change 正确释放旧 cache; +- Android / iOS 使用同一 Shared Core 语义; +- 不把 AIS / Route / Weather 等业务名称写进 Renderer; +- 不要求 NavSea 手工 invalidate Native cache; +- 不新增“只为 NavSea 绕过 Renderer”的私有 API。 + +## 11. 建议新增/补强的 Core 测试 + +至少建议覆盖: + +### Case A:无 placement 的 R0 + 有 placement 的 R1 +- R0 stable; +- R1 发生 placement transition; +- 期望 R0 继续 reuse; +- R1 单独 fallback/live; +- 最终 Z-order 正确。 + +### Case B:两个 Retained 都稳定 +- R0/R1 均 build 一次; +- 后续 reuse 持续增长; +- Continuous 段持续 live。 + +### Case C:单一 source fading +- fading source 只属于 R1; +- R1 fallback; +- R0 不 invalidation。 + +### Case D:Continuous source 高频 mutation +- mutation 只属于 C1; +- R0/R1 retained revision 不变化; +- cache 可继续 reuse。 + +### Case E:真正全局状态 +- global light transition、3D、Debug、context/resource failure 等; +- 继续 fail-closed; +- 不为追求 reuse 放宽安全边界。 + +### Case F:topology 变化 +- R-C-R-C → R-C; +- 旧 retained texture/compositor 正确释放; +- 不 composite stale resource。 + +## 12. Diagnostics 验收要求 + +修复后 diagnostics 必须能证明“局部失败不扩散”。 + +NavSea 重新做 R-C-R-C 真机测试时,至少需要看到类似: + +```text +R0: + sourceReady=true + sourceStable=true + cacheState=reuse + reuse 持续增长 + +R1: + finalDecision=fallback + lastFallbackReason=static-placement + +C0/C1: + 正常 live +``` + +关键验收不是 Fast Path hit。 + +多 Continuous Segment 本来就可以继续走 full CPU mixed renderer;即使 `fastPathHit=0`,只要稳定 Retained 段能够独立 reuse,优化就是有效的。 + +## 13. 与 NavSea 最新 Trace 的关系 + +`Trace-20261007T224751.json` 是 UDP + 天气 UI 的 DevTools Trace。 + +该 Trace 中约 11–13 秒和 21–23 秒出现两组很高的 JS 峰值;调用栈显示峰值主要落在 React root work 与 `WeatherForecastStrip / WeatherTable`,其中 `WeatherTable` 两次重 render 分别约 0.91s / 1.07s,AIS decoder 并不是这两个峰值的主要执行栈。 + +代码同时显示 `WeatherForecastStrip` 会订阅 `smoothGPS`,并用经纬度 `toFixed(2)` 形成天气采样位置 key;但当前尚未完成对这两次具体 WeatherTable render 触发源的一一证明,因此本文不把“约 10 秒”直接归因于 GPS 跨格或任何单一 NavSea 机制。 + +这些 JS/UI 峰值属于 NavSea 侧独立调查项,不应作为 Particle RenderSegment 修改的直接证据。 + +Particle 本次应以 **RenderSegment diagnostics 的 R0/R1 `static-placement` 同步 fallback** 为主要证据。 + +## 14. 管理员需要给出的结论 + +请管理员审查后明确回答: + +1. `.19/.20` 当前全局 `retainedPlacementStable` 是否确实过度保守? +2. 对“不含 placement layer”的 Retained Segment,是否可以安全解除全局 placementChanged/symbolFadeChange 阻塞? +3. `globalLiveFallback` 是否应去掉 placement 条件,转为 per-segment decision? +4. 若 placement 必须部分全局,最小安全局部化边界是什么? +5. 正式修改应基于 `release/particle.20`、合并回 `particle/main`,还是先在独立工作树完成? +6. 是否需要新 ADR/RFC?初步判断:若公共 API/C-001 语义不变,只是 Renderer 内部优化,可不新增跨项目 Contract;若管理员认为会改变 RenderSegment 公共语义,再交回 Integration 决定。 + +## 15. 当前 NavSea 状态 + +NavSea 临时 R-C-R-C 实验已经撤回。 + +当前恢复为原有保守方案: + +```text +R0 [0,31) RETAINED +C0 [31,114) CONTINUOUS +``` + +因此管理员可以先只做 Native/Core 审查,不需要 NavSea 同步修改。 + +等 Particle 给出明确方案和可验证版本后,再由 NavSea 重新选择真实 Style 边界并做 R-C-R-C / R-C-R-C-R 真机验证。 diff --git a/docs/20_调查/STEP-048_下一阶段规划调查报告_v1.md b/docs/20_调查/STEP-048_下一阶段规划调查报告_v1.md new file mode 100644 index 000000000..39de6b2e3 --- /dev/null +++ b/docs/20_调查/STEP-048_下一阶段规划调查报告_v1.md @@ -0,0 +1,82 @@ +# STEP-048 下一阶段规划调查报告 + +## 基线与方法 + +- 分支:`particle/main`;本地和 `origin/particle/main` HEAD 均为 `46dceb5cb6110c94393b7413465f843844dd6502`。 +- 使用 Gitea API `api/v1/repos/tei/MapLibreParticleLab/branches/particle%2Fmain` 确认,不依赖网页缓存。 +- 工作区只有用户既有 untracked 任务书、日志和 `STEP-046-e48fa087-review.diff`;本调查不触碰它们。 + +## 当前实际结构 + +### Core Runtime + +- `src/mbgl/style/sources/raster_array_source.cpp` 的 `RasterArrayTimelineRuntime` 持有 entries、cursor (`t0`/`mix`)、speed、state、gap/invalid、monotonic clock;`play/pause/setSpeed/seek/step/recover` 是低频 Core 命令。 +- 同文件的 `RasterArraySource::updateRuntimeFieldWindow()` 是已可作为正式输入窗口 API 的最小接入点:检查 validity/compatibility/content identity/generation,推进 T0/T1 并通知 source changed。`advanceRuntimeFieldWindow()`、`repeatRuntimeFieldWindow()` 和 `controlRuntimeTimeline()` 是 validation convenience/control 面。 +- `tryInitializeTimeline()` 目前将 initial/next/runtime fixture 解码为 6 个 Source-owned snapshot,Validation cache capacity=8;这是有界 validation preload,并非生产 forecast cache。 +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` 的 `RenderRasterParticleLayer` 读取 `RenderRasterArraySource::impl().field/nextField` 或 shared timeline;Field identity 决定 GPU upload/reuse,window overlap 保留旧 T1 作为新 T0。其 `ParticleGpuValidationTweaker::execute()` State pass 负责连续 tick/UBO mix;无需 Android per-frame JNI。 + +### RasterParticleLayer 与 Renderer + +- `include/mbgl/style/layers/raster_particle_layer.hpp` / `src/mbgl/style/layers/raster_particle_layer_impl.hpp` 定义正式 layer 和强类型 active、mask、timeMix、count、streak/trail/color properties。 +- GPU state/trail/field texture 生命周期在 `render_particle_validation_layer.cpp` 的 `ParticleGpuResources`;visual property 更新只更新 resources/UBO,不要求 Source reload。 +- Renderer 已是正式 raster-particle 路径,但同一实现文件仍含 validation coast mask、legacy validation display 和 fixture diagnostics。这些属于验证快捷路径,不应让 provider/network 进入 renderer。 + +### 数据输入边界 + +- `src/mbgl/particle/field/offline_fixture_particle_adapter.hpp/.cpp` 的 `ParticleFieldSnapshot` 是 backend-neutral immutable value;包含网格、EPSG:4326 bounds、canonical U=east/V=north、NoData、dataVersion、generation、SHA-256、velocity/valid。 +- `OfflineFixtureParticleAdapter::load(bytes)` 已是纯 C++ bytes loader,但它强制 fixture-v1 schema 和 `knownFixture()`(east/north/vortex/nodata)。`RasterArraySource::loadDescription()` 从 FileSource 请求 metadata/field bytes,再调用它。 +- Android `ParticleCoreLayerValidationActivity.kt::validationStyle()` 拼 `asset://integration/particle-fixtures/...` 与 runtime fixture descriptors;Kotlin 是 Validation Host,不持有 T0/T1/tick/clock,但确实构造 synthetic fixture source。 + +### Android/JNI 与 WeatherColorLayer + +- Android 的 Timeline 控制经 `MapLibreMap → NativeMapView → native_map_view.cpp::controlRasterArraySourceTimeline()` 低频进入 Source;没有 per-frame JNI。TIME/VIS/COUNT/MAP/MORE 都是 Test App 验证面,不应进入正式业务链路。 +- 当前仓库没有 `WeatherColorLayer` 实现类或 renderer 文件;它只出现在架构规范,故与当前 `RasterParticleLayer` 没有运行时 source/field/timeline 耦合。下一阶段不应实现或合并它。 + +## Validation-only 技术债 + +状态栏自动 MIX、WAITING/INVALID 可观测性不够明显,已验证为 `NON-BLOCKING`。它适合未来有 Android 综合 APK 时顺带改善,不能单独触发一次 Android build,也不是 Timeline/Renderer failure。 + +## 候选阶段 + +| 候选 | 目标与价值 | 风险/冻结边界 | Android 综合验收 | +|---|---|---|---| +| A:Provider-neutral C++ Data Adapter | 将 fixture-only decode contract 抽为通用 immutable snapshot adapter,保留 offline fixture adapter 为一个实现;使 Weather Service 未来可向 Source 提交数据而不进入 renderer。 | 中等;必须维持 snapshot identity、NoData、U/V、T0/T1/reuse/fail-closed。 | 高,适合一次验证 raw/in-memory T0/T1、A/B→B/C、invalid。 | +| B:生产 Renderer 清理 | 从 renderer 中拆 validation coast/diagnostics。 | 高,容易触碰已验证 particle path,当前没有真实 data input 的收益低。 | 中等,不推荐现在做。 | +| C:Validation UI 可观测性 | 状态行显示真实 playback state。 | 低但仅工具收益,不能单独编译。 | 仅作为 A 的附带项。 | +| D:WeatherColorLayer | 增加量化颜色层。 | 新模块且缺正式输入 contract,会提前耦合两层。 | 不推荐现在做。 | + +## 推荐:STEP-048「Provider-neutral C++ Field Data Adapter 与 Runtime 输入契约」 + +英文短名:`Field Data Adapter Contract`。 + +### 目标 + +1. 定义 provider-neutral C++ metadata+bytes → immutable `ParticleFieldSnapshot` adapter contract,解除 `knownFixture()` 对正式输入的限制。 +2. 保持 `OfflineFixtureParticleAdapter` 为 fixture-v1 validation implementation,不让 Android/asset API 跨入 Core adapter。 +3. 为 Source 增加面向已解码 snapshot/时间片描述的明确生产入口,复用现有 `updateRuntimeFieldWindow()` 的 T0/T1 identity、reuse、NOOP、fail-closed 语义。 +4. 为 invalid/incompatible/provider decode failure 给出明确 error,不污染当前窗口。 +5. 将 Validation UI observability 作为同一 APK 的小附带项,而非单独阶段。 + +### 不做什么 + +不接真实 HTTP/GRIB/provider/vendor;不改 Timeline clock、Particle Renderer/shader、GPU simulation、WeatherColorLayer、iOS/Metal/Vulkan、React Native、容量或独立 UI 重构。 + +### 最小修改面(预期) + +- Core:新增 `src/mbgl/particle/field/` 下 provider-neutral adapter/interface 与定向 C++ tests;保留 `ParticleFieldSnapshot` ownership(adapter 创建 immutable value,Source 接管/复制一次)。 +- Source:`raster_array_source.hpp/.cpp` 仅增加显式 production submit/decode boundary;复用现有 runtime window API。 +- Renderer:不需要修改,除非为读取已经存在的 snapshot identity 做极小编译适配。 +- Android:Test App 仅增加一次可发现的 in-memory/provider-neutral validation scenario 与状态展示;JNI 只发低频 submit 命令,绝无 per-frame API。 +- 文档:STEP/报告/测试矩阵。 + +### 接口方向与生命周期 + +adapter 输入为 provider-neutral descriptor(logical timestamp、field type/domain/grid/vector/NoData/integrity)与 bytes;输出 immutable snapshot 或 structured decode error。Adapter 不拥有 GPU、Style、Renderer 或网络。Source 是当前 T0/T1/timeline authority;Renderer 只消费 snapshot/GPU textures。decode invalid、time interpolation incompatible、generation/content identity conflict 均 fail-closed,继续最后有效 window。 + +### 一次 Android 综合验收 + +Codex 完成完整批次后,用户在 Linux 一次构建,Windows 一次 copy/install,再在设备 `192.168.1.146` 用 `adb -s 192.168.1.146 ...` 验收:初始 provider-neutral A/B;A/B→B/C→C/D reuse/only-new-T1;duplicate NOOP;invalid/incompatible fail-closed/recover;PLAY/PAUSE/MIX/VIS/MAP 回归。命令设计为 Windows PowerShell/CMD,不依赖 Linux grep/sed/awk/tee。 + +## 结论 + +推荐直接编写 STEP-048 实现任务书。需要一个小型 C++ adapter contract(新模块/文件,非新 renderer 架构);不需要新的 Renderer、Timeline 或 WeatherColorLayer 架构。 diff --git a/docs/20_调查/STEP-050_正式数据链路下一阶段调查报告_v1.md b/docs/20_调查/STEP-050_正式数据链路下一阶段调查报告_v1.md new file mode 100644 index 000000000..b6d4af4ea --- /dev/null +++ b/docs/20_调查/STEP-050_正式数据链路下一阶段调查报告_v1.md @@ -0,0 +1,199 @@ +# STEP-050 正式数据链路下一阶段调查报告 + +## 1. Git / 调查范围 + +- 仓库:`MapLibreParticleLab` +- 分支:`particle/main` +- Starting HEAD:`1572b6c3b1bcc2ab0007181bad0609e7d0134575` +- `origin/particle/main`:`1572b6c3b1bcc2ab0007181bad0609e7d0134575` +- Gitea API 分支 HEAD:同上 +- 调查性质:代码与既有文档只读调查;未修改 Core、Renderer、Android、Tests 或 build files。 + +## 2. 结论摘要 + +STEP-049 的 provider-neutral contract 已经存在,但当前真正落点仍是 Core 内部的“已校验 bytes → immutable Snapshot”边界,不是生产数据源。当前实际链路为: + +`FileSource::request(Resource::source(...))` +→ `RasterArraySource` 收集 metadata/field bytes +→ `OfflineFixtureParticleAdapter::load()` 解析并校验 `fixture-v1` +→ `ParticleFieldDataAdapter::decode()` +→ `ParticleFieldSnapshot` +→ `RasterArraySource::Impl::field/nextField` +→ `RasterArrayTimelineRuntime` / `updateRuntimeFieldWindow()` +→ Renderer。 + +仓库没有 Weather Service client、provider abstraction、正式天气网络 schema、GRIB/NetCDF decoder、Particle 生产 cache 或 production feed API。MapLibre 的通用 `FileSource`/HTTP 能力不能视为 Particle Weather Service。 + +推荐 STEP-051:**Local Production-like Field Feed 与 Core Snapshot Submit Bridge**。先用本地/离线外部 binary feed 驱动正式 neutral adapter 和既有 Runtime,建立稳定的 production-like producer/submit 边界,再在后续独立阶段讨论 provider/network decode。 + +## 3. ParticleFieldDataAdapter 现状 + +### 3.1 API 与真实文件 + +- `include/mbgl/particle/field/field_data_adapter.hpp` + - `ParticleFieldDescriptor`:`fieldID`、`fieldType`、`displayDomain`、`validTime`、尺寸、地理 bounds、`noData`、`dataVersion`、`byteLength`、`stride`、`layout`、`encoding`、可选 `fieldSha256`。 + - `ParticleFieldSnapshot`:拥有自己的 `std::string` 与 `std::vector`/`std::vector`,保存 canonical Runtime 数据和 `generation`。 + - `ParticleFieldAdapterError` / `ParticleFieldAdapterResult`:结构化错误与可日志化 message。 + - `ParticleFieldDataAdapter::decode(const ParticleFieldDescriptor&, const std::vector&, uint64_t generation)`:当前唯一正式 decode API。 +- `src/mbgl/particle/field/field_data_adapter.cpp` + - 校验 identity/generation、dimensions/bounds/noData、`interleaved-uv`、`float32-le`、stride、byte length 与 finite data。 + - 将 payload copy 到 Snapshot;adapter 不保存外部 bytes、Android 对象、Timeline、Renderer resource 或 provider 对象。 + - 当前 renderer 语义仍限定 `WIND` + `GLOBAL`,Wave/Tide/Current 尚未实现。 +- `src/mbgl/particle/field/offline_fixture_particle_adapter.cpp/.hpp` + - `OfflineFixtureParticleAdapter::load()` 是当前 caller 的 validation wrapper:解析 `maplibre-particle-fixture/v1`、校验 fixture whitelist、metadata、checksum,再构造 neutral descriptor 调正式 adapter。 + +### 3.2 Caller 与 generation/identity + +当前调用者为: + +1. `src/mbgl/style/sources/raster_array_source.cpp::tryLoad()`:初始 T0/T1 使用 `nextRasterArrayFieldGeneration`,再写入 `Impl::field/nextField`。 +2. `RasterArraySource::tryLoadRuntimeField()`:runtime candidate bytes 完整后调用 fixture wrapper,candidate 在 adapter 前获得 Core generation。 +3. `src/mbgl/renderer/layers/render_particle_validation_layer.cpp`:Validation renderer 的 fixture-only fallback/validation 资源路径仍调用 fixture wrapper。 +4. `test/style/source.test.cpp`:直接 adapter、fixture wrapper、Runtime Window 与 Timeline 定向测试。 + +因此当前没有 Weather provider 或 production caller。`generation` 是 Core runtime revision,由 Source/Runtime 负责分配;adapter 只校验调用者传入的非零 generation。`fieldID`/identity 由 descriptor producer 提供,adapter 不生成业务 identity。 + +## 4. ParticleFieldSnapshot ownership 与正式输入适配性 + +Snapshot 已基本 provider-neutral:不含 URL、token、provider 名称、网络客户端、Android 对象或 GPU resource;vector ownership 稳定,适合作为 Runtime 输入。现存 fixture 语义不再进入 Snapshot,fixture ID 在 wrapper 中映射为 `fieldID`。 + +它已包含当前 Runtime 所需的最小 grid/field metadata,但存在两项生产化边界: + +- `validTime` 已存在于 neutral descriptor/Snapshot,但当前 fixture metadata 的 `timestamp` 未映射,Timeline Source 仍使用固定逻辑时间。 +- Snapshot 的 `isValid()` 目前按 Wind/Global renderer 语义判定;未来 Ocean-only field/mask 需要另行设计,不应在本阶段偷偷扩大。 + +## 5. RasterArraySource / Runtime submit 入口 + +### 5.1 当前入口 + +`include/mbgl/style/sources/raster_array_source.hpp` 提供: + +- `updateRuntimeFieldWindow(ParticleFieldSnapshot newT1)`:已校验 Snapshot 进入唯一 authoritative T0/T1 Window 的正式入口。 +- `advanceRuntimeFieldWindow()` / `repeatRuntimeFieldWindow()`:当前 Source-owned validation convenience API。 +- `controlRuntimeTimeline()`:Android 低频命令桥,Core 持有 Timeline/Playback。 + +如果未来已经拥有一个合法 `ParticleFieldSnapshot`,最短路径是直接调用 `updateRuntimeFieldWindow(std::move(snapshot))`;它会复用现有 `OLD_T1 → NEW_T0`、only `NEW_T1`、Duplicate NOOP 和 fail-closed 逻辑。当前该路径只适合已有 T0/T1 的 Source,尚未提供独立的 production feed/submit queue 或外部 source ownership bridge。 + +### 5.2 当前 Source 的 fixture 限制 + +`src/mbgl/style/conversion/source.cpp::convertRasterArraySource()` 要求 `fixture`、`metadata`、`field`,runtime candidates 要求 `fixture`、`metadata`、`field`。`RasterArraySource::loadDescription()` 通过 MapLibre `FileSource` 请求 bytes,但随后仍直接调用 fixture wrapper。 + +所以 Source 已有可复用的异步 byte transport,却不是 production-neutral source:它的 style schema、成员命名、metadata parser 和 candidate preload 仍是 fixture validation 入口。下一阶段应增加一个极薄的 neutral feed/submit bridge,或把现有 Source 输入适配到 neutral descriptor;不应复制第二套 Window。 + +## 6. Timeline snapshot 数据来源 + +`RasterArraySource::tryInitializeTimeline()` 构造 `RasterArrayTimelineRuntime::Entry`: + +- `Impl::field` 作为 index 0/T0; +- `Impl::nextField` 作为 index 1/T1; +- `runtimeFields[index].snapshot` 作为后续 candidates; +- 逻辑时间当前硬编码为 `0`、`40000`、`80000 + index * 40000`。 + +`RasterArrayTimelineRuntime` 本身只消费 immutable Snapshot shared pointers、严格递增 timestamp 和有限 cache(capacity 8),其 `play/pause/tick/seek/step/gap/invalid/recover` 不认识 fixture schema。它能够消费外部 production Snapshot,但当前 Source 的 builder 只能从 fixture candidate 生成 Snapshot,且 candidate preload 仍由 fixture wrapper 完成。 + +结论:下一阶段原则上不改 Timeline;提供新的 Snapshot producer,并在 Source 侧把 producer 结果接到现有 entries/submit 入口即可。若生产 feed 需要真实 forecast timestamp,应在 producer/Source metadata 接线中补齐 Entry timestamp,而不是重做 Playback。 + +## 7. 仓库已有数据源能力与边界 + +### 已存在但不是 Particle 生产业务 + +- MapLibre 通用 `FileSource`、`Resource::source()`、异步 `request()`:可请求 asset/file/HTTP 资源,已有于 `GeoJSONSource`、`ImageSource`、`TileSource` 等通用 Source。 +- `src/mbgl/util/io.cpp::read_file()`:通用本地文件工具。 +- `platform/default/src/mbgl/storage/offline_database.cpp`、offline download:MapLibre 离线资源能力,不是天气字段 decoder 或 Particle field cache policy。 +- `test/fixtures/resources` 和 `particle-fixtures/*/field.f32`:测试资源。 + +### 不存在 + +搜索 `weather`、`forecast`、`provider`、`grib`、`netcdf`、`windy` 等 Particle 业务能力后,没有 Weather Service client、provider registry/factory、外部天气 schema、GRIB/NetCDF decoder、production field downloader 或专用 cache。当前唯一 binary field loader 是 fixture wrapper 的 `std::vector` 读取/异步 bytes 路径;没有 production local binary source。 + +因此不能因为 MapLibre 有 HTTP/FileSource 就直接引入网络。当前最小 production-like 验证应使用本地或离线 bytes,保持 provider-specific decode 在 adapter 上游,Renderer 只消费 Snapshot。 + +## 8. 候选方案比较 + +### 候选 A:Core Production Field Feed / Snapshot Submit API + +- 目标:建立接收已校验 descriptor/payload 或 Snapshot 的极薄 Core feed bridge。 +- 原因:当前 adapter 已稳定,但 Source 仍只有 fixture style schema;A 能先解耦 producer 与 fixture schema。 +- 涉及:新建小型 Core feed 类型,最小调整 `RasterArraySource`;复用 `ParticleFieldDataAdapter` 与 `updateRuntimeFieldWindow()`。 +- 新模块/新架构:可有一个小模块,不建立 Provider Framework。 +- network/vendor:不需要;不允许进入 Renderer。 +- Timeline/Renderer:不修改 Timeline 语义,不修改 Renderer。 +- RasterArraySource:需要最小 submit/initial-window 接线。 +- Android APK:若只做 Linux Core contract,可不需要;若增加 validation entry,则和 B 合并后一次 APK。 +- 风险:初始 T0/T1 建立、generation 分配与 timestamp ownership 需要明确,不能让 feed 管理第二套 Window。 + +### 候选 B:Local Binary / File-backed Production-like Field Source + +- 目标:用真实外部 binary + neutral metadata(不含 fixture whitelist/provider 信息)驱动 adapter、Timeline 和 Runtime,替代 synthetic fixture 验证生产链路。 +- 原因:当前缺口最具体,能验证 payload ownership、时间 metadata、preload 和 fail-closed,而不提前绑定天气供应商。 +- 涉及:local/asset feed loader、neutral metadata decode、`RasterArraySource` 最小 bridge、离线 fixture/test resource。 +- 新模块/新架构:一个轻量 source/loader;不需要 Provider Framework。 +- network/vendor:不需要;provider-specific decode 留在上游边界。 +- Timeline/Renderer:原则上不修改 Timeline/Renderer;复用现有 entries 与 Window。 +- RasterArraySource:需要最小修改,使 style/source 不再强制要求 `fixture`,或增加明确 production-like feed 构造入口。 +- Android APK:建议需要一次,用同一 APK 覆盖 local feed、Timeline、Window/Playback 回归;不单独拆 build。 +- 风险:本地 metadata schema 若设计过大,会重新形成第二套业务 schema;必须只覆盖当前 descriptor 与 forecast timestamp。 + +### 候选 C:Weather Service / Network Provider 接入骨架 + +- 目标:直接建立网络 weather service/provider client。 +- 原因:最终产品需要,但当前仓库没有成熟服务边界或外部 decoder。 +- 涉及:Android/Core network、鉴权、retry、cache、provider schema 和 decoder。 +- 新模块/新架构:会引入明显新架构。 +- network/vendor:需要,且风险最高;绝不能进入 Renderer。 +- Timeline/Renderer:应不改 Timeline/Renderer,但上游数据生命周期和错误映射复杂。 +- RasterArraySource:可能需要异步 feed/preload API。 +- Android APK:需要,且会显著扩大综合验收。 +- 风险:违反第一阶段离线边界,容易把 vendor/network 业务塞进 Core/Android 或模糊 fail-closed 语义。当前不推荐。 + +### 候选 D:Timeline/Preload 通用化 + +- 目标:先把 Timeline candidate/cache 从 fixture 命名和固定逻辑时间中抽象出来。 +- 原因:当前 `RasterArrayTimelineRuntime` 的消费接口其实已 neutral,只有 Source builder 仍 fixture-specific。 +- 涉及:`RasterArraySource` builder、timestamp metadata、candidate ownership。 +- 新模块/新架构:不应新增模块;属于现有 Source 小范围整理。 +- network/vendor:不需要。 +- Timeline/Renderer:尽量不修改 Timeline state machine,可能需要 Entry timestamp 接线;Renderer 不改。 +- Android APK:只有和 A/B 合并时需要一次。 +- 风险:单独做 D 会在没有真实 production producer 的情况下提前抽象,收益小于 B;不得变成第二套 cache/state machine。 + +## 9. 推荐下一实现阶段 + +- STEP 编号:`STEP-051` +- 中文名称:**Local Production-like Field Feed 与 Core Snapshot Submit Bridge** +- 英文短名:`Local Production-like Field Feed` + +目标范围: + +1. 定义一个最小 local/external binary feed 输入,使用 neutral descriptor + bytes,不使用 fixture whitelist。 +2. 让该 feed 通过 `ParticleFieldDataAdapter` 生成 immutable Snapshot,并明确 generation、identity、validTime 和 payload ownership。 +3. 在 `RasterArraySource` 增加最小 initial-window/next-feed bridge,继续调用既有 `updateRuntimeFieldWindow()` 和唯一 Timeline。 +4. 让 Timeline 使用 producer 提供的 forecast timestamps,验证 preload、PLAY、auto MIX、boundary、Gap/Invalid fail-closed。 +5. 仅使用离线 local/asset payload;增加 C++/Core 定向测试与一次 Android 综合验证入口。 + +明确不做: + +- 不进入 iOS/Metal; +- 不重做 Timeline、Playback 或 Runtime Field Window; +- 不重做 Renderer/GPU simulation/trail; +- 不把 provider/network/vendor 逻辑放入 Renderer; +- 不实现 Weather Service、HTTP client、鉴权、retry、cache server 或真实 provider decoder; +- 不合并 WeatherColorLayer 与 RasterParticleLayer; +- 不为 MIX observability 单独触发 Android build; +- 不为 Rotation/Viewport 技术债单独触发 Android build。 + +## 10. Rotation / Viewport 与 MIX 技术债 + +当前 `ParticleCoreLayerValidationActivity.kt` 具备 `onStart/onResume/onPause/onStop/onDestroy` 对 `MapView` 的标准生命周期转发,并通过 `onSaveInstanceState()` 保存 Validation 控件状态;代码中没有独立的 Particle viewport/camera 恢复状态机。历史文档仍记录过 Rotation/Viewport 自动恢复延期与后续 control-mode 恢复,现状不足以把它当作生产数据链路已完全证明的 contract。 + +STEP-051 推荐阶段不需要修改 Android Validation Host、viewport 或 source reload,因此 Rotation/Viewport 保持独立技术债,不合并、不为其单独 build。若 STEP-051 为增加 local feed 入口必须改 UI,则只在同一 APK 中顺带回归,不扩展为独立修复。 + +MIX UI 文本滞后仍是 `NON-BLOCKING observability`;Core MIX 已通过。STEP-051 若本来增加 Android 状态入口,可顺带刷新显示;若不需要 UI 变化则继续保留,不单独触发 build。 + +## 11. Android 成本与一次验收建议 + +STEP-051 先完成 Linux Core/OpenGL 精确编译、adapter/source/timeline 定向测试、离线 local feed 资源与验证入口,再由用户用一次 APK 覆盖:local feed load、Timeline READY/PLAY、auto MIX、至少两个 Window boundary、OLD_T1 reuse、only NEW_T1 upload、Pause/Resume、Step/Seek、Gap/Invalid、Duplicate NOOP、Renderer fail-closed、rotation/lifecycle 回归(若入口有改动)。不执行 Jest;Codex 不执行 Android Gradle、APK、ADB 或 App 启动。 + +## 12. 是否可进入 STEP-051 实现 MD + +可以。调查已确定最小实现方向、边界和验收策略:先写 STEP-051 设计/任务 MD,冻结 local feed descriptor/metadata、timestamp/generation ownership 与 Source bridge,再实现。不要在 STEP-051 直接引入 Weather Service/network/provider framework。 diff --git a/docs/20_调查/STEP-052_正式数据源接入下一阶段调查报告_v1.md b/docs/20_调查/STEP-052_正式数据源接入下一阶段调查报告_v1.md new file mode 100644 index 000000000..8ad0ff0f5 --- /dev/null +++ b/docs/20_调查/STEP-052_正式数据源接入下一阶段调查报告_v1.md @@ -0,0 +1,225 @@ +# STEP-052 正式数据源接入下一阶段调查报告 + +## 1. Git / 调查基线 + +- 仓库:`MapLibreParticleLab` +- 分支:`particle/main` +- Starting HEAD:`e4ab1b9d421a4559fa3627df07ee91dcd9b680e4` +- `origin/particle/main`:`e4ab1b9d421a4559fa3627df07ee91dcd9b680e4` +- STEP-049:`CLOSED / PASS` +- STEP-051:`CLOSED / PASS` +- 调查性质:代码与既有文档只读;本任务不修改功能代码。 + +## 2. 结论摘要 + +STEP-051 已把一条真实可验证的 local production-like 链路接到 Core: + +`asset metadata + binary payload → LocalParticleFieldFeed → ParticleFieldDescriptor → ParticleFieldDataAdapter → ParticleFieldSnapshot → RasterArraySource → Core Timeline/Runtime Field Window → Renderer`。 + +但它仍不是正式生产数据源。当前 local metadata 是新的 `maplibre-particle-field/v1`,payload 仍复用仓库已有的 canonical `particle-fixtures/*/field.f32`;`RasterArraySource` 直接拥有资源请求、candidate 收集、解析和 Timeline 初始化。仓库没有 Particle 专属的 feed coordinator、forecast catalog/manifest、异步 field-arrival API、retry/timeout/stale policy 或专用 transport cache。 + +MapLibre 通用 `FileSource` 已提供 asset、filesystem、HTTP、offline database 等资源能力,但这不等于已有 Weather Service。当前最合理的下一实现阶段不是直接绑定天气供应商,而是先建立 provider-neutral feed/manifest 与低频 arrival coordinator,并用 Local/File/Mock transport 验证。之后再单独接入真实 HTTP/provider。 + +## 3. STEP-051 后的真实数据链路 + +### 3.1 Local feed 起点与终点 + +入口和 contract: + +- `include/mbgl/particle/field/local_particle_field_feed.hpp` +- `src/mbgl/particle/field/local_particle_field_feed.cpp` +- metadata:`particle-production/field-00.json` 至 `field-03.json` +- Android metadata:`platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/` + +`LocalParticleFieldFeed::load(metadataText, payload)` 只处理 provider-neutral metadata 和内存 bytes,调用唯一正式 `ParticleFieldDataAdapter::decode()`。它不打开文件、不请求网络、不保存缓存,也不认识 Android、Timeline 或 Renderer。 + +STEP-051 的 Android `ParticleCoreLayerValidationActivity.kt` 使用 `production-fields` style source,metadata 走 `asset://integration/particle-production/field-*.json`,binary 仍来自既有 `asset://integration/particle-fixtures/*/field.f32`。这已经脱离 fixture metadata/wrapper,但 payload 内容尚是验证用 canonical binary,而非远程生产数据。 + +### 3.2 Provider-neutral 边界 + +正式 neutral 边界位于: + +`ParticleFieldDescriptor + payload bytes → ParticleFieldDataAdapter → immutable ParticleFieldSnapshot`。 + +`include/mbgl/particle/field/field_data_adapter.hpp` 的 Descriptor/Snapshot 不含 URL、token、HTTP client、provider name、Android object 或 GPU ownership。Descriptor/Snapshot 当前满足 WIND/GLOBAL 的 Runtime 最小输入;Ocean-only field、mask、更多物理变量尚未进入本阶段 contract。 + +当前仍留在上游/Source 的内容是资源 URL、metadata 文本、文件请求、asset 读取和 candidate 编排。它们不应进入 `RasterParticleLayer` 或 `ParticleFieldSnapshot`。 + +## 4. Adapter、Snapshot 与 Source 现状 + +### 4.1 Adapter / Snapshot + +- `ParticleFieldDataAdapter::decode(const ParticleFieldDescriptor&, const std::vector&, uint64_t)` 是唯一正式 decode API。 +- `ParticleFieldDescriptor` 已有 `fieldID`、`fieldType`、`displayDomain`、`generation`、`validTime`、dimensions、bounds、`noData`、`dataVersion`、`byteLength`、stride、layout、encoding、可选 checksum。 +- `ParticleFieldSnapshot` 自有 metadata、velocity 和 valid arrays;不拥有外部 bytes、FileSource、GPU texture 或 Timeline。 +- generation 必须由 producer/source 在 adapter 前提供,adapter 不把 0 改成 1;`fieldId` 与 `validTime` 由 producer metadata 提供。 +- `OfflineFixtureParticleAdapter` 仍存在,但只是 fixture-v1 validation wrapper,不是正式 production feed。 + +### 4.2 RasterArraySource / submit + +实际文件: + +- `include/mbgl/style/sources/raster_array_source.hpp` +- `src/mbgl/style/sources/raster_array_source.cpp` +- `src/mbgl/style/sources/raster_array_source_impl.hpp` +- `src/mbgl/style/conversion/source.cpp` + +STEP-051 新增 `RasterArrayLocalFieldDescriptor`、local candidates 和 `submitInitialRuntimeFieldWindow(T0, T1)`。Source 对每个 metadata/field URL 使用 `FileSource::request(Resource::source(...))`,收齐后交给 `LocalParticleFieldFeed`,再由 bridge 建立 authoritative initial T0/T1。后续 Timeline 从 local candidates 构造 entries,使用 Snapshot 的 producer `validTime`。 + +已有 `updateRuntimeFieldWindow(ParticleFieldSnapshot)` 是已有 T0/T1 之后的最短 submit 路径,继续实现 `OLD_T1 → NEW_T0`、only NEW_T1、Duplicate NOOP 和 fail-closed。它不是一个独立的外部 feed queue,也没有 feed-level arrival state、manifest index 或持久化生命周期。 + +### 4.3 Timeline / Playback + +`RasterArrayTimelineRuntime` 位于 `raster_array_source.hpp/.cpp`,只消费 immutable Snapshot entries 和严格递增 timestamp,持有 Core-owned cursor/speed/paused/gap/invalid 状态;cache capacity 当前为 8。 + +这属于 Timeline memory cache,不是 transport/download cache,也不是 persistent field cache。Timeline 已足够消费外部 Snapshot,下一阶段不应重写其 state machine 或 Playback ownership。缺口在 Source/feed producer 如何按时间目录、到达、失败和预取 Snapshot。 + +## 5. 仓库已有 transport / network / cache 能力 + +### 已有但属于 MapLibre 通用基础设施 + +- `include/mbgl/storage/file_source.hpp`:异步 `request()`、`Resource`、cache-only 能力、pause/resume 和资源转发。 +- `include/mbgl/storage/resource.hpp`:`asset/file/network` 资源请求语义、loading method、storage policy、etag/expiry 等 HTTP/cache 元数据。 +- `src/mbgl/storage/file_source_manager.cpp`:按类型与配置复用 FileSource 实例。 +- `platform/android/MapLibreAndroid/src/cpp/asset_manager_file_source.cpp`:Android `asset://` 读取。 +- `platform/android/MapLibreAndroid/src/cpp/http_file_source.cpp` 与 `include/mbgl/storage/http_file_source.hpp`:Android 通用 HTTP FileSource。 +- `include/mbgl/storage/database_file_source.hpp`:MapLibre ambient cache/offline database 的 put、invalidate、clear、容量和 revalidation API。 +- `include/mbgl/util/lru_cache.hpp`:通用内存 LRU 工具。 +- `include/mbgl/storage/response.hpp`:通用成功、NotFound、Server、Connection、RateLimit、etag、expires、retryAfter 等 Response/Error 字段。 + +这些能力可以作为 STEP-053 的底层 transport 适配点,但目前没有 Particle field 的 manifest、字段语义、时间序列目录、payload checksum policy、retry/stale policy 或 adapter 错误映射。 + +### 当前不存在的 Particle 正式能力 + +搜索当前 `src/`、`include/`、Android JNI/Host 和测试区域,没有发现: + +- Weather Service client、Open-Meteo/Windy/vendor client; +- Particle provider abstraction、provider registry 或 provider-specific decoder; +- GRIB/NetCDF/external weather decoder; +- Particle remote manifest/forecast endpoint contract; +- Particle 专用 download queue、transport coordinator、persistent field cache; +- Particle retry、timeout、stale-data、gap inventory 或 availability state policy; +- token/auth/config 与 Particle field service 的绑定。 + +因此不能把 MapLibre 的通用 HTTP/FileSource 误认成 Weather Service 已接入。 + +## 6. Cache、preload 与数据生命周期 + +当前存在四种不同层次,不能合并: + +1. `RasterArrayTimelineRuntime` 的 entries/cache capacity 8:已经解码的 Snapshot memory cache,服务 Timeline cursor。 +2. `RasterArraySource` 的 local/runtime candidate vectors:Source-owned preload candidates,服务初始窗口和后续 boundary。 +3. MapLibre `FileSource`/`DatabaseFileSource` ambient/offline cache:通用资源缓存,按 Resource URL/HTTP metadata 工作,不理解 fieldId、validTime、generation 或 Snapshot。 +4. Renderer GPU slots:RasterParticleLayer/validation layer 的 T0/T1 texture 与 simulation state,不是下载缓存,不能由数据源层直接管理。 + +STEP-051 当前是四个资源请求全部到达后构建 Timeline;没有独立的 manifest-first inventory、有限并发下载、field dedup、retry 或 stale policy。已有 MapLibre cache 可以降低资源获取成本,但不能替代 Particle feed lifecycle。 + +建议下一阶段先定义低频 field arrival/availability 状态和 manifest 顺序,明确:manifest 可用但 payload 缺失、metadata reject、payload mismatch、gap、stale、retry exhausted 时如何保持 last valid Field。Persistent cache 不应在没有 feed key/manifest 生命周期之前先做;第一步只需要 memory/injected transport validation,是否接 DatabaseFileSource 应后置。 + +## 7. Forecast manifest/catalog 最小模型 + +不要把所有未来业务字段塞入 Descriptor。进入正式 feed 必要的最小分层如下。 + +### 已属于 `ParticleFieldDescriptor/Snapshot` + +- field identity:`fieldID` +- producer revision:`generation` +- forecast `validTime` +- `fieldType` / `displayDomain` +- grid width/height、bounds +- vector layout/encoding/noData +- expected byte length、stride、data version、可选 payload checksum + +这些字段是解码、插值兼容性、fail-closed 和 Renderer canonical input 所必需的。 + +### 应属于上游 manifest/catalog,而不是 Snapshot/Renderer + +- forecast `runTime`/cycle +- `leadTime`(可由 runTime 与 validTime 推导,但目录可显式记录) +- ordered snapshot list / window membership +- grid identity、resolution、coverage version +- metadata URL 与 payload URL/resource key +- source revision、availability、retry hint、expiry/stale policy + +manifest 可以有 source/provider identity,但它不应穿过 Adapter 进入 Snapshot,也不应被 Renderer 或 Runtime Window 解释为业务 provider。 + +### 目前不应新增 + +不要仅为“以后可能需要”加入 level、ensemble、unit system、auth token、HTTP headers、vendor schema、download path、cache database key 或 ocean mask 字段。若未来 Wave/Tide/Current 需要这些语义,应单独设计并遵守 Ocean-only 与 mask 边界。 + +## 8. Weather Service 直接接入判断 + +当前不具备安全的直接接入条件。虽然 Android 有 `HTTPFileSource`,MapLibre 有通用 Response/cache/error,当前 Particle 仍缺少: + +- provider-specific metadata/payload decoder 与其到 neutral Descriptor 的映射; +- forecast run/lead/valid-time inventory; +- provider-independent feed API 和 arrival ordering; +- retry/timeout/stale/gap policy; +- service auth/config ownership; +- 可在不改 Renderer/Timeline 的情况下将网络数据稳定送入 Snapshot submit。 + +直接接入会把 HTTP/provider 选择、目录和失败策略塞进 `RasterArraySource` 或 Android Host,容易越过冻结边界。结论:先建立 provider-neutral feed/manifest coordinator,再以独立阶段接 Weather Service;真实 network 不是 STEP-052 后的立即实现目标。 + +## 9. 候选下一阶段 + +| 候选 | 目标 | 新架构/模块 | Network/Provider | Timeline/Renderer | Android | 主要风险 | +|---|---|---|---|---|---|---| +| A. 直接 Weather Service | 立即接真实远程天气接口 | 新增 client、decoder、auth、生命周期 | 需要 | 不应修改,但 Source 会膨胀 | 需要一次高成本 build/test | 缺少 manifest、retry、provider 边界,容易把业务逻辑塞入 Core/Host | +| B. Provider-neutral Feed/Manifest Coordinator | 先定义 manifest、低频 arrival、Snapshot submit 和错误生命周期,用 Local/File/Mock transport 验证 | 一个薄跨平台 feed/coordinator;不建立 Provider Framework | 不需要;可注入 Local/Mock transport | 不改 state machine;不改 Renderer | 若只做 Core 可不需要;若补入口则合并一次 | arrival ordering、initial T0/T1 和 last-valid 语义需冻结 | +| C. Transport Cache/Preload | 先建立持久缓存、重试、过期和预取 | 新增 cache policy/lifecycle | 可不需要 | 不改 Renderer,但会依赖未定义 feed key | 建议需要 | 过早把 MapLibre ambient cache 当 Particle cache,生命周期和 key 尚未稳定 | +| D. Provider Decoder | 先做 GRIB/NetCDF 或某一供应商 decoder | 明显 provider-specific 模块 | 通常需要 | 输出应是 neutral Snapshot | 视入口而定 | 当前无真实 provider contract,容易锁死错误 schema | + +候选 B 能以最小修改面冻结真正缺口,并兼容未来 Local/File、HTTP 和 provider decoder;C 应在 B 的 manifest/key/error contract 之后做,A/D 应后置。 + +## 10. 唯一推荐的下一实现阶段 + +### Recommended Next Implementation: STEP-053 Provider-neutral Field Feed / Manifest 与 Arrival Coordinator + +目标范围: + +1. 定义最小 provider-neutral field manifest/catalog 与 ordered snapshot arrival contract。 +2. 将 metadata/payload transport 与 `ParticleFieldDataAdapter`、`ParticleFieldSnapshot`、现有 initial T0/T1/update submit 接口连接起来。 +3. 以 injected Local/File/Mock transport 验证 initial T0/T1、subsequent T1、strict validTime ordering、duplicate、gap、invalid、retry exhausted 和 last-valid hold。 +4. 只做低频异步 callback/future 或等价 arrival API,不做 Android per-frame JNI,不拥有 Timeline cursor/speed/pause。 +5. 建立结构化 feed availability/error 分类,明确 metadata reject、payload mismatch、not-yet-available、gap 和 stale 的边界。 +6. 保持 provider/vendor identity、URL、auth、HTTP headers 和 cache policy 在上游 feed/transport 层,不进入 Snapshot、Runtime 或 Renderer。 + +输入:manifest/catalog entry、metadata bytes、payload bytes,或注入的 transport response。 + +输出:已通过正式 Adapter 的 `ParticleFieldSnapshot`/arrival result;初始窗口和后续窗口仍调用现有 Core submit/update 语义。 + +允许修改:新增薄的 Core-neutral feed/coordinator、Source 的最小接线、定向 C++ tests、local/mock transport test seam 和必要日志/文档。若增加 Android validation 入口,应与同一阶段其他功能合并一次 build/install。 + +禁止修改:Timeline state machine、Playback、Runtime Field Window 语义、RasterParticleLayer/GPU simulation/trail、WeatherColorLayer、Adapter 合法性、Provider Framework、Weather Service、HTTP client、vendor decoder、第二套 Snapshot/Timeline/Window。 + +STEP-053 默认不需要真实 network、服务器、真实 provider 或 Android APK;先完成 Linux/Core/OpenGL 精确验证即可。只有要验证 Android Host 的低频 feed arrival/错误可观测性时才需要一次 Android build/install。 + +## 11. 建议的后续 2~4 阶段顺序 + +1. **STEP-053 Provider-neutral Field Feed / Manifest 与 Arrival Coordinator**:先冻结 manifest、arrival、错误和 last-valid 生命周期,使用 Local/File/Mock transport。 +2. **STEP-054 HTTP Transport 与 Weather Service Adapter**:在不改变 neutral Snapshot/Runtime/Renderer 的前提下,把真实 HTTP/provider decoder 置于 feed 上游;此阶段才处理 auth/config 和真实服务契约。 +3. **STEP-055 Field Cache、Retry、Stale 与 Preload Hardening**:基于已冻结的 manifest key、generation、validTime 和 error policy,决定 memory/cache/database 组合、重试和容量。 + +这三个阶段足够表达当前真实缺口,不提前规划更多 provider 或 Ocean field 变体。 + +## 12. 平台 / Core / Renderer 责任边界 + +- Transport/platform:负责 FileSource/HTTP/asset 的资源获取、平台凭据配置和底层 request cancellation;不生成 Timeline 状态。 +- Feed/coordinator(建议跨平台 Core-owned seam):负责 manifest 顺序、低频 arrival、metadata/payload 配对、调用正式 Adapter、错误分类和 submit 意图;不负责 GPU。 +- `ParticleFieldDataAdapter`:只做 descriptor/payload contract validation 与 canonical Snapshot construction。 +- `RasterArraySource`:保持 Source-owned authoritative T0/T1、Timeline 接线和已有 Window update;不变成 provider client。 +- Timeline/Playback:Core-owned,消费 Snapshot 时间和 low-frequency feed 结果;不请求网络。 +- Renderer:只消费规范化 Snapshot/Window,继续执行 GPU upload/reuse/simulation/trail;不包含 provider/network/vendor/cache policy。 +- Android/Kotlin:只提供低频用户意图和可选资源配置;不拥有 Timeline、Window、feed state 或 per-frame JNI。 + +## 13. Android 成本、技术债与风险 + +后续实现应遵守:先 Linux/Core/OpenGL precise build 与定向测试,再由用户在 Windows 11 对 `192.168.1.146` 用一次 APK 完成综合验证;若使用 ADB,必须显式 `adb -s 192.168.1.146 ...`。不为单个 MIX 文本问题单独触发 Android 周期。 + +Validation UI 的 auto MIX 文本滞后、WAITING/INVALID 视觉提示不足继续是 `NON-BLOCKING observability`,不阻塞正式 feed 架构,不推荐作为 STEP-053 主任务。Rotation/Viewport 自动恢复继续保留技术债;STEP-053 若不触及 lifecycle/surface/source reload,不合并修复。 + +主要风险是把 manifest/catalog 与 Descriptor 重复建模,或让 Source 直接拥有下载/重试/缓存策略。应优先薄化并用测试锁定:Snapshot immutable、validTime strict ordering、invalid/gap fail-closed、last valid Field、Window reuse、MIX property-only 和 Renderer 分离。 + +## 14. 调查结论 + +当前可以进入 STEP-053 实现 MD 编写,但不能跳过 provider-neutral feed/manifest 与 arrival 生命周期直接实现 Weather Service。STEP-051 production-like local feed 作为已验证过渡层保留;STEP-052 本身不修改代码、不引入网络、不新增缓存或 Provider。 diff --git a/docs/20_调查/STEP-056_RN天气渲染桥接现状调查_v1.md b/docs/20_调查/STEP-056_RN天气渲染桥接现状调查_v1.md new file mode 100644 index 000000000..9c007b0e2 --- /dev/null +++ b/docs/20_调查/STEP-056_RN天气渲染桥接现状调查_v1.md @@ -0,0 +1,147 @@ +# STEP-056 React Native 天气渲染桥接现状调查 + +状态:调查完成(只读);STEP-055 仍为 `IMPLEMENTED / WAITING_ANDROID_VALIDATION`。 + +Starting HEAD:`44a50e6e4a4e79fae7ddf36af007b5e35e969a92`(`particle/main`,`origin/particle/main` 同步确认)。本报告没有修改功能代码、没有执行 Android Gradle/APK/ADB/设备测试或 Jest。 + +## A. 当前真实调用架构 + +```mermaid +flowchart LR + A[ParticleCoreLayerValidationActivity.kt\nsubmitRealWeatherDataset] --> B[MapLibreMap.submitRasterArraySourceHostField] + B --> C[NativeMapView.java\nnativeSubmitRasterArraySourceHostField] + C --> D[native_map_view.cpp\nNativeMapView::submitRasterArraySourceHostField] + D --> E[RasterArraySource::submitHostField] + E --> F[LocalParticleFieldFeed::load] + F --> G[ParticleFieldDataAdapter::decode] + G --> H[immutable ParticleFieldSnapshot] + H --> I[RasterArrayTimelineRuntime\nT0/T1 + cursor/mix] + I --> J[RenderRasterParticleLayer] + I --> K[RenderWeatherColorLayer] + J --> L[State/Trail/particle GPU passes] + K --> M[WeatherColor GPU pass] +``` + +当前真实 WeatherV2 Test App 资源位于 `platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-weatherv2/ecmwf-hres-wind-20260807/`;`ParticleCoreLayerValidationActivity.kt::submitRealWeatherDataset()` 读取每个 `metadata.json` 与 `field.f32`,依次提交 `2026080700/06/12/18`。 + +仓库没有被跟踪的 React Native package、TurboModule、Fabric、JSI 或 RN NativeModule。现有可复用入口是 Android Test App 的 Java/JNI/C++ Host Field Submission,不是 RN binding。 + +## B. 文件与职责清单 + +| 文件 | 类/函数 | 当前职责 | RN v1 可复用 | 备注 | +|---|---|---|---|---| +| `include/mbgl/particle/field/field_data_adapter.hpp` | `ParticleFieldDescriptor` / `ParticleFieldSnapshot` / `ParticleFieldDataAdapter` | canonical descriptor、immutable snapshot、校验/解码 contract | 是 | U=eastward、V=northward、float32-le、interleaved U/V;不含 provider/URL | +| `src/mbgl/particle/field/field_data_adapter.cpp` | `decode()` | 校验 identity/generation/time/dimension/bytes,复制并解码 payload | 是 | Adapter 不补 generation,也不理解 JS/provider | +| `include/mbgl/style/sources/raster_array_source.hpp` | `RasterArraySource::submitHostField` | Host ingress、submit result、Timeline holder | 是 | 当前缺少 `clearWindFields()` | +| `src/mbgl/style/sources/raster_array_source.cpp` | `submitHostField` / `RasterArrayTimelineRuntime` | T0/T1 创建、后续 append、Timeline/Window state | 是 | 有 bounded timeline(8 entries)及 log,不向 Host 发事件 | +| `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java` | `submitRasterArraySourceHostField` / `controlRasterArraySourceTimeline` | Android public facade | 是 | RN Android module 可调用或包裹它 | +| `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java` | native declarations | Java 到 JNI | 是 | payload 目前仅 `byte[]` | +| `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp` | `NativeMapView::submitRasterArraySourceHostField` | `byte[]` 拷贝进 C++、调用 Source | 是 | 返回 `jint` result;没有 callback | +| `platform/android/MapLibreAndroidTestApp/.../ParticleCoreLayerValidationActivity.kt` | `submitRealWeatherDataset` / `controlTimeline` | Test App asset 模拟 Host | 仅作参考 | 不是正式 RN 入口 | +| `src/mbgl/style/layers/raster_particle_layer.cpp` | RasterParticle properties | particle style property parser/model | 部分 | 无 arbitrary particle/trail color | +| `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` | `RenderRasterParticleLayer` | field upload、Timeline tick、GPU simulation/trail | 不直接 | 需保持 C++ ownership | +| `src/mbgl/style/layers/weather_color_layer.cpp` | WeatherColor property parser | 仅 opacity | 部分 | 无 dynamic speed stops | +| `src/mbgl/renderer/layers/render_weather_color_layer.cpp` | `RenderWeatherColorLayer` | 同一 Timeline frame 的色场绘制 | 不直接 | 重用 T0/T1 GPU content | +| `shaders/particle_gpu_validation.fragment.glsl` | `weatherSpeedColor` / `particleSpeedColor` | 当前固定色表/粒子 speed-color | 将来需扩展 | 色带硬编码在 shader | + +## C. 当前 Host Field Submission 路径 + +`NativeMapView::submitRasterArraySourceHostField(JNIEnv&, String, String, byte[])` 用 `jni::GetArrayRegion` 把 Java `byte[]` 复制到 `std::vector`,再调用 `RasterArraySource::submitHostField(metadataText, payload)`。 + +1. `LocalParticleFieldFeed::load()` 解析 provider-neutral metadata,并调用 `ParticleFieldDataAdapter::decode()`。 +2. Adapter 严格检查非空 fieldId、合法 generation/validTime、width/height、`float32-le` + `interleaved-uv`、精确 payload bytes;成功后生成拥有 `velocity` 与 `valid` 的 immutable Snapshot。 +3. 第一个合法 Field 成为 pending T0,结果为 `AcceptedPendingInitialWindow`。 +4. 第二个严格较新的 compatible Field 通过既有 `submitInitialRuntimeFieldWindow(T0,T1)`,建立 `RasterArrayTimelineRuntime`,结果为 `InitialWindowReady`。 +5. 后续 Field 按 producer `validTime` 追加同一个 bounded Timeline,结果为 `AcceptedNextSnapshot`。 +6. 相同 GPU content 返回 `Duplicate`;非法、非单调、capacity 等返回结构化负 result,最后有效 Field 保留(fail-closed)。 + +当前 513×769 单帧为 `513 × 769 × 2 × 4 = 3,155,976` bytes,约 3.01 MiB。Android Test App 路径至少有:asset 到 Java `ByteArray`、`ByteArray → vector`、`vector → Snapshot::velocity` 三段 materialization/copy;随后 renderer 还有 GPU image upload。它不适合转换成巨大 JSON 数组。 + +## D. Timeline 与时间插值 + +`RasterArrayTimelineRuntime`(`raster_array_source.hpp/.cpp`)持有按 timestamp 排序的 Snapshot entries、T0 index、mix、speed 与 state。`tick()` 使用 `steady_clock` wall elapsed 推进 mix;相邻有效 entries 决定 T0/T1,跨界时 `moveToLocked()` 递增 window revision 并记录 `OLD_T1_TO_NEW_T0`。 + +`RenderRasterParticleLayer::prepare/update` 读取 `timeline->frame()`,在 revision 变化时让 `RasterArraySource` 的 authoritative window 前进;renderer 用同一 GPU content 判定复用,因而正常 A/B→B/C 只上传 new T1,不 rebuild simulation/clear trail。`RenderWeatherColorLayer` 读取同一 frame/mix,在 shader 内先插值 U/V 再计算 speed。 + +Android 当前低频命令路径是:`ParticleCoreLayerValidationActivity::controlTimeline` → `MapLibreMap::controlRasterArraySourceTimeline` → `NativeMapView::nativeControlRasterArraySourceTimeline` → `RasterArraySource::controlRuntimeTimeline`。现有命令为 play/pause、0.5/1/2 speed、step、按 **index** seek、Gap/Invalid/Recover;没有 `setForecastTime(timestamp)` 的公开 Host API,也没有向 JS 返回 MIX/T0/T1 的 callback。 + +## E. WeatherColorLayer 当前颜色实现 + +`WeatherColorLayer` 当前唯一公开 paint property 是 `weather-color-opacity`(`weather_color_layer.cpp`)。`RenderWeatherColorLayer` 以 UBO mode 5 运行 `particle_gpu_validation.fragment.glsl::weatherSpeedColor(speed)`;该函数把 0–30 m/s 映射到硬编码蓝/青/红色,并在 shader 用 `smoothstep/mix` 插值。 + +结论:当前没有 dynamic stops/ramp 数据结构,也没有 Android/JNI setter。React dynamic color stops 最小新增点应是 WeatherColorLayer 的 style property/model(有序 `speedMps,color` contract)及紧凑 shader-visible ramp(uniform/texture),由 Native 插值、绘制;React 只提交 stops。不能把 provider 色表放入 Renderer,也不能继续把固定 preset 作为唯一 API。 + +## F. ParticleStyle 当前实现 + +已有 RasterParticleLayer paint entry:`raster-particle-active`、`mask`、`time-mix`、`particle-count`、`line-width`、`particle-length`、`particle-opacity`、`trail-opacity`、`trail-fade`、`speed-color-enabled`。Android 低频 `NativeMapView::setStyleLayerFloatProperty` 已转发上述大部分 float/bool property。 + +| 目标项 | 当前入口/行为 | 副作用 | +|---|---|---| +| `particleCount` | `raster-particle-count` | State/draw resources 必要重建;不应 field resubmit | +| `lineWidth`、`particleLength`、`particleOpacity`、`trailOpacity`、`trailFade` | RasterParticle paint property | visual/uniform 更新;当前路径不应 style reload、field upload 或 Timeline reset | +| `speedColor` | `raster-particle-speed-color-enabled` | shader 在固定 speed palette 与固定蓝色之间切换 | +| `particleColor` | 不存在 | 需新增明确 style property | +| `trailColor` | 不存在 | 当前 trail RGB 与 particle segment 路径耦合,需独立 style/shader contract | + +因此未来应将 `speedColor` 保留为可选模式;当它为 false 时,用 React 的 `particleColor`;`trailColor` 必须独立提交,不能让 WeatherColorLayer speed ramp 隐式控制粒子颜色。 + +## G. Runtime Enable / Disable / Release + +当前 generic Layer visibility 可以隐藏 layer;`RenderRasterParticleLayer::needsRepaint()` 仅在 `active && visibility != None` 时为真,故 particle hidden 时停止该层持续 repaint。它不等价于 release:layer/source 未 remove 时 State/Trail/field GPU resources 与 Snapshot/Timeline 仍被持有。Weather color 的 `needsRepaint()` 返回 `isLayerRenderable()`,visibility none 是否不再 render/repaint 由通用 Layer renderability 决定;本调查未用设备测试验证。 + +真实释放的现有路径是 layer removal:`RenderRasterParticleLayer::layerRemoved()` 移除 State/Trail targets 与 draw buffers;`RenderWeatherColorLayer::layerRemoved()` reset resources。source destruction 释放 `RasterArraySource` 的 timeline/pending Snapshot 共享引用;Activity 当前 `enabled=false` 是重载 validation style 并省略 layers,不是对 RN 暴露的一次性 runtime unload API。 + +`setWeatherEnabled(false)` 可实现真正释放,但当前没有一条 API 完整完成它。最小高层语义应在 map/style 安全线程中:停止低频 playback → remove Particle/WeatherColor layers(GPU release)→ clear timeline/pending host fields 或 remove RasterArraySource(Snapshot release)。需要新增 `clearWindFields()`/runtime lifecycle bridge;不能以 visibility 伪装 unload,也不应向 React 暴露 State/Trail/T0/T1。 + +## H. RN Binary Bridge 建议 + +当前没有 RN binary bridge。建议 v1 使用 `ArrayBuffer`/`Uint8Array` 在 RN Android module 侧稳定 materialize 为 `byte[]`,复用现有 `MapLibreMap.submitRasterArraySourceHostField(..., byte[])` 与 JNI;metadata 继续是小型 String/JSON。约 3.01 MiB/帧和四帧队列的峰值复制需要纳入内存预算,但在先验证正确性阶段不必承诺 zero-copy。 + +后续 profiling 若证明 JS→Java→JNI copies 是瓶颈,再增加 **新的** direct `ByteBuffer` JNI overload(或宿主 JSI ArrayBuffer native ownership seam)。不能假称现有 `byte[]` 通路可 zero-copy;也不应把文件/mmap 作为 RN v1 的必需前提。 + +## I. RN API 与 Native 能力映射 + +| RN 目标功能 | 当前 Native 能力 | 已有入口 | 缺口 / 建议接入点 | +|---|---|---|---| +| `setWeatherEnabled` | 层 removal/资源释放存在 | generic Style remove;layerRemoved | 新增高层 lifecycle/unload + clear fields,不用 visibility 代替 | +| `setWeatherColorVisible` | generic visibility | Style Layer visibility | 可包装;隐藏不释放 | +| `setParticleVisible` | generic visibility;active 控制 simulation | Style property | 可包装;明确 hide/suspend/unload 三语义 | +| `submitWindField` | 完整 Host canonical ingress | `submitRasterArraySourceHostField` | RN module/facade 与 binary adapter 缺失 | +| `clearWindFields` | 无 | 无 | Source clear/lifecycle seam | +| `setForecastTime` | 仅 index seek | timeline command 5 | 增 timestamp→entry selection API,JS 不设 T0/T1/MIX | +| `setPlaybackState` | play/pause | timeline commands 0/1 | 可包装 | +| `setPlaybackSpeed` | 0.5/1/2 | command 2 | 枚举/范围需正式 Host contract | +| `setWindColorStyle` | opacity only | WeatherColor paint property | 增 dynamic stops style model + shader ramp | +| `setParticleStyle` | 多数 float/bool 属性 | `setStyleLayerFloatProperty` | 增 particle/trail color typed properties;不要靠 speedColor | +| `setParticlePerformance` | 无 weather-local FPS gate | global `MapView#setMaximumFps` | 独立 render scheduling 阶段 | +| runtime/error callbacks | submit `jint` + logcat | JNI result | 增低频 structured event/callback/state query;不回调 per-frame MIX | + +## J. targetFps 调查 + +当前持续帧由 `RenderRasterParticleLayer::needsRepaint()`(active/visible)及 `RenderWeatherColorLayer::needsRepaint()`(renderable)驱动。`Map::Impl::onDidFinishRenderingFrame()` 在 `needsRepaint` 时再次 `onUpdate()`;Android frontend 最终调用 `MapRenderer::requestRender()`。因此接近 60 FPS 是 MapLibre continuous render request 链的结果,不是 Test App Timer。 + +现有 `MapView#setMaximumFps()` / `MapRenderer#setMaximumFps()` 是整张地图的 global cap,不应直接实现天气专属 targetFps。最安全的天气 30 FPS 节流点是 future `RenderRasterParticleLayer`/WeatherColor coordination 的 render scheduling gate:只在 deadline 到达时执行 State/Trail 与 dependent color redraw,使用累计 monotonic wall delta 驱动 lifecycle、trail fade 和 Timeline tick。这样不能把固定 1/30 秒塞给 shader;需保留真实 elapsed,并审查 simulation dt clamp/substep。WeatherColor 也必须参与 gate,否则它单独 `needsRepaint()` 仍保持全图约 60 FPS。建议该项独立于 RN Bridge v1,以免把 render scheduling 风险混入接口接线。 + +## K. 建议实施拆分与结论 + +**推荐下一实现阶段:STEP-057 Android Host Weather Runtime Bridge Foundation。** + +目标是完成可被未来 RN module 包装的 Android/Native typed facade,而不是在本仓库凭空建立 RN package(当前没有 RN dependency、package 或 host app topology)。建议一个较大 Batch: + +1. 低频 runtime lifecycle(enable/suspend/hide/unload)与 `clearWindFields`;安全 layer/source teardown。 +2. canonical `submitWindField(metadata, binary)`、timestamp seek、play/pause/speed 的 typed Android/JNI facade,复用 Source/Timeline。 +3. 可见性、现有 particle visual properties、WeatherColor opacity 的 typed API;增加 structured submission/runtime/error event,避免 per-frame callback。 +4. 定义并接入 dynamic WeatherColor stops 与独立 particle/trail colors 的 style property contract;所有 provider/network/forecast decision 仍留 JS。 +5. 以 Test App/Native tests 走完整 facade,最后一次 Android build/install 验收;真正 RN module 留给具备 RN host/build dependency 的应用仓库或后续明确任务。 + +**后续 Batch(STEP-058,建议独立):** weather-local `targetFps`、repaint gate、validation logging 降频与性能测量。不要把 `MapView#setMaximumFps()` 当作默认实现。 + +直接进入“RN package 实现”目前的阻塞不是 Core 数据 contract,而是仓库不存在 RN binding 与 RN Android build ownership。可立即开发的是上述 Host-native facade;等宿主提供 RN 依赖和 module placement 后,RN 层可薄包裹它。 + +最大三项风险: + +1. 3.01 MiB 级 payload 在 JS/Java/JNI/Snapshot 的多次 copy 造成峰值内存、GC 与提交抖动。 +2. visibility、suspend 与 unload 被混同,导致 Native/GPU/Snapshot 资源未真正释放,或在 style/render thread 上错误 teardown。 +3. Timeline tick、particle State pass 与持续 repaint 当前耦合;错误 targetFps gate 会改变 motion、wall-time lifecycle 或 Timeline 行为。 + +冻结边界继续有效:Descriptor/Snapshot、T0/T1 window/reuse、Core-owned Timeline/Playback、canonical m/s U/V、WeatherColor 与 RasterParticle 分离、Renderer 不含 provider/network/vendor 逻辑。 diff --git a/docs/20_调查/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField现状调查_v1.md b/docs/20_调查/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField现状调查_v1.md new file mode 100644 index 000000000..261418e24 --- /dev/null +++ b/docs/20_调查/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField现状调查_v1.md @@ -0,0 +1,44 @@ +# STEP-065 MapLibreParticleLab V3 通用 Scalar / Vector Field 现状调查 + +状态:完成(只读调查) +基线:`particle/main` / `2e3ab26275a8d1873d764cf6927783a7b0406836`,`origin/particle/main` 同一提交。 + +## 调查范围与方法 + +已完整阅读项目基线、协作/构建/Step/架构/官方基线规范、STEP-064 任务与冻结记录/报告;只读核对 Particle Core、两 renderer、GLSL、RasterArray Timeline、Android Host/JNI、RN package、两个 RN Test Host,以及 `/root/sourceserver/weatherV2` 指定六文件。未修改 WeatherV2/NavSea/生产代码,未执行 Gradle、APK、ADB 或 Jest。 + +## A. 当前 Field Contract 与 Wind-only 点 + +`include/mbgl/particle/field/field_data_adapter.hpp` 的 `ParticleFieldDescriptor` 只有一个 `layout/encoding/byteLength/stride`,`ParticleFieldSnapshot` 只有 `velocity: vector` 与 `valid: vector`。`field_data_adapter.cpp` 只接受 `interleaved-uv` + `float32-le`、每 cell 两 float;`isValid()` 与 decode 末尾硬性要求 `fieldType == "WIND"`、`displayDomain == "GLOBAL"`。`local_particle_field_feed.cpp` 的 `maplibre-particle-field/v1` parser 也强制 `vector` 与一个 `payload`。 + +`fieldType` / `displayDomain` 还参与 timeline-compatible 比较;当前 fixture、Consumer metadata 皆为 `WIND/GLOBAL`。这不是 provider-neutral 的能力表达,且不能承载 scalar-only 或 Wave 的 direction vector。 + +## B. GPU、validity、shader 的真实结构 + +`render_weather_color_layer.cpp` 每快照把 U/V 按快照最大绝对值编码进 RG、validity 编入 A;T0/T1 各一张 `Texture2D`。`render_particle_validation_layer.cpp`(正式 `RasterParticleLayer` 也经此 renderer)同样把 U/V+validity 压入 RGBA,维护 `fieldTexture/nextFieldTexture`、各自 scale 和 image。二者独立拥有资源,不共享 GPU texture;均以 `sameFieldGpuContent()` 比较并可把 OLD_T1 移至 NEW_T0,仅上传新内容。 + +`shaders/particle_gpu_validation.fragment.glsl` 的 Field pass 从 RG 恢复 U/V,A 同时是 shared validity;`weatherRampColor(length(velocity))` 是图场唯一值来源。无独立 scalar texture、mask texture 或 channel-level validity;粒子另有验证用 coast mask,但不属于 field validity。 + +## C. Particle 数值与单位假设 + +GLSL `fieldVelocityAt()` 混合 T0/T1 的 U/V,要求两时刻 A 有效且 `length > 1e-5`。状态 pass 用 `dx=u*simulationSeconds/EarthCircumference`、`dy=-v*simulationSeconds/(EarthCircumference*cos(latitude))`,明确 U east、V north、物理 m/s。C++ 的 `VisualSimulationSecondsPerRealSecond=1200` 只是展示时钟;不会改变 canonical U/V 单位。故现状不适合直接把 Wave unit direction 当 velocity。 + +## D. RN 到 Core binary ingress 与 ownership + +公共 `FieldRuntime.submitField(metadataJson, payloadFileUri)` 在 RN 薄映射内部 `submitWindField`。Kotlin bridge 只接受 `file://` 或绝对本地路径,metadata 上限 1 MiB、payload 1..64 MiB,读为 `ByteArray`;无 number[]/JSON array/Base64。`WeatherRuntime.submitWindField()` 传给 `MapLibreMap.submitRasterArraySourceHostField()`,JNI `NativeMapView::submitRasterArraySourceHostField` 把 Java `byte[]` 复制为 C++ `vector`,再调用 `RasterArraySource::submitHostField()`;Core adapter 再复制解码为 float vector。因此当前 ownership 是同步、三次宿主/核心内存持有,而非 zero-copy。 + +## Timeline / Runtime Window + +`RasterArraySource::submitHostField()` 首帧 pending、第二帧原子建立 Source-owned `RasterArrayTimelineRuntime` T0/T1;后续按 `validTime/generation` 单调 append,容量 8。`RasterArrayTimelineRuntime` 输出 immutable T0/T1 + MIX,暂停不推进。`RenderWeatherColorLayer` 与 `RenderRasterParticleLayer` 都取同一 Source timeline;正常 overlap rollover 保持 `simulationRebuild=false`、`trailCleared=false`,无 overlap 才 reset。invalid/gap 由 timeline 状态 fail-closed。 + +## Field Renderer 与 speedMps 链路 + +`WeatherColorStop.speedMps` 位于 C++ `weather_color_stop.hpp`,Android `WeatherRuntime.ColorStop`、Kotlin native bridge 和 generated TurboModule 均仍使用该内部 ABI;RN `.2` 已将公开 `FieldColorStop.value` 薄转换为 `speedMps`。当前 shader 默认 0..30 m/s palette,stop 值也按 speed 解释。因此 V3 必须新增明确 value source,不能以有无 scalar 猜测。 + +## WeatherV2 输入核对 + +`nsft.py` / `field_pmtiles.py` 证实 NSFT PMTiles `wm_grid_257`、NW origin(row north→south、column west→east)、同 zoom tile 257 样本。Wind 与 current 都写物理 U/V;`wind.py`、`tide.py` 计算 speed 为 `sqrt(u²+v²)`。Rain 写 `rain_mm_h + valid`;虽 decoder 有 `precipitation_type`,`encode_rain_tile()` 未写它。Wave 写 `height_m`、`period_s` 和 `unit_to_u/v`;`waves.py` 将 from 转 to 后归一化,非 m/s。`target_time` 由实际任务时间写入 header,不能推定小时 cadence。任务书给出的事实与代码一致。 + +## 直接结论 + +V3 应保留现有 Source-owned timeline、两个独立 renderer 和 overlap reuse 的行为,但将单个 velocity snapshot 升级为一个快照的 optional scalar、optional vector、shared validity 三资源。业务名称、单位换算、NSFT/PMTiles、tile 拼接、provider/缓存必须停在 NavSea 侧。 diff --git a/docs/20_调查/STEP-077_QBIN2_V4_Android_JNI_RN桥接调查报告_v1.md b/docs/20_调查/STEP-077_QBIN2_V4_Android_JNI_RN桥接调查报告_v1.md new file mode 100644 index 000000000..fd311b8bf --- /dev/null +++ b/docs/20_调查/STEP-077_QBIN2_V4_Android_JNI_RN桥接调查报告_v1.md @@ -0,0 +1,117 @@ +# STEP-077 QBIN2 V4 Android / JNI / RN 桥接调查报告 v1 + +状态:完成(只读调查) + +日期:2026-09-13 +基线:`0759a06104e1fb30f036db2efb6ef34ca5859759`(`particle/main` = `origin/particle/main` 于调查开始时) + +## 结论 + +V4 应新增一条与 V3 完全平行的、低频的 `WeatherRuntime` / `MapLibreMap` / `NativeMapView` 通道;不得接到现有 `submitField`、`clearFields`、`setForecastTime`、`setPlaybackState` 或 `setPlaybackSpeed`。这些现有命令分别写入 V3 Host field、`clearHostFields()`、`seekRuntimeTimelineToTimestamp()` 和 `controlRuntimeTimeline()`,会操作 V3 timeline,不能承载 V4 的 `EncodedFieldStore` / `ForecastClock`。 + +推荐 RN 只传绝对本地 `qbinFileUri`(或 `file://` URI)及小型、typed primitive ABI 元数据;Kotlin 有界读取为 Java `byte[]`,JNI 复制为 C++ `std::vector`,再调用 `RasterArraySource::submitEncodedField(...)`。不传 Base64,不由 JS 构造 `number[]` / 大型 `ReadableArray`。V4 Core 独占每个 field 的 T0/T1/MIX 选择;桥接只传 requested time、play/pause 与速度,绝不传 endpoint、mix 或 renderer snapshot。 + +## 实际当前链路 + +```text +FieldRuntime.ts public facade + -> NativeWeatherRuntimeBridge.ts TurboModule Spec (primitive arguments / Promise) + -> WeatherRuntimeBridgeModule.kt + runOnUiQueueThread -> Fabric reactTag + UIManagerHelper(FABRIC).resolveView -> MLRNMapView.mapLibreMap + local file bounded read -> WeatherRuntime + -> MapLibreMap (map-owned WeatherRuntime facade) + -> NativeMapView Java (checkState) -> registered NativeMapView JNI + -> native_map_view.cpp: style.getSource(id), dynamic_cast + -> RasterArraySource V3 Host API OR isolated V4 Host API +``` + +源码事实如下。 + +- `FieldRuntime.submitField` 仍是 V3 public API,封装 `FieldPayloadRefs` 为 validity/scalar/vector 三个 URI;`submitLegacyVectorField` 保留 v1 兼容入口。`NativeWeatherRuntimeBridge.ts` 的 codegen Spec 已刻意使用 primitive 参数,空 string 仅表达 V3 optional plane。 +- Kotlin 对 V3 已验证本地绝对路径或 `file://`、单文件和 aggregate 64 MiB、读中长度变化;再形成 `ByteArray`。它不保存 reactTag、map 或 timeline,而是每次在 UI queue 解析当前 Fabric view。 +- `WeatherRuntime.submitField` 把 V3 bytes 重新 pack 后调用 `submitRasterArraySourceHostFieldV2`;`NativeMapView` 用 `GetArrayRegion` 复制到 C++,然后拆 planes 并调用 `submitHostField`。这是可复用的“file -> Java byte[] -> JNI copy -> vector” seam,而不是可复用的 V4 API。 +- V4 `RasterArraySource` 已有独立 `EncodedFieldStore`、`ForecastClock`、binding intent 和 `submitEncodedField` / `set*Binding` / `setRequestedForecastTime` / `setForecastPlayback*` / `clear*EncodedFields`;注释和 STEP-076 public-path 测试都证明它不写 V3 T0/T1/MIX 或 renderer snapshot。 + +## QBIN 传输形态 + +| 方案 | 当前支持与成本 | 结论 | +| --- | --- | --- | +| A:RN `qbinFileUri`,Kotlin 读取 | 已有 `readLocalPayload` / `resolveLocalFile` 精确先例;仅接受本地文件、1..64 MiB,16 KiB buffer,有 read-time size guard;Java/JNI 仍各有一次受控 copy | 推荐。新增 V4 方法复用读取 helper,但不复用 V3 submit/timeline API。 | +| B:RN `byte[]` / `number[]` | 当前 TurboModule Spec 无 binary array ABI;`ReadableArray` 是现有 color stops 的小对象集合,不适合 QBIN。JS number array 造成 JS/native 转换、对象/number 开销与额外 copy;Base64 更扩大内容且无现有 ABI | 拒绝。不得为了方便绕过 64 MiB file guard。 | +| C:直接复用 Java/JNI `byte[]` seam | Kotlin -> `WeatherRuntime` -> Map -> `NativeMapView` 的 `byte[]` 和 `GetArrayRegion` 已存在,适合作为 A 的下半段 | 复用实现模式;新增命名为 V4-specific JNI 方法,不复用 V3 方法。 | + +64 MiB 是现有 Kotlin `MAX_PAYLOAD_BYTES` 与 Java `MAX_FIELD_PAYLOAD_BYTES` 的边界;V4 要在 Kotlin、Java、JNI 均检查 raw QBIN 长度 `1..64 MiB`,随后 Core Decoder 继续执行其自身 QBIN 全文件/decoded-raw 限制。该限制不授权桥接解析 QBIN。 + +## 推荐 V4 API 与跨层 ABI + +所有 String 以 UTF-8 byte length 做小型上限校验;`reactTag`、generation、dataVersion、time 使用 JS 安全整数检查(当前 bridge 对 timestamp 已采用 finite/nonnegative/integral guard)。枚举使用固定 string;所有 enum numeric 值只在 Kotlin/Java/C++ 边界转换。 + +| RN public / Turbo Spec | Kotlin / Java facade | JNI / Core | 参数形态及责任 | +| --- | --- | --- | --- | +| `submitEncodedField(tag, fieldKey, fieldID, generation, dataVersion, contractJson, qbinFileUri)` | `submitEncodedField(...)` | `submitRasterArraySourceEncodedField(sourceId, fieldKey, fieldID, long generation, long dataVersion, contractJson, byte[] raw)` | URI 为文件;metadata 用 primitive;contractJson 是唯一结构化小协议,Kotlin/Java 各只作长度、空值、range;JNI 解析为 Core structs 并调用 V4 API。 | +| `setColorBinding(tag, fieldKey, semanticID, groupID, expectedValueClass, expectedUnitID)` | 同名 | `setRasterArraySourceEncodedColorBinding(...)` | 完全 primitive;JNI 构造 `ColorBinding`,Core resolver 仍做最终 plane assertion。 | +| `setParticleBinding(tag, mode, fieldKey, firstSemanticID, firstGroupID, secondSemanticID, secondGroupID, expectedUnitID)` | 同名 | `setRasterArraySourceEncodedParticleBinding(...)` | mode-dependent selector 仍占固定 primitive slots;Direction 使用 first selector,second 传 0/0。Kotlin/Java 检查 mode 和 uint16 range,不引入 `ReadableMap`。 | +| `setRequestedForecastTime(tag, timestampMs)` | `setEncodedForecastTime` | `setRasterArraySourceEncodedForecastTime` | 独立 V4 名称,`long`;Core ForecastClock 选择各 field window/mix。 | +| `setForecastPlaybackState(tag, "PLAY"|"PAUSE")` | `setEncodedForecastPlaybackState` | `setRasterArraySourceEncodedForecastPlaybackState` | V4 enum,绝不调用 V3 `controlRasterArraySourceTimeline`。 | +| `setForecastPlaybackSpeed(tag, "HALF"|"NORMAL"|"DOUBLE")` | `setEncodedForecastPlaybackSpeed` | `setRasterArraySourceEncodedForecastPlaybackSpeed` | Kotlin/Java 映射为 0.5/1/2;Core 保留 speed validity。 | +| `clearEncodedField(tag, fieldKey)` / `clearAllEncodedFields(tag)` | 同名 | 同名 V4-only JNI | 不清 V3 Host fields、不改变 V3 cadence/timeline。single-clear 返回 `CLEARED` / `FIELD_NOT_FOUND`;all-clear 返回 accepted command。 | + +`ResolvedFieldProfileContract` 需要 `contractSchemaVersion, profileID, profileVersion, allowAdditionalPlanes, sharedValidityRequired, timeRelation, requiredPlanes[]`,每个 plane 为 `semanticID, valueClass, unitID, groupID, component, requiredFlag`。它是唯一确实需要 array 的稳定数据;当前 TurboModule codegen 对 public object ABI 已有历史约束,故不建议 `ReadableMap/ReadableArray`,也不应将每个平面拆成无边界的 JS 数组。推荐在 RN/Spec 中传受大小限制的 `contractJson`,在 JNI 的单一 parser 转为 C++ contract;Kotlin/Java 不语义解析。这样 Host/SDK 负责已经 resolved 的 profile contract,Native 不知道 provider/profile 业务;Core `FieldProfileValidator` 仍是 structural/semantic 唯一裁决者。 + +这不是让 JSON 承载 QBIN 或 server manifest:`contractJson` 是受 schema 版本约束的小 control-plane ABI。若 RN codegen 后续稳定支持 readonly typed object + typed array,才可在破坏性 ABI 版本中替换;本 Step 不建议 `ReadableMap`,因为它绕过 generated Spec、会把多层 field parsing 复制进 Kotlin。 + +## 兼容与所有权策略 + +- V3 `FieldRuntime.submitField`、`submitLegacyVectorField` 与现有 `FieldSubmitResult` 保留不变;V4 新增 `submitEncodedField` 与新的 `EncodedFieldSubmitResult` union,不能改名覆盖。 +- `WeatherRuntime.setForecastTime`、`setPlaybackState`、`setPlaybackSpeed` 必须不复用:它们当前调用 V3 `seekRasterArraySourceTimelineToTimestamp` / `controlRasterArraySourceTimeline`。V4 用 `setEncoded...` Java/JNI names;RN public 名可按任务书使用 `setRequestedForecastTime` 等,但需明确是 V4 namespace / facade(例如 `EncodedFieldRuntime`),避免误导。 +- `clearFields` 和 `clearEncodedFields` 必须分离。前者还参与 V3 state/cadence/disable 行为并调用 `clearHostFields()`;后者只清 `EncodedFieldStore`,保留 V4 bindings,正如 STEP-076 证明,且不得触及 V3。 +- `setColorBinding` / `setParticleBinding` 不修改 WeatherColor/RasterParticle renderer properties;它们只存 V4 binding intent。实际 renderer 接线不在本 Step 范围。 + +## 错误模型 + +Promise rejection 仅用于桥接前条件、生命周期或无法执行;已进入 Core 的 `RasterArrayEncodedFieldSubmitResult` 必须 resolve 成可区分的 V4 result string,不能折叠为 `UNKNOWN` 或 `NATIVE_REJECTED`。 + +| Core result | 推荐 RN resolved result | +| --- | --- | +| `AcceptedInserted` | `ACCEPTED_INSERTED` | +| `AcceptedReplacedRevision` | `ACCEPTED_REPLACED_REVISION` | +| `Duplicate` | `DUPLICATE` | +| `RejectedDecode` | `REJECTED_DECODE` | +| `RejectedContract` | `REJECTED_CONTRACT` | +| `RejectedInvalidFieldKey` | `REJECTED_INVALID_FIELD_KEY` | +| `RejectedIdentityConflict` | `REJECTED_IDENTITY_CONFLICT` | +| `RejectedStaleRevision` | `REJECTED_STALE_REVISION` | +| `RejectedIncompatible` | `REJECTED_INCOMPATIBLE` | +| `RejectedCapacity` | `REJECTED_CAPACITY` | + +桥接层 rejection code 应为:`INVALID_ARGUMENT`(tag、enum、整数、UTF-8/control size)、`PAYLOAD_FILE_UNREADABLE`(空/越限/非本地/不可读/读中变更)、`MAP_VIEW_NOT_FOUND`、`MAP_NOT_READY`、`STYLE_NOT_READY`、`WEATHER_OBJECTS_NOT_READY`、`RUNTIME_DISPOSED`,以及 V4 source 获取失败时的 `SOURCE_MISSING`。JNI 在 Source 不存在或类型不符时不得伪装为 Core `RejectedDecode`;应返回明确 native bridge status,再由 Java 映射为 `SOURCE_MISSING`。style 已存在但 source 尚未 ready 与 runtime disposed 同样不能混成 submit result。 + +## 生命周期与线程 + +`WeatherRuntime` 被 `MapLibreMap` 私有拥有,且是 style-scoped;`replaceWeatherRuntime` 先 dispose,`setStyle` 也 dispose,Kotlin 的 MapView detach listener 调用 `map.disposeWeatherRuntime()`。每个 bridge call 在 `runOnUiQueueThread` 通过 Fabric `reactTag -> UIManagerHelper -> MLRNMapView -> mapLibreMap` 当场解析;不允许新增 map registry、缓存 tag、长期保存 `MapLibreMap*` 或 native source pointer。 + +未来 V4 command 应沿用 `withRuntime`,在 Android UI 主线程上从当前 style 查 source。`NativeMapView.checkState` 是 Java 层 native lifetime guard;C++ 必须每次 `style.getSource(sourceId)` 和 `dynamic_cast`,不可缓存指针。style reload/detach/dispose 后,后续命令得到上述 lifecycle error;旧 V4 state 随 Source/style 销毁失效,Host 要在重新 attach、style ready 后按自己的缓存重新提交 raw QBIN 与 binding intent。 + +## 可用验证与后续验收 + +本调查没有执行 Android 编译、APK、ADB 或实体机操作。当前 Linux 可用入口与边界: + +| 层 | 后续可验证项 | 边界 | +| --- | --- | --- | +| TS | `npx tsc -p packages/maplibre-particle-rn/tsconfig.json --noEmit`;Test Host `npm run typecheck` | package 未定义独立 typecheck script;依赖由 Test Host node_modules/paths 提供。 | +| TurboModule | RN codegen 由 package `codegenConfig`(`WeatherRuntimeBridgeSpec`、`src`、Kotlin package)与 Android Gradle/RN integration 驱动 | 没有发现完全脱离 Gradle 的仓库级 codegen 命令;实现 Step 应记录实际 generation task。 | +| C++/JNI | 头文件/`native_map_view.cpp` 的精确 compile 与 Linux Core unit tests;JNI registration list 与 Java native declarations 静态一一审计 | 精确 Android ABI/JNI compile 仍需 Android NDK Gradle/native build;Linux Core build 不证明 Android linkage。 | +| Java/Kotlin | `WeatherRuntimeStateTest` 是 Mockito/JVM facade 状态测试,可新增 V4 mapping/隔离测试 | 它依赖 Android Gradle test classpath;未发现无需 Gradle 的现成 Kotlin/Java standalone validation。 | + +建议实施拆分: + +1. 先冻结 RN Spec/types 与 Kotlin file/control validation,新增 V4 result union,并以 typecheck + codegen 审计验收。 +2. 再新增 Java `WeatherRuntime` V4-only facade、`MapLibreMap` / `NativeMap` / `NativeMapView` Java declarations、C++ JNI registration 与 source lookup;JVM Mockito 覆盖 V3/V4 time-clear 不串线和 error mapping。 +3. 最后才接 renderer consumer(独立 Step),以 C++ Source tests 证明 V4 projection、style reload/detach rejection、JNI exact build、Android Debug OpenGL/Vulkan 与离线 fixture 验收。不得下载真实 QBIN。 + +## 实施文件清单与风险 + +预期修改:`packages/maplibre-particle-rn/src/{types.ts,NativeWeatherRuntimeBridge.ts,FieldRuntime.ts}`、`WeatherRuntimeBridgeModule.kt`、`WeatherRuntime.java`、`MapLibreMap.java`、`NativeMap.java`、`NativeMapView.java`、`native_map_view.hpp/.cpp`,以及相应 JVM/C++ tests。不会改 package version、Test Host、Renderer/GPU。 + +主要风险为 codegen 对 complex object/array 的限制、64 MiB 下 Kotlin/Java/JNI 三份瞬时内存、JS number 精度(所有 uint64 必须限制在 safe integer 或改 decimal string ABI)、source/style 生命周期竞争、以及错接旧 V3 timeline。前四项通过 primitive+small-contract JSON、固定 64 MiB guards、UI queue late lookup 和 typed result 处理;最后一项通过 V4-only 方法命名、独立 NativeMap API 和隔离测试消除。 diff --git a/docs/20_调查/STEP-086_Particle连续渲染唤醒MapLibre整图成本调查_v1.md b/docs/20_调查/STEP-086_Particle连续渲染唤醒MapLibre整图成本调查_v1.md new file mode 100644 index 000000000..b014bf92f --- /dev/null +++ b/docs/20_调查/STEP-086_Particle连续渲染唤醒MapLibre整图成本调查_v1.md @@ -0,0 +1,370 @@ +# STEP-086 Particle 连续渲染唤醒 MapLibre 整图成本调查 v1 + +状态:`INVESTIGATION / SOURCE_EVIDENCE_CONFIRMED / RUNTIME_PROFILE_PENDING` +日期:2026-09-21 + +## 1. 调查目标 + +本调查针对 NavSea 完整场景中出现的以下现象: + +- Particle 独立 Host / NavSea 最小 Particle Host 中,Particle 本体性能基本正常。 +- 完整 NavSea 在 Particle OFF、camera 静止时能够正常 idle。 +- 完整 NavSea 在 Particle ON 后,RenderThread、JS、main CPU 明显高于最小 Particle Host。 +- 已排除 NavSea 侧两个已确认的 JS 问题:NavigationEngine renderPulse 导致整树重渲染,以及 JsSchedulerProbe 常驻 RAF/16ms timer。 + +核心问题是验证: + +> Particle 连续动画是否通过 MapLibre 的全局 repaint/update/render 生命周期,把本来可 idle 的 PBF/vector/style/map scene 持续唤醒。 + +本调查优先检查 Shared Core / Renderer / Android frame scheduling,不修改 NavSea ↔ Particle C-001 公共责任边界。 + +## 2. 调查基线 + +Integration 仓库已同步最新 main: +- repo:`/root/sourceserver/navsea-particle-integration` +- branch:`main` +- HEAD:`39ad8415fbdb76c1822460bb09f3a3f35fe141e0` +- Contract:`C-001` +Particle 源码调查基线: +- repo:`/mnt/sda1/MapLibreParticleLab` +- branch:`origin/particle/main` +- HEAD:`70f112ea27acaaa4da0a307563b7868937f69691` +- commit:`fix: close STEP-085 V4 renderer efficiency` +- 当前该 HEAD 中 package/version 字段仍为 `13.4.1-particle.7`。 + +注意:用户当前运行环境称为 `.8`,而本次可直接审计的最新远端源码 HEAD 仍显示 `.7` 版本字段。 +因此本文的源码结论针对 `70f112ea2` 的 MapLibre/Particle 调度架构;后续如 `.8` 对应精确发布提交不同,应再做一次 commit-level 对照。 +不过本文确认的全局 repaint/render 机制位于 MapLibre Renderer 主链,不依赖 NavSea 业务代码。 + +## 3. 当前实测背景 + +用户提供的 Android 模拟环境参考量级: + +### 最小 Particle Host +- RenderThread:约 38–40% +- JS `mqt_v_js`:约 4–5% +- main:约 11% + +### 完整 NavSea,Particle OFF +- JS:7.08% +- RenderThread:1.31% +- main:3.34% + +说明完整 NavSea 的静止地图、PBF、UI 和业务逻辑本身能够进入低成本 idle。 + +### 完整 NavSea,Particle ON,且已排除两个 NavSea JS 问题 +- JS:29.07% +- RenderThread:80.13% +- main:15.51% + +相对于最小 Particle Host,大约额外: +- JS:+24 percentage points +- RenderThread:+40 percentage points +- main:+4~5 percentage points +## 4. 源码确认的核心结论 + +核心假设成立: + +> 当前 Particle 没有独立的“只绘制 Particle Layer”的动画循环。Particle 的连续动画通过 MapLibre 全局 `needsRepaint` / `onUpdate` / `Renderer::render` 链维持,因此一次合法的 Particle animation frame 会进入完整 MapLibre frame orchestration。 + +主链如下: + +```text +RenderRasterParticleLayer::needsRepaint() + ↓ +RenderOrchestrator::hasTransitions() + ↓ +RenderTreeParameters::needsRepaint = true + ↓ +Renderer::Impl::render() 完成当前整帧 + ↓ +Map::Impl::onDidFinishRenderingFrame() + ↓ +if (needsRepaint) onUpdate() + ↓ +重新构造完整 UpdateParameters + ↓ +AndroidRendererFrontend::update() + ↓ +MapRenderer.requestRender() + ↓ +Renderer::render() + ↓ +RenderOrchestrator::createRenderTree() + ↓ +完整 Map frame +``` + +因此 `needsRepaint()` 的语义不是“仅让 Particle Layer 再绘制一次”,而是“MapLibre 需要下一张完整地图 frame”。 + +## 5. Particle 每帧如何请求下一帧 + +`RenderRasterParticleLayer::needsRepaint()` 在 Particle active、visible、field-ready 且 cadence 到期时返回 true。 + +`RenderOrchestrator::hasTransitions()` 会遍历全部 RenderLayer: +- `hasTransition()` +- `needsRepaint()` + +任一 layer 返回 true,整个 RenderTree 即被标记为 `needsRepaint`。 +帧结束后,`Map::Impl::onDidFinishRenderingFrame()` 在 Continuous 模式下执行: + +```text +if (needsRepaint || transform.inTransition()) { + onUpdate(); +} +``` + +而 `Map::triggerRepaint()` 本身也只是: +```text +impl->onUpdate(); +``` + +Android 显式 `FPS_30/FPS_60` 还存在一层 platform wake: +- FPS_30:Handler 约每 34ms 调用一次 `map.triggerRepaint()` +- FPS_60:Handler 约每 17ms 调用一次 `map.triggerRepaint()` +- AUTO:不使用该 Handler,但 Particle 的 `needsRepaint()` 仍可使连续 frame 自行串联。 + +结论:当前没有 layer-scoped repaint 调度语义。 + +## 6. 一次 Particle repaint 会重新进入哪些 MapLibre 工作 + +`Map::Impl::onUpdate()` 每次都会重新生成完整 `UpdateParameters`,其中包括: +- transform state +- style loaded state +- light +- images +- sources +- layers +- glyph/font/sprite 状态 +- annotation manager +- tile LOD 参数等 + +随后 `Renderer::render(updateParameters)` 会重新调用: +- `RenderOrchestrator::createRenderTree()` +- `renderTree->prepare()` +- `Renderer::Impl::render()` + +因此不是复用一张静态 RenderTree 后只执行 Particle pass。 + +## 7. camera 静止时仍重复执行的非 Particle 工作 +在当前源码中,即使 camera 完全静止,连续 frame 仍会重复: + +1. 遍历全部 source。 +2. 对每个 source 遍历 ordered layers,计算 source/layer 关系、visibility、zoom fit、renderability。 +3. 对启用 source 调用 `source->update()`。 +4. Vector/PBF source 进入 `RenderTileSetSource::update()` → `TilePyramid::update()`。 +5. TilePyramid 会重新处理 wrap、zoom、covering tiles、retain 等逻辑。 +6. 对全部 enabled source 执行 `prepare()`。 +7. 对全部 renderable layer 执行 `prepare()`。 +8. symbol layer 进入 cross-tile symbol bookkeeping。 +9. 根据 placement freshness 条件周期性重新执行 placement。 +10. Renderer upload pass 遍历 source/layer RenderItem。 +11. `updateLayers()`、layer-group tweaker、layer-group upload 继续执行。 +12. opaque pass 重新遍历/绘制普通 layer groups。 +13. translucent pass 重新遍历/绘制普通 layer groups。 +14. 重新 present 整个默认 framebuffer。 + +因此“大量 PBF/vector/fill/line/symbol 在静止 camera 下由 Particle frame 唤醒”从源码结构上成立。 + +## 8. 需要精确区分:不是所有 Style 工作都每帧完整重算 + +当前代码并非每帧无条件重新 evaluate 所有 style property。 + +普通 RenderLayer 的 `evaluate()` 通常受以下条件约束: +- layer added/changed +- zoom changed 且该 layer 依赖 zoom +- crossfade +- active transition + +所以静止 camera、style 不变时,很多 property evaluation 可以跳过。 + +同样,完整 symbol placement 也不是严格 60 次/秒。 +`PlacementController::placementIsRecent()` 使用 placement update period 控制重新 placement;默认逻辑与约 300ms 的 transition/update period 相关。 +但这些优化不改变主结论: + +> 即使 property evaluation 和完整 placement 并非每帧执行,整张地图仍持续进入 source/layer traversal、source update/prepare、layer prepare、command encoding、普通 layer draw 与 framebuffer present。 + +这足以解释复杂 Style 与简单 Test Host 之间的大幅 RenderThread 差异。 + +## 9. Particle 是否每帧制造不必要的全局状态变化 + +本次静态审计没有发现 Particle 每个 simulation frame 都在修改以下全局状态的直接证据: +- camera state +- 普通 vector/PBF source data +- 全局 Style revision +- 普通 layer property +- sprite/image revision + +STEP-085 已针对 Particle-local 的不必要成本做过修复,包括: +- V4 WeatherColor 静态状态不再因为没有 legacy timeline 而永久 `needsRepaint()`。 +- V4 Particle 增加 Source-owned renderer cadence。 +- V4 snapshot 改用 immutable shared pointer,减少稳定 window 的大数组深复制。 +- WeatherColor 尽量复用 drawable,仅 wrap 变化时重建。 +- 增加 renderer debug counters。 + +所以当前问题与 STEP-085 并不矛盾。 + +STEP-085 解决的是: +```text +Particle 自己是否产生了多余 repaint / copy / drawable rebuild +``` + +本调查发现的是更外层问题: +```text +即使是“合法的 Particle frame”,当前仍等价于一次完整 MapLibre frame +``` + +## 10. 为什么简单 Particle Host 不能证明完整 NavSea 成本正常 +简单 Host 的 Style 很小,因此: +```text +Particle simulation/render ++ 少量普通 MapLibre layer 成本 +≈ 当前约 40% RenderThread +``` + +完整 NavSea Style 包含大量: +- PBF/vector source +- fill +- line +- symbol +- text/icon placement +- navigation marks +- 其他 MapLibre layers + +当 Particle 让 MapLibre 持续出帧后: +```text +Particle 自身成本 ++ 大型静态 map scene 的重复 orchestration / prepare / draw +≈ 当前约 80% RenderThread +``` + +因此必须使用复杂真实 Style 做性能判断,不能只使用 Particle Test Host。 + +## 11. JsSchedulerProbe 联动现象的当前解释边界 + +用户实测: +- Particle OFF:Probe OFF 7.08%,Probe ON 8.93%,约 +1.85pp JS。 +- Particle ON:Probe OFF 约 27~29%,Probe ON 96.47%,约 +69pp JS。 + +当前 Native 源码审计没有发现 Particle 每帧主动通过 RN bridge 回调 JS 的证据。 + +更合理的当前解释是: +- Particle 让 MapLibre/RenderThread 持续处于 frame pressure。 +- FPS_30/60 时 Android Main Looper 还有 cadence wake。 +- RN RAF / Choreographer / timer / JS scheduler 在不再 idle 的系统中发生严重放大或竞争。 + +但这部分尚不能仅靠 Native 静态源码判定根因。 +需要 RN scheduler trace / perfetto 才能进一步归因。 +由于 JsSchedulerProbe 将退出正常运行路径,它不应阻塞当前 Native Renderer 主问题。 +## 12. 下一阶段必须做的运行时 Profile + +需要固定 camera,并至少构造以下矩阵: + +| 场景 | Style | Particle | +|---|---|---| +| A0 | 简单 Style | OFF | +| A1 | 简单 Style | ON | +| B0 | 大量 PBF/vector/symbol/line/fill | OFF | +| B1 | 与 B0 完全相同 | ON | + +B1 还应分别验证: +- AUTO +- 60fps +- 30fps + +建议记录以下 Native 阶段的调用次数与 CPU 时间: +- `Map::Impl::onUpdate` +- `RenderOrchestrator::createRenderTree` +- source × layer traversal +- VectorSource / TilePyramid update +- source prepare +- layer prepare +- cross-tile symbol bookkeeping +- full placement +- `updateLayers` +- layer-group tweaker +- upload pass +- opaque pass +- translucent pass +- present +- Particle simulation advance +- Particle render/composite + +同时记录 MapLibre 已有 `gfx::RenderingStats`: +- encodingTime +- renderingTime +- numDrawCalls +- texture/buffer/uniform updates + +并结合 STEP-085 的 Particle debug counters,区分 Particle 自身和非 Particle 成本。 +预期验证特征: + +```text +B0 + stable camera +→ frame/update 很快降到接近 idle + +B1 + stable camera +→ onUpdate/createRenderTree 与 Particle frame 数量高度相关 +→ Vector/PBF source update/prepare 被持续调用 +→ 普通非 Particle draw calls 持续存在 +→ B1 non-Particle CPU 显著高于 A1 +``` + +如果该特征成立,则“continuous repaint 唤醒完整 scene”的运行时证据闭环完成。 + +## 13. 优化方向分层 + +### 第一层:低风险缓解 +优先验证 30fps / adaptive fps。 + +原因:在当前 full-map repaint 架构下,降低 Particle frame 数不仅减少 Particle 本身成本,也会近似同比减少被唤醒的整个 NavSea map scene 成本。 + +### 第二层:Renderer animation-only fast path +值得优先调查是否可在 MapLibre Native 内部实现: +- 复用稳定 map state / RenderTree +- Particle 下一帧不重新进入完整 `Map::Impl::onUpdate` +- 跳过普通 source update、TilePyramid update、部分 prepare 和 placement +- 仍允许必要的 framebuffer redraw + +这属于内部性能实现优化,原则上不需要改变 C-001。 + +### 第三层:静态地图 framebuffer cache + Particle composition +若目标是让静态 fill/line/symbol/PBF draw call 在 Particle 动画期间也消失,则需要 retained/offscreen composition。 + +这比 animation-only fast path 复杂得多,需要处理: +- layer ordering +- transparent layers +- depth/stencil +- 3D +- world wrap +- custom layer/render target +- camera/style/source invalidation +如果最终要求 Particle 固定作为静态地图之上的独立 overlay,架构可显著简化;但若这种限制成为 Host 可观察的公共语义,则必须重新进入 Integration RFC/Contract 评审。 + +## 14. 当前结论 + +源码层面可以确认: + +1. Particle 当前通过 MapLibre 全局 `needsRepaint` 维持动画。 +2. Particle 没有独立 layer-only render loop。 +3. 一次 Particle repaint 会进入完整 `Renderer::render`。 +4. camera 静止时仍会重复大量普通 source/layer update、prepare、render 工作。 +5. 普通 fill/line/symbol/PBF draw 会在连续 frame 中再次提交。 +6. 并非所有 style property 和 placement 都每帧完整重算,但不足以让复杂地图回到 idle。 +7. 当前没有证据表明主要原因是 Particle 每帧修改全局 Style/Source/Camera revision。 +8. STEP-085 已降低 Particle-local 的多余 repaint/copy/rebuild,但没有改变“合法 Particle frame = 完整 MapLibre frame”的架构。 +9. Test Host Style 太简单,不能用于排除真实大型 Style 下的 full-scene amplification。 +10. 下一步应先做 A0/A1/B0/B1 定量 profile,再决定 animation-only fast path 或 framebuffer composition。 +11. 现阶段不需要修改 Particle 公共 API,也不需要修改 NavSea ↔ Particle C-001。 + +## 15. 建议的下一任务 + +建议下一任务只做 Renderer observability / performance profile,不先做架构重构。 + +目标是把完整 frame 拆成 Particle 与 non-Particle 阶段,定量证明: +- 每秒进入多少次完整 map update/render; +- 大型 Style 中额外 CPU 具体落在哪些 source/layer/placement/draw 阶段; +- 30fps 对完整 NavSea 的实际收益; +- 哪些阶段可以通过复用 RenderTree 或 animation-only fast path 安全跳过。 + +完成该 profile 后,再决定是否进入具体 Renderer 优化实现。 diff --git a/docs/20_调查/STEP-095_V13独立严格代码与性能调查_v1.md b/docs/20_调查/STEP-095_V13独立严格代码与性能调查_v1.md new file mode 100644 index 000000000..8b48f7aef --- /dev/null +++ b/docs/20_调查/STEP-095_V13独立严格代码与性能调查_v1.md @@ -0,0 +1,285 @@ +# STEP-095 V13 独立严格代码与性能调查 v1 + +日期:2026-09-25(Asia/Tokyo)。性质:独立静态审查;只写指定中文文档,不修复源码,不运行构建、测试、codegen、安装或设备操作。 + +## 1. 执行摘要与结论边界 + +V13 的通用分段解析、Style 顺序及保守 Fast Path 门控有源码依据,但不能据此批准 GPU 正确性或性能。本轮先检查 V12→V13 diff、真实调用链和生命周期,再阅读旧报告逐项挑战。新增优先问题为:离屏目标切换未重置 CPU stencil 掩码记录;Metal depth/stencil 清除契约未完整下传;LayerGroup 抛错后 retained build 仍可能标成功;V4 PLAY 缺少生产时钟推进调用。前三项的实际像素、故障注入和设备行为仍需验证。 + +旧 F01/F02/F03/F04/F07 的成本路径成立,但没有本轮 profiler 数据证明热点排序、OOM 或收益。F06 多 Continuous 退出静态 CPU Fast Path、F08 保守全局失效属于显式设计限制。F09 可给出计数跨 topology 错归的具体链,但不证明 stale texture 被复用。F05 的 Host `.10`/native `.13` 错配成立,历史 TS2307 不能仅归因于版本号;F10 只证明协调文档滞后。 + +新增成本还包括 V4 全网格物化、UI 线程解码、普通粒子 Metal 离屏等待及 drawable 重建。未找到足以定 P0 的证据;不宣称不存在其他缺陷。调查内容完成,Git 交付结果见阶段记录的收尾条目。**V13 产品运行态不判 PASS:本轮所有构建、测试、设备、性能验证均 NOT RUN。** RDC 仍须独立复核和验证。 + +## 2. 冻结基线、环境与范围 + +| 项目 | 核实结果 | +| --- | --- | +| 机器/目录 | `Mac-mini-tei.lan`,Darwin x86_64;`/Users/tei/dev/MapLibreParticleLab` | +| 分支/起始 HEAD | `particle/main`;`05878d662308b9a92c20c433c3cd2943d74ae718` | +| 起始状态 | 工作树 clean;子模块未见偏离记录提交的标记 | +| 远端 | cached origin 与实际 `git ls-remote` 都是上述 HEAD;origin 为 `https://git.eyaku.jp/tei/MapLibreParticleLab/` | +| 冻结源码 | `ee0b89e711bf01335b3aa71d36def785cd97fec3` | +| V13 功能/旧报告归档 | `016fd1fff6a33cf795c10d4f5c5413d308383677` / `45c4dca84928a57a2cf0226737c34cabd7fbfbfb` | +| V12 比较点 | tag 解引用提交 `7eaff6e12360e26591dceb5e011129eeab7ba4c7` | +| V13 tag | `.13` tag 对象 `bccbceddec70f7a2b6362bce653f9c577aa2db1d`,解引用为冻结源码,远端一致 | +| manifest sourceCommit | `ddf20dec9fe9cd05b77274827b6825829500dd5a`;至冻结提交为发布元数据收尾,生产源码一致 | +| 模型记录 | 用户在本次同一会话恢复指令中明确指定 `gpt-6-astra high`,与任务要求一致;这是当前会话的用户指定记录。未获得可独立核验的底层启动参数,不声称已独立验证实际模型/effort | + +冻结源码至起始 HEAD 仅新增旧审查报告及 STEP-095 任务书;下文源码行号按冻结源码,与本轮起始工作树对应。V12→V13 整体 diff 为 45 文件、2393 行增加/203 行删除(含文档),主要新增 segment/parser/resolver、runtime diagnostics、renderer/orchestrator 调度与平台桥接,并非粒子数值 shader 的整体重写。 + +已读取 AGENTS、PARTICLE_BASELINE、项目规范、项目总则 README 所列当前有效规则及基线 v2,交叉阅读 STEP-088~094 的任务/设计/阶段/报告相关内容。总则仍含早期 `.3` 和 Linux 主线描述,本任务以 Mac `.13` 冻结点为准,不重写历史文档。官方 Core 基线 `18545ca10d296704b2cba675b182ff2c8da24588`,Android 官方 13.4.1、iOS 官方 6.28.0。 + +### 2.1 发布坐标(仅核对仓库记录) + +来源:`release/maplibre-particle/release-manifest.json` 与 `release/maplibre-particle/ios/release-manifest.json`。 + +| 产物 | 坐标 | manifest 大小/SHA-256 | +| --- | --- | --- | +| RN | `@navsea/maplibre-particle-rn@13.4.1-particle.13`,MLRN 11.3.6 | 25,966 bytes;`3a726db06cb967fe56de77ca4a72feed98cd642a2d1b73fd51b1531d15ef7539` | +| Android OpenGL | `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.13` | 169,786,873 bytes;`ddf1deced00cc963d1d625c73c68c449232602113a1490349e69100473dda82b` | +| iOS Metal | `6.28.0-particle.13`,MapLibre XCFramework/SPM exactVersion | 8,418,198 bytes;`8d47d9f6d44479aff51299c213887bd9be204da320da4fd4fff15dd064930556` | + +manifest 的 `PUBLISHED_PUBLIC_READBACK_PASS` 是 HISTORY。本轮没有下载、重算产物散列、重建或核实设备实际链接版本;文档提交不是新 V13 发布。 + +### 2.2 调用链和未覆盖范围 + +已沿 segment JSON/plan → runtime → orchestrator/renderer → cache → PaintParameters/GL/Metal target,及 canonical/QBIN/profile → Store/Clock/projection → RasterArraySource immutable Impl → WeatherColor/Particle RenderLayer → Field/State/Trail GPU 追踪;检查 Android Java/Kotlin/JNI、iOS ObjC++、RN typed API/JSON Spec、Host package/lock/plugin及 Integration 只读状态。阅读 resolver/cache/diagnostics/field/decoder/reseed 测试源码,不等同执行测试。 + +未逐行形式化验证全部官方 renderer/driver、Vulkan、业务 provider/HTTP/Route/ETA或所有并发交错;未使用设备、capture、sanitizer、fuzzer。此报告不是全仓无缺陷证明。 + +## 3. Finding 总表 + +D=源码可复核缺陷路径;C=源码确定成本;R=后果需运行验证;E=环境/文档偏差。置信度均指源码链而非事故发生概率。每项详细位置、修复与验收见后文。 + +| 编号 | 严重度/类别/置信度 | 触发与影响 | 旧报告关系 | +| --- | --- | --- | --- | +| N01 | P1/D+R/高 | 相同 tiles 跨 target,CPU stencil 映射复用,后段可能被裁掉 | 新增 | +| N02 | P1/D+R/高 | Metal retained depth/stencil clear 未完整转换,模拟器 attachment 缺口 | 新增 | +| N03 | P1/D+R/高 | group 绘制抛错被 catch,仍标 cache 成功,可能持续复用残图 | 新增 | +| N04 | P1/D/高 | V4 只 PLAY 不继续 set time,生产时钟无推进调用 | 新增 | +| N05 | P2/D/高 | JSON NUL/重复 strategy 被接受,严格解析失真 | 新增 | +| N06 | P2/D+R/高 | 结构合法但数值非法的场先入库,物化失败后可用投影被置空 | 新增 | +| N07 | P2/D/高 | 同 view 重复 attach,listener 和持有引用递增 | 新增 | +| C01 | P1/C/高 | full diagnostics 锁内深复制进入 fast/render | F01 | +| C02 | P1/C+R/高 | Metal retained build 与普通 State/Trail target 等待 | F02,新增扩大范围 | +| C03 | P1/C/高 | 内容 dirty 释放同尺寸可复用 GPU allocation | F03 | +| C04 | P1/C+R/高 | 多 R 全视口资源无总预算;Store 也缺 fieldKey 总预算 | F04,新增场预算 | +| C05 | P2/C/高 | ordinal 线性前缀遍历,fast update 仍扫全部 items | F07,新增扫描 | +| C06 | P1/C+R/高 | 同时间/mix-only 整网格物化;提交链可占用 UI | 新增 | +| C07 | P2/C/高 | tick 重建 drawable/attributes/tweaker,窗口切换复制场 | 新增 | +| O01 | P2/D/高 | 新 topology 先发布,旧 cache 析构向新 index 计数 | F09 | +| E01 | P1/E/高 | Host RN `.10` 与 Android 插件 `.13` 错配 | F05 | +| E02 | P3/E/高 | Integration 仍旧状态,实际接入 UNKNOWN | F10,降级 | + +## 4. 新增正确性和生命周期发现 + +### N01:CPU stencil 记录跨 framebuffer 复用 + +证据:`src/mbgl/renderer/renderer_impl.cpp:798-847,938-945`;`src/mbgl/renderer/retained_underlay_cache.cpp:304-319`;`src/mbgl/renderer/paint_parameters.cpp:185-201,365-374`;`src/mbgl/gl/layer_group_gl.cpp:67-98`;`src/mbgl/mtl/tile_layer_group.cpp:118-119`。 + +最短链:同一个 PaintParameters → 第一个 R 清 stencil、写 mask 并记录 tileClippingMaskIDs → 第二个 R 新 target/清 stencil → tileIDsCovered 为真而跳过写 mask → draw 仍 Equal 非零 ID。CPU 记录属于前一 target,后一 GL target 实际为零。main buffer 清除后也未清 CPU 记录;updateStencilBufferAvailability 不是映射重置。 + +触发用例:同 vector source、同 tile 集合的两个 fill 分置相邻 R-R,首次都 build;或 R build 后同 tiles 的 C 绘入主目标。tile 集合变化可触发清记录,所以不是所有画面必坏。V13 任意 R-R 扩大暴露面,V12 多 target 也应回归,不能断言只由 V13 引入。 + +最小修复:target/pass 切换时明确失效 CPU mask/编号,或按 target generation 隔离,并保证 GPU clear 一致。验收:GL/Metal cache OFF 对照,同 source R-R/R-C-R 的首帧、rebuild、reuse 像素和 stencil capture。缺口源码已确认,实际像素 NEEDS_RUNTIME。 + +### N02:Metal depth/stencil 描述未完整下传 + +证据:`retained_underlay_cache.cpp:304-310` 请求 clearDepth=1、clearStencil=0;`src/mbgl/mtl/render_pass.cpp:13-38` 只转换 clearColor;`src/mbgl/mtl/offscreen_texture.cpp:32-83,139-148` 绑定附件但该路径未设置 depth/stencil clear/load/store。模拟器不创建 stencilTexture,也未将 combined depth/stencil texture 绑定 stencilAttachment;格式选择见 `src/mbgl/mtl/texture2d.cpp:99-104`。 + +触发:Metal retained tile/depth 绘制,尤其首次 target、simulator。源码确认契约缺口,但未测驱动默认内容/validation 行为,不宣称所有真机黑屏。isRenderable 仅验证 color,depth/stencil create 在 bind,早期就绪判断不能证明所有附件就绪。 + +最小修复:完整翻译 descriptor、正确绑定 combined attachment,并验证所有附件后允许 build;保持官方接入薄。验收:Metal API Validation、真机/模拟器 capture 核对 attachment/load/store/clear,故障注入验证安全 fallback。NEEDS_RUNTIME。 + +### N03:绘制异常未阻止 cache 发布为成功 + +证据:`src/mbgl/renderer/render_orchestrator.hpp:156-178` 的 visitor catch 只通知 observer;`renderer_impl.cpp:798-824` 随后无错误结果判断,finishBuild/markBuildSucceeded。 + +触发:R 的一个 layerGroup.render 抛错,observer 正常返回,其他 group 继续,残缺图像可变 valid 并在稳定帧 reuse。反例:资源准备阶段已有失败回退(`retained_underlay_cache.cpp:240-255`、`renderer_impl.cpp:662-677`),问题不等于所有资源失败都没有 fallback。 + +最小修复:build 汇总明确成功状态,失败 target 不发布/复用,可恢复错误进入完整渲染或约定安全失败。验收:第二个 group 故障注入,确认 successful build/reuse 不增长、无 stale composite、恢复和资源释放正确。NOT RUN。 + +### N04:V4 PLAY 没有生产时钟推进 + +证据:`src/mbgl/style/sources/raster_array_source.cpp:453-470`;`src/mbgl/particle/field/field_runtime.cpp:196-220`;`src/mbgl/renderer/layers/render_particle_validation_layer.cpp:1174-1189,1276-1278`。生产树 src/include/platform/packages 中 advanceForecastClock 仅见声明和定义,无调用。Android `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java:225-242`、iOS `platform/ios/src/MLNParticleFieldRuntime.mm:147-165` 的 wake 只 repaint。 + +最短链:设一次 requested time → PLAY 改 clock state → repaint/particle cadence → renderer 读取不变的 immutable projection.mix。粒子运动不能证明预报时间在播放。Host 每次显式 set time 可绕过,但不代表 PLAY 本身有效。 + +最小修复:在既有 map/source 所属线程接入唯一单调时钟,明确速度、pause/resume、后台和销毁语义,不建立第二套时间真相。验收:仅设一次时间,PLAY 的 .5/1/2 倍 mix、PAUSE、跨窗口和恢复;`test/particle/field_runtime.test.cpp:272-310` 手动 advance 单测不覆盖生产接线。本轮 NOT RUN。 + +### N05:Segment JSON NUL 截断与重复键 + +证据:`src/mbgl/renderer/render_segment_json.hpp:16-49`。Parse 用 C 字符串,strategy/startLayerId 用 GetString 未带 GetStringLength;`"RETAINED\u0000x"` 截成 `RETAINED`,`"boundary\u0000x"` 可解析到另一真实 ID。segment 两个重复 strategy 满足 MemberCount<=2,白名单不检唯一性,读取首值。 + +触发可由 primitive JSON/native 入口构造,TS typed API 不替代校验。普通未知 strategy/schema、missing/reversed 仍会 invalid/fallback。最小修复:长度感知解析/构造,拒绝 NUL/重复键,保持 invalid 清旧 plan 语义。验收:上述 payload、raw NUL 后缀、正常 unicode、未知字段、invalid→valid及两桥接错误归一化。NOT RUN。 + +### N06:数值非法的场在入库后才被拒绝物化 + +证据:`src/mbgl/particle/ingress/qbin_v2_decoder.cpp:41-73` 校验结构、CRC、scale/offset但不扫描 Float32 样本;`src/mbgl/particle/field/field_profile_validator.cpp:77-166` 匹配契约;`field_projection_materialization.cpp:38-67,97-135` 才检查解码数值;`raster_array_source.cpp:422-440,496-522` 先 submit,再物化,失败 next 对应投影为空。 + +触发:已有绑定字段,同时间更高 revision、CRC 正确、valid=1、样本 NaN/Inf 的 QBV2。结构/契约可接受,替换旧帧后物化失败,但 submit 返回 AcceptedInserted/AcceptedReplacedRevision。有限 scale 乘法溢出也应纳入验证。此处不声称 NaN 上传 GPU——物化拒绝正是反例;缺陷是 accepted/可渲染语义及旧状态保留脱节。 + +最小修复:明确 ingress 数值约束,入库前校验或事务式验证,保留合法无效 cell/通用未绑定 plane 语义。验收:好旧帧→坏高 revision,检查返回值、Store/投影保持、noData/validity、极值及恢复。NOT RUN。 + +### N07:Android attach listener 积累 + +证据:`packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt:30-63`。每 attach 新增捕获 concrete map 的 listener;显式 detach 只 dispose runtime,view detach 回调也不移除 listener。 + +同一个存活 view 重复 attach/detach,listener/引用及回调为 O(k)。未证明 view 回收后永久泄漏或 UAF;每次重新解析 view、没有全局 reactTag registry 是正面约束。最小修复:view/map 单一 listener、attach 幂等、明确 remove 生命周期。验收:同 view 100 次、换 view、卸载/后台恢复,listener/dispose/存活对象回基线。NOT RUN。 + +## 5. 性能、资源与观测 + +### C01:完整 diagnostics 深复制进入热路径 + +证据:`renderer_impl.cpp:198-208,500-505` → `retained_underlay_runtime_state.hpp:243-254` 锁内复制 styleLayers、segments.layers、字符串。成功 fast frame 可两次,不是所有帧固定两次。成本 O(L+S),锁竞争程度待测。修复:一致的轻量热快照与完整外部 pull 分开,不能无同步拆字段。验收:稳定 30/60 FPS 的完整快照次数应随外部 pull,测 alloc/frame、锁等待、CPU;无收益百分比。 + +### C02:Metal 等待不限于 retained rebuild + +证据:`retained_underlay_cache.cpp:316-319` → `src/mbgl/mtl/command_encoder.cpp:29-31` → `mtl/offscreen_texture.cpp:86-91` commit/waitUntilCompleted,失效 R 逐段同步;稳定 reuse 没有 retained build wait。 + +新增链:`src/mbgl/renderer/render_target.cpp:71-119` 普通 target 也 present;`render_particle_validation_layer.cpp:1990-2111` tick 注册 State/Trail 写目标,初始化可能更多。因此 retained 稳定也可能有多个普通粒子 wait,属继承成本。修复前先 capture buffers、wait 栈及依赖,再设计有序异步提交,不能直接删 wait。验收:State A/B 无别名,Trail/composite 顺序与轨迹一致,CPU wait/GPU frame time 成对比较。NEEDS_RUNTIME。 + +### C03:内容失效与 allocation 销毁耦合 + +证据:`retained_underlay_cache.cpp:161-237` revisionChanged 同时 invalidate/release;`:240-255,365-374` 重建/释放 texture/compositor。触发为内容/相机等 revision 改变,不是稳定帧无条件分配。修复:contentDirty 与尺寸/context/格式的 resourceInvalid 分开,同尺寸有效 context 保留 allocation。验收:mutation、camera、resize、context loss 分开统计 build/create/release/peak;先保证 N01/N02 clear 正确。耗时收益待测。 + +### C04:多段 GPU 与多字段 CPU 无总预算 + +证据:`renderer_impl.cpp:506-524`;`retained_underlay_cache.cpp:240-255`;`src/mbgl/gl/offscreen_texture.cpp:18-35`、`mtl/offscreen_texture.cpp:20-55`。每 R 全视口 color/depth/stencil;RGBA8 color 理论下界 R×W×H×4 bytes,另有附件、alignment、主 framebuffer、State/Trail及延迟释放,不能把逻辑 release 当物理显存回收。 + +新增:`src/mbgl/particle/field/field_runtime.cpp:223-230` 对新 fieldKey 建 series;`include/mbgl/particle/field/field_runtime.hpp:115` 每 series 默认容量并非总 key/总 bytes 上限。QBIN 单文件64 MiB/解码32 MiB 上限也不约束累计持有量。 + +修复:map 预算、分配前溢出安全估算、超限明确 full-render/拒绝策略,不合并相邻 R、不淘汰仍绑定窗口。验收:R×viewport×字段数阶梯、allocation failure/context loss、30分钟以上 peak/回收。OOM/泄漏未证实,NEEDS_RUNTIME。 + +### C05:ordinal 前缀遍历和全层扫描 + +证据:`render_orchestrator.hpp:156-178` 的 multimap std::distance 是线性,S 次范围定位可能 O(S×G);`render_orchestrator.cpp:1202-1226` fast updateParticleLayers 仍遍历完整 items,非粒子只跳过 update;`:973-975` 按 retained 稳定性还可能扫 source fading。 + +修复:topology 改变时缓存 global ordinal/粒子列表,保持 generation 和原始全局深度编号。验收:固定粒子层扩大 L/G/S,测 CPU/调用次数并回归 reorder/delete。实际热点占比待测。 + +### C06:同时间/mix-only 仍全网格物化,UI 成本遗漏 + +证据:`raster_array_source.cpp:453-455,496-522` 在 sameProjection 前分别物化 color/particle;`field_runtime.cpp:196-200` 相同非零时间也返回 true;`field_projection_materialization.cpp:69-75,97-211` 分配/遍历 validity、T0/T1 scalar/vector。Wind 双绑定可重复同向量物化。mix 更新产生新 Impl/onSourceChanged,`render_orchestrator.cpp:361-365` 全局 SourceMutation 可进一步失效所有 R。 + +桥接:Android withMap UI 调度经 `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp:1501-1517` 同步到 Source;iOS `packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.mm:105-120,235-240` 的 withMap 与 `platform/ios/src/MLNParticleFieldRuntime.mm:222-252` 也同步提交 Core。预读取文件不等于 CRC/解码/物化已经离开 UI。成本随格点/plane 增长,实际卡顿/ANR未测。 + +修复:相同时间早退,按端点 identity/binding 共享不可变物化、mix 单独更新;若 worker 解码,owner-thread 发布必须带 map/source generation,禁止 worker 直接改 Source。局部失效必须先有完整依赖。验收:同时间、mix-only、跨窗口、双绑定、最大网格的 CPU/alloc/UI阻塞/upload/rebuild计数。NEEDS_RUNTIME。 + +### C07:tick 对象重建及端点复制 + +证据:`render_particle_validation_layer.cpp:826-904` clearDrawables 后新 attributes/builder/tweaker;`:1668-1669,1990-2111` 更新 target 注册及 State/Trail/composite;`:657` createOrUpdate UBO 不证明跨新 drawable 复用;`:1494-1499,1538-1563` 场端点切换复制数组。 + +反例:State/Trail textures 和共享顶点不是每 tick 全部重建,field 复制主要在窗口边界,不能称 CPU 每帧模拟全部粒子或无条件上传场。修复:稳定 drawable 保留,仅更新 uniforms/texture,端点共享不可变存储。验收:tick/bootstrap/reseed/resize/端点轮换分别测对象、UBO、upload和峰值。NOT RUN。 + +### O01:旧 cache release 记入新 topology + +证据:`retained_underlay_runtime_state.hpp:173-240` 先发布新 entries;`renderer_impl.cpp:500-524` 再 clear 旧 cache;`retained_underlay_cache.cpp:120-122,365-374` 析构按旧 segmentIndex 计数;runtime `:256-272` 寻找当前 entry。 + +旧 index 0 的 R 换成新 R,旧 release 可记入新 R;新 index 为 C/越界时 per-segment 计数丢失。不证明 GPU 未释放或旧图复用。另 runtime `:198` root fallback 只表示 resolution invalid,不能覆盖 cache OFF/unsupported/resource fallback;消费方不可读成 GPU 当前 retained 成功。 + +修复:计数带 cache identity/generation,聚合历史与当前段分开,明确两类 fallback。验收:真 cache R-R→C-R、相同 index 换边界、disable/restore析构计数,纯手动 counter 测试不足。NOT RUN。 + +## 6. 环境发现和旧 F01~F10 复核 + +E01 定位:`integration/react-native/MapLibreParticleRNTestHost/package.json:12`、`package-lock.json:12,1765-1766` 为 `.10`,`plugins/withParticleNativeAndroid.js:9` 注入 `.13`,当前 package/native 为 `.13`。默认干净 Host 不能证明 V13 全链。旧 typecheck exit2/TS2307/TS7006 为 HISTORY;缺模块和旧 API 是不同原因,本轮未复跑。修复方向:后续单独对齐 package/lock/native resolved version,干净安装、typecheck/codegen及设备调用;不能把临时工作树链接 PASS 当默认包 PASS。本轮不改依赖。 + +E02 定位:只读 `/Users/tei/dev/navsea-particle-integration` HEAD `39ad8415fbdb76c1822460bb09f3a3f35fe141e0`,`compatibility/MATRIX.md` 与 `status/` 仍含 Sept18 `.5`、iOS 等待、RN NOT_STARTED。它与本仓 `.13` 是不同时点,真实 NavSea 接入 UNKNOWN。降 P3 文档项,建议负责人以接入 commit/包/设备证据更新,不改 Core 协议;本轮不改 Integration。 + +| 旧项/原结论 | 当前意见 | 修正和依据 | 补证 | +| --- | --- | --- | --- | +| F01 P1 深复制 | CONFIRMED | C01,成功 fast 可两次而非每帧必两次 | alloc/锁/CPU NOT RUN | +| F02 P1 retained wait | CONFIRMED | C02,限定 build,补普通粒子 wait | 收益 NEEDS_RUNTIME | +| F03 P1 dirty 释放 | CONFIRMED | C03,稳定 reuse 不重建 | 内容/resize/context分测 | +| F04 P1 无预算 | CONFIRMED | C04,无预算不等于已 OOM | peak/压力 NEEDS_RUNTIME | +| F05 P1 Host 不可复现 | QUALIFIED | E01,错配确定,旧 TS2307非本轮且不可仅归版本 | 干净安装 NOT RUN | +| F06 P1 multi-C full CPU | QUALIFIED | 显式设计限制,建议 P2;不等于关闭 GPU retained reuse | 相同Style A/B NOT RUN | +| F07 P2 ordinal 线性 | CONFIRMED | C05,优化定位不能改全局编号 | S/G基准 NOT RUN | +| F08 P2 全局失效宽 | QUALIFIED | 保守设计,未证明 stale;未知依赖不能裁窄 | source/tile/glyph依赖 | +| F09 P2 错归风险 | CONFIRMED | O01具体先换entries后旧析构链;不证明stale texture | 真cache生命周期 NOT RUN | +| F10 P2 文档漂移 | QUALIFIED | E02降P3;不推定实际接入 | 负责人跨项目核对 | + +没有为凑数将整个旧项标 DISPROVED;被否定的是“多C关闭所有GPU reuse”“计数错归证明旧纹理复用”“.10必然造成TS2307”“单测证明像素正确”等外推。旧报告未明确作这些断言之处,也不反向指控它作过。 + +## 7. 反例、边界和设计限制 + +1. `render_segment_plan.hpp:32-157` 采用完整 ordered IDs(含 hidden),边界存在且递增,全R/全C合法,相邻同策略不合并,V12经同模型映射;首段可省 startLayerId 是契约。普通 invalid 清 resolution、恢复重解析有依据。 +2. reorder 不是“仅added/removed所以遗漏”:`src/mbgl/renderer/style_diff.cpp:9-42` 序列差异体现移动,`render_orchestrator.cpp:318-329` 重建 orderedLayers,未报告无可达链的 reorder bug。 +3. `renderer.cpp:37-59` 在 full createRenderTree 前清 prepared tree,fast gate 检查 style/source 等输入;不能只见缓存引用便断言 UAF。context/destroy 极端交错仍需验证。 +4. `renderer_impl.cpp:798-918` 保留 opaque reverse/translucent forward 与 global ordinal,compositor premultiplied alpha/depth disabled 有源码依据。但不能推导所有分段像素等价,N01/N02仍为 gate;不凭空宣称所有 alpha 顺序错误。 +5. Cache OFF、不支持内容、资源准备失败已有普通路径;多 C 只退出 CPU fast,R可reuse。3D/debug/legacy/custom gate保守,不建议绕过求性能。 +6. QBIN有文件/plane/cell/decoded上限、CRC/bounds/长度检查,Clock有速度/整数溢出检查;FieldSeries按时间/revision处理重复/旧版本并shared持有窗口。未找到可证实的任意写/越界链;N06是数值发布语义。旧canonical adapter已有有限性检查,不能一概否定。 +7. State A/B、Trail、T0/T1和方向/速度各有路径;`render_particle_validation_layer.cpp:1671-1810` resize重建Trail、按zoom-out gesture决定reseed并保留其他状态,不代表orientation/zoom-out设备通过。nearest/linear采样及双端点validity需数值视觉基准,本轮未断言错误插值。 +8. 共享海陆mask尚无验收证据:`field_projection_materialization.cpp:77-92` snapshot displayDomain为GLOBAL;`render_particle_validation_layer.cpp:278-293,1314-1324` 有synthetic coastal mask路径。不能据此证明Wave/Tide/Current共用正式Land/Ocean Mask;陆地覆盖显示不等于mask。列为架构/接入证据缺口,不擅改协议。 +9. 桥接保持map-scoped,typed object经内部JSON,没有引入业务协议;普通invalid plan会关闭旧计划。符号接线不等于ABI/codegen或销毁竞态已测。 + +## 8. V12/V13 架构与成本比较 + +L=层数、G=LayerGroup数、S=段数、R=retained数、W/H=物理视口、P=格点。仅源码模型,不是测量。 + +| 维度 | V12 | V13 | 判断 | +| --- | --- | --- | --- | +| topology | 固定range,最多两个R | 任意独立R/C,相邻R不合并 | 扩大管理/资源上界,不自动提高FPS | +| GPU复用 | 固定retained部分 | 每R独立 | Layer fingerprint局部化,全局依赖仍保守 | +| CPU fast | 合格单连续区间 | 恰好一个C且gate满足 | 多C full CPU不等于GPU cache关闭 | +| diagnostics | 旧统计结构 | 完整Style和重复段层列表 | C01 O(L+S)复制 | +| 范围定位 | 固定数量 | S段前缀可能O(S×G) | 实际占比待测 | +| retained资源 | 至多两组 | O(R×W×H) | 物理峰值与OOM未测 | +| Metal等待 | 固定targets | 随失效R增加 | 普通State/Trail是继承成本 | +| 场/粒子更新 | 已有物化/drawable | 多数继承 | C06/C07不全归因V13 | + +STEP-090 fast ON 21.7%/OFF97.0% RenderThread仅历史特定实验,不外推V13。历史一次退出及随后短时未复现,既不证明泄漏也不证明长稳。必须同设备、Style、源数据、视口、刷新率/热状态配对,先像素正确再比CPU/GPU。 + +## 9. 测试证据与 RDC 验证矩阵 + +| 项目 | 历史 | 本轮 | +| --- | --- | --- | +| STEP-094 GL构建、8 suites/43 tests | 报告PASS | HISTORY/NOT RUN;纯状态解析不能验证GPU | +| Field/QBIN/viewport69 tests、package TS | 旧审查PASS | HISTORY/NOT RUN | +| Metal-only iPhoneOS编译 | STEP-094 PASS | HISTORY/NOT RUN;不验证附件/像素 | +| Android APK、RN codegen | STEP-094 PASS,codegen用底层入口 | HISTORY/NOT RUN | +| Host工作树替换TS及旧默认失败 | 两种不同依赖环境 | HISTORY/NOT RUN,可同时成立 | +| 本轮静态/文档检查 | 不适用 | 已执行;Git结果见阶段收尾 | + +以下所有运行项本轮均未执行。 + +| Gate | Android GL | iOS Metal真机/模拟器 | Vulkan/其他 | 证据要求 | +| --- | --- | --- | --- | --- | +| R/C/R-R/R-C-R/C-R-R/R-R-C/R-C-R-C-R | NOT RUN | NOT RUN | NOT RUN | cache OFF/ON截图、像素差、pass/ordinal | +| 同source stencil、半透明fill/line/symbol | NOT RUN | NOT RUN | NOT RUN | 首帧/rebuild/reuse、depth/stencil capture | +| hidden/reorder/delete/restore/invalid | NOT RUN | NOT RUN | NOT RUN | 下一帧无stale、revision与plan | +| OFF/资源失败/draw失败/context loss | NOT RUN | NOT RUN | NOT RUN | 失败不可reuse,资源配对与恢复 | +| PLAY/T0T1/坏数值/乱序/旧revision | NOT RUN | NOT RUN | NOT RUN | time/mix、返回码、旧窗口保持策略 | +| pan/zoom-out/pitch/resize/orientation | NOT RUN | NOT RUN | NOT RUN | state/trail/reseed及无额外field提交 | +| 30/60FPS,单C/多C、mutation | NOT RUN | NOT RUN | NOT RUN | CPU/GPU p50/p95/p99、miss/wait/alloc | +| 预算/峰值/30分钟以上长稳 | NOT RUN | NOT RUN | NOT RUN | R×视口×字段阶梯、bytes/后台/卸载 | +| attach/detach/双map/销毁中完成 | NOT RUN | NOT RUN | NOT RUN | owner thread、generation、listener | +| 干净包版本/ABI/codegen | NOT RUN | NOT RUN | 不适用 | resolved包/散列/生成接口/实际调用 | + +RDC先记录HEAD/manifest/tag/构建配置;以下是既有精确命令,本轮未执行。新增缺陷尚未补测试,旧过滤集PASS不能关闭本报告。 + +```sh +cmake --build build-step088-opengl --target mbgl-test-runner -j3 +build-step088-opengl/mbgl-test-runner --gtest_filter='RenderSegmentPlan.*:RenderSegmentDiagnostics.*:ContinuousLayerRange.*:RetainedDynamicBoundary.*:RetainedLayerGroupRange.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:ParticleFastPath.*' +build-step088-opengl/mbgl-test-runner --gtest_filter='ParticleFieldRuntime.*' +cmake --build build-ios --target ios-sdk-static -- -quiet +``` + +Metal命令前确认构建目录MLN_WITH_METAL=ON、MLN_WITH_OPENGL=OFF;缺目录由RDC按规范配置,不复用未知缓存。RN先解决真实resolved `.13`前提,再按STEP-094/083现行入口typecheck/codegen。失败回传首个真实error、target、上下文和日志路径,成功仅exit/耗时/摘要。 + +## 10. 优先级与建议修复批次 + +只建议,不创建修复任务、不改源码。 + +| 批次 | 范围 | Gate/安全约束 | +| --- | --- | --- | +| A 正确性 | N01/N02/N03 target/Metal/失败发布,N04时钟另小提交 | 最小复现、故障注入、两平台OFF基线、PLAY端到端;不借机全面重构 | +| B 无设备可先回归 | N05/N06/N07/O01/E01 | 纯测试/桥接由RDC执行;版本可复现,不代替A的GPU gate | +| C CPU安全优化 | C01轻快照、C05ordinal/粒子列表、C06相同时间早退/端点复用 | 同步一致、Style唯一顺序、调用/分配次数证明 | +| D GPU资源/同步 | C02/C03/C04/C07 | 先profile,再预算和allocation/content分离;明确buffer依赖,不能直接删wait | +| E 较大架构 | multi-C fast、source依赖、异步大场、正式共享mask | 单独设计,未知依赖继续全局失效,不引入provider/HTTP业务调度 | + +## 11. Git 与交付 + +仅四个允许路径:本文、`docs/50_Step记录/STEP-095_V13独立严格代码与性能审查_v1.md`、`docs/报告/STEP-095_V13独立严格代码与性能审查_Codex完成报告_v1.md`、根`粒子开发记录.md`末尾追加。创建前同主题输出不存在,使用v1,不覆盖旧报告。 + +起始HEAD `05878d662308b9a92c20c433c3cd2943d74ae718`。检查路径/引用/`git diff --check`后仅scoped staged/commit/push origin particle/main。实际SHA/message、push/remote/status及最终收尾提交自引用说明见阶段记录第4节;推送未完成前不写Git PASS。冻结源码至最终文档提交的差异只含既有两份文档及本轮四份文档,不含源码/shader/API/依赖/build/manifest/tag/产物。 diff --git a/docs/20_调查/STEP-096_V13隔离盲审调查_v1.md b/docs/20_调查/STEP-096_V13隔离盲审调查_v1.md new file mode 100644 index 000000000..8160efdc2 --- /dev/null +++ b/docs/20_调查/STEP-096_V13隔离盲审调查_v1.md @@ -0,0 +1,231 @@ +# STEP-096 V13 隔离盲审调查 v1 + +本报告仅使用 `/Users/tei/dev/MapLibreParticleV13Blind096` 内的当前规范、任务书与源码。发现由本轮独立阅读、追踪调用链及反例检查形成;未读取外部仓库、其他轮次审查、历史报告、会话日志、网页或既有审查结论。未构建、未执行测试、未运行设备、未修改源码。 + +## 1. 输入、结论边界与证据等级 + +- 合同:[STEP-096 任务书](../40_任务/STEP-096_V13隔离盲审_Codex任务_v1.md)。已读取根 AGENTS、PARTICLE_BASELINE、项目规范目录、总则 README 指定的当前有效文件。 +- 唯一目录:`/Users/tei/dev/MapLibreParticleV13Blind096`。目标导出 SHA:`ee0b89e711bf01335b3aa71d36def785cd97fec3`,来源为任务书声明;本轮没有 Git 对象可供独立核验,未运行 Git。branch、当前 HEAD、Git clean/remote 状态均为 NOT AVAILABLE。 +- [release manifest](../../release/maplibre-particle/release-manifest.json#L4) 声明 particleRevision=13、sourceCommit=`ddf20dec9fe9cd05b77274827b6825829500dd5a`,RN/Android 为 `13.4.1-particle.13`,iOS 为 `6.28.0-particle.13`。manifest 的 sourceCommit 是产物来源声明,不能替代本次快照导出 SHA,也不能据此断言导出错误。 +- 当前基线总则仍包含 `.3` 发布坐标;本轮以快照源码及 `.13` manifest 描述实现。此为文档口径差异,未据此推断线上产物错误。manifest 的发布成功字样不是本轮公网回读证据。 +- 当前工作机器观察值为 `Mac-mini-tei.lan`;工作日期为 2026-09-25。实际 TUI 启动命令、screen 归属、模型配置及 reasoning 设置的独立启动记录均 NOT AVAILABLE;不访问目录外补证,不声称核实了 RDC 启动要求。 + +“确定缺陷”指限定触发条件下源码控制流或数值关系已闭合,不表示已复现用户事故。“源码确定成本”指可证明的调用、遍历、分配或等待,不表示 profiler 排序。“设计限制”指当前能力或上界不足。所有 GPU 画面、耗时、卡顿、OOM、崩溃、性能收益均 NEEDS_RUNTIME;所有构建和测试均 NOT RUN。未发现可由本轮证据成立的 P0。 + +## 2. 独立架构地图 + +| 路径 | 实际责任和所有权 | 审查定位 | +|---|---|---| +| RN RendererRuntime → 平台 bridge → Map/Renderer → RenderOrchestrator → segment plan | Host 提交策略配置;Style 层顺序仍是绘制顺序来源,策略在该顺序上划分区间 | [RN 入口](../../packages/maplibre-particle-rn/src/RendererRuntime.ts#L22)、[分段计划](../../src/mbgl/renderer/render_segment_plan.hpp)、[Renderer](../../src/mbgl/renderer/renderer_impl.cpp#L583) | +| Renderer → RetainedUnderlayCache → gfx RenderPass/OffscreenTexture → GL/Metal | Renderer 持有各 retained 段缓存;缓存拥有离屏纹理/合成资源;同帧 PaintParameters 穿过多个目标 | [缓存生命周期](../../src/mbgl/renderer/retained_underlay_cache.cpp#L201)、[渲染调度](../../src/mbgl/renderer/renderer_impl.cpp#L798) | +| RN FieldRuntime → Android/iOS bridge → WeatherRuntime/MLNParticleFieldRuntime → RasterArraySource | bridge 定位地图并派发;Source 持有 encoded store、绑定、forecast clock;发布不可变 Impl/投影供 RenderLayer 读取 | [RN API](../../packages/maplibre-particle-rn/src/FieldRuntime.ts)、[Source 提交](../../src/mbgl/style/sources/raster_array_source.cpp#L422) | +| encoded ingress → QBIN V2/V3 → profile validator → EncodedFieldStore/FieldSeries → binding/window | magic 分派格式,校验结构和 profile;每个 fieldKey 保存时间序列;时间窗选取后物化数组 | [V3 解码](../../src/mbgl/particle/ingress/qbin_v3_decoder.cpp)、[profile](../../src/mbgl/particle/field/field_profile_validator.cpp)、[series](../../src/mbgl/particle/field/field_runtime.cpp#L144) | +| projection → WeatherColorLayer / RasterParticleLayer → GL/Metal shader | 色场与粒子分层;颜色消费标量或向量幅值;粒子持有场纹理、State A/B、Trail A/B 和 drawable/tweaker | [物化](../../src/mbgl/particle/field/field_projection_materialization.cpp#L201)、[粒子更新](../../src/mbgl/renderer/layers/render_particle_validation_layer.cpp#L1489)、[颜色更新](../../src/mbgl/renderer/layers/render_weather_color_layer.cpp#L205) | + +正常路径存在实际保护:Style diff 处理增删/重排;无效分段与不支持场景可 fallback;稳定 retained 帧可复用;解码有结构、尺寸、CRC 等检查;同时间身份冲突和陈旧修订会拒绝;超出时间覆盖范围不会凭空外推。Source 使用共享不可变帧/投影,使读者可持有旧快照;粒子 tweaker 使用 weak_ptr,不能仅因资源与 drawable 互相引用的表象断言泄漏。 + +固定边界的实现情况:正式 Host 提交路径未要求 Native 承担 provider/manifest 下载;旧 fixture/local FileSource 路径应单独理解。RN 是控制入口,绘制仍在 Native Core。粒子模块有独立 field/ingress/render 层,但 Renderer/RenderOrchestrator 中也存在较多集成逻辑;没有上游对照输入,无法量化“薄接入”变更比例。Wave/Tide/Current 的共享地理掩码要求见 B09。GL 与 Metal 不能由共享 C++ 自动推出行为一致;Vulkan 完整粒子链未在本轮闭合。 + +## 3. 独立发现总表 + +编号按本轮源码证实顺序,排除的候选不占编号;优先级是建议处理顺序,不是实测事故等级。 + +| 编号 | 优先级 | 类型 | 置信度 | 发现及作用边界 | +|---|---|---|---|---| +| B01 | P1 | 确定缺陷 | 高 | 切换离屏/主目标未重置 tile stencil 的 CPU 覆盖记录 | +| B02 | P1 | 确定缺陷 | 高 | Metal RenderPass 没有落实 gfx 的 clearDepth/clearStencil 请求 | +| B03 | P2 | 源码确定成本 | 高 | Metal 每次离屏 present 显式等待 GPU 完成 | +| B04 | P2 | 源码确定成本 | 高 | retained 内容失效同时释放同尺寸 GPU 资源 | +| B05 | P2 | 源码确定成本 | 高 | 时间更新先全量物化场,再判断投影是否相同 | +| B06 | P1 | 确定缺陷 | 高 | encoded PLAY 无生产路径推进 forecast clock | +| B07 | P2 | 确定缺陷 | 高 | 8-bit 对称场编码不能表示零,产生有符号偏差 | +| B08 | P2 | 确定缺陷 | 高 | Mercator 东向 U 位移缺少纬度修正因子 | +| B09 | P2 | 设计限制 | 高 | 当前掩码为合成 fixture,未形成共享地理 Ocean Mask | +| B10 | P2 | 源码确定成本 | 高 | 平台 UI 派发链同步解码并物化 encoded 数据 | +| B11 | P2 | 设计限制 | 高 | encoded store 仅限制每序列帧数,无全局字节/key 上界 | +| B12 | P2 | 确定缺陷 | 高 | retained 段绘制抛异常后仍可能标记缓存构建成功 | +| B13 | P1 | 确定缺陷 | 高 | 粒子场更新门槛漏掉合法时间窗内的 V/有效性变化 | + +### B01 — stencil 覆盖记录没有随渲染目标切换 + +**触发与调用链。** 多个 retained 段,或 retained 构建后主目标绘制,复用相同瓦片集合且需要 stencil 裁剪。[Renderer buildRetainedRange](../../src/mbgl/renderer/renderer_impl.cpp#L798) 共用 PaintParameters;[beginBuild](../../src/mbgl/renderer/retained_underlay_cache.cpp#L304) 创建清 stencil=0 的离屏 pass,GL 只更新 stencil 可用性;随后[主 pass](../../src/mbgl/renderer/renderer_impl.cpp#L839) 也清 stencil=0。[updateStencilBufferAvailability](../../src/mbgl/renderer/paint_parameters.cpp#L100) 不清覆盖表;[tileIDsCovered](../../src/mbgl/renderer/paint_parameters.cpp#L147) 和[早退](../../src/mbgl/renderer/paint_parameters.cpp#L185) 只看 tile ID,不看 attachment。下一目标仍会用[旧的 Equal stencil ID](../../src/mbgl/renderer/paint_parameters.cpp#L365)。[GL LayerGroup](../../src/mbgl/gl/layer_group_gl.cpp#L67) 正常调用掩码生成,也无法绕过这个早退。 + +**所有权、正常/错误/销毁。** 覆盖表属于同帧 PaintParameters,stencil 内容属于每个目标附件。下一帧重建 PaintParameters 不能修复同帧跨目标;[显式 clearStencil](../../src/mbgl/renderer/paint_parameters.cpp#L156) 会重置记录,但上述目标切换没有调用。资源创建失败有 fallback,与本问题不同;正常创建成功即可触发。在 GL 已清为 0 的新目标上,旧正 ID 与实际内容不一致的路径已证实。Metal 的清屏另受 B02 影响。 + +**反例与边界。** 同一 attachment 内复用覆盖记录是正确优化;瓦片集合改变可能重绘掩码;无裁剪图层、没有跨目标构建或全部退回主绘制的帧不满足条件。不能据此称所有 retained 帧都缺图,实际画面 NEEDS_RUNTIME。 + +**安全修复/最小验证。** 在目标/attachment 世代切换时失效掩码元数据及 ID 分配状态,保留同目标复用;不可在已绘制内容中途随意清 stencil。构造 R/C/R 和 R/R 共用瓦片的 fill/line 层,逐目标检查掩码写入及截图,与关闭 retained 对照;含稳定复用、相机变化、主目标后续 live 层。 + +### B02 — Metal 没有传递 depth/stencil 清屏语义 + +**触发与证据。** retained beginBuild 明确请求 depth=1、stencil=0;[Metal offscreen bind](../../src/mbgl/mtl/offscreen_texture.cpp#L62) 新建 descriptor 并挂接 color/depth/stencil 纹理。[Metal RenderPass 构造](../../src/mbgl/mtl/render_pass.cpp#L13) 只处理 clearColor,没有读取 clearDepth 或 clearStencil;[GL 对应构造](../../src/mbgl/gl/render_pass.cpp#L14) 把这些请求交给 clear。调用链为 Renderer → cache.beginBuild → gfx descriptor → Metal RenderPass → MTL descriptor,丢失发生在最后的转换。 + +**所有权与反例。** 附件属于 offscreen texture,pass/command encoder 管理本次命令。创建和销毁附件均不等于每次按请求清屏。主平台 descriptor 可自行配置清屏,不能证明新建离屏 descriptor 已正确配置;无 depth/stencil 的 pass 不受这两个字段影响。未使用外部 SDK 默认值推断确定黑屏,也未把未显式设置 StoreAction 直接定性为丢帧。模拟器 stencil attachment 分支另需设备验证。 + +**影响/修复/验证。** 确定的是 gfx 请求未落实,裁剪/深度表现 NEEDS_RUNTIME。统一转换各附件的 clear/load/store 语义,分别覆盖有无 clear 值、有无附件及模拟器组合格式。用非默认 depth/stencil clear 值做离屏后续比较测试,并核对 GPU capture;不得以 color 正常替代 depth/stencil 验证。 + +### B03 — Metal 离屏 present 中存在同步等待 + +**触发与链。** [RenderTarget::render](../../src/mbgl/renderer/render_target.cpp#L71) 在每个 target 绘制结束后 present;[Metal command encoder](../../src/mbgl/mtl/command_encoder.cpp#L29) 调 resource.swap;[OffscreenTexture::swap](../../src/mbgl/mtl/offscreen_texture.cpp#L86) commit 后执行 waitUntilCompleted,再重置命令缓冲。粒子稳态在[目标注册](../../src/mbgl/renderer/layers/render_particle_validation_layer.cpp#L2097) 中至少包含 StateWrite 和 TrailWrite,初始化还有额外读目标;retained 构建也有 present。 + +**所有权、边界和反例。** 等待保护命令/资源完成,是确定同步点,不是已测出的长时间阻塞。仅实际执行这些目标的帧计入;停绘、未激活层不计,retained 稳定复用不计新的 cache build 等待,GL 不经过此 Metal 函数。完成/销毁安全不能通过简单删 wait 保证。 + +**修复/最小验证。** 研究同队列命令依赖与完成回调持有资源,合并或延后提交,保留读写顺序及销毁保护。Metal capture 记录单粒子层每帧 command buffer、wait 次数和 CPU 等待时长,再测多粒子层、首帧、retained 重建及移除层;性能收益 NEEDS_RUNTIME。 + +### B04 — 内容失效与资源释放绑定 + +**触发与链。** 相机矩阵/内容 revision 改变而 viewport 尺寸不变。[Renderer revision 更新](../../src/mbgl/renderer/renderer_impl.cpp#L583) → [cache evaluate](../../src/mbgl/renderer/retained_underlay_cache.cpp#L201) → releaseResources → [prepareResources](../../src/mbgl/renderer/retained_underlay_cache.cpp#L244) 重新创建离屏资源。[releaseResources](../../src/mbgl/renderer/retained_underlay_cache.cpp#L365) 清掉纹理、合成组及资源键。 + +**正常/错误/销毁与反例。** 缓存对象持有资源,失效已实际释放所有权;稳定 valid 分支会 Reuse,不能称每帧都分配。viewport、格式、context 改变或最终销毁确需释放;资源创建异常已有 fallback。本 finding 针对同尺寸、同 context 的内容失效成本,不主张旧图继续有效。 + +**修复/验证。** 分开“内容有效性”和“附件可复用性”,保留匹配资源键的附件并重绘。固定 viewport 平移/缩放,对比 offscreen 创建次数、峰值显存、帧时间;再覆盖 resize/context 重建,防止错误跨 context 复用。成本路径已证实,分配耗时与收益 NEEDS_RUNTIME。 + +### B05 — 时间更新重复物化整个场 + +**触发与链。** Host 高频 setEncodedForecastTime,包括相同非零时间。[setRequestedForecastTime](../../src/mbgl/style/sources/raster_array_source.cpp#L453) → refreshV4RendererState;[clock setter](../../src/mbgl/particle/field/field_runtime.cpp#L196) 不因同值返回 false;[refresh](../../src/mbgl/style/sources/raster_array_source.cpp#L496) 先物化两类投影,再用 sameProjection 比较。[validity](../../src/mbgl/particle/field/field_projection_materialization.cpp#L69) 展开 bitset,[vectorSnapshot](../../src/mbgl/particle/field/field_projection_materialization.cpp#L105) 遍历向量;颜色 UV 与粒子各有自己的 T0/T1 物化。[sameProjection](../../src/mbgl/style/sources/raster_array_source.cpp#L65) 还会比较实际数组。 + +**所有权和反例。** 新数组在临时共享快照中,比较相同后可释放;发布新 Impl 时旧读者继续持有旧快照。无投影、无绑定、无法解出窗口会少做或跳过相关分支。相同内容可避免发布或 GPU 重传,故不等于每次都上传纹理;每个动画帧也不一定调用该时间 setter。完整性校验是必要成本,问题是同内容重复重建。 + +**成本/修复/验证。** N 个格点、两个时间面、颜色 UV 与粒子同时激活时,四份 vector+valid 基础数组约为 36N 字节的新逻辑存储,另有比较/校验遍历和旧快照重叠。缓存以完整不可变内容身份(含两分量、validity、grid、解释规则)为键,将 mix/time 与数组物化分离。统计同值时间、仅 mix 变化、跨窗、修订替换时的分配与上传次数;不得牺牲 B13 所需的完整身份。 + +### B06 — encoded PLAY 没有接入 forecast clock 推进 + +**触发与链。** 提交两帧、绑定、设置时间后,经公开 API 切 PLAY 并等待 repaint。[Source 播放设置及 advanceForecastClock](../../src/mbgl/style/sources/raster_array_source.cpp#L459) 中,播放设置只改 clock 状态,推进由独立方法完成。对 src/include/platform/RN 生产路径检索 advanceForecastClock 仅有[声明](../../include/mbgl/style/sources/raster_array_source.hpp#L240)和定义,没有调用方。Android [repaint timer](../../platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java#L225) 与 iOS [timer](../../platform/ios/src/MLNParticleFieldRuntime.mm#L147) 只触发重绘;粒子[读取 v4 mix/cadence](../../src/mbgl/renderer/layers/render_particle_validation_layer.cpp#L1262) 不推进该 clock。 + +**正常/错误/销毁与反例。** clock 由 Source 所有;平台 timer 的生命周期清理不提供时间增量。[advance 实现](../../src/mbgl/particle/field/field_runtime.cpp#L209) 本身存在且处理溢出;[测试](../../test/source/raster_array_v4_host_api.test.cpp#L297) 直接手动调用,所以不能证明公开 PLAY 可自行推进。Host 持续设置 forecast time、C++ 调用方自行 advance、旧 sourceTimeline 路径均是反例;粒子在固定场中移动也不代表预报时刻在播放。 + +**修复/验证。** 为 encoded clock 指定唯一推进所有者和单调时钟来源,处理暂停/恢复、rate、地图移除、颜色单层及多层,避免每层重复推进。仅调用一次 setTime 和 PLAY,观察 requestedForecastTime/mix 随 elapsed 改变;暂停保持、恢复不跳跃、不同 rate 成比例,窗口末端行为明确。公开路径缺口可静态确定,运行表现 NEEDS_RUNTIME。 + +### B07 — 零值在 8-bit 场纹理中变成负值 + +**触发与证据。** 有效向量为零或接近抵消。[粒子编码](../../src/mbgl/renderer/layers/render_particle_validation_layer.cpp#L174) 用 uint8 转换 `(x / scale * 0.5 + 0.5) * 255`,scale 至少为 1;[颜色编码](../../src/mbgl/renderer/layers/render_weather_color_layer.cpp#L45) 同类处理;[GL 解码](../../shaders/particle_gpu_validation.fragment.glsl#L76) 还原为 `(channel * 2 - 1) * scale`,Metal [解码](../../include/mbgl/shaders/mtl/particle_gpu_validation.hpp#L48) 同理。x=0 被截为 127,解码为 `-scale/255`,两个零分量都带偏差,最小 scale=1 时向量长度也高于 1e-5 的近零阈值。 + +**边界和反例。** 场快照的 CPU float 正确不代表上传表示正确;无效点被 valid 分支拒绝,不计入这个有效零场问题。不同数值/缩放下误差相对大小不同;方向归一化可放大近零偏差。理论截断误差小于 `2*scale/255`,这是假定合法编码区间的算术界,不是 GPU 实测。重用或销毁纹理不会改变表示误差。 + +**修复/验证。** 选取可精确表达零的带符号编码或 float/half 表示,并评估纹理兼容性;仅改四舍五入仍无法让奇数 255 的对称映射精确表示零。以有效零场、微小速度、正负对消、纯东/北场做 GPU 解码读回和粒子位移检查,GL/Metal 同测。实际漂移像素/速度 NEEDS_RUNTIME。 + +### B08 — Mercator 东向位移少一个 sec(latitude) + +**触发与链。** formal profile 使用 eastward/northward、m/s 的 U/V 语义,见[profile 校验](../../src/mbgl/particle/field/field_profile_validator.cpp#L25)。上传缩放在 shader 中还原后,[GL formal 位移](../../shaders/particle_gpu_validation.fragment.glsl#L169) 取 `dx=U*dt/C`、`dy=-V*dt/(C*cos(latitude))`;[Metal](../../include/mbgl/shaders/mtl/particle_gpu_validation.hpp#L133) 同样处理。这里 C 为地球周长,输入链未见对 U 预乘 sec(latitude)。 + +**推导、反例和边界。** Mercator x=λ/(2π)+0.5,而局部东向速度满足 dλ/dt=U/(R*cosφ),故 dx 应为 U*dt/(C*cosφ)。赤道、U=0 时不受此项影响;60° 纬度当前东向投影位移为应有值的一半。统一 visual time scale 不修复东西/南北相对畸变。此为固定输入语义下的数学缺陷,实际视觉速度、极区处理 NEEDS_RUNTIME;与资源销毁和错误 fallback 无关。 + +**修复/验证。** 在一致纬度与极区夹紧策略下对两个分量使用正确度量转换,保留既定视觉时间倍率。0°/45°/60° 的等物理速度东西向和南北向场,比较 CPU 解析预测与单步 GPU 位移;含符号、反向、极区边界与跨日期变更线。 + +### B09 — Ocean Mask 尚未形成正式共享地理输入 + +**触发与证据。** Wave/Tide/Current 需要 OCEAN_ONLY,或启用当前 mask 模式。[makeCoastalMaskFixture](../../src/mbgl/renderer/layers/render_particle_validation_layer.cpp#L281) 用格点坐标表达式生成合成海岸,formal 资源初始化[仍安装该 fixture](../../src/mbgl/renderer/layers/render_particle_validation_layer.cpp#L1314)。物化[统一写 GLOBAL](../../src/mbgl/particle/field/field_projection_materialization.cpp#L77)。[顶点 shader](../../shaders/particle_gpu_validation.vertex.glsl#L56) 的 v_world 源自 quad 坐标,粒子合成[据此采样 mask](../../shaders/particle_gpu_validation.fragment.glsl#L262);[颜色分支](../../shaders/particle_gpu_validation.fragment.glsl#L113) 在该处理前返回。 + +**所有权、正常路径与反例。** mask 纹理由粒子资源拥有,随资源重建/销毁;缺少正式地理 mask 数据入口和两类层共享采样语义。默认关闭 mask 的 GLOBAL wind 不因此错误,Host 数据 nodata 也可能已有陆地缺测,但这不等价于独立共享 Land/Ocean Mask。不能把本限制扩展为所有风场不可用。 + +**修复/验证。** 由 Host 决定业务域策略,Core 接收明确地理坐标、网格和版本的共享掩码,两层统一采样;不把 provider/下载引入 Native。加载真实小型固定海岸 mask,平移、缩放、旋转后地理边界应固定;分别验证 Wind GLOBAL 与 Wave/Current OCEAN_ONLY,以及 nodata 叠加、删除与替换。当前设计缺口已证实,设备画面 NEEDS_RUNTIME。 + +### B10 — encoded 提交在 UI 派发链同步完成重工作 + +**触发与链。** RN 提交较大 QBIN。[Android bridge](../../packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt#L310) 经 runOnUiQueueThread 定位并调用 runtime;[Java submit](../../platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java#L97) → [JNI](../../platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp#L1501) 拷贝 bytes 并同步调用 Source。iOS [withMapForTag](../../packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.mm#L105) 使用 UI block → [runtime submit](../../platform/ios/src/MLNParticleFieldRuntime.mm#L222) 拷贝并解码。V3 [CRC](../../src/mbgl/particle/ingress/qbin_v3_decoder.cpp#L66) 对字节做位循环,后续解码/校验/投影也在同步调用链内。 + +**边界、错误与反例。** Android 本地文件读取在 UI 派发之前,不能笼统写成全部文件 IO 都在 UI;小文件和 Duplicate 后的工作量不同,格式失败会提前返回。解码有大小上限,但允许的输入仍可较大。临时 bytes、decoded frame、Source 持久状态的所有权清楚;问题是所在线程及连续占用,并非已证明泄漏。未观测 ANR 或卡顿。 + +**修复/验证。** worker 只做不可变 bytes 解码/校验,带 map/style/source 世代令牌返回所有者线程提交;限制队列并处理取消、地图销毁和过时结果,不能把整个可变 Source 移到 worker。在中小/近上限输入、CRC 失败、连发及页面退出下记录 UI 区间耗时、峰值内存和错误回调。性能影响 NEEDS_RUNTIME。 + +### B11 — 每序列八帧不是全局内存预算 + +**触发与链。** 单 Source 连续提交不同有效 fieldKey。[EncodedFieldStore 成员](../../include/mbgl/particle/field/field_runtime.hpp#L151) 是 map 和 perSeriesCapacity=8;[submit](../../src/mbgl/particle/field/field_runtime.cpp#L223) 可为新 key 创建 series;[FieldSeries](../../src/mbgl/particle/field/field_runtime.cpp#L168) 只检查本序列容量。Source [提交](../../src/mbgl/style/sources/raster_array_source.cpp#L422) 不要求该 key 已绑定。V3 [输入上限](../../src/mbgl/particle/ingress/qbin_v3_decoder.cpp#L19) 限单帧 encoded/decoded bytes,不限 store 总量。 + +**所有权和反例。** store 持有共享帧,直到显式 clear/clearAll 或 Source 销毁;渲染读者可能延长旧快照寿命。单帧 decoded 上限 32 MiB 与八帧组合形成每序列最高 256 MiB 的逻辑上界,但不表示每个 profile 都能达到该值。key 数无总上界,不能据此保证 Source 总内存。Host 固定少量 key 并主动清理时可控;本轮没有 OOM 证据。 + +**修复/验证。** 在提交前计入全局 key/字节预算,包含共享去重与临时峰值;明确容量拒绝语义,保护当前绑定窗口,避免静默淘汰。循环提交多个合法 key,验证达到预算后稳定拒绝且现有窗口可用;clear 后可恢复,地图销毁后所有者释放,读者完成后最终释放。设计上界不足已证实,实际内存峰值 NEEDS_RUNTIME。 + +### B12 — 构建过程错误没有阻止 retained 缓存成功提交 + +**触发与链。** retained 范围内某 LayerGroup 在 render 时抛出被捕获的异常,observer 记录错误后正常返回。[RenderOrchestrator 遍历](../../src/mbgl/renderer/render_orchestrator.hpp#L156) 捕获异常并 onRenderError,不把失败状态返回调用方。[buildRetainedRange](../../src/mbgl/renderer/renderer_impl.cpp#L798) 在 opaque/translucent 遍历后继续 finishBuild、markBuildSucceeded;[成功标记](../../src/mbgl/renderer/retained_underlay_cache.cpp#L328) 使其可在后续稳定帧 Reuse。 + +**所有权、错误/销毁与反例。** 缓存持有可能只完成部分绘制的纹理,直到下次失效或销毁。资源准备阶段[有 catch/fallback](../../src/mbgl/renderer/retained_underlay_cache.cpp#L240),不属于本路径;无异常、observer 再抛出异常、随后 revision 立刻改变都不满足持续复用条件。此处只讨论同步可捕获 render 异常,不将异步 GPU 错误视作同一证据。 + +**修复/验证。** 让范围绘制返回成败或设置本次构建错误标记,失败不提交 valid 缓存,安全 fallback/下帧重试;保留 observer 通知,不引入每帧 GPU 等待作为错误检测。用一次性抛异常的假 LayerGroup 和正常返回的 observer,检查 BuildFailed、禁止 Reuse、下一帧成功恢复,再由 GL/Metal 场景验证画面。 + +### B13 — 粒子更新身份没有覆盖第二分量和有效性 + +**触发与可构造输入。** 同 fieldKey 的三个不同 validTime 帧使用相同 fieldID/generation/dataVersion,U 内容相同、V 依次变化(或 validity 变化),网格和 schema 兼容。此输入在[FieldSeries 新时间插入](../../src/mbgl/particle/field/field_runtime.cpp#L146) 合法:newerRevision 检查针对同一 timestamp,未要求不同时间递增 generation。窗口从 T0/T1 切到 T1/T2。 + +**完整调用链。** [materializeParticleProjection](../../src/mbgl/particle/field/field_projection_materialization.cpp#L201) 仅以 t0/t1FirstPlane 身份传给 baseSnapshot;[baseSnapshot](../../src/mbgl/particle/field/field_projection_materialization.cpp#L77) 用 fieldID+该身份组成 fieldID,保留 producer generation/dataVersion。[Source sameProjection](../../src/mbgl/style/sources/raster_array_source.cpp#L65) 比较实际数组,可正确发布新的 V/validity。但[粒子 sourceChanged](../../src/mbgl/renderer/layers/render_particle_validation_layer.cpp#L1489) 只调用[fieldIdentityChanged](../../include/mbgl/particle/field/field_data_adapter.hpp#L111),其只比 generation/fieldID/dataVersion。两个槽身份都未变时,不进入内部 sameFieldGpuContent/纹理更新分支,继续使用旧场纹理。 + +**所有权、反例和边界。** Source 新投影存在、Renderer 仍持有旧 resources.field/nextField 及纹理,直到其他身份变化、重建或销毁。每时次主动递增 generation、U 也变化、初次资源创建都能触发更新;纯 mix 变化且窗口内容确实不变应保留纹理。此 finding 不指向颜色层:颜色投影的实际内容比较是反例,已排除“颜色仅看 U 而漏掉 V”的候选。 + +**修复/最小验证。** 粒子更新门槛使用包含 U/V、validity、grid 与解释语义的完整不可变内容身份;窗口时间状态单独更新,保持相同内容可复用。采用上述三帧并让 V/validity 可见地不同,seek 跨窗,检查 CPU 选窗、GPU 上传/复用判定及读回;增加不同时间同 generation、仅 V 改变、仅有效性改变、完全相同内容四组。错误门槛可静态证明,旧场画面 NEEDS_RUNTIME。 + +## 4. 反例检查及未成案候选 + +- **颜色投影身份:排除。** Source sameProjection → sameSnapshot → sameFieldGpuContent 会比较两分量/validity 实际数组;不能仅因第一 plane 参与 fieldID 就断言颜色不更新。B13 另有粒子特定的前置门槛证据。 +- **粒子资源强引用环:排除。** [ValidationDrawableTweaker](../../src/mbgl/renderer/layers/render_particle_validation_layer.cpp#L770) 持 weak_ptr;[layerRemoved](../../src/mbgl/renderer/layers/render_particle_validation_layer.cpp#L1140) 移除目标。本轮未形成泄漏闭环。 +- **关闭缓存后辅助段状态残留:排除简单结论。** [synchronizeRetainedUnderlayState](../../src/mbgl/renderer/renderer_impl.cpp#L142) 覆盖段缓存同步;不能仅查主缓存 setter。 +- **纯重排未被处理:排除。** [Style diff](../../src/mbgl/renderer/style_diff.cpp#L19) 与[orchestrator 更新](../../src/mbgl/renderer/render_orchestrator.cpp#L318) 处理移除/添加并重建有序层;分段不是独立 Z-order 数据源。 +- **任意交错不受支持:排除笼统结论。** 单 continuous 段的 fast path 限制不等于普通 segmented path 不支持多段;[普通绘制](../../src/mbgl/renderer/renderer_impl.cpp#L938) 仍按计划遍历。透明混合、深度与遮挡整体等价性尚需运行验证。 +- **异常输入必然越界:未成案。** QBIN 有 MaxCells、字节预算、偏移/结构/CRC 检查,FieldSeries 有冲突/陈旧/兼容性拒绝。未完成形式化证明或 fuzz,不能声称完全安全,也未凭大输入符号制造越界 finding。 + +## 5. 性能与资源模型(静态模型,非 benchmark) + +设视口为 W×H,retained 段数 K,格点数 N,活跃粒子层数 L,每层有效粒子数 P,encoded 字节数 E,fieldKey 数 F。 + +| 路径 | 静态成本模型 | 稳定路径/限制 | +|---|---|---| +| retained | 每段至少一张 RGBA8 颜色纹理约 4WH 字节,另加实际 depth/stencil 附件、对齐及 drawable;总量随 K 线性增长 | 稳定内容复用;Build 时绘制该段并合成;B04 增加同尺寸分配抖动 | +| 粒子 | State A/B 随状态纹理面积增长,Trail A/B 随视口面积增长;积分和粒子绘制约随 P 增长,全屏衰减/合成随 WH 增长 | 初始化/重播种还有额外工作;不可只凭 particleCount 推断全部成本 | +| Metal 提交 | 实际执行的粒子层稳态至少两次离屏同步点/层,加当帧 retained Build 数量及其他 target | 这是显式 wait 次数下界模型,不是每次都发生长等待;需 capture 核对实际启用分支 | +| 时间 setter | 激活投影物化和数组相等比较为 O(N),颜色 UV+粒子约 36N 字节基础新数组 | 单纯 draw 并不必然调用 setter;不同 scalar/vector 绑定成本不同 | +| QBIN | CRC 和解码大体随 E/样本数增长;提交链还会临时复制并可能物化 | 上限约束单帧;失败可以提前结束,不可套用最坏值到所有输入 | +| encoded store | 每 key 最多八帧,但总量随 F 增长;另有 materialized snapshot、上传图像、在途旧读者 | 需要显式 clear;现有容量拒绝保护单序列,未构成全局预算 | + +未取得 profiler、锁竞争、allocator、GPU capture 或设备温度数据。不提供热点排名、FPS、毫秒、节省比例或确定 OOM 阈值。查到同步等待和 UI 同步调用,不等于证明死锁;本轮未形成有闭环证据的锁死 finding。 + +## 6. 测试源码覆盖与缺口 + +阅读测试源码只表示理解其断言,以下全部 NOT RUN。 + +| 已阅读的代表性测试 | 能说明的设计意图 | 不能替代的验证 | +|---|---|---| +| [retained 状态测试](../../test/renderer/retained_underlay_cache.test.cpp#L116)、[失败测试](../../test/renderer/retained_underlay_cache.test.cpp#L479)、[segment 测试](../../test/renderer/retained_underlay_cache.test.cpp#L657) | Build/Reuse/Fallback、范围与交错计划 | 真实 attachment stencil、Metal 清屏、异常经 orchestrator 传递、透明/深度像素等价 | +| [clock 测试](../../test/particle/field_runtime.test.cpp#L272)、[Host API clock](../../test/source/raster_array_v4_host_api.test.cpp#L297) | 手动 advance 的时间算法 | 平台 PLAY 是否接入生产推进者 | +| [materialization 测试](../../test/source/raster_array_v4_host_api.test.cpp#L392)、[颜色幅值](../../test/source/raster_array_v4_host_api.test.cpp#L433) | 快照不变性、mix、重建计算 | GPU 8-bit 数值误差、B13 跨窗同 producer 身份、数组分配次数 | +| [binding 测试](../../test/particle/field_binding.test.cpp#L84)、[V3 解码测试](../../test/particle/qbin_v3_decoder.test.cpp#L346) | 内容身份/选面、格式失败和边界 | 全 renderer 资源切换、UI 延迟、全局 key 预算及竞态销毁 | + +未覆盖或未闭合:Vulkan/WebGPU 完整粒子实现、所有 Style 图层组合、真实设备 Metal simulator/device 差异、GPU 厂商驱动行为、平台业务调用频率、RN 安装/codegen/ABI、所有 error string 和 promise 回调分支、跨线程竞争注入、完整 fuzz、产物字节与 manifest 的对应关系。当前审查是有边界的静态追踪,不是逐行穷尽全仓或安全证明。 + +## 7. 后续可执行验证矩阵(交给 RDC,全部 NOT RUN) + +本轮禁止执行以下构建/测试。RDC 在独立获准的验证阶段准备测试 runner 与设备;新增回归用例/修复属于后续代码任务,不能把下表当作本轮已提供可运行补丁。 + +| 项目 | 准备与执行动作 | 最小通过条件 | +|---|---|---| +| B01 | GL、Metal 各加载同瓦片 fill/line,策略 R/C/R、R/R、C/R/C;首帧/重建/复用逐帧对照关闭缓存 | attachment 切换均生成对应掩码,像素裁剪等价 | +| B02 | 离屏附件预置非默认值,创建带 depth/stencil clear 的 pass,再以比较绘制检查;device/simulator 各一次 | clear 值与请求一致,无依赖未定义旧内容 | +| B03/B04 | 1/2 粒子层、1/3 retained 段;固定视口平移 300 帧,记录 capture/分配/CPU 等待 | 给出次数、时长分布;资源复用变化有证据且无画面回归 | +| B05 | 两时间面,固定时间重复 100 次、mix 小步变化 100 次,再跨窗和修订 | 输出物化/分配/上传次数;相同内容复用且变化可靠可见 | +| B06 | 仅颜色、仅粒子、两者共存;setTime 一次后 PLAY、PAUSE、不同 rate | forecast time/mix 正确推进,每地图只推进一次 | +| B07 | 有效零场、微值、正负对消、东西南北常量场,GPU 单步读回 | 零保持零,近零阈值一致,误差有明确预算 | +| B08 | 0°/45°/60°,相同 m/s、相同 dt,比较解析 Mercator 位移 | U/V 投影度量一致,符号正确,极区有限值 | +| B09 | 同一固定地理 mask 同时供颜色/粒子,平移缩放旋转,切 GLOBAL/OCEAN_ONLY | 两层海陆边界一致且固定在地理位置 | +| B10 | 合法中小/近上限 QBIN、坏 CRC、连发、加载中移除地图 | UI 耗时可量化;回调一次;无过时提交/释放后访问 | +| B11 | 多 key 连续提交直至预算,clear 后再提交 | 可预测拒绝、峰值有界、当前窗口不破坏、最终释放 | +| B12 | 假 LayerGroup 首次抛异常,observer 记录后返回,下一帧正常 | 首次不得缓存成功;下一帧重试并恢复 | +| B13 | 同 generation/dataVersion/U 的三时次,分别仅改 V、validity,再 seek 跨窗 | 新窗口 GPU 内容正确;完全相同内容仍可复用 | +| 兼容 | 增删/重排/隐藏边界层,透明层交错、3D/custom fallback、resize/context 重建 | Style 顺序一致,fallback 可恢复,无残留目标 | + +已有 C++ 定向测试的执行入口需使用平台生成的实际 runner;[test/CMakeLists](../../test/CMakeLists.txt#L1) 的 mbgl-test 是库,[macOS 配置](../../platform/macos/macos.cmake#L84) 定义 mbgl-test-runner。RDC 已构建并确认路径后可执行下列命令(本轮未执行;路径必须由 RDC 填写): + +```sh +RDC_TEST_RUNNER=/absolute/path/to/mbgl-test-runner +"$RDC_TEST_RUNNER" --gtest_filter='Retained*:ContinuousLayerRange.*:RenderSegment*:ParticleFastPath.*:ParticleField*:ParticleQbin*:ParticleEncoded*:Source.RasterArrayV4*' +``` + +这只运行现有匹配测试,不覆盖全部上述新增场景。RDC 应先确认 runner 的过滤输出确实命中测试,保存命令、exit code、测试数量和日志路径;设备场景记录设备/后端/版本、输入 fixture、截图或 capture。实际构建目录、签名、设备 ID、安装产物及历史日志均 NOT AVAILABLE,本轮不臆造构建命令或成功状态。 + +## 8. 分批建议与停止边界 + +1. **先补正确性闭环:** B01/B02 的目标附件状态、B06 的唯一时钟推进者、B13 的完整内容身份;加入最小回归后再做跨后端运行验证。B12 同批修复失败提交语义,避免错误缓存长期复用。 +2. **再统一数值与域语义:** B07/B08 的数值表示/投影度量,以及 B09 的共享地理掩码;用解析输入和小型固定 fixture 定量验收。 +3. **最后按测量做性能与预算:** B03/B04/B05/B10/B11,先记录同步、分配、线程耗时和峰值,再分别改命令提交、资源复用、物化缓存、工作线程和容量策略;避免多项同时修改而无法归因。 + +本轮仅交付三份中文盲审文档,不修改实现或测试。完成后停止,由 RDC 冻结三份原稿 SHA-256 和时间,再独立移交、追加开发记录并做正式仓库 scoped 提交及远端核验。本会话不参与解除盲态比较,不根据外部材料回写发现。 + +配套:[Step 记录](../50_Step记录/STEP-096_V13隔离盲审_v1.md) · [完成报告](../报告/STEP-096_V13隔离盲审_Codex完成报告_v1.md) diff --git a/docs/30_设计/MapLibreParticleLab_V3通用ScalarVectorField运行时架构设计_v1.md b/docs/30_设计/MapLibreParticleLab_V3通用ScalarVectorField运行时架构设计_v1.md new file mode 100644 index 000000000..00a6973da --- /dev/null +++ b/docs/30_设计/MapLibreParticleLab_V3通用ScalarVectorField运行时架构设计_v1.md @@ -0,0 +1,117 @@ +# MapLibreParticleLab V3 通用 Scalar / Vector Field 运行时架构设计 + +状态:冻结(V3.0 Contract,尚未实现) +决策:新 canonical schema 为 `maplibre-particle-field/v2`;RN package 首个 V3 实现版至少 `13.4.1-particle.3`,Native AAR 独立升版,绝不覆盖 `.2`/`.1`。 + +## 边界与目标 + +V3 从 NavSea canonical Field submission 开始,是 provider-neutral、business-neutral 的二维时序 Scalar / Vector runtime。它不解析 NSFT/PMTiles,不做 HTTP、manifest、time_index、缓存、业务单位转换,也不以 WIND/WAVE/RAIN/TIDE 决定能力。V3.0 固定为 **0/1 scalar + 0/1 vector + 1 shared validity**,拒绝 neither;不设计 N-channel graph。 + +```text +WeatherV2 → NSFT → NavSea JS/TS(解码、拼接、单位/业务 canonical) + │ metadata + local scalar/vector file reference + ▼ +MapLibreParticleLab V3: validate → Snapshot → Source Timeline T0/T1/MIX + ├─ Generic Field Renderer (config.valueSource) + └─ Particle Renderer (config.vectorSemantics) +``` + +当前实现为 `v1 vector(U/V)+valid → Timeline → 两个各自 RGBA U/V texture renderer`;V3 仍是一份 Source snapshot,两 renderer 独立显示、独立 GPU resource,而非两份 host field。 + +## 冻结数据 Contract + +```text +FieldSnapshot + descriptor: fieldId, generation, validTime, grid, bounds, dataVersion, content hash + scalar?: Float32[width*height] // scalar plane + vector?: Float32[width*height*2], interleaved U/V // vector plane + valid: UInt8[width*height] // shared validity + vectorSemantics?: VELOCITY | DIRECTION // only when vector exists +``` + +- 所有 plane 必为 `float32-le`、NW-origin row-major;vector U=east、V=north。 +- scalar-only、vector-only、scalar+vector 合法;neither、尺寸/byteLength/finite/validity 不一致均 reject。 +- `valid` 是一个 snapshot-wide shared logical mask;V3.0 不加 channel mask。任何无效 cell 对全部现有 channel 无效。 +- `validTime` 是实际时刻,不假设固定间隔;T0/T1 必须相同 grid/bounds/channel presence/vector semantics/encoding,且都递增。 +- `fieldId` 是 opaque identity,删除 `fieldType` / `displayDomain` 对能力、adapter validation 和 compatibility 的强制依赖。领域/mask 选择若仍需要,作为 renderer config 的 business-neutral spatial policy,不能作为天气 enum。 + +### v2 JSON 与 binary plane + +```json +{ + "schema":"maplibre-particle-field/v2", "fieldId":"opaque-id", "generation":1, + "validTime":1760000000000, "dataVersion":1, + "grid":{"width":257,"height":257,"origin":"north-west","rowOrder":"north-to-south"}, + "bounds":{"west":0,"south":0,"east":1,"north":1}, + "validity":{"encoding":"uint8","byteLength":66049}, + "scalar":{"encoding":"float32-le","byteLength":264196,"noData":-9999}, + "vector":{"layout":"interleaved-uv","encoding":"float32-le","byteLength":528392,"noData":-9999,"semantics":"DIRECTION"} +} +``` + +提交 API 冻结为 `submitField(metadataJson, payloadRefs)`,其中 `payloadRefs={validityFileUri, scalarFileUri?, vectorFileUri?}`;每个 URI 独立 local file、独立限额、先验证存在性与总大小再同步读入。metadata 仍小,绝不采用 JS number[]、JSON array、Base64。v2 validity 采用显式 `uint8` plane,避免让任一 channel 的 noData 充当隐式共享 mask;Kotlin/JNI 最小修改为从单 `byte[]` 改为三个 nullable byte[](或等价 native struct),Core parser/adapter 接收 plane bundle。若实施时 JNI 参数数量/ABI 风险高,可在 Kotlin 读三文件后封装为有明确 offsets/lengths 的 **v2 binary container**;不能复用 v1 的单 payload 产生歧义。 + +## v1/v2 版本决策 + +选择 **v2**,而非 v1 additive:v1 单 `vector`、单 payload、`velocity` snapshot 和 WIND/GLOBAL validation 都是结构性约束;在同 schema 内重载 payload 会令 scalar-only 与 scalar+vector 的 bytes、validity、noData、compatibility ambiguous。 + +- v1 vector-only 继续由旧 parser/adapter 可读,作为 compatibility ingress;最薄兼容层将 v1 decode 规范化成 v2 内部 snapshot(vector=present、shared validity 从 v1 U/V noData 派生、semantics=VELOCITY)。 +- Runtime 内只使用一个 V3 snapshot/timeline 实现;允许 v1 首帧与 v2 vector-only 共同进入同 Runtime,前提是 normalize 后 descriptor/semantics/channel presence 完全 compatible。否则 fail-closed,不维护两套 runtime。 +- v1 不增加 scalar 字段;发布 `.3` 才公开 v2 `payloadRefs`,旧 `.2` ABI 可继续作为 legacy ingress,Native/AAR coordinate 独立推进。 + +## Renderer Contract(与数据分离) + +`FieldSnapshot` 只回答“有什么”;renderer config 显式回答“怎样画”: + +```text +FieldRendererConfig.valueSource = SCALAR | VECTOR_MAGNITUDE +ParticleRendererConfig.vectorSemantics = VELOCITY | DIRECTION +ParticleRendererConfig.advectionScale (finite, >0; display/config policy) +``` + +`SCALAR` 要求 scalar present;`VECTOR_MAGNITUDE` 要求 vector present;不满足即该 renderer unavailable/reject,绝不猜。`FieldColorStop.value` 保持 generic public semantics。Native 内部 `speedMps`、shader default 0..30m/s 和 speed-color 命名将在 V3-03 统一改为 generic `fieldValue`/`value`; legacy JNI ABI 可薄转换,不能改变 RN `.2`。 + +`SnapshotGpuResources` 为每一个 T0/T1 快照包含 `scalarTexture?`、`vectorTexture?`、`validityTexture` 与各 plane scale/format。rollover 把整个 bundle(而不是单 texture)从 OLD_T1 原子复用为 NEW_T0;只上传 NEW_T1 bundle。style reload/context loss 清空 renderer-owned resource,Source timeline 保留;下一帧从当前 immutable window 重建。Field ON/Particle OFF、Field OFF/Particle ON、双 ON 都继续合法;无 vector 时 Particle 不可用,不让 scalar 伪装 U/V。 + +## Vector semantics + +`VELOCITY` 保持当前 Wind/Tide 行为:T0/T1 component-wise mix,m/s 地理推进,`advectionScale=1` 默认。 + +`DIRECTION` 用 unit U/V 表示方向,不代表速度。先 component-wise mix,再 normalize;若长度非 finite 或小于冻结阈值(建议 `1e-5`)则 cell invalid、粒子不移动/respawn,fail-closed。反向邻帧在 mix 约 0.5 归零亦遵此规则,不能任选方向。实际视觉推进量只由 particle renderer 的 `advectionScale`(及现有 visual clock)控制,必须明确由宿主/renderer 配置提供,不能从 Wave height/period 或业务名称推算。本任务未定义 Wave physics。 + +## WeatherV2→NavSea→V3 映射 + +|业务输入|NavSea canonical V3|Field renderer|Particle| +|---|---|---|---| +|Wind|vector U/V, `VELOCITY`|`VECTOR_MAGNITUDE`|`VELOCITY`| +|Tide/Current|vector U/V, `VELOCITY`|`VECTOR_MAGNITUDE`|`VELOCITY`| +|Rain|scalar rainRate + shared valid|`SCALAR`|unavailable| +|Wave|scalar height + unit direction-to U/V, `DIRECTION`|`SCALAR`|`DIRECTION`| + +Wave period 留在 NavSea model/cache,不增加第二 scalar;Rain precipitation type 未写入 NSFT,V3 不推断或补造。NavSea 在拼接 257 tile 时保持 `(tiles*256)+1`,Core 不理解 tile。 + +## 受影响文件矩阵与风险 + +|区域|预计修改| +|---|---| +|Core|`field_data_adapter.*`、`local_particle_field_feed.cpp`、`raster_array_source.*`、fixtures/tests| +|shader/renderer|`render_weather_color_layer.cpp`、`render_particle_validation_layer.cpp`、`particle_gpu_validation.fragment.glsl`、UBO/manifest/generator 后端产物| +|Android|`WeatherRuntime.java`、`MapLibreMap.java`、`native_map_view.*`、JNI registration| +|RN|`types.ts`、`FieldRuntime.ts`、`NativeWeatherRuntimeBridge.ts`、Kotlin TurboModule/codegen、package version| +|Test Host|style fragment、v1 regression 与 Wind/Rain/Wave/synthetic fixtures;不引入业务逻辑到 package| + +风险:plane 增加 GPU memory、3 URI read 后 Java/C++ copy 峰值、texture precision/scale、v1/v2 ABI 共存、direction opposite interpolation、style/context lifecycle 和 timeline compatibility。缓解是 bounded total bytes、明确 channel-presence compatibility、bundle-level reuse、fail-closed 和先 Linux/Core verification。 + +## 实施批次与门槛 + +|批次|修改范围 / 不修改范围|完成门槛 / 下一入口| +|---|---|---| +|V3-02 Core Contract + Scalar Adapter|v2 descriptor/snapshot/parser、v1 normalize;不改 GPU/JNI|单元/fixture覆盖三组合、invalid/compatibility;进入 V3-03| +|V3-03 GPU + Generic Field|scalar/vector/validity bundle、`FieldValueSource`;不改 particle semantics/JNI|OpenGL Core fixture:scalar 与 magnitude 不混读;进入 V3-04| +|V3-04 Vector Semantics|DIRECTION normalize、advectionScale;不改 Wave physics|VELOCITY Wind 回归、opposite/zero fail-closed;进入 V3-05| +|V3-05 Android/JNI/RN ingress|v2 plane refs、新 `.3`,保留 v1 thin ingress|payload limits/ownership/static checks;进入 V3-06| +|V3-06 fixtures|真实 Wind/Rain/Wave,Tide 回归、wave synthetic|T0/T1/MIX/reuse/style/visibility/gap coverage;进入 V3-07| +|V3-07 Linux freeze|Core/OpenGL 精确验证与文档|双 renderer/lifecycle/perf evidence;进入唯一 Android 周期| +|V3-08 Android 综合验收|一次 AAR/APK/device;不分散小周期|Wind/Rain/Wave/Tide、三 visibility 组合、seek/play/pause/rollover/style reload、hard-gate log 与 memory/perf evidence| + +Wave synthetic fixture 固定:scalar west-low→east-high、vector 全场 north;色场必须只东西渐变,粒子必须只向北。所有 fixture还覆盖 T0/T1 scalar、T0/T1 vector、共同 MIX、OLD_T1→NEW_T0、only NEW_T1 upload、invalid/gap fail-closed。 diff --git a/docs/30_设计/MapLibreParticle_QBIN_V3_Decoder与BlockGeometry实施决策_v1.md b/docs/30_设计/MapLibreParticle_QBIN_V3_Decoder与BlockGeometry实施决策_v1.md new file mode 100644 index 000000000..a0beb7198 --- /dev/null +++ b/docs/30_设计/MapLibreParticle_QBIN_V3_Decoder与BlockGeometry实施决策_v1.md @@ -0,0 +1,68 @@ +# MapLibreParticle QBIN V3 Decoder 与 Block Geometry 实施决策 v1 + +## 1. 结论 + +STEP-086 将 `QBV3` / version 3 作为新的压缩天气场容器格式接入 MapLibre Native。生产 Decoder 固定使用 **16×16 Block Geometry**,不在运行时尝试 16×16 / 32×32 双解码,也不通过启发式猜测块尺寸。 + +既有 `QBV2` 解码链继续保留。`EncodedFieldIngress` 依据 magic 显式分流:`QBV2` → `QbinV2Decoder`,`QBV3` → `QbinV3Decoder`;version 由对应 Decoder 严格校验。解码完成后继续统一进入 `FieldProfileValidator`,不改变既有 profile contract 分层。 + +## 2. Block Geometry 决策依据 + +使用仓库既有 Current / Rain / Wave / Wind 四类真实 Golden 数据,共 T0/T1 8 帧,按 v2.2 冻结稿规则实现确定性的 C++ benchmark encoder/decoder,对 16×16 与 32×32 做相同输入对比。 + +| 指标 | 16×16 | 32×32 | 结论 | +|---|---:|---:|---| +| 数值 payload 总字节 | 2,470,419 | 2,514,288 | 32×32 增加 43,869 字节,约 +1.7758% | +| 总 block 数 | 22,704 | 5,984 | 32×32 block 更少 | +| 非空 block 数 | 19,120 | 5,190 | 32×32 block 更少 | +| C++ encode 总时间 | 218.713 ms | 200.101 ms | 32×32 约快 8.5% | +| C++ decode 总时间 | 104.121 ms | 101.451 ms | 32×32 约快 2.6% | + +8 帧合计 decode 差异约 2.67 ms,平均每帧约 0.33 ms;相比之下,32×32 对 payload 尺寸有稳定负收益,Rain 两帧的增幅尤其明显。QBIN V3 的核心目标之一是降低网络、缓存和跨 RN/Native 传输成本,因此选择 **16×16**。 + +此决定同时冻结: + +- `QbinV3Decoder::BlockWidth = 16`; +- `QbinV3Decoder::BlockHeight = 16`; +- Golden Fixture manifest 中 blockWidth/blockHeight 均为 16; +- 后续生产 Writer 必须输出相同 ABI,不得自行改为 32×32。 + +## 3. Decoder 责任边界 + +`QbinV3Decoder` 只负责把已取得的 QBV3 二进制解码成现有 `DecodedFieldFrame`。它不负责 Weather provider、HTTP、下载、重试、缓存、manifest 获取、路线调度或 React 业务逻辑。 + +实现包含: + +- 固定 Header / Descriptor 布局与 little-endian 读取; +- file size、plane count、cell count、decoded bytes 等硬上限; +- CRC32 校验; +- canonical descriptor 排序、唯一键、16-byte packing 与 padding 校验; +- VALIDITY 唯一性、长度、unused bits 校验; +- Numeric dtype 约束与 all-invalid 零长度编码; +- 16×16 block 的 Predictor + ZigZag + Rice 解码; +- Direction 的 canonical scale、环形预测与 half-circle tie 规则; +- 非法 Rice-K、截断、额外字节、非零 bit padding、残差越界等 fail-closed; +- 解码后构造既有 `DecodedFieldFrame`,继续使用既有 FieldProfileValidator。 + +## 4. Golden Fixtures + +正式 QBIN V3 Golden Fixture 位于: + +`integration/react-native/MapLibreParticleRNTestHost/assets/qbin-v3-golden/` + +覆盖: + +- current:T0 / T1,513×513; +- rain:T0 / T1,513×513; +- wave:T0 / T1,513×769; +- wind:T0 / T1,513×769。 + +`manifest.json` 固定每个文件的 bytes、SHA-256、width、height 以及 16×16 Block Geometry。校验脚本为: + +`integration/react-native/MapLibreParticleRNTestHost/scripts/verify-qbin-v3-golden-fixtures.py` + +8 个 V3 fixture 已与对应 V2 canonical raw 解码结果做逐 plane / validity byte-for-byte 对比,结果一致。 + +## 5. 兼容性与版本 + +本次不移除 QBV2,不改变 RN `submitEncodedField` 公共调用形态,不把 V3 压缩细节暴露给 JS/TS。Native / RN 发行版本统一升级为 `13.4.1-particle.9`。 diff --git a/docs/30_设计/MapLibre粒子Android发布与ReactNative包设计_v1.md b/docs/30_设计/MapLibre粒子Android发布与ReactNative包设计_v1.md new file mode 100644 index 000000000..e5d485ab6 --- /dev/null +++ b/docs/30_设计/MapLibre粒子Android发布与ReactNative包设计_v1.md @@ -0,0 +1,27 @@ +# MapLibre 粒子 Android 发布与 React Native 包设计 v1 + +## 版本与坐标 + +真实基础为 MapLibre Native Android `13.4.1`,共同 core commit 为 `18545ca10d296704b2cba675b182ff2c8da24588`,本发行只准备 Android OpenGL。正式规则为 `<上游版本>-particle.<修订号>`;首发为 `13.4.1-particle.1`。正式 npm 包为 `@navsea/maplibre-particle-rn@13.4.1-particle.1`,正式 Maven 坐标为 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`。 + +`step061` 是已经完成验证的开发阶段制品编号,不是正式发行版本。release manifest 是 `release/maplibre-particle/release-manifest.json`。Android Maven 正式仓库已确定为 `https://pbf.jpcn.ccwu.cc/maven`;npm registry 仍暂不决定。 + +## Consumer 依赖关系 + +```text +Consumer + ↓ 直接安装 +@navsea/maplibre-particle-rn + ↓ 唯一 MapLibre RN native module +@maplibre/maplibre-react-native 11.3.6 + ↓ Gradle dependency substitution +cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1 +``` + +正式包 re-export 已有 MLRN `Map`、`Camera` 等公共 API,并包含 WeatherRuntime TurboModule。它不是 MLRN 的完整 fork;`@maplibre/maplibre-react-native@11.3.6` 是正式包的 peer dependency;现代 npm 在安装正式包时自动解析到 App 根依赖层。Consumer 不应再直接声明或安装第二份 MLRN。package plugin 把 MLRN 唯一的 `org.maplibre.gl:android-sdk-opengl` 请求替换为正式 NavSea AAR,避免官方 AAR 与 Particle AAR 同时进入 APK。 + +WeatherColorLayer 与 RasterParticleLayer 是 Core-only style layer,不是 React 组件;包公开 `createParticleWeatherStyle()` 和稳定 IDs 以声明样式。field 仍只经既有 `metadataJson + local file:// payload → WeatherRuntime → RasterArraySource::submitHostField()` 进入 Native/Core;不创建第二套 ingress、Timeline 或 Runtime Window。 + +## 发布与升级边界 + +Consumer 不编译 MapLibre Native 源码,也不使用 workspace、relative source path、CMake path 或软链接。Android Native 统一从 `https://pbf.jpcn.ccwu.cc/maven` 解析;npm registry 未决定时,`.tgz` 只用于正式 npm 包的预发布 Consumer 验证。未来升级到新的上游版本时,按 `13.5.0-particle.1` 等新发行线重新完成 Native、tarball Consumer 和 APK 验证;通过后才单独创建 release tag。 diff --git a/docs/30_设计/MapLibre粒子ReactNative使用说明_v1.md b/docs/30_设计/MapLibre粒子ReactNative使用说明_v1.md new file mode 100644 index 000000000..e9cc6208c --- /dev/null +++ b/docs/30_设计/MapLibre粒子ReactNative使用说明_v1.md @@ -0,0 +1,383 @@ +# MapLibre 粒子 React Native 使用说明 v1 + +本文面向 NavSea / React Native 调用侧,说明如何通过正式包 `@navsea/maplibre-particle-rn` 使用 MapLibre Particle Android 能力。本文描述的是当前已验证的 `13.4.1-particle.1` 契约,不是 Weather Provider、下载服务或天气业务层设计。 + +## 1. 使用模型 + +React Native App 只直接依赖正式包: + +```text +@navsea/maplibre-particle-rn@13.4.1-particle.1 +``` + +Android Native 由该包的 Expo/Gradle 接线从正式 Maven 仓库解析: + +```text +cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1 +``` + +正式 Maven 仓库:`https://pbf.jpcn.ccwu.cc/maven`。 + +包会复用唯一的 `@maplibre/maplibre-react-native@11.3.6` peer dependency,并把其 OpenGL Native SDK 请求替换为 Particle AAR。App 不应再并行引入第二份 MapLibre Native AAR、Maven Local AAR、源码路径 bridge 或 MapLibre Native 源码树。 + +以后升级粒子版本的目标流程是:升级 `@navsea/maplibre-particle-rn` 版本 → npm 安装 → Gradle 取得对应正式 AAR → 重新编译 App。 + +## 2. Expo / React Native 配置 + +`app.json` / app config 中加入正式 plugin: + +```json +{ + "expo": { + "plugins": ["@navsea/maplibre-particle-rn/app.plugin"] + } +} +``` + +React 侧统一从正式包导入,不直接从官方 MLRN 或旧 bridge 导入: + +```ts +import { + Camera, + Map, + WeatherRuntime, + createParticleWeatherStyle, + particleWeatherIds, +} from "@navsea/maplibre-particle-rn"; +``` + +`WeatherColor` 与 `RasterParticle` 当前是 MapLibre Core style layer,不是 React 组件。测试或独立页面可直接使用 `createParticleWeatherStyle()`;NavSea 有自己的正式 style 时,应把同样的 `raster-array` source、`weather-color` layer 和 `raster-particle` layer 合入宿主 style,而不是在 MapView 外再画一层 Canvas/WebView。 + +## 3. Map 生命周期与 WeatherRuntime + +必须在 Map style 已完成加载后 attach Native Runtime,并在页面销毁时 detach: + +```ts +const [reactTag, setReactTag] = useState(null); + +const onStyleReady = useCallback(async (event: unknown) => { + const tag = findNodeHandle((event as { target?: unknown }).target as never); + if (tag == null) return; + await WeatherRuntime.attach(tag); + setReactTag(tag); +}, []); + +useEffect(() => () => { + if (reactTag != null) void WeatherRuntime.detach(reactTag); +}, [reactTag]); +``` + +典型 Map: + +```tsx + + + +``` + +`WeatherRuntime.getState(tag)` 返回 Native/Core 权威状态。至少两帧兼容的合法 Field 建立 initial window 后,Runtime 才进入 `READY`;React 不自行维护第二套 T0/T1/MIX Runtime Window。 + +## 4. 显示数据的总体结构 + +这是本接口最重要的部分。每一个时间片不是图片,也不是粒子坐标列表,而是一张**规则二维风矢量场**。一个 Field 由两部分组成: + +```text +metadataJson ++ +local file:// binary payload +``` + +其中 metadata 描述时间、范围、网格和编码;binary payload 只保存每个网格点的 U/V 风矢量。 + +当前正式 schema: + +```text +maplibre-particle-field/v1 +``` + +固定语义: + +```text +二维规则 grid +origin = north-west +row order = north-to-south +column order = west-to-east +storage = row-major +layout = interleaved-uv +encoding = float32 little-endian +U = eastward-positive,单位 m/s +V = northward-positive,单位 m/s +每 cell = 2 × float32 = 8 bytes +``` + +MapLibre 接收的是这张向量场;WeatherColor 根据 U/V 得到色场,RasterParticle 从同一 U/V Field 采样并推进 GPU 粒子。React 不需要提前生成每个粒子的经纬度或轨迹。 + +## 5. metadataJson 结构 + +当前真实 WeatherV2 T0 metadata 的核心结构如下: + +```json +{ + "schema": "maplibre-particle-field/v1", + "fieldId": "ecmwf-hres-wind-2026080700", + "fieldType": "WIND", + "displayDomain": "GLOBAL", + "generation": 1786060800000, + "validTime": 1786060800000, + "dataVersion": 1786060800000, + "bounds": { "west": 112.5, "south": 0.0, "east": 157.5, "north": 55.77657301866769 }, + "grid": { "width": 513, "height": 769 }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "noData": -9999.0, + "unit": "m/s", + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east", + "uPositive": "east", + "vPositive": "north" + }, + "payload": { "byteLength": 3155976 } +} +``` + +字段说明: + +| 字段 | 含义 / 当前约束 | +| --- | --- | +| `schema` | 必须为 `maplibre-particle-field/v1` | +| `fieldId` | Producer 提供的非空 Field 标识 | +| `fieldType` | 当前实现必须为 `WIND` | +| `displayDomain` | 当前实现必须为 `GLOBAL` | +| `generation` | 非 0 `uint64`;Producer 负责赋值,Core 不猜版本 | +| `validTime` | 非 0 `uint64`;同一序列使用同一时间基准并严格递增;当前 WeatherV2 使用 Unix 毫秒 | +| `dataVersion` | 可选 `uint64`,用于数据版本 / identity;当前真实 fixture 提供 | +| `bounds` | `west < east`、`south < north`,描述规则网格覆盖范围 | +| `grid.width/height` | 大于 0;总 cell 数=`width × height` | +| `vector.layout` | 只支持 `interleaved-uv` | +| `vector.encoding` | 只支持 `float32-le` | +| `vector.noData` | 必须为有限数值;当前 fixture 为 `-9999.0` | +| `payload.byteLength` | 必须严格等于 `width × height × 8` | +| `stride` | 可省略;若提供,当前 interleaved UV 必须为 8 bytes | + +真实 fixture 还带 `unit/origin/rowOrder/columnOrder/uPositive/vPositive`、`payload.sha256`、`source` 等审计字段。当前 Native parser 强制读取的是 schema、identity、时间、bounds、grid、layout、encoding、noData 与 byteLength;额外审计字段不会替代上述 canonical 语义。 + +注意:当前 `payload.sha256` 可用于文件生产/发布审计,但 `LocalParticleFieldFeed` 目前只读取 `payload.byteLength`,不要把 `sha256` 写成 Core 已强制校验的安全门槛。 + +## 6. binary payload 的精确排列 + +二维索引: + +```text +cell = y * width + x +``` + +其中 `x=0` 是最西列,`y=0` 是最北行。payload 按 row-major 连续存储:先完整保存最北一行,再向南逐行保存。 + +每个 cell 占 8 bytes: + +```text +byteOffset = cell * 8 +[byteOffset + 0 .. +3] = U,float32 little-endian +[byteOffset + 4 .. +7] = V,float32 little-endian +``` + +等价的 float 序列是: + +```text +U0, V0, U1, V1, U2, V2, ... +``` + +也就是: + +```text +velocity[2 * cell] = U +velocity[2 * cell + 1] = V +``` + +U>0 表示向东,U<0 表示向西;V>0 表示向北,V<0 表示向南。`noData` 只要出现在某 cell 的 U 或 V 任一分量,该 cell 就标记为无效。NaN/Infinity 不是 noData:当前 Adapter 会直接拒绝整张 Field。 + +真实 WeatherV2 fixture: + +```text +width = 513 +height = 769 +cells = 513 × 769 = 394,497 +bytes = 394,497 × 8 = 3,155,976 +``` + +所以 `payload.byteLength` 必须正好是 `3155976`;少 1 byte、多 1 byte 或 metadata 声明与实际文件不一致都会 fail-closed。 + +一个 2×2 示例: + +```text +北行: NW(U=5,V=1) NE(U=6,V=1) +南行: SW(U=4,V=0) SE(U=5,V=0) +``` + +payload 中的 float 顺序必须是: + +```text +5,1, 6,1, 4,0, 5,0 +``` + +总长度为 `2 × 2 × 8 = 32 bytes`。 + +React/Node 侧若自己生成 canonical payload,可用 `DataView.setFloat32(offset, value, true)`;最后一个参数必须为 `true`,明确写成 little-endian。业务层也可以从服务器取得已经生成好的 `.f32` 文件,不要求 JS 重新编码。 + +## 7. React 侧如何提交一帧 + +当前正式 bridge 不把大块 U/V 数据变成 JS `number[]`。React 侧准备好 metadata JSON 和本地文件 URI 后直接提交: + +```ts +const result = await WeatherRuntime.submitWindField( + reactTag, + JSON.stringify(metadata), + payloadFileUri, // 必须是 local file:// URI +); +``` + +测试 Host 使用 `expo-asset` 把随 App 打包的 `.f32` materialize 到本地 cache: + +```ts +const asset = Asset.fromModule(require("./field.f32")); +await asset.downloadAsync(); +const payloadFileUri = asset.localUri; +``` + +正式 NavSea 不要求数据必须来自 Expo Asset。可以由天气业务层下载、转换或合成 canonical U/V Field,然后保存到 App 可读的本地文件,再把 `file://` URI 交给 `WeatherRuntime`。Native 读取 bytes 后进入唯一的 `RasterArraySource::submitHostField()` / Adapter / Snapshot / Timeline 路径。 + +不要使用以下正式 transport: + +```text +number[] +JSON float array +base64 +hex string +每 cell 一个 {lat,lon,u,v} 对象 +React 每帧传粒子位置 +``` + +这些都会扩大 JS/Native 开销并破坏当前已冻结的 binary Field 边界。 + +## 8. 多时间片与 Timeline + +至少提交两个彼此兼容、时间递增的 Field,Core 才能建立 initial window: + +```text +T0 → ACCEPTED_PENDING_INITIAL_WINDOW +T1 → INITIAL_WINDOW_READY +T2 → ACCEPTED_NEXT_SNAPSHOT +T3 → ACCEPTED_NEXT_SNAPSHOT +``` + +随后: + +```ts +const state = await WeatherRuntime.getState(reactTag); +// 预期 state.state === "READY" +``` + +同一时间序列必须保持相同的 grid 尺寸、bounds、noData、fieldType 和 displayDomain,`validTime` 必须使用同一时间基准并严格递增。当前真实 WeatherV2 验收使用 2026-08-07 00/06/12/18 四帧,每帧相隔 6 小时。 + +React 可以用: + +```ts +await WeatherRuntime.setForecastTime(reactTag, targetTimestampMs); +await WeatherRuntime.setPlaybackState(reactTag, "PLAY"); +await WeatherRuntime.setPlaybackSpeed(reactTag, "NORMAL"); +``` + +但 Timeline cursor、T0/T1 选择、时间插值、Runtime Field Window 和 GPU Field 切换仍由 Core 管理;React 只发低频控制命令,不在 JS 中复制一套播放状态机。 + +## 9. 显示与视觉控制 + +当前公开控制包括: + +```text +setWeatherEnabled +setWeatherColorVisible +setParticleVisible +setTargetFps +setWeatherColorStops +setParticleColor +setTrailColor +setParticleCount +setParticleLineWidth +setParticleLength +setParticleOpacity +setTrailOpacity +setTrailFade +setWeatherColorOpacity +``` + +例如: + +```ts +await WeatherRuntime.setWeatherColorVisible(reactTag, true); +await WeatherRuntime.setParticleVisible(reactTag, true); +await WeatherRuntime.setParticleCount(reactTag, 4096); +await WeatherRuntime.setParticleLineWidth(reactTag, 1.5); +await WeatherRuntime.setParticleOpacity(reactTag, 0.9); +await WeatherRuntime.setTrailOpacity(reactTag, 0.8); +await WeatherRuntime.setTrailFade(reactTag, 0.94); +await WeatherRuntime.setWeatherColorOpacity(reactTag, 0.62); +``` + +这些参数只控制显示,不改变 U/V canonical 数据结构。天气业务数据和渲染视觉参数必须分开管理。 + +## 10. 当前 Native 会拒绝哪些数据 + +当前 Adapter 采用 fail-closed。以下情况会拒绝 Field,而不是尽量猜测: + +- schema 不是 `maplibre-particle-field/v1`; +- `fieldId/fieldType/displayDomain` 缺失或类型错误; +- `generation` 或 `validTime` 为 0; +- width/height 为 0; +- `west >= east` 或 `south >= north`; +- `noData` 不是有限数值; +- layout 不是 `interleaved-uv`; +- encoding 不是 `float32-le`; +- stride 提供但不是 8; +- metadata `byteLength` 不等于 `width × height × 8`; +- 实际文件长度与 expected byte length 不一致; +- payload 中包含 NaN/Infinity; +- 当前 fieldType 不是 `WIND`; +- 当前 displayDomain 不是 `GLOBAL`; +- 时间序列不满足既有 compatibility / monotonicity 规则。 + +因此 React 侧不要通过容错转换掩盖 producer 错误。Producer/天气业务层应在提交前完成 canonical 化并保持 Field identity、时间和二进制长度稳定。 + +## 11. 当前已验证的真实数据基线 + +Package Consumer 已使用 ECMWF HRES WeatherV2 真实数据完成 Android 实机验证:4 帧 `2026080700 / 06 / 12 / 18`,每帧 `513×769`、`3,155,976 bytes`,bounds=`112.5,0.0 → 157.5,55.77657301866769`,transport=`FILE_REF / FLOAT32_LE / INTERLEAVED_UV`。用户确认正式 package Consumer APK 正常。 + +## 12. NavSea 侧责任边界 + +推荐保持以下分工: + +```text +React Native / NavSea 天气业务层 + 负责:取数、Route/ETA、多地点/多时间合成、canonical U/V 生成、文件缓存、提交时机 + ↓ metadataJson + local file:// field +@navsea/maplibre-particle-rn + ↓ +MapLibre Native / Core + 负责:校验、Snapshot、Timeline、时间插值、空间采样、WeatherColor、Particle、GPU 资源与渲染 +``` + +MapLibre Native 不理解 provider、HTTP、WeatherV2、Route、ETA 或业务缓存;React 不接管 GPU 粒子模拟和 Core Timeline。 + +## 13. 版本升级检查 + +每次升级 `@navsea/maplibre-particle-rn` 后,至少确认: + +1. npm 根层只有一份 `@maplibre/maplibre-react-native`,没有嵌套 native module; +2. Gradle `dependencyInsight` 最终解析到对应 `cc.navsea.maplibre:android-sdk-particle-opengl:`; +3. 不出现旧 `step061`、Maven Local 或源码路径; +4. Package Consumer 用真实 T0–T3 Field 达到 Runtime `READY`; +5. WeatherColor 与 Particle 均正常显示,地图交互无异常; +6. Consumer 通过后再升级 NavSea 的包版本。 + +正式参考实现:`integration/react-native/MapLibreParticlePackageConsumerTestHost/`。它的意义不是演示 UI,而是验证“正式 npm package → 正式 Maven AAR → 真实 canonical WeatherV2 → Core/Renderer”的完整消费链。 diff --git a/docs/30_设计/MapLibre粒子ReactNative使用说明_v2.md b/docs/30_设计/MapLibre粒子ReactNative使用说明_v2.md new file mode 100644 index 000000000..cd822ce79 --- /dev/null +++ b/docs/30_设计/MapLibre粒子ReactNative使用说明_v2.md @@ -0,0 +1,965 @@ +# MapLibre 粒子 React Native 使用说明 v2 + +本文面向使用 `@navsea/maplibre-particle-rn` 的 React Native / Expo 调用方,描述当前 `13.4.1-particle.1` 的真实公共接口、参数结构、安装方式与调用顺序。 + +本文只说明已经存在的公开 API。`@navsea/maplibre-particle-rn` 会完整 re-export `@maplibre/maplibre-react-native@11.3.6`;上游 MapLibre React Native 的全部通用 Map API 不在本文重复展开,本文重点说明 NavSea Particle 新增能力。 + +## 1. 当前发布物 + +React Native 包名: + +```text +@navsea/maplibre-particle-rn +``` + +当前版本: + +```text +13.4.1-particle.1 +``` + +当前采用静态 HTTPS tarball 发布,不使用 npm Registry,也不需要 `.npmrc`: + +```text +https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.1.tgz +``` + +Android Native AAR 独立位于 Maven 仓库: +```text +cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1 +https://pbf.jpcn.ccwu.cc/maven/ +``` + +## 2. 安装 + +新 React Native / Expo 项目建议一次安装正式 tarball 和固定的 MapLibre React Native peer dependency: + +```bash +npm install https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.1.tgz @maplibre/maplibre-react-native@11.3.6 +``` + +安装后,`package.json` 中 `@navsea/maplibre-particle-rn` 可能记录为完整 HTTPS URL,这是当前预期行为。代码中仍然使用正式包名导入: + +```ts +import { Map, Camera, WeatherRuntime } from "@navsea/maplibre-particle-rn"; +``` + +当前 peer dependency 契约: + +```text +@maplibre/maplibre-react-native = 11.3.6 +react >= 19.1.0 +react-native >= 0.80.0 +``` + +不要同时再安装另一份自定义 MapLibre Native AAR、旧 STEP AAR、源码路径 bridge 或第二份 MLRN native module。 +## 3. Expo 配置 + +Expo / Expo Prebuild 项目必须在 `app.json` 或 app config 中加入: + +```json +{ + "expo": { + "plugins": ["@navsea/maplibre-particle-rn/app.plugin"] + } +} +``` + +这个 plugin 会完成两件事: + +1. 向 Android Gradle 加入 `https://pbf.jpcn.ccwu.cc/maven`; +2. 把官方 `org.maplibre.gl:android-sdk-opengl` 替换为 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`,避免 APK 中同时进入官方 AAR 与 Particle AAR。 + +首次接入、plugin/AAR 版本变化或 Android Native 代码变化后,需要重新生成/编译 Android 原生工程。仅 JS/TS 逻辑变化时,已有包含正确 Native bridge/AAR 的 Debug APK 可以继续通过 Metro 验证新 JS。 + +Bare React Native 如果不使用 Expo config plugin,必须在宿主 Android Gradle 中等价完成 Maven repository 与 dependency substitution;不能只依赖 autolinking。 + +## 4. 公共导入入口 + +包根入口当前导出: + +```ts +export * from "@maplibre/maplibre-react-native"; +export { default as WeatherRuntime } from "./NativeWeatherRuntimeBridge"; +export * from "./weatherV2"; +``` + +因此调用方统一从一个入口导入即可: +```ts +import { + Map, + Camera, + WeatherRuntime, + WeatherV2HttpClient, + loadWeatherV2WindIntoRuntime, + decodeWeatherV2WindFrame, + createParticleWeatherStyle, + particleWeatherIds, + type ColorStop, + type RuntimeSnapshot, + type WeatherV2WindContext, + type LoadWeatherV2WindResult, +} from "@navsea/maplibre-particle-rn"; +``` + +`Map`、`Camera` 以及其他通用 MapLibre React Native API 都来自上游 `@maplibre/maplibre-react-native@11.3.6`,只是由本包重新导出。本文不重新定义这些上游组件的全部 Props。 + +## 5. 固定 Weather 对象 ID + +```ts +particleWeatherIds = { + source: "rn-weather-fields", + weatherColorLayer: "rn-weather-color", + rasterParticleLayer: "rn-weather-particles", +}; +``` + +`WeatherRuntime.attach()` 会按这三个固定 ID 查找 source/layer。正式宿主 style 如果不用 `createParticleWeatherStyle()`,也必须提供同名对象。 +## 6. `createParticleWeatherStyle()` + +签名: + +```ts +createParticleWeatherStyle( + overrides?: Record, +): string +``` + +返回 MapLibre Style JSON 字符串。默认包含: + +```text +source: rn-weather-fields / raster-array / host-field-submission=true +layer : rn-weather-color / weather-color +layer : rn-weather-particles / raster-particle +底图 : GSI standard raster +``` + +最小调用: + +```tsx + + + +``` + +`overrides` 当前是**顶层浅覆盖**,不是深度 merge。若传入 `sources` 或 `layers`,会覆盖 helper 原来的整个顶层字段,因此正式 NavSea style 更推荐显式把 Particle source/layers 合入宿主 style,而不是依赖复杂 overrides。 + +## 7. Map 生命周期与 `reactTag` + +`WeatherRuntime` 是 per-MapView Native facade。所有方法第一个参数都是当前 MapView 的正整数 `reactTag`。必须在 Map style 完成加载以后取得 tag 并 attach。 +```tsx +const [reactTag, setReactTag] = useState(null); + +const onStyleReady = useCallback(async (event: unknown) => { + const tag = findNodeHandle((event as { target?: unknown }).target as never); + if (tag == null) return; + + const state = await WeatherRuntime.attach(tag); + setReactTag(tag); + console.log(state); +}, []); + +useEffect(() => { + return () => { + if (reactTag != null) { + void WeatherRuntime.detach(reactTag).catch(() => undefined); + } + }; +}, [reactTag]); +``` + +Map: + +```tsx + + + +``` + +`attach()` 前必须满足 style、`rn-weather-fields`、`rn-weather-color`、`rn-weather-particles` 都已存在,否则 Promise 会 reject。页面销毁时应调用 `detach()`;MapView 自身从 Window detach 时 Native 也有兜底 dispose。 +## 8. WeatherRuntime 公共类型 + +```ts +type RuntimeState = + | "DISABLED" + | "WAITING_FOR_FIELDS" + | "READY" + | "ERROR"; + +type PlaybackState = "PLAY" | "PAUSE"; +type TargetFps = "AUTO" | "FPS_30" | "FPS_60"; +type CommandResult = "ACCEPTED" | "REJECTED"; + +type RuntimeSnapshot = Readonly<{ + state: RuntimeState; + playbackState: PlaybackState; + targetFps: TargetFps; + disposed: boolean; +}>; +``` + +Field 提交结果: + +```ts +type FieldSubmitResult = + | "REJECTED_SOURCE_MODE" + | "REJECTED_ADAPTER" + | "REJECTED_INCOMPATIBLE" + | "REJECTED_NON_MONOTONIC" + | "REJECTED_CAPACITY" + | "ACCEPTED_PENDING_INITIAL_WINDOW" + | "INITIAL_WINDOW_READY" + | "ACCEPTED_NEXT_SNAPSHOT" + | "DUPLICATE" + | "UNKNOWN"; +``` + +注意:Native 日志内部可能打印更详细的 C++ reason 名称;React API 应以这里的 `FieldSubmitResult` 字符串为准。 +## 9. WeatherRuntime 方法总表 + +除 `detach()` 外,方法返回值均通过 Promise 返回。Bridge 参数错误、Map/style/runtime 不存在等情况会 Promise reject;Core 正常处理但拒绝业务命令时通常返回 `"REJECTED"` 或对应 `FieldSubmitResult`。 + +| 方法 | 参数 | 返回 | 用途 | +| --- | --- | --- | --- | +| `attach(reactTag)` | `reactTag:number` | `Promise` | 为当前 MapView 建立 WeatherRuntime | +| `detach(reactTag)` | `reactTag:number` | `Promise` | dispose 当前 MapView Runtime | +| `getState(reactTag)` | `reactTag:number` | `Promise` | 读取 Native 权威状态 | +| `clearWindFields(reactTag)` | `reactTag:number` | `Promise` | 清空 Host Field/Timeline | +| `submitWindField(reactTag, metadataJson, payloadFileUri)` | tag + JSON + 本地文件 | `Promise` | 提交一张 canonical 风场 | +| `setWeatherEnabled(reactTag, enabled)` | `boolean` | `Promise` | 总开关 | +| `setWeatherColorVisible(reactTag, visible)` | `boolean` | `Promise` | 场图显示开关 | +| `setParticleVisible(reactTag, visible)` | `boolean` | `Promise` | 粒子显示开关 | +| `setTargetFps(reactTag, target)` | `AUTO/FPS_30/FPS_60` | `Promise` | Runtime 绘制 cadence | +| `setForecastTime(reactTag, timestampMs)` | epoch ms | `Promise` | seek 到目标预报时间 | +| `setPlaybackState(reactTag, playback)` | `PLAY/PAUSE` | `Promise` | 播放/暂停 Timeline | +| `setPlaybackSpeed(reactTag, speed)` | `HALF/NORMAL/DOUBLE` | `Promise` | 0.5x/1x/2x 播放速度 | +| `setWeatherColorStops(reactTag, stops)` | `ColorStop[]` | `Promise` | 风速→场图颜色 | +| `setParticleColor(...)` | RGBA | `Promise` | 粒子颜色 | +| `setTrailColor(...)` | RGBA | `Promise` | 尾迹颜色 | +| `setParticleCount(reactTag, count)` | 非负整数 | `Promise` | 粒子数量 | +| `setParticleLineWidth(reactTag, value)` | finite number | `Promise` | 粒子线宽 | +| `setParticleLength(reactTag, value)` | finite number | `Promise` | 粒子长度 | +| `setParticleOpacity(reactTag, value)` | finite number | `Promise` | 粒子透明度 | +| `setTrailOpacity(reactTag, value)` | finite number | `Promise` | 尾迹透明度 | +| `setTrailFade(reactTag, value)` | finite number | `Promise` | 尾迹衰减 | +| `setWeatherColorOpacity(reactTag, value)` | finite number | `Promise` | 场图整体透明度 | +### 9.1 `attach / detach / getState / clearWindFields` + +```ts +await WeatherRuntime.attach(reactTag); +await WeatherRuntime.getState(reactTag); +await WeatherRuntime.clearWindFields(reactTag); +await WeatherRuntime.detach(reactTag); +``` + +`reactTag` 必须是正整数并解析到当前 Fabric `MLRNMapView`。`clearWindFields()` 成功后 Runtime 回到 `WAITING_FOR_FIELDS`;已有 Timeline 和当前 Field 会被清空。 + +`setWeatherEnabled(false)` 会隐藏 WeatherColor/Particle 并清空 Field,Runtime 进入 `DISABLED`。重新 `setWeatherEnabled(true)` 后进入 `WAITING_FOR_FIELDS`,需要重新提交至少 T0/T1。 + +### 9.2 `submitWindField()` + +```ts +const result = await WeatherRuntime.submitWindField( + reactTag, + metadataJson, + payloadFileUri, +); +``` + +参数: + +- `reactTag`:正整数 MapView tag; +- `metadataJson`:非空 UTF-8 JSON,Bridge 上限 1 MiB; +- `payloadFileUri`:`file://` URI,或 Android 可读的绝对本地路径;不能使用 HTTP URL; +- payload 文件必须可读、非空,Bridge 上限 64 MiB。 + +正式数据路径不使用 JS `number[]`、base64 或逐 cell 对象。 +严格时间序列典型返回: + +```text +T0 -> ACCEPTED_PENDING_INITIAL_WINDOW +T1 -> INITIAL_WINDOW_READY +T2 -> ACCEPTED_NEXT_SNAPSHOT +T3 -> ACCEPTED_NEXT_SNAPSHOT +``` + +至少 T0/T1 两张兼容 Field 建立 initial window 后,Runtime 才进入 `READY`。 + +### 9.3 显示开关 + +```ts +await WeatherRuntime.setWeatherEnabled(reactTag, true); +await WeatherRuntime.setWeatherColorVisible(reactTag, true); +await WeatherRuntime.setParticleVisible(reactTag, true); +``` + +语义区别: + +- `setWeatherEnabled`:整个 Weather Runtime 的总开关;关闭时同时清空 Field; +- `setWeatherColorVisible`:只控制背景 WeatherColor 场图; +- `setParticleVisible`:只控制运动粒子层。 + +### 9.4 时间与播放 + +```ts +await WeatherRuntime.setForecastTime(reactTag, timestampMs); +await WeatherRuntime.setPlaybackState(reactTag, "PLAY"); +await WeatherRuntime.setPlaybackSpeed(reactTag, "NORMAL"); +await WeatherRuntime.setTargetFps(reactTag, "AUTO"); +``` + +`timestampMs` 必须是非负 Unix epoch 毫秒整数。播放速度允许 `HALF`、`NORMAL`、`DOUBLE`;Target FPS 允许 `AUTO`、`FPS_30`、`FPS_60`。React 只设置目标时间/播放状态,T0/T1 选择、MIX 和 GPU 时间插值由 Native/Core 管理。 +### 9.5 WeatherColor 色阶 + +类型: + +```ts +type ColorStop = Readonly<{ + speedMps: number; + red: number; + green: number; + blue: number; + alpha: number; +}>; +``` + +调用: + +```ts +await WeatherRuntime.setWeatherColorStops(reactTag, [ + { speedMps: 0, red: 0.08, green: 0.20, blue: 0.80, alpha: 0.72 }, + { speedMps: 8, red: 0.00, green: 0.85, blue: 0.90, alpha: 0.72 }, + { speedMps: 16, red: 1.00, green: 0.85, blue: 0.05, alpha: 0.72 }, + { speedMps: 30, red: 0.90, green: 0.08, blue: 0.08, alpha: 0.72 }, +]); +``` + +约束: + +- 当前 Core capacity 为最多 8 个 stop;至少 2 个; +- `speedMps` 必须 finite 且严格递增,单位 m/s; +- RGBA 必须 finite 且全部在 `[0,1]`; +- 两个 stop 之间由 Shader 做线性颜色插值; +- 小于第一个 stop 使用第一个颜色,大于最后一个 stop 使用最后一个颜色。 + +例如若 0 m/s 与 2 m/s 两个 stop 使用相同 RGBA,则 0~2 m/s 会保持同色。 +### 9.6 粒子与尾迹视觉参数 + +```ts +await WeatherRuntime.setParticleColor(reactTag, 1, 1, 1, 0.95); +await WeatherRuntime.setTrailColor(reactTag, 0.35, 0.85, 1, 0.8); +await WeatherRuntime.setParticleCount(reactTag, 4096); +await WeatherRuntime.setParticleLineWidth(reactTag, 1.5); +await WeatherRuntime.setParticleLength(reactTag, 0.012); +await WeatherRuntime.setParticleOpacity(reactTag, 0.9); +await WeatherRuntime.setTrailOpacity(reactTag, 0.8); +await WeatherRuntime.setTrailFade(reactTag, 0.94); +await WeatherRuntime.setWeatherColorOpacity(reactTag, 0.62); +``` + +硬性 Bridge 参数约束: + +- `setParticleColor` / `setTrailColor`:RGBA 各分量 `[0,1]`; +- `setParticleCount`:0 到 Android `Int.MAX_VALUE` 的整数; +- line width / length / opacity / fade / WeatherColor opacity:Bridge 要求 finite number,最终是否接受仍由 Native style property 决定。 + +上面的 `4096 / 1.5 / 0.012 / 0.9 / 0.8 / 0.94 / 0.62` 是当前 Consumer Test Host 的验证值,不是所有参数的固定默认契约。 + +WeatherColor 色阶、粒子颜色、尾迹颜色是三套独立视觉设置: + +```text +setWeatherColorStops -> 背景风速场颜色 +setParticleColor -> 粒子本体颜色 +setTrailColor -> 粒子尾迹颜色 +``` + +## 10. canonical Wind Field 数据结构 + +MapLibre Particle 接收规则二维 U/V 向量场,而不是图片或粒子坐标数组。每个时间片由: + +```text +metadataJson + local binary payload file +``` + +组成。 +当前 canonical schema: + +```text +maplibre-particle-field/v1 +``` + +固定向量语义: + +```text +origin = north-west +rowOrder = north-to-south +columnOrder = west-to-east +storage = row-major +layout = interleaved-uv +encoding = float32-le +U = eastward-positive, m/s +V = northward-positive, m/s +noData = -9999(当前 WeatherV2 converter) +每 cell = U float32 + V float32 = 8 bytes +``` + +metadata 示例: + +```json +{ + "schema": "maplibre-particle-field/v1", + "fieldId": "ecmwf-hres-wind-2026080700", + "fieldType": "WIND", + "displayDomain": "GLOBAL", + "generation": 1786060800000, + "validTime": 1786060800000, + "dataVersion": 1786060800000, + "bounds": { "west": 112.5, "south": 0, "east": 157.5, "north": 55.77657301866769 }, + "grid": { "width": 513, "height": 769 }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "noData": -9999, + "unit": "m/s", + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east", + "uPositive": "east", + "vPositive": "north" + }, + "payload": { "byteLength": 3155976 } +} +``` +二进制排列: + +```text +cell = y * width + x +byteOffset = cell * 8 +[offset + 0..3] = U float32 little-endian +[offset + 4..7] = V float32 little-endian + +float sequence = U0,V0,U1,V1,U2,V2,... +``` + +`payload.byteLength` 必须严格等于 `width × height × 8`。有效 U/V 不能是 NaN/Infinity;当前 WeatherV2 converter 对 invalid cell 把 U/V 写为 `-9999`。 + +同一 Timeline 内,Field 必须保持兼容 grid/bounds/语义,并且 `validTime`、`generation` 按 Native monotonicity 契约严格向前。React 不直接构造 T0/T1/MIX window。 + +## 11. `WeatherV2HttpClient` + +WeatherV2 HTTP 层负责读取服务器 manifest/time index、选择预报帧、下载 `WVECBIN1` 并转换成 canonical U/V。Native/Core 不理解 WeatherV2/provider/HTTP。 + +### 11.1 构造函数 + +```ts +type WeatherV2HttpClientOptions = Readonly<{ + baseUrl: string; + headers?: Readonly>; + fetchImpl?: WeatherV2Fetch; +}>; + +const client = new WeatherV2HttpClient({ + baseUrl: "https://weather.jpcn.ccwu.cc", +}); +``` + +`baseUrl` 必须是绝对 `http://` 或 `https://` URL。`headers` 可增加/覆盖请求 header;默认会带 `Accept: application/json, application/octet-stream`。`fetchImpl` 用于宿主注入兼容 fetch,实现测试、代理或特殊网络层。 +### 11.2 `getWindContext()` + +签名: + +```ts +client.getWindContext(options?: { + datasetId?: string; + referenceTime?: string; +}): Promise +``` + +返回: + +```ts +type WeatherV2WindContext = Readonly<{ + datasetId: string; + model: string; + referenceTime: string; + targetTimes: readonly string[]; + particleTemplate: string; + bounds?: { + west: number; + south: number; + east: number; + north: number; + }; +}>; +``` + +调用示例: + +```ts +const context = await client.getWindContext({ + datasetId: "ecmwf-hres", + referenceTime: "2026080600", +}); +``` + +两个参数都省略时,从 manifest 的 wind dataset 列表选择第一个匹配项。`referenceTime`/`targetTimes` 当前服务协议使用 `YYYYMMDDHH` 字符串。 +### 11.3 `selectWindFrames()` + +签名: + +```ts +client.selectWindFrames( + context, + options?: { + targetTimes?: readonly string[]; + count?: number; + }, +): readonly WeatherV2WindFrameRef[] +``` + +返回元素: + +```ts +type WeatherV2WindFrameRef = Readonly<{ + datasetId: string; + model: string; + referenceTime: string; + targetTime: string; + url: string; +}>; +``` + +若不传 `targetTimes`,默认从 `context.targetTimes` 开头取 `count ?? 4` 帧;`count` 必须是 `>=2` 的整数。若显式传 `targetTimes`,这些时间必须都存在于 context,并严格递增,且至少两帧。 + +```ts +const refs = client.selectWindFrames(context, { + targetTimes: ["2026080700", "2026080706", "2026080712", "2026080718"], +}); +``` + +正式 NavSea 应根据当前时间、Route/ETA 或业务策略选择 targetTimes;固定四帧只是当前回归样例。 +### 11.4 `fetchCanonicalWindFrame()` + +```ts +const field = await client.fetchCanonicalWindFrame(ref); +``` + +返回: + +```ts +type CanonicalWindField = Readonly<{ + metadataJson: string; + payload: Uint8Array; + fieldId: string; + validTime: number; + generation: number; + bounds: WeatherV2Bounds; + width: number; + height: number; + sourceUrl: string; +}>; +``` + +该方法会: + +```text +GET frame.url +-> 校验 WVECBIN1 magic/header/fields/length +-> 读取 U/V/speed/valid planes +-> 只保留 canonical U/V +-> invalid cell 写 noData=-9999 +-> 生成 maplibre-particle-field/v1 metadata +``` + +`payload` 此时仍在 JS 内存中,是 `Uint8Array`;提交 Native 前应通过 `WeatherV2BinaryFileStore` 写成临时本地文件。 + +## 12. `decodeWeatherV2WindFrame()` + +这是公开的底层 converter,适合已经自行取得 WVECBIN1 `ArrayBuffer` 的调用方: + +```ts +const canonical = decodeWeatherV2WindFrame(arrayBuffer, frameRef); +``` + +签名等价于: + +```ts +decodeWeatherV2WindFrame( + source: ArrayBuffer, + frame: WeatherV2WindFrameRef, +): CanonicalWindField +``` + +一般业务代码优先使用 `WeatherV2HttpClient.fetchCanonicalWindFrame()`,不需要直接调用 decoder。 +## 13. `WeatherV2BinaryFileStore` + +WeatherV2 helper 不绑定 Expo FileSystem。宿主只需提供: + +```ts +type WeatherV2BinaryFileStore = Readonly<{ + writeTempFile( + fileName: string, + bytes: Uint8Array, + ): Promise; + + removeTempFile?( + fileUri: string, + ): Promise; +}>; +``` + +`writeTempFile()` 必须返回 Native 可读的 `file://` URI或绝对本地路径。`removeTempFile()` 可选;helper 在每帧提交完成后 best-effort 清理。 + +Expo FileSystem 示例: + +```ts +import { File, Paths } from "expo-file-system"; + +const fileStore = { + async writeTempFile(fileName: string, bytes: Uint8Array) { + const file = new File(Paths.cache, fileName); + file.write(bytes); + return file.uri; + }, +}; +``` +## 14. `loadWeatherV2WindIntoRuntime()` + +这是 WeatherV2 最常用的高层入口。它把 HTTP 取数、转换、临时文件和 Native submission 串成一条流程。 + +参数类型: + +```ts +type LoadWeatherV2WindOptions = Readonly<{ + reactTag: number; + client: WeatherV2WindClient; + fileStore: WeatherV2BinaryFileStore; + datasetId?: string; + referenceTime?: string; + targetTimes?: readonly string[]; + count?: number; + weatherColorVisible?: boolean; + particleVisible?: boolean; + playbackState?: "PLAY" | "PAUSE"; +}>; +``` + +调用: + +```ts +const loaded = await loadWeatherV2WindIntoRuntime({ + reactTag, + client, + fileStore, + datasetId: "ecmwf-hres", + referenceTime: "2026080600", + targetTimes: ["2026080700", "2026080706", "2026080712", "2026080718"], + weatherColorVisible: true, + particleVisible: true, + playbackState: "PAUSE", +}); +``` +内部顺序固定为: + +```text +clearWindFields +-> setWeatherEnabled(true) +-> 设置 WeatherColor / Particle visibility +-> getWindContext +-> selectWindFrames +-> 逐帧 fetch + WVECBIN1 decode +-> writeTempFile +-> submitWindField +-> best-effort removeTempFile +-> setForecastTime(第一帧 validTime) +-> 可选 setPlaybackState +-> getState,要求 READY +``` + +当前实现按帧**串行**下载/转换/提交,避免同时在 JS 内存保留多张大 Field。 + +同一个 `reactTag` 上,该函数带 single-flight:前一次完整加载尚未结束时再次调用,会直接返回同一个 in-flight Promise,不会再次 `clearWindFields()` 或交叉提交另一套 T0/T1/T2/T3。调用方仍建议在 UI 上把加载按钮置为 disabled/loading。 + +注意当前调用前提:Runtime 已 `attach()`,且不要处于 `DISABLED`。如果此前调用过 `setWeatherEnabled(false)`,应先: + +```ts +await WeatherRuntime.setWeatherEnabled(reactTag, true); +``` + +再调用完整 WeatherV2 loader。 + +返回类型: +```ts +type LoadedWeatherV2WindFrame = Readonly<{ + fieldId: string; + validTime: number; + generation: number; + bounds: WeatherV2Bounds; + width: number; + height: number; + sourceUrl: string; + submitResult: FieldSubmitResult; +}>; + +type LoadWeatherV2WindResult = Readonly<{ + context: WeatherV2WindContext; + frames: readonly LoadedWeatherV2WindFrame[]; + runtime: RuntimeSnapshot; +}>; +``` + +成功返回时 `runtime.state` 必须已经是 `READY`,否则 helper 自己 throw。 + +## 15. `WeatherV2WindClient` 接口 + +`loadWeatherV2WindIntoRuntime()` 不强制 `client` 必须是内置 `WeatherV2HttpClient`,也可以注入自定义实现,只要满足: + +```ts +type WeatherV2WindClient = Readonly<{ + getWindContext(options?: { + datasetId?: string; + referenceTime?: string; + }): Promise; + + selectWindFrames( + context: WeatherV2WindContext, + options?: { + targetTimes?: readonly string[]; + count?: number; + }, + ): readonly WeatherV2WindFrameRef[]; + + fetchCanonicalWindFrame( + frame: WeatherV2WindFrameRef, + ): Promise; +}>; +``` + +因此以后 NavSea 可以保留统一的 Runtime loader,同时替换 HTTP/cache/provider 层。 +## 16. 最小完整 WeatherV2 React Native 示例 + +以下示例演示:Map style ready → attach → 从 HTTP 获取 WeatherV2 → canonical U/V → Native Runtime → WeatherColor + Particle。 + +```tsx +import { useCallback, useEffect, useState } from "react"; +import { View, findNodeHandle } from "react-native"; +import { File, Paths } from "expo-file-system"; +import { + Map, + Camera, + WeatherRuntime, + WeatherV2HttpClient, + createParticleWeatherStyle, + loadWeatherV2WindIntoRuntime, +} from "@navsea/maplibre-particle-rn"; + +const client = new WeatherV2HttpClient({ + baseUrl: "https://weather.jpcn.ccwu.cc", +}); + +const fileStore = { + async writeTempFile(fileName: string, bytes: Uint8Array) { + const file = new File(Paths.cache, fileName); + file.write(bytes); + return file.uri; + }, +}; + +export default function WeatherMap() { + const [reactTag, setReactTag] = useState(null); + const [loading, setLoading] = useState(false); + const onStyleReady = useCallback(async (event: unknown) => { + const tag = findNodeHandle((event as { target?: unknown }).target as never); + if (tag == null) return; + await WeatherRuntime.attach(tag); + setReactTag(tag); + }, []); + + const loadWeather = useCallback(async () => { + if (reactTag == null || loading) return; + setLoading(true); + try { + const context = await client.getWindContext({ + datasetId: "ecmwf-hres", + }); + + const targetTimes = context.targetTimes.slice(0, 4); + + const result = await loadWeatherV2WindIntoRuntime({ + reactTag, + client, + fileStore, + datasetId: context.datasetId, + referenceTime: context.referenceTime, + targetTimes, + weatherColorVisible: true, + particleVisible: true, + playbackState: "PAUSE", + }); + + console.log(result.runtime); + console.log(result.frames); + } finally { + setLoading(false); + } + }, [reactTag, loading]); + useEffect(() => { + if (reactTag == null) return; + void loadWeather(); + + return () => { + void WeatherRuntime.detach(reactTag).catch(() => undefined); + }; + }, [reactTag, loadWeather]); + + return ( + + + + + + ); +} +``` + +实际产品中不要简单使用 `context.targetTimes.slice(0, 4)` 作为长期业务策略。应由 React/NavSea 天气业务层根据当前时间、船位、Route/ETA、缓存和预测有效期决定要提交哪些 canonical Field。 + +## 17. 错误处理 + +需要区分两类失败: + +1. Promise reject:参数格式错误、MapView 找不到、style 未 ready、Runtime 已 dispose、本地文件不可读等 Bridge/生命周期错误; +2. Promise resolve 但返回 `REJECTED` / `REJECTED_*`:请求到达 Native/Core,但被 Runtime、Adapter、Timeline 或 capacity 契约拒绝。 +建议所有 command 都检查结果: + +```ts +const commandResult = await WeatherRuntime.setParticleVisible(reactTag, true); +if (commandResult !== "ACCEPTED") { + throw new Error(`setParticleVisible failed: ${commandResult}`); +} +``` + +直接提交 Field 时同样不要忽略返回值。`loadWeatherV2WindIntoRuntime()` 已经内置严格的 T0/T1/T2... expected result 检查,因此高层 WeatherV2 路径优先使用该 helper。 + +常见 Field 拒绝原因包括:schema/metadata 不合法、payload 长度不匹配、NaN/Infinity、时间不递增、Field 不兼容、Timeline capacity 已满或 source 不是 Host submission 模式。 + +## 18. React 与 Native 的责任边界 + +```text +React Native / NavSea JS/TS + provider / HTTP + cache / retry / prefetch + 当前时间与 forecast time 选择 + Route / ETA + WeatherV2 解析或其他 provider -> canonical U/V + 临时文件管理 + 低频 Runtime 控制 + ↓ +metadataJson + local file + ↓ +@navsea/maplibre-particle-rn + ↓ +MapLibre Native / C++ + Field 校验 + Snapshot / Timeline + T0/T1 Runtime Field Window + GPU 空间采样 + 时间插值 + WeatherColor + Particle / Trail + GPU rendering +``` + +React 不向 Native 传 provider 业务对象,也不每帧驱动粒子位置;Native 不负责 HTTP、Route、ETA 或业务缓存。 +## 19. Bare React Native Android 手工配置 + +如果宿主不使用 Expo config plugin,需要在 Android 根工程中完成与 `app.plugin.js` 等价的配置。核心要求不是“再加一份 AAR”,而是让 MLRN 的 OpenGL Native SDK 请求统一解析到 NavSea Particle AAR。 + +当前等价 Groovy 逻辑为: + +```groovy +allprojects { p -> + p.repositories.maven { + url = p.uri("https://pbf.jpcn.ccwu.cc/maven") + } + + p.configurations.configureEach { configuration -> + configuration.resolutionStrategy.dependencySubstitution { + substitute module("org.maplibre.gl:android-sdk-opengl") using module( + "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1" + ) + } + } +} + +rootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1") +rootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl") +``` + +包自身 `react-native.config.js` 已声明 Android `sourceDir=./android`,正常 React Native autolinking 应自动接入 `WeatherRuntimeBridgePackage`。 + +## 20. 版本与升级 + +当前静态包 URL 包含完整版本号,因此项目升级必须显式修改 URL: + +```text +.../navsea-maplibre-particle-rn-13.4.1-particle.1.tgz +.../navsea-maplibre-particle-rn-13.4.1-particle.2.tgz +``` + +已经发布的 tarball 不覆盖。版本有修改时创建新的 `particle.N`,重新 `npm pack`、上传、Consumer 验证后再给 NavSea 使用。 +## 21. WeatherV2 低层网络类型 + +只有需要替换默认 `globalThis.fetch` 时才需要这些类型: + +```ts +type WeatherV2FetchResponse = Readonly<{ + ok: boolean; + status: number; + json(): Promise; + arrayBuffer(): Promise; +}>; + +type WeatherV2Fetch = ( + url: string, + init?: Readonly<{ + headers?: Readonly>; + }>, +) => Promise; +``` + +例如可以用自定义 fetch 包装日志、认证 header 或测试 mock,但返回对象必须至少实现上述四项契约。 + +## 22. 当前推荐的 React 调用层级 + +普通业务优先级: + +```text +第一选择:loadWeatherV2WindIntoRuntime() + -> 完整 WeatherV2 HTTP + canonical + submit 流程 + +需要自定义选帧:WeatherV2HttpClient.getWindContext() + + selectWindFrames() + + loadWeatherV2WindIntoRuntime() + +需要自定义缓存/Provider:实现 WeatherV2WindClient + + WeatherV2BinaryFileStore + + loadWeatherV2WindIntoRuntime() + +已有 canonical 本地文件:WeatherRuntime.submitWindField() + +已有 WVECBIN1 ArrayBuffer:decodeWeatherV2WindFrame() +``` + +不要为了普通显示直接绕过高层 helper 操作 Core T0/T1/MIX;这些不是 React 公共控制面。 \ No newline at end of file diff --git a/docs/30_设计/MapLibre粒子ReactNative使用说明_v3.md b/docs/30_设计/MapLibre粒子ReactNative使用说明_v3.md new file mode 100644 index 000000000..b734fab3b --- /dev/null +++ b/docs/30_设计/MapLibre粒子ReactNative使用说明_v3.md @@ -0,0 +1,79 @@ +# MapLibre 粒子 React Native 使用说明 v3 + +本文对应 `@navsea/maplibre-particle-rn@13.4.1-particle.2`。v2 保留为 `.1` 历史接口说明;不要将其中的旧接口用于本版本。 + +## 安装与版本 + +Consumer 直接安装 `@navsea/maplibre-particle-rn@13.4.1-particle.2` 和 `@maplibre/maplibre-react-native@11.3.6`。RN 包 `.2` 继续使用 Android Native artifact `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`;本版本没有新的 AAR。 + +```ts +import { Camera, Map } from "@maplibre/maplibre-react-native"; +import { FieldRuntime } from "@navsea/maplibre-particle-rn"; +``` + +Map 与 Camera 必须来自官方 MLRN。NavSea 包仅为 peer dependency 对接 MLRN,不转导出地图组件。 + +## Style 合入 + +宿主保有自己的底图、source 和 layer。将纯 renderer fragment 合入既有 style: + +```ts +import { createParticleFieldStyleFragment } from "@navsea/maplibre-particle-rn"; + +const fragment = createParticleFieldStyleFragment(); +const style = { + version: 8, + sources: { ...hostSources, ...fragment.sources }, + layers: [...hostLayers, ...fragment.layers], +}; +``` + +fragment 只有 field source、field layer 和 particle layer;不含 GSI 或任何业务底图。使用 `particleFieldIds` 引用 renderer ID,不要自行硬编码其历史内部字符串。 + +## Canonical Field 提交 + +React/宿主选择业务数据、下载/解码、校验、缓存和本地文件写入;SDK 不理解 provider、network、WeatherV2 或数据集。宿主将既有 Native 数据契约的 `metadataJson` 与本地 `file://` payload 提交一次: + +```ts +await FieldRuntime.attach(reactTag); +await FieldRuntime.clearFields(reactTag); +await FieldRuntime.submitField(reactTag, metadataJson, localFileUri); +``` + +契约保持 `maplibre-particle-field/v1`、`float32-le`、`interleaved-uv`。同一 canonical Field 同时供 field renderer 和 particle renderer 使用,不需要重复提交。 + +## FieldRuntime 方法 + +所有方法均以 `reactTag: number` 为首参,异步返回现有 bridge 的结果;无 Native bridge 时调用会明确失败,import 本身不会失败。 + +| 方法 | 其余参数 | 返回值 | +| --- | --- | --- | +| `attach` | 无 | `Promise` | +| `detach` | 无 | `Promise` | +| `getState` | 无 | `Promise` | +| `clearFields` | 无 | `Promise` | +| `submitField` | `metadataJson: string`, `payloadFileUri: string` | `Promise` | +| `setEnabled` | `enabled: boolean` | `Promise` | +| `setFieldVisible` | `visible: boolean` | `Promise` | +| `setParticleVisible` | `visible: boolean` | `Promise` | +| `setTargetFps` | `target: "AUTO" | "FPS_30" | "FPS_60"` | `Promise` | +| `setTargetTime` | `timestampMs: number` | `Promise` | +| `setPlaybackState` | `state: "PLAY" | "PAUSE"` | `Promise` | +| `setPlaybackSpeed` | `speed: "HALF" | "NORMAL" | "DOUBLE"` | `Promise` | +| `setFieldColorStops` | `stops: readonly FieldColorStop[]` | `Promise` | +| `setParticleColor` / `setTrailColor` | `red, green, blue, alpha: number` | `Promise` | +| `setParticleCount` | `count: number` | `Promise` | +| `setParticleLineWidth` / `setParticleLength` | `value: number` | `Promise` | +| `setParticleOpacity` / `setTrailOpacity` / `setTrailFade` / `setFieldOpacity` | `value: number` | `Promise` | + +`FieldColorStop` 为 `{ value, red, green, blue, alpha }`。`RuntimeSnapshot` 包含 `state`、`playbackState`、`targetFps`、`disposed`;`CommandResult` 为 `"ACCEPTED" | "REJECTED"`。 + +`setFieldColorStops()` 继续遵循既有 Native 色阶约束:至少 2 个 stop,`value` 必须为 finite 且严格递增,RGBA 分量位于 `[0,1]`。 + +## 独立显示 + +`setFieldVisible()` 与 `setParticleVisible()` 可独立切换同一 Field 的两种 renderer。`setEnabled()` 控制 runtime 总开关。SDK 不根据业务类型自动选择显示组合;显示策略由宿主决定。 + +## 边界 + +本包不提供业务数据类型、HTTP、provider、重试、缓存、路线或 ETA 策略。Native/Core、JNI、shader 和 Android AAR 均保持 `.1` 已验证实现不变。Android 实机验证应在 Consumer 安装 `.2` tarball 后单独执行。 diff --git a/docs/30_设计/NavSea_PBF自动识别与轻量混淆跨平台方案设计_v1.md b/docs/30_设计/NavSea_PBF自动识别与轻量混淆跨平台方案设计_v1.md new file mode 100644 index 000000000..68e1d95bc --- /dev/null +++ b/docs/30_设计/NavSea_PBF自动识别与轻量混淆跨平台方案设计_v1.md @@ -0,0 +1,95 @@ +# NavSea PBF 自动识别与轻量混淆跨平台方案设计 v1 + +日期:2026-09-27 +状态:**规划 / DESIGN ONLY;未实现、未测试、未发布。** +适用范围:NavSea 自有矢量海图 PBF,MapLibre Native Android / iOS 与 MapLibre GL JS Web。 +归属:MapLibre 资源加载适配层;**不是粒子、Weather Field 或 QBIN 格式变更**。 + +## 1. 背景与目标 + +NavSea 计划对自己分发的 PBF 进行低成本字节混淆,避免直接下载后被普通 PBF/MBTiles 工具读取;客户端收到瓦片后恢复标准 PBF,继续使用原有 MapLibre 解析和渲染链。Web 版本也要消费同一份服务器资源。 +本方案明确只是增加直接复用数据的门槛,不承诺数字版权保护、防逆向或防止明文提取。客户端可被调试,解码后的 PBF 可在内存或缓存环节被截获;公开 Web 尤其如此。 + +**核心规则:检测到 NavSea 专用 Magic Header → 按版本解码;未检测到 → 维持 MapLibre 原有标准资源流程。** 不依据文件后缀决定是否解码,不修改标准 PBF 解析器,不建立第二套地图引擎。 + +## 2. 范围与不变项 + +- 只处理自有矢量瓦片资源;普通 PBF、第三方瓦片、Style JSON、Sprite、Glyph、图片以及 Weather/QBIN 不受影响。 +- 原有 URL 路径、Source/Layer、Style Z-order、粒子绘制、GPU、地图交互与正常 HTTP 重试行为不变。 +- 服务器可同时提供普通 PBF 与封装 PBF;三端遵守同一二进制契约,各自实现资源加载适配。 +- 只在有效响应体上进行格式识别;404、500、304、取消、空响应等继续走既有错误/缓存语义,不把错误页当 PBF 解码。 +- 本文是未来工作的设计草案;不指定当前源码已有某个具体 Hook,进入实施前须按当前 fork 做只读调查。 + +## 3. 封装结构草案 + +建议文件名仍为 `.pbf`,但编码后内容已不是标准 PBF,必须先解封装。以下 **16 字节头仅为待验证候选格式**,实施前冻结字节序、长度和兼容矩阵。 + +| 偏移 | 长度 | 字段 | 候选语义 | +| --- | ---: | --- | --- | +| 0 | 8 | Magic | ASCII `NSVTILE\0`,固定 8 字节 | +| 8 | 1 | Version | `1` | +| 9 | 1 | Codec | `1 = XOR-v1`;其他值拒绝 | +| 10 | 1 | Flags | bit0 表示内部 GZIP,其余保留位须校验 | +| 11 | 1 | HeaderLength | v1 固定 16,给后续扩展留入口 | +| 12 | 4 | CRC32 | 解封装且内部解压后,标准 PBF 明文的 CRC32;约定字节序 | +| 16… | 可变 | Payload | 经 XOR 混淆的原始 PBF 或内部 GZIP 字节 | + +CRC32 只检测意外损坏或密钥不匹配的常见现象,**不是抗恶意篡改认证**。Magic 是类型标识,不是秘密,也不是加密强度来源。头部存在但格式、版本、长度、Flags、Codec 或校验不合法时,必须拒绝,不能退回普通 PBF。 + +## 4. 编码、识别、解码 + +服务器预处理一次:标准 PBF → 计算明文 CRC32 → 可选内部 GZIP → XOR → 添加 Header → 存储/CDN。对于已经是 GZIP 存储的原始瓦片,应先核对实际字节语义,再决定保留内部压缩还是重新封装,避免重复压缩。 +客户端:收到已按 HTTP 传输层规则处理的响应体 → 检查 Magic → 读取并验证头部 → XOR 还原 Payload → 按 Flags 内部解压 → 校验长度/CRC32 → 把标准 PBF 交给现有解析器。普通资源无 Magic 时完全返回既有处理链,不新增一次盲目 GZIP 解压。 + +**HTTP `Content-Encoding` 与封装内部 GZIP 必须分开。** XOR 后的压缩 Payload 不得冒充 HTTP gzip,否则网络库可能在 XOR 前尝试解压。封装可用 `Content-Type: application/octet-stream`,自定义 `X-NavSea-Tile-Encoding` 仅作辅助诊断;格式识别以响应体 Header 为准。HTTP 层若另行压缩整个封装,须确认客户端收到的是解开传输编码后的封装字节。 + +已声明为“必须受保护”的自有 Source 可另设严格策略:缺少 NavSea Header 也报错(fail-closed),防止服务端误发明文;普通/混合 Source 则执行“无头=原路径”。Source 保护策略与字节自动识别是不同层面,不得混为一谈。 + +## 5. 三个平台的接入边界 + +| 平台 | 未来接入方向 | 重要约束 | +| --- | --- | --- | +| Android | 在当前 fork 的 MapLibre HTTP 资源响应交给解析器前解封装 | 核对 OkHttp/Native 缓存与 Response 回调实际位置,避免双重解压或额外整份复制 | +| iOS | 在当前 MapLibre Native 的对应资源响应路径解封装 | 以实际 iOS 网络资源实现为准,不假设 Android Java 修改会覆盖 iOS | +| Web | MapLibre GL JS `addProtocol()` 负责 fetch、校验、解封装并返回标准 PBF `ArrayBuffer` | 自有瓦片 URL 经自定义 scheme 或 `transformRequest` 导向注册的协议;后者本身不能修改响应体 | + +Web 必须核对 GL JS 版本及协议回调契约、Style/TileJSON 的 URL 重写、Worker/主线程负载、CORS 和取消信号;服务端需暴露必要响应头。第三方请求不接入解码器。三端共享格式测试向量,不共享平台相关 HTTP 实现。 + +## 6. 缓存、错误与日志 + +- CDN/HTTP 缓存以**封装字节**为对象;ETag、Content-Length、缓存键、版本及更新失效策略要与实际封装内容一致。 +- HTTP 304 没有新响应体;由现有缓存/重验证机制恢复对应实体,不能把空体交给解码器。按当前 Native/Web 路径调查 MapLibre 自有缓存究竟保存封装还是明文,不作默认安全保证。 +- 若业务要求离线缓存也不可直接导出标准 PBF,需单独设计落盘边界;仅 HTTP 层 XOR 不保证 MapLibre 离线库/内存缓存保存密文。 +- 解码前限制输入大小,解压后限制最大明文大小与压缩比,避免异常瓦片造成内存放大。数据损坏、版本不支持、密钥不匹配归类为独立 Codec 错误。 +- 将前述 HTTP URL 诊断与 Codec 诊断分开:`INVALID_URL`、`HTTP_ERROR`、`NETWORK_ERROR`、`UNSUPPORTED_TILE_CODEC`、`TILE_DECODE_FAILED`;只记录必要的 URL(敏感查询参数脱敏)、状态码、版本与长度,不输出密钥或明文 PBF;正常 200 不逐张刷日志,可限频。 +- 404/500、取消、超时、304、第三方瓦片和正常 200 行为均不因本功能改变。 + +## 7. 性能与安全预期 + +XOR 是 O(n) 的逐字节操作,优先服务器离线预编码、客户端原地处理,避免 React/JS Bridge 往返和多次完整 Buffer 拷贝。封装额外增加 Header 字节;“XOR 不改变 Payload 长度”不等于整体零额外字节。压缩应在 XOR 前进行,否则压缩收益可能明显恶化。 +Web 先评估主线程处理时长与地图交互,再决定是否使用 Worker;Android/iOS 关注实际缓存策略、内存峰值与线程阻塞。性能影响只能经真实 PBF 样本、冷/热缓存及拖动缩放实测后定论。 + +固定或循环 XOR 密钥可以被分析,已知格式和客户端代码会进一步降低逆向门槛;把算法放到 C++/WASM 或换成 AES 也无法阻止客户端截获已解码的明文。本方案定位为**轻量混淆**。若未来目标提高到防批量下载,应另行规划 HTTPS、授权、短期访问凭据、配额/限速、异常下载监测与必要的数据分发范围;不要把 XOR 描述成加密认证。 + +## 8. 未来实施阶段(本次均不执行) + +| 阶段 | 工作 | 产出/闸门 | +| --- | --- | --- | +| A 只读调查 | 当前 fork Android/iOS HTTP、资源与磁盘缓存;GL JS 版本;服务器 PBF/GZIP、CDN、TileJSON/Style 路径 | 确认真实接入点、压缩语义和数据所有权 | +| B 协议冻结 | 冻结 Header 字节序/CRC 覆盖、Codec、Flags、版本策略、普通/受保护 Source、Golden vectors | 三端可交叉解码同一 fixture | +| C 服务端 | 离线编码工具、标准/封装样本、响应头及缓存策略 | 普通与封装资源同时可分发 | +| D Android/iOS | 各自最小资源适配,保持解析器与业务接口不变 | 各平台原生样本验证;构建/实机由用户手工执行 | +| E Web | 注册自定义协议、URL 路由、解码与取消/CORS | 自有/第三方混合地图正常 | +| F 综合验收 | 性能、离线缓存、故障、日志、安全边界与回退 | 三端统一验收通过后再决定发布 | + +验收矩阵至少覆盖:原始 PBF、标准 HTTP gzip、封装 RAW、封装内部 gzip、无头普通资源、受保护 Source 缺头、短头/错误 Magic/版本/Flags、错误 CRC/密钥、空瓦片、404/500/304、网络失败/取消、第三方瓦片、冷/热/离线缓存、跨平台 Golden Fixture、Web CORS 与长时间拖动缩放;不得以单张瓦片可显示作为最终通过。 + +## 9. 待实施前决策 + +1. 是否采用 XOR-v1 作为正式首版,还是只做试验;客户端密钥静态分发的风险是否接受。 +2. Magic/Header 的精确二进制布局、CRC 字节序、最大明文长度、Codec ID 和压缩 Flags 是否按本草案冻结。 +3. 哪些 NavSea Source 必须受保护;普通/混合资源的无头旁路范围。 +4. 离线缓存是否必须保存封装字节,以及 MapLibre 内建缓存如何满足该要求。 +5. Web URL 路由和跨域部署方案;第三方瓦片是否保持完全独立。 + +**本次结论:仅保留规划文档。没有新增 Codec、改动服务器资源、修改 MapLibre HTTP、引入密钥、触发构建/测试或宣布任何平台支持完成。** diff --git a/docs/30_设计/README.md b/docs/30_设计/README.md new file mode 100644 index 000000000..d1598172d --- /dev/null +++ b/docs/30_设计/README.md @@ -0,0 +1,3 @@ +# 设计目录 + +调查后再写设计:ParticleState、VelocityField、Trail、RasterParticleLayer接口、LandOceanMask。 diff --git a/docs/30_设计/STEP-057_AndroidHost天气运行时接口契约_v1.md b/docs/30_设计/STEP-057_AndroidHost天气运行时接口契约_v1.md new file mode 100644 index 000000000..75899eee2 --- /dev/null +++ b/docs/30_设计/STEP-057_AndroidHost天气运行时接口契约_v1.md @@ -0,0 +1,27 @@ +# STEP-057 Android Host 天气运行时接口契约 + +## 入口与线程 + +公开入口为 `org.maplibre.android.maps.WeatherRuntime`,由 Android 主线程创建和调用。`MapLibreMap` 同样要求主线程访问;本 facade 不创建 Timer、线程或 per-frame Java 回调。Core 继续在其既有 Map/Render 线程完成 Timeline、窗口和 GPU 工作。 + +`WeatherRuntime.Config(sourceId, weatherColorLayerId, particleLayerId)` 绑定一个已存在的 style runtime;ID 不可为空,未硬编码 Test App ID。 + +## API + +- `submitWindField(metadataJson, byte[]) -> FieldSubmitResult`:复用 canonical ingress。结果逐一映射 `REJECTED_*`、`ACCEPTED_PENDING_INITIAL_WINDOW`、`INITIAL_WINDOW_READY`、`ACCEPTED_NEXT_SNAPSHOT`、`DUPLICATE`。 +- `clearWindFields() -> CommandResult`:暂停 Timeline,清除 pending T0、Timeline entries、authoritative T0/T1 和 retained snapshots;Source/Layer 可复用。 +- `setWeatherEnabled(boolean)`:OFF 暂停、隐藏两层并 clear fields;Render 线程发现空场时释放粒子 State/Trail/draw、field texture 和 WeatherColor 资源。ON 只恢复 visibility,状态为 `WAITING_FOR_FIELDS`,必须重新提交 T0/T1。 +- `setWeatherColorVisible` / `setParticleVisible`:仅 visibility,`hide != unload`。 +- `setForecastTime(timestampMs)`:Core 按 timestamp 选相邻 T0/T1 并计算 MIX。恰好命中末快照为 MIX=1;中间线性插值;超范围、未 READY、Gap/Invalid 返回 `REJECTED`,不会沿用错误旧时间。正常 seek 不 submit field、不 reset particle、不 clear trail;仅跨窗口时沿用既有 OLD_T1→NEW_T0 reuse。 +- `setPlaybackState(PLAY/PAUSE)`、`setPlaybackSpeed(HALF/NORMAL/DOUBLE)`:复用 Source Timeline,不创建 Android Timer。 +- `setWeatherColorOpacity`、`setParticleCount`、`setParticleLineWidth`、`setParticleLength`、`setParticleOpacity`、`setTrailOpacity`、`setTrailFade`、`setParticleSpeedColorEnabled`:明确 typed 的低频 visual 更新;正式 Host API 不接收 property string。 +- `setWeatherColorStops(List)`:2 至 Core `MaxWeatherColorStops` 个 stop(v1 为 8,Java 经 JNI `getWeatherColorStopCapacity()` 获取),speed finite 严格递增,RGBA 均为有限 [0,1]。低于/高于边界分别钳到首/末颜色,中间线性插值。无效输入 reject,并保留上一套有效 ramp。 +- `setParticleColors(...)`:独立 particle/trail RGBA;不依赖 WeatherColor ramp。legacy `speed-color-enabled=true` 时仅当前 segment 可使用旧 speed 色,不改变 WeatherColor stops。 + +状态为 `DISABLED`、`WAITING_FOR_FIELDS`、`READY`、`ERROR`,仅由低频命令和 submit result 更新。v1 提供同步 typed result/state query;没有 listener,未来 RN 薄层可在这些调用完成后读取结果。 + +## JNI 与边界 + +Java facade 经 `MapLibreMap` / `NativeMapView` 到 JNI:field submit、clear、timestamp seek、Timeline command、layer visibility、particle/trail colour、WeatherColor stops。C++ Renderer 不接触 provider、HTTP、route、ETA 或预测目录;没有向 Host 暴露 T0/T1、MIX、State A/B、Trail A/B、资源代际或 texture 格式。 + +Shader-visible ramp 是 UBO 的 8 个 stop/颜色槽;改 ramp 不上传 Field,也不重建粒子 simulation。粒子/尾迹颜色也只更新 UBO。 diff --git a/docs/30_设计/STEP-058_WeatherLocalTargetFps与RepaintGate设计_v1.md b/docs/30_设计/STEP-058_WeatherLocalTargetFps与RepaintGate设计_v1.md new file mode 100644 index 000000000..28a235647 --- /dev/null +++ b/docs/30_设计/STEP-058_WeatherLocalTargetFps与RepaintGate设计_v1.md @@ -0,0 +1,33 @@ +# STEP-058 Weather-local Target FPS 与 Repaint Gate 设计 + +## Host contract + +`WeatherRuntime.TargetFps` 只提供 `AUTO`、`FPS_30`、`FPS_60`,默认 `AUTO`。`WeatherRuntime.setTargetFps()` 返回 typed `CommandResult`,`getTargetFps()` 返回当前 Host 选择。Host 不接触 RenderLayer、deadline、`needsRepaint()` 或 renderer `requestRender()` 细节;未来 RN 只包裹该 facade 方法。 + +## Shared cadence authority + +天气 cadence 归 `RasterArrayTimelineRuntime` 所有,与 source 的 T0/T1、MIX、playback 共存。它保存 target mode、单一 monotonic last cadence tick、deadline 判定和低频 tick 计数。Particle 与 WeatherColor renderer 都读取同一 timeline authority;没有各自 timer 或 gate。AUTO 始终 due,30/60 使用约 33.33/16.67ms 间隔,且 target 切换重置 cadence deadline,不改变 Timeline/field/window。 + +`tick()` 仅在 deadline 到达时推进真实 `steady_clock` elapsed;不把 1/30 或 1/60 写入 simulation delta。这样 particle lifecycle、Trail fade、Timeline playback 和速度仍使用真实 wall time。 + +## Repaint 与 delayed wake-up + +`RenderRasterParticleLayer::needsRepaint()` 与 `RenderWeatherColorLayer::needsRepaint()` 共享 source timeline 的 due 判定。Android `WeatherRuntime` 使用已有 `MapLibreMap.triggerRepaint()` 作为最小 delayed wake-up seam,在 READY 且至少一层需要天气持续动画时按目标间隔唤醒;AUTO 不安装天气 wake-up。Runtime disable/clear、两层 hidden 或错误状态会停止 wake-up。未使用 `MapView#setMaximumFps()`,因此 camera/其他 layer 仍可保持地图自身帧率。 + +地图高频 camera frame 在未到 deadline 时只重用现有粒子/Trail composite,不推进 State、Trail、Timeline 或 field upload;天气到 deadline 后两层再次消费同一 cadence。WeatherColor visibility 不依赖 particle visibility:Particle hidden + WeatherColor PLAY 仍可唤醒;两层 hidden、WeatherColor PAUSE 且 Particle hidden 时不持续唤醒。 + +## 动态修改与边界 + +target 切换仅更新 source/timeline cadence,不 reload Style、不重提交 field、不 reset particle State、不 clear Trail、不重建 field texture。clear/disabled 后 timeline 被清空,cadence naturally 无 due frame。Core 保留 canonical U/V、T0/T1/MIX、Runtime Field Window、OLD_T1→NEW_T0 reuse 与 only NEW_T1 upload。 + +## 测试策略 + +cadence deadline 通过显式 `steady_clock::time_point`/duration 的 deterministic 测试验证,不使用 sleep:AUTO always due、30/60 deadline 前后、target switch 和 single-authority 行为。Android popup 仅作为 Host 模拟器提供 FPS AUTO/30/60 与 HUD 显示;Android Gradle/设备验收另行执行。 + +## STEP-058A shared advance 与 Runtime 生命周期修正 + +`RasterArrayTimelineRuntime::advanceIfDue(now)` 在同一 mutex 内完成 due 判定、cadence revision、真实 wall-time Timeline 推进和 frame 返回。Particle 与 WeatherColor 均可作为 consumer;先到者推进,后到者收到相同 `cadenceRevision` 且不会再次推进。Particle 用 revision 判断是否运行 State/Trail,因而 WeatherColor-only PLAY 不再依赖 Particle pass,也不会发生双 tick。 + +WeatherColor 在 PLAY 时参与 shared advance;PAUSE 时仅显示当前 frame,`needsRepaint()` 不主动保持连续刷新。Particle 保持 active 时,即使 Timeline PAUSE,仍按 cadence 推进自身 simulation。两层 hidden 时 facade 不再安装 delayed wake-up。 + +`WeatherRuntime.dispose()` 是 map/style-scoped、幂等的 ownership 结束接口:取消 Handler callback,旧 Runnable 被执行时也不会 repaint 或再次 schedule,后续 control API 拒绝或 fail-fast。Test App 在 style reload、离开 REAL WEATHER 与 Activity destroy 前都先 dispose 旧 Runtime。 diff --git a/docs/30_设计/STEP-081_iOS_Metal_Renderer与真机Smoke入口设计_v1.md b/docs/30_设计/STEP-081_iOS_Metal_Renderer与真机Smoke入口设计_v1.md new file mode 100644 index 000000000..e0d16c2e9 --- /dev/null +++ b/docs/30_设计/STEP-081_iOS_Metal_Renderer与真机Smoke入口设计_v1.md @@ -0,0 +1,53 @@ +# STEP-081 iOS Metal Renderer 与真机 Smoke 入口设计 v1 + +状态:CLOSED / PASS。已在 Pleiades(iPad mini 5 / iPadOS 26.5.2)完成签名安装、Metal runtime、deterministic fixtures 与 QBIN V4 T0/MID/T1/Clear 真机验收。 + +## 目标和边界 + +Metal 路径消费既有 `RasterArraySource` V4 renderer projection、`WeatherColorLayer` 与 `RasterParticleLayer`。没有 iOS Field model、QBIN decoder 分支或公开 iOS SDK API。Smoke 代码仅在 `platform/ios/app/MBXViewController.mm`,RN iOS bridge 仍是明确非目标。 + +## OpenGL → Metal pass 对照 + +| `u_pass.x` | OpenGL 既有语义 | Metal `ParticleGpuValidationShader` | +| --- | --- | --- | +| `< 0.5` | State A/B update / seed / respawn | 同一 T0/T1 decode、timeMix、VELOCITY/DIRECTION advection 与 fail-closed state write | +| `< 1.5` | Trail fade | 同一 retention fade;legacy validation marker 保留 | +| `1.5–3.5` | Composite / FrozenComposite | 同一 trail/particle 色彩、mask、speed-color 和 legacy diagnostic 分支 | +| `> 3.5` | Trail particle line draw | State read/write displacement line,respawn alpha 抑制跨屏线 | +| `> 4.5` | WeatherColor | 同一 scalar/vector magnitude、stops/ramp 和 invalid discard | +| `> 5.5` | 无 | Metal-only current-endpoint 1 px quad coverage;只补 raster coverage,不改变 State、速度、时间步或 previous→current 物理长度 | + +MSL 使用 `ParticleGpuValidationUBO` 的同字段 vec4/float4x4 布局;声明与 C++ 的 `alignas(16)` 相同,并在 MSL 中静态断言 `31 * 16` 字节。此 Step 未改动 C++ UBO、QBIN V4 contract 或 OpenGL source。 + +## Metal binding + +| 类别 | Metal index | Stable ID | +| --- | ---: | --- | +| vertex attribute `short2 a_pos` | attribute 0 | `idParticleGpuValidationPosVertexAttribute` | +| UBO | buffer `drawableReservedUBOCount` | `idParticleGpuValidationUBO` | +| previous | texture/sampler 0 | `idParticleGpuValidationPreviousTexture` | +| mask | texture/sampler 1 | `idParticleGpuValidationMaskTexture` | +| field T0 | texture/sampler 2 | `idParticleGpuValidationFieldTexture` | +| field T1 | texture/sampler 3 | `idParticleGpuValidationNextFieldTexture` | +| state | texture/sampler 4 | `idParticleGpuValidationStateTexture` | + +这些 index 来自 Metal `ShaderSource` 的 `attributes`/`textures` 表以及 existing shader IDs,而非 OpenGL location 推测。注册位于 `src/mbgl/mtl/renderer_backend.cpp`,CMake source/include list 位于 `cmake/metal.cmake`。 + +## Smoke 数据链 + +Test App Runtime Styling 菜单新增 `Particle / QBIN V4 Smoke (T0)`。点击后可选 T0、midpoint、T1 或 clear/release。它从 app bundle `ParticleSmokeFixtures/T0.qbin`、`T1.qbin` 读取现有 RN Wind Golden fixtures;`platform/ios/app/CMakeLists.txt` 在构建后从唯一 fixture 来源复制,不创建第二种数据格式。 + +private `MLNMapView` seam 提供 `mbglMap`。Smoke 直接创建 `RasterArraySource`,提交 `fixture.wind` T0/T1、Wind profile contract、ColorBinding 与 VectorComponents ParticleBinding,再添加 `WeatherColorLayer` 与 `RasterParticleLayer`。T0=1789239600000、midpoint=1789241400000、T1=1789243200000,全部通过 source-owned requested forecast time 选择窗口/mix。clear 先移除 layers 后移除 source,允许重复创建。Wind Golden fixture 实际范围为 112.5E..157.5E、0..55.776573N,Test App smoke 会把相机置于约 27.8883N / 135E、zoom 3,保证 FIELD_READY 后画面可观察;这只是 Test App 可视化入口,不改变 Core 数据范围。 + +## Metal 真机收口结论 + +真机调试确认两个 Metal 特有问题并形成正式修复: + +1. `ShaderProgram::getRenderPipelineState()` 的缓存复用 key 原先只包含调用方 reuse hash,没有包含 color/depth/stencil attachment pixel format。粒子 State 使用 RGBA32Float、Trail 使用 RGBA8Unorm、主 renderable 常见 BGRA8Unorm,同一 shader 因此可能复用到不兼容的 pipeline。修复后 cache key 显式合并 color/depth/stencil format;物理 iPad 上 Trail RGBA8 写入从全零恢复为有效像素。 +2. deterministic Vortex 实测真实 previous→current 线段平均约 0.065 px、最大约 0.24 px,全部小于 0.5 px。Metal `MTLPrimitiveTypeLine` 对该亚像素长度没有稳定可见覆盖。正式 previous→current line 保持不变,Metal 额外在真实 currentPosition 绘制 1 px quad coverage;不拉长轨迹、不改变 State、advection、时间步或物理速度。 + +同时确认 Metal vertex-stage texture/sampler binding、State A/B RGBA32Float、Trail A/B RGBA8、矩阵投影与 Composite 均可工作。 + +## 后续边界 + +STEP-081 已关闭。下一轮如需 iOS RN,必须另行设计桥接;不得把本 App 的 private Core seam 提升为 SDK 或 NavSea API。当前 Metal vertex+fragment 双阶段纹理绑定可在后续独立技术债中收窄为 stage-specific binding,但不得在 STEP-081 收尾中继续重构。 diff --git a/docs/30_设计/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行设计_v1.md b/docs/30_设计/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行设计_v1.md new file mode 100644 index 000000000..5b881f2dc --- /dev/null +++ b/docs/30_设计/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行设计_v1.md @@ -0,0 +1,27 @@ +# STEP-082 iOS Native FieldRuntime Facade 与 XCFramework 发行设计 v1 + +状态:IMPLEMENTED / WAITING_MAC_XCFRAMEWORK_VALIDATION + +## 公开边界 + +新增 `MLNParticleFieldRuntimeConfiguration`、`MLNParticleFieldRuntime` 和 `MLNMapView (ParticleFieldRuntime)`。公开头只依赖 Foundation 与已有 public Objective-C 前置类型;QBIN 以 `NSData` 传入,不暴露 `mbgl`、STL、Core 指针或 `MLNMapView_Private.h`。 + +Configuration 保存 Host-neutral 的 `sourceIdentifier`、`weatherColorLayerIdentifier`、`particleLayerIdentifier`。它不硬编码 RN ID;后续 RN 可继续传 `rn-weather-fields`、`rn-weather-color`、`rn-weather-particles`。 + +## 生命周期与 fail-closed + +Runtime 被 `MLNMapView` 的 associated object 持有,Runtime 只弱引用 MapView。replace 先 dispose 并移除旧实例,attach 仅在当前 style 同时存在正确 Core 类型的 `RasterArraySource`、`WeatherColorLayer`、`RasterParticleLayer` 且两个 layer 都引用该 source 时成功。任何不满足条件的 style-ready/source/type/layer 错误都以 `NSError` 返回,不创建 source/layer,不替换 style。 + +每条命令重新从 current MapView/current style 查找并验证三对象;不缓存 `RasterArraySource*` 或任何跨 style 生命周期 Core pointer。因此 style reload 后旧 runtime 不会操作悬空对象,而是 fail-closed,Host 应在 style ready 后 replace/attach。 + +## V4 转发 + +`submitEncodedField` 先守住与 Android 相同的 UTF-8 ID 1..64、contract 1..256 KiB、QBIN 1..64 MiB、generation>0 边界,再调用 Core `parseResolvedFieldProfileContractJson(...)` 和 `RasterArraySource::submitEncodedField(...)`。十个 Core stable result 逐值映射为 ObjC enum;facade 生命周期错误不会伪装成 submit result。 + +Color binding 传递 fieldKey、semantic/group、SCALAR/VELOCITY_COMPONENT/DIRECTION_ANGLE 和 expected unit。Particle binding 保留 VECTOR_COMPONENTS 与 DIRECTION_ANGLE,inactive selector 严格为 `(0, 0)`。forecast time/play/pause/speed 和 clear 都只转发给 Core;T0/T1/mix、boundary reuse、QBIN decoder 与 renderer 不在 ObjC 实现重写。 + +## 导出与 distribution + +`MLNParticleFieldRuntime.h` 列入 `MLN_IOS_SDK_HEADERS`,由现有 `public_hdrs` 进入 `MapLibre.dynamic`,并由 `Mapbox.h` 导入。`MLNMapView_Private.h` 继续仅列于 private list。 + +`release/maplibre-particle/ios/` 建立 module 名不变的 SwiftPM binary template。后续 STEP-083 的链路固定为:`reactTag -> React View Registry -> Fabric ComponentView -> contentView -> MLNMapView -> public MLNParticleFieldRuntime`。当前 peer dependency 是 MLRN 11.3.6;STEP-083 不建立第二个 Map registry、不把 MLRN private header 作为长期 API,并用 `$MLRN_SPM_SPEC` 令 MLRN 与 Bridge 消费同一 MapLibre binary。 diff --git a/docs/30_设计/STEP-085_V4_Renderer性能修复设计_v1.md b/docs/30_设计/STEP-085_V4_Renderer性能修复设计_v1.md new file mode 100644 index 000000000..a0e15a7fe --- /dev/null +++ b/docs/30_设计/STEP-085_V4_Renderer性能修复设计_v1.md @@ -0,0 +1,45 @@ +# STEP-085 V4 Renderer 性能修复设计 v1 + +状态:`IMPLEMENTED / WAITING_FOR_RDC_NATIVE_BUILD` + +## 目标与根因 + +V4 的 `RasterArraySource` 通过独立的 field-keyed window 产出已经 materialize 的 T0/T1/MIX。旧 renderer 将 V4 误当作“没有 timeline”,从而产生两项错误:WeatherColor 的 `needsRepaint()` 永远为 true;Particle 也永远请求下一帧,并在 `prepare()` 将包含大 vector/scalar/valid 数组的 snapshot 按值复制。 + +本设计保持 V3 timeline、V4 Runtime Field Window、QBIN 和所有 RN/Host 公共接口不变,只在 Source 内部和共用 renderer seam 修复 cadence/lifetime。 + +## 共用 cadence 语义 + +新增 Source-owned `RasterArrayRendererCadence`。它只负责 visual simulation 的 deadline,不携带 field、不会变更 T0/T1/MIX,也不会调用 ForecastClock。 + +| targetFps | Core cadence | platform wake | +| --- | --- | --- | +| AUTO | 每个 MapLibre animation frame 可消费一次 | 沿用平台 animation-frame 语义 | +| 30 | 约 33.3ms 才允许一次 simulation | Android Handler / iOS NSTimer 约 34ms 请求一次 repaint | +| 60 | 约 16.7ms 才允许一次 simulation | Android Handler / iOS NSTimer 约 17ms 请求一次 repaint | + +`RenderRasterParticleLayer::needsRepaint()` 仅对 active、visible、field-ready 的 V4 Particle 在 deadline 到期时返回 true。因 camera 而来的额外 frame 仍可 composite 当前 trail,但 `consumeIfDue()` 未成功时不建 State/Trail pass、不推进 ping-pong、不衰减 trail。Forecast PAUSE 不影响该 visual cadence。 + +Android/iOS runtime timer 仅是已有 facade 的“下一帧 wake”薄层;deadline 与是否推进由共用 C++ Source cadence 判定。hidden、clear、disable 和 dispose 都停止 V4 Particle wake。 + +## WeatherColor idle 与 drawable + +V4 WeatherColor 在 PAUSE、field/window/stops/opacity/transform 不变时 `needsRepaint()==false`。它不再以 `sourceTimeline == nullptr` 作为“永远动画”的代理。 + +稳定状态下 WeatherColor drawable 复用既有 LayerGroup drawable。T0/T1 texture、mix、opacity、color stops 与 value source 由 Resources/Tweaker 在 render 时更新 UBO;仅 wrap 集合变化才 `clearDrawables()` 后重建。该方案保留 gfx abstraction,未引入 OpenGL/Metal 直接调用。 + +## V4 snapshot lifetime + +materialize 仍发生在 Source 的 binding/window 真正变化时,但 `Materialized*Projection` 与 renderer `sourceField/sourceNextField` 全部改为 `shared_ptr`。Source Impl 的不可变副本持有 projection,RenderLayer 只复制 shared handle;稳定 window 的 `prepare()` 不再深复制 scalar/vector/valid arrays。旧 V3 optional snapshot 路径仍在 Source 变化时构造独立 handle,未改变其 API。 + +## Debug/Validation observability + +`include/mbgl/particle/renderer_debug_counters.hpp` 在非 release 建立低成本 relaxed atomic 累计器,release 编译为 no-op。覆盖:两 layer 的 needsRepaint/update、particle simulation advance、WeatherColor/Particle drawable rebuild、field texture upload、GPU resource create/release-reset、trail create/resize、V4 projection materialize 与 field-window revision。它不逐帧写普通日志,也不暴露给 NavSea。 + +## ForecastClock 审查 + +全仓确认 `RasterArraySource::advanceForecastClock(elapsedMs)` 只有定义,当前 Android WeatherRuntime、iOS `MLNParticleFieldRuntime` 与 RN bridge 均没有 production caller。故 V4 PLAY 自动推进尚无 owner;本 Step 不将 cadence timer 偷接成第二个 forecast clock。Host 显式 `setEncodedForecastTime` 继续触发 window refresh;自动 forecast playback 应单独设计 owner 后处理。 + +## 生命周期边界 + +本 Step 不改变 `detach()`、dispose、Host clear/destroy contract,也不对 retained allocation/Native Heap 归因。计数器和减少 repaint/copy/rebuild 仅为后续 RDC 采样提供证据。 diff --git a/docs/30_设计/STEP-094_通用RenderSegment模型与Style分类Diagnostics设计_v1.md b/docs/30_设计/STEP-094_通用RenderSegment模型与Style分类Diagnostics设计_v1.md new file mode 100644 index 000000000..e724e9bd9 --- /dev/null +++ b/docs/30_设计/STEP-094_通用RenderSegment模型与Style分类Diagnostics设计_v1.md @@ -0,0 +1,141 @@ +# STEP-094 通用 Render Segment 模型与 Style 分类 Diagnostics 设计 v1 + +## 1. 目标与边界 + +Renderer 只接收 Style 顺序和一组连续 Segment 定义。每个 Segment 的执行策略只有 `RETAINED` 或 `CONTINUOUS`;不引入 LOW/MEDIUM/HIGH,不识别 AIS、Route、Weather 等业务语义,不改变 Style Z-order,也不增加 Anchor/Marker Layer。 + +V12 的 Bottom Retained / Continuous / Top Retained 是本模型的兼容特例。新实现只有一套 resolved topology;旧 `setContinuousLayerRange()` 先转换为通用 Segment Plan,再进入同一 Renderer 路径。 + +## 2. 公共数据模型 + +Core 公共类型位于 `include/mbgl/renderer/render_segment.hpp`: + +```text +RenderSegmentPlan + schemaVersion = 1 + segments[1..N] + strategy = RETAINED | CONTINUOUS + startLayerId = 第 1 段省略;后续段必填 +``` + +RN 公共类型位于 `packages/maplibre-particle-rn/src/types.ts`,`RendererRuntime.setRenderSegmentPlan(reactTag, plan)` 接收 typed object。TurboModule、Android/iOS package bridge 与 platform API 内部使用 schema-versioned JSON,JS 调用方不接触 raw JSON。 + +## 3. Config / Resolve 生命周期 + +1. map-scoped runtime state 保存 active config、`configSource` 和单调递增 `configRevision`。 +2. `setRenderSegmentPlan()` 将 active source 切到 `explicit-render-segment-plan`。 +3. `setContinuousLayerRange()` 将 active source 切到 `continuous-range-compat`,不会再并存第二套 Renderer topology。 +4. 没有显式配置时,既有 `startsRetainedDynamicRange()` 仅通过 `unconfigured/legacy` adapter 生成兼容 plan。 +5. RenderOrchestrator 只在 config、Style ready、Layer add/remove/change 时重新解析 boundary ID 和 diagnostics metadata;Particle 高频 Fast Path 不扫描/复制 Style 字符串,也不 stringify JSON。 +6. `topologyRevision` 只在 resolved strategy/begin/end 签名变化时递增。Visibility 变化更新 `visible`,不改变 topology revision。 + +显式 plan 合法条件:definitions 非空;第 0 段无 boundary;第 1..N-1 段 boundary 存在、非空且 index 严格递增;每段非空;完整覆盖 `[0, layerCount)`。失败时 segments 为空、`valid=false`、释放旧 cache 并走 full renderer;ordered `styleLayers` 仍可读取。 + +## 4. V12 Compatibility Mapping + +旧闭区间 `[startLayerId,endLayerId]` 解析为: + +```text +startIndex > 0 -> RETAINED [0,startIndex) + CONTINUOUS [startIndex,endIndex+1) +endIndex+1 < styleLayerCount -> RETAINED [endIndex+1,layerCount) +``` + +因此自然退化为 R-C-R、R-C、C-R 或 C。`configSource=continuous-range-compat`。旧 `getRetainedUnderlayDiagnostics()` 继续保留摘要;多段真实事实以新 API 为准。 + +## 5. Renderer 执行与 Cache Ownership + +Resolved Segment 保留 Style 全局 `layerIndex` 和 LayerGroup 原 ordinal。Retained 构建继续使用 opaque reverse / translucent forward;Continuous 直接绘制同一全局 index 范围。`parameters.currentLayer` 和 `depthRangeSize` 不按 Segment 重新编号。 + +Renderer 按 resolved topology 动态创建 `retainedSegmentCaches[segmentIndex]`: + +- RETAINED:独立 `RetainedUnderlayCache`、key、revision、texture、compositor 和 counters; +- CONTINUOUS:对应 slot 为空,不创建 retained texture/FBO/compositor; +- topology 改变:旧 cache 在 Renderer 线程释放,不能迁移或 composite stale resource; +- GPU resource 仍由 `prepareResources()` lazy 创建,稳定 viewport 复用。 + +主 buffer 按 Segment 顺序执行:Retained composite 或 Continuous direct render。相邻同策略不会合并。 + +## 6. Invalidation + +Layer immutable Impl identity 与全局 Style index 组成 retained Segment content fingerprint。Layer property/visibility 在某 retained Segment 内变化时,只推进该 Segment 的 content revision;其他 retained Segment 可继续 reuse。 + +以下仍保守 invalidate 全部 retained Segment:camera/viewport/pixel ratio、source/tile、image/sprite/glyph、placement/transition、context reset、debug/3D、style reload、resource/compositor failure。原因是当前 Core 没有足够稳定的通用 source-to-render-segment dependency graph;本 Step 不扩建该图。 + +## 7. Particle Fast Path + +恰好一个 Continuous Segment、其内容满足既有 Particle-only 支持条件、所有实际 Retained cache 有效时,沿用 V12 Fast Path。缺少某个不存在的 retained cache 不构成 miss。 + +多个 Continuous Segment 暂不做高级 Fast Path:gate 以 `unsupported-continuous-layer` fail closed,进入正常 full CPU frame;Segment topology、cache 与 Z-order 不变。 + +## 8. Android / iOS / RN API + +```text +RN RendererRuntime + setRenderSegmentPlan(reactTag, typedPlan) + getRenderSegmentDiagnostics(reactTag) + ↓ schemaVersion=1 JSON transport +WeatherRuntimeBridge(既有唯一 bridge) + ↓ +Android MapLibreMap / iOS MLNMapView + ↓ +mbgl::Map typed RenderSegmentPlan / RenderSegmentDiagnostics + ↓ +RetainedUnderlayRuntimeState + RenderOrchestrator + RendererImpl +``` + +Android platform 内部 API 为 `setRenderSegmentPlan(String)` / `getRenderSegmentDiagnostics()`;iOS 为 `setRenderSegmentPlanJSON:` / `renderSegmentDiagnosticsJSON`。Core API 保持 typed。JSON/schema/strategy 非法时返回 REJECTED,同时把 active topology 置 invalid,避免继续复用旧 cache。 + +## 9. Diagnostics Schema + +Snapshot 为 pull-based。顶层包含 enabled/configured/valid/configSource/configRevision/topologyRevision/counts/coverage/fallback/reason/Fast Path counters;`styleLayers[]` 始终按 Style 顺序;`segments[]` 包含 begin/end、实际 Layer 成员。Retained 的 `cache` 为 object;Continuous 的 `cache=null`,不会伪装为 disabled retained cache。 + +完整示例: + +```json +{ + "schemaVersion": 1, + "enabled": true, + "configured": true, + "valid": true, + "configSource": "explicit-render-segment-plan", + "configRevision": 7, + "topologyRevision": 4, + "styleLayerCount": 6, + "segmentCount": 4, + "retainedSegmentCount": 3, + "continuousSegmentCount": 1, + "coverageComplete": true, + "fallback": false, + "lastReason": "none", + "fastPathEligible": true, + "fastPathActive": true, + "fastPathFallbackReason": "none", + "fastPathHit": 120, + "fastPathMiss": 3, + "fullCpuFrame": 4, + "styleLayers": [ + {"layerIndex":0,"layerId":"background","layerType":"background","visible":true,"sourceId":null,"sourceLayer":null}, + {"layerIndex":1,"layerId":"water","layerType":"fill","visible":true,"sourceId":"chart","sourceLayer":"water"}, + {"layerIndex":2,"layerId":"rn-weather-color","layerType":"weather-color","visible":true,"sourceId":"rn-weather-fields","sourceLayer":null}, + {"layerIndex":3,"layerId":"rn-weather-particles","layerType":"raster-particle","visible":true,"sourceId":"rn-weather-fields","sourceLayer":null}, + {"layerIndex":4,"layerId":"land-area","layerType":"fill","visible":true,"sourceId":"chart","sourceLayer":"land"}, + {"layerIndex":5,"layerId":"ais-layer","layerType":"symbol","visible":true,"sourceId":"ais","sourceLayer":null} + ], + "segments": [ + {"segmentIndex":0,"strategy":"RETAINED","configuredStartLayerId":null,"beginIndex":0,"endExclusive":2,"resolvedStartLayerId":"background","resolvedEndLayerId":"water","layerCount":2,"layers":[{"layerIndex":0,"layerId":"background","layerType":"background","visible":true,"sourceId":null,"sourceLayer":null},{"layerIndex":1,"layerId":"water","layerType":"fill","visible":true,"sourceId":"chart","sourceLayer":"water"}],"cache":{"cacheState":"reuse","contentRevision":19,"build":1,"reuse":120,"invalidation":0,"lastInvalidationReason":"none","textureCreate":1,"compositeCreate":1,"resourceRelease":0}}, + {"segmentIndex":1,"strategy":"CONTINUOUS","configuredStartLayerId":"rn-weather-color","beginIndex":2,"endExclusive":4,"resolvedStartLayerId":"rn-weather-color","resolvedEndLayerId":"rn-weather-particles","layerCount":2,"layers":[{"layerIndex":2,"layerId":"rn-weather-color","layerType":"weather-color","visible":true,"sourceId":"rn-weather-fields","sourceLayer":null},{"layerIndex":3,"layerId":"rn-weather-particles","layerType":"raster-particle","visible":true,"sourceId":"rn-weather-fields","sourceLayer":null}],"cache":null}, + {"segmentIndex":2,"strategy":"RETAINED","configuredStartLayerId":"land-area","beginIndex":4,"endExclusive":5,"resolvedStartLayerId":"land-area","resolvedEndLayerId":"land-area","layerCount":1,"layers":[{"layerIndex":4,"layerId":"land-area","layerType":"fill","visible":true,"sourceId":"chart","sourceLayer":"land"}],"cache":{"cacheState":"ready","contentRevision":22,"build":2,"reuse":80,"invalidation":1,"lastInvalidationReason":"layer-mutation","textureCreate":2,"compositeCreate":2,"resourceRelease":1}}, + {"segmentIndex":3,"strategy":"RETAINED","configuredStartLayerId":"ais-layer","beginIndex":5,"endExclusive":6,"resolvedStartLayerId":"ais-layer","resolvedEndLayerId":"ais-layer","layerCount":1,"layers":[{"layerIndex":5,"layerId":"ais-layer","layerType":"symbol","visible":true,"sourceId":"ais","sourceLayer":null}],"cache":{"cacheState":"reuse","contentRevision":11,"build":1,"reuse":40,"invalidation":0,"lastInvalidationReason":"none","textureCreate":1,"compositeCreate":1,"resourceRelease":0}} + ] +} +``` + +有效 snapshot 保证 Segment layers 拼接等于 styleLayers,Layer 不重号、不遗漏、不重复。RN Test Host 的按钮只在用户操作时配置/拉取并显示一次完整 JSON,不轮询、不逐帧打印。 + +## 10. 当前限制 + +- 多 Continuous 只保证正确 full renderer,不提供高级 Particle Fast Path。 +- Source/Tile mutation 尚无通用 dependency graph,仍全 retained invalidation。 +- Legacy diagnostics 只是兼容摘要,不能表达任意 N Segment。 +- OpenGL/Metal 编译、Native tests、Android codegen/Gradle 与设备运行由 RDC 独立验证后才能形成 PASS 事实。 diff --git a/docs/30_设计/STEP-097_V13三轮问题去重与修复矩阵_v1.md b/docs/30_设计/STEP-097_V13三轮问题去重与修复矩阵_v1.md new file mode 100644 index 000000000..80ff5acb2 --- /dev/null +++ b/docs/30_设计/STEP-097_V13三轮问题去重与修复矩阵_v1.md @@ -0,0 +1,76 @@ +# STEP-097 V13 三轮问题去重与修复矩阵 v1 + +基线:Mac-mini-tei.lan;`particle/main`;起始 `720d309364628f85049cf9969d71c71e8dfacce9`,与查询时 `origin/particle/main` 一致;工作区干净。冻结 `.13` tag 指向 `ee0b89e71`。该 HEAD 相对任务书的 `b5c1c4879` 只新增 STEP-097 任务文档。Integration 本地 C-001/RFC-001 已只读;其远端是否最新尚未由 RDC 证实。所有构建、测试和运行验证:**NOT RUN**。 + +## 公共不变量 + +`fieldId/generation/validTime`、东向 U/北向 V、T0/T1/MIX、旧 T1→新 T0 复用、Style 全局 Z 顺序、每个 R 独立缓存、WeatherColor 与 Particle 分离、Host 拥有天气获取。无权改变 C-001 的公开生命周期、错误或数据所有权语义。 + +以下“入口”均为待执行回归,不能当作运行结果。源码状态随阶段更新;`WAITING_RDC_BUILD_AND_RUNTIME` 表示仅完成定位,尚未实现。 + +| # / 原编号 | 当前源码定位与最短触发 | 反例/安全约束 | 拟修文件与回归入口 | 状态 / commit / 平台待测 | +| --- | --- | --- | --- | --- | +| 01 N01/B01 | `paint_parameters.cpp:185` tile 集合相同跨 R-R target 复用 CPU mask | 同一未清 stencil target 可复用 | PaintParameters、Renderer、RenderTarget 已在新目标重置;同 tiles R-R/R-C-R 像素入口尚缺 | WAITING_RDC_BUILD_AND_RUNTIME / `e67e4961b` / GL、Metal | +| 02 N02/B02 | `mtl/render_pass.cpp:21` 仅 color clear;`offscreen_texture.cpp:67` 附件创建 | 无 depth/stencil 的 target 不可盲设附件 | Metal descriptor/combined 附件已修改;专用附件故障测试源码尚缺 | WAITING_RDC_BUILD_AND_RUNTIME / `e67e4961b` / Metal | +| 03 N03/B12 | `render_orchestrator.hpp:156` catch;`renderer_impl.cpp:824` 仍 mark success | 失败后 full fallback、下帧重试 | visitor 错误标志与 fallback 已接入;异常注入测试源码尚缺 | WAITING_RDC_BUILD_AND_RUNTIME / `e67e4961b` / GL、Metal | +| 04 N04/B06 | `Map::Impl::onUpdate()` 在 Continuous update 驱动每个 RasterArraySource;Source 使用 monotonic tick,并用 `forecastTickInProgress` 阻止同步 SourceObserver 重入二次推进 | 显式 setTime/PLAY/PAUSE/rate 由一个 Core clock owner 推进;平台 timer 只唤醒 | `map_impl.cpp`、RasterArraySource、Android/iOS WeatherRuntime;`RasterArrayV4ProductionClockTicksOnceForBothBindingsAndResetsOnCommands`、`RasterArrayV4ClockSourceNotificationReentryDoesNotAdvanceTwice` | CODE_IMPLEMENTED_STATIC_CHECKED / A2 源码提交 `f49ffa68a` / OpenGL 与 Metal host A2 定向 3/3 PASS;FZG1 V4 Golden PLAY/PAUSE/速率/clear/reload PASS;无 iOS runtime(用户豁免) | +| 05 B13 | `sameFieldGpuContent()` 保持 revision/version 无关的 payload 相等;renderer GPU 槽位更新使用严格 `sameFieldGpuEndpoint()`;Source mix-only cache 使用 projection identity、binding、immutable frame 和 plane/validity storage pointer identity | mix-only 复用同一 T0/T1 snapshot;frame/revision、grid、validity、U/V、encoding 或 binding 改变则重物化并失效;invalid/clear fail-closed | `field_data_adapter.hpp`、projection materialization、RasterArraySource 与 renderer;Source cache 测试 + `ParticleGpuField.*` | CODE_IMPLEMENTED_STATIC_CHECKED / A2 源码提交 `f49ffa68a` / broad OpenGL 与 Metal host 各 121/121 PASS,A2 定向各 3/3 PASS;FZG1 stable mix 时 upload/reset 计数稳定;clear/reload 后合法增加 | +| 06 B07 | 新增内部 signed RGBA8 编码,byte 128 明确表示零;GL/Metal shader 以 high precision 还原 byte | 不改公开 QBIN/FieldProfile encoding;量化误差与 sub-half-quantum 微速边界明确测试 | `particle_gpu_math.hpp`、GL/Metal shader 源及生成头、`particle_gpu_math.test.cpp` | CODE_IMPLEMENTED_STATIC_CHECKED / A2 源码提交 `f49ffa68a` / broad OpenGL 与 Metal host 各 121/121 PASS,含 signed zero、取消、NaN/invalid 与微速边界;两次 shader generator 幂等;无 iOS runtime | +| 07 B08 | 统一 helper 与 GL/Metal 粒子位移路径都为 Mercator 东向 U、北向 V 应用相同 cos(latitude) metric,并夹紧极区分母 | 保持 U/V canonical m/s、V 北向为正、既有视觉时间倍率 | `particle_gpu_math.hpp`、renderer 与 GL/Metal shader 源及生成头;0°/45°/60° 四方向和极区测试 | CODE_IMPLEMENTED_STATIC_CHECKED / A2 源码提交 `f49ffa68a` / broad OpenGL 与 Metal host 各 121/121 PASS,含数学定向测试;Android FZG1 场景闭环通过;无 iOS runtime | +| 08 N05 | 长度感知 JSON 解析,拒绝 NUL/重复键/非法 schema,invalid fail-closed | 清旧计划、合法恢复 | `render_segment_json.hpp`、双平台 bridge 与 GTest | CODE_IMPLEMENTED / B Core 定向 6/6、扩展 139/139 PASS;待 B Git,iOS runtime 未测 | +| 09 N06 | FieldProfileValidator 在 Store commit 前检验有效数值及 scale/offset | validity=0/noData 合法;坏高 revision 不覆盖好帧 | FieldProfileValidator/Source GTest,修正旧 16-bit fixture | CODE_IMPLEMENTED / B Core 定向 6/6、扩展 139/139 PASS;待 B Git | +| 10 N07 | View owner、generation guard、map destroy observer 源码已写 | 旧 map 回调不得影响新世代,监听须释放 | Kotlin/Java JUnit,真实 MapLibreMap.onDestroy | Android Native JUnit 2/2、Java 编译与 owner 隔离 JVM JUnit 4/4 PASS;BLOCKED_NATIVE_AAR_COMPATIBILITY:正式 `.13` 缺新 Map 方法,默认 RN consumer 不可配对编译,B 不判 PASS | +| 11 F01/C01 | `renderer_impl.cpp:505` full diagnostics 快照 | 外部 pull schema 不变 | runtime/renderer;快照计数 | WAITING_RDC_BUILD_AND_RUNTIME / 待提交 / Core | +| 12 F02/C02/B03 | Metal offscreen `swap()` 同步等待 | 不删不明依赖 wait | Metal;GPU capture | DEFERRED_DESIGN / 无 / Metal | +| 13 F03/C03/B04 | cache revision 变化释放 texture | target clear 必须先正确 | cache;同尺寸重绘/resize | WAITING_RDC_BUILD_AND_RUNTIME / 待提交 / GL、Metal | +| 14 F04/C04 | R 数 × viewport 独立附件内存 | 保留独立 R 与失败 fallback | cache;资源阶梯、失败注入 | DEFERRED_DESIGN / 无 / GL、Metal | +| 15 C04/B11 | encoded Store 无总 key/byte 预算 | 不静默驱逐绑定窗口 | Store;容量/旧读者 | DEFERRED_DESIGN / 无 / Core | +| 16 F07/C05 | `render_orchestrator.hpp:156` 每段前缀 distance | 全局 ordinal 与 Z 顺序不变 | orchestrator;重排/正反遍历 | WAITING_RDC_BUILD_AND_RUNTIME / 待提交 / Core | +| 17 C06/B05/B10 | A2 已处理相同 immutable projection 的 mix-only 重物化;其余 diagnostics pull、ordinal 与 UI owner 世代发布仍属后续范围 | A2 mix-only 不再复制/比较整张浮点快照;不据此宣称 C 阶段整体完成 | A2 部分:RasterArraySource cache 定向测试及 FZG1;C 部分仍按原任务另行实施 | A2 mix-only 子项已验证 / 当前工作树 `5bb1a0e` / OpenGL、Metal host 定向 3/3 PASS;C 其余项目未开始 | +| 18 C07 | drawable/tweaker tick 重建、端点复制 | 不保留 stale GPU 引用 | Particle renderer;tick/resize | DEFERRED_DESIGN / 无 / GL、Metal | +| 19 F09/O01 | `runtime_state.hpp:230` 新 topology;旧 cache 析构按 index 计数 | 总量与当前 topology 分离 | runtime/cache 按 revision 过滤;新增 `OldCacheReleaseCannotChargeReplacementAtSameIndex` GTest | CODE_IMPLEMENTED_STATIC_CHECKED / `e67e4961b` / 2026-09-26 Core GTest 1/1 PASS,诊断组 5/5 PASS;GPU 资源验收仍待测 | +| 20 F05/E01 | Test Host package/lock 已对齐正式 `.13`,SHA-256/SRI 核对一致 | RN/Native 必须精确同版,不改 `.13` 产物 | package/lock、release manifest、默认 RN 编译 | STATIC_VERSION_LOCK_PASS;B10 新 Native method 不在正式 `.13`,默认消费者仍阻塞 | +| 21 F06 | `renderer_impl.cpp` multi-C 走 full CPU | 保留安全 fallback | 后续 A/B 设计 | DEFERRED_DESIGN / 无 / GL、Metal | +| 22 F08 | source/tile 等全局失效 | 无依赖图前不缩窄 | 后续依赖设计 | DEFERRED_DESIGN / 无 / Core | +| 23 F10/E02 | Integration status/compatibility 仍写 `.5` | 发布≠NavSea 支持 | 负责人协调证据请求 | DEFERRED_DESIGN / 无 / 跨项目 | +| 24 STEP-095 §7/B09 | synthetic mask 非共享地理 mask | 不新建公开 mask API | C-001/RFC/ADR 协调 | DEFERRED_DESIGN / 无 / 跨项目 | + +## 操作次数模型(静态,非性能实测) + +- A1:跨 target 清除 CPU mask 为每次目标切换一次 O(1) 状态清理;当前同 tiles 可能零次重绘但产生错误像素。Metal clear 每个 pass 最多各一次附件 load 设置。异常后额外一次 full fallback,避免错误缓存跨帧复用。 +- A2:PLAY 由唯一 Map/Source clock 每次 update 推进一次;V4 mix-only 的投影重物化计数为 0(50 次 Source tick 定向测试),decoded endpoint 变化时 color/particle projection 各物化一次。FZG1 日志在单次 stable mix-only epoch 内 upload/reset 计数不变;clear/reload 会合法增加计数。以上是源码测试和该次设备记录,不构成普遍 GPU 性能结论。 +- B:JSON 严格校验 O(输入字节长度),有效数值验证 O(有效 cell × plane 数),View/map 每世代各至多一活跃监听;源码已实施,但正式 .13 Native ABI 不含新方法。无 GPU 性能实测。 +- C:预期 full diagnostics 深复制从每 render 次降到每外部 pull 次;ordinal 前缀从每段 O(G) 降到 topology 更新 O(G);同时间物化从 O(cell) 降为 O(1)。尚未实施。 + +## 2026-09-26 第 19 项实测更新 + +- RDC 使用 `0d13552be` 重编译并执行 GTest,第 19 项 1/1 PASS、RenderSegmentDiagnostics 全组 5/5 PASS;详见 `docs/报告/STEP-097_A1_第19项GTest运行验证报告_v1.md`。其他问题状态不因此变化,A1 仍为 PARTIAL。 + +## 2026-09-26 第 19 项 FZG1 设备辅助证据 + +- 基于相同生产源码的 APK 在 FZG1-2 通过覆盖安装和设备 SHA-256 校验。两轮拓扑循环、重排、缓存 OFF/ON、缺失边界回退与恢复的 20 张截图及 UI 诊断已保存;有效场景的 B/T 粗粒度 textureCreate-resourceRelease 均为 0 或 1。此证据只支持 Android 运行态冒烟,不替代此前旧 revision 归属的 GTest 1/1 PASS,也不能证明 GPU 无泄漏。详见 `docs/报告/STEP-097_A1_第19项FZG1运行验证报告_v1.md`。A1 仍因 01/02/03 未闭合保持 PARTIAL。 + +## 2026-09-26 A1 专用入口补充(待 RDC 执行) + +- 01:Android Validation Activity 的 STEP-093 菜单新增 STEP-097 same-source stencil 子菜单;`Step097StencilFixture` 用一个本地 GeoJSON source 与四个 fill layer 生成 R-R、R-C-R、C-R-R,菜单可重载与读 segment 诊断,既有 CACHE ON/OFF 可切换。RDC 仍需首帧/重建/reuse 的实际像素截图与 cache 计数;此处仅是可操作 fixture,未实测。 +- 02:`mtl_retained_attachments.test.cpp` 检查实际离屏资源的 color/depth/stencil 绑定、simulator/x86 combined attachment、无 depth/stencil 时跳过 clear;生产 `RenderPass` 共用的 clear 辅助函数可检查 load/clear/store。设备资源创建失败与 encoder 失败的运行注入仍待 RDC 验证。 +- 03:`render_orchestrator_error.test.cpp` 注入正/反向范围 visitor 异常,核对 observer/failed 标志,并用 cache 状态机核对失败后继续 Build、成功重试后 Reuse。真实 GPU 绘制故障与完整 renderer fallback 仍待 RDC 注入验证。 +- 01/02/03 均保持 `WAITING_RDC_BUILD_AND_RUNTIME`;源码入口准备不等于编译或运行 PASS。19 的既有实测状态不变。 + +## 2026-09-26 A1 Android/Core 实测收口 + +- 01:same-source tiled fixture RR/RCR/CRR 两轮、rebuild/cache OFF/ON 共 18 点,屏幕中心与区域外采样两组像素均 18/18 一致;状态 valid=true/fallback=false,Android FZG1 实测通过。源码/测试提交 `d0ced7235`,编译修正 `8c006ac83`。 +- 02:Metal host runner 已编译,GTest 两个测试均因没有 Metal device SKIP;用户决定不执行 iOS 运行测试,记 `USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`,不得称 Metal 实测 PASS。 +- 03:`RenderOrchestratorError.*` 原生测试 2/2 PASS;真实 Android GPU fault injection 不在本次证据中。 +- 19:前述 Core 1/1 PASS、诊断组 5/5 PASS、FZG1 辅助测试均保留。A1 在 Android/Core 验收及 iOS 用户豁免口径下进入 A2;详细事实见 `docs/报告/STEP-097_A1_Android专项验收报告_v1.md`。 + +## 2026-09-26 A2 RDC 最终验证 + +- 04–07 已在同一 dirty 源码工作树完成实现与 RDC 独立验证。旧 `sameFieldGpuContent()` 测试断言修复后原 58 项 58/58 PASS;更新后的 broad filter OpenGL 与 Metal host 各 121/121 PASS,三项 A2 Source 定向测试两平台各 3/3 PASS。 +- 标准 Android Validation APK 为 OpenGL Debug/x86_64,61,753,803 bytes,SHA-256 `27bf4a6cc191bd86c4d1c10398478d81c2999edd61fe7242e40e5188516b0ca5`;安装到 FZG1-2 后设备 APK hash 相同。V4 Golden Wind test-only T0/T1 场景 17 张 PNG、24 个步骤、runner exit 0、ANR=0;PLAY 30 秒 UI 可响应,PAUSE 稳定,clear/reload 后再次 PLAY/PAUSE。 +- 日志按资源窗口分别记录:clear/reload 前 mix-only epoch `fieldUploadCount=6/resetCount=2` 稳定;explicit clear/reload 后计数合法增加,达到 `fieldUploadCount=10/resetCount=4`,此后 replay 稳定。不可把 clear/reload 期间的上传与 reset 写成零。 +- build source diff SHA-256 `18927a5f30a88f5c1c3a00e0017473dc33b04151c227e4baf52250dc4ad2e6a2` 与忽略目录中的 APK source snapshot 完全一致。未运行 iOS device runtime,按用户豁免记 `USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`;不得称 Metal host GTest 为 iOS PASS。A2 Android/Core 完成,B/C 未开始,整个 STEP-097 仍 PARTIAL。 + +## 2026-09-26 B 独立验证及阻塞 + +- RDC Core 定向 4/4、修正旧 16-bit fixture 后 broad 134/134 PASS;正式 .13 Android SDK 缺 B10 新 map destroy observer,默认 RN consumer 无法与该新源码配对。B PARTIAL,C 不启动;详见 B 独立报告。 diff --git a/docs/30_设计/STEP-098_QBIN_V4单帧与八帧滚动缓存设计_v1.md b/docs/30_设计/STEP-098_QBIN_V4单帧与八帧滚动缓存设计_v1.md new file mode 100644 index 000000000..5aa923b01 --- /dev/null +++ b/docs/30_设计/STEP-098_QBIN_V4单帧与八帧滚动缓存设计_v1.md @@ -0,0 +1,76 @@ +# STEP-098 QBIN V4 单帧与八帧滚动缓存设计 + +日期:2026-09-26 +状态:**已按接受的设计契约实现源码;Native 构建、定向测试与平台运行验收待 RDC 执行**。 + +## 1. 权威契约 + +- RFC-004:`/Users/tei/dev/navsea-particle-integration/rfc/RFC-004_QBIN_V4单帧首显与八帧滚动缓存.md` +- ADR-001:`/Users/tei/dev/navsea-particle-integration/adr/ADR-001_QBIN_V4单帧与滚动缓存语义.md` +- Contract C-002:`/Users/tei/dev/navsea-particle-integration/contracts/C-002_QBIN_V4单帧与滚动缓存.md` +- COORD-005:`/Users/tei/dev/navsea-particle-integration/coordination/active/COORD-005/决议.md` +- 读取时 Integration `main` / `origin/main` 均为 `1dd576d3304e267e53ed64d31f9711d2350ce10a`。四份文件为 ACCEPTED / DESIGN ONLY 或 IMPLEMENTATION_PENDING,不代表实现、设备或 V15 发布通过。 +- 原任务 v1 的旧 RFC-003 闸门和 `docs/报告/STEP-098_公共契约阻塞报告_v1.md` 属于此前状态。用户已确认旧 RFC-003/COORD-004 B 作废删除;本次不恢复、不改写任务书或旧报告。 + +八帧时间示例按 C-002 为 **13:00–20:00**。提交 21:00 后删除最早端 13:00。等距离时固定删除原最早端。 + +## 2. Core 存储与身份 + +`EncodedFieldStore` 按 `fieldKey` 保存独立 `FieldSeries`,默认每个序列容量 8。每个序列按 `validTimeMs` 严格升序存储;插入新帧使用 lower-bound 定位,不假定时间间隔。重复时间继续走 Duplicate、revision、stale、identity conflict 现有判定,不触发容量淘汰。 + +为同时支持新预报批次单帧首显和防止跨批次混插,分开处理两类兼容性: + +- **同 key 共存兼容性**:grid、validity plane schema、所有 data plane 的数量与 schema 相同。不同 `fieldID`、QBIN `referenceTimeMs` 或解析 profile 的批次可以在序列中共存;否则有效的新日/新批次单帧可能无法进入序列。 +- **时间插值兼容性**:除共存 schema 相同外,`fieldID`、`referenceTimeMs`、`profileID` 和 `profileVersion` 必须全部相同。resolve 只在相邻时间端点满足这些条件时返回双帧;不同批次之间的时间返回 unavailable。Ingress 会把已验证的 profile 身份保留到解码帧,避免校验后丢失身份。 + +满额插入先复制现有有序序列,在候选序列上执行淘汰和新增端点 schema 校验。候选有效后才 swap 到正式序列;grid/plane schema 不兼容时返回既有 `RejectedIncompatible` 路径,不淘汰现有帧。对容量 8 的正常有效插入返回既有 `AcceptedInserted`;`RejectedCapacity` 枚举与 RN 字符串继续保留兼容,不用于正常满额滚动。 + +对提交时间 `n` 和原序列最早/最晚端点 `e/l`,淘汰较远端:`|n-e| >= |l-n|` 时淘汰原最早端,否则淘汰原最晚端。距离计算避免无符号时间相减下溢。该规则不读取 forecast clock,所以仍符合 RN 先 submit、后 setTime 的顺序。 + +## 3. 单帧、双帧与投影 + +- 精确匹配某个 `validTimeMs` 时 resolver 返回同一个不可变 frame 作为 T0/T1,`mix=0`。颜色和有合法 U/V 或方向分量的粒子可从该帧物化;标量帧仍不能通过向量绑定校验。 +- 非精确时间只在合法的相邻双帧 bracket 内插值,mix 按两端实际时间比例计算。落在窗口外、跨 batch/profile 边界或只有一帧但时间不匹配时返回 unavailable;不回退为最近帧。 +- 后续帧到达只更新实际变化的投影。新帧位于当前 bracket 外时保留 Source 的不可变物化 projection 与样本时刻;当前端点被淘汰后重新 resolve,原投影不可用则清除。 +- 单帧 materialization 只创建一个 `ParticleFieldSnapshot` 并让 T0/T1 共用它。Particle 和 WeatherColor 的首次资源安装也共享相同 endpoint 的 GPU texture/image/scale;GL 与 Metal 共用这段 renderer 代码及语义。设备资源实际行为仍须各平台独立验证。 +- 淘汰仅释放 Store 对已移除 frame 的引用。已发布 projection 或 GPU 更新仍持有共享 snapshot/texture 引用时,按 shared ownership 生命周期释放。 + +## 4. 内存观测范围 + +新增 `DecodedFieldFrame::decodedByteSize()`、`FieldSeries::decodedByteSize()` 与 `EncodedFieldStore::decodedByteSize()`,返回 validity bitset 加所有非 validity 原始 plane storage 的字节总和;共享 validity plane storage 不重复计数。该计数**不包含** profile/容器元数据、物化 Float32 snapshot、图像、GL/Metal texture、粒子 state 或 trail。 + +公式为单帧 `ceil(cellCount / 8) + Σ(plane payload bytes)`;序列与 Store 分别对帧及 key 求和。Native 定向测试代码覆盖滚动时每 key 保持 8 帧及此原始字节计数回落/清空,但本轮没有运行测试,也没有 CPU/GPU 峰值、32 帧总量或长期泄漏实测。8 帧不构成全局内存保证;不新增固定 MiB 上限或公开容量错误码。 + +## 5. API、职责与平台 + +RN TypeScript、Android Kotlin/JNI、iOS Objective-C++/Facade 保持 `submitEncodedField(...)` 与 `setEncodedForecastTime(...)` 两步调用及原有方法名、参数和结果名称;Codex 静态核对现有传递路径,本轮没有改 bridge。submit 不修改目标时钟,setTime 不下载数据。两调用仍非原子,不承诺无闪烁。 + +`DecodedFieldFrame` 增加内部解析 profile 身份字段及 decoded-byte 计数方法,改变该 C++ 数据结构的二进制布局;不改变 RN/Android/iOS bridge 的公开方法签名或结果枚举。Core 与两个平台必须使用同一源码重编;不得把既有 `.13` AAR 或旧发布物当作新 ABI 消费证据。 + +NavSea、Server、下载/cache/manifest、日期 UI、QBIN 格式、Rice 解码、V3、Style Z-order、provider、公开 RN API 均未改。 + +## 6. 定向测试映射 + +以下是本轮新增或扩展的测试代码覆盖,不代表已执行: + +| 项目 | 测试入口 | 本轮代码覆盖 | +| --- | --- | --- | +| T01 | `ParticleFieldBinding.SingleFrameProjectsAtItsExactTimeAndUpgradesToInterpolation`;`Source.RasterArrayV4SingleExactFrameMaterializesAndSecondFrameEnablesInterpolation` | 单帧精确颜色/向量 projection、T0=T1、mix=0、第二帧中点升级;标量-only 无向量绑定 | +| T02 | 同上 | 第二帧到达后返回双帧与 mix=0.5 | +| T03 | `ParticleFieldRuntime.UnorderedSubmissionsAreSortedAndReferenceCyclesStaySeparate` | 乱序排序;wind/wave 分 key;跨 reference/profile 只可单帧解析 | +| T04 | `ParticleFieldRuntime.FullWindowRollsBothDirectionsAndBreaksTiesAtEarliest`;`Source.RasterArrayV4RollingWindowPreservesAnUnchangedActivePair` | 满 8 后向未来提交、淘汰最早端并保持活动 pair | +| T05 | `ParticleFieldRuntime.FullWindowRollsBothDirectionsAndBreaksTiesAtEarliest`;Source rolling 测试 | 满额向过去提交并淘汰最晚端 | +| T06 | `ParticleFieldRuntime.FullWindowRollsBothDirectionsAndBreaksTiesAtEarliest` | 中部等距插入固定淘汰最早端、有序且 8 帧 | +| T07 | `Source.RasterArrayV4StoresNewReferenceAndProfileAsSingleFrameWithoutCrossBatchMix` | 新 reference/profile 批次精确首显、补帧后同批插值、回到早批次单帧 | +| T08 | `ParticleFieldRuntime.RevisionIdentityAndIndependentCadence`;既有 Source stale/revision cases | Duplicate、identity conflict、revision;既有 stale 路径保留。满额下 revision 不占槽由同一 Store 分支处理 | +| T09 | `Source.RasterArrayV4HostApiRejectsKeepPublicProjectionStable`、`...RejectsNumericOverflowBeforeReplacingAcceptedRevision`、`ParticleFieldRuntime.RejectedFullWindowInsertDoesNotEvictAndKeysRemainIndependent` | 解码/profile/数值和 grid schema 拒绝路径、有效帧/投影/clock 零副作用。数据帧入 Store 前仍由现有 ingress 完成解码与 profile 校验 | +| T10 | `ParticleFieldBinding.MixChangesSampleStateButNotContentIdentityOrStorage`、单帧升级 Source 测试 | 合法 bracket 的中间比例;精确端点以单帧语义表示 | +| T11 | 既有 binding `WindowUnavailable` assertions;新单帧测试 | 非覆盖时间 unavailable,不用最近帧顶替 | +| T12 | `Source.RasterArrayV4RollingWindowPreservesAnUnchangedActivePair` | 外围补帧/双向滚动不改变当前物化 projection 与 requested time | +| T13 | `Source.RasterArrayV4EvictedActiveFrameClearsItsMaterializedProjection` | 当前单帧端点淘汰后 fail-closed 清除物化 projection | +| T14 | `ParticleFieldRuntime.RejectedFullWindowInsertDoesNotEvictAndKeysRemainIndependent` | 两个 key 各自 8 帧,独立计数与字节 | +| T15 | 单帧 binding/materialization 测试;GL/Metal renderer 资源安装源码 | T0/T1 共用物化 snapshot;renderer 首装共用 endpoint texture/image。GPU/context-loss 运行验证未覆盖 | +| T16 | `ParticleFieldRuntime.RepeatedBidirectionalRollingAndClearStayBounded` | 120 次双向提交、每 key 8 帧边界、clear/clearAll 和 decoded bytes 归零 | +| T17 | 现有 `FieldRuntime.ts`、`NativeWeatherRuntimeBridge.ts`、Android/iOS bridge 源码静态核对 | 旧 API、类型、方法名和两步语义未变;RN 自动化/双平台 consumer build 未运行 | + +所有测试代码的执行状态均为 **NOT RUN**。完整执行状态与 RDC 命令见 STEP-098 完成报告。 diff --git a/docs/30_设计/WeatherV2_QBIN_V3通用天气场二进制格式与React独立Render_Binding设计_v2.2_最终冻结稿.md b/docs/30_设计/WeatherV2_QBIN_V3通用天气场二进制格式与React独立Render_Binding设计_v2.2_最终冻结稿.md new file mode 100644 index 000000000..f2b067091 --- /dev/null +++ b/docs/30_设计/WeatherV2_QBIN_V3通用天气场二进制格式与React独立Render_Binding设计_v2.2_最终冻结稿.md @@ -0,0 +1,4444 @@ +# WeatherV2 QBIN V3 通用天气场二进制格式与 React 独立 Render Binding 设计 + +文档修订:v2.2 +QBIN Container Version:3 +Magic:`QBV3` +日期:2026-09-20 +状态:设计冻结候选 / 可进入 Writer + C++ Decoder Prototype;正式发布前仍需冻结 Block Geometry、完成 Round-trip 与 Native 性能验证 +适用范围:WeatherV2 → NavSea → MapLibreParticle +上一代权威基线:`WeatherV2_QBIN_V2通用天气场二进制格式与React独立Render_Binding设计_v4.4.md` +实施前补充依据:`WeatherV2 QBIN V3 实施前补充要求(给 Weather 生产方) v1.0` + + + + + +v2.2 相对 v2.1 的最终文档清理: + +```text +1. 修正验收表对 non-finite physical sample 的表述: + validity=1 时 physical MUST finite; + validity=0 时 numeric physical value 不参与 Quantizer, + 可为 NaN / Inf / missing。 + +2. 将 Corruption / Fail-closed 验收拆分为: + Binary Reject Cases + Binary Accept Cases + Writer Validation Cases + 避免把 MUST accept / Writer-only 行为误写成统一 fail-closed。 + +3. 不再新增任何 QBV3 Binary ABI 设计项。 + 除最终 BLOCK_WIDTH / BLOCK_HEIGHT 仍需 benchmark 后冻结外, + v2.2 作为进入 Writer + Decoder Prototype 的最终设计冻结稿。 +``` + +v2.1 相对 v2.0 的最终收口重点: + +```text +1. Rice canonicality 正式冻结为 Writer-canonical / Decoder-permissive: + Writer MUST 计算并写入最优 k*; + Decoder 只验证 riceK∈[0,31] 与 bitstream / symbolCount / padding / + residual / raw range 等合法性,不为 canonicality 重算 32 个候选 k。 + 非最优但可合法解码的 riceK 不属于 QBV3 binary corruption。 + +2. WEB_MERCATOR + NODE 的数学定义补全: + 直接冻结 dimensionless spherical Web Mercator + X=λ,Y=ln(tan(π/4+φ/2)), + 并给出 inverse 公式,消除“Mercator”实现歧义。 + +3. VALIDITY semantic 增加双向约束: + semantic==3 ⇔ valueClass==VALIDITY ⇔ dtype==BITSET; + semantic=3 不得被 Numeric Plane 复用。 + +4. Quantizer 增加 intermediate finite 要求: + delta64 / normalized64 / normalizedDirection64 MUST finite; + 否则 Writer FAIL。 + +5. fieldType=0 明确冻结为 UNKNOWN / UNSPECIFIED opaque hint, + binary-valid,不得单独导致 reject。 + +6. 清理正文中遗留的 v1.9 / v2.0 临时字样,并连续整理后半部章节编号。 + +7. v2.1 之后,除最终 BLOCK_WIDTH / BLOCK_HEIGHT 仍需通过 + 16×16 / 32×32 C++ benchmark 实测选择外, + 不再保留其他核心 QBV3 Binary ABI 未决语义。 +``` + +v2.0 相对 v1.9 的最终收口重点: + +```text +1. Invalid-cell Writer 语义冻结: + validity=1 的 physical sample MUST finite; + validity=0 的 physical payload value 被忽略,可为 NaN/Inf/missing, + 不参与 Quantizer、不产生 Rice symbol,decoded canonical raw 固定为 0。 + Writer 不得通过 NaN/Inf 自动推导 validity=0。 + +2. ZigZag / inverse ZigZag 冻结为精确数学公式, + 不再依赖“标准 ZigZag”约定或实现自行解释。 + +3. Quantizer binary64 计算语义收口: + Descriptor binary32 scale/offset 先提升为 binary64; + subtraction / division 使用 IEEE-754 binary64 round-to-nearest, + ties-to-even 语义后,再执行 round_half_away_from_zero。 + Direction Writer 要求 canonical input 已归一化到 [0,360), + QBV3 Writer 不承担天气输入清洗。 + +4. unknown fieldType 的 Binary 行为冻结: + fieldType 是 opaque uint16 hint; + 未知值 MUST NOT solely cause binary rejection, + 保证所有 conforming Decoder 对同一文件得到一致 Accept/Reject。 + +5. Numeric offset canonical zero 规则全局化: + 任意 Numeric Plane 若 offset 数值等于 0, + MUST 使用 binary32 +0.0;-0.0 MUST reject。 + +6. Validity BITSET 增加精确索引公式: + byteIndex=index>>3, bitIndex=index&7, + valid=(byte>>bitIndex)&1。 + +7. 修正文档遗留: + §17 的 v1.8 字样统一为 v2.0; + v2.0 之后,除 BLOCK_WIDTH/BLOCK_HEIGHT benchmark freeze 外, + 不再保留其他核心 Binary ABI 未决语义。 +``` + +v1.9 相对 v1.8 的收口重点: + +```text +1. round_half_away_from_zero 冻结为明确数学公式; + Quantizer 使用 Descriptor 已确定的 binary32 scale/offset 提升到 binary64 计算, + non-finite physical sample Writer MUST reject。 +2. 补齐 Descriptor byte-level structural invariants 与必要机器值: + valueClass / component / core unit / VALIDITY semantic 的 Container 约束集中定义。 +3. referenceTimeUtc / validTimeUtc 只要求各自为正 Unix UTC seconds; + QBV3 Container 不规定 validTimeUtc 与 referenceTimeUtc 的相对大小。 +4. CRC32 参数完全展开: + CRC-32/ISO-HDLC,poly=0x04C11DB7、reflected poly=0xEDB88320、 + init=0xFFFFFFFF、refin/refout=true、xorout=0xFFFFFFFF。 +5. Block partition 数学公式冻结; + 最终只需在 16×16 / 32×32 benchmark 后回填 BLOCK_WIDTH / BLOCK_HEIGHT。 +6. 当前 Parser conformance limits 继续作为 QBV3 Version 3 normative acceptance limits; + 现阶段容量确认够用。未来如需扩大,不在已发布 Version 3 上静默放宽, + 应通过后续 Container Version / successor spec 明确升级。 +7. 将 ContentIdentity、QBV2 legacy validator、Manifest、Distribution、 + Runtime/Binding 等非 Binary ABI 内容进一步移出 normative 主体, + 只保留为 Non-normative Appendix。 +8. v1.9 之后,Block Geometry 是正式发布前唯一尚未通过实测冻结的核心 Binary ABI 参数。 +``` + +v1.8 相对 v1.7 的收口重点: + +```text +1. 明确 QBV3 binary-valid 与外部 Profile-valid 是两层: + QbinV3Decoder 只负责 Container / byte-level validation; + Profile Contract validation 属于 Encoded Field Ingress, + 不属于 QBV3 Binary ABI,也不得被称为 binary corruption。 +2. 在 normative 主体补全 DType byte-level enum: + 1=INT16, 2=UINT16, 3=UINT8, 4=INT8, 5=FLOAT32, 6=BITSET; + QBV3 Numeric 仅允许 1/2/3/4,Validity 固定 6,FLOAT32 在 V3 第一版 reject。 +3. 补全 fieldType byte-level machine hint 语义与已知值; + fieldType 不得决定 Plane layout / Profile completeness / Rice 解释。 +4. 补全 WEB_MERCATOR + NODE Grid 的精确空间含义: + Header bounds 为 WGS84 lon/lat degrees, + Grid sample 在 Web Mercator projected space 等间距; + row0=north,col0=west,index=row×width+col。 +5. 冻结 QBV3 Parser conformance limits: + MAX_PLANE_COUNT=32、 + MAX_CELL_COUNT=2,097,152、 + MAX_QBIN_BYTES=64 MiB、 + MAX_DECODED_BYTES_PER_FRAME=32 MiB。 +6. Header float64 Bounds 增加 canonical zero: + 数值为 0 时 MUST serialize 为 IEEE-754 binary64 +0.0, + 禁止 -0.0,保证 byte-for-byte deterministic Writer。 +7. Block Geometry 仍是唯一尚未冻结的核心 Binary ABI 参数; + v1.8 不擅自选择 16×16/32×32,正式 production QBV3/version=3 + 仍必须等 C++ benchmark 后回填唯一尺寸。 +``` + +v1.7 相对 v1.6 的收口重点: + +```text +1. 文档规范范围重新收口到 QBIN V3 Binary Container 本身; + NavSea / React / Store / Binding / Renderer / 平台 Bridge / Contract JSON + 等消费与集成内容移出 normative 主体,仅保留为 Non-normative Integration Notes。 +2. Quantization 冻结为“先序列化 float32,再用该 float32 值量化”: + Descriptor 中 scale / offset 的 IEEE-754 binary32 bit-pattern + 是 Quantizer 的唯一参数来源,禁止使用序列化前更高精度配置值直接量化。 +3. Shared Validity 明确采用两阶段逻辑解析: + 先解析全部 Descriptor 并定位唯一 Validity Plane, + 再验证/读取 Validity,最后解 Numeric Rice Plane; + 不要求 Validity 在 payload 中物理排在 Numeric Plane 前面。 +4. Numeric Plane byteLength 精确定义为所有 non-empty block 的 + `1-byte riceK + ceil(RiceSymbolBits/8)` 之和, + 不包含 Plane 间 align16 padding、Descriptor 或其他 Plane。 +5. Rice k 的 deterministic 选择公式正式冻结: + `bits(k)=Σ((z_i>>k)+1+k)`,k∈[0,31]; + 只按 Rice symbol bit count 比较,tie 选较小 k, + 不把 1-byte riceK 与 byte-boundary padding 纳入优化目标。 +6. QBV3 Header 的 FRAME 固定值直接写入本规范: + layout=1、projection=1、sampleOrder=1、gridRegistration=1、 + tileZ=0xffff、tileX=0、tileY=0; + referenceTimeUtc / validTimeUtc 明确为正 int64 Unix UTC seconds。 +7. Descriptor canonical float 增加 +0.0 规则: + 语义要求 offset=0 时 MUST 写 binary32 +0.0 (0x00000000), + Validity 固定 scale=+1.0、offset=+0.0,禁止 -0.0。 +8. 第一阶段验收表同步收口为 Binary ABI / Writer / Parser / + Round-trip / Codec Benchmark,不再把消费侧行为作为 QBV3 冻结条件。 +``` + +v1.6 相对 v1.5 的收口重点: + +```text +1. Field Profile Contract Schema 升级方向明确: + 为 required Plane 增加 expectedDType / expectedScaleBits / expectedOffsetBits, + 使 wind/2、wave/2、rain/2、current/2 的量化参数由 Profile Contract + data-driven 校验,而不是只依赖 Container 或 Generic Unit 验证。 +2. MAX_DECODED_BYTES_PER_FRAME 口径补齐 Validity: + decodedFrameBytes = decodedNumericBytes + decodedValidityBytes, + validityPlane.storage / validityBits 同一 storage 只计一次。 +3. Direction N 固定为 floor(ratioToN + 0.5),不依赖 host FPU rounding mode。 +4. QBV2 frozen behavior 定义为 authoritative ingress acceptance 不变。 +5. MAX_RETAINED_DECODED_BYTES 仅约束 EncodedFieldStore 当前 retained Frames 可达的 canonical decoded storage。 +6. Validity ContentIdentity 冻结为与 Numeric Plane 相同的 hash algorithm / input order,并调用同一 identity helper。 +``` + +v1.5 相对 v1.4 的收口重点: + +```text +1. QBV2 Direction legacy acceptance 精确对齐当前 C++: + 保留 canonical=360/65536 与 4×float epsilon tolerance; + QBV3 继续使用 canonical float32(360/N) bit-pattern,二者不互相放宽或收紧。 +2. Plane ContentIdentity 进一步冻结当前 QBV2 的实际 hash 常数、 + uint64 wrap 语义、逐 byte XOR→multiply 顺序与最终 hex 输出格式; + 禁止“顺手修正”为标准 FNV-1a offset basis。 +3. EncodedFieldIngress::decodeAndValidate() 升级为正式运行时唯一 authoritative + decode + contract validation 入口;RasterArraySource::submitEncodedField() + 不得继续直接调用 QbinV2Decoder。 +4. MAX_RETAINED_DECODED_BYTES 明确为 V3 实施阶段新增 Store safety policy, + 不是当前 v4.4 已存在行为;按唯一 retained storage 计数, + shared_ptr alias / validityPlane.storage 与 validityBits 不重复计费。 +5. 补齐 v1.3 revision history,并把当前版本引用统一更新为 v1.5。 +``` + +v1.4 相对 v1.3 的收口重点: + +```text +1. Direction validation 分为 Container-specific + Generic 两层: + QBV2 保留 legacy 360/65536° frozen acceptance; + QBV3 使用 Generic Circular Contract,不反向放宽 V2。 +2. Plane ContentIdentity 冻结为与当前 QBV2 完全相同的 hash algorithm、 + input byte order 与 field order。 +3. MAX_DECODED_BYTES_PER_FRAME 由 Decoder 在 allocation 前检查; + MAX_RETAINED_DECODED_BYTES 由 EncodedFieldStore / owning Store layer + 在接受 Frame 前检查。 +4. all-invalid 下 zero-length Numeric Plane 的 canonical packing 明确允许 + 相邻 zero-length Plane / 下一 non-empty Plane 共享 byteOffset。 +5. Direction canonical scale 的 float64→float32 转换固定为 + IEEE-754 round-to-nearest, ties-to-even; + 计算 N 前先验证 ratio finite 且处于允许范围。 +``` + +v1.3 相对 v1.2 的收口重点: + +```text +1. 明确 FieldSeries::submit() 遇到不兼容 Frame 时返回 + RejectedIncompatibleFrame;V2→V3 / schema 切换由上层 clear + rebuild, + 不假设 Store 自动 replacement/reset。 +2. Direction decode 除 UINT16 dtype range 外,额外要求 + 0 <= rawDirection < N;超出 Circular Contract 必须 fail-closed。 +3. 修正 Rice bitstream 章节引用,并统一文档状态/版本号为 v1.3。 +``` + +v1.2 相对 v1.1 的收口重点: + +```text +1. Direction scale 改为 canonical float32(360/N) bit-pattern 校验;固定 half-circle tie=-N/2,并给出整数 circular 参考公式。 +2. Plane / Validity ContentIdentity 精确对齐当前 QBV2 C++ hash input;Grid identity 保持独立。 +3. 不新增 FieldSeriesCompatibilityKey;继续以 DecodedFieldFrame::isInterpolationCompatibleWith() 的 canonical schema + grid 为权威。 +4. 固定 Parser 顺序:结构+Budget → CRC → Validity → Rice decode。 +5. 增加 dtype/circular logical residual 上界与 unary MAX_Q hardening。 +6. Block Geometry 未冻结的实验文件禁止进入 production Manifest/CDN qbin_v3;V2/V3 benchmark 使用同一 CRC 实现。 +``` + +--- + +# 0. 与 QBIN V2 / v4.4 的关系 + +本文是 **QBIN V3 Container 的第一版设计文档修订版**。 + +上一代权威设计基线为: + +```text +WeatherV2_QBIN_V2通用天气场二进制格式与React独立Render_Binding设计_v4.4.md +``` + +其中: + +```text +文档设计版本:v4.4 +QBIN Container Version:2 +Magic:QBV2 +``` + +本文中的 **QBIN V3** 是上述 QBIN V2 / v4.4 基线的直接后继版本。 + +## 0.1 继承关系 + +QBIN V3 不重新设计 WeatherV2 → NavSea → MapLibreParticle 的整体架构。 + +除本文明确修改的 Container / Encoded Plane 编码规则外,QBIN V2 v4.4 已冻结的下列架构原则继续有效: + +```text +WeatherV2 +负责 Provider 获取、物理语义归一化、Canonical Unit、 +Quantization、QBIN Writer、Manifest 和协议数据。 + +NavSea / React +负责 Manifest / Registry / Field Profile Contract Resolve、 +fieldKey、下载、缓存、预取、requestedForecastTime、 +Color Binding 和 Particle Binding。 + +MapLibreParticle Encoded Field Ingress +负责 QBIN Parser / Decoder、CRC、Generic Field Profile Validator, +并输出格式无关的 DecodedFieldFrame / Plane Registry。 + +Runtime / Renderer +继续只处理通用 Scalar / Velocity / Direction / Validity, +不认识 Weather Provider、Manifest 或具体天气业务类型。 +``` + +以下 v4.4 原则继续冻结: + +```text +Semantic Registry +Unit Registry +Field Profile Contract +ResolvedFieldProfileContract + +Canonical Unit +Value Class +Component / groupId + +REQUIRED 完整性规则 +Unknown Profile / Unknown Scalar Semantic 通用处理 + +Color Binding 与 Particle Binding 独立 +Color Projection 与 Particle Projection 独立 + +Core-owned Forecast Clock / Playback +每个 fieldKey 独立 Runtime Field Window +Native Core 自行解析 T0 / T1 / mix + +FrameContentIdentity +generation / dataVersion 修订识别 + +Renderer 不增加: +if WIND +if WAVE +if RAIN +等天气业务分支 +``` + +因此: + +```text +QBIN V2 Decoder + ↓ +DecodedFieldFrame + ↓ +Runtime / Binding / Renderer +``` + +升级为: + +```text +QBIN V3 Decoder + ↓ +DecodedFieldFrame + ↓ +Runtime / Binding / Renderer +``` + +**Decoder 之后的公共架构原则保持不变。** + +## 0.2 V3 为什么需要新的 Container Version + +QBIN V2 将 Numeric Plane 定义为固定宽度的直接量化整数数组,例如: + +```text +INT16 → byteLength = count × 2 +UINT16 → byteLength = count × 2 +``` + +Numeric Plane 的 `byteLength` 可以直接由: + +```text +count × dtype size +``` + +推导。 + +QBIN V3 引入新的 Numeric Plane 编码: + +```text +Physical Value +↓ +Quantization +↓ +Integer Grid +↓ +2D Spatial Predictor +↓ +Signed Residual +↓ +ZigZag +↓ +固定 Block Geometry 的 adaptive Rice Coding +↓ +Variable-length Encoded Plane +``` + +因此 Numeric Plane 的编码长度不再等于: + +```text +count × dtype size +``` + +而成为真正的: + +```text +variable-length encoded payload +``` + +这改变了 QBIN V2 的二进制 Plane Contract,因此不能继续标记为 Container Version 2。 + +正式升级为: + +```text +QBIN Container Version:3 +Magic:QBV3 +Version:3 +``` + +## 0.3 V3 不代表业务协议重做 + +Container Version 升级只表示: + +```text +Encoded Binary Representation +``` + +发生不兼容变化。 + +它不意味着以下协议重新开始: + +```text +Semantic ID +Unit ID +Value Class +Component +groupId +Field Profile +Render Binding +Forecast Runtime +Renderer +``` + +这些仍继承 v4.4 的长期通用设计。 + +因此: + +```text +Container Version +``` + +和: + +```text +Field Profile Version +``` + +必须继续独立管理。 + +第一阶段建议: + +```text +QBIN V3 + +wind / 2 +wave / 2 +rain / 2 +current / 2 +``` + +表示: + +```text +Container 使用 QBIN V3 ++ +各天气 Field 使用自己的 Profile Version +``` + +而不是把天气业务规则硬编码进 Container Version。 + +## 0.4 v4.4 文档的历史地位 + +`WeatherV2_QBIN_V2通用天气场二进制格式与React独立Render_Binding设计_v4.4.md` + +从 QBIN V3 生效后仍永久保留,作为: + +```text +QBIN V2 最终冻结规范 +QBIN V3 架构来源基线 +旧 QBV2 文件解释依据 +V2/V3 兼容与迁移依据 +``` + +不得覆盖、改名或修改其已经冻结的历史定义。 + +本文只定义: + +```text +QBIN V3 +``` + +以后对 V3 的小幅修订使用本文自身的: + +```text +Document Revision +``` + +管理,例如: + +```text +v1.0 +v1.1 +v1.2 +``` + +不得再用文档修订号代替 Container Version。 + +因此版本体系统一为: + +```text +QBIN V2 +└── 最终设计基线:v4.4 + +QBIN V3 +└── 本文初始设计版本:v1.0 +``` + + +## 0.5 本文的 Normative Scope + +从 v1.7 开始,本文的 normative 主体只定义: + +```text +QBV3 Header +Plane Descriptor +Endian / canonical float +Quantization +Shared Validity +2D Predictor +Direction Circular Predictor +ZigZag +Rice Block / bitstream +Numeric Plane byteLength +Plane Packing / Alignment +CRC32 +Parser Safety / Fail-closed +Deterministic Writer +Golden Binary / bit-exact Round-trip +Codec encode/decode benchmark +``` + +下列内容**不属于 QBV3 Binary ABI**: + +```text +NavSea 如何选择 / 下载 / 缓存 QBV3 +React / RN API +Profile Contract JSON 的 wire schema +EncodedFieldStore / FieldSeries +T0 / T1 / mix +Binding / Projection / Renderer +Android / iOS Bridge transport +Store retained-memory policy +业务层 V2→V3 切换流程 +``` + +这些内容只可作为 Non-normative Integration Notes 保存,不能反向改变同一个: + +```text +magic = QBV3 +version = 3 +``` + +的文件解释。 + +目标是: + +> Weather 生产方只阅读本文 normative 主体,就能生成 byte-for-byte deterministic、跨平台可解码、可严格校验的 QBV3 文件;消费方实现细节不属于 Container 语义。 + +--- + +# 1. V3 设计目标 + +QBIN V3 的核心目标只有一个: + +> **在保持 v4.4 通用天气场架构不变的前提下,大幅降低每个时间 Frame 的网络与缓存体积,并保持 Native 解码简单、确定、可校验。** + +设计目标: + +```text +1. 一个 targetTime 仍对应一个独立 Frame 文件。 +2. 不把多个时间点打成一个大归档。 +3. NavSea 可只下载当前 T0/T1 和必要预取 Frame。 +4. 数值误差只来自明确的 Quantization Profile。 +5. Predictor / ZigZag / Rice 全部可逆,不增加额外物理误差。 +6. Decoder 之后仍输出既有 DecodedFieldFrame / Plane Registry。 +7. Runtime / Renderer 不感知 QBIN V2/V3 差异。 +8. Wind / Wave / Rain / Current 共用同一 Container 与编码方法。 +9. 不为每种天气增加独立二进制格式。 +10. Writer 必须 deterministic;相同输入与同一 Profile 产生 byte-for-byte 相同文件。 +``` + +--- + +# 2. 不在 V3 中解决的问题 + +V3 不负责: + +```text +颜色表 +粒子颜色 +粒子数量 +Trail +UI +Provider 网络请求 +Route / ETA 业务 +Forecast 下载调度 +缓存淘汰策略 +``` + +V3 也不改变: + +```text +ColorBinding / ParticleBinding +Core Forecast Clock +FieldSeries +T0 / T1 / mix +GPU Renderer +``` + +--- + +# 3. 最终数据链 + +```text +Provider + ↓ +WeatherV2 Normalizer + ↓ +Server Canonical Field + ↓ +Unified Quantizer / Field Profile + ↓ +QBIN V3 Writer + ↓ +2D Predictor + ↓ +ZigZag + ↓ +固定 Block Geometry 的 adaptive Rice + ↓ +QBV3 Frame + ↓ +HTTP Distribution Encoding(可 gzip) + ↓ +NavSea Download / Cache + ↓ +MapLibreParticle Encoded Field Ingress + ↓ +QbinV3Decoder + ↓ +DecodedFieldFrame / Plane Registry + ↓ +EncodedFieldStore / FieldSeries + ↓ +Binding Resolver + ├── Color Projection + └── Particle Projection + ↓ +Core-owned Forecast Clock / Runtime Field Window + ↓ +WeatherColorLayer / RasterParticleLayer +``` + +QBIN V2 与 V3 的分界只允许存在于 Encoded Field Ingress: + +```text +QBV2 → QbinV2Decoder ┐ + ├→ DecodedFieldFrame +QBV3 → QbinV3Decoder ┘ +``` + +不得把 V3 编码细节泄漏到 Runtime/Renderer。 + +--- + +# 4. 文件总体结构 + +V3 第一版继续采用简单固定结构: + +```text +┌──────────────────────────────┐ +│ Fixed Header │ 112 bytes +├──────────────────────────────┤ +│ Plane Descriptor Table │ 32 bytes × planeCount +├──────────────────────────────┤ +│ Payload │ variable +└──────────────────────────────┘ +``` + +继续不保存: + +```text +headerSize +descriptorSize +descriptorOffset +payloadOffset +totalLength +``` + +固定: + +```text +HEADER_SIZE = 112 +DESCRIPTOR_SIZE = 32 + +descriptorOffset = 112 +payloadOffset = 112 + planeCount × 32 +expectedTotalLength = payloadOffset + payloadLength +``` + +V3 第一版不利用 Header/Descriptor 的 reserved 字段承载新业务信息。 + +所有 reserved 字段仍必须: + +```text +0 +``` + +这样保留二进制结构的简单性。 + +--- + +# 5. Magic / Version / Endian + +```text +magic = ASCII "QBV3" +version = 3 +``` + +未知 Version: + +```text +fail-closed +``` + +所有 multi-byte integer 与 IEEE-754 floating point: + +```text +Little Endian +``` + +Rice bitstream 的 bit order 由本文第 18 节单独定义,不使用 host-native bitfield。 + +## 5.1 QBV3 第一版 FRAME 固定 Header ABI + +QBV3 第一版只定义: + +```text +layout = FRAME +``` + +对应 Header 固定值: + +```text +layout = 1 +projection = 1 # WEB_MERCATOR +sampleOrder = 1 # NORTH_TO_SOUTH_WEST_TO_EAST +gridRegistration = 1 # NODE + +tileZ = 0xffff +tileX = 0 +tileY = 0 +``` + +以下时间字段: + +```text +referenceTimeUtc +validTimeUtc +``` + +固定为: + +```text +int64 +Unix UTC seconds +MUST > 0 +``` + +不得在 Header 中直接写毫秒。 + +QBV3 Container **不规定**二者的相对大小: + +```text +不得在 Binary Parser 中强制: +validTimeUtc >= referenceTimeUtc +``` + +以下情况都可以由外部 Profile / Dataset 语义决定: + +```text +Forecast +Analysis +Observation +Reanalysis / Hindcast +``` + +因此 Binary-valid 只要求两个时间字段各自满足格式与范围要求, +不因 `validTimeUtc < referenceTimeUtc` 自动判定 corruption。 + +Bounds: + +```text +west / south / east / north +``` + +必须是 finite `float64`,第一版 WEB_MERCATOR FRAME 至少满足: + +```text +-180 <= west < east <= 180 +-85.0511287798066 <= south < north <= 85.0511287798066 +``` + +未知 layout / projection / sampleOrder / gridRegistration: + +```text +fail-closed +``` + +这些值直接属于 QBV3 Container ABI,不要求 Writer 通过查阅 QBIN V2 文档才能生成合法 Header。 + +## 5.2 WEB_MERCATOR + NODE Grid 的精确空间语义 + +Header: + +```text +west / south / east / north +``` + +是: + +```text +WGS84 longitude / latitude degrees +``` + +但 Grid node 在南北方向的等间距不是按 latitude degree 线性插值, +而是在 Web Mercator projected space 中等间距。 + +定义数学上的 dimensionless spherical Web Mercator: + +```text +λ = longitudeDeg × π / 180 +φ = latitudeDeg × π / 180 + +mercatorX(longitudeDeg) = + λ + +mercatorY(latitudeDeg) = + ln( + tan( + π/4 + φ/2 + ) + ) +``` + +其 inverse: + +```text +longitudeDeg = + X × 180 / π + +latitudeDeg = + ( + 2 × atan(exp(Y)) + - π/2 + ) × 180 / π +``` + +由于 Header 已限制: + +```text +-85.0511287798066 <= latitude <= 85.0511287798066 +``` + +正文不再额外定义 latitude clamp。 + +若某实现使用 EPSG:3857 meter coordinate: + +```text +X_meter = 6378137 × X +Y_meter = 6378137 × Y +``` + +其 Grid node 位置与本文 dimensionless 公式完全等价; +规范语义以本文 dimensionless 公式为准。 + +实现用于 Grid sample 定位时应采用 IEEE-754 binary64 计算。 + +因此: + +```text +xWest = mercatorX(west) +xEast = mercatorX(east) + +yNorth = mercatorY(north) +ySouth = mercatorY(south) +``` + +对于 NODE grid: + +```text +dx = (xEast - xWest) / (width - 1) +dy = (yNorth - ySouth) / (height - 1) +``` + +逻辑 sample `(row, col)`: + +```text +x = xWest + col * dx +y = yNorth - row * dy +``` + +因此: + +```text +row = 0 +→ north edge + +row = height - 1 +→ south edge + +col = 0 +→ west edge + +col = width - 1 +→ east edge +``` + +flattened cell index: + +```text +index = row * width + col +``` + +sample order: + +```text +NORTH_TO_SOUTH_WEST_TO_EAST +``` + +等价于: + +```text +row-major +每一 row 内 west → east +rows north → south +``` + +Writer / Decoder / Golden Fixture 不得把 `south..north` latitude degree +直接做线性 sample spacing 来替代上述 Web Mercator projected-space 语义。 + + +--- + +# 6. Fixed Header + +V3 第一版继续沿用 v4.4 的 112-byte Header 字段位置,只修改 Magic / Version: + +| Offset | Size | Type | Field | +|---:|---:|---|---| +| 0 | 4 | char[4] | magic=`QBV3` | +| 4 | 1 | uint8 | version=3 | +| 5 | 1 | uint8 | layout | +| 6 | 2 | uint16 | fieldType | +| 8 | 1 | uint8 | projection | +| 9 | 1 | uint8 | sampleOrder | +| 10 | 1 | uint8 | gridRegistration | +| 11 | 1 | uint8 | reserved0=0 | +| 12 | 2 | uint16 | planeCount | +| 14 | 2 | uint16 | reserved1=0 | +| 16 | 4 | uint32 | width | +| 20 | 4 | uint32 | height | +| 24 | 8 | int64 | referenceTimeUtc | +| 32 | 8 | int64 | validTimeUtc | +| 40 | 8 | float64 | west | +| 48 | 8 | float64 | south | +| 56 | 8 | float64 | east | +| 64 | 8 | float64 | north | +| 72 | 4 | uint32 | payloadLength | +| 76 | 4 | uint32 | fileCrc32 | +| 80 | 2 | uint16 | tileZ | +| 82 | 2 | uint16 | reserved2=0 | +| 84 | 4 | uint32 | tileX | +| 88 | 4 | uint32 | tileY | +| 92 | 4 | uint32 | reserved3=0 | +| 96 | 4 | uint32 | reserved4=0 | +| 100 | 4 | uint32 | reserved5=0 | +| 104 | 4 | uint32 | reserved6=0 | +| 108 | 4 | uint32 | reserved7=0 | + +时间、Bounds、Projection、Sample Order、Grid Registration 的语义继续继承 v4.4。 + + +## 6.1 fieldType:Machine Hint,不是 Layout Authority + +`fieldType` 是 `uint16` machine-readable hint,沿用上一代已知值: + +```text +1 = WIND +2 = WAVE +3 = CURRENT +4 = RAIN +5 = AIR_TEMPERATURE +6 = SEA_TEMPERATURE +7 = AIR_PRESSURE +8 = HUMIDITY +9 = VISIBILITY +``` + +未知 `fieldType`: + +```text +MUST NOT solely cause binary rejection +``` + +只要 Header / Descriptor / Plane / CRC 等其他 QBV3 Binary Contract 全部合法, +Generic QBV3 Parser 必须继续按 binary-valid 处理。 + +也就是说 `fieldType` 是: + +```text +opaque uint16 machine hint +``` + +已知值具有当前约定含义,未知值仍保持 forward-compatible hint 语义。 + +特别冻结: + +```text +fieldType = 0 += UNKNOWN / UNSPECIFIED += binary-valid opaque hint +``` + +`fieldType=0` 或其他未知 `uint16` 值都不得单独导致 Binary reject。 + +`fieldType` MUST NOT 决定: + +```text +Plane count +Plane semantic set +dtype +scale / offset +REQUIRED completeness +Rice block interpretation +Profile compatibility +Renderer behavior +``` + +因此: + +```text +fieldType == WIND +``` + +不能替代外部: + +```text +fieldProfileId = wind +fieldProfileVersion = 2 +``` + +也不能使 Parser 自动要求 U/V Plane。 + +`fieldType` 与具体 Profile 是否一致属于外部 Profile / Product validation, +不属于 QBV3 binary corruption。 + +## 6.2 Header Bounds 的 Canonical Float64 Zero + +`west / south / east / north` 使用 IEEE-754 binary64 little-endian。 + +为了 deterministic Writer: + +```text +如果任一 bound 的数学值 == 0: +MUST serialize as +0.0 binary64 + +bit-pattern: +0x0000000000000000 +``` + +禁止: + +```text +-0.0 +bit-pattern = 0x8000000000000000 +``` + +其他有限非零值按其 canonical binary64 value 写入。 + +--- + +# 7. Plane Descriptor + +V3 第一版继续保持每个 Descriptor 32 bytes: + +| Offset | Size | Type | Field | +|---:|---:|---|---| +| 0 | 2 | uint16 | semantic | +| 2 | 1 | uint8 | dtype | +| 3 | 1 | uint8 | flags | +| 4 | 4 | float32 | scale | +| 8 | 4 | float32 | offset | +| 12 | 4 | uint32 | count | +| 16 | 4 | uint32 | byteOffset | +| 20 | 4 | uint32 | byteLength | +| 24 | 2 | uint16 | unit | +| 26 | 2 | uint16 | groupId | +| 28 | 1 | uint8 | valueClass | +| 29 | 1 | uint8 | component | +| 30 | 2 | uint16 | reserved=0 | + +## 7.1 DType Byte-level Enum + +QBV3 `dtype` 是 `uint8`,byte-level 值冻结为: + +```text +1 = INT16 +2 = UINT16 +3 = UINT8 +4 = INT8 +5 = FLOAT32 +6 = BITSET +``` + +QBV3 Version 3 第一版合法组合: + +```text +Numeric Plane: +dtype ∈ {INT16, UINT16, UINT8, INT8} +即 byte value ∈ {1,2,3,4} + +Validity Plane: +dtype = BITSET +即 byte value = 6 +``` + +以下在 QBV3 Version 3 第一版必须 reject: + +```text +Numeric dtype = FLOAT32 (5) +Numeric dtype = BITSET (6) + +Validity dtype != BITSET (6) + +unknown dtype +``` + +`FLOAT32=5` 的 enum 值保留是为了保持跨代 registry / decoded type 一致性, +但它不代表 QBV3 Version 3 第一版允许 FLOAT32 Rice Plane。 + +V3 与 V2 的关键区别: + +### Numeric Plane + +`dtype` 表示 **解码后的逻辑 Quantized Integer 类型**,不是 Payload 的固定宽度存储长度。 + +```text +count = width × height +byteLength = 该 Plane 的实际 Rice encoded byte 数 +``` + +因此 V3 Numeric Plane 不再要求: + +```text +byteLength == count × dtypeSize +``` + +### Validity Plane + +第一版仍保持直接 BITSET: + +```text +dtype = BITSET +count = width × height +byteLength = ceil(count / 8) +``` + +## 7.2 Descriptor Canonical Float Representation + +Descriptor 中: + +```text +scale +offset +``` + +都是 IEEE-754 binary32 little-endian。 + +QBV3 deterministic Writer 必须使用 canonical binary32 representation。 + +特别冻结: + +```text +如果某语义要求 offset == 0: + offset MUST = +0.0f + bit-pattern = 0x00000000 + +不得写: + -0.0f + bit-pattern = 0x80000000 +``` + +Validity Plane 固定: + +```text +scale = +1.0f +bit-pattern = 0x3f800000 + +offset = +0.0f +bit-pattern = 0x00000000 +``` + +对于所有 Numeric Plane: + +```text +scale MUST finite +scale > 0 +offset MUST finite +``` + +并冻结全局 canonical zero: + +```text +if offset == 0.0 numerically: + bit-pattern MUST = +0.0f + 0x00000000 + +-0.0f +0x80000000 +→ MUST reject +``` + +该规则不依赖外部 Profile 是否显式声明“offset=0”。 + +该规则属于 binary canonicalization,目的包括: + +```text +byte-for-byte deterministic Writer +CRC 稳定 +Content Identity 稳定 +跨语言 / 跨平台一致 +``` + +--- + + +## 7.3 Descriptor Byte-level Structural Invariants + +为了让 QBV3 Binary Parser 不依赖外部 Profile 名称, +以下机器值与通用结构约束直接属于 Container Version 3。 + +### Value Class + +```text +0 = SCALAR +1 = VELOCITY_COMPONENT +2 = DIRECTION_ANGLE +3 = VALIDITY +``` + +未知 `valueClass`: + +```text +MUST reject +``` + +### Component + +```text +0 = NONE +1 = U_EAST +2 = V_NORTH +3 = W_UP +``` + +未知 `component`: + +```text +MUST reject +``` + +### Core Unit IDs + +QBV3 第一版直接使用: + +```text +0 = NONE +1 = MPS +4 = DEGREE +``` + +Scalar 的其他 unit ID 可以由外部 Unit Registry 定义; +Binary Parser 不因为“不认识某个 Scalar unit ID”而自动判定 corruption。 + +### Core Semantic + +Shared Validity 的 frozen core semantic: + +```text +VALIDITY semantic = 3 +``` + +并冻结双向约束: + +```text +semantic == 3 +⇔ valueClass == VALIDITY +⇔ dtype == BITSET +``` + +因此: + +```text +semantic = 3 ++ SCALAR / VELOCITY_COMPONENT / DIRECTION_ANGLE +→ MUST reject + +valueClass = VALIDITY ++ semantic != 3 +→ MUST reject + +dtype = BITSET ++ valueClass != VALIDITY +→ MUST reject +``` + +一个合法 QBV3 Frame 仍必须有且只有一个满足上述关系的 Shared Validity Plane。 + +Numeric semantic: + +```text +semantic = 0 +→ reserved / invalid +``` + +除 `semantic=3` 已被冻结为 VALIDITY 外, +其他 Numeric semantic ID 的业务含义由外部 Semantic Registry 决定。 + +### 通用组合约束 + +```text +SCALAR: + dtype ∈ {INT16, UINT16, UINT8, INT8} + component = NONE + +VELOCITY_COMPONENT: + dtype ∈ {INT16, UINT16, UINT8, INT8} + unit = MPS + component ∈ {U_EAST, V_NORTH, W_UP} + groupId != 0 + +DIRECTION_ANGLE: + dtype = UINT16 + unit = DEGREE + component = NONE + groupId = 0 + offset = canonical +0.0f + scale obeys 第 15 节 Circular Contract + +VALIDITY: + semantic = 3 + dtype = BITSET + unit = NONE + groupId = 0 + component = NONE + flags.REQUIRED = 1 + scale = canonical +1.0f + offset = canonical +0.0f +``` + +一个 Frame: + +```text +MUST 有且只有一个 VALIDITY Plane +``` + +Binary Parser **不负责**验证: + +```text +某天气 Profile 是否需要 U/V 成对 +某 Scalar semantic 是否 REQUIRED +某 Field Profile 是否允许 additional Plane +``` + +这些属于外部 Profile Contract,而不是 QBV3 corruption。 + +# 8. Value Class / Semantic / Unit / groupId + +QBV3 Binary Parser 使用第 7.3 节冻结的: + +```text +valueClass +component +core unit +VALIDITY semantic +``` + +作为 byte-level structural contract。 + +外部 Semantic Registry / Unit Registry 可以为: + +```text +Scalar semantic +Scalar unit +未来扩展语义 +``` + +赋予更具体含义,但不得改变 `QBV3/version=3` 已冻结的 byte-level 解释。 + +`groupId` 的 Container 级基本规则: + +```text +VELOCITY_COMPONENT → groupId != 0 +DIRECTION_ANGLE → groupId = 0 +VALIDITY → groupId = 0 +``` + +SCALAR 的 `groupId` 是否具有额外业务含义由外部 Profile 决定。 + +V3 不因为编码变化增加: + +```text +if WIND +if WAVE +if RAIN +if CURRENT +``` + +等天气业务分支。 + +# 9. REQUIRED Flag 与 Binary-valid / Profile-valid 分层 + +Descriptor `flags`: + +```text +bit0 = REQUIRED +bit1..7 = reserved +``` + +在 QBV3 Binary Container 层,Parser 只验证: + +```text +flags 的 reserved bits == 0 +Descriptor 本身 structurally valid +Plane bytes / semantic / dtype / validity / packing 等满足本文 Binary Contract +``` + +QBV3 Binary Container **不根据天气 Profile 名称决定 required Plane 集合**。 + +因此必须区分: + +```text +Binary-valid +``` + +和: + +```text +Profile-valid +``` + +## 9.1 Binary-valid + +只由: + +```text +QbinV3Decoder +``` + +根据本文 normative Binary Contract 判断。 + +一个 QBV3 文件即使缺少某个产品 Profile 所需 Plane, +只要文件自身 Header / Descriptor / Payload / CRC / Validity / Rice 等合法, +仍可以是: + +```text +binary-valid +``` + +## 9.2 Profile-valid + +具体: + +```text +wind / 2 +wave / 2 +rain / 2 +current / 2 +``` + +要求哪些 Plane、dtype、scale、offset、unit、group/component, +属于外部 Profile Contract / Registry。 + +例如一个 binary-valid 文件: + +```text +VECTOR_U +VALIDITY +``` + +拿 `wind / 2` Contract 验证时因为缺失 `VECTOR_V`: + +```text +Profile validation → reject +``` + +但不能称为: + +```text +QBV3 corruption +``` + +## 9.3 REQUIRED flag 的职责 + +Descriptor REQUIRED flag 是: + +```text +文件内 requirement declaration / mirror +``` + +外部 Profile Contract 可以要求: + +```text +required Plane 必须存在 +对应 Descriptor REQUIRED bit 必须一致 +``` + +这种一致性属于: + +```text +Encoded Field Ingress / Profile Contract Validation +``` + +而不是 QBV3 binary parser 对天气业务完整性的判断。 + +不得在 `QbinV3Decoder` 内写: + +```text +if fieldType == WIND: + require U + require V +``` + +也不得让 Binary Decoder 依赖: + +```text +NavSea +React +Contract JSON +fieldProfileId +fieldProfileVersion +``` + +## 9.4 QBV3 Quantization Contract 的边界 + +QBV3 Binary Container 自身承载: + +```text +dtype +scale +offset +``` + +Parser 必须验证它们满足本文通用 Binary Contract: + +```text +dtype 合法 +scale / offset finite +canonical float / +0.0 +Direction Circular Contract +Validity fixed descriptor +``` + +具体 Profile 是否要求: + +```text +INT16 +scale = 0.1 +offset = +0.0 +``` + +由外部 Profile Contract 决定。 + +因此: + +> 同一个合法 QBV3 的 byte-level 解释不能依赖 Profile 名称或外部 Contract。 + + +# 10. Quantization 与编码必须分离 + +V3 强制区分: + +```text +Physical Quantization +``` + +和: + +```text +Binary Entropy Coding +``` + +物理误差只允许来自 Quantization: + +```text +physical → quantized integer +``` + +以下步骤必须完全可逆: + +```text +2D Predictor +ZigZag +Rice +``` + +因此: + +```text +V3 decode 后的 quantized integer +MUST byte/number-exact 等于 Writer Quantizer 的输出整数 +``` + +不得在 Rice/Predictor 阶段引入第二次近似。 + +--- + +# 11. Generic Numeric Quantization + +普通非循环 Numeric Plane: + +```text +physical = raw × scale + offset +``` + +## 11.1 Quantizer 参数以 Descriptor binary32 为唯一来源 + +Writer 必须先确定最终要写入 Descriptor 的: + +```text +scaleBits : IEEE-754 binary32 +offsetBits : IEEE-754 binary32 +``` + +然后: + +```text +scaleF32 = binary32_from_bits(scaleBits) +offsetF32 = binary32_from_bits(offsetBits) +``` + +实际 Quantizer 计算必须使用**已经序列化确定的 binary32 值**,必要时提升为 binary64: + +```text +scaleQ = double(scaleF32) +offsetQ = double(offsetF32) + +normalized = (physical - offsetQ) / scaleQ +raw = round_half_away_from_zero(normalized) +``` + +禁止: + +```text +用配置文件中的 decimal 原值 +用序列化前更高精度的 double scale / offset +量化完成后再把 scale / offset 截成 float32 +``` + +原因: + +> QBV3 的 Quantization 参数必须与文件 Descriptor 中实际携带的 bit-pattern 完全一致。 + +在“相同 canonical physical input + 相同 Descriptor bit-pattern”的前提下,Writer 必须产生相同 quantized integer。 + +普通 Writer: + +```text +normalized = (physical - offsetQ) / scaleQ +raw = round_half_away_from_zero(normalized) +``` + +Validity 必须在 Numeric Quantizer 之前已经确定。 + +对每个 cell: + +```text +if validity[index] == 1: + physical MUST be finite IEEE-754 binary64 + 执行 Numeric / Direction Quantization + +if validity[index] == 0: + physical value is ignored + MAY be NaN / +Infinity / -Infinity / missing + MUST NOT 进入 Quantizer + MUST NOT 产生 Rice symbol + decoded canonical raw MUST = 0 +``` + +禁止: + +```text +通过 physical 是 NaN / Inf 自动推导 validity = 0 +``` + +Validity 是独立、权威的 Shared Validity Domain; +non-finite numeric payload 不能替代 Validity BITSET。 + +`round_half_away_from_zero(x)` 的数学定义冻结为: + +```text +if x >= 0: + result = floor(x + 0.5) +else: + result = ceil(x - 0.5) +``` + +Quantizer 的规范计算域: + +```text +physical : IEEE-754 binary64 +scaleQ : exact binary64(double(scaleF32)) +offsetQ : exact binary64(double(offsetF32)) +``` + +规范计算步骤: + +```text +delta64 = + roundTiesToEven_binary64( + physical - offsetQ + ) + +normalized64 = + roundTiesToEven_binary64( + delta64 / scaleQ + ) + +delta64 MUST finite +normalized64 MUST finite + +如果任一 intermediate 为: + +```text +NaN ++Infinity +-Infinity +``` + +Writer MUST FAIL。 + +raw = + round_half_away_from_zero(normalized64) +``` + +这里的: + +```text +roundTiesToEven_binary64 +``` + +表示 IEEE-754 binary64 round-to-nearest, ties-to-even。 + +Writer 必须得到与上述步骤相同的 binary64 结果,不得依赖: + +```text +host locale +decimal string formatting +host default integer cast rounding +FPU 当前 rounding mode +excess precision / extended register precision +``` + +来决定最终 raw integer。 + +超出逻辑 dtype: + +```text +Writer FAIL +``` + +禁止: + +```text +silent clamp +integer wrap +隐式 modulo +``` + +Invalid Cell 不进入 Numeric Rice symbol stream;Decoder materialize 时其 raw 值必须恢复为 0。 + +--- + +# 12. V3 第一版支持的 Numeric dtype + +第一版编码支持: + +```text +INT16 +UINT16 +INT8 +UINT8 +``` + +所有上述 dtype 在进入 Predictor 前转换到至少 `int32` 运算域。 + +Predictor residual: + +```text +int32 +``` + +ZigZag 输出: + +```text +uint32 +``` + +`FLOAT32` 不进入 V3 第一版 Rice Numeric Plane。 + +需要 FLOAT32 的 Field: + +```text +继续使用其他已支持 Container +或未来明确扩展 V3 codec contract +``` + +不得私自把 FLOAT32 bytes 当整数 Rice 编码。 + +--- + +# 13. Shared Validity + +V3 第一版继续保留 v4.4 的单 Frame Shared Validity Domain: + +```text +semantic = VALIDITY +valueClass = VALIDITY +dtype = BITSET +unit = NONE +groupId = 0 +component = NONE +flags = REQUIRED +``` + +BITSET: + +```text +LSB first +byteLength = ceil(count / 8) +``` + +最后 byte 未使用 high bits: + +```text +MUST = 0 +``` + +Validity bit 与 flattened cell index 的对应关系冻结为: + +```text +index = row * width + col + +byteIndex = index >> 3 +bitIndex = index & 7 + +valid = + (validityBytes[byteIndex] >> bitIndex) & 1 +``` + +因此每个 byte 内: + +```text +bit 0 → 较小 index +bit 7 → 较大 index +``` + +即明确的 LSB-first。 + +一个 Frame 内: + +```text +所有 Numeric Plane MUST 共享完全相同 validity domain +``` + +不同 validity domain 的数据继续拆成不同 Field/QBIN。 + +V3 第一版**不增加**: + +```text +ALL_VALID special payload +ALL_INVALID special payload +RLE validity +per-plane validity +per-group validity +``` + +原因:保持 Container 简单统一。 + +Distribution 层 gzip 可以自然压缩全 1 或高度重复的 BITSET。 + +## 13.1 Validity 的逻辑解析顺序 + +Descriptor Table 仍按本文 canonical descriptor order 排序,Payload 仍按 Descriptor 顺序打包。 + +QBV3 **不要求** Validity Plane 在物理 Payload 中排在 Numeric Plane 前面。 + +Decoder 必须采用两阶段逻辑解析: + +```text +1. 解析全部 Header / Descriptor +2. 定位唯一 Shared Validity Plane +3. 完成 CRC / padding / Validity BITSET 校验 +4. 读取完整 Validity BITSET +5. 计算每个 Rice block 的 valid symbolCount +6. 再解各 Numeric Plane +``` + +因此: + +```text +逻辑上 Validity 必须先可用 +!= +物理上 Validity 必须排在最前 +``` + +不得为了 Rice 解码方便而改变 Descriptor canonical order 或 Plane packing order。 + +--- + +# 14. 2D Spatial Predictor + +V3 第一版所有 Numeric Plane 使用同一个二维 predictor。 + +预测在**整个 Grid 的全局坐标**上进行,不因 Rice block 边界 reset。 + +对当前 cell `(r,c)`: + +```text +L = (r, c-1) +U = (r-1, c) +UL = (r-1, c-1) +``` + +预测规则: + +```text +如果 current valid 且 L/U/UL 全部 valid: + predicted = raw(L) + raw(U) - raw(UL) + +否则如果 current valid 且 L valid: + predicted = raw(L) + +否则如果 current valid 且 U valid: + predicted = raw(U) + +否则: + predicted = 0 +``` + +普通 Numeric residual: + +```text +residual = raw(current) - predicted +``` + +所有中间计算至少使用: + +```text +int32 +``` + +不得用 int16 中间运算导致 overflow。 + +Invalid cell: + +```text +不产生 Rice symbol +Decoder materialize raw = 0 +``` + +--- + +# 15. Direction Circular Quantization / Predictor + +`DIRECTION_ANGLE` 继续使用 v4.4 的物理约定: + +```text +DIRECTION_TO +0° True North +90° East +180° South +270° West +clockwise positive +[0°, 360°) +``` + +QBIN V3 不再把 `360 / 65536°` 作为唯一合法 Direction scale。 +Direction quantization 正式升级为**通用 Circular Contract**。 + +Descriptor 基本约束: + +```text +valueClass = DIRECTION_ANGLE +dtype = UINT16 +unit = DEGREE +component = NONE +offset = 0 +scale > 0 +scale finite +``` + +## 15.0 QBV3 Direction Validation Scope + +本节只定义: + +```text +QBV3 / version=3 +``` + +自身的 Direction Circular Contract。 + +QBV3 Parser 对 `DIRECTION_ANGLE` 必须验证: + +```text +dtype = UINT16 +unit = DEGREE +component = NONE +groupId = 0 +offset = canonical +0.0f +scale = canonical float32(360/N) +rawDirection ∈ [0,N) +``` + +QBV2 的 legacy acceptance、共享 Validator 如何重构等兼容实现问题: + +```text +不属于 QBV3 Binary ABI +``` + +移至 Appendix A。 + +## 15.1 Canonical Direction Scale + +Direction scale 不使用实现方自行决定的 epsilon / tolerance。 +协议直接冻结 canonical float32 计算规则。 + +Reader / Writer 均定义: + +```text +scaleF32 = Descriptor 中 scale 的 IEEE-754 float32 值 + +ratioToN = 360.0 / double(scaleF32) +``` + +在把 `ratioToN` 转为整数前必须先验证: + +```text +ratioToN finite +1 <= ratioToN <= 65536 +``` + +然后固定: + +```text +N = floor(ratioToN + 0.5) +``` + +由于 `ratioToN > 0`,该规则等价于正数域的 `round_half_away_from_zero`,不依赖 host FPU rounding mode。 + +必须满足: + +```text +1 <= N <= 65536 + +ratio64 = 360.0 / double(N) +ratio64 finite + +canonicalScaleF32 = +IEEE-754 binary32( + ratio64, + round-to-nearest, + ties-to-even +) + +bit_pattern(scaleF32) == bit_pattern(canonicalScaleF32) +``` + +即:Descriptor 中的 `scale` 必须是 `360/N` 转为 IEEE-754 float32 后的**唯一 canonical bit pattern**。 +不得接受“足够接近”的任意浮点值。 + +因此不再需要 `DIRECTION_SCALE_TOLERANCE`。 + +逻辑 raw 范围: + +```text +rawDirection ∈ [0, N) +``` + +合法 Profile 示例: + +```text +N = 65536 → scale = float32(360/65536)° +N = 360 → scale = 1° +N = 720 → scale = 0.5° +``` + +QBV3 Writer 的 Direction input 必须在进入 Writer 前已经是 canonical: + +```text +physicalDirection finite +0 <= physicalDirection < 360 +DIRECTION_TO +clockwise positive +True North = 0° +``` + +QBV3 Writer 不负责: + +```text +FROM → TO 转换 +任意角度 wrap +天气 Provider 方向约定修正 +``` + +这些属于上游 Normalizer。 + +Writer 使用: + +```text +scaleQ = double(scaleF32) + +normalizedDirection64 = + roundTiesToEven_binary64( + physicalDirection / scaleQ + ) + +normalizedDirection64 MUST finite +否则 Writer MUST FAIL。 + +rawDirection = + round_half_away_from_zero(normalizedDirection64) mod N +``` + +其中 `mod N` 只用于处理接近 360° 边界时量化得到 `N` 的合法 wrap, +输入 physicalDirection 本身仍必须位于 `[0,360)`。 + +QBV3 Binary Parser 只验证上述通用 Circular Contract,禁止: + +```text +if profile == wave: + allow 1 degree +``` + +合法性来自 `scale → N → canonical float32(360/N)`, +不得把具体天气 Profile 名称写进 Container Parser。 + +## 15.2 Direction Integer Normalization + +所有 Direction predictor / residual 运算必须使用**整数圆周语义**,不得依赖不同语言对负数 `%` 的行为。 + +定义 `normalize_direction(x, N)`,其结果必须位于 `[0, N)`。 +对于本协议 predictor 产生的范围,固定参考规则为: + +```text +if x < 0: + x = x + N +else if x >= N: + x = x - N +return x +``` + +该规则成立是因为输入 raw 均在 `[0,N)`,三邻居 predictor `L + U - UL` 的结果必定位于: + +```text +[-(N-1), 2(N-1)] +``` + +因此最多需要一次 `+N` 或 `-N`。 + +如果实现使用通用 modulo helper,也必须保证得到相同的 non-negative modulo 结果。 + +## 15.3 Circular Predictor + +空间邻居选择和 fallback 顺序继续使用第 14 节 validity-aware 规则。 + +当 `L/U/UL` 全部 valid 时,Direction predictor 必须先计算 widened integer: + +```text +p = int32(raw(L)) + int32(raw(U)) - int32(raw(UL)) +predictedRaw = normalize_direction(p, N) +``` + +Fallback: + +```text +L valid → predictedRaw = raw(L) +U valid → predictedRaw = raw(U) +否则 → predictedRaw = 0 +``` + +不得把未经 `mod N` 规范化的 `L + U - UL` 直接用于 Direction residual。 + +## 15.4 circular_shortest_delta() 精确公式 + +设: + +```text +d = int32(currentRaw) - int32(predictedRaw) +``` + +先将差值规范到 `[0,N)`: + +```text +m = d +if m < 0: + m = m + N +``` + +此时 `m ∈ [0,N)`。 + +然后固定: + +```text +if 2*m > N: + residual = m - N +else if 2*m < N: + residual = m +else: + # 仅 N 为偶数且恰好半圈时成立 + residual = -(N / 2) +``` + +因此 half-circle tie **固定选择负方向 `-N/2`**。 + +该选择属于 QBV3 Version 3 ABI,不得由 Writer / Decoder 自行选择 `+N/2`。 + +Residual 范围: + +```text +N 为偶数:[-N/2, N/2) +N 为奇数:[-floor(N/2), floor(N/2)] +``` + +Decoder: + +```text +x = int32(predictedRaw) + residual +rawDirection = normalize_direction(x, N) +``` + +边界 Golden Test 至少覆盖: + +```text +359° → 0° +0° → 359° +N-1 → 0 +0 → N-1 +偶数 N 的 +N/2 / -N/2 tie → 固定解为 -N/2 +三邻居 L+U-UL < 0 +三邻居 L+U-UL >= N +``` + +在 `scale = 1°` 时,`359° → 0°` 必须得到最短圆周残差,而不是 `±359`。 + +--- + +# 16. ZigZag 与固定宽度整数语义 + +Signed residual 使用 32-bit 逻辑 ZigZag。 + +精确数学映射冻结为: + +```text +zigzag(r): + +if r >= 0: + z = 2 * r +else: + z = -2 * r - 1 +``` + +inverse: + +```text +unzigzag(z): + +if (z & 1) == 0: + r = z / 2 +else: + r = -(z / 2) - 1 +``` + +示例: + +```text +0 → 0 +-1 → 1 +1 → 2 +-2 → 3 +2 → 4 +... +``` + +文档中的位运算表达只用于说明数学关系,**不得直接要求 C++ 对 signed integer 执行可能产生 UB / implementation-defined 行为的 shift**。 + +正式 Writer / Decoder 必须冻结: + +```text +residual / predictor arithmetic:至少 int32 +ZigZag 数学计算:使用 int64 / uint64 widened domain +最终 ZigZag symbol:验证后收敛为 uint32 +bit manipulation:uint32 / uint64 +shift count:先验证再执行 +add / sub / multiply:checked 或 widened arithmetic +``` + +ZigZag 与 inverse ZigZag 必须提供 Android / iOS 共用的固定宽度 C++ reference implementation, +不得依赖: + +```text +signed left shift overflow +signed right shift implementation behavior +host integer width +compiler bitfield layout +``` + +Rice remainder mask 也必须使用明确的 unsigned 宽度,例如逻辑上: + +```text +uint64_t{1} << k +``` + +并保证: + +```text +k = 0..31 +``` + +时不会发生 signed `1 << 31` 或非法 shift。 + +Golden Test 至少覆盖: + +```text +0 +1 +-1 +INT16_MIN +INT16_MAX +最大合法 predictor residual +``` + +Predictor、ZigZag、Rice 必须完全可逆,不能引入任何新的数值误差。 + +--- + +# 17. Rice Block Geometry:冻结前候选与最终 ABI + +V3 已确定采用: + +```text +validity-aware 2D Predictor +→ ZigZag +→ block-adaptive Rice +``` + +**截至文档 v2.2,Block Geometry 是 QBV3 Version 3 尚未冻结的唯一核心 Binary ABI 参数。** + +因此: + +```text +v2.2 仍是 freeze candidate +不是 production-final ABI +``` + +在 16×16 / 32×32 benchmark 完成并回填唯一尺寸前, +任何实验文件都不得作为长期可解释的 production `QBV3/version=3` 发布。 + +但在正式发布 `QBV3 / version=3` 前,**Block Geometry 尚需用真实 C++ Decoder 做最后一次 16×16 / 32×32 对比**。 + +当前候选: + +```text +Candidate A:16×16 +Candidate B:32×32 +``` + +原因:现有 Wind 143 帧实验中,两者编码体积差异约只有: + +```text +Rice16 ≈ 43.929 MiB +Rice32 ≈ 43.953 MiB +差约 25 KiB / 143 帧 +≈ 175 B / Frame +≈ 0.056% +``` + +而 32×32 的 block 数约为 16×16 的 1/4,可能减少: + +```text +block metadata +loop / branch 次数 +per-block k 搜索次数 +``` + +因此正式冻结前必须在同一 C++ prototype 中记录: + +```text +encoded bytes +decode ms +encode ms +peak temporary bytes +block count +``` + +分别比较四类真实数据: + +```text +Wind +Rain +Wave +Current +``` + + +## 17.0 Block Partition 数学定义 + +在 Block Geometry 最终冻结前,用参数: + +```text +BLOCK_WIDTH +BLOCK_HEIGHT +``` + +表示候选尺寸。 + +定义: + +```text +blockCols = + (width + BLOCK_WIDTH - 1) / BLOCK_WIDTH + # integer floor division + +blockRows = + (height + BLOCK_HEIGHT - 1) / BLOCK_HEIGHT +``` + +对: + +```text +br ∈ [0, blockRows) +bc ∈ [0, blockCols) +``` + +定义: + +```text +rowBegin = br * BLOCK_HEIGHT +rowEnd = min(rowBegin + BLOCK_HEIGHT, height) + +colBegin = bc * BLOCK_WIDTH +colEnd = min(colBegin + BLOCK_WIDTH, width) +``` + +Block traversal: + +```text +for br = 0 .. blockRows-1: + for bc = 0 .. blockCols-1: + process block(br, bc) +``` + +等价: + +```text +blockIndex = br * blockCols + bc +``` + +Block 内: + +```text +for row = rowBegin .. rowEnd-1: + for col = colBegin .. colEnd-1: + index = row * width + col + + if validity[index] == 1: + emit one Rice symbol + else: + emit no symbol +``` + +边缘 block 自然裁切到实际 Grid 边界,不补虚拟 cell。 + +Predictor 继续使用全局 Grid 邻居, +不得因为进入新 block 而 reset。 + +## 17.1 正式发布规则 + +QBV3 第一版**不增加 blockSize 字段,也不增加 codec enum**。 + +这意味着:一旦 benchmark 选择最终 Block Geometry 并正式发布: + +```text +magic = QBV3 +version = 3 +``` + +选定的 block width / height 就成为 Container V3 ABI 的固定组成部分。 + +在正式冻结前,测试 Writer / Decoder 可以通过编译参数或测试参数运行 16×16 / 32×32; +**测试参数不得写进正式 QBV3 文件,也不得形成两种 production version=3 文件解释。** + +Block Geometry 未冻结期间产生的实验文件: + +```text +不得进入 production Manifest +不得进入正式 CDN / qbin_v3 发布路径 +不得被 NavSea production 配置发现或缓存 +不得标记为正式 QBV3 Golden Fixture +``` + +实验产物必须位于明确的: + +```text +/tmp +_runtime +experimental/ +或其他非 production 路径 +``` + +只有 Block Geometry 选定、文档回填唯一尺寸并完成对应 Writer/Decoder round-trip 后,才允许启用 production `QBIN_V3 / container_version=3` Manifest。 + +边缘 block 可以小于正式 block width / height。 + +Block 顺序: + +```text +north → south +west → east +``` + +即 block row-major。 + +Block 内 symbol 顺序: + +```text +cell row-major +仅遍历 validity=1 的 cell +``` + +如果 block 中没有 valid cell: + +```text +该 block 写 0 bytes +``` + +如果至少存在一个 valid cell: + +```text +1 byte riceK +随后为 Rice bitstream +最后补 0 bit 到 byte boundary +``` + +`riceK`: + +```text +uint8 +允许范围 0..31 +``` + +Writer 必须选择使该 block Rice bit 数最小的 `k`。 + +Tie: + +```text +选择较小 k +``` + +保证 deterministic output。 + +--- + +# 18. Rice Bitstream 精确定义 + +每个 ZigZag value `z`: + +```text +q = z >> k +r = z & ((1u << k) - 1) # 数学表达;实现使用安全 unsigned width +``` + +编码: + +```text +1. 写 q 个 0 bit +2. 写 1 个终止 bit +3. 写 k 个 remainder bit +``` + +remainder bit 顺序: + +```text +Least Significant Bit first +``` + +整个 block bitstream 也采用: + +```text +LSB-first within each byte +``` + +block 最后不足 8 bit: + +```text +high unused bits MUST = 0 +``` + +Decoder 根据 validity mask 可以提前知道该 block 的 `symbolCount`。 + +Decoder 必须: + +```text +精确解出 symbolCount 个 symbol +然后到达/验证该 block 的 byte boundary +``` + +不得依赖 sentinel。 + +不得读过 Descriptor.byteLength。 + +在 unary quotient 扫描过程中,Decoder 必须在每次 bit advance 前检查剩余 encoded range; +以下情况全部 fail-closed: + +```text +终止 bit 在 Plane 结束前不存在 +q 超过可安全重建 uint32 residual 的范围 +q << k / q * 2^k 发生溢出 +remainder 不完整 +非零 block padding bit +symbolCount 不足或超出 +``` + + +## 18.1 Logical Residual Upper Bound + +除 bitstream / uint32 overflow 检查外,Decoder 还必须根据逻辑 dtype 拒绝**数学上不可能出现**的 residual,避免 CRC 合法但恶意构造的超长 unary quotient 消耗无意义 CPU。 + +对普通非 Direction Numeric Plane,设 logical raw dtype 的最小/最大值为: + +```text +minRaw +maxRaw +``` + +在第 14 节 predictor 下,最大可能 residual 绝对值为: + +```text +MAX_ABS_RESIDUAL = 2 × (maxRaw - minRaw) +``` + +因此: + +```text +INT16 / UINT16 → MAX_ABS_RESIDUAL = 131070 +INT8 / UINT8 → MAX_ABS_RESIDUAL = 510 +``` + +对 Direction: + +```text +MAX_ABS_RESIDUAL = floor(N / 2) +``` + +并遵守第 15 节 half-circle tie 固定负方向规则。 + +Decoder 可据此得到安全上界: + +```text +MAX_ZIGZAG <= 2 × MAX_ABS_RESIDUAL +MAX_Q = MAX_ZIGZAG >> k +``` + +在扫描 unary zeros 时,一旦: + +```text +q > MAX_Q +``` + +即可立即 reject,不必继续扫描到 Plane 尾部。 + +完成 inverse ZigZag 后仍必须再次验证: + +```text +abs(residual) <= MAX_ABS_RESIDUAL +``` + +然后再执行 predictor reconstruction 与最终 logical raw dtype range 检查。 + +这些检查属于 parser hardening,不改变任何合法 QBV3 bitstream。 + +--- + + +## 18.2 Rice k 的 Deterministic 选择公式 + +对一个 non-empty block,设其按 block 内 canonical cell order 得到的 ZigZag symbol 为: + +```text +z_0, z_1, ... z_(n-1) +``` + +对每个候选: + +```text +k ∈ [0, 31] +``` + +定义纯 Rice symbol bit count: + +```text +bits(k) = +Σ_i [ (z_i >> k) + 1 + k ] +``` + +其中: + +```text +(z_i >> k) = unary zero count q ++1 = unary terminator bit ++k = remainder bits +``` + +Writer 必须选择: + +```text +k* = argmin bits(k) +``` + +如果多个 `k` 得到相同 `bits(k)`: + +```text +选择数值较小的 k +``` + +该 `k*` 属于 **Deterministic Writer Contract**: + +```text +Writer MUST 写入 k* +``` + +但 QBV3 Decoder 的 Binary acceptance 不要求重新计算全部 32 个候选 `k`。 + +Decoder 对每个 non-empty block 只要求: + +```text +0 <= riceK <= 31 +bitstream 可在 descriptor.byteLength 内完整解出 +symbolCount 精确 +padding 合法 +residual / reconstructed raw 合法 +``` + +因此: + +```text +riceK != k* +``` + +如果其他 Binary Contract 全部合法: + +```text +MUST NOT solely cause Decoder reject +``` + +也就是说: + +```text +最优 k* += canonical deterministic Writer output + +非最优但合法的 riceK +!= QBV3 binary corruption +``` + +Decoder MUST NOT 为 canonicality 验证而强制执行: + +```text +对已解出的每个 block 再扫描 k=0..31 重算最优 k* +``` + +优化目标**只计算 Rice symbol bits**。 + +以下固定开销不参与 k 比较: + +```text +1-byte riceK +block 尾部补到 byte boundary 的 zero padding +``` + +原因是必须冻结与当前编码实验一致的 deterministic 选择语义,避免不同 Writer 分别按: + +```text +raw bit count +``` + +或: + +```text +ceil(bit count / 8) +``` + +选择出不同 `k`。 + +选定 `k*` 后,该 non-empty block 的 encoded byte 数: + +```text +blockByteLength = +1 ++ +ceil(bits(k*) / 8) +``` + +其中前 `1` byte 为 `riceK`。 + +# 19. Numeric Plane Payload + +一个 Numeric Plane payload: + +```text +Block(0,0) +Block(0,1) +... +Block(last) +``` + +Block Geometry 使用第 17 节在正式 ABI freeze 前选定的唯一尺寸。 +正式 QBV3 不增加独立 block index table。 + +原因: + +```text +Grid size 已知 +Block size 在 Container V3 ABI 中固定 +Validity 已知 +每个非空 block 自带 1-byte k +Decoder 可以按 symbolCount 顺序解析 +``` + +V3 第一版优先完整 Frame decode,不为随机访问单个 block 增加额外索引成本。 + +## 19.0 Numeric Plane byteLength 的精确定义 + +对 Numeric Plane: + +```text +Descriptor.byteLength +``` + +严格定义为该 Plane 自身所有 block encoded bytes 的总和。 + +对每个 block: + +```text +validCount == 0 +→ block contributes 0 bytes + +validCount > 0 +→ block contributes: + 1 byte riceK + + ceil(RiceSymbolBits / 8) +``` + +因此: + +```text +NumericPlane.byteLength = +Σ(non-empty blockByteLength) +``` + +`byteLength` **不包含**: + +```text +Plane 之前的 align16 padding +Plane 之后的 align16 padding +Descriptor bytes +Header bytes +其他 Plane bytes +HTTP / gzip 外层编码 +``` + +Block 内最后一个 byte 的未使用 high bits仍必须为 0; +这些 bit 位属于该 block 的最后一个 encoded byte,因此属于 `byteLength`。 + +Plane 间的 16-byte alignment padding 不属于任何 Plane 的 `byteLength`,只由 `byteOffset / payloadLength` canonical packing 规则体现。 + +## 19.1 All-invalid Frame + +V3 第一版正式允许: + +```text +validity 全 0 +``` + +此时: + +```text +Validity Plane: +正常 BITSET,全 0 + +Numeric Plane: +byteLength = 0 +``` + +Decoder: + +```text +不读取 Rice block +直接 materialize 全 0 canonical raw Plane +``` + +成立条件必须严格为: + +```text +validCount == 0 +``` + +如果: + +```text +validCount > 0 +&& numeric byteLength == 0 +``` + +必须 reject。 + +`all-invalid`: + +```text +不是 corruption +不需要特殊 codec +不写虚假的 1-byte block +``` + +产品层如果不希望发布 all-invalid Frame,可以在 Weather Writer / Product Validation 拒绝, +但 Container 行为必须保持上述确定语义。 + +--- + +# 20. Plane Packing / Alignment + +Descriptor Table 排序继续继承 v4.4: + +```text +semantic +→ groupId +→ component +``` + +Plane payload 按 Descriptor 顺序排列。 + +```text +plane[0].byteOffset = 0 +plane[n].byteOffset = align16(previousEnd) +payloadLength = align16(lastEnd) +``` + +其中: + +```text +previousEnd = previous.byteOffset + previous.byteLength +align16(x) = (x + 15) & ~15 +``` + +Plane 间与尾部 padding: + +```text +MUST = 0 +``` + +区别仅在: + +```text +Numeric Plane byteLength += Rice encoded bytes +``` + +Validity: + +```text +byteLength = ceil(count/8) +``` + +V3 仍要求 canonical minimal packing,不允许额外预留空间。 + +--- + +# 21. CRC32 + +QBV3 使用: + +```text +CRC-32 / ISO-HDLC +``` + +完整参数冻结为: + +```text +width = 32 +poly = 0x04C11DB7 +init = 0xFFFFFFFF +refin = true +refout = true +xorout = 0xFFFFFFFF +check("123456789") = 0xCBF43926 +``` + +对 reflected 实现,等价使用: + +```text +reflected polynomial = 0xEDB88320 +``` + +与: + +```text +zlib.crc32 +``` + +兼容。 + +CRC 覆盖完整未经 HTTP Distribution Encoding 的 QBV3 bytes: + +```text +Header +Descriptor Table +Payload +Plane padding +Tail padding +``` + +计算时: + +```text +Header offset 76..79 +fileCrc32 的 4 bytes +按 0x00 0x00 0x00 0x00 参与计算 +``` + +Writer: + +```text +1. 先写 fileCrc32 = 0 +2. 对完整 QBV3 bytes 计算 CRC +3. 将 uint32 CRC little-endian 写回 offset 76..79 +``` + +Parser: + +```text +读取 stored CRC +↓ +计算时把 offset 76..79 逻辑视为 0 +↓ +比较 uint32 +``` + +不得对 gzip / HTTP transport 后的 bytes 计算 QBV3 CRC。 + +如果同时 benchmark QBV2 / QBV3, +CRC helper 必须一致,并单独记录 `crcMs`。 + +# 22. Parser 安全规则:Encoded Budget 与 Decoded Budget 分离 + +QBV3 Version 3 的 parser conformance limits 冻结为: + +```text +MAX_PLANE_COUNT = 32 +MAX_CELL_COUNT = 2,097,152 +MAX_QBIN_BYTES = 64 MiB +MAX_DECODED_BYTES_PER_FRAME = 32 MiB +``` + +其中: + +```text +64 MiB = 64 × 1024 × 1024 bytes +32 MiB = 32 × 1024 × 1024 bytes +``` + +这些值属于 QBV3 Version 3 的 normative acceptance limits。 + +当前 Wind / Rain / Wave / Current 生产需求确认在该容量范围内, +因此 QBV3 Version 3 继续冻结这些上限。 + +任何 conforming Decoder 对超过上述限制的文件: + +```text +MUST fail-closed +``` + +实现可以额外拥有更严格的本地资源 policy, +但不得把一个超出上述 QBV3 conformance limit 的文件视为 Version 3 合法文件。 + +未来如果业务确实需要突破任一上限: + +```text +MAX_PLANE_COUNT +MAX_CELL_COUNT +MAX_QBIN_BYTES +MAX_DECODED_BYTES_PER_FRAME +``` + +不得在已发布 `QBV3/version=3` 上静默放宽后仍声称 ABI 未改变。 + +应通过: + +```text +新的 Container Version +或明确 successor binary specification +``` + +升级并重新冻结对应 conformance limits。 + + + +QBIN V3 的 encoded size 与 decoded size 不再近似相等,因此必须分别限制,并冻结 Parser 执行顺序。 + +## 22.1 Stage A:便宜的结构与 Per-frame Decoded Budget 预检查 + +在 CRC 与任何 Rice entropy decode 之前,先执行不需要解释 Rice bitstream 的结构检查: + +```text +magic == QBV3 +version == 3 + +planeCount > 0 +planeCount <= MAX_PLANE_COUNT + +width >= 2 +height >= 2 +cells = checked(width × height) +cells <= MAX_CELL_COUNT + +payloadLength 合理 +actualFileLength == 112 + planeCount×32 + payloadLength +actualFileLength <= MAX_QBIN_BYTES + +reserved == 0 +Header 固定 ABI 值合法 +bounds / time 合法 + +Descriptor order canonical +(semantic, groupId) unique +count == cells +dtype / valueClass / component 合法 +scale / offset 满足 canonical float 规则 +byteOffset / byteLength 范围合法 +canonical align16 packing / payloadLength 公式合法 +``` + +同时执行 Encoded Budget: + +```text +actual file bytes +payloadLength +每个 Plane encoded byteLength +所有 offset + length checked arithmetic +``` + +并在任何大 Plane allocation 前执行 Decoder 自己负责的 per-frame decoded budget: + +```text +decodedNumericPlaneBytes = + checked(count × logical numeric dtype width) + +decodedNumericBytes = + checked(sum(decodedNumericPlaneBytes)) + +decodedValidityBytes = + ceil(count / 8) + +decodedFrameBytes = + checked(decodedNumericBytes + decodedValidityBytes) +``` + +同一个 canonical Validity BITSET 只计一次。 + +必须满足: + +```text +decodedFrameBytes <= MAX_DECODED_BYTES_PER_FRAME +``` + +否则: + +```text +fail-closed +``` + +QBV3 Binary ABI 只冻结“必须有 per-frame decoded allocation budget 与 checked arithmetic”这一安全要求。 + +跨多个 retained Frame 的 Store memory policy: + +```text +不属于 QBV3 Binary Container +``` + +移至 Appendix A 的 Non-normative Integration Notes。 + +Stage A 不扫描 Rice unary,不根据 Rice 内容做 entropy decode。 + +## 22.2 Stage B:CRC32 + +Stage A 通过后,**先验证完整 QBV3 CRC32**。 + +```text +结构 / length / bounds / budget +↓ +CRC32 +``` + +CRC 失败: + +```text +立即 fail-closed +``` + +不得先对 CRC 已损坏文件执行 Rice entropy decode。 + +## 22.3 Stage C:Canonical Payload / Validity + +CRC 通过后再验证 Payload 内容级规则: + +```text +Plane / tail padding byte == 0 +Validity dtype == BITSET +Validity byteLength == ceil(count/8) +Validity tail unused high bits == 0 +``` + +随后计算: + +```text +validCount +``` + +并冻结 all-invalid 对 Numeric Plane 的约束: + +```text +validCount == 0 → Numeric byteLength MUST == 0 +validCount > 0 → Numeric byteLength MUST > 0 +``` + +### 22.3.1 Zero-length Plane Canonical Packing + +all-invalid Frame 中可能出现多个 zero-length Numeric Plane。 + +canonical packing 继续按: + +```text +nextOffset = align16(previousEnd) +``` + +计算。 + +因此: + +```text +zero-length Plane MAY share byteOffset +with adjacent zero-length Plane +or the next non-empty Plane. +``` + +合法示例: + +```text +Plane A offset=0 length=0 +Plane B offset=0 length=0 +Plane C offset=0 length=validityBytes +``` + +这属于合法 canonical representation,不是真实 payload overlap。 + +Parser 的 overlap validation MUST: + +```text +只对实际占用非零 byte range 的 Plane 做 overlap 判断; +zero-length interval 不得被误判为 overlapping payload corruption。 +``` + +## 22.4 Stage D:Rice Entropy Decode + +只有 Stage A/B/C 全部通过后,才进入 Numeric Rice decode。 + +逐 block / symbol 验证: + +```text +riceK <= 31 +非最优 riceK 本身不属于 corruption,不要求 Decoder 重算 k* +block exact symbolCount +unary quotient <= 第 18.1 节 logical MAX_Q +bitstream 不越界 +q / remainder / ZigZag arithmetic 不溢出 +inverse ZigZag residual 在逻辑上限内 +predictor reconstruction checked arithmetic + +普通 Numeric: +decoded raw 必须落入 logical dtype range + +Direction: +decoded raw 必须同时满足: +0 <= rawDirection < N + +否则: +fail-closed + +block padding bit == 0 +Plane 解码后精确消费 descriptor.byteLength +``` + +Malformed Rice stream: + +```text +fail-closed +``` + +Direction corruption / acceptance 必须额外覆盖: + +```text +decoded rawDirection >= N +→ reject +``` + +`UINT16` dtype range 合法但超出当前 Circular Contract `[0,N)` 的值不得被接受。 + +## 22.5 固定执行顺序 + +正式 Parser 顺序冻结为: + +```text +Header / Descriptor / file length / bounds +↓ +Encoded + Decoded budget 预检查 +↓ +CRC32 +↓ +Canonical padding + Validity BITSET 检查 / validCount +↓ +Rice entropy decode + ZigZag + Predictor reconstruction +↓ +canonical raw materialization +↓ +QBV3 Binary Decoder Accepted +``` + +之后如果存在外部 Profile Contract: + +```text +Encoded Field Ingress +↓ +Profile Contract Validation +↓ +Product / Runtime acceptance +``` + +该第二阶段不属于 QBV3 Binary Container validation。 + +实现可以合并无副作用的小检查,但不得把 Rice entropy decode 提前到 CRC 之前,也不得在 budget 预检查前进行大内存 allocation。 + +--- + +# 23. Canonical Decoded Raw Reference Model + +本节不是 Runtime API 规范,而是 QBV3 Round-trip 的参考解码结果定义。 + +Numeric Plane: + +```text +QBV3 Rice Plane +↓ +Rice decode +↓ +inverse ZigZag +↓ +Predictor reconstruction +↓ +logical quantized integer +↓ +canonical little-endian fixed-width raw bytes +``` + +Canonical raw: + +```text +INT16 → count × 2 bytes, little-endian two's-complement +UINT16 → count × 2 bytes, little-endian +UINT8 → count × 1 byte +INT8 → count × 1 byte, two's-complement +``` + +Invalid cell: + +```text +validity = 0 +→ canonical decoded raw MUST = 0 +``` + +Validity reference decoded form: + +```text +保持 canonical BITSET bytes +LSB-first +tail unused high bits = 0 +``` + +bit-exact Round-trip 的权威比较对象: + +```text +Writer Quantizer integer grid ++ +Writer Validity BITSET +``` + +与: + +```text +Decoder canonical integer grid ++ +Decoder Validity BITSET +``` + +必须完全一致。 + +任何 Runtime-specific: + +```text +DecodedFieldFrame +ContentIdentity +hash +FieldSeries +Projection +Renderer +``` + +均不属于 QBV3 Binary ABI,移至 Appendix A。 + +# 24. 非 Normative 的 Profile / 实验 / Distribution 信息 + +Wind / Rain / Wave / Current 第一阶段候选 Profile、压缩实验统计、 +HTTP Distribution Encoding 与 Manifest 示例都属于产品 / Profile / 发布层信息, +不属于 `QBV3/version=3` Binary ABI。 + +统一移至 Appendix B。 + +QBV3 Binary Parser 不得根据: + +```text +wind / 2 +wave / 2 +rain / 2 +current / 2 +Manifest +文件扩展名 +HTTP Content-Encoding +``` + +改变 Header / Descriptor / Validity / Predictor / ZigZag / Rice / Packing / CRC 的解释。 + +# 25. Container Version Dispatch 的最小要求 + +如果同一系统同时支持 QBV2 / QBV3: + +```text +QBV2 → magic "QBV2", version=2 +QBV3 → magic "QBV3", version=3 +``` + +Unknown magic/version combination: + +```text +fail-closed +``` + +除以上识别规则外,迁移、Store、Runtime、平台实现均不属于 QBV3 Binary ABI。 + +# 26. Binary Spec 与消费实现分离 + +QBV3 Binary Container 只规定: + +```text +bytes 如何写 +bytes 如何验证 +bytes 如何无损恢复 quantized integer grid +``` + +NavSea、React、Store、Binding、Renderer、平台 Bridge 等消费方式: + +```text +不属于本文 normative Binary ABI +``` + +相关背景统一放入 Appendix A。 + +# 27. Decoder 实现语言中立 + +QBV3 不规定 Decoder 必须使用何种语言。 + +任何 conforming implementation 都必须对同一合法 QBV3 bytes 得到相同: + +```text +Header +Descriptor +Validity +canonical quantized integer grid +``` + +并对同一 malformed bytes 得到相同 Binary Accept / Reject 结论。 + +# 28. Deterministic Writer 与 Container V3 Codec ABI + +相同: + +```text +Canonical Field +Field Profile Version +Container Version +``` + +必须生成: + +```text +byte-for-byte identical QBV3 +``` + +正式 `QBV3 / version=3` 发布前,必须先完成第 17 节 Block Geometry benchmark 并选择唯一尺寸。 +一旦正式发布,下列内容全部成为 Container V3 Binary ABI: + +```text +validity-aware predictor 规则 +predictor fallback 顺序 +Direction circular residual 规则 +ZigZag 定义 +最终冻结的 Block Geometry +Block traversal order +Cell traversal order +valid-cell-only symbol stream +Rice k search 0..31 +Rice k tie → smaller k +Rice unary form +Rice remainder bit order +byte 内 bit order +block padding +Descriptor canonical order +Plane canonical packing +Padding = 0 +reserved = 0 +CRC algorithm +``` + +不得依赖: + +```text +hash/map iteration order +platform endian +compiler bitfield layout +undefined signed overflow / shift behavior +``` + +如果正式发布后要改变: + +```text +Block Geometry +Predictor +Rice bitstream +bit order +新增第二种内部 codec +``` + +不能只修改文档修订号让相同 `QBV3/version=3` 出现两种解释。 +必须: + +```text +升级新的 Container Version +``` + +或者在**新的 Container Version 设计阶段**明确引入可识别 codec 字段。 +V3 第一版继续保持单一固定 codec,不为未来可能性提前增加 codec switch。 + +--- + +# 29. Golden Binary Fixtures + +正式冻结后至少准备: + +```text +Wind +Wave +Rain +Current +``` + +四类真实数据的 Golden QBV3 Fixture。 + +每个 Fixture 至少记录: + +```text +QBV3 document revision +container version +magic + +referenceTimeUtc +validTimeUtc +grid / bounds + +Header bytes +Descriptor bytes + +canonical physical baseline +quantized integer baseline +validity baseline + +正式 QBV3 bytes +CRC32 +SHA-256 + +raw QBV3 byteLength +gzip byteLength(若另外做 distribution benchmark) + +per-plane encoded byteLength +blockCount +nonEmptyBlockCount +Rice k distribution + +writer timing +decoder timing +peak temporary memory +``` + +Block Geometry 冻结前允许生成: + +```text +experimental/16x16/ +experimental/32x32/ +``` + +两组 benchmark fixture。 + +最终 Block Geometry 冻结后必须重新生成唯一正式 Golden Fixture 集合。 + +# 30. Round-trip 验收 + +最重要的 Round-trip 先验证整数 bit-exact: + +```text +Writer Quantizer output integer array +↓ +QBV3 Writer +↓ +QBV3 file +↓ +C++ QbinV3Decoder +↓ +Decoded canonical raw integer array +``` + +结果必须: + +```text +array_equal == true +``` + +不是近似比较。 + +同时: + +```text +validity array_equal == true +invalid cell decoded raw == 0 +``` + +物理误差测试单独比较: + +```text +原始 canonical physical +vs +decoded raw × scale + offset +``` + +确保: + +```text +Predictor / ZigZag / Rice 不贡献任何额外误差 +物理误差只来自 Field Profile Quantization +``` + +全量验证阶段建议覆盖全部生成 Frame,而不是只抽样。 + +Direction 必须包含: + +```text +359° ↔ 0° +N-1 ↔ 0 +half-circle tie +``` + +All-invalid fixture 必须验证: + +```text +Validity 全 0 +Numeric byteLength = 0 +Decoded raw 全 0 +zero-length Numeric Plane 可共享 canonical byteOffset +``` + + +--- + +# 31. Binary Reject / Accept / Writer Validation 验收 + +本节按职责拆分为三组,避免把: + +```text +Binary corruption +Binary canonical-but-unusual input +Writer input validation +``` + +混为同一类。 + +## 31.1 Binary Reject Cases + +以下 malformed / corruption 情况必须: + +```text +MUST fail-closed +``` + +至少覆盖: + +```text +Bad magic +Bad version +Big-endian/mixed endian + +Header reserved != 0 +Descriptor reserved != 0 + +Invalid planeCount +Invalid cell count +Overflowed width×height + +Bad byteOffset +Bad byteLength +Non-canonical descriptor order + +Extra padding +Nonzero padding + +Bad CRC + +Truncated Rice block +Rice unary never terminates before end +riceK > 31 + +Block symbol count insufficient +Block symbol count extra data +Nonzero block padding bits + +Invalid validity tail bits + +Numeric offset = -0.0f +Header bound = -0.0 binary64 + +Direction invalid canonical scale +QBV3 decoded rawDirection >= N + +non-zero Plane actual byte-range overlap + +semantic = 3 but valueClass != VALIDITY +semantic = 3 but dtype != BITSET +valueClass = VALIDITY but semantic != 3 +dtype = BITSET but valueClass != VALIDITY +``` + +以上情况: + +```text +QbinV3Decoder +→ reject +``` + +## 31.2 Binary Accept Cases + +以下情况本身不得单独导致 Binary reject: + +```text +non-optimal riceK ++ otherwise-valid block +→ MUST accept + +Unknown fieldType +→ MUST accept as opaque hint + +fieldType = 0 +→ MUST accept as UNKNOWN / UNSPECIFIED opaque hint + +all-invalid Frame +Validity 全 0 +Numeric byteLength = 0 +→ MUST accept + +all-invalid zero-length adjacent Plane shared byteOffset +→ MUST accept as canonical packing +``` + +特别冻结: + +```text +riceK != Writer 最优 k* +``` + +如果其余: + +```text +riceK range +symbolCount +padding +residual +raw range +descriptor.byteLength +``` + +全部合法,则: + +```text +MUST NOT solely cause Decoder reject +``` + +Decoder 不要求为 canonicality 重新计算: + +```text +k = 0..31 +``` + +的全部候选。 + +## 31.3 Writer Validation Cases + +以下属于 Writer input / deterministic output 验证, +不是“Decoder corruption”: + +```text +validity=1 ++ physical = NaN / +Inf / -Inf +→ Writer MUST reject + +validity=0 ++ numeric physical = NaN / +Inf / -Inf / missing +→ Writer MAY accept +→ value MUST be ignored +→ MUST NOT enter Quantizer +→ MUST NOT emit Rice symbol + +Writer MUST NOT: +通过 NaN / Inf 自动推导 validity=0 + +delta64 non-finite +→ Writer MUST reject + +normalized64 non-finite +→ Writer MUST reject + +normalizedDirection64 non-finite +→ Writer MUST reject + +physicalDirection 不满足: +finite +0 <= angle < 360 +DIRECTION_TO +→ Writer MUST reject / 上游先 normalize + +Writer 对每个 non-empty block: +MUST 写 deterministic 最优 k* +``` + +## 31.4 外部 Profile / Integration Validation + +以下属于外部 Integration / Profile Contract validation, +不得标记为 QBV3 binary corruption: + +```text +Missing Profile-required Plane +Undeclared REQUIRED relative to external Contract +Contract mismatch +fieldType / fieldProfile mismatch +QBV2 Direction legacy acceptance +``` + +QBV2 的 Direction legacy regression 应在 V2 ingress compatibility test 中覆盖, +不属于 QBV3 corruption suite。 + + +# 32. Codec 性能验收 + +QBV3 的 Binary ABI 不规定消费方 Runtime 生命周期,但正式冻结 Block Geometry 前必须完成 codec-level benchmark。 + +同一测试平台、同一 build 配置下至少记录: + +```text +raw QBV3 bytes +gzip QBV3 bytes(若测试 distribution gzip) + +crcMs +entropyDecodeMs +totalDecodeMs +encodeMs + +peakTemporaryBytes +decodedFrameBytes + +blockCount +nonEmptyBlockCount +Rice k distribution +``` + +必须比较: + +```text +16×16 +vs +32×32 +``` + +并覆盖: + +```text +Wind +Rain +Wave +Current +``` + +如果同时比较 QBV2 / QBV3: + +```text +CRC 实现必须一致 +crcMs 与 entropyDecodeMs 必须分开 +``` + +不要求 QBV3 entropy decode 比固定宽度 QBV2 decode 更快。 + +Block Geometry 的选择依据应综合: + +```text +encoded bytes +encode CPU +decode CPU +peak temporary memory +实现复杂度 +``` + +而不是只看压缩率。 + +## 32.1 Streaming reconstruction 实现目标 + +推荐 Decoder: + +```text +compressed Plane input +↓ +BitReader +↓ +逐 symbol Rice decode +↓ +inverse ZigZag +↓ +立即 Predictor reconstruction +↓ +直接写最终 canonical fixed-width raw storage +``` + +应避免为了实现方便同时建立多个 full-grid 临时数组: + +```text +residual[] +zigzagDecoded[] +predictorOutput[] +rawOutput[] +``` + +这是性能实现目标,不改变 QBV3 bitstream。 + +# 33. QBV3 第一阶段 Binary ABI 验收基线 + +正式冻结 / 发布 `QBV3/version=3` 前至少满足: + +```text +1. Header 112-byte layout、Little Endian、reserved=0 已冻结。 +2. layout=1 / projection=1 / sampleOrder=1 / gridRegistration=1 已冻结。 +3. tileZ=0xffff / tileX=0 / tileY=0 已冻结。 +4. referenceTimeUtc / validTimeUtc 为正 int64 Unix UTC seconds。 +5. Binary Parser 不强制 validTimeUtc >= referenceTimeUtc。 +6. Bounds 是 finite binary64 WGS84 lon/lat,0 必须编码为 +0.0。 +7. WEB_MERCATOR + NODE projected-space sample 公式有 Golden Test。 +8. Descriptor 32-byte layout 与 dtype byte enum 已冻结。 +9. valueClass / component / core unit / VALIDITY semantic structural invariants 已冻结。 +10. Numeric dtype 仅允许 INT16/UINT16/UINT8/INT8;FLOAT32 Rice Plane reject。 +11. Shared Validity 有且只有一个,semantic=3、dtype=BITSET、REQUIRED。 +12. Validity scale=+1.0f、offset=+0.0f。 +13. Numeric scale > 0 finite;offset finite;零 offset 使用 +0.0f。 +14. Quantizer 先确定 Descriptor binary32 scale/offset,再提升到 binary64 计算。 +15. validity=1 的 canonical physical sample MUST 为 finite binary64; + validity=0 的 numeric physical value 不参与 Quantizer,可为 NaN / Inf / missing。 +16. round_half_away_from_zero 精确公式有 ±0.5 边界 Golden Test。 +17. Shared Validity BITSET LSB-first,tail unused high bits=0。 +18. Decoder 先解析全部 Descriptor,逻辑上先定位/读取 Validity,再解 Numeric。 +19. 2D Predictor 在整个 Grid 连续,不在 block 边界 reset。 +20. Direction canonical float32(360/N)、N=floor(ratio+0.5)、raw∈[0,N)。 +21. Direction half-circle tie 固定为 -N/2。 +22. ZigZag / inverse ZigZag 使用固定宽度无 UB 语义。 +23. Block partition 数学公式、block row-major、cell row-major 已冻结。 +24. 16×16 / 32×32 使用同一 Writer/Decoder prototype 完成四类真实 benchmark。 +25. Rice k 使用 bits(k)=Σ((z>>k)+1+k),tie 取较小 k。 +26. k 优化不把 riceK byte / byte padding 纳入目标。 +27. non-empty blockByteLength = 1 + ceil(RiceSymbolBits/8)。 +28. Numeric Plane byteLength = Σ(non-empty blockByteLength),不含 align16 padding。 +29. all-invalid:Validity 全 0、Numeric byteLength=0、decoded raw 全 0。 +30. zero-length Plane shared byteOffset 不误判 overlap。 +31. Plane / tail align16 padding 全 0。 +32. CRC-32/ISO-HDLC 完整参数、check value 与 CRC field-zeroing 已冻结。 +33. Parser 顺序:结构+budget → CRC → Validity → Rice。 +34. MAX_Q / residual logical upper-bound hardening 生效。 +35. malformed Rice / truncation / overflow / nonzero padding 全部 fail-closed。 +36. MAX_PLANE_COUNT=32。 +37. MAX_CELL_COUNT=2,097,152。 +38. MAX_QBIN_BYTES=64 MiB。 +39. MAX_DECODED_BYTES_PER_FRAME=32 MiB。 +40. 超出上述 Version 3 conformance limits 的文件 fail-closed。 +41. 全部 Numeric Plane integer bit-exact round-trip。 +42. strict Validity bit-exact round-trip。 +43. Direction 359°↔0° / N-1↔0 / half-circle / predictor wrap 通过。 +44. Writer deterministic:相同 canonical input + 相同 Descriptor → byte-for-byte 相同 QBV3。 +45. Golden Binary Fixtures 覆盖 Wind / Rain / Wave / Current。 +46. Block Geometry 未冻结前实验文件不进入 production。 +47. 最终选择唯一 BLOCK_WIDTH / BLOCK_HEIGHT 后重新生成正式 Golden Binary。 +48. validity=1 的 physical sample 必须 finite;validity=0 的 numeric physical value 被忽略,不参与 Quantizer。 +49. Writer 不得通过 NaN/Inf 自动推导 Validity。 +50. ZigZag / inverse ZigZag 精确数学公式有 Golden Test。 +51. Quantizer binary64 subtraction/division 使用 round-to-nearest, ties-to-even 语义。 +52. Direction input 在 Writer 前已经 canonical 为 DIRECTION_TO 且 0<=angle<360。 +53. unknown fieldType 不得单独导致 binary reject。 +54. 任意 Numeric offset 数值为 0 时必须使用 +0.0f,-0.0f reject。 +55. Validity BITSET 的 byteIndex / bitIndex 公式有 byte-exact Golden。 +56. semantic==3 ⇔ valueClass==VALIDITY ⇔ dtype==BITSET 的双向规则通过 corruption test。 +57. delta64 / normalized64 / normalizedDirection64 non-finite → Writer FAIL。 +58. fieldType=0 作为 UNKNOWN / UNSPECIFIED opaque hint 可 binary-accept。 +59. Writer 对每个 non-empty block 写最优 k*。 +60. Decoder 不因非最优但 otherwise-valid 的 riceK 单独 reject,也不要求重算 k*。 +61. WEB_MERCATOR mercatorX / mercatorY / inverse 数学公式有定位 Golden Test。 +62. 一个 targetTime 一个独立 QBV3 Frame。 +``` + +在 v2.2 中: + +> **除最终 `BLOCK_WIDTH / BLOCK_HEIGHT` 尚需通过 16×16 / 32×32 C++ benchmark 实测选择外,其他核心 QBV3 Binary ABI 语义已完成文档冻结;Rice canonicality 采用 Writer-canonical / Decoder-permissive。v2.2 之后不再因实现偏好继续修改 Binary Spec。** + +消费侧 Store、Binding、Renderer、NavSea、React、Profile Contract、Manifest 与平台 Bridge 不作为 QBV3 Binary ABI 的冻结条件。 + +# 34. 版本与命名最终规则 + +以后统一使用: + +```text +QBIN V2 += Container Version 2 += Magic QBV2 += 最终权威设计基线文档 v4.4 + +QBIN V3 += Container Version 3 += Magic QBV3 += 本文从 Document Revision v1.0 开始;当前修订为 v2.2 +``` + +禁止再使用: + +```text +“QBIN V4” +``` + +来代指 Container V2。 + +如果需要说明历史: + +```text +QBIN V2 / v4.4 design baseline +``` + +如果需要说明新格式: + +```text +QBIN V3 +``` + +Field Profile Version 独立: + +```text +wind / 2 +wave / 2 +rain / 2 +current / 2 +``` + +--- + +# 35. 最终冻结候选语句 + +**架构继承:** + +> QBIN V3 继承 QBIN V2 / v4.4 的 Semantic、Unit、Field Profile Contract、Independent Binding、Forecast Runtime 与 Generic Renderer 架构;只在 Encoded Field Ingress 之前改变二进制 Numeric Plane 表达。 + +**数值误差:** + +> V3 的物理误差只来自 Field Profile Quantization;2D Predictor、ZigZag 与 Rice 必须 bit-exact 可逆。 + +**统一编码:** + +> V3 第一版所有整数 Numeric Plane 统一使用 validity-aware 2D Predictor + ZigZag + 单一固定 Block Geometry 的 adaptive Rice;Validity 继续使用 shared BITSET。Block Geometry 在正式 `QBV3/version=3` 发布前通过 16×16 / 32×32 C++ benchmark 选定,发布后成为不可兼容修改的 Container V3 ABI。 + +**Direction:** + +> QBV3 `DIRECTION_ANGLE` 使用 Generic Circular Quantization Contract;合法 scale 必须满足 canonical `float32(360/N)`,Container Parser 不根据具体天气 Profile 名称放宽或改变该规则。 + +**Decoded contract:** + +> QBV3 Decoder 的 reference decoded result 是 canonical fixed-width quantized raw grid;Predictor / ZigZag / Rice 必须完全可逆,Invalid cell 的 canonical raw 固定为 0。 + +**职责边界:** + +> QBV3 Binary ABI 只规定 Writer 如何生成 bytes、Parser 如何验证 bytes、Decoder 如何无损恢复 quantized integer grid;消费侧架构不属于 Container 规范。 + +**时间单位:** + +> 一个 targetTime = 一个独立 QBV3 Frame,不把多个时间点合并成一个网络归档。 + +**四类第一阶段 Profile:** + +```text +Wind U/V 0.1 m/s +Rain 0.1 mm/h +Wave Height 0.05 m / Period 0.1 s / Direction 1° +Current U/V 0.02 m/s +``` + +**Wave Range:** + +> Wave Height 不以 10 m 为格式上限;第一阶段至少验证 0–30 m,10 m 必须正常表达。 + +**版本:** + +> 新编码不继续冒充 QBIN V2,也不创建“QBIN V5”第二套编号;直接使用 QBIN V3 / QBV3 / version=3。 + +--- + +# 36. 当前状态与下一步 + +本文当前状态: + +```text +设计冻结候选 v2.2 +``` + +已经完成: + +```text +Header / Descriptor byte layout +Little Endian / canonical float +Header fixed FRAME ABI +WEB_MERCATOR + NODE Grid 空间语义 +DType / ValueClass / Component / Core Unit 机器值 +Shared Validity BITSET +Generic Numeric Quantization +round_half_away_from_zero +Direction Circular Contract +2D Predictor +ZigZag +Rice bitstream +Rice k deterministic 选择公式 +Block partition 数学公式 +Numeric Plane byteLength +Plane Packing / Alignment +All-invalid representation +CRC-32/ISO-HDLC 完整参数 +Parser 执行顺序与 fail-closed +Parser conformance limits +Canonical decoded raw reference model +Deterministic Writer 规则 +Golden / Round-trip 验收规则 +Invalid-cell / non-finite physical Writer 语义 +ZigZag / inverse ZigZag 精确数学 ABI +Quantizer binary64 rounding 语义 +unknown fieldType forward-compatible Binary 行为 +Validity BITSET 精确位索引 +Numeric canonical +0.0 offset 全局规则 +WEB_MERCATOR 精确数学公式 +VALIDITY semantic 双向约束 +Quantizer intermediate finite 规则 +fieldType=0 UNKNOWN / UNSPECIFIED 语义 +Rice Writer-canonical / Decoder-permissive 规则 +``` + +尚未完成: + +```text +16×16 / 32×32 真实 Writer/Decoder benchmark +最终 BLOCK_WIDTH / BLOCK_HEIGHT freeze +正式 QBV3 Writer +正式 QBV3 Decoder +四类 full integer/validity bit-exact round-trip +正式 Golden Binary Fixtures +corruption / fail-closed 全量验证 +codec encode/decode CPU + peak temporary memory benchmark +``` + +其中: + +```text +最终 BLOCK_WIDTH / BLOCK_HEIGHT +``` + +是正式发布前唯一仍需通过实测选择的核心 Binary ABI 参数。 +其余 Binary 结构与数据语义在 v2.2 中不再保留未决分支。 + +从 v2.2 起,除非 Prototype / Golden / Corruption Test 证明现有 Binary Contract +存在真实不可实现、不可唯一解释或自相矛盾的问题,否则不得再因实现偏好修改 QBV3 Binary ABI。 + +在以上实现与验证完成前: + +```text +QBIN V2 / v4.4 仍是已实施历史基线 +QBIN V3 v2.2 是下一代候选生产格式设计 +``` + +下一步建议严格按以下顺序: + +```text +1. 以本文 v2.2 作为最终设计冻结稿,实现最小 deterministic QBV3 Writer + Decoder prototype。 +2. 同一 prototype 仅以测试参数运行 16×16 / 32×32。 +3. 用 Wind / Rain / Wave / Current 真实 Frame 做 integer/validity bit-exact round-trip。 +4. 同时记录 raw/gzip bytes、encodeMs、crcMs、entropyDecodeMs、totalDecodeMs、peakTemporaryBytes。 +5. 选择唯一 BLOCK_WIDTH / BLOCK_HEIGHT。 +6. 将唯一 Block Geometry 回填本文,删除所有实验双选语义。 +7. 生成正式 Golden Binary Fixtures。 +8. 完成 corruption / fail-closed 全量验证。 +9. 将该版本作为 QBV3/version=3 production-final Binary ABI 冻结稿。 +``` + +--- + +# Appendix A. Non-normative Integration / Compatibility Notes + +本 Appendix **不属于 QBV3 Binary ABI**。 + +## A.1 QBV2 Legacy Direction Compatibility + +当前项目若继续支持 QBV2,应保持其既有 authoritative ingress acceptance, +包括历史 Direction scale tolerance。 + +该规则属于: + +```text +QBV2 compatibility +``` + +不是 QBV3 Container 规则。 + +QBV3 只使用正文第 15 节的 canonical Circular Contract。 + +## A.2 ContentIdentity / Runtime Hash + +当前项目可为了 QBV2/QBV3 共存,让 decoded Plane 复用既有 runtime identity helper。 + +这些: + +```text +uint64 hash +runtime contentIdentity string +``` + +不写入 QBV3 文件,也不参与 QBV3 CRC,因此不是 Container Version 3 ABI。 + +## A.3 Profile Contract + +外部 Profile Contract 可以比 QBV3 Binary Parser 更严格,例如校验: + +```text +expectedDType +expectedScaleBits +expectedOffsetBits +required Plane 集合 +``` + +这些属于: + +```text +Profile-valid +``` + +而不是: + +```text +Binary-valid +``` + +一个 binary-valid 但 profile-invalid 的 QBV3 文件不应被称为 binary corruption。 + +## A.4 Store / Runtime / NavSea / React / Platform Bridge + +以下都不是 QBV3 Binary ABI: + +```text +EncodedFieldStore +FieldSeries +T0 / T1 / mix +Binding +Renderer +NavSea 下载/缓存/预取 +React / RN API +Android / iOS transport +retained-memory policy +``` + +这些实现不得反向改变正文对 `QBV3/version=3` bytes 的解释。 + +--- + +# Appendix B. Informative Profile / Product / Distribution Notes + +本 Appendix 只保留当前 WeatherV2 第一阶段实验与产品背景, +用于 Writer Prototype 和 Golden Fixture 选样。 + +## B.1 第一阶段候选 Profile + +```text +Wind / 2 + U/V INT16 + 0.1 m/s + offset +0.0 + +Rain / 2 + UINT16 + 0.1 mm/h + offset +0.0 + +Wave / 2 + Height UINT16 0.05 m + Period UINT16 0.1 s + Direction UINT16 1° + offset +0.0 + +Current / 2 + U/V INT16 + 0.02 m/s + offset +0.0 +``` + +这些 Profile 用于当前真实数据 benchmark, +但 QBV3 Parser 不得写 `if WIND / WAVE / RAIN / CURRENT`。 + +## B.2 当前量化误差实验 + +```text +Wind: +speed MAE ≈ 0.02461 m/s +speed p95 ≈ 0.05205 m/s +speed p99 ≈ 0.06201 m/s +speed max ≈ 0.07071 m/s + +Rain: +MAE ≈ 0.0149 mm/h +max ≈ 0.0500 mm/h + +Wave: +Height max additional error ≈ 0.02 m +Direction max additional error ≈ 0.5° + +Current: +speed MAE ≈ 0.00646 m/s +speed max ≈ 0.0141 m/s +``` + +Predictor / ZigZag / Rice 不允许增加额外数值误差。 + +## B.3 Block-size 既有实验 + +Wind 143 帧既有模型结果: + +```text +Rice16 ≈ 43.929 MiB +Rice32 ≈ 43.953 MiB + +差约: +25 KiB / 143 Frames +≈ 175 B / Frame +≈ 0.056% +``` + +因此最终 Block Geometry 不能只看压缩率, +必须等待真实 Writer/Decoder CPU 与 memory benchmark。 + +## B.4 Distribution / Manifest + +QBV3 Container 与 HTTP Distribution Encoding 分离。 + +可以外层发布: + +```text +.qbin +.qbin.gz +HTTP Content-Encoding +``` + +但解压后必须恢复同一 QBV3 bytes。 + +Manifest / CDN path / Profile metadata: + +```text +不属于 QBV3 Binary ABI +``` diff --git a/docs/40_任务/10_架构调查指令书_v1.md b/docs/40_任务/10_架构调查指令书_v1.md new file mode 100644 index 000000000..1b32b6cc3 --- /dev/null +++ b/docs/40_任务/10_架构调查指令书_v1.md @@ -0,0 +1,754 @@ +# MapLibreParticleLab 10_架构调查指令书 v1 + +## 1. 任务基本信息 + +- 项目:MapLibreParticleLab +- 大阶段:`10_架构调查` +- 当前基线分支:`particle/main` +- 当前前置状态:总则 v1 已提交并推送 +- 任务性质:只读架构调查 +- 是否允许修改业务代码:否 +- 是否允许升级或安装依赖:否 +- 是否允许顺手修复发现的问题:否 + +--- + +## 2. 任务目标 + +对 MapLibreParticleLab 当前真实代码、依赖、原生工程、MapLibre 接入方式和渲染架构进行一次完整调查。 + +本阶段的目的不是设计最终粒子系统,而是回答: + +> 当前项目实际上是怎样运行的,MapLibre 实际经过哪些层完成地图渲染,未来粒子系统有哪些真实可用的接入位置,以及当前仍有哪些关键问题无法仅通过现有代码确认。 + +最终必须形成一份基于实际仓库源码和配置的中文架构调查报告,为下一阶段 `20_架构设计` 提供事实基础。 + +--- + +## 3. 核心原则 + +### 3.1 以实际代码为唯一事实来源 + +不得根据: + +- 记忆 +- MapLibre 一般架构 +- React Native 常见做法 +- 网上其他项目的实现 +- 对新版 MapLibre 的印象 + +推断当前项目架构。 + +所有结论必须优先来自当前 `particle/main` 实际源码、配置、依赖锁定文件和原生工程。 + +如果某项无法通过仓库确认,必须明确标记为: + +`未确认` + +不得自行补全。 + +### 3.2 本任务只调查,不设计 + +本任务不得直接决定: + +- 最终粒子渲染技术路线 +- CPU 粒子还是 GPU 粒子 +- CustomLayer 是否作为最终方案 +- 是否修改 MapLibre Native +- 是否自行创建原生 Renderer +- 是否使用 Metal / OpenGL / Vulkan +- 最终数据纹理格式 +- 最终 Shader 架构 +- 最终线程模型 + +可以列出候选扩展位置和现有限制,但不得提前作最终技术选型。 + +### 3.3 不预设多个调查任务 + +本次应尽可能一次完成整个 `10_架构调查`。 + +只有在调查后发现某个关键问题同时满足以下条件时,才建议追加新的调查任务: + +1. 无法依靠当前项目源码确认; +2. 该问题会直接影响下一阶段技术路线; +3. 需要额外读取第三方源码、编译验证、最小实验或平台专项验证。 + +不得为了形式完整主动拆出大量 `10-01 / 10-02 / 10-03` 子任务。 + +--- + +## 4. 开始前检查 + +开始调查后先确认以下信息: + +- [ ] 当前仓库确实为 MapLibreParticleLab +- [ ] 当前分支为 `particle/main` +- [ ] 读取当前 Git 状态 +- [ ] 确认工作区是否干净 +- [ ] 找到并完整阅读当前“总则 v1” +- [ ] 确认项目根目录结构 +- [ ] 确认主要源码目录 +- [ ] 确认 Android / iOS 原生目录是否存在 +- [ ] 确认依赖清单及 lock 文件 + +如果当前分支或代码状态与上述基线明显不符: + +不要修改仓库状态。 + +继续进行能够完成的只读调查,并在报告中明确记录实际状态。 + +--- + +## 5. 必做调查内容 + +### 5.1 项目整体架构 + +调查当前项目的真实目录与模块组织。 + +至少确认: + +- 项目入口 +- React Native / Expo / Bare RN 等实际工程形态 +- TypeScript / JavaScript 使用情况 +- Android 工程 +- iOS 工程 +- 公共源码目录 +- 地图相关源码目录 +- 实验代码目录 +- 数据处理目录 +- Shader 或图形相关资源目录 +- assets +- scripts +- docs +- 测试目录 +- 构建配置 +- Metro 配置 +- Babel 配置 +- TypeScript 配置 + +报告中需要给出简化的当前项目结构图。 + +只列架构相关目录,不需要机械复制整个文件树。 + +### 5.2 当前依赖与版本 + +读取实际: + +- `package.json` +- lock 文件 +- Android Gradle 相关配置 +- iOS Pod 相关配置 +- 与地图和图形渲染相关的依赖配置 + +重点确认: + +- React Native 版本 +- React 版本 +- MapLibre React Native 包及实际版本 +- MapLibre Android / iOS Native 版本,如能够从当前工程确认 +- Skia 或其他图形库是否存在 +- Reanimated 等与渲染相关的辅助依赖 +- OpenGL / Metal / WebGL / GPU 相关第三方库 +- Native Module / TurboModule / Fabric 相关配置 + +必须区分: + +1. 直接依赖 +2. 间接依赖 +3. 原生 MapLibre 底层实际版本 + +如果底层版本无法确认,明确写“未确认”。 + +--- + +## 6. MapLibre 当前接入链调查 + +这是本任务重点。 + +必须从项目实际地图页面或地图 Host 开始向下追踪。 + +调查: + +`业务/页面` +↓ +`React Native Map 组件` +↓ +`MapLibre React Native 封装` +↓ +`Android / iOS Native View` +↓ +`MapLibre Native` + +需要确认当前代码实际使用的: + +- MapView +- Camera +- Style +- Source +- Layer +- ShapeSource +- RasterSource +- ImageSource +- CustomLayer,如存在 +- 地图事件 +- 地图生命周期事件 +- onPress +- camera change +- style load +- render/frame 相关事件 + +报告中必须给出一张文字形式的实际调用关系图。 + +例如: + +`Screen → MapHost → MapView → RN MapLibre Binding → Native Map View` + +但最终结构必须根据当前代码得出,不得照抄该例子。 + +--- + +## 7. 地图生命周期调查 + +调查当前代码能够观察或控制哪些生命周期阶段。 + +至少关注: + +- 地图组件创建 +- Native Map 创建 +- Style 加载开始 +- Style Ready +- 地图 Ready +- Camera 变化 +- 地图移动 +- 地图缩放 +- 地图旋转 +- 每帧刷新能力 +- App 前后台 +- MapView unmount +- Native 资源释放 + +明确: + +### 当前 TS/RN 层已经直接获得哪些生命周期事件 + +以及: + +### 哪些生命周期只存在 Native 或 MapLibre 内部,目前 JS 层拿不到 + +--- + +## 8. 自定义渲染扩展点调查 + +调查当前工程和当前依赖版本实际存在的自定义渲染能力。 + +重点寻找: + +- CustomLayer +- CustomLayerHost +- Native Layer +- Render Layer +- MapLibre 自定义 layer 接口 +- 自定义 View Overlay +- Surface +- Texture +- GL Context +- Metal Context +- Renderer hook +- Render pass +- Native callback +- Frame callback + +每个发现必须分类。 + +### A. 当前项目已经直接使用 + +### B. 当前依赖已经暴露,但项目尚未使用 + +### C. MapLibre Native 可能支持,但 RN 封装当前未暴露 + +### D. 当前代码无法确认 + +不要把 B/C/D 写成“当前已经支持”。 + +--- + +## 9. Android 渲染架构调查 + +单独调查 Android。 + +至少确认: + +- MapLibre Android Native 如何被当前 RN 包引入 +- MapView 对应原生 View +- renderer 所处代码边界 +- 当前工程是否显式涉及 OpenGL +- 是否能够找到 render thread 相关实现或接口 +- 地图帧刷新由谁驱动 +- Camera / transform 数据当前在哪一层 +- Native Module 是否能够接触 MapView 实例 +- 是否存在可插入自定义渲染的明确入口 +- JS 与 Native 的当前桥接方式 + +如果当前项目使用 New Architecture,也需要说明其实际启用状态。 + +--- + +## 10. iOS 渲染架构调查 + +Android 与 iOS 不得合并推断。 + +单独确认: + +- iOS MapLibre Native 依赖 +- MapView 原生类型 +- Metal / OpenGL 实际情况 +- renderer 生命周期 +- Camera / transform 数据 +- RN 到 Native 的连接方式 +- 是否能够确认自定义渲染接口 +- 当前 Native 层能够访问的 MapLibre 对象范围 + +如果 Android 与 iOS 能力不一致,必须明确列出。 + +--- + +## 11. JS / UI / Native / Render / GPU 边界 + +基于当前工程实际情况,整理线程和执行层级。 + +至少区分: + +- JavaScript Thread +- React Native UI/Main Thread +- Android/iOS Native 层 +- MapLibre 内部 Worker +- MapLibre Render Thread +- GPU + +如果某个平台实际名称有所不同,使用项目和 MapLibre 当前版本对应的真实名称。 + +重点调查: + +- 当前高频数据从 JS 传 Native 会经过什么链路 +- 地图 Camera 高频变化如何传播 +- 是否存在 JS 每帧回调 +- 是否存在 Native 每帧回调 +- 是否存在直接 GPU 资源更新的位置 +- 哪些数据如果每帧跨 JS Bridge 会形成明显风险 + +这里只描述架构事实和风险,不提前设计最终优化方案。 + +--- + +## 12. Camera 与坐标变换能力 + +粒子系统未来必须与地图同步,因此重点调查当前代码能够获得哪些: + +- center +- zoom +- bearing +- pitch +- bounds +- viewport size +- screen coordinate +- geographic coordinate +- projection / unprojection +- camera matrix +- projection matrix +- view matrix +- transform matrix + +区分: + +1. JS API 可以直接获取 +2. Native API 可以获取 +3. MapLibre 内部存在但当前封装未暴露 +4. 无法确认 + +尤其要明确: + +> 当前是否可以获得足够信息,使自定义粒子渲染与 MapLibre 地图的平移、缩放、旋转、pitch 保持严格同步。 + +如果不能确认,不得猜测。 + +--- + +## 13. 帧驱动能力 + +调查未来粒子动画最关键的问题: + +> 谁负责下一帧? + +确认当前可利用的: + +- requestAnimationFrame +- RN animation frame +- Reanimated frame callback +- Native Choreographer / DisplayLink +- MapLibre render loop +- CustomLayer render callback +- GPU render callback + +需要说明: + +- 哪些帧驱动来自地图 Renderer 本身 +- 哪些与地图 Renderer 独立 +- 如果使用独立动画循环,理论上可能出现什么同步问题 + +这里只进行架构分析。 + +--- + +## 14. 当前数据与实验代码调查 + +搜索当前仓库内已经存在的: + +- particle +- wind +- current +- wave +- grid +- vector field +- raster +- interpolation +- texture +- shader +- velocity +- animation +- GPU +- GL +- Metal +- MapLibre custom rendering + +以及相关实验代码。 + +对发现的代码进行分类: + +- 可复用基础代码 +- 仅实验用途 +- 与最终架构可能无关 +- 已弃用 +- 状态不明确 + +不得因为文件名相似就判定“可复用”。 + +需要查看真实实现。 + +--- + +## 15. 海洋/气象格点数据未来可能经过的现有层 + +本任务不设计数据系统,只调查当前项目有哪些已经存在的基础设施。 + +关注未来可能涉及: + +`原始格点数据` +→ `解析` +→ `内存结构` +→ `插值` +→ `渲染资源` +→ `地图显示` + +调查当前是否已经存在: + +- 数据 Fetch 层 +- ArrayBuffer +- TypedArray +- 二进制解码 +- Grid 数据模型 +- 经纬度格点模型 +- 缓存 +- Worker +- Native Buffer +- Texture Upload +- Shader Uniform +- 数据生命周期管理 + +报告只描述现状。 + +--- + +## 16. 性能风险调查 + +不得运行大型性能优化工作。 + +仅根据真实架构识别潜在风险。 + +至少关注: + +- JS 高频循环 +- 每帧 JS → Native 调用 +- 大数组 Bridge 传输 +- JSON 序列化 +- ArrayBuffer 复制 +- Native → GPU 重复复制 +- Texture 高频重建 +- 地图 Camera 变化导致的大规模重新计算 +- React re-render +- MapView Overlay 与地图不同步 +- Android/iOS 实现分叉 +- GPU context 共享风险 +- 地图销毁后的资源泄漏 + +风险需要标记等级: + +- 高 +- 中 +- 低 +- 尚无法判断 + +--- + +## 17. 当前架构图 + +最终调查报告必须至少包含以下几个简化架构图。 + +全部使用 Markdown 文本图即可。 + +### 17.1 当前 App / MapLibre 总体调用图 + +### 17.2 JS → Native → MapLibre 调用链 + +### 17.3 Android 地图渲染相关层级 + +### 17.4 iOS 地图渲染相关层级 + +### 17.5 当前能够确认的数据与渲染边界 + +不得为了图看起来完整而补画未经确认的组件。 + +未知位置使用: + +`[未确认]` + +--- + +## 18. 关键结论分类 + +调查报告结尾必须将全部关键结论整理成四类。 + +### 18.1 已确认事实 + +只能放已有源码或配置证据的事实。 + +### 18.2 当前限制 + +当前架构真实存在的限制。 + +### 18.3 候选扩展点 + +当前看来可能承载未来粒子系统的位置。 + +这里只列候选,不做最终选择。 + +### 18.4 未确认问题 + +列出不能通过本次仓库静态调查确认,但会影响后续架构设计的问题。 + +每个“未确认问题”注明: + +- 为什么当前无法确认 +- 是否会阻塞 `20_架构设计` +- 如果需要继续调查,建议采用什么最小方式确认 + +--- + +## 19. 是否需要追加调查任务的判断 + +本次调查结束时必须明确给出: + +### A. 可以直接进入 `20_架构设计` + +或: + +### B. 必须追加一个新的 `10_架构调查` 子任务 + +只有关键技术事实无法确认并且会直接影响架构选择时才能选择 B。 + +如果选择 B,只提出真正需要的最少调查任务。 + +不得为了保险而把所有未知问题拆成新任务。 + +--- + +## 20. 禁止事项 + +本任务禁止: + +- 修改生产源码 +- 修改实验源码 +- 新建粒子实现 +- 修改 MapLibre 接入 +- 升级 MapLibre +- 升级 React Native +- 安装任何新 npm 包 +- 安装任何新 Pod +- 修改 Gradle 依赖 +- 修改 Podfile +- 修改 Metro +- 修改 Babel +- 修改 TypeScript 配置 +- 修改 Android 原生代码 +- 修改 iOS 原生代码 +- 创建 Shader 实现 +- 创建 CustomLayer 实现 +- 创建 Native Module +- 进行架构重构 +- 顺手修复发现的问题 + +发现问题只记录。 + +--- + +## 21. 允许执行的验证 + +允许执行不会改变项目状态的只读命令,例如: + +- Git 状态查询 +- Git 分支查询 +- 文件搜索 +- 文本搜索 +- 查看 package 配置 +- 查看 Gradle 配置 +- 查看 Pod 配置 +- 查看源码 +- 查看当前项目已有脚本定义 + +如果某个命令可能: + +- 自动安装依赖 +- 修改 lock 文件 +- 修改构建产物 +- 修改 Native 工程 +- 写入项目配置 + +则不要执行。 + +本任务主要依赖静态调查。 + +--- + +## 22. 必做输出 + +### 22.1 架构调查成果 + +必须生成中文调查报告: + +`docs/20_调查/10_架构调查报告_v1.md` + +如果同名文件已经存在,不得覆盖,应递增版本号。 + +该报告是后续 `20_架构设计` 的主要输入。 + +### 22.2 Codex 完成报告 + +必须生成中文执行完成报告: + +`docs/报告/10_架构调查_Codex完成报告_v1.md` + +如果 `docs/报告/` 不存在,则创建该目录。 + +如果同名报告已经存在,不得覆盖旧报告,应递增版本号。 + +--- + +## 23. 架构调查报告必须包含 + +`docs/20_调查/10_架构调查报告_v1.md` 至少包含: + +1. 调查环境 +2. Git / 分支 / 工作区状态 +3. 总则 v1 读取情况 +4. 当前项目整体架构 +5. 核心依赖与版本 +6. MapLibre 接入链 +7. MapLibre 生命周期 +8. 自定义渲染能力 +9. Android 渲染架构 +10. iOS 渲染架构 +11. JS / Native / Render / GPU 边界 +12. Camera / Matrix / 坐标转换能力 +13. 帧驱动能力 +14. 当前数据与粒子相关实验代码 +15. 当前可复用基础设施 +16. 性能与线程风险 +17. 当前架构图 +18. 已确认事实 +19. 当前限制 +20. 候选扩展点 +21. 未确认问题 +22. 是否需要追加调查任务 +23. 是否已经具备进入 `20_架构设计` 的条件 + +--- + +## 24. Codex 完成报告必须包含 + +`docs/报告/10_架构调查_Codex完成报告_v1.md` 至少包含: + +1. 本次实际调查范围 +2. 必做项逐项状态:`已完成 / 未完成 / 无法验证` +3. 未完成或无法验证的原因 +4. 实际生成或修改的文档清单 +5. 是否修改生产代码 +6. 是否安装或升级依赖 +7. 是否存在需要用户关注的阻塞项 +8. 下一步建议 + +--- + +## 25. 必做项完成检查 + +完成前逐项检查: + +- [ ] 已读取总则 v1 +- [ ] 已确认当前实际 Git 基线 +- [ ] 已调查整体目录和工程形态 +- [ ] 已确认核心依赖版本 +- [ ] 已追踪 MapLibre 实际调用链 +- [ ] 已调查地图生命周期 +- [ ] 已调查自定义渲染扩展点 +- [ ] 已分别调查 Android +- [ ] 已分别调查 iOS +- [ ] 已调查线程与执行边界 +- [ ] 已调查 Camera / 坐标 / Matrix 能力 +- [ ] 已调查帧驱动能力 +- [ ] 已搜索现有粒子/场数据/Shader 实验代码 +- [ ] 已调查当前可复用的数据基础设施 +- [ ] 已整理性能风险 +- [ ] 已绘制必要架构图 +- [ ] 已区分事实、限制、候选和未知项 +- [ ] 已判断是否需要追加调查 +- [ ] 已判断能否进入 `20_架构设计` +- [ ] 未修改生产代码 +- [ ] 未升级或安装依赖 +- [ ] 已生成 `docs/20_调查/` 下的正式调查报告 +- [ ] 已生成 `docs/报告/` 下的 Codex 完成报告 + +不得静默跳过任何必做项。 + +无法完成的项目必须明确写: + +`未完成` + +或: + +`无法验证` + +并说明原因。 + +--- + +## 26. 完成标准 + +本任务完成的标准不是“找到一个粒子方案”。 + +真正完成标准是: + +> 已经能够基于 MapLibreParticleLab 当前真实代码,准确描述项目、React Native、Native、MapLibre 和 GPU 之间的现有边界,并明确下一阶段进行粒子系统架构设计时,哪些能力已经确认、哪些扩展位置值得考虑、哪些关键事实仍需要额外验证。 + +达到这个状态后,`10_架构调查` 才算完成。 + +除非存在真正阻塞架构选择的未知项,否则下一步直接进入: + +`20_架构设计` diff --git a/docs/40_任务/20_架构设计指令书_v1.md b/docs/40_任务/20_架构设计指令书_v1.md new file mode 100644 index 000000000..a3ce374f7 --- /dev/null +++ b/docs/40_任务/20_架构设计指令书_v1.md @@ -0,0 +1,1092 @@ +# MapLibreParticleLab 20_架构设计指令书 v1 + +## 1. 任务基本信息 + +- 项目:MapLibreParticleLab +- 大阶段:`20_架构设计` +- 当前基线分支:`particle/main` +- 前置阶段:`10_架构调查` 已完成 +- 前置调查报告:`docs/20_调查/10_架构调查报告_v1.md` +- 前置完成报告:`docs/报告/10_架构调查_Codex完成报告_v1.md` +- 任务性质:架构设计 +- 是否允许修改生产代码:否 +- 是否允许实现粒子系统:否 +- 是否允许安装或升级依赖:否 +- 是否允许运行会修改工程状态的构建/测试:否 + +--- + +## 2. 任务目标 + +基于 `10_架构调查` 已确认的真实仓库结构、MapLibre Core / Android / iOS 渲染链、CustomLayer / CustomDrawable / gfx 扩展能力、Camera/Matrix、帧驱动和生命周期事实,完成 MapLibreParticleLab 第一版正式粒子系统总体架构设计。 + +本阶段必须完成的是: + +> 从已经确认的候选扩展点中做出明确架构选择,冻结粒子系统的模块边界、数据边界、渲染边界、线程边界、平台边界、生命周期、Mask 机制和第一阶段离线 fixture 接口,并明确哪些内容必须进入后续最小技术验证。 + +本阶段不是实现阶段。 + +不得直接创建粒子 Shader、Renderer、Source、CustomLayerHost 实现或平台业务代码。 + +--- + +## 3. 必须先读取的现有文档 + +开始设计前必须完整读取并遵守: + +1. 根目录 `AGENTS.md` +2. `PARTICLE_BASELINE.md` +3. `docs/00_项目总则/` 当前全部有效总则 +4. `docs/00_项目规范/` 当前全部有效规范 +5. `docs/20_调查/10_架构调查报告_v1.md` +6. `docs/报告/10_架构调查_Codex完成报告_v1.md` +7. `docs/10_架构/` 下当前已有架构文档 +8. `粒子开发记录.md` + +如果现有架构文档与本次真实调查结果有冲突: + +- 不得静默覆盖; +- 必须在设计文档中明确指出冲突; +- 以当前有效总则、规范和 `10_架构调查报告_v1` 中的已确认事实作为设计输入; +- 旧文档仅作为历史参考。 + +--- + +## 4. 已冻结的调查事实 + +本任务不得重新把下列已确认事实当成“未知问题”反复调查。 + +### 4.1 仓库形态 + +当前仓库是 MapLibre Native monorepo,不是 React Native / Expo / Bare RN App。 + +当前不存在: + +- RN 页面 +- RN MapLibre binding +- Metro +- Babel +- NativeModule / TurboModule / Fabric 粒子宿主 +- JS 每帧粒子运行时 + +因此当前架构设计必须先以 Native Core + Android/iOS 平台薄接入为事实基础。 + +### 4.2 Core 主渲染链 + +当前已确认主链: + +`mbgl::Map` +→ `RendererFrontend` +→ `UpdateParameters` +→ `Renderer` +→ `RenderTree / RenderOrchestrator` +→ `RenderLayer / LayerGroup / Drawable` +→ `gfx` +→ 平台 GPU backend + +### 4.3 当前候选扩展点 + +已确认候选包括: + +1. Core `CustomLayerHost / RenderCustomLayer` +2. Core `CustomDrawableLayer` +3. 独立 Core particle render layer +4. 现有 Source / FileSource / tile / fixture 管线的薄接入 +5. Android/iOS 仅负责必要的 backend 与生命周期薄适配 + +本任务必须在这些真实候选基础上做架构选择。 + +### 4.4 平台差异 + +- Android 当前存在 OpenGL / Vulkan backend。 +- iOS 当前存在 OpenGL / Metal backend。 +- 两个平台共享 Core,但高层 CustomLayer、context、encoder、surface 生命周期不完全对称。 +- Android raw `CustomLayerHost` 业务侧稳定用法尚未经过运行时验证。 +- iOS 最终目标构建采用 OpenGL 还是 Metal,需要以后结合目标工程确认。 + +### 4.5 当前粒子实现状态 + +当前仓库没有: + +- 正式粒子运行时 +- 速度场解析器 +- 粒子 shader +- GPU ping-pong particle state +- trail render target +- 正式 Grid 数据模型 +- 正式粒子数据 contract + +`particle-fixtures/` 当前只有占位目录。 + +--- + +## 5. 架构设计原则 + +### 5.1 粒子系统必须高度隔离 + +不得把粒子逻辑散落到: + +- RasterLayer +- 普通 Style Layer +- Map 主循环 +- Camera +- Source 通用逻辑 +- Android MapView +- iOS MLNMapView + +粒子系统必须形成清晰、可独立关闭、可独立测试、可独立演进的模块边界。 + +### 5.2 不复制外部闭源或其他产品实现 + +不得复制: + +- Windy APK 代码 +- Mapbox 新版闭源实现 +- 其他商业产品的反编译实现 + +允许参考当前仓库已有 MapLibre 结构和公开 API 形成自己的架构。 + +### 5.3 跨平台共用优先 + +尽量把: + +- 粒子状态 +- 场数据 +- 坐标转换 +- 粒子更新 +- 渲染资源描述 +- Shader contract +- Mask contract +- 生命周期状态机 + +放在 Core 层。 + +Android / iOS 只保留不可避免的: + +- backend resource 接入 +- surface/context/device 生命周期 +- 平台 render callback 连接 +- 平台能力探测 + +不得分别实现两套粒子系统。 + +### 5.4 设计必须支持 backend 差异 + +第一版架构不得假定所有平台永远使用同一种 GPU API。 + +设计必须至少能表达: + +- Android OpenGL +- Android Vulkan +- iOS OpenGL +- iOS Metal + +但本阶段不要求全部 backend 同时实现。 + +必须区分: + +- 架构必须支持 +- 第一阶段实际验证 +- 后续扩展 + +--- + +## 6. 必须完成的核心架构决策 + +本任务最重要的输出不是列候选,而是明确选择。 + +必须对以下问题逐项给出: + +- 推荐方案 +- 为什么 +- 为什么不选其他候选 +- 风险 +- 后续验证点 + +### 6.1 粒子渲染核心入口 + +必须在以下候选中做主路线选择: + +- `CustomLayerHost / RenderCustomLayer` +- `CustomDrawableLayer` +- 新建独立 Core particle render layer +- 其他基于当前源码能够成立的方案 + +不得只写“都可以”。 + +必须明确: + +> 第一阶段正式主路线是什么。 + +如果采用“两阶段方案”,例如先 CustomLayer 验证、后独立 RenderLayer 正式化,也必须说明每阶段退出条件,不能无限双轨。 + +### 6.2 与现有 MapLibre RenderTree 的关系 + +设计必须说明粒子模块: + +- 是否进入 Style / Layer 系统 +- 是否进入 RenderTree +- 是否属于 translucent pass +- 是否需要自己的 render target +- 是否需要 trail accumulation +- 是否参与 MapLibre repaint +- 是否需要主动 `triggerRepaint` +- Camera 更新时如何取得同帧 Transform + +### 6.3 gfx 层使用边界 + +明确粒子系统可使用哪些现有 gfx 能力: + +- Texture2D +- DynamicTexture +- Buffer +- UniformBuffer +- RenderTarget +- DrawableBuilder +- DrawableTweaker +- Shader registry +- UploadPass +- CommandEncoder + +并明确哪些需要新增粒子专用抽象。 + +不得直接把 backend-specific OpenGL/Vulkan/Metal 调用散到公共粒子业务逻辑。 + +--- + +## 7. 模块划分 + +必须给出明确的模块图和建议源码目录。 + +至少考虑以下职责,名称可根据现有 MapLibre 目录风格调整: + +### 7.1 Particle Field + +职责: + +- 描述风、浪、潮、海流等矢量场 +- 范围 +- 网格 +- 时间片 +- U/V 分量 +- NoData +- 有效区 +- 数据版本 + +### 7.2 Particle Data Adapter + +职责: + +- 从第一阶段 fixture 转为统一 Particle Field +- 后续可替换为真实数据源 +- 不把真实天气 Server 协议耦合到 Renderer + +### 7.3 Particle Simulation + +职责: + +- 粒子状态 +- age +- seed +- advection +- respawn +- field sample +- timestep + +必须明确哪些在 CPU,哪些预期在 GPU。 + +### 7.4 Particle Render + +职责: + +- 粒子绘制 +- trail +- fade +- opacity +- viewport +- transform +- render pass + +### 7.5 Particle GPU Resources + +职责: + +- velocity texture +- particle state +- trail target +- uniforms +- shader +- context loss / recreate + +### 7.6 Particle Mask + +职责: + +- GLOBAL +- OCEAN_ONLY +- Land/Ocean Mask +- mask texture / geometry contract +- mask lifecycle + +### 7.7 Particle Controller / Layer State + +职责: + +- enable/disable +- type +- data ready +- GPU ready +- map ready +- context lost +- renderable state +- repaint request + +不得让 UI 直接管理 GPU resource。 + +--- + +## 8. WeatherColorLayer 与 RasterParticleLayer 边界 + +必须明确区分: + +### WeatherColorLayer + +用于: + +- 颜色场 +- 栅格填色 +- 连续色带 +- 不承担粒子动画 + +### RasterParticleLayer + +用于: + +- 风向/海流等动态粒子表现 +- 粒子状态 +- 动画 +- trail +- GPU simulation/render + +两者可以共享: + +- 数据来源 +- grid metadata +- mask +- time slice + +但不得共享: + +- RenderLayer 实例 +- 粒子 GPU 状态 +- 动画生命周期 + +设计文档必须明确二者不是一个 Layer 的两种开关模式。 + +--- + +## 9. Wind / Wave / Tide / Current 显示域 + +当前规则必须进入正式架构: + +### Wind + +`GLOBAL` + +允许覆盖: + +- 海洋 +- 陆地 + +### Wave / Tide / Current + +`OCEAN_ONLY` + +只显示海洋部分。 + +不能要求每种海洋数据各自实现一套陆地裁剪。 + +必须设计统一可复用: + +`Land/Ocean Mask` + +机制。 + +--- + +## 10. Land/Ocean Mask 架构 + +必须给出独立设计。 + +至少明确: + +- Mask 数据来源属于哪个模块 +- Mask 是 CPU geometry 还是 GPU texture,或允许两级实现 +- Mask 坐标系 +- Mask 分辨率 +- viewport / tile / world 范围 +- zoom 时的行为 +- wrap / antimeridian +- pitch / bearing 下如何与地图保持一致 +- Mask 更新条件 +- Mask 缓存 +- 海岸边界 artifact 风险 +- context loss 后如何恢复 + +Mask 必须可以被: + +- Wave +- Tide +- Current +- 未来其他 OCEAN_ONLY 场 + +共享。 + +不得把 Mask 写死到某一种粒子 Layer 内。 + +--- + +## 11. 第一阶段离线 fixture contract + +第一阶段仍然只使用离线 fixture。 + +架构设计必须冻结一个“最小 fixture contract”。 + +至少定义: + +- fixture id +- field type +- bounds +- grid width / height +- coordinate reference +- U/V 分量 +- NoData +- timestamp 或 timeIndex +- data version +- mask 信息 +- endian / precision,如涉及二进制 +- metadata 文件 +- binary/data 文件 + +必须覆盖现有预留 fixture: + +- `uniform-east` +- `uniform-north` +- `vortex` +- `shear` +- `coastal-mask` + +本阶段可以设计格式,但不得生成真实天气数据下载器。 + +--- + +## 12. 数据流设计 + +必须画出完整数据链: + +```text +Offline Fixture +→ Decode / Validate +→ Particle Field +→ CPU/GPU Resource Preparation +→ Velocity Resource +→ Simulation +→ Render +→ MapLibre Render Pass +→ GPU +``` + +同时说明: + +### 哪些步骤低频 + +例如: + +- fixture load +- data decode +- texture upload + +### 哪些步骤每帧 + +例如: + +- particle simulation +- trail update +- draw + +### 哪些步骤 Camera 变化时更新 + +例如: + +- transform uniform +- viewport +- visible bounds + +不得让每帧流程重新解析 field 或重建 texture。 + +--- + +## 13. 线程模型设计 + +必须明确: + +- Map / UI thread +- worker thread +- render thread +- GPU + +各自负责什么。 + +至少回答: + +### Field 解码在哪执行 + +### CPU 插值在哪执行 + +### GPU resource 创建在哪执行 + +### 每帧 Simulation 在哪里执行 + +### Camera Matrix 从哪里读取 + +### Data 更新如何安全交给 Render Thread + +### 如何避免大数组频繁跨线程复制 + +### immutable snapshot 或 resource handle 如何使用 + +### context loss 时哪个线程负责恢复 + +--- + +## 14. Camera / Matrix 同步设计 + +必须基于当前调查确认的: + +- `TransformState` +- `TransformParameters` +- `PaintParameters` +- `CustomLayerRenderParameters` + +设计粒子同帧同步方式。 + +至少覆盖: + +- pan +- zoom +- bearing +- pitch +- resize +- pixel ratio +- world wrap +- antimeridian +- projection/unprojection + +必须明确: + +> 粒子 Renderer 读取的是 MapLibre 当前 render frame 的 transform,而不是由另一个独立 UI/JS 动画循环猜 Camera。 + +--- + +## 15. 帧驱动与动画时钟 + +必须正式定义谁驱动粒子动画。 + +至少说明: + +- 是否依赖 MapLibre render loop +- 粒子活动时如何持续请求下一帧 +- 粒子关闭时如何停止额外 repaint +- 地图静止但粒子活动时如何继续帧 +- App 后台如何暂停 +- View 不可见如何暂停 +- Map/Style 销毁如何停止 +- context loss 如何停止并恢复 + +不得设计独立于 MapLibre Renderer 的第二套无同步动画循环作为正式主路线。 + +--- + +## 16. GPU 粒子状态模型 + +本阶段必须设计,但不实现。 + +至少定义: + +- particle count +- position state +- age +- seed/random +- current / next state +- velocity sampling +- timestep +- respawn +- viewport clipping +- field bounds +- NoData 处理 + +如果采用 GPU simulation,需要明确: + +- ping-pong resource +- compute 或 render-pass simulation +- backend capability gate +- 无 compute 时的 fallback 思路 + +如果第一阶段不使用 compute,也必须明确原因。 + +--- + +## 17. Trail 架构 + +必须说明是否第一阶段就包含 trail。 + +如果包含: + +至少设计: + +- accumulation target +- fade pass +- particle draw pass +- compositing +- resize +- camera movement +- zoom/bearing/pitch 改变时旧 trail 如何处理 +- context loss +- clear conditions + +如果第一阶段不包含 trail: + +必须明确第一阶段视觉范围,并说明后续如何扩展。 + +--- + +## 18. 生命周期状态机 + +必须设计粒子 Layer/Renderer 状态机。 + +建议至少包含: + +- DISABLED +- WAITING_MAP +- WAITING_FIELD +- PREPARING +- READY +- RENDERING +- PAUSED +- CONTEXT_LOST +- ERROR + +可以调整名称,但必须覆盖: + +- Map 创建 +- Style reload +- 数据更新 +- App background +- App foreground +- Surface recreation +- GPU context loss +- Layer removal +- Map destroy + +必须明确资源释放和重新创建责任。 + +--- + +## 19. Android / iOS 薄接入 + +### 19.1 Android + +架构必须说明: + +- Java/Kotlin 业务 API 应保持多薄 +- JNI 是否需要新增稳定 wrapper +- raw `long CustomLayerHost` 是否允许进入正式接口 +- OpenGL / Vulkan backend 差异如何封装 +- SurfaceView / TextureView 生命周期影响 +- requestRender 如何连接 + +不得把 Android 平台代码变成第二套 particle engine。 + +### 19.2 iOS + +必须说明: + +- `MLNCustomStyleLayer` +- `MLNCustomDrawableStyleLayer` +- Metal / OpenGL resource +- CADisplayLink / MapLibre render callback +- backend resource + +在正式架构中的位置。 + +不得因为 iOS 高层 CustomStyleLayer 比 Android 方便,就把 Core 共用逻辑放到 ObjC/Swift。 + +--- + +## 20. 公共 API 设计 + +本阶段需要设计未来供宿主使用的最小 API,但不实现。 + +例如需要能够表达: + +- 创建/删除粒子 layer +- enable/disable +- 设置 field +- 设置 display domain +- 设置 particle options +- 设置 mask +- 数据更新 +- 状态查询 + +必须遵循 MapLibre 当前 API 风格。 + +公共 API 不得暴露: + +- GL texture id +- VkImage +- MTLTexture +- backend-specific command encoder +- raw native host pointer + +除非是严格内部接口。 + +--- + +## 21. 配置与可调参数 + +架构设计应区分: + +### 稳定业务参数 + +例如: + +- particle count +- speed factor +- lifetime +- opacity +- trail fade + +### 内部 backend 参数 + +例如: + +- texture format +- buffer alignment +- workgroup size +- render target format + +业务层不得直接依赖 backend 参数。 + +--- + +## 22. 错误与降级策略 + +设计必须定义: + +- field 无效 +- field 越界 +- mask 无效 +- GPU resource 创建失败 +- backend 不支持 +- context loss +- shader 创建失败 +- memory pressure + +发生时的行为。 + +原则: + +- 地图本身不得因此崩溃 +- 粒子层可以禁用或进入 ERROR +- 不得破坏其他 Style Layer +- 资源失败后必须可安全释放 +- 是否自动重试必须明确 + +--- + +## 23. 性能预算 + +本阶段不要求真实性能测试,但必须建立设计预算。 + +至少定义需要后续验证的指标: + +- FPS +- frame time +- CPU time +- GPU time +- particle count +- texture memory +- buffer memory +- field upload time +- context recreation time + +并明确: + +- 哪些属于第一阶段 gate +- 哪些属于后续优化 + +不得写未经测试的“能稳定 60 FPS”等结论。 + +--- + +## 24. 后续最小技术验证清单 + +架构设计完成后,必须给出“最少数量”的技术验证。 + +只列会影响实现路线的关键验证。 + +可能包括但不限于: + +- Android CustomLayerHost 稳定生命周期 +- Android OpenGL/Vulkan 中粒子资源能否共用 MapLibre render context +- iOS Metal / OpenGL CustomStyleLayer 同帧资源接入 +- context loss / Surface recreate +- MapLibre transform 下的同帧矩阵同步 +- trail render target +- OCEAN_ONLY mask + +不得为了保险列几十个验证任务。 + +必须按: + +1. 是否阻塞正式实现 +2. 是否会改变架构 +3. 最小验证方式 + +进行排序。 + +--- + +## 25. 必须明确拒绝的架构 + +设计报告必须专门列出“不采用的方案”。 + +至少评估: + +- React Native / JS 每帧粒子 +- MapView 外部 Overlay +- 每种海洋场各自一套 Mask +- 直接改造现有 RasterLayer 承担粒子 +- Android/iOS 各写一套独立粒子引擎 +- 每帧重新创建 field texture +- 每帧大数组跨 Java/ObjC/JS 边界 +- backend-specific GPU API 暴露到公共业务 API + +说明拒绝原因。 + +--- + +## 26. 必须生成的架构图 + +正式设计文档至少包含: + +### 26.1 粒子系统总体架构图 + +### 26.2 Core 模块边界图 + +### 26.3 Offline Fixture → GPU 数据流图 + +### 26.4 每帧渲染链图 + +### 26.5 Camera / Matrix 同步图 + +### 26.6 Android 薄接入图 + +### 26.7 iOS 薄接入图 + +### 26.8 Land/Ocean Mask 共用图 + +### 26.9 生命周期状态机图 + +全部使用 Markdown / Mermaid / ASCII 均可。 + +图中不得补入当前不存在且未经设计定义的外部系统。 + +--- + +## 27. 正式设计结论必须冻结的内容 + +设计报告结尾必须列出“架构冻结项”。 + +至少包括: + +- 主粒子渲染路线 +- Core / Platform 分工 +- Layer 边界 +- WeatherColorLayer / RasterParticleLayer 边界 +- Field 模型 +- Fixture contract +- Wind GLOBAL +- Wave/Tide/Current OCEAN_ONLY +- Land/Ocean Mask +- Frame Driver +- Camera Matrix 来源 +- GPU resource 生命周期 +- Android/iOS 薄接入原则 +- 第一阶段 backend 验证范围 +- 第一阶段粒子视觉范围 +- 后续最小技术验证项 + +冻结项是后续 `30_最小技术验证` 和实现任务的依据。 + +--- + +## 28. 不得在本阶段冻结的内容 + +以下如果没有运行时证据,不得假装已确定: + +- 最终 FPS +- 最终最大 particle count +- 最终最佳 texture format +- 最终 Vulkan/Metal compute workgroup +- 具体 GPU 厂商优化 +- 真实天气 Server 数据格式 +- NavSea / RN 最终 Host API +- 最终生产设备能力矩阵 + +这些只能记录为后续验证项。 + +--- + +## 29. 禁止事项 + +本任务禁止: + +- 修改生产源码 +- 创建粒子 Renderer 实现 +- 创建粒子 Shader +- 创建 GPU Buffer/Texture 实现 +- 创建 CustomLayerHost 实现 +- 创建 Android JNI 粒子模块 +- 创建 iOS 粒子类 +- 修改现有 MapLibre Renderer +- 修改 RasterLayer +- 修改 Style parser +- 安装/升级依赖 +- 改 Gradle +- 改 Pod +- 改 CMake +- 改 package lock +- 运行会写入工程状态的自动构建 +- 顺手修复无关问题 + +本任务只允许: + +- 阅读 +- 设计 +- 生成/更新架构文档 +- 生成 Codex 完成报告 +- 追加 `粒子开发记录.md` + +--- + +## 30. 必做输出 + +### 30.1 正式架构文档 + +生成: + +`docs/10_架构/20_粒子系统总体架构设计_v1.md` + +如果同名文件已经存在,不得覆盖,应递增版本号。 + +该文件是本阶段最主要的正式输出。 + +### 30.2 Codex 完成报告 + +生成: + +`docs/报告/20_架构设计_Codex完成报告_v1.md` + +如果同名文件已经存在,不得覆盖,应递增版本号。 + +### 30.3 开发记录 + +追加: + +`粒子开发记录.md` + +记录: + +- `20_架构设计` 已执行 +- 生成的架构文档 +- 核心架构选择 +- 是否需要进入最小技术验证 +- 仍未确认的关键事项 + +不得重写历史记录。 + +--- + +## 31. 不主动创建 Step + +本任务不主动新建或修改 `docs/50_Step记录/` 下的 Step 文件。 + +除非用户已有明确 Step 指令,否则只生成本任务规定的: + +- 架构文档 +- Codex 完成报告 +- 开发记录追加 + +--- + +## 32. Codex 完成报告格式 + +必须逐项报告: + +- `已完成` +- `未完成` +- `无法验证` + +至少包括: + +1. 读取前置文档 +2. 候选渲染路线比较 +3. 主路线选择 +4. Core 模块设计 +5. Data/Field contract +6. GPU resource 设计 +7. Mask 设计 +8. 线程模型 +9. Camera/Matrix 同步 +10. 帧驱动 +11. Android 薄接入 +12. iOS 薄接入 +13. 生命周期 +14. 错误/降级 +15. 性能预算 +16. 最小技术验证清单 +17. 正式架构冻结项 +18. 生成文件 +19. 是否修改生产代码 +20. 是否修改依赖或构建配置 + +--- + +## 33. 完成标准 + +本任务完成,不是指“列出几个可选方案”。 + +必须达到: + +> 已经基于当前 MapLibreParticleLab 的真实 MapLibre Native 架构,为粒子系统选择唯一主架构路线,并冻结 Core / Platform 边界、Particle Field、Simulation、Render、GPU Resource、Mask、Frame Driver、Camera Matrix、生命周期和第一阶段 fixture contract,使下一阶段能够直接针对少量关键未知项做最小技术验证,而不再重新讨论总体架构。 + +如果仍然存在两个以上彼此冲突的主路线且没有选择,则本任务不能标记为完成。 + +--- + +## 34. 下一阶段判定 + +设计结束时必须明确选择: + +### A. 直接进入实现 + +只有所有关键架构假设都已有运行时证据时才允许。 + +或: + +### B. 进入 `30_最小技术验证` + +如果仍存在会影响实现成败的 backend、CustomLayer、context、matrix、trail 或 Mask 等运行时未知项,应选择 B。 + +按照当前调查结果,预期大概率选择 B,但必须由本次设计结果最终判断。 diff --git a/docs/40_任务/30_01_CoreRenderLayer接入验证指令书_v1.md b/docs/40_任务/30_01_CoreRenderLayer接入验证指令书_v1.md new file mode 100644 index 000000000..9d590d13a --- /dev/null +++ b/docs/40_任务/30_01_CoreRenderLayer接入验证指令书_v1.md @@ -0,0 +1,731 @@ +# MapLibreParticleLab 30_最小技术验证-01_Core RenderLayer 接入验证指令书 v1 + +## 1. 任务基本信息 + +- 项目:MapLibreParticleLab +- 阶段:`30_最小技术验证` +- 子任务:`30-01 Core RenderLayer 接入验证` +- 当前主线:`particle/main` +- 前置阶段: + - `10_架构调查` 已完成 + - `20_架构设计` 已完成 +- 前置正式架构文档: + - `docs/10_架构/20_粒子系统总体架构设计_v1.md` +- 任务性质:最小运行时技术验证 +- 本任务不是正式粒子功能实现 +- 本任务允许最少量验证代码,但必须保持隔离、可识别、可移除 +- 不允许顺手重构 MapLibre +- 不允许扩大到 GPU 粒子、真实场数据、Trail 或 Land/Ocean Mask 实现 + +--- + +## 2. 为什么本阶段先只做 30-01 + +当前总体架构已经选择独立 Core 粒子 Layer 作为正式主路线。 + +本阶段后续原计划还需要验证: + +1. Core RenderLayer / Style order / 同帧 transform / repaint +2. GPU State A/B + Trail A/B ping-pong 与 context recreation +3. Camera / world wrap / Land-Ocean Mask +4. iOS Core Layer 薄接入 + +但第 1 项属于前置门槛。 + +如果第 1 项失败,说明当前正式主路线可能需要调整,后续 GPU、Mask 和平台验证的代码位置、生命周期和验证方式都会变化。 + +因此本任务只完成 `30-01`。 + +只有 `30-01` 得出 PASS 后,才进入下一项。 + +不得提前为后续验证大量写代码。 + +--- + +## 3. 必须先读取 + +Codex 开始前必须完整读取: + +1. 根目录 `AGENTS.md` +2. `PARTICLE_BASELINE.md` +3. `docs/00_项目总则/` 下当前全部有效文档 +4. `docs/00_项目规范/` 下当前全部有效文档 +5. `docs/20_调查/10_架构调查报告_v1.md` +6. `docs/10_架构/20_粒子系统总体架构设计_v1.md` +7. `docs/报告/10_架构调查_Codex完成报告_v1.md` +8. `docs/报告/20_架构设计_Codex完成报告_v1.md` +9. `粒子开发记录.md` +10. 与下列现有实现直接相关的当前源码: + - Style Layer 注册链 + - RenderLayer / RenderTree / RenderOrchestrator + - RasterLayer 或其他最接近的现有 Layer 示例 + - CustomLayer / CustomDrawableLayer 示例 + - Android MapRenderer / AndroidRendererFrontend + - `triggerRepaint` / `requestRender` + - `TransformState` / `TransformParameters` / `PaintParameters` + +不得凭记忆或 MapLibre 通用知识直接开写。 + +--- + +## 4. 开始前 Git 检查 + +先执行只读检查并记录: + +- 当前分支 +- 当前 HEAD +- `git status` +- `origin` / 当前公开远端 +- `particle/main` 是否与远端同步 + +如果存在与本任务无关的本地修改: + +- 不得删除 +- 不得覆盖 +- 不得顺带提交 +- 在完成报告中记录 +- 本任务提交时只 stage 本任务相关文件 + +如果当前不是 `particle/main`,不要擅自切换覆盖用户工作。 + +先明确报告实际状态。 + +--- + +## 5. 验证目标 + +本任务只回答下面四个问题。 + +### V1. 独立 Core Layer 能否进入真实 MapLibre Style / RenderTree 顺序 + +必须验证一个最小实验 Layer: + +- 由 Core Style/Layer 系统识别 +- 能生成对应 RenderLayer +- 能进入真实 RenderTree +- 能按 Style Layer 顺序参与渲染 +- 不依赖 MapView 外部 Overlay +- 不依赖 JS/RN 每帧逻辑 +- 不把实现塞进现有 RasterLayer + +### V2. 该 Layer 能否在同一 MapLibre render frame 获得当前 Transform + +至少验证能够在真正绘制路径中读取或使用: + +- viewport size +- zoom +- bearing +- pitch +- projection / transform matrix 中架构要求的必要部分 + +验证重点不是把数值打印出来,而是确认这些值来自: + +> 当前 MapLibre render frame 的真实 `PaintParameters / TransformParameters / TransformState` 链 + +而不是另建独立 Camera 状态。 + +### V3. Layer 活动时能否驱动持续 repaint + +验证: + +- Layer 处于“动画活动”状态时,可以通过 MapLibre 自己的 render loop 持续获得下一帧 +- 不建立独立于 MapLibre 的第二套动画循环 +- 不依赖 Android UI Timer +- 不依赖 JS `requestAnimationFrame` + +### V4. Layer 停止活动后能否停止额外 repaint + +验证: + +- Layer 切换为静止/disabled 后,不继续无意义请求下一帧 +- 地图自身正常 pan/zoom/render 不受影响 +- 不产生永久 continuous rendering + +--- + +## 6. 本任务不是粒子实现 + +验证代码只能证明架构入口成立。 + +禁止在本任务实现: + +- 速度场 +- U/V Grid +- Field decode +- particle position +- age +- seed +- advection +- velocity texture +- particle state texture +- ping-pong +- Trail +- fade pass +- Land/Ocean Mask +- Wave/Tide/Current +- Wind fixture +- 真正 Particle Shader +- compute shader + +如需要视觉确认,只允许使用最简单的验证图元或调试绘制。 + +例如: + +- 单色矩形 +- 三角形 +- 简单全屏/局部 quad +- 简单随帧变化的 alpha / position + +不得把验证图元包装成“粒子初版”。 + +--- + +## 7. 验证代码命名 + +验证代码必须明确表明其临时技术验证属性。 + +优先使用: + +`ParticleValidation*` + +或: + +`RasterParticleValidation*` + +不要直接把不成熟的验证代码命名成最终生产类并假装已经冻结。 + +如果必须使用正式架构类名才能验证注册链,例如: + +- `RasterParticleLayer` +- `RenderRasterParticleLayer` + +则必须: + +1. 只实现验证所需最小骨架; +2. 在代码和文档中明确 `VALIDATION ONLY`; +3. 不提前加入未来未验证业务接口; +4. 不把临时调试字段伪装成正式公共 API。 + +--- + +## 8. 最小实现范围 + +Codex 应先调查当前最小实现需要触及哪些文件。 + +原则: + +> 能用 5 个文件解决,不要改 15 个文件。 + +预计可能涉及,但不得机械照搬: + +- Core style layer 声明 +- Core style layer 实现 +- 对应 RenderLayer +- LayerManager / 注册位置 +- Android Test App 或现有测试入口 +- 最小验证日志/状态 + +最终以当前真实代码为准。 + +不得为了“以后可能用到”提前创建整套 particle 目录树。 + +--- + +## 9. Style / RenderTree 验证要求 + +必须证明实验 Layer 是通过正式 MapLibre Layer 链进入渲染。 + +验证报告至少记录实际链: + +```text +Style Layer +→ LayerManager / RenderLayer creation +→ RenderTree / RenderOrchestrator +→ 对应 RenderLayer +→ Render pass +``` + +必须指出: + +- 实际插入哪个 pass +- 与前后普通 Style Layer 的 order 是否正确 +- 是否需要 tile/source +- 当前验证 Layer 是否 source-less +- 如果 source-less 不可行,实际最小 Source 依赖是什么 + +不能只通过 CustomLayer 示例证明。 + +本任务要验证的是总体架构选择的“独立 Core Layer 路线”。 + +--- + +## 10. 同帧 Camera / Matrix 验证 + +验证代码必须从当前 render path 内取得 transform。 + +至少记录: + +- 获取位置 +- 对应源码对象 +- 生命周期 +- 是否每 frame 更新 +- 是否与当前 RenderTree frame 同步 + +至少验证以下地图变化不会让实验 Layer 明显脱离地图: + +- pan +- zoom +- bearing +- pitch +- resize + +本任务不要求完成 world wrap / antimeridian 专项验证。 + +这些留给后续 Mask/Camera 技术验证。 + +--- + +## 11. Repaint 验证 + +实验 Layer 必须提供最小“活动/停止”活动模型。 + +例如内部验证状态: + +```text +ACTIVE +STATIC +``` + +### ACTIVE + +必须证明: + +- 当前 frame 完成后仍会请求 MapLibre 下一帧 +- 连续帧来自 MapLibre render/repaint 链 +- 能观察到 frame counter 持续增长 + +### STATIC + +必须证明: + +- 不再由该 Layer 主动触发下一帧 +- 地图静止时 frame counter 不继续无意义增长 +- 用户操作地图时仍能正常重新绘制 + +不得使用平台 Timer 模拟。 + +--- + +## 12. 日志要求 + +验证期间允许增加最少量调试日志。 + +日志至少能识别: + +- Layer create +- RenderLayer create +- first render +- frame counter +- current active/static +- repaint request +- relevant transform snapshot +- destroy/deinitialize + +禁止每帧输出大量完整 matrix 导致日志爆炸。 + +建议: + +- 首帧输出一次完整关键参数 +- 后续每 N 帧输出 frame counter +- 状态变化时输出 +- 销毁时输出 + +验证完成后是否保留日志,由报告明确说明。 + +--- + +## 13. Android 第一验证平台 + +本任务第一运行平台固定为: + +**Android OpenGL** + +原因: + +- 当前架构设计将它作为第一共同验证入口; +- 本任务只验证 Core Layer + 同帧 transform + repaint; +- 不在本任务同时扩大到 Vulkan / iOS。 + +Vulkan 和 iOS 留到后续对应验证。 + +--- + +## 14. Android 编译规则:必须由用户手工执行 + +本项目 Android 编译必须由用户手工执行。 + +Codex 禁止运行: + +- Gradle Android build +- `./gradlew assemble...` +- `./gradlew install...` +- Android npm build +- 任何等价 Android 编译命令 + +Codex 可以: + +- 检查现有 Gradle 配置 +- 准备代码 +- 给出用户应执行的准确编译命令 +- 分析用户返回的编译错误 +- 在允许范围内修正本任务代码 + +Codex 不得代替用户执行 Android 编译。 + +--- + +## 15. 运行/设备验证规则 + +本任务不得默认替用户执行: + +- ADB 安装 +- ADB 启动 +- ADB 日志抓取 +- 真机操作 + +除非用户在后续明确授权。 + +因此第一轮 Codex 工作完成后,应输出: + +1. 用户手工编译命令 +2. 应打开的 Test App / 页面 / Activity +3. 需要执行的地图操作 +4. 需要观察的日志 +5. PASS/FAIL 判定方法 + +用户提供编译/运行结果后,再完成最终技术验证结论。 + +--- + +## 16. 两阶段执行方式 + +本任务允许同一份指令书分两次 Codex 执行。 + +### 阶段 A:验证代码准备 + +Codex: + +1. 读取基线 +2. 写最小验证代码 +3. 做静态检查 +4. 生成暂定验证报告 +5. 给出用户手工编译/运行步骤 +6. commit + push 到 `particle/main` + +此时报告结论必须写: + +`WAITING_MANUAL_VALIDATION` + +不得写 PASS。 + +### 阶段 B:用户返回实际编译/运行结果后 + +Codex: + +1. 读取用户提供的编译结果、日志和现象 +2. 如果失败,只修复本验证范围内问题 +3. 如果架构假设本身失败,停止,不强行修成另一套架构 +4. 更新正式验证报告 +5. 更新 Codex 完成报告 +6. 更新 `粒子开发记录.md` +7. commit + push 到 `particle/main` + +最终只能是: + +- `PASS` +- `FAIL` +- `BLOCKED` + +--- + +## 17. PASS 标准 + +只有同时满足以下条件,`30-01` 才能 PASS: + +- [ ] 独立 Core Style Layer 被正式识别 +- [ ] 对应 RenderLayer 被创建 +- [ ] 真正进入 RenderTree / Render pass +- [ ] Layer order 正确 +- [ ] 从当前 render frame 获得必要 Transform +- [ ] pan 正常 +- [ ] zoom 正常 +- [ ] bearing 正常 +- [ ] pitch 正常 +- [ ] resize 正常 +- [ ] ACTIVE 时由 MapLibre render loop 持续 repaint +- [ ] STATIC 时停止该 Layer 的额外 repaint +- [ ] 地图普通渲染不受破坏 +- [ ] 未使用外部 Overlay +- [ ] 未建立平台 Timer 动画循环 +- [ ] 未实现超出本任务范围的粒子系统 + +任何一项关键门槛失败,不得标 PASS。 + +--- + +## 18. FAIL 与停止条件 + +出现以下任一情况时,应停止继续向后实现: + +### F1 + +独立 Core Layer 无法在不破坏 MapLibre 现有主架构的情况下进入真实 RenderTree。 + +### F2 + +无法从该 Layer 的真实 render frame 获得足够 transform。 + +### F3 + +必须依赖外部 Overlay 或独立动画 loop 才能工作。 + +### F4 + +持续 repaint 必须通过明显侵入 Map 主循环才能实现。 + +### F5 + +验证要求迫使 Android/iOS 分别拥有不同 particle engine。 + +若触发 F1-F5: + +- 停止 +- 不继续 GPU ping-pong +- 不继续 Mask +- 不继续 iOS 实现 +- 报告失败事实 +- 回到架构设计重新决策 + +不得为了让验证“通过”扩大修改范围。 + +--- + +## 19. 禁止事项 + +本任务禁止: + +- 正式实现 Particle Field +- 正式实现 RasterArraySource 数据链 +- 正式实现 GPU Simulation +- 正式实现 Trail +- 正式实现 Mask +- 接真实天气数据 +- 引入新第三方依赖 +- 升级 MapLibre +- 升级 Android SDK / NDK / Gradle +- 修改无关 Layer +- 重构 Renderer +- 重构 Map +- 修改 iOS 代码 +- 实现 Vulkan 版本 +- 建立 RN/JS bridge +- 新增产品 API +- 清理与本任务无关的旧代码 + +--- + +## 20. 文档输出 + +### 20.1 技术验证报告 + +生成: + +`docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v1.md` + +如果同主题已有文件,按版本递增,不覆盖旧文件。 + +报告必须区分: + +- 静态确认 +- 用户手工编译结果 +- 用户运行结果 +- 实际日志证据 +- 推论 + +没有用户运行结果时,只能写: + +`WAITING_MANUAL_VALIDATION` + +### 20.2 Codex 完成报告 + +生成: + +`docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v1.md` + +同主题已有报告时递增版本号。 + +### 20.3 开发记录 + +追加: + +`粒子开发记录.md` + +不得覆盖历史内容。 + +--- + +## 21. 不主动创建 Step + +本任务不主动创建或修改 `docs/50_Step记录/`。 + +只有用户明确要求时才创建 Step。 + +--- + +## 22. Git 收尾规则 + +本任务凡产生文件修改,都必须提交并 push。 + +目标: + +- 本地主线:`particle/main` +- 公开远端对应分支:`particle/main` + +### 22.1 提交范围 + +只允许提交: + +- 本任务验证代码 +- 本任务验证文档 +- 本任务完成报告 +- 本任务对 `粒子开发记录.md` 的追加 + +不得把与本任务无关的本地修改一起提交。 + +### 22.2 阶段 A + +验证代码准备完成后: + +1. 检查 `git diff` +2. 明确列出本任务文件 +3. commit +4. push 到公开 `particle/main` +5. 确认远端可见 + +推荐提交信息: + +`test: add core particle render layer validation` + +### 22.3 阶段 B + +用户手工验证结果回填后: + +1. 更新验证报告 +2. 更新完成报告 +3. 更新开发记录 +4. 如有必要修复最小验证代码 +5. commit +6. push 到公开 `particle/main` + +推荐提交信息: + +`test: finalize core particle render layer validation` + +### 22.4 Push 失败 + +如果 push 失败: + +- 不得声称任务完整完成 +- 完成报告必须写明: + - push 失败 + - 错误原因 + - 本地 commit hash + - 目标远端 + - 目标分支 + +--- + +## 23. Git 信息记录 + +Codex 完成报告必须记录: + +- 任务开始 HEAD +- 本次主要变更 commit hash +- commit message +- push 是否成功 +- 公开远端 +- 远端分支 +- push 后远端可见状态 +- 是否存在未提交的无关本地改动 + +为避免“提交文件记录自己的 commit hash”产生自引用问题: + +- 完成报告中的 `主要变更 commit hash` 记录包含验证代码和主要文档的实质性提交; +- 如果随后还有仅用于回填报告元数据的收尾提交,Codex 最终回复中必须另外报告最终远端 HEAD; +- 不得为了回填当前提交自己的 hash 无限 amend。 + +--- + +## 24. Codex 完成报告必须逐项说明 + +至少包括: + +1. 前置文档读取 +2. 当前 Git 基线 +3. 实际修改文件 +4. Style Layer 接入位置 +5. RenderLayer 创建链 +6. RenderTree / pass +7. Transform 获取链 +8. Repaint 链 +9. Android OpenGL 验证入口 +10. 用户手工编译命令 +11. 用户手工运行步骤 +12. 当前状态: + - WAITING_MANUAL_VALIDATION + - PASS + - FAIL + - BLOCKED +13. 是否修改生产代码 +14. 是否扩大架构范围 +15. 是否安装/升级依赖 +16. Git commit +17. push 结果 +18. 是否可以进入下一个最小技术验证 + +--- + +## 25. 进入下一验证的条件 + +只有最终结果: + +`PASS` + +才允许进入下一个技术验证。 + +下一项预计验证: + +> GPU State A/B + Trail A/B 的 gfx ping-pong、RenderTarget、同步/barrier 与 context recreation。 + +如果本任务是: + +- FAIL +- BLOCKED + +则不得提前执行下一项。 + +必须先回到架构层决定如何处理。 + +--- + +## 26. 本任务完成定义 + +`30-01` 真正完成必须满足: + +> 用户手工 Android OpenGL 编译和运行后,已经通过实际运行证据确认:独立 Core Layer 可以进入 MapLibre 正式 Style/RenderTree 渲染链,在同一 render frame 使用 MapLibre 当前 Transform,并通过 MapLibre 自己的 repaint 机制在活动时持续渲染、停止时停止额外帧。 + +只有静态代码完成、没有实际运行结果,不叫验证完成。 + +只能标记: + +`WAITING_MANUAL_VALIDATION` diff --git a/docs/40_任务/30_02_GPU_PingPong与ContextRecreation验证指令书_v1.md b/docs/40_任务/30_02_GPU_PingPong与ContextRecreation验证指令书_v1.md new file mode 100644 index 000000000..8faf120e3 --- /dev/null +++ b/docs/40_任务/30_02_GPU_PingPong与ContextRecreation验证指令书_v1.md @@ -0,0 +1,1184 @@ +# MapLibreParticleLab 30_最小技术验证-02_GPU Ping-Pong 与 Context Recreation 验证指令书 v1 + +## 1. 任务基本信息 + +- 项目:`MapLibreParticleLab` +- 大阶段:`30_最小技术验证` +- 子任务:`30-02 GPU Ping-Pong 与 Context Recreation 验证` +- 当前主线:`particle/main` +- 前置阶段: + - `10_架构调查` 已完成 + - `20_架构设计` 已完成 + - `30-01 Core RenderLayer 接入验证` 必须已经正式 `PASS` +- 前置正式架构文档: + - `docs/10_架构/20_粒子系统总体架构设计_v1.md` +- 任务性质:最小运行时技术验证 +- 本任务不是正式粒子功能实现 +- 本任务允许增加最少量、明确标记为 `VALIDATION ONLY` 的 GPU 验证代码 +- 不允许顺手重构 MapLibre +- 不允许扩大到真实 Particle Field、真实风/浪/潮/海流数据、Land/Ocean Mask、Vulkan 或 iOS + +本任务不加入 Jest。 + +--- + +## 2. 前置门槛 + +`30-02` 只能建立在 `30-01` 已经正式 `PASS` 的基础上。 + +Codex 开始前必须读取当前仓库中的: + +- `docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_*.md` +- `docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_*.md` +- `粒子开发记录.md` + +并确认当前有效版本明确记录: + +`30-01 = PASS` + +如果当前仓库仍然是: + +- `WAITING_MANUAL_VALIDATION` +- `FAIL` +- `BLOCKED` +- 或无法找到正式 PASS 记录 + +则: + +1. 不得开始 30-02 GPU 代码; +2. 不得自行把 30-01 改成 PASS; +3. 只生成阻塞说明; +4. 明确报告“30-02 前置条件未满足”; +5. 不提交任何 30-02 生产/验证代码。 + +用户当前已经明确要求进入 30-02,但最终仍以仓库中当前正式验证记录为执行门槛。 + +--- + +## 3. 本任务为什么是 30-02 + +总体架构与 30-01 已经把最小技术验证顺序定义为: + +1. Core RenderLayer / Style order / 同帧 Transform / repaint +2. GPU State A/B + Trail A/B ping-pong 与 context recreation +3. Camera / world wrap / Land-Ocean Mask +4. iOS Core Layer 薄接入 + +因此本任务只验证第 2 项。 + +本任务必须回答: + +> 已经通过 30-01 验证的独立 Core Particle RenderLayer,能否在 MapLibre 当前 gfx / RenderTarget / CommandEncoder 生命周期内稳定管理 GPU State A/B 和 Trail A/B,完成无读写冲突的 ping-pong,并在 viewport resize、Layer disable/enable、Surface/GPU context recreation 后正确释放和重建资源。 + +本任务不得提前处理第 3、4 项。 + +--- + +## 4. 必须先读取 + +Codex 开始前必须完整读取: + +1. 根目录 `AGENTS.md` +2. `PARTICLE_BASELINE.md` +3. `docs/00_项目总则/` 下当前全部有效文档 +4. `docs/00_项目规范/` 下当前全部有效文档 +5. `docs/20_调查/10_架构调查报告_v1.md` +6. `docs/10_架构/20_粒子系统总体架构设计_v1.md` +7. `docs/报告/10_架构调查_Codex完成报告_*.md` +8. `docs/报告/20_架构设计_Codex完成报告_*.md` +9. `docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_*.md` +10. `docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_*.md` +11. `粒子开发记录.md` +12. 30-01 当前实际验证代码 +13. 与下列 gfx / GPU 资源链直接相关的当前源码: + - `gfx::Context` + - `Texture2D` + - `DynamicTexture` + - `RenderTarget` + - `CommandEncoder` + - render pass / upload pass + - texture sampling / attachment 相关实现 + - framebuffer / render target backend 实现 + - Android OpenGL backend + - EGL / surface / renderer context 生命周期 + - resize / surface recreation 相关链 + - 现有使用 offscreen RenderTarget 的 MapLibre 内部示例 + - 现有 texture ping-pong、双缓冲或多 pass 示例,如仓库存在 + +不得根据通用 OpenGL 知识直接猜当前 MapLibre gfx 行为。 + +必须以当前 `particle/main` 实际代码为准。 + +--- + +## 5. 开始前 Git 检查 + +先执行只读检查并记录: + +- 当前分支 +- 当前 HEAD +- `git status` +- 当前公开远端 +- `particle/main` 是否与远端同步 +- 30-01 最终 PASS 对应 commit +- 是否存在与本任务无关的本地修改 + +如果存在无关修改: + +- 不得删除 +- 不得覆盖 +- 不得一起提交 +- 在完成报告中记录 +- 本任务只 stage 本任务相关文件 + +如果当前不是 `particle/main`: + +- 不得擅自覆盖用户工作 +- 先记录实际状态 +- 不得为了完成任务强制 reset + +--- + +## 6. 验证目标 + +本任务只回答下面五个问题。 + +### V1. GPU State A/B 能否通过当前 gfx 资源体系建立并稳定 ping-pong + +必须创建一组最小验证资源: + +- `State A` +- `State B` + +要求: + +- 两个资源在逻辑上独立 +- 当前帧只读其中一个,只写另一个 +- 帧结束后交换角色 +- 下一帧反向读取/写入 +- 不允许同一 pass 内把同一 texture 同时作为读取源和写入目标 +- 不允许依赖 CPU 每帧搬运完整 state +- 不允许依赖平台侧独立 GL 线程逻辑 + +本任务不冻结正式粒子状态 texture 的最终格式。 + +应优先选择当前 Android OpenGL + MapLibre gfx 已明确支持、便于验证的最小格式。 + +报告中必须明确: + +- 选择了什么 gfx texture / renderable resource +- 为什么适合本次验证 +- 哪些属性只是验证选择,不属于正式生产 contract + +--- + +### V2. Trail A/B RenderTarget 能否稳定 ping-pong + +必须创建: + +- `Trail A` +- `Trail B` + +基本要求: + +- Trail 使用真实 offscreen `RenderTarget` +- 当前帧读取 previous trail +- 写入 next trail +- 完成后交换 A/B +- 最后把当前有效 trail 合成到 MapLibre 正常主 render pass +- 不覆盖地图正常 framebuffer 生命周期 +- 不建立 MapView 外部 Overlay + +本任务中的 Trail 只是验证图形。 + +允许使用: + +- 简单移动点 +- 简单线段 +- 简单 alpha 衰减 +- 固定颜色图形 +- 可重复确定性 pattern + +禁止把本任务包装成正式风粒子视觉效果。 + +Trail 必须能够直观看出: + +- 前一帧结果被下一帧正确采样 +- A/B 正在交替 +- 不存在明显 feedback corruption +- resize 后重新建立正确尺寸的 target + +--- + +### V3. 多 Pass 同步 / hazard 是否能在现有 gfx 抽象内正确完成 + +必须确认实际执行顺序至少包含: + +```text +State Read +→ State Write +→ State swap +→ Trail Read +→ Trail Write +→ Trail swap +→ Composite to main render target +``` + +实际顺序可根据当前 gfx API 调整,但必须满足无读写冲突。 + +重点调查并验证: + +- render pass 结束后下一 pass 采样刚写入资源是否成立 +- 当前 gfx 是否自动处理必要状态切换 +- Android OpenGL backend 是否依赖 pass ordering / framebuffer unbind / texture state +- 是否存在显式 barrier API +- 如果 OpenGL 路径不需要显式 barrier,必须如实记录,不得为了“看起来完整”自行增加无意义 barrier +- 如果其他 backend 未来需要不同同步策略,应通过 gfx/backend 边界承载,不得把 GL/Vulkan/Metal 特例散到公共粒子逻辑 + +本任务要验证的是: + +> 当前 MapLibre gfx 抽象是否足够表达安全的 GPU ping-pong。 + +不是要求强行创造一套新的通用同步框架。 + +--- + +### V4. resize / disable / enable 后资源生命周期是否正确 + +必须验证: + +#### viewport resize + +- Trail A/B 按新 viewport 尺寸重新创建 +- 旧 Trail 资源被安全释放 +- State A/B 如果与 viewport 无关,不得无意义重建 +- resize 后第一帧状态明确 +- 不采样已释放 target +- 不出现黑屏、花屏或地图崩溃 + +#### Layer disable + +- 停止新增 GPU 更新 +- 不继续无意义 repaint +- GPU 资源如何保留或释放必须明确 +- 行为应与总体架构生命周期一致 + +#### Layer re-enable + +- 如果资源仍有效,允许复用 +- 如果已失效,必须重新创建 +- ping-pong index / generation 必须恢复到明确状态 +- 不允许使用旧 generation 的资源句柄 + +--- + +### V5. Surface / GPU Context Recreation 后能否正确恢复 + +这是 30-02 的关键门槛。 + +必须调查当前 Android OpenGL 实际: + +- EGL context 生命周期 +- SurfaceView / TextureView surface 生命周期 +- renderer backend activate/deactivate +- gfx context / resource 生命周期 +- Activity background / foreground +- Activity recreate +- surface destroy / recreate + +验证目标: + +1. 能观察到一次真实的 surface/context/resource generation 变化; +2. 旧 State A/B、Trail A/B 不再被使用; +3. 新 GPU context/resource generation 中重新创建验证资源; +4. ping-pong 从明确初始状态恢复; +5. Trail 可以清空重新开始,不要求保留 context loss 前历史; +6. 地图本体继续正常工作; +7. 不 crash; +8. 不访问 stale texture / render target; +9. 不要求宿主重新创建第二套粒子 engine。 + +如果用户的普通后台/前台操作没有触发真实 GPU context recreation: + +- 不得把“Activity 回来了”当成 context recreation 已验证; +- 必须通过日志或资源 generation 证明; +- Codex 应基于当前 Test App / renderer 生命周期给出最小可重复触发方法; +- 如果当前设备/虚拟机环境无法可靠触发,则该项标记 `BLOCKED` 或 `UNVERIFIED`,不得伪造 PASS。 + +--- + +## 7. 验证代码必须保持最小 + +本任务不是正式 GPU Particle Simulation。 + +验证代码只需要证明: + +- gfx texture / renderable resource 可以建立 +- State A/B 可以轮换 +- Trail A/B 可以轮换 +- pass 顺序正确 +- resize 正确 +- context recreation 正确 + +原则: + +> 能用一个极小 deterministic validation state 证明,就不要提前实现真实粒子状态。 + +推荐但不强制: + +- State texture:极小固定尺寸,例如 4×4、8×8 或 16×16 +- State 内容:确定性递增值、位置标记或颜色编码 +- Trail target:按 viewport 尺寸 +- Trail 图形:简单移动 marker / alpha fade / pattern + +最终尺寸和格式必须以当前 gfx 实际能力为准。 + +禁止因为方便直接引入第三方 GPU 库。 + +--- + +## 8. State A/B 验证要求 + +至少记录逻辑: + +```text +frame 1: read=A, write=B +frame 2: read=B, write=A +frame 3: read=A, write=B +... +``` + +必须验证: + +- A/B logical role 每帧交替 +- swap 只在对应 pass 成功完成后发生 +- pass 失败时不能错误推进 index +- disable 后停止推进 +- resource recreate 后 generation 增加并重置 index +- 不跨 generation 复用逻辑引用 + +推荐内部验证元数据: + +- `resourceGeneration` +- `frameIndex` +- `stateReadSlot` +- `stateWriteSlot` +- `trailReadSlot` +- `trailWriteSlot` +- `viewportWidth` +- `viewportHeight` + +这些字段只用于验证,不代表正式公共 API。 + +--- + +## 9. Trail A/B 验证要求 + +Trail A/B 必须真正经过 offscreen RenderTarget。 + +至少验证: + +1. previous trail 被采样 +2. 执行简单 fade / copy / pattern pass +3. 当前验证图元写入 next trail +4. next trail 成为新的 current trail +5. current trail 合成到地图 +6. 下一帧角色交换 + +不得: + +- 直接在主 framebuffer 上画完后声称有 Trail ping-pong +- 只创建两个 texture 但不实际作为 RenderTarget +- 用 CPU image buffer 模拟 GPU trail + +resize 后: + +- old size target 必须退出 +- new size target 必须建立 +- 第一帧允许 clear +- 不要求保留 resize 前 trail 历史 + +--- + +## 10. gfx / backend 边界 + +公共验证逻辑必须优先使用当前 MapLibre gfx API。 + +禁止把下列调用散到 Core 粒子逻辑: + +- raw `glBindFramebuffer` +- raw `glFramebufferTexture*` +- raw `glMemoryBarrier` +- raw Vulkan image barrier +- raw Metal encoder 生命周期 + +如果当前 MapLibre gfx 缺少本验证不可绕过的能力: + +1. 先确认仓库中确实不存在可复用抽象; +2. 只增加最小、粒子无关或 backend 合理归属的 gfx 扩展; +3. 不为未来猜测设计大型抽象; +4. 报告为什么必须新增; +5. 明确 Android OpenGL 与未来 Vulkan/Metal 的边界; +6. 如果必须大量侵入 gfx 通用架构,立即停止并标记架构阻塞。 + +--- + +## 11. Shader 范围 + +本任务允许最少验证 shader,但必须明确标记: + +`VALIDATION ONLY` + +允许: + +- copy +- simple increment +- fade +- constant color +- moving marker +- simple composite + +禁止: + +- 正式 velocity sampling +- 正式 advection +- respawn +- random seed 系统 +- 正式 particle age +- 正式 field bounds +- 正式 wind/current shader +- Land/Ocean Mask +- 正式颜色映射 +- 正式产品参数 + +验证 shader 文件名也应明确体现 validation 属性。 + +--- + +## 12. Context / Resource Generation 记录 + +为了避免把普通 lifecycle 误认为 context recreation,验证必须建立逻辑 generation。 + +例如: + +```text +ParticleGpuValidation: +contextGeneration=1 +resourceGeneration=1 +create StateA +create StateB +create TrailA +create TrailB +``` + +发生真实 context/surface resource invalidation 后: + +```text +contextGeneration=2 +resourceGeneration=2 +create StateA +create StateB +create TrailA +create TrailB +``` + +不要求暴露底层 GL texture id。 + +如果当前 backend 能安全识别 context identity,可记录安全的调试信息。 + +如果不能: + +- 使用 renderer/backend lifecycle 事件 + resource generation 组合证明 +- 不得为了日志强行暴露 backend 私有句柄 + +--- + +## 13. 日志要求 + +日志必须足够判断 PASS/FAIL,但不能每帧爆量输出。 + +至少包含: + +- validation layer create +- GPU validation resources create +- resource generation +- state A/B create +- trail A/B create +- first ping-pong frame +- 每 N 帧一次: + - frameIndex + - state read/write slot + - trail read/write slot + - generation +- resize detected +- trail targets recreate +- disable +- enable +- surface/context invalidation +- resources destroy +- resources recreate +- first frame after recreation +- validation layer destroy + +建议每 60 帧或状态变化时输出一次。 + +禁止每帧完整输出 matrix、texture 描述或大量 GPU 状态。 + +--- + +## 14. Android 第一验证平台 + +本任务第一运行平台固定为: + +**Android OpenGL** + +不在本任务同时验证: + +- Android Vulkan +- iOS OpenGL +- iOS Metal + +原因: + +30-02 的目标是验证 Core gfx ping-pong 与资源生命周期这个架构门槛,不是一次完成所有 backend。 + +如果 Android OpenGL 证明当前 Core/gfx 路线本身无法成立,应先停止回到架构层,不得继续复制到其他平台。 + +--- + +## 15. Android 编译规则:必须由用户手工执行 + +Codex 禁止运行 Android 编译。 + +禁止: + +- `./gradlew assemble...` +- `./gradlew install...` +- Android Studio build automation +- 等价 Gradle Android build +- 自动安装 APK + +Codex可以: + +- 修改验证代码 +- 做静态源码检查 +- 检查 CMake / Gradle 配置 +- 给出用户手工编译命令 +- 分析用户提供的编译错误 +- 在本任务范围内修正代码 + +用户当前 Android OpenGL 构建基线已经采用仅 OpenGL 的目标。 + +Codex 必须根据当前实际 Gradle task 给出准确命令,不得凭旧命令猜测。 + +--- + +## 16. ADB / 设备操作规则 + +Android 安装、启动和实体/虚拟设备验证由用户手工执行。 + +Codex 不得默认: + +- `adb install` +- `adb shell am start` +- 清 App 数据 +- 自动抓 logcat +- 自动重启设备 + +如需给出 ADB 命令,当前 MapLibreParticleLab / AutoNav Android 虚拟机地址固定为: + +`192.168.1.146` + +所有命令必须显式使用: + +`adb -s 192.168.1.146 ...` + +不得省略 `-s`。 + +不得使用旧地址 `192.168.200.145`。 + +--- + +## 17. Test App 验证入口 + +优先复用 30-01 已建立并跑通的 Particle Core Layer Validation 入口。 + +不得为了 30-02 再建立一套完全平行的 Test App。 + +可以在现有验证 Activity / 页面中增加独立 GPU validation 模式或按钮,但必须: + +- 保持 30-01 原有验证能力不被破坏 +- 明确区分 30-01 和 30-02 +- 不让旧测试默认变成正式粒子 demo +- 不引入产品 UI + +建议提供最少控制: + +- `ACTIVE` +- `STATIC` +- `DISABLE` +- `ENABLE` +- `CLEAR/RESET`(如果确有必要) +- 显示当前 generation / frame / ping-pong slots + +具体形式以现有 Test App 结构为准。 + +--- + +## 18. 用户手工验证步骤 + +Codex 阶段 A 完成后,必须给出准确、可直接执行的手工步骤。 + +至少覆盖: + +### T1. 初始创建 + +观察: + +- State A/B 创建 +- Trail A/B 创建 +- generation 初值 +- 首帧成功 + +### T2. ACTIVE ping-pong + +保持至少数十帧。 + +确认: + +- State A/B 持续交替 +- Trail A/B 持续交替 +- frame counter 增长 +- 地图本身正常 + +### T3. STATIC + +确认: + +- ping-pong 停止推进 +- 不继续由 validation layer 触发无意义帧 +- 地图手势仍可触发正常 redraw + +### T4. resize + +执行当前设备环境下真实可行的 resize,例如: + +- 旋转屏幕 +- 改变 Activity / window 尺寸 +- 或当前 Test App 已有明确 resize 方法 + +确认: + +- Trail A/B 被重建为新尺寸 +- State A/B 不发生无必要重建 +- 地图正常 +- 无 stale target + +### T5. disable / enable + +确认: + +- disable 后停止 +- enable 后恢复 +- generation/index 状态合理 +- 无 crash + +### T6. context / surface recreation + +Codex 必须根据当前 Android OpenGL backend 实际生命周期,给出能最大概率触发真实 recreation 的步骤。 + +可能包括但不限于: + +- Activity recreate +- Surface destroy/recreate +- 后台/前台 +- 离开并重新进入验证 Activity + +但最终判定必须看日志是否真正发生资源/context generation 变化。 + +仅“界面重新出现”不算通过。 + +### T7. recreation 后继续运行 + +确认: + +- old resources destroy/invalid +- new resources create +- generation 增加 +- ping-pong 重新正常工作 +- Trail 从清空状态重新积累 +- 地图无异常 + +--- + +## 19. 两阶段执行方式 + +本任务继续采用 30-01 的两阶段方式。 + +### 阶段 A:验证代码准备 + +Codex: + +1. 检查 30-01 PASS 前置条件 +2. 读取当前 gfx / Android OpenGL 生命周期 +3. 设计最小验证实现 +4. 写验证代码 +5. 做静态检查 +6. 生成暂定技术验证报告 +7. 生成 Codex 完成报告 +8. 追加开发记录 +9. 给出用户手工编译/运行步骤 +10. commit +11. push 到公开 `particle/main` + +此时结论只能是: + +`WAITING_MANUAL_VALIDATION` + +不得写 PASS。 + +--- + +### 阶段 B:用户返回编译/运行结果后 + +Codex: + +1. 读取用户提供的编译输出、日志和现象 +2. 只修复 30-02 范围内的问题 +3. 如果发现架构/gfx 路线本身失败,停止 +4. 不通过扩大范围强行修成另一套架构 +5. 更新技术验证报告 +6. 更新 Codex 完成报告 +7. 追加开发记录 +8. commit +9. push 到公开 `particle/main` + +最终只能是: + +- `PASS` +- `FAIL` +- `BLOCKED` + +--- + +## 20. PASS 标准 + +只有同时满足以下条件,30-02 才能 PASS: + +- [ ] 30-01 当前正式状态为 PASS +- [ ] State A/B 两套独立 GPU resource 成功建立 +- [ ] State A/B 按帧安全交替 +- [ ] 不发生同资源同时读写 hazard +- [ ] Trail A/B 两套真实 RenderTarget 成功建立 +- [ ] Trail previous → next 的 GPU pass 实际运行 +- [ ] Trail A/B 按帧交替 +- [ ] Trail 能合成回 MapLibre 正常主 render pass +- [ ] 多 pass 顺序在当前 gfx 中稳定 +- [ ] 当前 Android OpenGL 同步语义已被真实确认 +- [ ] 不依赖 CPU 每帧复制完整状态 +- [ ] 不依赖 MapView 外部 Overlay +- [ ] 不建立平台独立动画循环 +- [ ] resize 后 Trail target 正确重建 +- [ ] disable 后停止无意义 GPU 更新 +- [ ] enable 后可恢复 +- [ ] 实际观察到资源/context generation 变化 +- [ ] context/surface recreation 后旧资源不再使用 +- [ ] recreation 后新资源重新建立 +- [ ] recreation 后 ping-pong 恢复 +- [ ] recreation 后 Trail 可重新开始 +- [ ] MapLibre 地图渲染不被破坏 +- [ ] 无 crash +- [ ] 无明显 stale resource / framebuffer corruption +- [ ] 未扩大到正式粒子算法、真实 Field、Mask、Vulkan、iOS +- [ ] 未引入新第三方依赖 + +任何关键门槛缺少实际运行证据,不得标记 PASS。 + +--- + +## 21. FAIL / BLOCKED 与停止条件 + +出现以下情况时停止继续向后实现。 + +### F1. gfx 无法表达安全 ping-pong + +如果必须绕开当前 gfx,在 Core 粒子逻辑中大量直接写 raw OpenGL 才能完成: + +- 停止 +- 标记架构问题 +- 不继续 30-03 + +### F2. RenderTarget 无法作为下一 pass 的稳定采样源 + +如果当前架构下无法安全完成: + +`write → pass end → sample` + +且无法通过很小的 gfx 合理扩展解决: + +- 停止 +- 报告真实限制 + +### F3. Context recreation 后存在 stale resource + +如果旧 texture / render target 在新 context/resource generation 中仍被使用: + +- 30-02 FAIL +- 不得忽略 crash 或图形损坏 + +### F4. 必须把 GPU 生命周期放到 Android 平台层形成第二套 engine + +如果 Core/gfx 无法共用,必须在 Android 独立维护完整 particle GPU engine: + +- 停止 +- 回到架构设计 + +### F5. 为了验证需要大规模修改 Renderer / Map 主循环 + +如果本验证迫使明显侵入: + +- Renderer 总体架构 +- Map 主循环 +- 通用 Layer 系统 +- 大量 gfx backend + +则停止,不得以“先能跑”为理由继续扩张。 + +### B1. 无法触发真实 context recreation + +如果代码本身运行正常,但当前测试环境无法可靠触发或确认真实 GPU context recreation: + +最终状态应为: + +`BLOCKED` + +不得写 PASS。 + +--- + +## 22. 禁止事项 + +本任务禁止: + +- 正式 Particle Field +- 正式 U/V Grid +- fixture decode 正式实现 +- velocity texture 正式 contract +- 正式粒子 position / age / seed +- 正式 advection +- 正式 respawn +- 正式 random 系统 +- 正式风粒子效果 +- Wave/Tide/Current 正式实现 +- Land/Ocean Mask +- world wrap 专项实现 +- antimeridian 专项实现 +- 真实天气/海洋服务 +- Android Vulkan +- iOS OpenGL +- iOS Metal +- RN/JS bridge +- 新产品 API +- 新第三方依赖 +- MapLibre 版本升级 +- Android SDK / NDK / Gradle 升级 +- 与本任务无关的旧代码清理 +- Jest + +--- + +## 23. 允许修改范围 + +优先只修改: + +- 30-01 已建立的 Particle validation 相关 Core Layer / RenderLayer +- 新增的 `ParticleGpuValidation*` 验证资源/辅助类 +- validation-only shader +- Android Test App 当前粒子验证入口 +- 必要且最小的 gfx 接口扩展 +- 本任务文档 +- `粒子开发记录.md` + +不得机械创建完整正式 particle production 目录树。 + +如果需要改动通用 gfx 文件: + +- 必须逐项列出 +- 解释为什么现有 API 不足 +- 保持修改最小 +- 不得夹带重构 + +--- + +## 24. 建议验证命名 + +优先使用: + +- `ParticleGpuValidationResources` +- `ParticleGpuValidationPass` +- `ParticlePingPongValidation` +- `ParticleTrailValidation` +- `ParticleGpuValidationShader` + +或与当前仓库命名风格一致的等价名称。 + +所有临时代码应明确: + +`VALIDATION ONLY` + +不得把未冻结代码命名为最终 `ParticleSimulation` / `ParticleTrailRenderer` 并假装已经生产化。 + +--- + +## 25. 技术验证报告 + +生成: + +`docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v1.md` + +如果同主题已有文件: + +- 不覆盖 +- 递增版本号 + +报告必须严格区分: + +1. 静态源码确认 +2. Codex 可确认的实现事实 +3. 用户手工编译结果 +4. 用户实际运行结果 +5. 日志证据 +6. 推论 +7. 未验证项 + +阶段 A 时必须写: + +`WAITING_MANUAL_VALIDATION` + +--- + +## 26. Codex 完成报告 + +生成: + +`docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v1.md` + +如果同主题已有报告: + +- 递增版本号 +- 不覆盖旧报告 + +报告至少逐项说明: + +1. 30-01 前置状态 +2. 当前 Git 基线 +3. 前置文档读取 +4. 实际修改文件 +5. State A/B 实现位置 +6. Trail A/B 实现位置 +7. RenderTarget 创建链 +8. ping-pong swap 逻辑 +9. pass 顺序 +10. 同步 / hazard 处理 +11. Android OpenGL backend 事实 +12. resize 生命周期 +13. disable/enable 生命周期 +14. context/surface lifecycle +15. resource generation 机制 +16. Test App 验证入口 +17. 用户手工编译命令 +18. 用户手工运行步骤 +19. 当前验证状态 +20. 是否修改通用 gfx +21. 是否修改生产代码 +22. 是否引入依赖 +23. 是否扩大范围 +24. 未验证项 +25. PASS / FAIL / BLOCKED 依据 +26. 是否允许进入 30-03 +27. Git commit +28. push 结果 +29. 最终远端 HEAD + +--- + +## 27. 开发记录 + +追加: + +`粒子开发记录.md` + +必须记录: + +- 30-02 开始 +- 30-01 前置状态 +- State A/B 验证结论 +- Trail A/B 验证结论 +- gfx 同步结论 +- resize 结论 +- context recreation 结论 +- 最终状态 +- 是否允许进入 30-03 +- 相关报告路径 + +不得重写已有历史。 + +--- + +## 28. 不主动创建 Step + +本任务不主动创建或修改: + +`docs/50_Step记录/` + +只有用户明确要求时才创建 Step。 + +--- + +## 29. Git 收尾规则 + +本任务只要产生代码或文档修改,就必须 commit 并 push。 + +目标: + +- 本地主线:`particle/main` +- 公开远端对应分支:`particle/main` + +### 29.1 提交范围 + +只允许提交: + +- 本任务验证代码 +- 本任务 shader +- 本任务 Test App 调整 +- 本任务技术验证报告 +- 本任务 Codex 完成报告 +- 本任务对 `粒子开发记录.md` 的追加 + +不得提交与本任务无关的本地修改。 + +--- + +### 29.2 阶段 A 提交 + +验证代码准备完成后: + +1. 检查 `git diff` +2. 明确本任务文件列表 +3. stage 本任务文件 +4. commit +5. push 到公开 `particle/main` +6. 确认远端可见 + +推荐 commit message: + +`test: add particle gpu ping-pong validation` + +阶段 A 报告状态必须仍为: + +`WAITING_MANUAL_VALIDATION` + +--- + +### 29.3 阶段 B 提交 + +用户手工验证结果回填后: + +1. 更新技术验证报告 +2. 更新 Codex 完成报告 +3. 更新开发记录 +4. 如需修复,只修复 30-02 范围 +5. commit +6. push 到公开 `particle/main` +7. 确认远端可见 + +推荐 commit message: + +`test: finalize particle gpu ping-pong validation` + +--- + +### 29.4 Push 失败 + +如果 push 失败: + +- 不得声称完整完成 +- 不得声称公开远端已经更新 +- 完成报告必须写明: + - push 失败 + - 失败原因 + - 本地 commit hash + - commit message + - 目标远端 + - 目标分支 + - 当前本地 HEAD + +--- + +## 30. Git 信息记录 + +Codex 完成报告必须记录: + +- 任务开始 HEAD +- 30-01 PASS commit +- 本次主要变更 commit hash +- commit message +- push 是否成功 +- 公开远端 +- 远端分支 +- push 后远端可见状态 +- 最终远端 HEAD +- 是否存在未提交的无关本地改动 + +为避免报告自引用 commit hash: + +- 完成报告中的“主要变更 commit hash”记录包含验证代码和主要文档的实质性提交; +- 如果还有仅用于回填报告元数据的收尾 commit,Codex 最终回复中额外报告最终远端 HEAD; +- 不得无限 amend 只为让报告包含自己的 commit hash。 + +--- + +## 31. 30-03 进入条件 + +只有 30-02 最终状态为: + +`PASS` + +才允许进入下一项最小技术验证。 + +下一项预计为: + +> Camera / world wrap / Land-Ocean Mask 最小技术验证。 + +如果 30-02 为: + +- `FAIL` +- `BLOCKED` + +则不得提前开始 30-03。 + +必须先处理 GPU/gfx/context 生命周期阻塞。 + +--- + +## 32. 本任务完成定义 + +30-02 真正完成不是“创建了四个 texture”。 + +必须满足: + +> 在 Android OpenGL 的真实 MapLibre RenderLayer / gfx 渲染链中,已经通过用户实际运行证据确认:State A/B 与 Trail A/B 可以使用 MapLibre 当前 GPU resource / RenderTarget 体系安全 ping-pong;多 pass 顺序和同步语义成立;viewport resize 后资源正确重建;Layer disable/enable 生命周期正确;真实 surface/context/resource recreation 后旧资源不会被继续使用,新 generation 可以安全重建并恢复 ping-pong,同时不破坏地图正常渲染。 + +没有用户实际运行证据时,只能标记: + +`WAITING_MANUAL_VALIDATION` + +真实 context recreation 无法验证时: + +`BLOCKED` + +关键架构门槛失败时: + +`FAIL` + +不得为了阶段推进把未验证结果写成 PASS。 diff --git a/docs/40_任务/README.md b/docs/40_任务/README.md new file mode 100644 index 000000000..f2615d364 --- /dev/null +++ b/docs/40_任务/README.md @@ -0,0 +1,3 @@ +# Codex 任务目录 + +文件名 `STEP-xxx_<主题>_Codex任务_vN.md`;完成报告固定 `docs/报告/`。 diff --git a/docs/40_任务/STEP-060E_CoreOnlyLayer存在性检查修复_Codex任务_v1.md b/docs/40_任务/STEP-060E_CoreOnlyLayer存在性检查修复_Codex任务_v1.md new file mode 100644 index 000000000..1a158eac0 --- /dev/null +++ b/docs/40_任务/STEP-060E_CoreOnlyLayer存在性检查修复_Codex任务_v1.md @@ -0,0 +1,255 @@ +# STEP-060E CoreOnly Layer 存在性检查修复 — Codex任务 v1 + +## 1. 任务定位 + +当前仍为 **STEP-060**,不得进入 STEP-061。 + +本轮修复 STEP-060D 代码审查确认的 Android 根因: + +- `raster-particle` 与 `weather-color` layer 在 Android `LayerManagerAndroid` 中通过 `addLayerTypeCoreOnly(...)` 注册; +- Core 可以正常解析、持有和渲染这些 layer; +- 但 CoreOnly layer 没有 Java `Layer` peer; +- `Style.getLayer(id)` 最终依赖 `LayerManagerAndroid::createJavaLayerPeer(...)`; +- 对 CoreOnly layer,该函数返回 null; +- 因此 Bridge 用 `Style.getLayer()` 判断 layer 是否存在,必然产生假阴性。 + +这与当前现象一致:原生 Particle validation 成功,但 RN `WeatherRuntimeBridge.attach()` 报 weather layer 缺失。 + +本任务只修复这一 Android Java/JNI/Core 存在性检查链,并保持现有 CoreOnly 设计。 +## 2. 开工基线 + +正式仓库: + +`/mnt/sda1/MapLibreParticleLab` + +分支:`particle/main` + +本任务开工目标基线: + +`2d2375cd532468821c6e3c158ee1a275f4522044` + +该提交已包含 Win11 已验证的 RN Test Host 修复: + +- React `19.2.3` +- RN 0.85.2 codegen 配置 +- `NativeWeatherRuntimeBridge.ts` 同文件 type aliases +- 保留 Expo/RN repositories 的 Maven Local plugin 逻辑 +- STEP-060D `13.4.1-step060d` 版本接线 + +开工前执行 `git pull --ff-only origin particle/main`,确认基线没有落后。 +不得回退上述 Win11 已验证修复。 +## 3. 已确认源码证据 + +以下证据已经由 ChatGPT 直接审查当前源码确认,不要用 Codex 完成报告替代: + +1. `platform/android/MapLibreAndroid/src/cpp/style/layers/layer_manager.cpp` + - `RasterParticleLayerFactory` 使用 `addLayerTypeCoreOnly(...)` + - `WeatherColorLayerFactory` 使用 `addLayerTypeCoreOnly(...)` + - `createJavaLayerPeer(...)` 找不到 Java peer factory 时返回 null +2. `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp` + - `NativeMapView::getLayer(...)` 取得 Core layer 后调用 `createJavaLayerPeer(...)` + - 因而 CoreOnly layer 即使真实存在,Java `Style.getLayer()` 仍可得到 null +3. 当前 Bridge `WeatherRuntimeBridgeModule.attach()` 使用 `style.getLayer(...)` 做存在性判断,因此判断机制本身错误。 +4. STEP-060D 新增 `style.getLayers().map { it.id }` 也不安全:CoreOnly layer 没有 Java peer,不能用 Java Layer peer 枚举作为 Core layer 真值来源。 +5. `MapLibreMap.getStyle()` 只在 Style fully loaded 时返回非 null,所以本轮不是普通 style-ready 时机修复。 + +不得重新把问题归因于 React、MLRN mapStyle JSON 传递、AAR 混用或简单 attach 重试时机。 +## 4. 必做实现:Core 直接存在性查询 + +新增通用、窄范围的 Android MapLibre API,用来查询 **Core Style 真值**,不得依赖 Java Source/Layer peer。 + +公开语义固定为: + +- `MapLibreMap.hasStyleSource(String sourceId): boolean` +- `MapLibreMap.hasStyleLayer(String layerId): boolean` + +具体落点至少覆盖: + +- `NativeMap.java` +- `NativeMapView.java` +- `MapLibreMap.java` +- `native_map_view.hpp` +- `native_map_view.cpp` +- JNI method registration + +C++ 最终判断必须直接基于: + +- `map->getStyle().getSource(id) != nullptr` +- `map->getStyle().getLayer(id) != nullptr` + +不得通过 `createJavaLayerPeer()`、`Style.getLayer()` 或其他 Java peer 路径间接判断。 +## 5. Bridge attach 修正 + +修改正式: + +`integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/src/main/java/org/maplibre/particle/rntesthost/weather/WeatherRuntimeBridgeModule.kt` + +attach 在取得 fully-loaded `style` 后,改为: + +- source:使用 `map.hasStyleSource(WeatherIds.SOURCE)` +- weather color layer:使用 `map.hasStyleLayer(WeatherIds.COLOR_LAYER)` +- particle layer:使用 `map.hasStyleLayer(WeatherIds.PARTICLE_LAYER)` + +三个结果继续独立输出。 + +必须删除/停止使用以下方式作为存在性真值: + +- `style.getLayer(...)` +- `style.getLayers().map { it.id }` +- Java Layer peer 数量作为 Core layer 数量 + +source 也统一改用 Core 直接查询,不再让 Source/Layer 使用两套存在性语义。 +## 6. STEP-060D 诊断修正 + +保留稳定错误码: + +`WEATHER_OBJECTS_NOT_READY` + +失败信息至少包含: + +- `reactTag` +- `mapView=present|missing` +- `mapLibreMap=present|missing` +- `style=present|missing` +- `source[rn-weather-fields]=present|missing` +- `layer[rn-weather-color]=present|missing` +- `layer[rn-weather-particles]=present|missing` + +另外使用公开 `Style.getUri()` / Kotlin 对应属性输出 `styleUri`;JSON style URI 为空时明确写 `` 或等价稳定表示。 + +不要再调用 `style.getLayers()` 构造诊断列表。 +如果确实需要 ID 列表,只允许使用直接 Core ID 查询;本任务默认不要求新增列表 API,避免扩大修改面。 + +成功路径仍必须保持现有 `replaceWeatherRuntime(...)`、snapshot、listener 和 Phase A/B 控制语义不变。 +## 7. 明确禁止 + +本任务不得: + +- 将 `RasterParticleLayerFactory` 或 `WeatherColorLayerFactory` 从 `addLayerTypeCoreOnly(...)` 改成普通 Java layer registration +- 为了让 `Style.getLayer()` 非 null 而新建 WeatherColor/RasterParticle Java peer 类 +- 改变 CoreOnly layer 的产品边界 +- 用解析完整 Style JSON 作为正式存在性判断 +- 增加 attach 定时重试或无限重试 +- 修改 MLRN mapStyle 传递链来掩盖问题 +- 修改 WeatherRuntime 控制状态机 +- 实现 binary U/V field transport +- 进入 STEP-061 +- 在 Win11 临时修改 Native 正式源码 +- 执行 Win11 APK build/install、ADB 或设备测试 + +本轮目标是让 Java/RN Host 能以一个窄 API 查询 Core Style 真值,而不是把 CoreOnly 类型暴露成完整 Android Java Layer API。 +## 8. Maven / AAR 版本 + +本轮必须使用新的临时版本,禁止复用 STEP-060D 内容不同但版本相同的 artifact: + +`org.maplibre.gl:android-sdk-opengl:13.4.1-step060e` + +同步更新至少包括: + +- `platform/android/VERSION` +- RN Test Host `modules/weather-runtime-bridge/android/build.gradle` +- `plugins/withParticleNativeAndroid.js` + +Codex **不得构建 AAR,也不得执行 Android/Gradle AAR 打包或 Maven Local 发布**。 + +Codex 本轮只负责把源码与版本接线准备到可由用户手工构建的状态,并在完成 Linux 静态检查/精确编译后 commit + push。 + +用户在 Codex 停止后手工执行 OpenGL Release custom AAR 构建与 Maven Local 发布。用户构建完成后再记录: + +- Maven coordinate +- AAR build output 路径 +- Maven Local AAR 路径 +- AAR SHA256 +- 四 ABI 是否存在 + +Codex 完成报告中必须明确写:`AAR_BUILD_NOT_EXECUTED_BY_CODEX`,不得把“源码已准备”表述成“AAR 已生成”。 + +不得建立新的正式 Maven pipeline;本轮仍为 STEP-060 临时可追溯版本。 +## 9. Linux 验证要求 + +Codex 只执行与本轮源码相关的轻量、精确验证,不执行 AAR 打包。 + +至少完成: + +- Android JNI/C++ 相关源码的精确编译或现有等价最小验证; +- Android Java/Kotlin 受影响源码的可行精确编译;若因当前生成工程/codegen 缺失无法完成,必须明确记录限制,不得通过扩大修改范围绕过; +- 检查新增 JNI 方法声明、实现、注册签名一致; +- 检查 `MapLibreMap.hasStyleSource/hasStyleLayer` 最终确实直达 Core Style; +- 检查 Bridge 已完全停止用 Java `Style.getLayer()` / `Style.getLayers()` 判断 CoreOnly layer 存在性; +- `git diff --check`; +- 检查最终 diff,不得混入无关文件。 + +编译输出遵循全局 `$build-output-throttle`。 + +本任务不执行 Jest,也不得新增 Jest 要求。 +## 10. 文档要求 + +完成后更新 STEP-060E 对应中文 Step 记录与中文 Codex 完成报告。 + +同主题已有文档时优先更新或使用递增版本号,不得无意义创建多个不同文件名。 + +文档必须明确: + +- 根因为 CoreOnly layer 无 Java peer,导致 Java `Style.getLayer()` 假阴性; +- 本轮采用 Core 直接存在性查询,不改变 CoreOnly 架构; +- Win11 已验证 RN Test Host 基线来自 `2d2375cd532468821c6e3c158ee1a275f4522044`; +- Maven 临时版本已切到 `13.4.1-step060e`; +- `AAR_BUILD_NOT_EXECUTED_BY_CODEX`; +- Android APK/ADB/设备验证尚未执行; +- STEP-060 尚未 CLOSED,未进入 STEP-061。 +## 11. Git 收尾 + +完成规定源码、验证和文档后,只提交本任务相关变更到当前主线 `particle/main`。 + +必须: + +- commit 本任务相关源码、版本接线、任务文件及文档; +- push 到 `origin/particle/main`; +- push 失败时不得声称任务完成; +- 不得提交与本任务无关的本地改动。 + +完成报告必须记录: + +- source commit SHA; +- final HEAD; +- commit message; +- push 结果; +- remote branch; +- 预期 Maven coordinate:`org.maplibre.gl:android-sdk-opengl:13.4.1-step060e`; +- `AAR_BUILD_NOT_EXECUTED_BY_CODEX`。 +## 12. 完成报告格式 + +结束时按以下必做项逐项报告:`已完成 / 未完成 / 无法验证`,并说明未完成或无法验证原因: + +- Core `hasStyleSource` 查询链; +- Core `hasStyleLayer` 查询链; +- Bridge attach 改用 Core 真值; +- 移除危险的 `style.getLayers()` 诊断枚举; +- 增加 `styleUri` 诊断; +- 成功路径与 WeatherRuntime 控制语义保持不变; +- `13.4.1-step060e` 版本接线; +- Linux 精确验证; +- 文档更新; +- Git commit/push; +- AAR 未由 Codex 构建; +- 未进入 STEP-061。 + +不得用“基本完成”“应该可以”等模糊表述替代状态。 +## 13. 停止条件与后续人工链路 + +Codex 成功 push 后立即停止,不得自行构建 AAR,不得进入 Win11 或 Android 运行验证。 + +后续固定人工/ChatGPT 链路: + +1. ChatGPT 审核远端实际代码; +2. 用户在 Linux 手工构建 `13.4.1-step060e` OpenGL Release AAR并发布 Maven Local; +3. 记录 Linux AAR SHA256; +4. Win11 从 Git pull 最新 RN Test Host 源码; +5. Win11 通过 SCP 从 `root@192.168.200.184` 拉取 AAR; +6. Win11 计算 SHA256,必须与 Linux 完全一致; +7. 更新 Win11 Maven Local; +8. 执行 Bridge 与 MLRN 两条 `dependencyInsight`,必须共同解析到 `13.4.1-step060e`; +9. 用户手工构建 APK; +10. 再进行 Android attach/runtime 验证。 + +在上述 Android 结果出来前,不得宣布 STEP-060 CLOSED,也不得进入 STEP-061。 diff --git a/docs/40_任务/STEP-060F_WeatherColorOpacity与Android收尾修复_Codex任务_v1.md b/docs/40_任务/STEP-060F_WeatherColorOpacity与Android收尾修复_Codex任务_v1.md new file mode 100644 index 000000000..00e974431 --- /dev/null +++ b/docs/40_任务/STEP-060F_WeatherColorOpacity与Android收尾修复_Codex任务_v1.md @@ -0,0 +1,297 @@ +# STEP-060F WeatherColor Opacity 与 Android 收尾修复 — Codex任务 v1 + +## 1. 任务定位 + +当前仍属于 **STEP-060**,不得进入 STEP-061。 + +STEP-060E 已完成 CoreOnly layer 存在性检查修复,并已完成 Android 实际验证: + +- `WeatherRuntime attached` 成功; +- `reactTag` 为有效 Fabric MLRNMapView tag; +- `WEATHER_OBJECTS_NOT_READY` 已消失; +- Phase A 的 enable / weather visible / particle visible / FPS 30 均返回 `ACCEPTED`; +- 无 Binary Field 时 runtime 保持 `WAITING_FOR_FIELDS / PAUSE / ...` 符合预期。 + +本任务是 STEP-060 最后一次源码收尾,集中处理: + +1. 修复 `WeatherRuntime.setWeatherColorOpacity()` Android 真 bug; +2. 固化 Test Host Metro 8091 的 Android 8081 reverse 兼容; +3. 保持已提交的 Test Host 横屏锁定; +4. 准备 `13.4.1-step060f`,由用户后续一次性手工构建 AAR + APK 并做最终验证。 + +## 2. 开工基线 + +仓库:`/mnt/sda1/MapLibreParticleLab` +分支:`particle/main` +开工目标 HEAD / remote HEAD:`204102f26049049151cd5471fada804d471e15a9` + +开工前必须: + +- `git pull --ff-only origin particle/main`; +- 确认 HEAD / remote HEAD 一致; +- 检查 working tree,不得清理或提交与本任务无关的历史文件; +- 保留 STEP-060E `hasStyleSource/hasStyleLayer` Core 真值链; +- 保留 `app.json` 的 `"orientation": "landscape"`; +- 不得回退 Win11 已验证的 RN 0.85.2 / React 19.2.3 / Expo 56 / MLRN 11.3.6 基线。 + +## 3. 已确认 Android 实测证据 + +ChatGPT 已通过 CARBON7i + ADB 对 `13.4.1-step060e` 实测,以下为已确认输入,不要重新猜根因: + +- `color stops` → `ACCEPTED` +- `particle RGBA` → `ACCEPTED` +- `trail RGBA` → `ACCEPTED` +- `particle count` → `ACCEPTED` +- `line width` → `ACCEPTED` +- `particle length` → `ACCEPTED` +- `particle opacity` → `ACCEPTED` +- `trail opacity` → `ACCEPTED` +- `trail fade` → `ACCEPTED` +- `weather opacity` → **`REJECTED`** + +时间线相关控制在尚无 T0/T1 field window 时返回: + +- PLAY / PAUSE → `REJECTED` +- HALF / NORMAL / DOUBLE → `REJECTED` +- seek now → `REJECTED` +- Native warning:`ParticleTimeline: event=REJECTED reason=TIMELINE_NOT_READY` + +这是当前无 field 的正常 fail-closed 行为,本任务不得为了让这些命令在 `WAITING_FOR_FIELDS` 下返回 ACCEPTED 而修改 Timeline/Playback 语义。 + +真正 blocker 为设备两次稳定复现: + +`ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_PROPERTY` + +对应 UI:`weather opacity: REJECTED`。 + +## 4. 已确认源码根因 + +当前 `WeatherRuntime.setWeatherColorOpacity(float)` 走: + +`MapLibreMap.setStyleLayerFloatProperty(weatherColorLayerId, "weather-color-opacity", opacity)`。 + +但 `NativeMapView::setStyleLayerFloatProperty(...)` 只允许 `raster-particle-*` 属性,并把目标 layer `dynamic_cast` 为 `RasterParticleLayer`。 + +因此 `weather-color-opacity` 必然被 `UNSUPPORTED_PROPERTY` 拒绝。 + +Core `WeatherColorLayer` 已有正式 typed API: + +- `float getOpacity() const` +- `void setOpacity(float)` +- finite value;Core 内部 clamp 到 `[0,1]` + +Android 当前也已经有独立 `setWeatherColorStops(...)` Java → JNI → Core 路径,并直接 `dynamic_cast`。本任务应沿该模式做最小修正,不得扩大通用 particle property setter。 + +## 5. 必做实现 A:专用 WeatherColor opacity 路径 + +新增窄范围 typed Host API,建议稳定命名: + +`MapLibreMap.setWeatherColorOpacity(String layerId, float opacity): boolean` + +必须贯通至少: + +- `NativeMap.java` +- `NativeMapView.java` +- `MapLibreMap.java` +- `native_map_view.hpp` +- `native_map_view.cpp` +- JNI method registration + +C++ 必须: + +- 直接从 Core Style 按 id 取 layer; +- `dynamic_cast`; +- 非 WeatherColorLayer / layer 不存在 / 非 finite 输入时返回 false; +- 成功时调用 `WeatherColorLayer::setOpacity(opacity)` 并返回 true; +- 保持 `WeatherColorLayer` CoreOnly,不创建 Java Layer peer; +- 日志不得再复用误导性的 `ParticleGPU ... UNSUPPORTED_PROPERTY` 作为 WeatherColor 正常路径。 + +修改 `WeatherRuntime.setWeatherColorOpacity(float)`: + +- 不再调用 `setStyleLayerFloatProperty(..., "weather-color-opacity", ...)`; +- 改调新的 typed `setWeatherColorOpacity(...)`; +- `WeatherRuntime.CommandResult` 契约保持 `ACCEPTED / REJECTED`; +- 不在 Bridge/JS 增加第二套状态或兼容分支。 + +不得把 `weather-color-opacity` 加进现有 `setStyleLayerFloatProperty()` 的 particle whitelist 后再混合处理两个 layer 类型;应保持 WeatherColor 与 RasterParticle 两条 typed 路径分离。 + +## 6. 必做实现 B:Metro 8091 启动兼容 + +Android 实测确认:Test Host Metro 固定监听 CARBON7i `8091`,但当前 Debug APK 的 RN dev-server 探测仍从设备 `localhost:8081` 发起。 + +成功运行时需要同时存在: + +```text +adb -s 192.168.1.146 reverse tcp:8091 tcp:8091 +adb -s 192.168.1.146 reverse tcp:8081 tcp:8091 +``` + +仓库当前未跟踪 Win11 legacy 的 `scripts/05-install-and-run.ps1`。本任务应先检查现有仓库 README / 测试文档 / scripts 状态,再以最小可维护方式把该启动规则纳入 Git 受控来源。 +Win11 已验证 legacy 脚本现状为: + +- `$device = '192.168.1.146'` +- `adb -s $device get-state` +- `adb -s $device reverse tcp:8091 tcp:8091` +- `adb -s $device install -r $apk` +- force-stop + `org.maplibre.particle.rntesthost/.MainActivity` 启动 + +修正时只需在 install 前增加: + +`adb -s $device reverse tcp:8081 tcp:8091` + +不得删除原 `8091 -> 8091` reverse,不得改设备地址、package、activity 或 Metro 端口。 + +如果决定把脚本正式纳入仓库,则创建 `integration/react-native/MapLibreParticleRNTestHost/scripts/05-install-and-run.ps1`,内容以上述已验证行为为基线;同时更新中文 README / Android 人工验收文档说明双 reverse。不得顺手引入整套无关 Win11 脚本。 + +## 7. 必做实现 C:横屏配置保持 + +`204102f26049049151cd5471fada804d471e15a9` 已把: + +`app.json -> "orientation": "landscape"` + +提交到 `particle/main`。 + +本任务只需保护该配置,并在文档中明确下一次 Android prebuild/build 后应生成 `MainActivity android:screenOrientation="landscape"`。不得回退 portrait;Codex 不需要为了横屏运行 Android prebuild 或 APK build。 + +## 8. Maven / AAR 临时版本 + +Native Android API/JNI 会变化,因此必须使用新 coordinate: + +`org.maplibre.gl:android-sdk-opengl:13.4.1-step060f` + +同步更新至少: + +- `platform/android/VERSION` +- `integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/build.gradle` +- `integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js` +- README 中当前 temporary native version 描述 + +不得复用 `13.4.1-step060e` 发布内容不同的 artifact。 + +Codex **不得构建 AAR,不得 publish Maven Local,不得构建 APK,不得运行 ADB/安装/启动 App**。 + +完成报告必须明确:`AAR_BUILD_NOT_EXECUTED_BY_CODEX`。 + +## 9. Linux 验证边界 + +只执行本轮必要的静态/精确非 Android-runtime 验证: + +- JNI Java/C++ 声明、实现、registration 签名一致性; +- 新 WeatherColor opacity 路径最终直达 `WeatherColorLayer::setOpacity()`; +- `WeatherRuntime.setWeatherColorOpacity()` 不再走 particle `setStyleLayerFloatProperty()`; +- 保持 particle float setter whitelist 与 `RasterParticleLayer` 语义不扩大; +- 检查 Metro 双 reverse 脚本/文档内容; +- 检查 `app.json` 仍为 landscape; +- 可执行 Linux OpenGL/C++ 精确编译或已有等价最小验证; +- 不执行 Android Gradle build/compile task; +- 不执行 Jest; +- `git diff --check`; +- 最终 diff 不得混入无关文件。 + +编译输出遵循全局 `$build-output-throttle`。 + +## 10. 明确禁止 + +本任务不得: + +- 修改 `RasterParticleLayerFactory` / `WeatherColorLayerFactory` 的 CoreOnly 注册; +- 新建 WeatherColor Java Layer peer; +- 把 WeatherColor opacity 混进 particle property whitelist; +- 改变 Timeline 在无 field 时 `TIMELINE_NOT_READY` 的 fail-closed 行为; +- 实现或提前提交 Binary U/V Field Bridge; +- 进入 STEP-061; +- 修改 NavSea 正式代码; +- 启动 Metro、Android emulator、ADB 或设备验证; +- 为了验证横屏单独构建 APK; +- 清理历史 build/node_modules/untracked 文件。 + +## 11. 文档要求 +完成后至少: + +- 新增 `docs/报告/STEP-060F_WeatherColorOpacity与Android收尾修复_Codex完成报告_v1.md`; +- 生成 `docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v2.md`,保留 v1 作为历史; +- 更新 `docs/60_测试/STEP-060_RNTestHost_Android人工编译与验收_v1.md` 或递增版本,加入:双 reverse、横屏、weather opacity 最终验证; +- 更新 `粒子开发记录.md`。 + +文档必须记录: + +- STEP-060E Android attach 已 PASS; +- `WEATHER_OBJECTS_NOT_READY` 已解决; +- `TIMELINE_NOT_READY` 在无 Binary Field 时是预期行为; +- STEP-060F blocker 是 WeatherColor opacity 错走 particle-only setter; +- 修复后临时版本为 `13.4.1-step060f`; +- Android 最终 AAR/APK/设备复验仍等待用户; +- 当前状态必须是 `IMPLEMENTED / WAITING_ANDROID_VALIDATION`,不得提前写 CLOSED / PASS。 + +## 12. Git 收尾 + +只提交本任务相关源码、版本接线、脚本/文档和本任务文件。 + +必须: + +- `git diff --check` PASS; +- commit 到当前 `particle/main`; +- push `origin/particle/main`; +- push 失败时不得声称完成; +- 不得提交与本任务无关的本地改动。 + +完成报告必须记录: + +- Starting HEAD; +- source commit SHA; +- Final HEAD; +- Remote HEAD; +- commit message; +- push 结果; +- remote branch; +- 预期 Maven coordinate:`org.maplibre.gl:android-sdk-opengl:13.4.1-step060f`; +- `AAR_BUILD_NOT_EXECUTED_BY_CODEX`。 + +## 13. Codex 最终报告清单 + +结束时逐项报告 `已完成 / 未完成 / 无法验证`: + +- WeatherColor typed opacity Java API; +- NativeMap / NativeMapView JNI 链; +- C++ 直达 `WeatherColorLayer::setOpacity()`; +- `WeatherRuntime.setWeatherColorOpacity()` 切换到 typed 路径; +- particle property setter 未扩大; +- Metro `8081 -> 8091` 与 `8091 -> 8091` 双 reverse 纳入受控来源; +- landscape 配置保持; +- `13.4.1-step060f` 版本接线; +- Linux 精确验证; +- 中文文档更新; +- Git commit/push; +- AAR/APK/ADB 未由 Codex 执行; +- 未进入 STEP-061。 + +## 14. 停止条件与后续人工链路 + +Codex 成功 push 后立即停止,不得自行进入 Android artifact/runtime 阶段。 + +后续固定顺序: + +1. ChatGPT 审核 `particle/main` 远端实际源码与 diff; +2. 审核通过后,用户在 Linux 手工构建并 publish `13.4.1-step060f` OpenGL Release AAR; +3. Linux 记录 build AAR 与 Maven Local AAR SHA256,必须一致; +4. CARBON7i Git pull 最新 Test Host 源码; +5. AAR 临时复制到 `/mnt/nas/pbf/`,Win11 使用 `curl.exe -L` 从 `https://pbf.jpcn.ccwu.cc/` 下载; +6. 下载 URL 必须使用每次不同的 `?v=` 防止 CDN cache; +7. Win11 SHA256 必须与 Linux 完全一致; +8. 安装 Win11 Maven Local; +9. Bridge 与 MLRN 两条 `dependencyInsight` 必须共同解析到 `13.4.1-step060f`; +10. 用户手工 `assembleDebug`; +11. 使用修正后的启动流程安装/启动; +12. Android 最终验收。 + +最终 Android hard gate: + +- App 锁定横屏; +- `WeatherRuntime attached`; +- `WEATHER_OBJECTS_NOT_READY` 不出现; +- `weather opacity: ACCEPTED`; +- 不再出现 `STYLE_PROPERTY_SET result=UNSUPPORTED_PROPERTY`; +- Phase A 基础命令继续工作; +- 无 field 时 `TIMELINE_NOT_READY` 仍允许存在。 + +只有上述最终 Android 结果通过后,才允许把 **STEP-060 标记为 CLOSED / PASS**,然后进入 **STEP-061 Binary U/V Field Transport**。 diff --git a/docs/40_任务/STEP-061_Binary_UV_Field_Transport_Codex任务_v1.md b/docs/40_任务/STEP-061_Binary_UV_Field_Transport_Codex任务_v1.md new file mode 100644 index 000000000..433a9b321 --- /dev/null +++ b/docs/40_任务/STEP-061_Binary_UV_Field_Transport_Codex任务_v1.md @@ -0,0 +1,173 @@ +# STEP-061 Binary U/V Field Transport Codex任务 v1 + +## 1. 任务状态与目标 + +- 当前正式分支:`particle/main`。 +- 本任务开始前必须确认本地 HEAD 与 `origin/particle/main` 一致。 +- STEP-060 已 `CLOSED / PASS`,当前正式进入 STEP-061。 +- 目标:打通 RN Test Host → WeatherRuntime → Android Java/JNI → RasterArraySource/Core 的正式 Binary U/V Field 提交链,并在 RN Host 中得到可验证的 Timeline / Runtime Field Window 证据。 +- 本任务只实现 STEP-061 源码与静态验证;不要执行最终 AAR 发布、Windows Maven Local、APK、ADB 或 Android 实机验收,这些由任务完成后的端到端流程执行。 + +## 2. 开工前必读 + +必须先读取并遵守: + +1. `AGENTS.md` +2. `PARTICLE_BASELINE.md` +3. `docs/00_项目规范/` 全部当前规范 +4. `docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v3.md` +5. `docs/报告/STEP-060F_Android最终验收报告_v1.md` +6. `docs/40_任务/STEP-053_Windy-style_Canonical_Field与Host_MapLibre_Submission_Contract_Codex任务_v1.md` +7. `docs/50_Step记录/STEP-053_Windy-style_Canonical_Field与Host_MapLibre_Submission_Contract_v1.md` +8. `docs/报告/STEP-053_Windy-style_Field与Host_Submission_Codex完成报告_v2.md` +9. `粒子开发记录.md` 最新相关记录 +## 3. 已调查并冻结的实际基线 + +当前源码已经存在以下正式链路,禁止重复造第二套 Core ingress: + +- `WeatherRuntime.submitWindField(String metadataJson, byte[] payload)` +- `MapLibreMap.submitRasterArraySourceHostField(...)` +- `NativeMapView` / JNI `jbyteArray` +- `RasterArraySource::submitHostField(metadata, payload)` +- `LocalParticleFieldFeed::load(...)` +- `ParticleFieldDataAdapter::decode(...)` + +Canonical Field Contract 继续沿用 STEP-053,不重新定义: + +- schema:`maplibre-particle-field/v1` +- layout:`interleaved-uv` +- encoding:`float32-le` +- origin:north-west +- storage:row-major +- U:eastward m/s +- V:northward m/s +- payload byteLength 必须与 width × height × 2 × sizeof(float) 一致。 +- `generation`、`validTime` 必须非零并满足 Timeline 单调性。 + +RN Test Host 当前为 React Native `0.85.2`,`@react-native/codegen 0.85.3`,Expo `56.0.4`;当前 Codegen Spec 不提供可直接映射为 Kotlin `byte[]` 的 ArrayBuffer/Uint8Array 参数类型。Expo FileSystem `56.0.11` 已存在并支持 `File.write(string | Uint8Array)`。 +## 4. 本阶段正式 Binary Transport 设计 + +本阶段固定采用 **binary file-reference transport**: + +1. JS/TS 用 `ArrayBuffer + DataView` 或等价方式构造 canonical U/V bytes,并显式以 little-endian 写入 float32。 +2. JS/TS 用 Expo FileSystem `File.write(Uint8Array)` 把原始 bytes 写入 App 本地临时文件。 +3. TurboModule 公共调用只传递 `metadataJson + payloadFileUri/path`,不把 U/V 数组塞进 JSON。 +4. Android bridge 从本地文件引用读取原始 `byte[]`,然后调用既有 `WeatherRuntime.submitWindField(metadataJson, byte[])`。 +5. Core 仍由既有 `LocalParticleFieldFeed` / `ParticleFieldDataAdapter` 做最终 schema、维度、bounds、layout、encoding、byteLength、generation、validTime 等验证并 fail-closed。 + +该设计是正式 Binary U/V transport,不是临时编码兼容层。禁止: + +- `number[]` / `Array` 跨 RN bridge 传 U/V payload; +- 把 payload 放入 JSON; +- base64; +- hex/string 编码; +- provider/network/vendor 下载逻辑进入 MapLibre Native/Core; +- 新建第二套 RasterArray/Timeline/Field Adapter; +- 绕过 `WeatherRuntime.submitWindField(...)` 直接写 Core 内部状态。 + +本阶段不要求 JSI zero-copy;如果未来需要纯内存/zero-copy buffer transport,应另开性能 Step,不在这里叠加复杂度。 +## 5. 必做实现 + +### 5.1 RN bridge API + +在现有 `weather-runtime-bridge` 上最小扩展,不新建第二个模块: + +- 增加一个稳定的 Binary Field 提交 API,参数语义为 `metadataJson + payloadFileUri/path`。 +- 返回值必须能让 RN Host 看到 `WeatherRuntime.FieldSubmitResult` 的结构化结果名称或等价稳定字符串。 +- 可以同时增加一个最小 `clearWindFields()`/reset API,若它对重复验收和 Fast Refresh 后清空 Timeline 是必要的;必须继续调用既有 `WeatherRuntime.clearWindFields()`,不得自己管理 Timeline。 +- Kotlin 侧只负责:解析本地引用、读取原始 bytes、做必要的文件可读性/大小保护、调用 `WeatherRuntime.submitWindField()`、归一化返回值。 +- 文件读取失败必须 fail-closed,不能提交空 payload,也不能伪造成功。 + +### 5.2 RN Test Host canonical fixture + +在 Test Host 内生成确定性、很小但有效的至少两帧 canonical WIND Field: + +- T0 / T1 grid 的 width、height、bounds、layout、encoding、noData 完全兼容。 +- T1 的 `validTime` 与 `generation` 严格大于 T0。 +- payload 使用 `DataView.setFloat32(offset, value, true)` 或等价明确 little-endian 的方法写 U/V。 +- 写成原始二进制临时文件;禁止 base64 或数组 JSON。 +- metadata 必须包含 Core 当前真实 parser 要求的 schema、fieldId、fieldType、displayDomain、generation、validTime、grid、bounds、vector、payload.byteLength 等字段。 +- Host 不得计算或持有 T0/T1/MIX Runtime 状态;它只生产并提交帧,窗口选择继续由 Core 管理。 +### 5.3 正向与负向证据 + +RN Host 必须提供无需猜测的验收状态,至少包括: + +- Binary Transport:READY / ERROR; +- payload 形式:明确显示 `FILE_REF / FLOAT32_LE / INTERLEAVED_UV` 或等价语义; +- Invalid Probe:提交一个确定性非法 payload(优先 byteLength/实际 bytes 不匹配),必须得到 Core 的拒绝结果; +- T0 Submit Result:应为 `ACCEPTED_PENDING_INITIAL_WINDOW`; +- T1 Submit Result:应为 `INITIAL_WINDOW_READY`; +- Runtime Field Window:最终不再是 `TIMELINE_NOT_READY`,应能显示来自 Native/Core 的 T0/T1/MIX/generation/time 等已有 runtime window 证据。 + +负向 probe 只能验证既有 Core fail-closed,不得为了让 probe 通过去放宽 Core 校验。 + +### 5.4 依赖与工程 + +- `expo-file-system` 当前实际安装版本为 `56.0.11`;如果源码直接 import,必须把它作为 Test Host 的明确直接依赖并保持与当前 Expo 56 兼容,不升级整个 Expo/RN 栈。 +- 只修改完成本任务真正必要的 RN Test Host / bridge 文件。 +- 默认不修改 MapLibre Core;只有调查证明现有 STEP-053/057 WeatherRuntime contract 存在真实阻塞缺陷时才能做最小修正,并在完成报告中单独说明原因和影响。 +- 不进入 NavSea,不修改任何 NavSea 文件。 +## 6. 允许修改范围 + +优先允许: + +- `integration/react-native/MapLibreParticleRNTestHost/App.tsx` +- `integration/react-native/MapLibreParticleRNTestHost/package.json` +- `integration/react-native/MapLibreParticleRNTestHost/package-lock.json` +- `integration/react-native/MapLibreParticleRNTestHost/README.md` +- `integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/src/**` +- `integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/**` 中源码和必要配置;禁止提交 `build/` 产物。 +- 本任务要求的中文完成报告。 + +默认禁止: + +- provider/network/vendor/Weather Service/HTTP/download/cache/prefetch 业务代码进入 Native/Core; +- 修改 NavSea; +- 修改与 STEP-061 无关的 UI、粒子视觉参数、地图行为; +- 大范围重构 STEP-053~060 已冻结链路; +- 提交现有历史 untracked 文件、build 目录、临时日志、AAR/APK。 + +若必须超出允许范围,先停止并在完成报告中标为阻塞,不要自行扩大任务。 +## 7. 验证要求 + +Codex 完成源码后必须执行并记录: + +1. `npm run typecheck`(RN Test Host)。 +2. 当前 bridge Codegen Spec 的必要生成/静态检查;只做验证,不提交生成的 build 产物。 +3. 对修改文件执行必要的语法/静态检查。 +4. `git diff --check`。 +5. 明确检索并证明正式提交链中不存在以 `number[]`、base64、payload JSON 传输 U/V 的实现。 + +未经用户明确要求,本任务**不加入 Jest,不新增 Jest 测试,不运行 Jest**。 + +本阶段不要运行: + +- `make apackage`; +- 最终 AAR Maven publish; +- Windows Gradle APK build; +- ADB 安装或实机测试。 + +AAR 编译与端到端 Android 验收将在 Codex 源码审查通过后,于同一个 `particle-codex:step061` tmux window 中继续执行。 + +编译/Codegen 等 noisy 命令遵循全局 `$build-output-throttle`。 +## 8. 必做输出 + +完成时必须实际生成: + +1. STEP-061 所需源码修改。 +2. 必要的 Test Host README/bridge contract 说明,明确 binary file-reference transport、canonical bytes、调用链和限制。 +3. `docs/报告/STEP-061_Binary_UV_Field_Transport_Codex完成报告_v1.md`。 + +完成报告至少写明: + +- 调查后的真实实现路径; +- 修改文件清单; +- RN binary file-reference transport 如何避免 number[] / base64; +- Kotlin 如何把原始 bytes 交给 `WeatherRuntime.submitWindField()`; +- invalid probe、T0、T1、Runtime Field Window 的预期结果; +- 实际执行的静态验证及结果; +- 未执行的 AAR / Windows / Android 实机项目; +- 是否修改 Core;若修改,必须逐项说明必要性; +- `git status --short` 与当前未提交范围摘要。 + +不要 commit,不要 push。完成后停在工作区,等待 ChatGPT 通过 RDC 审查真实 diff。 diff --git a/docs/40_任务/STEP-061_新窗口交接与端到端执行流程_v1.md b/docs/40_任务/STEP-061_新窗口交接与端到端执行流程_v1.md new file mode 100644 index 000000000..1c31ad141 --- /dev/null +++ b/docs/40_任务/STEP-061_新窗口交接与端到端执行流程_v1.md @@ -0,0 +1,272 @@ +# STEP-061 新窗口交接与端到端执行流程 v1 + +## 1. 文档目的 + +本文件用于在新的 ChatGPT 会话窗口中无缝继续 MapLibreParticleLab / 粒子系统开发。 +新窗口不得依赖旧聊天记录猜测状态;应先读取本文件,再读取仓库中的实际基线与最新 Step 记录。 + +本文件同时固定从“写 STEP-061 Codex 任务 MD”到“Codex 实现、源码审查、AAR、Win11 Maven、APK、Android 实机验收、Git 收尾”的完整协作流程。 + +## 2. 当前冻结基线 + +- Linux repo:`/mnt/sda1/MapLibreParticleLab` +- branch:`particle/main` +- STEP-060 closeout baseline commit:`3d34ec7628d6b429d223a9022f05a9192a0a8d4d` +- 新窗口开始时必须实时查询 `particle/main` 的 local HEAD / remote HEAD;不得把本文件中的历史 commit 当作当前 HEAD +- STEP-060:`CLOSED / PASS` +- STEP-060 最终文档:`docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v3.md` +- STEP-060F Android 最终验收:`docs/报告/STEP-060F_Android最终验收报告_v1.md` +- 当前最后验证 AAR:`org.maplibre.gl:android-sdk-opengl:13.4.1-step060f` +- 最后验证 AAR SHA256:`53dfcd43f3479ae43a3c0fd91c10ccbfec7cf1c84ae22ae3435c94393806da6d` + +STEP-060 已实测:横屏、双 Metro reverse、WeatherRuntime attach、CoreOnly source/layer 真值链、WeatherColor opacity typed setter 均通过。 +无 Binary Field 时 `TIMELINE_NOT_READY` 仍是预期 fail-closed。 +## 3. 新窗口第一步必须读取 + +依次读取并以实际仓库内容为准: + +1. `AGENTS.md` +2. `PARTICLE_BASELINE.md` +3. `docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v3.md` +4. `docs/报告/STEP-060F_Android最终验收报告_v1.md` +5. `粒子开发记录.md` +6. STEP-053 Host → MapLibre submission contract 相关任务/记录 +7. 当前与 Binary U/V / Canonical Field / Runtime Field Window 相关源码 + +若本文件中的 HEAD 已落后于远端,先以 `origin/particle/main` 的最新实际代码重新确认差异,不得用旧聊天内容覆盖新提交。 + +## 4. STEP-061 架构边界 + +STEP-061 才允许开始 Binary U/V Field Transport。 +正式数据获取、Weather Service、HTTP/provider、下载、重试、缓存、预取和业务调度属于最终宿主 React Native NavSea 的 JS/TS 层。 +MapLibre Native/C++ 不得引入 provider、network、vendor 或天气业务获取逻辑。 + +底层 field 采用 Windy-style 二维规则网格向量场:metadata + U/V payload。 +宿主通过稳定的 Host → MapLibre 接口提交已取得并规范化的数据;不得把大 field 作为 `number[]` JSON 数组或 base64 方案长期传输。 +C++ 负责接收、校验、fail-closed、ParticleFieldSnapshot、Timeline、Runtime Field Window、GPU simulation/rendering。 +WeatherColorLayer 与 RasterParticleLayer 继续分离,现有 CoreOnly 架构不得倒退。 +## 5. 固定 tmux / Codex 工作流 + +所有粒子系统 Codex 操作统一使用固定 session:`particle-codex`。 +不要为新任务创建第二个 tmux session;每个独立 Step 新建一个 window。 + +当前历史窗口: +- `0: manual` +- `1: step060f` + +STEP-061 使用新 window,例如:`step061`。 +建议命令: + +```bash +tmux new-window -t particle-codex -n step061 -c /mnt/sda1/MapLibreParticleLab +``` + +用户查看: + +```bash +ssh root@192.168.200.184 +tmux attach -t particle-codex +``` + +只读查看:`tmux attach -r -t particle-codex`。 +ChatGPT/RDC 可用 `tmux capture-pane -pt particle-codex:step061` 读取执行状态。 +SSH 断开不影响 tmux 中的 Codex、编译或 shell 任务。 +## 6. Codex CLI 固定方式 + +当前可用 Codex:`codex-cli 0.153.4`。 +已验证启动方式: + +```bash +codex -C /mnt/sda1/MapLibreParticleLab \ + -m gpt-5.6-terra \ + -s workspace-write \ + -a never \ + --no-alt-screen \ + "完整阅读并执行 ;严格遵守任务范围。" +``` + +默认使用 medium reasoning;需要改变模型/权限时必须先明确说明原因。 +`workspace-write` 可以改源码,但 Codex sandbox 已实测会把 `.git` 视为只读,无法可靠创建 `.git/index.lock` 或写 `FETCH_HEAD`。 +因此新固定流程是: + +- Codex:实现源码、允许范围内的静态/精确验证、中文文档和完成报告; +- Codex 不得伪称 commit/push;报告写 `GIT_CLOSEOUT_BY_RDC_AFTER_REVIEW`; +- ChatGPT:读取真实 diff 并审查; +- 审查通过后,由 ChatGPT 通过 RDC 执行精准 `git add`、commit、push; +- 不得把历史 untracked 文件顺手加入提交。 + +除非用户明确要求,不写/跑 Jest。Android APK 编译仍由用户手工执行;AAR 编译可按本文件授权由 ChatGPT/RDC 在 tmux 中执行。 +## 7. AAR 构建固定流程 + +只有 Codex 源码完成、ChatGPT 审查通过、source commit 已 push 后,才进入 AAR。 +AAR 在 STEP 对应的同一个 tmux window 中执行,不新建第二个编译 window。 + +**JOBS 必须固定为 1。** + +```bash +cd /mnt/sda1/MapLibreParticleLab/platform/android +JOBS=1 RENDERER=opengl BUILDTYPE=Release make apackage +``` + +必须确认输出含 `--max-workers=1`,并最终 `BUILD SUCCESSFUL`。 +随后在同一 window: + +```bash +./gradlew :MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain +``` + +再比较 build output 与 Maven Local AAR: + +```bash +sha256sum \ + MapLibreAndroid/build/outputs/aar/MapLibreAndroid-opengl-release.aar \ + /root/.m2/repository/org/maplibre/gl/android-sdk-opengl//android-sdk-opengl-.aar +``` + +两个 SHA256 必须完全一致,否则停止后续 Win11 流程。 +## 8. AAR → Win11 传输固定流程 + +Linux 将新版本 AAR/POM/module 临时复制到 `/mnt/nas/pbf`,该目录对应公网根目录 `https://pbf.jpcn.ccwu.cc/`。 +只作为临时交接,Win11 验证完成后立即删除公开文件。 + +Win11 固定设备:CARBON7i。 +下载目录:`D:\React\MapLibreParticleAAR\\`。 +必须使用 `curl.exe -L`,并在 URL 后加每次不同的 `?v=` 防止 CDN/cache 命中旧产物,例如: + +```powershell +curl.exe -L --retry 5 --retry-delay 2 ` + -o "D:\React\MapLibreParticleAAR\\android-sdk-opengl-.aar" ` + "https://pbf.jpcn.ccwu.cc/android-sdk-opengl-.aar?v=" +``` + +POM/module 同样使用新 `?v=` 下载。 +然后: + +```powershell +Get-FileHash "D:\React\MapLibreParticleAAR\\android-sdk-opengl-.aar" -Algorithm SHA256 +``` + +Win11 SHA256 必须与 Linux 一致。 +一致后复制三件套到: +`C:\Users\taibe\.m2\repository\org\maplibre\gl\android-sdk-opengl\\`。 +再次核对 Maven Local AAR SHA256。 +## 9. Win11 源码同步与 Test Host + +Git sparse 工作副本: +`D:\React\MapLibreParticleLab-Win11-SYNC\integration\react-native\MapLibreParticleRNTestHost` + +当前实际运行 Test Host: +`D:\React\MapLibreParticleRNTestHost-Win11-STEP060-20260907` + +固定顺序: + +1. Win11 sparse repo `git pull --ff-only origin particle/main`; +2. 以 Git 工作副本为来源同步本 Step 受控 Test Host 源文件到 legacy runtime 目录; +3. 不覆盖 legacy 目录中已验证且本 Step 无关的生成环境; +4. 若新 Step 修改 Expo plugin/nativeVersion,则同步更新当前生成 `android/build.gradle` 的对应临时版本; +5. 保持 `app.json` landscape,并确认生成 Manifest 仍是 `android:screenOrientation="landscape"`; +6. `05-install-and-run.ps1` 必须保留两条 reverse:`8091 -> 8091` 与 `8081 -> 8091`。 + +Metro 固定 `8091`,不要启动第二个 Metro,也不要改成 8081。 + +Win11 Gradle 基线: +`C:\Users\taibe\.gradle\wrapper\dists\gradle-8.14.3-bin\cv11ve7ro1n3o1j4so8xd9n66\gradle-8.14.3\bin\gradle.bat` + +依赖验收必须分别检查 Bridge 与 MLRN,两者都只能解析到当前 Step 的 ``。 +建议在 legacy Test Host `android` 目录执行: + +```powershell +$g = 'C:\Users\taibe\.gradle\wrapper\dists\gradle-8.14.3-bin\cv11ve7ro1n3o1j4so8xd9n66\gradle-8.14.3\bin\gradle.bat' +& $g :maplibre-particle_weather-runtime-bridge:dependencyInsight --configuration debugRuntimeClasspath --dependency org.maplibre.gl:android-sdk-opengl --console=plain --no-daemon --no-watch-fs +& $g :maplibre_maplibre-react-native:dependencyInsight --configuration debugRuntimeClasspath --dependency org.maplibre.gl:android-sdk-opengl --console=plain --no-daemon --no-watch-fs +``` + +两条输出必须共同出现 `org.maplibre.gl:android-sdk-opengl:`,且均 `BUILD SUCCESSFUL`。 + +## 10. APK 编译边界 + +Android APK 编译默认仍由用户手工执行;ChatGPT/RDC 不自行触发 `assembleDebug`,除非用户在当次任务中再次明确授权。 +固定命令: + +```powershell +cd D:\React\MapLibreParticleRNTestHost-Win11-STEP060-20260907\android +C:\Users\taibe\.gradle\wrapper\dists\gradle-8.14.3-bin\cv11ve7ro1n3o1j4so8xd9n66\gradle-8.14.3\bin\gradle.bat ` + :app:assembleDebug ` + --console=plain ` + --no-daemon ` + --no-watch-fs ` + -PreactNativeArchitectures=x86_64 +``` + +只有用户返回 `BUILD SUCCESSFUL` 后才进入安装和设备验收。 +## 11. Android 安装与运行时验收 + +Android 测试设备:`192.168.1.146`。 +所有 ADB 命令必须显式带:`adb -s 192.168.1.146 ...`。 + +优先使用 legacy Test Host: +`D:\React\MapLibreParticleRNTestHost-Win11-STEP060-20260907\scripts\05-install-and-run.ps1` + +该脚本负责:设备检查、`8091 -> 8091`、`8081 -> 8091`、install -r、force-stop、启动 `.MainActivity`。 +安装后先确认: + +- Activity 为 `SCREEN_ORIENTATION_LANDSCAPE`; +- Metro bundle 正常; +- `WeatherRuntime attached`; +- 无 `WEATHER_OBJECTS_NOT_READY`、JNI crash、SIGSEGV、FATAL EXCEPTION。 + +STEP-061 的 Binary U/V 具体验收必须由正式 STEP-061 任务 MD 明确,包括提交 payload 后的 Runtime/Field Window/Timeline 预期状态。 +原则上必须证明 binary ingress 实际进入 Native/Core,而不是只验证 JS 参数接受。 +若任务要求完整控制 UI,可使用现有 `07-enable-full-validation-ui.ps1`,测试完成后必须执行 `08-restore-baseline-ui.ps1`。 + +运行时结果应同时读取 UI result、runtime snapshot 与当前 App PID 的 logcat;不得只凭屏幕视觉判断。 +测试失败时先判断是 Host/Bridge、JNI、Core field validation、Timeline 还是 renderer,不得在 NavSea 层增加兼容逻辑掩盖上游问题。 +## 12. 临时公开文件清理 + +Win11 AAR/POM/module 下载和 SHA 验证完成后,Linux 必须删除 `/mnt/nas/pbf` 中本 Step 临时公开的三件套。 +不得把 AAR 放进 Git。 + +## 13. 每个 STEP 的完整顺序 + +新窗口必须按下列顺序推进,不跳步: + +1. 读取本交接 MD + 实际仓库基线; +2. 调查 STEP-061 当前源码与边界; +3. 生成中文 STEP-061 Codex 任务 MD; +4. 先 commit/push 任务 MD; +5. 在 `particle-codex` 新建 `step061` window; +6. 在该 window 启动新 Codex CLI; +7. Codex 完成源码/文档/允许验证后退出到 shell; +8. ChatGPT 审查真实 diff; +9. RDC 精准 commit/push source changes; +10. 同一 tmux window 以 `JOBS=1` 构建 AAR; +11. Maven Local publish + Linux 双 SHA; +12. `/mnt/nas/pbf` 临时发布; +13. CARBON7i `curl.exe -L ?v=` 下载 + SHA; +14. Win11 Maven Local + 两条 dependencyInsight; +15. 用户手工 APK build; +16. ChatGPT/RDC 安装、ADB、UI/logcat 实测; +17. 恢复 baseline validation UI; +18. 删除 `/mnt/nas/pbf` 临时文件; +19. 写最终 Step/验收报告/开发记录; +20. commit/push 最终文档并核对 local/remote HEAD。 +## 14. 关键停止条件 + +出现以下任一情况必须停止后续打包/设备流程并先定位: + +- Codex 修改越过 STEP-061 边界; +- 引入 provider/network/vendor 逻辑到 MapLibre Native/Core; +- 采用大规模 `number[]` JSON/base64 作为正式 binary field transport; +- source review 未通过却直接 build AAR; +- Linux build/Maven Local/Win11 SHA 不一致; +- Bridge 与 MLRN dependencyInsight 解析到不同 AAR 或旧版本; +- Android build 失败; +- 设备出现 crash/JNI/SIGSEGV 或 field fail-open; +- 未完成 Android hard gate 就提前把 STEP 标记 CLOSED/PASS。 + +## 15. 新窗口开始语义 + +用户在新 ChatGPT 窗口只需说明: +“继续 MapLibreParticleLab STEP-061,先读取 `docs/40_任务/STEP-061_新窗口交接与端到端执行流程_v1.md` 并按实际仓库状态执行。” + +新窗口收到后应直接读取此文件和仓库,不要求用户重新复述 AAR、tmux、Win11、ADB 或 STEP-060 历史。 diff --git a/docs/40_任务/STEP-062_RNTestHost真实WeatherV2底图粒子综合验证_Codex任务_v1.md b/docs/40_任务/STEP-062_RNTestHost真实WeatherV2底图粒子综合验证_Codex任务_v1.md new file mode 100644 index 000000000..5c1004735 --- /dev/null +++ b/docs/40_任务/STEP-062_RNTestHost真实WeatherV2底图粒子综合验证_Codex任务_v1.md @@ -0,0 +1,346 @@ +# STEP-062 RN Test Host 真实 WeatherV2 底图粒子综合验证 — Codex任务 v1 + +## 1. 任务定位 + +当前正式分支:`particle/main`。 + +开工基线: + +- STEP-061 已正式 `CLOSED / PASS`。 +- 当前 local / remote HEAD 均为 `729d22169da5cb2d5a4eafbded8dde894739dee6`。 +- Native/Core 冻结制品:`org.maplibre.gl:android-sdk-opengl:13.4.1-step061`。 +- STEP-062 不进入 NavSea,只在 `MapLibreParticleRNTestHost` 内完成接近最终使用形态的天气地图综合验证。 + +本阶段目标不是继续扩展 AAR,而是证明: + +```text +GSI 底图 + + +真实 WeatherV2 canonical T0/T1/T2/T3 + + +WeatherColorLayer + + +RasterParticleLayer + + +Timeline / Playback +``` + +在 React Native Test Host APK 中可以稳定共同工作。 + +## 2. 最高优先级边界 + +本任务默认**禁止修改和禁止重新构建 AAR**: + +- 不修改 `src/`、`include/` 下 MapLibre Native/Core 粒子实现; +- 不修改 `platform/android/MapLibreAndroid`; +- 不修改 JNI; +- 不修改 `WeatherRuntime.java`; +- 不修改 STEP-061 Binary ingress; +- 不执行 `make apackage`; +- 不执行 Maven Local publish; +- 不生成新的 `13.4.1-step062` AAR; +- 不进入 NavSea。 + +Test Host 继续固定消费已经通过实机验收的: + +```text +org.maplibre.gl:android-sdk-opengl:13.4.1-step061 +``` + +如果实现或 Android 验收证明 `step061` Native/Core 存在真实阻塞缺陷,必须停止扩大 STEP-062,并在报告中明确 `BLOCKED_BY_NATIVE`;不得为了方便顺手改 Core 后重出 AAR。 + +## 3. 开工前必读与真实基线 + +必须先读取并遵守: + +1. `AGENTS.md` +2. `PARTICLE_BASELINE.md` +3. `docs/00_项目规范/` 当前规范 +4. `docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v2.md` +5. `docs/报告/STEP-061_Android最终验收报告_v1.md` +6. `docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v7.md` +7. `docs/20_调查/STEP-056_RN天气渲染桥接现状调查_v1.md` +8. `integration/react-native/MapLibreParticleRNTestHost/` 当前实际源码 +9. Android Validation 中当前 GSI / REAL WEATHER 的实际实现,不得凭记忆重写。 + +当前已确认事实: + +- RN Test Host 的底图目前只是 `background-color: #10243a`,没有真实地图瓦片。 +- Android Validation 已使用 GSI 标准地图:`https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png`,tileSize=256,maxzoom=18。 +- 当前真实 WeatherV2 数据为 ECMWF HRES 2026-08-07 四帧:`2026080700/06/12/18`。 +- 每帧 grid=`513×769`,payload=`3,155,976 bytes`。 +- bounds=`west 112.5 / south 0.0 / east 157.5 / north 55.77657301866769`。 +- canonical contract 仍是 `maplibre-particle-field/v1`、`float32-le`、`interleaved-uv`、U eastward、V northward。 +- RN bridge 已有 `submitWindField`、forecast time、PLAY/PAUSE、playback speed、WeatherColor stops、particle/trail color、粒子视觉参数、targetFps、getState;本阶段不新增 Native API。 + +## 4. 真实 WeatherV2 资产 + +源资产固定来自: + +```text +platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-weatherv2/ + ecmwf-hres-wind-20260807/ +``` + +把本阶段实际需要的 `dataset.json`、四份 `metadata.json`、四份 `field.f32` **原样复制**到 RN Test Host 自己的受控 assets 目录,例如: + +```text +integration/react-native/MapLibreParticleRNTestHost/assets/weather/ + ecmwf-hres-wind-20260807/... +``` + +要求: + +- 不重新生成、不重采样、不裁剪、不改 metadata; +- 不把 `.f32` 转成 JSON、base64、hex 或 `number[]`; +- 复制后逐文件 SHA-256 与源资产一致; +- `field.f32` 内容必须与已有 Git blob 完全一致; +- 不创建第二套“近似 WeatherV2”测试数据。 + +## 5. RN 资产加载与 Binary transport + +继续使用 STEP-061 的正式 file-reference transport,不新增第二条路径。 + +建议实现: + +1. 将 `expo-asset` 作为 Test Host 的明确直接依赖,版本保持当前 Expo 56 兼容范围,不升级 Expo/RN。 +2. 新增 `metro.config.js`,只补充 `.f32` 为 asset extension;不得改动无关 Metro 行为。 +3. 新增一个小型 `realWeatherDataset.ts`(或等价文件),用静态 `require()` 注册四帧 `.f32` asset 与 metadata。 +4. 通过 `Asset.fromModule(...).downloadAsync()` 获得 app 本地 `file://` URI。 +5. metadata 继续以小型 JSON string 传入;payload 只传本地文件引用。 +6. 不在 JS 中把 3.01 MiB payload materialize 成数值数组;不做 base64。 + +必须明确显示并验证: + +```text +REAL DATA TRANSPORT = FILE_REF / FLOAT32_LE / INTERLEAVED_UV +``` + +若 Expo Asset 最终不能稳定给出本地文件 URI,先报告阻塞;不要退回 base64/number[]。 + +## 6. GSI 底图 + +将 RN Test Host 的纯色背景升级为真实 GSI 标准底图。 + +Style 最小结构固定为: + +```text +background +GSI raster basemap +WeatherColorLayer +RasterParticleLayer +``` + +要求: + +- raster source 使用 Android Validation 已验证 URL; +- source 写明合适 attribution(至少标明国土地理院 / GSI); +- tileSize=256,maxzoom=18; +- WeatherColor 和 Particle 必须在 GSI 之上; +- 不把 WeatherColor 或粒子烘焙进底图; +- 不增加 provider-specific Weather 逻辑。 + +使用 MLRN 11.3.6 当前真实 `Camera` API,初始视图应直接覆盖 WeatherV2 bounds 或合理聚焦日本及周边;不得让用户每次启动后手工从世界视图找数据。 + +页面至少显示: + +- `Basemap: GSI READY / ERROR / LOADING` +- map/style loading error(若存在) +- 当前 weather dataset identity。 + +## 7. T0~T3 提交与 Timeline + +新增一个明确的 `LOAD REAL WEATHER T0-T3` 综合入口。 + +每次执行前必须: + +1. `clearWindFields()`; +2. 确认四帧本地 asset 已 materialize 为 file URI; +3. 按 validTime 严格递增提交 T0/T1/T2/T3; +4. 每帧提交后记录结构化结果; +5. 最终重新读取 Native `getState()`。 + +硬断言: + +- T0 = `ACCEPTED_PENDING_INITIAL_WINDOW` +- T1 = `INITIAL_WINDOW_READY` +- T2 = `ACCEPTED_NEXT_SNAPSHOT` +- T3 = `ACCEPTED_NEXT_SNAPSHOT` +- 最终 Native state = `READY` + +任一不符都显示 `ERROR`,不得把部分成功显示为 READY。 + +UI 至少显示: + +```text +Dataset: ECMWF HRES 20260807 +T0 2026080700: +T1 2026080706: +T2 2026080712: +T3 2026080718: +Runtime: READY / ... +``` + +RN 只提交 producer timestamp,不保存第二套 T0/T1/MIX Runtime Window;Core Timeline 仍是唯一 owner。 + +## 8. Playback / Forecast 综合控制 + +在现有 Bridge API 上补齐 Test Host 可操作 UI,不新增 Native contract: + +- PLAY +- PAUSE +- HALF / NORMAL / DOUBLE +- seek T0 / T1 / T2 / T3 对应的 forecast timestamp +- FPS AUTO / 30 / 60 +- WeatherColor visible / opacity +- Particle visible +- WeatherColor stops(可使用已冻结的测试色带) +- particleColor / trailColor +- particle count / line width / length / opacity / trail opacity / trail fade + +测试 UI 可以紧凑,但必须让真实地图保持主要可视区域;不要把控制面做成遮住地图的大面板。 + +Forecast seek 必须调用现有 `setForecastTime(timestampMs)`;禁止把 index/T0/T1/MIX 暴露成 JS 业务状态。 + +## 9. 视觉与地图综合验收准备 + +代码必须让后续 Android 一次 APK 验收能够观察: + +- GSI 海岸线、地名等底图正常; +- WeatherColor 与地图地理位置对齐; +- Particle 与 WeatherColor 使用同一真实 Field; +- pan / zoom / bearing 后粒子与底图保持地理锁定; +- T0/T1/T2/T3 seek 后场景可继续渲染; +- PLAY/PAUSE/速度切换不造成 style reload; +- WeatherColor visible 与 Particle visible 相互独立; +- FPS 切换不改变 forecast/canonical 数据语义。 + +## 10. 临时文件与稳定性 + +本阶段要明确 3 MiB 级 asset 的生命周期,但不做 zero-copy 性能工程。 + +要求: + +- `expo-asset` 产生的本地文件由 Expo/系统 cache 生命周期管理;不要额外复制多份 payload; +- 若代码另建临时文件,提交结束后必须有明确清理路径; +- Fast Refresh、重复 LOAD、clear、detach 不得无限累积自建 `.f32` 临时文件; +- 不为了“减少 copy”引入 JSI、direct ByteBuffer、mmap 或新的 Native transport; +- Android 验收阶段观察连续 T0~T3 提交是否出现 OOM、长时间卡死或明显异常 GC;本任务不提前做性能优化。 + +## 11. 允许修改范围 + +优先只允许: + +- `integration/react-native/MapLibreParticleRNTestHost/App.tsx` +- `integration/react-native/MapLibreParticleRNTestHost/src/**` +- `integration/react-native/MapLibreParticleRNTestHost/assets/**` +- `integration/react-native/MapLibreParticleRNTestHost/metro.config.js` +- `integration/react-native/MapLibreParticleRNTestHost/package.json` +- `integration/react-native/MapLibreParticleRNTestHost/package-lock.json` +- `integration/react-native/MapLibreParticleRNTestHost/README.md` +- 本 Step 的中文记录/报告/开发记录。 + +如非必要,不修改 `modules/weather-runtime-bridge/**`;当前 Bridge API 已足够完成任务。 + +明确禁止提交: + +- `android/` generated build 目录; +- `node_modules/`; +- `.codex-build-logs/`; +- APK/AAR; +- 与本任务无关的历史 untracked 文件。 + +## 12. Codex 静态验证 + +Codex 必须执行并记录: + +1. `npm run typecheck`。 +2. `metro.config.js` 的 Node 语法/配置检查。 +3. 四帧 Test Host asset 与源 WeatherV2 asset 的 SHA-256 一致性检查。 +4. 检索正式 transport 不存在 `number[]` / payload JSON / base64 / hex。 +5. 检查 style layer 顺序确实为 GSI → WeatherColor → Particle。 +6. `git diff --check`。 + +未经用户明确要求:**不加入 Jest,不运行 Jest**。 + +Codex 不执行: + +- Android Gradle; +- `assembleDebug`; +- ADB; +- APK install; +- Android 设备测试; +- AAR build/publish。 + +Android APK 编译和设备综合验收仍由后续 CARBON7i 手工周期执行,并尽量一次覆盖全部 STEP-062 验收项。 + +## 13. 必做输出 + +至少完成: + +1. RN Test Host 的 GSI + Real Weather + WeatherColor + Particle + Playback 代码。 +2. 原样复用的四帧 WeatherV2 assets。 +3. 更新 Test Host 中文 README,说明 STEP-062 操作方法和冻结的 `step061` AAR。 +4. `docs/50_Step记录/STEP-062_RNTestHost真实WeatherV2底图粒子综合验证_v1.md`。 +5. `docs/报告/STEP-062_RNTestHost真实WeatherV2底图粒子综合验证_Codex完成报告_v1.md`。 +6. 追加 `粒子开发记录.md`。 + +完成报告必须明确: + +- 实际修改文件; +- GSI source/layer 与 camera 实现; +- 四帧 asset 来源、大小和 SHA 对照; +- Expo Asset → local file URI → STEP-061 bridge 的实际调用链; +- T0~T3 的预期 submit result; +- Playback/forecast/visual 控件接线; +- 是否修改任何 Native/Core/WeatherRuntime/JNI(预期:否); +- 是否构建/发布 AAR(必须:否); +- 实际静态验证结果; +- Android APK/设备仍待哪些项目验证。 + +源码阶段状态只能写: + +```text +IMPLEMENTED / WAITING_ANDROID_VALIDATION +``` + +不得提前写 Android PASS。 + +## 14. Git 收尾 + +本任务会生成/修改项目文件,因此完成后必须: + +1. 逐项对照本任务必做项; +2. `git diff --check` PASS; +3. 只 `git add` 本任务相关文件; +4. commit 到当前 `particle/main`; +5. `git push origin particle/main`; +6. 完成报告记录 final commit、commit message、push 结果、remote branch; +7. push 失败不得声称任务完成。 + +不得把现有历史 untracked 文件加入提交。 + +## 15. 最终必做清单 + +- [ ] GSI raster basemap 已进入 RN Test Host +- [ ] layer order = GSI → WeatherColor → Particle +- [ ] Camera 初始范围能直接看到 WeatherV2 区域 +- [ ] 四帧真实 WeatherV2 asset 原样复制且 SHA 一致 +- [ ] `.f32` 走 Expo Asset local file URI +- [ ] 继续使用 STEP-061 file-reference transport +- [ ] T0/T1/T2/T3 结果做严格 hard assertion +- [ ] 最终 Native state 必须要求 READY +- [ ] PLAY/PAUSE/速度/forecast seek 可操作 +- [ ] WeatherColor 与 Particle 独立显隐和视觉控制保留 +- [ ] FPS AUTO/30/60 可操作 +- [ ] 不创建第二套 T0/T1/MIX 状态 +- [ ] 不使用 number[]/payload JSON/base64/hex +- [ ] 不修改 Native/Core/JNI/WeatherRuntime +- [ ] 不构建或发布 AAR +- [ ] 不进入 NavSea +- [ ] 不加入或运行 Jest +- [ ] `npm run typecheck` PASS +- [ ] asset SHA 检查 PASS +- [ ] `git diff --check` PASS +- [ ] Step/报告/开发记录完成 +- [ ] commit + push `origin/particle/main` diff --git a/docs/40_任务/STEP-063_MapLibre粒子Android_v1发布工程化与ReactNative可导入包_Codex任务_v1.md b/docs/40_任务/STEP-063_MapLibre粒子Android_v1发布工程化与ReactNative可导入包_Codex任务_v1.md new file mode 100644 index 000000000..c1db06874 --- /dev/null +++ b/docs/40_任务/STEP-063_MapLibre粒子Android_v1发布工程化与ReactNative可导入包_Codex任务_v1.md @@ -0,0 +1,889 @@ +# STEP-063 MapLibre 粒子 Android v1 发布工程化与 React Native 可导入包 Codex 任务 v1 + +## 0. 任务性质 + +本任务开始把已经通过 Android 实机综合验证的 MapLibre Particle 从“开发工程状态”整理为“可版本化、可发布、可由 React Native 通过包依赖导入的组件”。 + +本任务属于发布工程化与包结构整理,不是继续开发新的粒子渲染能力。 + +当前用户已确认: + +- Android MapLibre Particle 当前能力达到满意水平。 +- STEP-062 可按 `CLOSED / PASS` 收尾。 +- 当前阶段不接入 NavSea。 +- 发布服务器、npm Registry、Maven Repository 的最终部署位置暂不决定。 +- npm 正式包名采用: + - `@navsea/maplibre-particle-rn` +- 正式版本规则采用: + - `<上游 MapLibre Native 版本>-particle.` +- 当前计划首个正式版本: + - `13.4.1-particle.1` +- Android Maven artifact 计划采用: + - group: `cc.navsea.maplibre` + - artifact: `android-sdk-particle-opengl` + - version: `13.4.1-particle.1` + +如果仓库实际调查证明基础 MapLibre Native 并非 `13.4.1`,必须停止版本改写并先报告,不得自行猜测或强行套用版本号。 + +--- + +# 1. 本任务目标 + +完成以下四个目标: + +1. 冻结并记录当前 MapLibre Particle 的真实上游版本基线。 +2. 建立正式版本号、release manifest、CHANGELOG 与发布契约。 +3. 把当前 React Native 接入整理成一个正式 npm package: + - `@navsea/maplibre-particle-rn` +4. 建立一个“只通过发布包消费”的独立 Consumer Test Host,用于后续证明 NavSea 不需要源码路径或 MapLibre Native 源码编译即可使用。 + +本任务结束时,不要求真正上传到任何服务器。 + +最终服务器发布位置后续单独决定。 + +--- + +# 2. 强制边界 + +## 2.1 禁止继续扩展粒子功能 + +本任务不得新增或重写: + +- Particle GPU simulation +- WeatherColor 算法 +- Timeline / Playback 算法 +- HostField 二进制入口 +- RasterArraySource 核心语义 +- U/V canonical contract +- MapLibre renderer +- JNI 核心路径 +- WeatherRuntime 业务语义 + +除非当前发布工程化过程中发现明确的“无法打包/无法消费”的结构性阻塞。 + +如果出现此类阻塞: + +1. 停止扩大修改。 +2. 明确记录阻塞位置。 +3. 说明最小修复建议。 +4. 不得借发布任务顺便重构 Native/Core。 + +--- + +## 2.2 不接入 NavSea + +本任务不得修改 NavSea 仓库。 + +不得: + +- 在 NavSea 中安装 npm package +- 修改 NavSea Metro +- 修改 NavSea Gradle +- 修改 NavSea Pod +- 修改 NavSea Store / Adapter / Action +- 将 Test Host 逻辑搬入 NavSea + +本任务只负责把发布组件准备好。 + +--- + +## 2.3 不决定最终发布服务器 + +本任务不得擅自加入: + +- 公司 npm Registry URL +- GitHub Packages +- GitLab Packages +- Gitea Packages +- Nexus +- Artifactory +- Verdaccio +- 私有 Maven Repository URL +- 生产用户名、密码、Token、证书 + +可以建立与 Registry 无关的标准发布配置和说明。 + +可以使用本地 `.tgz`、本地 staging 目录作为“未上传发布物”验证手段。 + +--- + +## 2.4 Android 编译边界 + +严格遵守项目长期规则: + +**所有 Android 编译由用户手工执行。** + +Codex 不得执行: + +- Android Gradle assemble +- Android Gradle build +- AAR build +- AAR publish +- Maven Local publish +- APK build +- APK install +- ADB +- Android 实机测试 + +Codex 可以: + +- 修改构建配置 +- 修改版本号 +- 准备 publication 配置 +- 做非 Android 编译的静态检查 +- 执行 TypeScript 编译/类型检查(若不触发 Android build) +- 执行 `npm pack` +- 检查 `.tgz` 内容 +- 编写用户手工 Android 构建命令 + +--- + +## 2.5 不执行 Jest + +本任务不执行 Jest,也不新增 Jest 作为验收要求。 + +--- + +# 3. 第一步:真实基线只读调查 + +开始修改前必须先从当前仓库真实代码调查。 + +至少确认: + +## 3.1 MapLibre Native 基础版本 + +从实际构建文件、版本定义、Gradle publication、Git metadata 等确认: + +- upstream MapLibre Native version +- 当前 Android renderer variant +- 当前 STEP-061 AAR 坐标 +- 当前 STEP-061 AAR version +- 当前 Native/Core source 与该版本之间的关系 + +预期: + +```text +upstreamMapLibreNative = 13.4.1 +currentDevelopmentArtifact = org.maplibre.gl:android-sdk-opengl:13.4.1-step061 +renderer = opengl +``` + +但必须以仓库实际内容为准。 + +--- + +## 3.2 React Native 基线 + +调查当前 RN bridge / package 的真实位置与结构。 + +至少确认: + +- 当前 RN package 名 +- 当前 RN package version +- 当前 `package.json` +- 当前 Android module / Gradle 文件 +- 当前 autolinking 方式 +- 当前 MapLibre RN 公共 API 导出方式 +- `WeatherRuntime` +- `RasterParticleLayer` +- `WeatherColorLayer` +- HostField submit API +- RN Test Host 当前如何引用该 RN 模块 +- 是否存在 `file:`、workspace path、source path 等开发期依赖 + +必须明确: + +**哪些路径只是开发期源码引用,正式 npm package 后必须消失。** + +--- + +## 3.3 当前正式能力链路 + +只读确认当前已冻结链路: + +```text +React Native Host + ↓ metadataJson + local file:// binary payload +RN bridge + ↓ +WeatherRuntime + ↓ +RasterArraySource::submitHostField() + ↓ +LocalParticleFieldFeed + ↓ +ParticleFieldDataAdapter + ↓ +ParticleFieldSnapshot + ↓ +Core Timeline / Runtime Field Window + ↓ +WeatherColorLayer + RasterParticleLayer +``` + +不得因为发布工程化另建第二套入口。 + +--- + +# 4. 第二步:正式版本契约 + +调查确认基础版本无误后,建立正式版本契约。 + +## 4.1 正式版本 + +首个正式版本: + +```text +13.4.1-particle.1 +``` + +语义: + +```text +13.4.1 += 上游 MapLibre Native 基础版本 + +particle += NavSea Particle 扩展发行线 + +1 += 基于该 upstream version 的第 1 个 NavSea Particle 正式修订 +``` + +后续规则: + +```text +13.4.1-particle.2 +13.4.1-particle.3 + +13.5.0-particle.1 + +14.0.0-particle.1 +``` + +禁止以后恢复使用 `step061` 这种开发阶段编号作为正式发行版本。 + +--- + +## 4.2 npm package + +正式 npm package: + +```text +@navsea/maplibre-particle-rn +``` + +正式首发 version: + +```text +13.4.1-particle.1 +``` + +要求 package 名清楚表达: + +- NavSea 维护 +- MapLibre +- Particle +- React Native + +--- + +## 4.3 Android Maven artifact + +正式 Android artifact: + +```text +cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1 +``` + +不得继续使用官方坐标作为 NavSea 正式发行坐标: + +```text +org.maplibre.gl:android-sdk-opengl +``` + +注意: + +Java/Kotlin package namespace 不要求因此重命名。 + +本任务重点是 Maven publication coordinate 与依赖契约,不允许进行大规模 Java/C++ namespace rename。 + +--- + +# 5. 第三步:建立 Release Manifest + +在项目中建立稳定、机器可读的 release manifest。 + +路径由 Codex根据现有仓库结构选择,但建议类似: + +```text +release/maplibre-particle/release-manifest.json +``` + +或: + +```text +release/maplibre-particle/13.4.1-particle.1.json +``` + +至少包含: + +```json +{ + "release": "13.4.1-particle.1", + "upstreamMapLibreNative": "13.4.1", + "extensionLine": "particle", + "extensionRevision": 1, + "platform": { + "android": { + "renderer": "opengl" + } + }, + "reactNativePackage": { + "name": "@navsea/maplibre-particle-rn", + "version": "13.4.1-particle.1" + }, + "nativeArtifact": { + "group": "cc.navsea.maplibre", + "artifact": "android-sdk-particle-opengl", + "version": "13.4.1-particle.1" + }, + "publication": { + "npmRegistry": null, + "mavenRepository": null + } +} +``` + +可以增加必要字段,例如: + +- source commit +- compatibility +- canonical field schema +- RN compatibility +- minSdk / compileSdk(如果项目已有真实事实) +- release state + +但不得猜测未确认版本。 + +--- + +# 6. 第四步:正式 React Native package + +目标: + +```text +@navsea/maplibre-particle-rn@13.4.1-particle.1 +``` + +必须成为 NavSea 未来唯一面对的 React Native MapLibre Particle 包入口。 + +--- + +## 6.1 设计原则 + +NavSea 未来目标使用方式: + +```ts +import { + MapView, + Camera, + RasterParticleLayer, + WeatherColorLayer, + WeatherRuntime, +} from '@navsea/maplibre-particle-rn'; +``` + +实际导出名称必须依据当前仓库真实 API。 + +不得为了示例强行重命名现有稳定 API。 + +--- + +## 6.2 不允许正式消费源码路径 + +正式 package 不得要求 Consumer: + +```text +file:../MapLibreParticleLab/... +../../platform/react-native +workspace 源码路径 +CMake source path +MapLibre Native source tree +本地源码软链接 +``` + +Consumer 只能依赖正式 npm package。 + +Android Native 应通过正式 artifact dependency 解决,而不是 Consumer 直接编译 MapLibre Native 源码。 + +--- + +## 6.3 包内部结构 + +根据当前 React Native package 实际结构做最小整理。 + +要求: + +- `package.json` 名称正式改为 `@navsea/maplibre-particle-rn` +- version = `13.4.1-particle.1` +- 保留正确的 React Native autolinking +- 保留当前稳定 JS/TS 公共 API +- 保留 Native bridge +- 内部 Android Gradle dependency 改为正式 artifact: + - `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1` +- 不得同时再自动引入官方 `org.maplibre.gl:android-sdk-opengl` 导致双 Native AAR +- 不得引入第二份 MapLibre RN native module + +--- + +## 6.4 唯一 MapLibre RN 入口原则 + +需要重点审计: + +Consumer 是否仍然需要同时安装: + +```text +@maplibre/maplibre-react-native +``` + +如果当前 NavSea Particle RN package 本质上是对 MapLibre RN 的完整 fork/扩展,则正式目标应是: + +```text +Consumer + ↓ +@navsea/maplibre-particle-rn + ↓ +唯一 RN native module + ↓ +cc.navsea.maplibre:android-sdk-particle-opengl +``` + +不应形成: + +```text +@maplibre/maplibre-react-native ++ +@navsea/maplibre-particle-rn +``` + +两套 Native MapLibre 同时进入 App。 + +如果仓库实际结构暂时无法做到唯一入口,必须在报告中明确说明,不得假装已经完成。 + +--- + +# 7. 第五步:发布包内容审计 + +必须能够执行: + +```text +npm pack +``` + +生成类似: + +```text +navsea-maplibre-particle-rn-13.4.1-particle.1.tgz +``` + +实际文件名以 npm 规则为准。 + +Codex 必须检查 tarball 内容。 + +至少确保: + +必须包含: + +- package.json +- JS/TS build output 或正式 package 所需源码 +- type declarations +- React Native native module 必需文件 +- android module 必需文件 +- README +- LICENSE/NOTICE(依据仓库现有许可要求) +- 必需 plugin/config 文件 + +不得包含: + +- node_modules +- `.git` +- 大量 build cache +- Android APK +- 临时 log +- Test Host +- WeatherV2 测试数据 +- `.codex-build-logs` +- unrelated docs +- 任何 credential + +--- + +# 8. 第六步:Android artifact publication 配置 + +把 Android Native publication 从开发阶段坐标整理成正式坐标。 + +目标: + +```text +groupId: +cc.navsea.maplibre + +artifactId: +android-sdk-particle-opengl + +version: +13.4.1-particle.1 +``` + +必须保留: + +```text +renderer = opengl +``` + +本阶段只准备 publication 配置。 + +Codex 不执行 AAR build 或 publish。 + +--- + +# 9. 第七步:建立独立 Consumer Test Host + +新增一个真正用于验证“发布包消费”的最小 React Native Test Host。 + +名称建议: + +```text +MapLibreParticlePackageConsumerTestHost +``` + +实际路径应遵守现有 integration 结构。 + +--- + +## 9.1 Consumer Test Host 的核心约束 + +它不得: + +- 引用 MapLibreParticleLab RN 源码目录 +- 使用 workspace source dependency +- 使用相对路径导入正式 RN package 源码 +- 直接引用 MapLibre Native source tree +- 自己实现 WeatherRuntime bridge +- 复制 Native bridge 源码 + +它只能通过安装后的: + +```text +@navsea/maplibre-particle-rn +``` + +使用功能。 + +--- + +## 9.2 当前服务器未决定时的测试方式 + +由于最终 npm/Maven 服务器尚未决定,本阶段允许: + +### npm + +使用本地: + +```text +npm pack +``` + +生成 `.tgz` 后安装: + +```text +npm install +``` + +这属于“发布包消费”,不是源码路径消费。 + +### Android Native artifact + +本任务只准备正式 artifact publication。 + +后续由用户手工构建并放入 Maven Local 或临时 staging repository,再进行 Consumer APK 编译。 + +不得因为服务器未决定,就退回 source path。 + +--- + +## 9.3 Consumer Test Host 最低功能 + +先做最小 smoke host,不要求复制 STEP-062 全部复杂测试 UI。 + +最低要求: + +- MapLibre MapView +- GSI basemap +- `WeatherColorLayer` +- `RasterParticleLayer` +- `WeatherRuntime` +- 一个最小真实或 canonical fixture 提交入口 +- Runtime state 可见 +- Particle / WeatherColor 显示 + +如果已有可复用 STEP-062 Test Host 逻辑,可以复用业务层代码,但不得通过源码 dependency 复用 RN package。 + +--- + +# 10. 第八步:文档 + +所有文档中文。 + +至少新增或更新: + +## 10.1 架构/发布设计 + +建议: + +```text +docs/30_设计/MapLibre粒子Android发布与ReactNative包设计_v1.md +``` + +至少说明: + +- upstream version +- NavSea Particle version rule +- npm package name +- Maven coordinate +- RN package 与 Native artifact 关系 +- 为什么 Consumer 不编译 MapLibre Native 源码 +- 为什么不能同时装官方 RN MapLibre 和 NavSea Particle RN +- server registry 尚未决定 +- 本地 `.tgz` / staging 只是预发布验证手段 +- 后续 upstream upgrade 规则 + +--- + +## 10.2 STEP 记录 + +建议: + +```text +docs/50_Step记录/STEP-063_MapLibre粒子Android_v1发布工程化_v1.md +``` + +--- + +## 10.3 Codex 完成报告 + +建议: + +```text +docs/报告/STEP-063_MapLibre粒子Android_v1发布工程化_Codex完成报告_v1.md +``` + +--- + +## 10.4 CHANGELOG + +为正式 package 建立中文 CHANGELOG 或遵循仓库现有发布记录结构。 + +至少记录: + +```text +13.4.1-particle.1 + +- 基于 MapLibre Native 13.4.1 +- Android OpenGL +- WeatherColor +- RasterParticle +- HostField binary ingress +- Timeline / Playback +- React Native WeatherRuntime +- STEP-061 Native/RN ingress 验证 +- STEP-062 GSI + WeatherV2 + Particle Android 综合验证 +``` + +不得把没有验证的能力写成已通过。 + +--- + +# 11. 静态验收 + +Codex 可执行且应执行: + +- package JSON 校验 +- TypeScript typecheck +- `npm pack` +- tarball 内容列表审计 +- 检查正式 package 中不存在开发期 `file:` dependency +- 检查正式 package 中不存在 MapLibre Native source path +- 检查 Android Gradle dependency 指向正式 coordinate +- 检查没有同时依赖正式 Particle AAR 与官方 MapLibre AAR +- `git diff --check` +- repository search 审计旧 `13.4.1-step061` 使用位置 + +注意: + +历史文档中出现 `13.4.1-step061` 是合法的。 + +但正式 release/package/build config 不得继续把它作为当前发行依赖。 + +--- + +# 12. 用户手工 Android 验证入口 + +Codex 完成本任务后,必须给出准确的用户手工命令,但不得自行执行。 + +后续用户将手工完成两个阶段: + +## 阶段 A:正式 AAR + +构建: + +```text +cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1 +``` + +并验证 artifact 生成。 + +最终 publish server 尚未决定,因此可先由用户手工发布到 Maven Local 或指定 staging 目录用于验证。 + +--- + +## 阶段 B:Consumer Test Host APK + +Consumer 只安装: + +```text +@navsea/maplibre-particle-rn@13.4.1-particle.1 +``` + +通过正式 artifact dependency 获取 Android Native。 + +然后用户手工: + +- Android assembleDebug +- 安装 APK +- 实机 smoke +- GSI +- WeatherColor +- Particle +- Runtime READY + +通过后才能正式创建 release tag。 + +--- + +# 13. Git Tag 规则 + +本任务暂时不得创建正式 release tag。 + +原因: + +`13.4.1-particle.1` 必须先经过: + +1. 正式 AAR 手工构建 +2. npm tarball package consumer 验证 +3. Consumer Test Host Android APK 实机验证 + +全部通过后,再单独执行最终 Release Freeze 任务创建 tag。 + +建议未来 tag: + +```text +maplibre-particle/android/13.4.1-particle.1 +``` + +实际 tag 命名在最终发布任务确认。 + +--- + +# 14. Git 收尾 + +本任务会修改项目文件,因此必须提交并 push 到: + +```text +origin/particle/main +``` + +严格遵守项目 Git 规则。 + +不得: + +```text +git add . +git add -A +``` + +必须精确 add 本任务文件。 + +不得提交历史无关 untracked 文件。 + +完成后: + +```text +git fetch origin +git rev-parse HEAD +git rev-parse origin/particle/main +git status --short +``` + +必须确认: + +```text +HEAD == origin/particle/main +``` + +如果 push 失败: + +**不得声称任务完成。** + +--- + +# 15. 任务完成状态 + +如果代码/package/publication 配置均完成,静态检查和 npm pack 均通过,但尚未执行用户手工 Android 构建: + +状态写: + +```text +IMPLEMENTED / WAITING_MANUAL_RELEASE_VALIDATION +``` + +不得提前写: + +```text +RELEASED +CLOSED / PASS +``` + +正式 `CLOSED / PASS` 等待后续用户手工 AAR + Consumer APK 验证。 + +--- + +# 16. Codex 最终报告必须包含 + +- 实际确认的 upstream MapLibre Native version +- 当前 RN 基线 +- 正式版本号 +- npm package name +- Maven coordinate +- release manifest 路径 +- 正式 RN package 路径 +- Consumer Test Host 路径 +- `npm pack` 结果 +- tarball 文件名 +- tarball 内容审计结果 +- 是否仍存在正式配置中的 `step061` +- 是否存在 `file:` 或 source-path consumer dependency +- 是否修改 Native/Core 功能代码 +- 是否执行 Android Gradle +- 是否构建 AAR +- 是否执行 ADB +- 是否执行 Jest +- 用户下一步手工命令 +- commit hash +- commit message +- push result +- remote branch +- Final HEAD +- 历史无关 untracked 项说明 + diff --git a/docs/40_任务/STEP-064_RN纯FieldRenderer接口V2_Codex任务_v1.md b/docs/40_任务/STEP-064_RN纯FieldRenderer接口V2_Codex任务_v1.md new file mode 100644 index 000000000..61f1185bf --- /dev/null +++ b/docs/40_任务/STEP-064_RN纯FieldRenderer接口V2_Codex任务_v1.md @@ -0,0 +1,517 @@ +# STEP-064 RN 纯 Field Renderer 接口 V2 — Codex 任务 v1 + +## 0. 任务定位 + +本任务把 `@navsea/maplibre-particle-rn` 从当前开发期的 Weather/Wind 导向 RN API,整理为正式宿主可使用的**纯 Field Renderer RN SDK**。 + +目标 RN 包版本: + +```text +13.4.1-particle.2 +``` + +本任务不是粒子算法升级,不是 WeatherV2 功能扩展,也不是 Native/Core 重构。 + +核心原则只有一条: + +> React/宿主负责业务、数据获取、provider、数据类型和显示策略;Particle RN SDK 只接收 canonical Field 并控制 MapLibre Native 图场与粒子渲染。 + +当前除了 Test App 没有正式消费者,因此本任务**不承担旧 RN 公共 API 兼容责任**,允许直接删除旧接口并同步修改测试宿主。 +## 1. 仓库与当前基线 + +仓库: + +```text +/mnt/sda1/MapLibreParticleLab +``` + +分支: + +```text +particle/main +``` + +任务书编写时源码基线: + +```text +f31c0ea1571aa3429ec170fbf38bdecf45c847ae +``` + +开始执行时必须重新确认 `git status --short`、当前分支、`HEAD` 与 `origin/particle/main`。若远端已合法推进,先阅读新增提交,不得 reset 或覆盖。 + +当前工作树存在与本任务无关的 modified/untracked 文件。禁止 `git add .`、`git add -A`,禁止恢复、删除、覆盖或提交这些无关文件。 +## 2. 执行前必须阅读 + +至少阅读以下实际文件: + +```text +AGENTS.md +PARTICLE_BASELINE.md +docs/00_项目规范/01_Codex协作规范_v1.md +docs/00_项目规范/02_构建与验证规范_v1.md +docs/00_项目规范/03_Step阶段记录规范_v1.md +docs/00_项目规范/05_粒子架构原则_v1.md +packages/maplibre-particle-rn/package.json +packages/maplibre-particle-rn/src/index.ts +packages/maplibre-particle-rn/src/types.ts +packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts +packages/maplibre-particle-rn/src/weatherV2/* +packages/maplibre-particle-rn/app.plugin.js +integration/react-native/MapLibreParticlePackageConsumerTestHost/* +release/maplibre-particle/release-manifest.json +docs/30_设计/MapLibre粒子ReactNative使用说明_v2.md +``` + +实现必须依据当前实际源码,不得根据任务书示例猜接口或目录。 +## 3. 冻结的架构边界 + +### 3.1 SDK 不理解业务 + +`@navsea/maplibre-particle-rn` 的正式 RN 公共接口不得出现或要求调用方传入以下业务含义: + +```text +WeatherV2 +ECMWF +wind +wave +rain +tide +weather layer +dataset +manifest +time_index +HTTP URL/provider/cache/retry +Route / ETA +NavSea 业务状态 +``` + +这些全部属于 React/宿主层。 + +### 3.2 数据只有一份 + +同一份 canonical Field 进入 Runtime 一次。图场 Renderer 与 Particle Renderer 是对同一 Field 的两种独立显示方式,不建立两套 Field 数据、不要求重复提交。 +逻辑必须保持: + +```text +Canonical Field + ├─ Field Renderer ON / OFF + └─ Particle Renderer ON / OFF +``` + +宿主可以任意组合两个 Renderer 的显示状态。SDK 不根据业务类型自动决定是否显示粒子。 + +### 3.3 本任务不改变 canonical / Native 技术 + +本任务保持当前已验证 Native/Core 能力: + +```text +maplibre-particle-field/v1 +metadataJson + local file payload +float32-le / interleaved-uv +Timeline / T0-T1 Runtime Field Window +GPU 时间插值 +WeatherColorLayer +RasterParticleLayer +Particle State / Trail +Playback / target FPS +``` + +不得为了 RN 命名整理修改 C++、shader、JNI、Java/Kotlin WeatherRuntime、MapLibre Native renderer 或 Android AAR 内容。 +## 4. 必做项 A:建立最终 RN 公共 API + +新增纯渲染公共入口,建议名称: + +```ts +FieldRuntime +``` + +正式调用方应从包根入口使用它。目标公开方法至少包括: + +```text +attach() +detach() +getState() +clearFields() +submitField() +setEnabled() +setFieldVisible() +setParticleVisible() +setTargetFps() +setTargetTime() +setPlaybackState() +setPlaybackSpeed() +setFieldColorStops() +setParticleColor() +setTrailColor() +setParticleCount() +setParticleLineWidth() +setParticleLength() +setParticleOpacity() +setTrailOpacity() +setTrailFade() +setFieldOpacity() +``` + +这些方法可以在 RN/TS 层薄映射到当前已验证的 Native bridge 方法,不得因此修改 Native 方法名。 +公共 API 中不得继续暴露以下旧名称: + +```text +WeatherRuntime +submitWindField +clearWindFields +setWeatherEnabled +setWeatherColorVisible +setWeatherColorStops +setWeatherColorOpacity +setForecastTime +``` + +当前内部 `NativeWeatherRuntimeBridge.ts`、TurboModule 名 `WeatherRuntimeBridge`、Native 生成类名等若属于既有 Native ABI/Codegen 接缝,可作为本任务内部实现细节保留;但不得从包根入口继续暴露,也不得要求宿主使用这些名字。 + +由于没有正式消费者,不建立 deprecated alias,不增加 compatibility wrapper,不同时维护两套公共 API。 + +## 5. 必做项 B:公共类型去业务化 + +将面向宿主的类型改为纯 Field Renderer 语义。 + +例如当前 `ColorStop.speedMps` 不应继续作为最终 RN 公共命名;建议公开为: + +```ts +type FieldColorStop = Readonly<{ + value: number; + red: number; + green: number; + blue: number; + alpha: number; +}>; +``` + +RN 内部可把 `value` 薄转换成当前 Native bridge 所需字段,不修改 Native/Core。 +以下现有类型可在确认无业务耦合后继续使用或适当重命名: + +```text +RuntimeState +PlaybackState +TargetFps +CommandResult +FieldSubmitResult +RuntimeSnapshot +``` + +不得新增: + +```text +WeatherLayer +WindLayer +RainLayer +WaveLayer +TideLayer +ProviderType +DatasetType +``` + +`submitField()` 仍只负责把当前 canonical metadata 与本地 binary payload 交给既有 Native ingress。RN SDK 不解析业务类型,也不根据 field 内容决定 Renderer 组合。 + +当前 canonical metadata 中若存在 Native/Core 已冻结的历史字段名,本任务不为追求命名纯净而修改其 schema;RN 层只把它视为 Native 数据契约,不增加业务解释。 +## 6. 必做项 C:WeatherV2 完全移出正式 package + +当前 package 根入口存在: + +```ts +export * from "./weatherV2"; +``` + +且 `packages/maplibre-particle-rn/src/weatherV2/` 内包含 HTTP、manifest、time index、WVECBIN1 decoder 和高层 loader。 + +这些全部属于宿主业务/数据适配,不属于纯 Renderer SDK。 + +必须: + +1. 从正式 package 根导出删除 WeatherV2 API; +2. 从正式 package 源码中移除 `src/weatherV2/`; +3. 不在 package 内留下其他 provider/network/weather helper 替代品; +4. Consumer Test Host 若仍要验证真实 WeatherV2,可以把必要 WeatherV2 逻辑放到 Test Host 自己的 `src/` 中; +5. Test Host 的业务代码最终只把 canonical Field 交给 `FieldRuntime.submitField()`。 + +禁止新增诸如 `loadWeatherV2IntoRuntime()`、`loadLayer()`、`WeatherFieldType` 等换名后的业务包装。 +## 7. 必做项 D:MapLibre RN 与 Style Helper 收口 + +正式 package 不再把自己伪装成完整 MapLibre RN facade。 + +包根入口应停止: + +```ts +export * from "@maplibre/maplibre-react-native"; +``` + +宿主继续直接安装并使用固定 peer dependency: + +```text +@maplibre/maplibre-react-native@11.3.6 +``` + +宿主地图组件从官方包导入;粒子/图场 Runtime 从 `@navsea/maplibre-particle-rn` 导入。必须确认这不会引入第二份 MLRN native module。 + +删除业务化的 `particleWeatherIds`、`createParticleWeatherStyle()` 公共命名。建议提供纯渲染命名: + +```text +particleFieldIds +createParticleFieldStyleFragment() +``` + +Style helper 只能返回/描述 Particle 所需 source + Field layer + Particle layer,不得内置 GSI、WeatherV2 或任何业务底图。 +当前 Native attach 依赖的内部 style ID 若仍是: + +```text +rn-weather-fields +rn-weather-color +rn-weather-particles +``` + +本任务不得为了字符串改名进入 Native/Core。允许 `particleFieldIds` 暂时映射这些既有内部 ID;README 必须要求宿主使用导出的常量/helper,不要自行硬编码这些历史字符串。 + +## 8. 必做项 E:RN import 在无 Native module 平台安全 + +当前 `NativeWeatherRuntimeBridge.ts` 使用 `TurboModuleRegistry.getEnforcing()`,会让不具备 `WeatherRuntimeBridge` 的平台在 import 阶段直接失败。 + +本任务只在 RN/TS 层解决: + +- 包根入口被 import 时,不得因为 Native module 缺失立即 throw; +- `FieldRuntime` 真正执行 Native 方法时,若 bridge 不存在,应以清楚、稳定的错误失败; +- 不为此新增 iOS Native 实现; +- 不修改 Android Native bridge ABI; +- 不允许静默返回伪 `ACCEPTED` 或伪 Runtime state。 + +具体实现应依据 RN 0.85.2 TurboModule API 做最小改动。 +## 9. 必做项 F:版本关系必须真实 + +RN package 版本升级为: + +```text +@navsea/maplibre-particle-rn@13.4.1-particle.2 +``` + +但本任务不修改 Native,因此 Android Native artifact 继续使用当前已验证版本: + +```text +cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1 +``` + +不得仅为了数字一致而复制、重编或发布内容相同的 `.2` AAR。 + +`app.plugin.js` 的 dependency substitution 必须继续解析到 Native `.1` AAR,除非实际调查发现当前正式 AAR 坐标已经合法变化。 + +更新 release manifest,使其明确区分: + +```text +RN package version = 13.4.1-particle.2 +Native artifact version = 13.4.1-particle.1 +``` + +并注明 `.2` 是 RN-only public API revision,Native/Core source 与已验证 `.1` AAR 不变。 +不得覆盖或重新上传现有: + +```text +navsea-maplibre-particle-rn-13.4.1-particle.1.tgz +``` + +本任务只生成新的 `.2` 本地 pack 产物供后续验证,不负责正式静态 HTTPS 上传或 release tag。 + +## 10. 必做项 G:Consumer Test Host 同步到最终 API + +修改: + +```text +integration/react-native/MapLibreParticlePackageConsumerTestHost/ +``` + +要求: + +- `Map` / `Camera` 改为直接从 `@maplibre/maplibre-react-native` 导入; +- Renderer Runtime 只从 `@navsea/maplibre-particle-rn` 导入 `FieldRuntime` 和纯渲染类型/helper; +- 本地真实 WeatherV2 测试能力可以继续存在,但 WeatherV2 HTTP、decoder、选帧、临时文件管理全部位于 Test Host 自己的业务目录; +- 最终提交给 SDK 的仍是 canonical metadata + local binary file; +- Test Host 不得反向依赖 package 内部源码路径; +- Test Host UI 可以继续使用 Weather/ECMWF 文案,因为它是测试宿主,不是 SDK 公共接口。 +旧 `MapLibreParticleRNTestHost` 属于历史 bridge 验证宿主,不要求为了本任务全部重构;除非它参与当前 package 构建/类型检查并形成 blocker,否则不要扩大范围。 + +## 11. 必做项 H:文档更新 + +保留现有: + +```text +docs/30_设计/MapLibre粒子ReactNative使用说明_v2.md +``` + +作为 `.1` 历史接口说明,不覆盖。 + +新增: + +```text +docs/30_设计/MapLibre粒子ReactNative使用说明_v3.md +``` + +v3 必须以 `13.4.1-particle.2` 最终真实代码为准,说明: + +- 安装 `.2` RN package 与 MLRN 11.3.6; +- RN `.2` 对应 Native AAR `.1`; +- Map/Camera 从官方 MLRN 导入; +- `FieldRuntime` 全部公共方法、参数和返回值; +- canonical Field submission; +- 图场与粒子独立显示开关; +- Style fragment 如何合入宿主现有 style; +- React/宿主负责业务数据,SDK 不负责 provider/network/weather。 +同步更新: + +```text +packages/maplibre-particle-rn/README.md +packages/maplibre-particle-rn/CHANGELOG.md +packages/maplibre-particle-rn/package.json +``` + +README 不得继续推荐 WeatherV2 高层 loader,也不得把 SDK 描述成天气客户端。 + +CHANGELOG 必须明确 `.2`: + +```text +- RN 公共接口去 Weather/Wind 业务化 +- WeatherV2 helper 移出正式 package +- 图场与粒子继续消费同一 canonical Field,并可独立开关 +- Native/Core/GPU 未修改 +- 继续复用 Android AAR 13.4.1-particle.1 +``` + +## 12. 明确禁止范围 + +本任务禁止修改: + +```text +include/mbgl/** +src/mbgl/** +platform/android/MapLibreAndroid/** +shaders/** +CMake / Core renderer +Android AAR 内容与 publication 源码 +NavSea 仓库 +``` + +若实现纯 RN API 时发现必须修改以上任一 Native/Core 文件,立即停止扩大修改,在完成报告中写明 blocker 和原因。 +## 13. 静态与包验证 + +Codex 必须完成以下非 Android 验证: + +```text +1. package TypeScript typecheck +2. Consumer Test Host TypeScript typecheck +3. npm pack +4. tarball 内容审计 +5. package 根导出审计 +6. 业务语义残留审计 +7. git diff --check +``` + +`npm pack` 生成的新 `.2` tgz 不提交 Git,除非仓库当前发布规则明确要求;不得碰现有未跟踪 `.1` tgz。 + +包内容必须确认不存在: + +```text +src/weatherV2/ +WeatherV2HttpClient +loadWeatherV2WindIntoRuntime +decodeWeatherV2WindFrame +NavSea 业务源码 +Test Host +node_modules +临时日志/构建缓存 +``` + +允许内部保留 Native ABI 接缝中的 `WeatherRuntimeBridge` 名称和现有 `rn-weather-*` style ID,但这些不得作为新的业务 API 暴露。 +## 14. Android 验证边界 + +本任务不得执行 Android Gradle、AAR build/publish、APK build/install、ADB 或设备操作。 + +因为 Native AAR 不变,本任务完成后优先采用既有包含 `.1` Native bridge/AAR 的 Consumer APK,通过 Metro 验证新的 `.2` JS/TS API;只有后续确认 Native 文件或 Android 构建配置实际发生变化时,才需要用户重新编译 APK。 + +Codex 最终必须给出用户后续最小验证步骤,但不得自行执行 Android 操作。 + +## 15. Step、开发记录与完成报告 + +执行完成后新增/更新: + +```text +docs/50_Step记录/STEP-064_RN纯FieldRenderer接口V2_v1.md +docs/报告/STEP-064_RN纯FieldRenderer接口V2_Codex完成报告_v1.md +粒子开发记录.md +``` + +Step 状态在未完成人工 Consumer 验证前写为: + +```text +进行中(代码完成,待 Consumer 验证) +``` + +完成报告可使用: + +```text +IMPLEMENTED / WAITING_CONSUMER_VALIDATION +``` + +不得提前写 RELEASED、CLOSED / PASS 或 Android validated。 +## 16. Git 收尾 + +完成代码、文档和静态验证后,必须只精确 stage 本任务文件并提交,随后 push: + +```text +origin/particle/main +``` + +禁止把当前已有无关 modified/untracked 文件带入提交。 + +建议代码提交信息: + +```text +refactor: make RN particle API field-renderer only +``` + +如文档单独提交,建议: + +```text +docs: record STEP-064 RN field renderer API v2 +``` + +Push 后必须确认: + +```text +HEAD == origin/particle/main +``` + +Push 失败不得声称任务完成。 +## 17. 最终必做清单 + +- [ ] 确认 `particle/main`、HEAD、远端 HEAD 与工作树 +- [ ] 阅读任务要求与当前 `.1` 实际 package/Consumer 源码 +- [ ] RN package version 升为 `13.4.1-particle.2` +- [ ] 建立 `FieldRuntime` 纯渲染公共入口 +- [ ] 公共 API 删除 Weather/Wind 专用名称 +- [ ] 不建立旧 API 兼容 wrapper +- [ ] `submitField()` / `clearFields()` 继续薄映射既有 Native ingress +- [ ] 图场与粒子保持同一 Field、独立显示开关 +- [ ] `FieldColorStop.value` 等公共类型去业务化 +- [ ] `WeatherV2` 完全移出正式 package +- [ ] package 不再 re-export 整个 MLRN +- [ ] Style helper 改为纯 Particle Field fragment,不内置业务底图 +- [ ] 无 Native module 平台 import package 不立即崩溃 +- [ ] Consumer Test Host 改用最终 `.2` API +- [ ] WeatherV2 测试逻辑如需保留,仅存在于 Test Host 业务层 +- [ ] Native/Core/JNI/shader/AAR 无功能修改 +- [ ] Android AAR 继续为 `13.4.1-particle.1` +- [ ] release manifest 明确 RN `.2` ↔ Native `.1` +- [ ] README、CHANGELOG、React Native 使用说明 v3 更新 +- [ ] package/Consumer TypeScript 检查通过 +- [ ] `.2` `npm pack` 与 tarball 内容审计通过 +- [ ] `git diff --check` 通过 +- [ ] 更新 STEP-064、完成报告、粒子开发记录 +- [ ] 精确 commit 并 push `origin/particle/main` +- [ ] 最终报告 commit hash、message、push result、Final HEAD + +Codex 不需要在开工前提交实施计划。结束时直接按本清单逐项报告“已完成 / 未完成 / 无法验证”,任何未完成项必须说明原因,不得静默跳过。 \ No newline at end of file diff --git a/docs/40_任务/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_Codex任务_v1.md b/docs/40_任务/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_Codex任务_v1.md new file mode 100644 index 000000000..b3bd44fce --- /dev/null +++ b/docs/40_任务/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_Codex任务_v1.md @@ -0,0 +1,532 @@ +# STEP-065 MapLibreParticleLab V3 通用 Scalar / Vector Field 架构冻结 — Codex 任务 v1 + +## 0. 任务定位 + +本任务是 MapLibreParticleLab V3 的第一步:**只读调查当前真实实现,并冻结 V3 通用 Scalar / Vector Field Contract 与迁移方案。** + +本任务不直接修改 C++、shader、JNI、Kotlin/Java、React Native Runtime 或 Android AAR。 + +V3 的目标不是“增加 Rain/Wave 两种天气类型”,而是把当前主要围绕 WIND + interleaved U/V 的运行时升级为: + +> provider-neutral、business-neutral 的二维时序 Scalar / Vector Field Rendering Runtime。 + +当前已冻结的上游边界: + +```text +WeatherV2 -> NSFT -> NavSea JS/TS -> Canonical Field -> MapLibreParticleLab V3 +``` + +WeatherV2 统一向 NavSea 提供 NSFT;NavSea JS/TS 负责网络、缓存、NSFT 解码、tile 拼接、业务单位与 canonical 化;MapLibreParticleLab 从 canonical field 开始接管。 +## 1. 仓库与执行基线 + +仓库: + +```text +/mnt/sda1/MapLibreParticleLab +``` + +分支:`particle/main` + +任务书生成时 HEAD: + +```text +2e3ab26275a8d1873d764cf6927783a7b0406836 +``` + +该 HEAD 已完成并冻结 STEP-064 RN 纯 Field Renderer `13.4.1-particle.2`。 + +执行开始时必须重新确认: + +```bash +git branch --show-current +git rev-parse HEAD +git rev-parse origin/particle/main +git status --short +``` + +若远端已合法推进,先阅读新增提交后再继续;禁止 reset、checkout 覆盖或回退用户工作。 +当前工作区已知存在与本任务无关的用户修改和历史未跟踪文件,特别包括 STEP-062 文档、Android/Expo/build 目录、旧任务书和 tgz。 + +必须遵守: + +- 禁止 `git add .`、`git add -A`。 +- 禁止删除、恢复、格式化、移动无关 modified/untracked 文件。 +- 本任务只允许新增/修改 STEP-065 明确列出的文档产物和本任务书自身。 +- 本任务不允许以“清理工作区”为理由处理历史文件。 + +## 2. 执行前必须阅读 + +至少完整阅读: + +```text +AGENTS.md +PARTICLE_BASELINE.md +docs/00_项目规范/01_Codex协作规范_v1.md +docs/00_项目规范/02_构建与验证规范_v1.md +docs/00_项目规范/03_Step阶段记录规范_v1.md +docs/00_项目规范/05_粒子架构原则_v1.md +docs/40_任务/STEP-064_RN纯FieldRenderer接口V2_Codex任务_v1.md +docs/50_Step记录/STEP-064_RN纯FieldRenderer接口V2_v1.md +docs/报告/STEP-064_RN纯FieldRenderer接口V2_Codex完成报告_v1.md +``` +并重点阅读当前真实实现: + +```text +include/mbgl/particle/field/field_data_adapter.hpp +src/mbgl/particle/field/field_data_adapter.cpp +所有 Timeline / Runtime Field Window 实现 +WeatherColorLayer 相关 C++ / shader / shader generator +RasterParticleLayer 相关 C++ / particle simulation shader +GPU field texture / upload / reuse / lifecycle 实现 +Android Host WeatherRuntime / JNI submission +packages/maplibre-particle-rn/src/index.ts +packages/maplibre-particle-rn/src/types.ts +packages/maplibre-particle-rn/src/FieldRuntime.ts +packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts +integration/react-native 中当前正式 Consumer/Test Host +``` + +必须通过当前源码回答问题,不得根据历史任务书猜实现。 + +## 3. WeatherV2 外部输入事实与只读核对 + +WeatherV2 实际目录: + +```text +/root/sourceserver/weatherV2 +``` +可只读核对以下文件,不得修改 WeatherV2: + +```text +src/dev/core/nsft.py +src/dev/core/field_pmtiles.py +src/dev/layers/wind.py +src/dev/layers/tide.py +src/dev/layers/rain.py +src/dev/layers/waves.py +``` + +已调查结论必须作为 V3 设计输入: + +- Wind NSFT:物理速度 U/V,单位 m/s,vector-only。 +- Tide/Current NSFT:物理速度 U/V,单位 m/s,vector-only。 +- Rain NSFT:rain rate scalar + valid mask;当前 NSFT 不保存 precipitation type。 +- Wave NSFT:height scalar + period scalar + normalized direction-to U/V;direction U/V 不是 m/s。 +- NSFT grid 为 `wm_grid_257`,NW origin,行 north->south,列 west->east。 +- 同 zoom 相邻 257 tile 有共享边界,NavSea 拼接后 canonical grid 可保持 `(tiles*256)+1` 规则,无需 Core 理解 tile。 +- Forecast target time 必须按实际 validTime/time_index 使用,Core 不假设固定 1h cadence。 + +这些事实如与当前 WeatherV2 代码不一致,报告差异并以当前代码为准。 +## 4. V3 冻结边界 + +V3 Core/Native 不得负责: + +- NSFT / PMTiles / WeatherV2 解析; +- HTTP、provider、manifest、time_index、cache、retry、prefetch; +- ECMWF / CMEMS 等业务来源; +- Wind / Rain / Wave / Tide 业务分支; +- kt、mm/h、m、m/s 等宿主业务单位转换; +- NavSea 当前 Layer、Forecast UI 或业务状态。 + +V3 从 Canonical Field Submission 开始负责: + +- descriptor / payload validation; +- Snapshot; +- Timeline / target time / playback; +- T0/T1/MIX; +- Runtime Field Window; +- OLD_T1 -> NEW_T0 reuse; +- GPU channel resource 生命周期; +- Field Renderer; +- Particle Renderer; +- MapLibre style/viewport lifecycle recovery; +- fail-closed。 +## 5. 必须冻结的最小 Canonical 模型 + +V3.0 只设计: + +```text +FieldSnapshot +├─ common descriptor +├─ scalar channel? 0 or 1 +├─ vector channel? 0 or 1 (U/V) +└─ shared validity +``` + +禁止第一版扩成 N scalar / N vector generic graph。 + +必须支持三种合法组合: + +```text +vector-only +scalar-only +scalar + vector +``` + +Neither 必须 invalid / reject。 + +优先扩展当前 `ParticleFieldDescriptor / ParticleFieldSnapshot / ParticleFieldDataAdapter`,禁止并行创建 `GenericWeatherField`、`WindFieldV2` 等第二套重复模型。 + +当前 `fieldType == "WIND"`、`displayDomain == "GLOBAL"` 等硬编码必须被调查并给出 V3 最小迁移方案;Core 最终不得靠 WIND/WAVE/RAIN/TIDE 决定数据能力。 +## 6. Renderer Contract 必须与数据 Contract 分离 + +必须调查并冻结以下原则: + +`FieldSnapshot` 只描述“有什么数据”;Renderer config 描述“如何画”。 + +第一版 Field Renderer 的通用值来源建议为: + +```text +SCALAR +VECTOR_MAGNITUDE +``` + +即: + +- `SCALAR`:色场读取 explicit scalar channel; +- `VECTOR_MAGNITUDE`:色场读取 `sqrt(U^2 + V^2)`。 + +不得让 Renderer 根据 business type 或“有没有 scalar”偷偷猜来源。 + +必须调查当前 WeatherColorLayer/shader 是否硬依赖 `length(U,V)`,以及将其泛化所需最小接口、uniform/property、GPU resource 与 style property 改动。 + +`FieldColorStop.value` 的 RN 公共语义保持 generic;调查 Native/JNI/C++ 内 `speedMps` 命名实际范围,并给出兼容迁移路径。 +## 7. Vector Semantics 必须业务无关 + +必须调查当前 Particle Integrator / shader 是否隐含: + +```text +U/V = physical velocity (m/s) +``` + +V3 需要覆盖至少两类数学语义: + +```text +VELOCITY +DIRECTION +``` + +目标映射: + +- Wind/Tide -> VELOCITY; +- Wave direction-to U/V -> DIRECTION。 + +不得在 Core 增加 Wind/Wave/Tide enum 来解决。 + +对于 DIRECTION,设计必须说明: + +- T0/T1 vector mix 后是否 normalize; +- 接近零向量/接近反向插值如何 fail-closed; +- particle movement 的独立 advection scale 放在哪一层; +- 如何保证 VELOCITY 当前 Wind 行为不被改变。 + +本任务只冻结方案,不实现 Wave physics。 +## 8. Binary Submission / Channel Plane 设计 + +当前 RN 公共入口是: + +```text +submitField(metadataJson, payloadFileUri) +``` + +当前 Core canonical payload 是: + +```text +float32-le / interleaved-uv +``` + +必须调查 RN -> Android bridge -> JNI -> Core 的真实 ownership、文件读取、byte[]/buffer、size limit 与 decode 流程。 + +V3 优先评估独立 plane: + +```text +metadata +vectorPayload? // float32-le interleaved U/V +scalarPayload? // float32-le scalar plane +``` + +不得使用巨大 JS number[]、JSON Array 或 Base64 作为正式大数据通路。 + +必须明确:scalar-only、vector-only、scalar+vector 三种提交如何表达;文件引用是一个 container 还是两个 file URI;JNI/Core 最小修改面是什么。 + +独立 plane 是首选方向,但最终冻结必须依据当前代码,不能为了迎合任务书而忽略更安全的现有接缝。 +## 9. Timeline / Runtime Window 保护项 + +以下现有能力视为 V3 保护基线,不得在设计中推倒重来: + +```text +Core-owned Timeline / Playback +T0 / T1 / MIX +正常连续播放 simulationRebuild=false +正常连续播放 trailCleared=false +Runtime Field Window +OLD_T1 -> NEW_T0 reuse +only NEW_T1 upload +Pause 语义 +Invalid / Gap fail-closed +WeatherColorLayer 与 RasterParticleLayer 分离 +``` + +V3 设计要说明如何把 Snapshot GPU resource 从单 vector texture 扩展成: + +```text +SnapshotGpuResources +├─ scalar texture? +├─ vector texture? +└─ validity resource +``` + +并保持整个 Snapshot 在 rollover 时一起 reuse,Host 不参与 GPU reuse。 +## 10. Canonical schema 版本决策 + +必须明确回答: + +```text +maplibre-particle-field/v1 additive extension +还是 +maplibre-particle-field/v2 +``` + +当前已知 v1 descriptor 是单 `layout/encoding/byteLength/stride`,Snapshot 只有 `velocity`,RN `submitField()` 只有一个 payload file URI,且 adapter 严格接受 `interleaved-uv`。 + +因此本任务的默认判断倾向:**如果 Scalar + Vector 会导致字段重载、隐式兼容或 ambiguous payload,则应正式升 v2,而不是污染 v1。** + +但最终结论必须根据完整 parser/bridge/fixture/consumer 调查给出。 + +如果选择 v2,设计必须说明: + +- v1 Vector-only 是否继续可读; +- compatibility 应放在哪个最薄层; +- 是否允许 v1/v2 同时进入同一个 Runtime; +- 新 RN package 版本至少应升到 `13.4.1-particle.3`,不得覆盖已发布 `.2`; +- Native/AAR version 如何独立推进。 + +不得为了“兼容”长期维护两套 Runtime。 +## 11. Validity 与业务映射冻结 + +V3.0 优先保持一个 Snapshot 一个共享 validity,不提前设计 channel-specific mask。 + +必须给出四类 WeatherV2 -> NavSea canonical -> V3 的映射表,至少明确: + +```text +Wind : vector(U/V velocity) -> field=VECTOR_MAGNITUDE, particle=VELOCITY +Tide : vector(U/V velocity) -> field=VECTOR_MAGNITUDE, particle=VELOCITY +Rain : scalar(rainRate) -> field=SCALAR, particle=unavailable +Wave : scalar(height) + vector(direction-to U/V) + -> field=SCALAR, particle=DIRECTION +``` + +Wave `period` 当前保留在 NavSea Weather model/cache,V3.0 不因此增加第二 Scalar Channel。 + +Rain `precipitation_type` 当前未进入 NSFT,因此 V3 不推断、不补造该数据。 + +Field/Particle visibility 必须继续独立: + +```text +Field ON / Particle OFF +Field OFF / Particle ON +Field ON / Particle ON +``` + +没有 vector 时 Particle unavailable / reject,不允许 scalar 伪装成 U/V。 +## 12. 必须输出的调查结论 A-J + +设计文档必须逐项回答: + +A. 当前 Field Contract 实际结构与 Wind-only 硬编码点。 + +B. 当前 GPU Texture / upload / validity / shader 的实际结构。 + +C. 当前 Particle Integrator 对 vector 数值、单位、dt、scale 的实际假设。 + +D. 当前 RN -> Android -> JNI -> Core binary submission 路径与 ownership。 + +E. Scalar Channel 的最小扩展方案与受影响对象。 + +F. Wave direction-only 的最小、业务无关表达方案。 + +G. v1 additive extension 还是 canonical v2,并说明理由。 + +H. 预计受影响文件清单,按 Core / shader / Android / RN / Test Host 分类。 + +I. 向后兼容、性能、GPU memory、lifecycle、Timeline 风险。 + +J. 推荐的正式实施批次、验证门槛和 Android 最终一次综合验收范围。 +## 13. 推荐实施路线必须被审查并冻结 + +设计结果至少应评估以下批次是否合理: + +```text +V3-02 Core Contract + Scalar Snapshot/Adapter +V3-03 Scalar GPU Texture + Generic Field Renderer +V3-04 VectorSemantics VELOCITY/DIRECTION + Particle +V3-05 Android/JNI/RN generic binary submission +V3-06 Wind/Rain/Wave 三类 fixture + Tide 回归 +V3-07 Linux/Core/OpenGL 精确验证与文档冻结 +V3-08 用户一次 Android build/install/device 综合验收 +``` + +可以根据当前代码合并或调整批次,但必须遵守: + +- 尽量减少 Android 重复编译/安装周期; +- 在 Linux/Core 可验证的内容先集中完成; +- 不因单一小按钮/日志单独触发 Android 周期; +- 高风险接口分叉必须在 Android 实现前冻结。 + +最终路线要给每个批次明确“修改范围 / 不修改范围 / 完成门槛 / 下一批入口”。 + +## 14. Validation Fixture 规划 +V3 后续至少规划三类数学能力 fixture: + +```text +A. Vector-only : 真实 WeatherV2 Wind +B. Scalar-only : 真实 WeatherV2 Rain +C. Scalar+Vector: 真实 WeatherV2 Wave +``` + +Tide 作为第二套 VELOCITY vector 做回归,不单独创造业务代码路径。 + +Wave 必须规划一个独立性 synthetic fixture: + +```text +scalar: west low -> east high +vector: 全场 north +``` + +预期: + +```text +色场只呈东西渐变 +粒子只统一向北 +``` + +该 fixture 用于证明 Field Renderer 不偷读 vector magnitude,Particle Renderer 不偷读 scalar。 + +同时规划 T0/T1 scalar、T0/T1 vector、共同 MIX、window rollover、style reload、visibility、invalid/gap fail-closed 验证。 +## 15. 本任务禁止事项 + +本任务不得: + +- 修改任何生产代码; +- 修改 shader / generator; +- 修改 Android/JNI/RN API; +- 生成新的 AAR/tgz release; +- 执行 Android Gradle build、APK 安装或 ADB 验收; +- 把 NSFT decoder 放入 ParticleLab; +- 新增 Weather Provider / HTTP / cache; +- 新增 WIND/WAVE/RAIN/TIDE Core 分支; +- 实现 Wave physics; +- 设计 N-channel framework; +- 做 JSI/zero-copy 大重构; +- 加入或执行 Jest; +- 修改 NavSea 或 WeatherV2。 + +本任务是设计冻结任务,不是实现任务。 + +## 16. 必做文档输出 + +新增: +```text +docs/20_调查/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField现状调查_v1.md +docs/30_设计/MapLibreParticleLab_V3通用ScalarVectorField运行时架构设计_v1.md +docs/50_Step记录/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_v1.md +docs/报告/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_Codex完成报告_v1.md +``` + +并追加更新: + +```text +粒子开发记录.md +``` + +所有文档必须使用中文,代码标识符、路径、schema/property 名保持实际英文。 + +设计文档必须包含至少: + +- 当前真实架构图; +- V3 目标架构图; +- Scalar / Vector / validity 数据模型; +- Renderer config 与 Snapshot data 的边界; +- Binary plane 方案; +- Timeline/GPU reuse 迁移; +- API/schema 版本决策; +- WeatherV2 四层映射; +- 受影响文件矩阵; +- 风险与实施顺序。 +## 17. 必做清单 + +执行结束前逐项对照,不得静默跳过: + +- [ ] 读取全部项目规范与 STEP-064 冻结基线。 +- [ ] 重新确认 branch / HEAD / origin / dirty worktree。 +- [ ] 调查 Descriptor / Snapshot / Adapter 当前实际结构。 +- [ ] 找全 WIND / GLOBAL / fieldType 等行为硬编码。 +- [ ] 调查 Runtime Window / Timeline / MIX / GPU reuse。 +- [ ] 调查 WeatherColorLayer value 获取和 `speedMps` 链路。 +- [ ] 调查 Particle Integrator 的 velocity 假设。 +- [ ] 调查 RN -> Android -> JNI -> Core binary submission。 +- [ ] 只读核对 WeatherV2 NSFT 四层语义。 +- [ ] 冻结 0/1 Scalar + 0/1 Vector + shared validity 模型。 +- [ ] 冻结 FieldValueSource 与 VectorSemantics 方案。 +- [ ] 冻结 binary payload / file-reference 方案。 +- [ ] 明确 canonical v1 additive 或 v2。 +- [ ] 给出 V3 分批实施路线与综合 Android 验收计划。 +- [ ] 生成调查、设计、Step、完成报告。 +- [ ] 追加粒子开发记录。 +- [ ] 确认没有修改生产代码和无关用户文件。 +## 18. Git 收尾要求 + +本任务会新增/修改项目文档,因此完成后必须 Git 收尾并 push 到公开远端 `particle/main`。 + +提交前必须再次执行 `git status --short`,只暂存本任务明确产物: + +```text +docs/40_任务/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_Codex任务_v1.md +docs/20_调查/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField现状调查_v1.md +docs/30_设计/MapLibreParticleLab_V3通用ScalarVectorField运行时架构设计_v1.md +docs/50_Step记录/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_v1.md +docs/报告/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_Codex完成报告_v1.md +粒子开发记录.md +``` + +禁止把任何既有 STEP-062 修改、历史 untracked、build/cache/android/tgz 一并提交。 + +完成后: + +1. 创建 scoped commit; +2. push 到 `origin/particle/main`; +3. 确认远端 HEAD; +4. 在完成报告记录 commit hash、commit message、push 结果、最终远端分支/HEAD。 + +push 失败不得声称任务完成,必须明确报告原因。 +## 19. 完成报告格式 + +完成报告必须按必做项逐项写: + +```text +已完成 / 未完成 / 无法验证 +``` + +并明确: + +- 本任务是否修改生产代码:预期“否”; +- WeatherV2 是否被修改:预期“否”; +- Android Gradle / APK / ADB:预期“未执行,按任务边界”; +- Jest:预期“未加入、未执行”; +- 当前最终 V3 schema 推荐; +- 下一实施任务建议名称与范围; +- 所有未解决设计风险。 + +## 20. 最终完成标准 + +只有以下全部满足,STEP-065 才可标记 CLOSED / PASS: + +1. 当前实际代码调查完整,并有文件/符号级依据; +2. V3 Scalar/Vector/validity Contract 已冻结; +3. Renderer data source 与 VectorSemantics 已冻结; +4. binary submission 与 schema version 路线已冻结; +5. Timeline/Runtime Window/GPU reuse 保护方案明确; +6. V3 分批实现和最终一次 Android 综合验收路线明确; +7. 四份正式文档 + 粒子开发记录完成; +8. 无生产代码、WeatherV2、NavSea 和无关工作区文件被修改; +9. scoped commit 已成功 push 到 `origin/particle/main`。 diff --git a/docs/40_任务/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_Codex任务_v1.md b/docs/40_任务/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_Codex任务_v1.md new file mode 100644 index 000000000..592ad789e --- /dev/null +++ b/docs/40_任务/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_Codex任务_v1.md @@ -0,0 +1,318 @@ +# STEP-066 MapLibreParticleLab V3-02 Core Contract + Scalar Snapshot/Adapter — Codex任务 v1 + +## 0. 任务定位 + +本任务承接 STEP-065 已冻结的 MapLibreParticleLab V3 架构,只实施 **Core Contract / Snapshot / Adapter 层**。 + +目标:把当前 Wind-only、单 U/V payload 的 `maplibre-particle-field/v1` Core 输入模型,升级为 V3 内部统一的 generic snapshot,支持: + +- vector-only +- scalar-only +- scalar + vector +- shared validity +- vector semantics: `VELOCITY | DIRECTION` +- v1 vector-only 作为最薄兼容 ingress normalize 到同一套 V3 snapshot + +本任务不进入 GPU、shader、Android/JNI、RN、NavSea、WeatherV2,也不做 Android 构建。 + +## 1. 当前基线 + +仓库:`/mnt/sda1/MapLibreParticleLab` + +分支:`particle/main` + +任务书创建时 HEAD / origin: + +`d23ae2079dcfef3ce48517f734fe487207e28d23` + +执行时必须重新确认 branch、HEAD、origin 和 `git status --short`。若远端已合法推进,先阅读新增提交,不得 reset、checkout 覆盖或丢弃用户修改。 + +当前工作区已有 STEP-062 用户修改和大量历史 untracked/build 产物,全部视为无关用户内容。禁止 `git add .`、`git add -A`、clean/reset/stash 覆盖它们。 + +## 2. 开工前必须阅读 + +必须完整阅读: + +- `AGENTS.md` +- `PARTICLE_BASELINE.md` +- `docs/00_项目规范/` +- `docs/20_调查/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField现状调查_v1.md` +- `docs/30_设计/MapLibreParticleLab_V3通用ScalarVectorField运行时架构设计_v1.md` +- `docs/50_Step记录/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_v1.md` +- `docs/报告/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_Codex完成报告_v1.md` +- 当前 `field_data_adapter.*` +- 当前 `local_particle_field_feed.*` +- 当前 `raster_array_source.*` 中 host field parser / Timeline / Runtime Window 相关实现 +- 与 `ParticleFieldDescriptor`、`ParticleFieldSnapshot`、`ParticleFieldTimeWindow`、`sameFieldGpuContent`、`fieldIdentityChanged` 直接相关的所有调用点 +- 现有相关 C++ tests/fixtures + +不得根据 STEP-065 文档示例直接猜当前源码。 + +## 3. 冻结架构,禁止重新讨论 + +STEP-065 已冻结以下方向,本任务必须按此实现,不重新发明第二套模型: + +1. 新 canonical schema:`maplibre-particle-field/v2`。 +2. 一个 Snapshot:0/1 scalar + 0/1 vector + 1 shared validity。 +3. scalar-only、vector-only、scalar+vector 合法;neither 非法。 +4. vector 为 `float32-le / interleaved-uv`,U eastward、V northward。 +5. scalar 为 `float32-le` 单 plane。 +6. validity 为显式 shared logical validity;V3 Core 内统一为 `std::vector` 或现有等价表示。 +7. `VectorSemantics = VELOCITY | DIRECTION`,只在 vector present 时合法。 +8. `fieldType` / `displayDomain` 不再作为 Core capability 或 validity 判断依据。 +9. Renderer 的 `FieldValueSource` 属于后续 V3-03,不在本任务实现。 +10. Timeline / T0-T1 / MIX / Pause / rollover ownership 不重构。 + +## 4. 本任务必须形成的 Core 模型 + +优先扩展现有 `ParticleFieldDescriptor` / `ParticleFieldSnapshot`,不要创建 `GenericWeatherField`、`RainField`、`WaveField` 等平行模型。 + +目标语义至少包含: + +```text +ParticleFieldSnapshot + common identity/grid/time + scalar? : width*height float + vector? : width*height*2 float interleaved UV + valid : width*height uint8 shared validity + vectorSemantics? : VELOCITY | DIRECTION +``` + +字段命名可结合现有代码做最小调整,但必须 generic,不出现 Wind/Rain/Wave/Tide 业务枚举。 + +## 5. v2 Descriptor / Plane Bundle + +本任务需要在 Core 层定义明确的 v2 descriptor 和 decode 输入形态。 + +推荐采用独立 plane bundle,而不是把三种数据重新塞回一个 ambiguous `std::vector`: + +```text +ParticleFieldPayloadBundle + scalar? bytes + vector? bytes + validity? bytes +``` + +具体 C++ 类型可根据当前接口选择 `std::optional>`、span/view 或等价简单结构,但不能引入大型新抽象。 + +必须保证: + +- vector plane expected bytes = `cells * 2 * sizeof(float)` +- scalar plane expected bytes = `cells * sizeof(float)` +- validity plane expected bytes = `cells`(uint8) +- descriptor 声明的 channel presence 与实际 payload presence 一致 +- 所有 float 必须 finite,除明确 noData 策略外不得接受 NaN/Inf +- shared validity 为 0 的 cell 可以允许 payload 内存在 finite placeholder/noData;validity=1 的 cell 对 present channel 必须是有效 finite 数据 +- 不允许 neither +- DIRECTION 但无 vector 必须 reject +- vector present 但缺 vector semantics 必须 reject(除 v1 normalize) + +## 6. v1 最薄兼容 ingress + +现有 `maplibre-particle-field/v1` 必须继续可读,但不要维护第二套 Runtime。 + +v1 只支持现有 vector-only: + +- `interleaved-uv` +- `float32-le` +- validity 从当前 U/V noData 规则派生 +- normalize 后:`vector present`、`scalar absent`、`VectorSemantics::VELOCITY` + +兼容层应该尽量放在 parser/adapter ingress,进入 Timeline 前就成为与 v2 相同的统一 snapshot。 + +不得给 v1 增加 scalar 字段、不得重新定义 v1 payload。 + +## 7. Identity / compatibility / hashing + +必须重新审查并调整: + +- `ParticleFieldSnapshot::isValid()` +- `isTimeInterpolationCompatibleWith()` +- `fieldIdentityChanged()` +- `sameFieldGpuContent()` + +要求: + +### isValid +不再硬编码 `fieldType == WIND` / `displayDomain == GLOBAL`。 + +必须验证至少:identity、generation、validTime、grid/bounds、channel presence、plane sizes、shared validity size、vector semantics 与 channel presence 的一致性。 + +### Timeline compatibility +T0/T1 必须至少保持: + +- width/height 相同 +- bounds 相同 +- scalar presence 相同 +- vector presence 相同 +- vector semantics 相同 +- plane encoding/layout 兼容 + +不得允许 scalar-only 与 vector-only 被当成同一 interpolation window。 + +### GPU content identity +虽然本任务不改 GPU,但 `sameFieldGpuContent()` 必须能够在未来正确区分 scalar/vector/validity 内容变化。 + +如果当前单 `fieldSha256` 已无法无歧义表示多 plane,应在 Core contract 中明确设计并实现最小 content identity,例如 per-plane hash 或一个明确的 aggregate hash。不得靠 fieldId/dataVersion 猜内容相同。 + +## 8. RasterArraySource host parser 范围 + +本任务只允许修改 host metadata/parser 到能够构造 v1/v2 Core descriptor/plane bundle 的程度。 + +若当前 `RasterArraySource::submitHostField(metadataText, payload)` 是唯一入口,允许在 C++ 层增加一个 **Core-only v2 overload / typed internal entry**,用于 tests/fixtures 直接提交 plane bundle。 + +但本任务禁止: + +- 修改 Java/Kotlin API +- 修改 JNI signature +- 修改 RN API +- 修改已发布 `.2` package + +因此 Android/RN 仍只能走现有 v1 入口是允许的;v2 正式跨 JNI ingress 留到 V3-05。 + +## 9. Local fixture / feed + +若 `local_particle_field_feed` 或现有 C++ fixture 是最合适的验证入口,可做最小扩展以生成: + +1. v1 vector-only fixture +2. v2 vector-only fixture +3. v2 scalar-only fixture +4. v2 scalar+vector fixture +5. invalid/neither fixture +6. channel mismatch fixture + +Fixture 只用于 Core contract 验证,不做 GPU 可视化。 + +## 10. 必做验证 + +不加入 Jest。 + +必须使用现有 C++ 测试体系或最小精确 executable/test,至少验证: + +- v1 Wind-style vector-only decode PASS,并 normalize 为 VELOCITY vector +- v2 vector-only PASS +- v2 scalar-only PASS +- v2 scalar+vector PASS +- neither REJECT +- scalar byteLength mismatch REJECT +- vector byteLength mismatch REJECT +- validity byteLength mismatch REJECT +- missing vector semantics REJECT +- DIRECTION without vector REJECT +- NaN/Inf in valid cell REJECT +- invalid cell 可按冻结策略安全接受 finite noData/placeholder +- T0/T1 compatible:same channel shape/semantics PASS +- scalar-only vs vector-only incompatible +- VELOCITY vs DIRECTION incompatible +- changed scalar/vector/validity content 不会被 `sameFieldGpuContent()` 误判为相同 + +执行规定的精确编译/测试,编译输出遵循全局 `$build-output-throttle`。 + +不得执行 Android Gradle、AAR、APK、ADB。 + +## 11. 回归保护线 + +本任务不得破坏: + +- 当前 v1 canonical Wind fixture parser +- Source-owned Timeline +- Runtime Field Window +- T0/T1/MIX +- Pause semantics +- OLD_T1→NEW_T0 reuse 的现有决策逻辑 +- normal rollover 时 `simulationRebuild=false / trailCleared=false` 的架构条件 +- WeatherColorLayer / RasterParticleLayer 分离 + +本任务不要求 renderer 能消费 scalar;在 V3-03 前 scalar snapshot 进入 renderer 应保持 fail-closed/unavailable,而不是伪造成 U/V。 + +## 12. 禁止事项 + +不得: + +- 修改 shader 或 shader generator +- 修改 `render_weather_color_layer.cpp` / `render_particle_validation_layer.cpp` 的渲染语义,除非纯编译适配且不改变行为;若确需适配必须在报告逐项列出 +- 增加 FieldValueSource +- 实现 DIRECTION particle movement +- 增加 Wave physics +- 修改 Android/JNI/Java/Kotlin +- 修改 RN package/API/version +- 修改 Test Host JS/TS +- 修改 WeatherV2 / NavSea +- 解析 NSFT/PMTiles +- 新增 provider/network/cache +- 设计 N scalar/N vector channel framework +- 做 JSI/zero-copy 重构 +- 执行 Jest +- 执行 Android build/install/ADB + +## 13. 文档输出 + +必须新增/更新: + +- `docs/50_Step记录/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_v1.md` +- `docs/报告/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_Codex完成报告_v1.md` +- 追加 `粒子开发记录.md` + +若实施中对 STEP-065 冻结设计产生必要的小修正,必须更新 `docs/30_设计/MapLibreParticleLab_V3通用ScalarVectorField运行时架构设计_v1.md` 并明确原因;禁止静默改变设计。 + +所有文档使用中文。 + +## 14. 必做清单 + +结束前逐项报告“已完成 / 未完成 / 无法验证”: + +- [ ] 阅读规范、STEP-065 与当前实际源码 +- [ ] 确认 branch/HEAD/origin/dirty worktree +- [ ] 定义 V3 Core generic snapshot/descriptor/payload bundle +- [ ] 去除 Core validity 中 WIND/GLOBAL capability hardcode +- [ ] 实现 v2 vector-only +- [ ] 实现 v2 scalar-only +- [ ] 实现 v2 scalar+vector +- [ ] 实现 shared validity +- [ ] 实现 VectorSemantics contract +- [ ] 实现 v1 vector-only thin normalize +- [ ] 调整 interpolation compatibility +- [ ] 调整 content identity / sameFieldGpuContent +- [ ] 保持 Timeline/Runtime Window ownership 不变 +- [ ] 加入最小 Core fixtures/tests +- [ ] 完成精确 Linux/Core 编译/测试 +- [ ] `git diff --check` PASS +- [ ] 确认无 Android/JNI/RN/WeatherV2/NavSea 修改 +- [ ] 生成 Step、完成报告、追加开发记录 + +不得静默跳过。 + +## 15. Git 收尾 + +完成本任务规定修改与文档后,只提交本任务相关文件到当前 `particle/main` 并 push `origin/particle/main`。 + +禁止提交现有 STEP-062 用户修改、历史 untracked、build/cache/android/tgz。 + +必须使用显式文件列表 `git add -- `,禁止 `git add .` / `-A`。 + +完成报告必须记录: + +- source/function commit hash 与 message +- docs handoff commit(如有) +- push 结果 +- 最终 `HEAD` +- 最终 `origin/particle/main` +- 未完成/无法验证项 + +push 失败不得声称完成。 + +## 16. 完成门槛 + +STEP-066 只有同时满足以下条件才可 CLOSED / PASS: + +1. v1 vector-only 继续可 decode,并 normalize 到统一 V3 snapshot; +2. v2 vector-only/scalar-only/scalar+vector 三种组合全部 Core 验证通过; +3. neither、尺寸/semantic/finite/validity 错误全部 fail-closed; +4. Timeline compatibility 已按 channel presence + vector semantics 收紧; +5. content identity 不会把 plane 内容变化误判为 GPU same-content; +6. 未改 GPU 渲染语义、Android/JNI/RN、WeatherV2/NavSea; +7. 精确 Linux/Core 编译/测试与 `git diff --check` 通过; +8. Step/报告/开发记录完成; +9. scoped commit 成功 push 到 `origin/particle/main`。 + +下一步仅在 STEP-066 CLOSED / PASS 后进入 V3-03:Scalar GPU Texture + Generic Field Renderer。 diff --git a/docs/40_任务/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_Codex任务_v1.md b/docs/40_任务/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_Codex任务_v1.md new file mode 100644 index 000000000..248f3be89 --- /dev/null +++ b/docs/40_任务/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_Codex任务_v1.md @@ -0,0 +1,169 @@ +# STEP-067 MapLibreParticleLab V3-03 Scalar GPU + Generic Field Renderer — Codex任务 v1 + +## 0. 任务定位 + +本任务承接已关闭的 STEP-066,只实施 **GPU field bundle / WeatherColor generic value source**。 + +目标:让 V3 scalar snapshot 首次可由 WeatherColorLayer 真正渲染,并保留 vector magnitude 色场;不实现 DIRECTION 粒子推进,不做 JNI/RN v2 ingress,不做 Android 编译。 + +## 1. 当前基线 + +仓库:`/mnt/sda1/MapLibreParticleLab` + +分支:`particle/main` + +任务书创建时: + +`HEAD == origin/particle/main == 9dabb95e4425ffbcb863c520923f5d4b537a3feb` + +执行时重新确认 HEAD、origin、`git status --short`,不得 reset/clean/stash。 + +STEP-062 用户修改、历史 untracked、build/cache/tgz 均视为无关内容,禁止 `git add .` / `git add -A`。 +## 2. 开工前必须完整阅读 + +- `AGENTS.md` +- `PARTICLE_BASELINE.md` +- `docs/00_项目规范/` +- `docs/20_调查/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField现状调查_v1.md` +- `docs/30_设计/MapLibreParticleLab_V3通用ScalarVectorField运行时架构设计_v1.md` +- `docs/50_Step记录/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_v1.md` +- `docs/报告/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_Codex完成报告_v1.md` +- 当前 `field_data_adapter.*` +- 当前 `render_weather_color_layer.*` +- 当前 `render_particle_validation_layer.*` 中 field texture / upload / reuse 路径 +- `weather_color_layer.*`、`weather_color_stop.hpp`、相关 shader/UBO/generator 与 tests + +不得按任务书猜源码;先以当前 HEAD 为准。 + +## 3. 冻结语义 + +1. `FieldValueSource = SCALAR | VECTOR_MAGNITUDE`,必须显式选择,不能根据 channel presence 猜。 +2. `SCALAR` 要求 scalar present;`VECTOR_MAGNITUDE` 要求 vector present;不满足则 WeatherColor renderer fail-closed/unavailable。 +3. Wind/Tide 使用 `VECTOR_MAGNITUDE`;Rain/Wave color 使用 `SCALAR`。 +4. Particle renderer 本任务仍只接受 `VectorSemantics::Velocity`;DIRECTION 留 V3-04。 +5. T0/T1/MIX、Pause、Timeline ownership、OLD_T1→NEW_T0 reuse、only NEW_T1 upload 的架构不变。 +6. WeatherColorLayer 与 RasterParticleLayer 继续分离,但消费同一 Source-owned snapshots。 +7. `FieldColorStop.value` 是 generic 值。C++ 内部 `speedMps` 命名必须在本 Step 收敛为 generic `value`/`fieldValue`;Android/JNI/RN 旧 ABI 本 Step 不修改。 +## 4. 必做实现 + +### A. Generic FieldValueSource + +在 C++ style/render 层新增最小 enum/config,推荐:`enum class FieldValueSource { Scalar, VectorMagnitude };`。 + +放在与 WeatherColorLayer 最贴近且不引入业务域的位置。WeatherColorLayer 必须持有明确 value source;默认值必须保持现有 Wind 行为,即 `VECTOR_MAGNITUDE`,保证旧路径兼容。 + +### B. WeatherColor scalar/vector GPU 输入 + +扩展现有 WeatherColor field texture 生成/上传,使: + +- `VECTOR_MAGNITUDE`:继续从 vector U/V 得到 magnitude 语义;不得改变现有 Wind 可视结果。 +- `SCALAR`:真正上传/编码 scalar 值,shader 根据 T0/T1 MIX 对 scalar 做时间插值后进入相同 generic color ramp。 +- shared validity 必须进入 alpha/有效性判断;T0/T1 任一无效时按现有 fail-closed 策略不可显示错误值。 +- 不允许 scalar 伪装成 U/V。 + +可继续复用现有 texture/UBO/shader 基础设施,但要让 field texture 的数据语义显式,不要复制第二套 WeatherColor renderer。 + +### C. Generic color ramp 命名 + +把 C++ Core/style/render/shader 中与 WeatherColor 色阶相关的 `speedMps` / 固定 speed 语义改成 generic value。至少: + +- `WeatherColorStop.speedMps` → generic `value` +- 校验逻辑变成 finite、严格递增 generic value +- shader/UBO 注释和日志不再声称单位固定 m/s +- 删除/替换 `scale=0..30mps` 这类 Wind-only 日志 + +不得改变 RN `.2` 公共 API,也不得修改 Android/JNI/Kotlin/TS。 +### D. Particle renderer 保护 + +本任务不得实现 DIRECTION advection。现有 RasterParticleLayer 必须显式要求: + +- vector present +- `VectorSemantics::Velocity` + +scalar-only 或 DIRECTION vector 必须 fail-closed,不创建/继续使用错误粒子资源。 + +### E. GPU reuse/content identity + +复核 WeatherColor 的 T0/T1 resource reuse: + +- normal boundary OLD_T1→NEW_T0 可复用相同 decoded plane bundle +- 新 T1 只上传新需要的 texture +- scalar/vector/validity 变化必须触发正确 content change +- 正常 playback 不允许因为 generic value source 改造而重建 Particle simulation 或 clear trail + +如果当前 `sameFieldGpuContent()` 已满足,无需再改 Core Contract。 + +## 5. Shader 约束 + +允许修改 WeatherColor 实际使用的 shader/UBO/generator,只限完成 generic field value。必须保持: + +- T0/T1 GPU 时间插值 +- spatial sampling/bounds +- current vector particle shader physics 不变 +- 不把 provider、WeatherV2、Rain/Wave/Tide 业务名写进 shader +- 不增加第二套专用 rain/wave shader + +如果同一 shader 当前同时承载 particle/weather pass,可做最小 pass/value-source 分支,但不能借此重构整个粒子 shader 架构。 +## 6. 最小验证 + +不加入 Jest。使用现有 C++/OpenGL 测试体系,至少覆盖: + +- WeatherColor default `VECTOR_MAGNITUDE` 保持旧 Wind 行为 +- scalar-only + `SCALAR` 可建立 field texture/render resources +- vector-only + `VECTOR_MAGNITUDE` 可建立资源 +- scalar+vector 可由 value source 明确选择对应 channel +- scalar-only + `VECTOR_MAGNITUDE` fail-closed +- vector-only + `SCALAR` fail-closed +- T0/T1 channel/valueSource compatibility 错误不被渲染 +- scalar T0/T1 MIX 数值路径正确 +- vector magnitude T0/T1 MIX 不回归 +- validity mask 对 scalar/vector 都生效 +- DIRECTION vector 在 Particle renderer fail-closed +- existing v1 Wind fixture/source tests 回归通过 +- `git diff --check` PASS + +执行规定的 Linux OpenGL 精确编译/测试,遵循全局 `$build-output-throttle`。 + +不得执行 Android Gradle、AAR、APK、ADB。 + +## 7. 硬边界 + +本任务禁止修改 Android/JNI/Java/Kotlin、RN package/API/version、Test Host JS/TS、WeatherV2、NavSea;禁止实现 DIRECTION particle movement / wave physics;禁止解析 NSFT/PMTiles;禁止新增 provider/network/cache;禁止重构 Timeline/Playback/Runtime Window;禁止新建 RainRenderer/WaveRenderer/TideRenderer;禁止 Jest 与 Android build/install/ADB。 +## 8. 文档输出 + +必须新增/更新: + +- `docs/50_Step记录/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_v1.md` +- `docs/报告/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_Codex完成报告_v1.md` +- 追加 `粒子开发记录.md` + +所有文档中文。报告逐项列出生产代码修改文件、Renderer/shader 语义变化、精确编译/测试结果、未执行 Android/Jest、未触碰无关工作区。 + +## 9. 必做清单 + +- [ ] 复读规范、STEP-065/066 与当前源码 +- [ ] 引入显式 `FieldValueSource::Scalar / VectorMagnitude` +- [ ] 默认保持 VectorMagnitude 旧 Wind 行为 +- [ ] WeatherColor scalar GPU path 可用 +- [ ] WeatherColor vector magnitude path 回归 +- [ ] shared validity 同时保护 scalar/vector +- [ ] C++ WeatherColor `speedMps` 语义改为 generic value +- [ ] Particle renderer 拒绝 scalar-only 和 DIRECTION +- [ ] T0/T1/MIX 与 resource reuse 不回归 +- [ ] 最小精确 Linux/OpenGL tests PASS +- [ ] `git diff --check` PASS +- [ ] Step/报告/开发记录完成 +- [ ] scoped Git commit + push `origin/particle/main` +## 10. Git 收尾 + +完成后只显式 `git add` 本任务文件与实际修改文件,严禁把 STEP-062 用户修改、历史 untracked、build/cache/tgz 带入提交。 + +创建合理 scoped commit;更新完成报告记录全部 commit hash/message、push 结果与最终远端分支。 + +必须 push 到 `origin/particle/main`,最终复核: + +`HEAD == origin/particle/main` + +push 失败不得声称完成。 + +完成后直接按必做清单报告“已完成 / 未完成 / 无法验证”,不要输出实施计划。 \ No newline at end of file diff --git a/docs/40_任务/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_Codex任务_v1.md b/docs/40_任务/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_Codex任务_v1.md new file mode 100644 index 000000000..a3274689b --- /dev/null +++ b/docs/40_任务/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_Codex任务_v1.md @@ -0,0 +1,245 @@ +# STEP-068 MapLibreParticleLab V3-04 DIRECTION 粒子推进 — Codex 任务 v1 + +执行状态(2026-09-11):IMPLEMENTED / WAITING_VALIDATION。C++/shader scoped 实现与 shader generator 已完成;本沙箱对最终含新增 source test 的精确编译连续在单次 30 秒终端上限前中止,未将其或全量 Ninja 写为 PASS。详见同编号 Step 记录与完成报告。 + +## 0. 任务定位 + +本任务承接已关闭的 STEP-065/066/067,在 MapLibreParticleLab V3 Core/GPU 中实现第二种通用 VectorSemantics:`DIRECTION`。 + +目标不是 Wave 业务实现,而是让任意 canonical direction-only vector field 可以驱动 RasterParticleLayer。Core/renderer 仍不得理解 WIND/WAVE/RAIN/TIDE、WeatherV2、NSFT 或 provider。 + +本任务只做到 C++/shader/Linux OpenGL 可验证闭环;不得进入 Android/JNI/RN v2 ingress,不执行 Android Gradle/AAR/APK/ADB,不加入或执行 Jest。 + +## 1. 仓库与基线 + +仓库:`/mnt/sda1/MapLibreParticleLab` +分支:`particle/main` +任务书编写时 HEAD:`89c72554c5fb5cd469bdd8ba06ff4b03a1a67ff9` +该 HEAD 已等于 `origin/particle/main`,STEP-067 为 `CLOSED / PASS`。 + +开工时必须重新确认 branch / HEAD / origin / `git status --short`。若远端已合法推进,先阅读新增提交,不得 reset。 + +当前工作区仍有 STEP-062 两处用户修改和历史 untracked/build/cache/tgz。全部保留;禁止 `git add .` / `git add -A`,禁止恢复、删除、覆盖或提交无关内容。 + +## 2. 必须阅读 + +至少阅读: + +- `AGENTS.md` +- `PARTICLE_BASELINE.md` +- `docs/00_项目规范/*.md` +- `docs/30_设计/MapLibreParticleLab_V3通用ScalarVectorField运行时架构设计_v1.md` +- `docs/50_Step记录/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_v1.md` +- `docs/50_Step记录/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_v1.md` +- `docs/50_Step记录/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_v1.md` +- `include/mbgl/particle/field/field_data_adapter.hpp` +- `src/mbgl/particle/field/field_data_adapter.cpp` +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `include/mbgl/style/layers/raster_particle_layer.hpp` +- `src/mbgl/style/layers/raster_particle_layer.cpp` +- `src/mbgl/style/layers/raster_particle_layer_impl.hpp` +- `src/mbgl/style/layers/particle_render_layer_properties.hpp` +- `shaders/particle_gpu_validation.vertex.glsl` +- `shaders/particle_gpu_validation.fragment.glsl` +- `include/mbgl/shaders/particle_gpu_validation_ubo.hpp` +- 对应 generated OpenGL shader header 与现有 layer/source tests。 + +实现必须以实际源码为准,不得按任务书示例猜接口。 + +## 3. 冻结语义 + +V3 vector semantics 只有: + +- `VELOCITY`:canonical U east / V north,物理速度,保持现有 m/s Web Mercator 推进路径。 +- `DIRECTION`:canonical U east / V north,只表达方向;输入通常为单位向量,但 GPU 不可假设时间插值后仍为单位长度。 + +`DIRECTION` 的 GPU 规则必须是: + +1. 分别解码 T0/T1 vector; +2. 按当前 `timeMix` 对 U/V component-wise 插值; +3. 检查 finite; +4. 对插值结果求长度; +5. 长度 <= epsilon 时 fail-closed; +6. 否则 normalize; +7. 乘独立的 renderer `advectionScale`; +8. 再进入现有 Web Mercator displacement 计算。 + +严禁: + +- 先分别 normalize T0/T1 再做最终不归一化 mix; +- 使用 scalar height、period 或 vector 原 magnitude 决定推进速度; +- 将 DIRECTION 当 m/s; +- 在 Core 中出现 Wave 分支。 + +反向方向示例:T0=(1,0)、T1=(-1,0)、MIX=0.5,插值向量长度近零,必须 invalid/fail-closed,不得任意选择东或西。 + +## 4. VELOCITY 回归保护 + +VELOCITY 是已通过 Android 的旧正式路径。本任务必须保持默认行为: + +- 不 normalize VELOCITY; +- T0/T1 仍 component-wise mix; +- 默认参数下 displacement 公式不变; +- U east / V north 与 world-y southward 的符号不变; +- latitude/cos(latitude)/Earth circumference 计算不变; +- lifecycle、respawn、Trail、T0/T1/MIX、pause、cadence、window rollover 不重写。 + +允许为共享 helper 重构 shader,但必须证明 VELOCITY 结果等价。 + +## 5. advectionScale + +为 DIRECTION 增加 business-neutral renderer 控制 `advectionScale`。 + +优先以 RasterParticleLayer 的 C++ Core style/control property 实现,建议属性名:`raster-particle-advection-scale`,C++ API 为 `getAdvectionScale()/setAdvectionScale()`;最终命名需与现有 property 体系一致。 + +冻结要求: + +- 默认值 `1.0`,确保不隐式引入产品级速度偏好; +- 必须 finite 且 > 0;需要上限时根据现有数值安全给出保守、文档化的界限,不得随意设为天气业务值; +- V3-04 中 `advectionScale` 只改变 DIRECTION 的推进 magnitude;VELOCITY 默认/现有运动数学不因该属性变化而改变; +- 不从 scalar、fieldId、fieldType、displayDomain 推导该值; +- RN 公共 API 暂不增加,留给 V3-05 一并接线。 + +如果当前 CoreOnly layer property 机制不适合新增 serialize property,Codex 可采用最小等价 C++ renderer config,但必须在报告解释原因;不得因此提前改 Android/JNI/RN。 + +## 6. Shader/UBO 一致性 + +当前 State fragment shader 和正式 particle line vertex shader都会独立读取 `u_field_texture/u_next_field_texture`。 + +两者必须使用相同 semantics: + +`decode T0/T1 -> mix -> resolve VELOCITY or DIRECTION -> validity` + +不得出现 State 按 Direction 推进、但 line vertex 仍把 raw mixed direction 当 velocity 的不一致。 + +建议增加明确的 UBO advection/semantics 参数,例如独立 vec4,避免挤占已有字段导致语义混乱。若修改 UBO: + +- 同步 vertex GLSL; +- 同步 fragment GLSL; +- 同步 C++ UBO struct/static_assert; +- 运行官方 shader generator 更新 generated OpenGL header; +- 不手工只改 generated header。 + +helper 命名应从 `fieldVelocityAt` 收敛为 generic advection/vector 语义;不得继续用 velocity 名称表达 DIRECTION 分支。 + +## 7. Renderer acceptance + +STEP-067 当前主动拒绝 `VectorSemantics::Direction`。本任务需改为: + +- scalar-only:Particle unavailable/fail-closed; +- vector VELOCITY:允许; +- vector DIRECTION:允许; +- T0/T1 semantics 不一致:继续 fail-closed(正常应已被 Timeline compatibility 拦截,但 renderer 仍防御); +- invalid/NaN/near-zero mixed direction:该 sample invalid,按既有 lifecycle/respawn 安全处理;不得产生 NaN position。 + +切换 semantics/window 时不得错误复用与新 semantics 不兼容的 GPU resource。既有 `sameFieldGpuContent`/Timeline compatibility 的 semantics 保护必须保留。 + +## 8. Trail 与粒子线段 + +正式 particle line 必须与 State 实际位移一致: + +- line segment 继续连接 previousState -> currentState; +- vertex shader 用于 `v_particle_velocity`/颜色判断的向量必须使用与 State 相同的 resolved advection vector; +- DIRECTION 下可把 `length(resolvedAdvection)` 作为纯视觉强度输入,但不得声称它是原始物理 speed; +- 不允许用 scalar 驱动 particle color; +- 本任务不重新设计 speedColorEnabled 公共能力,若命名仍带历史 speed,可记录技术债,避免扩大范围。 + +## 9. Timeline / GPU reuse 硬保护 + +不得破坏: + +- Source-owned Timeline; +- T0/T1/MIX; +- PAUSE; +- 连续 MIX 时零 field upload; +- OLD_T1 -> NEW_T0 reuse; +- 正常 rollover 只上传 NEW_T1; +- window overlap 时 `simulationRebuild=false`、`trailCleared=false`; +- style/context loss 的既有重建规则; +- invalid/gap fail-closed。 + +改变 `advectionScale` 不应重新上传 field texture。是否清 trail/simulation 必须依据当前生命周期设计最小化,默认不允许为了纯 renderer 参数变化重新提交 field。 + +## 10. 必须补的验证 + +本任务需新增/调整最小测试,至少覆盖: + +1. RasterParticleLayer advectionScale 默认值与 set/get/非法值策略; +2. VELOCITY 与 DIRECTION renderer eligibility; +3. scalar-only 仍 fail-closed; +4. T0/T1 semantics mismatch fail-closed/不绘制; +5. shader/UBO semantics 参数正确传递; +6. DIRECTION 数学:east、north、斜向; +7. T0 east + T1 north + MIX=0.5 -> normalize 后 northeast; +8. T0 east + T1 west + MIX=0.5 -> near-zero invalid; +9. advectionScale 改变 magnitude、不改变 direction; +10. VELOCITY 路径不 normalize,默认结果保持现状。 + +优先使用当前已有 C++ test/fixture 体系;不要为了测试引入新的大型 framework。 + +## 11. Linux 验证 + +必须执行: + +- `node shaders/generate_shader_code.mjs`; +- 受影响 C++ 精确对象编译; +- 相关现有/新增过滤测试; +- 在可行时 `ninja -C build-linux-opengl -j3 mbgl-test mbgl-test-runner`; +- `git diff --check`。 + +构建日志遵循项目全局 build-output-throttle。 + +如果全量 Ninja 再受单次终端时间限制,不得伪造 PASS;但应先确保受影响精确对象与过滤测试通过,并在报告明确缺口。不得要求用户现在编 Android。 + +## 12. 明确禁止 + +本任务不得: + +- 修改 Android Java/Kotlin/JNI bridge; +- 修改 RN package API/version; +- 实现 v2 multi-plane file ingress; +- 修改 WeatherV2/NavSea; +- 解析 NSFT/PMTiles; +- 添加 Wind/Wave/Rain/Tide enum 或业务分支; +- 使用 scalar height/period 控制 DIRECTION 速度; +- 增加第二 scalar/N-channel; +- 重写 Timeline/Playback/Runtime Window; +- JSI/zero-copy 重构; +- 执行 Android Gradle/AAR/APK/ADB; +- 加入或执行 Jest。 + +## 13. 文档与状态 + +新增/更新: + +- 本任务书 `docs/40_任务/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_Codex任务_v1.md` +- `docs/50_Step记录/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_v1.md` +- `docs/报告/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_Codex完成报告_v1.md` +- 追加 `粒子开发记录.md` + +全部文档中文。完成报告逐项写“已完成 / 未完成 / 无法验证”,明确生产代码范围、验证结果、Android/Jest 未执行、风险和下一步。 + +若代码与 Linux 验证满足本任务门槛,可标记 `CLOSED / PASS`;若只有实现而关键精确编译/过滤测试未通过,则只能 `IMPLEMENTED / WAITING_VALIDATION`。 + +## 14. Git 收尾 + +本任务有生产代码修改,完成后必须 scoped commit + push 到 `origin/particle/main`。 + +提交前再次 `git status --short`。只暂存本任务实际修改文件与上述 STEP-068 文档;禁止纳入 STEP-062 两处用户修改和所有历史 untracked/build/cache/tgz。 + +允许按“实现代码”与“文档收尾”做 1-2 个 scoped commit。禁止 `git add .`、`git add -A`。 + +push 后必须确认: + +- commit hash/message; +- push 成功; +- `HEAD == origin/particle/main`; +- 最终工作区剩余 dirty 仍只属于原有用户/历史内容。 + +push 失败则任务不得声称完全完成。 + +## 15. 下一阶段边界 + +本任务完成后下一阶段应为 V3-05:Android/JNI/RN generic v2 multi-plane ingress。 + +V3-05 之前不要求 Android build。最终仍按 V3 总规划,将 Android 编译/安装/设备综合验收集中到后续大阶段,避免每个小 Step 重复一次 Android 周期。 diff --git a/docs/40_任务/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_Codex任务_v1.md b/docs/40_任务/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_Codex任务_v1.md new file mode 100644 index 000000000..87cbecb17 --- /dev/null +++ b/docs/40_任务/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_Codex任务_v1.md @@ -0,0 +1,264 @@ +# STEP-069 MapLibreParticleLab V3-05 Android/JNI/RN v2 多 Plane 入口 — Codex 任务 v1 + +## 1. 任务目标 + +基于当前 `particle/main` HEAD `9e9ea07bc74633714836aa4fff4e963109f4e3b9`,完成 `maplibre-particle-field/v2` 从 RN → Android Kotlin → Java Host facade → JNI → `RasterArraySource` → 现有 Core V3 Snapshot/Timeline 的明确多 plane 二进制入口。 + +本任务只做通用 Scalar/Vector Field runtime 的 Host ingress 与已经冻结的 renderer 配置桥,不引入 Wind/Wave/Rain/Tide provider/business 逻辑,不解析 NSFT/PMTiles,不改 Timeline/T0/T1/MIX/rollover/lifecycle 算法。 + +开始前必须完整阅读:`AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目规范/`、STEP-065/066/067/068 的 Step/报告,以及当前实际源码。不得按记忆重建。 + +## 2. 当前已确认基线 + +- RN package:`packages/maplibre-particle-rn` 当前版本 `13.4.1-particle.2`。 +- `.2` 公共 `FieldRuntime.submitField(reactTag, metadataJson, payloadFileUri)` 内部仍映射 `submitWindField` 单文件 ABI。 +- Kotlin `WeatherRuntimeBridgeModule` 当前只读取一个本地 payload,单 payload 上限 64 MiB。 +- Java `WeatherRuntime.submitWindField(metadataJson, byte[])` → `MapLibreMap.submitRasterArraySourceHostField(...)`。 +- JNI `NativeMapView::submitRasterArraySourceHostField(...)` 当前只转一个 `byte[]` 为 `std::vector`。 +- `RasterArraySource::submitHostField(metadata, vector)` 是 v1 legacy ingress。 +- Core 已有 `ParticleFieldPayloadBundle { scalar?, vector?, validity? }` 与 `LocalParticleFieldFeed::load(metadata, bundle)`,但 `RasterArraySource` 尚无 v2 bundle overload。 +- STEP-067 已有 `WeatherColorLayer::FieldValueSource { Scalar, VectorMagnitude }`,尚无 Android/RN setter。 +- STEP-068 已有 `raster-particle-advection-scale`,尚无 Android/RN setter。 + +## 3. v2 RN 公共 API + +将 package 版本递增到 **`13.4.1-particle.3`**,不得覆盖或继续宣称 `.2`。 + +新增并导出 provider-neutral 类型: + +```ts +type FieldPayloadRefs = Readonly< + { validityFileUri: string } & ( + | { scalarFileUri: string; vectorFileUri?: string } + | { scalarFileUri?: string; vectorFileUri: string } + ) +>; + +type FieldValueSource = "SCALAR" | "VECTOR_MAGNITUDE"; +``` + +`.3` 的主入口冻结为: + +```ts +FieldRuntime.submitField(reactTag, metadataJson, payloadRefs) +FieldRuntime.clearFields(reactTag) +FieldRuntime.setFieldValueSource(reactTag, source) +FieldRuntime.setParticleAdvectionScale(reactTag, scale) +``` + +`submitField` 不得再接受 JS number[]、Base64 或巨大 JSON payload。 + +为了保留 v1 thin compatibility ingress,可保留现有 native/internal `submitWindField(metadata, singlePayload)`,并在 `.3` 公共层提供命名明确的 legacy 入口(例如 `submitLegacyVectorField`);不得让 v1/v2 共用一个模糊单 payload 形状。README/CHANGELOG 必须明确 `.2` 与 `.3` 差异。 + +## 4. payloadRefs 文件规则 + +v2 必须使用三个语义明确的 local file reference: + +- `validityFileUri`:必填; +- `scalarFileUri`:可选; +- `vectorFileUri`:可选; +- scalar/vector 至少一个存在,否则在 Kotlin bridge fail-closed。 + +只允许 `file://` 或绝对本地路径,沿用现有 local-file 安全规则。 + +在实际读文件前先完成所有路径、可读性、文件长度检查,再读取;避免读到一半才发现 aggregate 超限。 + +冻结本桥接层 v3 初版内存上限: + +- metadata UTF-8 ≤ 1 MiB(沿用现状); +- 每个存在的 plane 必须非空; +- **三个 plane 文件总字节数 ≤ 64 MiB**; +- 单文件自然也不得超过 64 MiB; +- 总长度计算必须防溢出; +- Core 仍负责按 metadata 声明验证每 plane exact byteLength/encoding/layout/validity。 + +不得把缺失 scalar/vector 伪造为零数据文件。validity 必须显式存在。 + +## 5. Android/Kotlin bridge + +在 `WeatherRuntimeBridgeModule` 中新增 generic v2 submit 方法,命名可以继续受现有 TurboModule/codegen seam 限制,但语义必须明确为 Field v2,不能叫新的 Wind-only API。 + +推荐流程: + +1. 校验 metadata; +2. 校验 refs presence; +3. resolve 三个 local file; +4. stat 全部存在 plane,计算 aggregate; +5. aggregate ≤ 64 MiB 后才逐个 bounded read; +6. 调 Java `WeatherRuntime.submitField(...)`; +7. Promise 返回现有 `FieldSubmitResult` name。 + +不得保存 reactTag/runtime/timeline 第二份状态,不改变现有 per-view lookup 与生命周期。 + +legacy `submitWindField` 可以保留作为 thin v1 compatibility,不得删除导致旧 `.2` native seam 无法理解。 + +## 6. Java Host facade + +`WeatherRuntime` 新增 provider-neutral: + +- `submitField(metadataJson, scalarBytes?, vectorBytes?, validityBytes)`; +- `clearFields()`(旧 `clearWindFields()` 可作为 deprecated/thin alias); +- `setFieldValueSource(...)`; +- `setParticleAdvectionScale(float)`。 + +不得在 Java 根据 `fieldId`、Wind/Wave/Rain/Tide 名称猜 renderer 行为。 + +`setFieldValueSource` 只接 `SCALAR` / `VECTOR_MAGNITUDE`;`setParticleAdvectionScale` 必须 finite 且 >0。二者只控制已有 renderer config,不改 Source timeline。 + +WeatherColor 内部历史 `speedMps` 命名若本任务不影响功能,可保留为兼容技术债,禁止借机大规模重命名。 + +## 7. NativeMap / JNI + +新增明确的 v2 JNI 方法,不得重载旧单 payload 产生 ABI 歧义。 + +优先使用三 plane 参数:`scalar byte[]?`、`vector byte[]?`、`validity byte[]`。必须先检查当前 jni.hpp 对 nullable primitive array 的实际支持;若 nullable array 在当前 binding 容易产生注册/调用风险,可以改用**明确 v2 container 参数结构**,但必须: + +- 不与 v1 单 payload 混淆; +- 明确记录 offsets/lengths; +- 不在 JS 层构造巨大 container; +- Kotlin/Java → JNI 后最终恢复成 `ParticleFieldPayloadBundle`。 + +不允许因为参数处理困难而用“空 scalar/vector 字节数组代表缺失”去绕过语义,除非在 ABI 层有明确 presence flags 且 Core bundle 恢复为 `std::nullopt`。 + +JNI 必须最终调用新的 `RasterArraySource::submitHostField(metadata, ParticleFieldPayloadBundle)`。 + +## 8. RasterArraySource 最薄 v2 overload + +新增: + +```cpp +submitHostField(const std::string&, const particle::ParticleFieldPayloadBundle&) +``` + +或等价明确 overload。 + +它必须调用现有 `LocalParticleFieldFeed::load(metadata, bundle)`;v1 overload 继续调用 legacy load/normalize。 + +两条 ingress decode 后必须进入**同一份** duplicate / monotonic / compatibility / pending initial T0 / initial window / next snapshot 提交流程。应提取最小公共 helper 避免复制 Timeline 状态机,但不得重写 Timeline。 + +日志改为 provider-neutral plane presence/bytes,不得假称所有 payload 为 `interleaved-uv`。legacy v1 日志可明确标注 legacy/vector-only。 + +## 9. FieldValueSource Android/JNI bridge + +Core 已有 `WeatherColorLayer::setFieldValueSource()`,本任务只加最薄 Android/JNI Host setter,例如以稳定 int/enum code 表达: + +- `0 = SCALAR` +- `1 = VECTOR_MAGNITUDE` + +非法值 fail-closed。 + +不得新增天气业务 enum。该 setter 不重传 Field、不改变 Timeline。 + +## 10. advectionScale Android/RN bridge + +利用 STEP-068 已有 `raster-particle-advection-scale` style property,Java Host 可通过现有 `setStyleLayerFloatProperty` 设置;如无需新 JNI 就不要新增 JNI。 + +RN 公开 `setParticleAdvectionScale()`;必须 finite、>0,最终只修改 renderer property,不重传 field、不清 Timeline/Trail/State。 + +## 11. 必须验证的行为 + +至少覆盖: + +1. v2 scalar-only bundle 可 decode/submit; +2. v2 vector-only VELOCITY 可 decode/submit; +3. v2 scalar+vector DIRECTION 可 decode/submit; +4. 缺 validity 拒绝; +5. scalar/vector 都缺拒绝; +6. metadata 声明与 plane presence/长度不一致由 Core 拒绝; +7. v1 legacy vector-only ingress 仍通过 thin normalize; +8. v1 与 compatible v2 vector-only 可进入同一个 Runtime/Timeline 契约; +9. T0/T1 channel presence 或 vector semantics 不同继续拒绝; +10. aggregate local file size >64 MiB 在读入前拒绝; +11. 非 file URI / 相对路径拒绝; +12. FieldValueSource 非法值拒绝; +13. advectionScale <=0 / NaN / Inf 拒绝; +14. `.3` TypeScript public type 不允许 payloadRefs 同时缺 scalar/vector; +15. legacy API 明确命名,不与 v2 `submitField` 形状混淆。 + +## 12. 测试与静态验证 + +本任务**不执行 Android Gradle、AAR、APK、ADB,也不执行 Jest**。 + +允许并要求: + +- Core/RasterArraySource 相关 C++ 精确对象编译; +- 相关 C++ 过滤测试,补最小测试覆盖 v2 Source submit 与 legacy regression; +- `ninja -C build-linux-opengl -j3 mbgl-test mbgl-test-runner` 仅当变更触发 Core 并且已有 build 可用; +- `git diff --check`; +- RN package TypeScript `tsc --noEmit` / 项目现有等价静态检查; +- 检查 TurboModule spec 与 Kotlin override 参数一致性; +- 可运行无需 Gradle 的 codegen/文本生成检查,但不得为了验证启动 Android build。 + +所有 noisy build 遵守 `$build-output-throttle`。 + +由于 Android 真正 Java/Kotlin/JNI 编译由用户后续 V3-08 手工统一执行,本 Step 不能声称 Android compile PASS。若静态审计仍存在签名不确定性,报告必须写明。 + +## 13. 明确禁止 + +不得: + +- Android Gradle/AAR/APK/ADB; +- Jest; +- 修改 WeatherV2/NavSea; +- 解析 NSFT、PMTiles 或 provider manifest; +- 加 Wind/Wave/Rain/Tide enum/if 分支; +- 从 Wave height/period 推断粒子速度; +- 引入第二 scalar 或 N-channel; +- 改 Timeline/T0/T1/MIX/rollover/playback 算法; +- JSI/zero-copy 大重构; +- 删除 `.2` 已冻结 legacy native ABI; +- 修改 STEP-062 两处用户 dirty 文档; +- 清理历史 untracked/build/cache/tgz。 + +## 14. 必做文档 + +新增/更新: + +- 本任务书 `docs/40_任务/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_Codex任务_v1.md` +- `docs/50_Step记录/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_v1.md` +- `docs/报告/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_Codex完成报告_v1.md` +- `packages/maplibre-particle-rn/README.md` +- `packages/maplibre-particle-rn/CHANGELOG.md` +- 追加 `粒子开发记录.md` + +全部文档中文说明为主,代码/API/命令保留英文。 + +任务完成后逐项报告“已完成 / 未完成 / 无法验证”,并明确 Android 编译未执行。 + +## 15. 状态判定 + +若 Core/Linux/TS 静态验证与 contract 检查满足要求,可标记: + +`IMPLEMENTED / WAITING_ANDROID_VALIDATION` + +**不得在本 Step 标记 Android CLOSED/PASS**,因为按照项目规则 Android 编译/实体机验收统一留到 V3-08。 + +该状态允许进入 V3-06 fixtures,只要不存在已知 ABI/类型阻塞。 + +## 16. Git 收尾 + +本任务修改代码,必须 scoped commit + push `origin/particle/main`。 + +提交前检查 `git status --short`,只暂存 STEP-069 实际文件。严禁 `git add .` / `git add -A`。 + +STEP-062 两处 tracked dirty、所有历史 untracked、`.codex-build-logs/`、旧 tgz/build 目录一律不得提交。 + +允许 1 个实现 commit + 1 个文档 handoff commit。 + +完成报告必须记录: + +- implementation commit hash/message; +- handoff commit hash/message(若有); +- push result; +- remote branch; +- Final HEAD; +- `HEAD == origin/particle/main`; +- 未纳入的既有 dirty 内容摘要。 + +push 失败不得声称任务完成。 + +## 17. 执行回填(2026-09-11) + +- 已按本任务实现 `.3` v2 refs、Kotlin/Java/JNI/Core bundle ingress、`FieldValueSource` 与 `advectionScale` 桥接;v1 ABI 保留为明确 legacy ingress。 +- 未执行 Android Gradle、AAR、APK、ADB 或 Jest;Android 编译留给 V3-08。 +- 结果状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。最终 commit/push 与允许验证结果见本 Step 记录和完成报告。 diff --git a/docs/40_任务/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex任务_v1.md b/docs/40_任务/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex任务_v1.md new file mode 100644 index 000000000..eff6dd7a4 --- /dev/null +++ b/docs/40_任务/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex任务_v1.md @@ -0,0 +1,157 @@ +# STEP-070 MapLibreParticleLab V3-06 四类场 Fixture 与 RN 通用回归 — Codex 任务 v1 + +## 1. 目标 +基于 `particle/main` 当前 HEAD `3b012759838f1e1ecc44559faffe061d396b6cce`,完成 V3 通用 Scalar/Vector Field 的离线业务语义映射与回归资产:Wind、Rain、Wave、Tide/Current 四类场,外加固定 synthetic Wave independence fixture。 + +本 Step 的重点不是增加新的 Runtime 架构,而是证明 STEP-065~069 冻结的通用契约能够由真实/可追溯数据映射并通过 `.3` RN 公共 API 驱动。Android 编译/安装/ADB/设备视觉验证继续留到后续统一周期,不在本任务执行。 + +开始前完整阅读 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目规范/`、STEP-065~069 Step/报告,以及当前实际源码。不得按记忆重建。 + +## 2. 必须保持的架构边界 +- Core/Renderer 不得出现 Wind/Wave/Rain/Tide/provider/network/vendor 业务分支。 +- Host/RN 负责业务字段到 canonical scalar/vector/validity 的映射。 +- canonical schema 继续使用 `maplibre-particle-field/v2`。 +- 一个 Snapshot:0/1 scalar + 0/1 vector + shared validity;neither 非法。 +- Wind / Tide(Current):vector `VELOCITY`,Field=`VECTOR_MAGNITUDE`,Particle 使用 VELOCITY。 +- Rain:scalar rain rate,Field=`SCALAR`,不启用 Particle。 +- Wave:scalar=wave height;vector=direction-to unit vector;semantics=`DIRECTION`;Field=`SCALAR`;Particle=`DIRECTION`,推进视觉速度只来自 `advectionScale`。 +- Wave period 不进入第二 scalar,不参与推进,不从 height/period 推导速度。 +- Rain precipitation_type 暂不进入 canonical V3.0。 +- T0/T1/MIX、Timeline、Playback、OLD_T1→NEW_T0 reuse 等 Core 逻辑不得修改。 + +## 3. 当前基线事实 +- RN package 已是 `@navsea/maplibre-particle-rn@13.4.1-particle.3`。 +- 正式 v2 API:`FieldRuntime.submitField(reactTag, metadataJson, payloadRefs)`。 +- `payloadRefs` 为必填 validity + optional scalar/vector local file URI,scalar/vector 至少一个。 +- `.3` 已提供 `setFieldValueSource()` 与 `setParticleAdvectionScale()`。 +- Android/JNI/Core multi-plane ingress 已实现,但 Android 编译留待 V3-08。 +- 现有 RN Test Host 的 REAL WEATHER 仍直接调用旧 `WeatherRuntimeBridge.submitWindField()`;本 Step 必须迁移验证入口到 package `.3` 的 `FieldRuntime`,不能继续用旧 bridge 证明新 API。 +- 仓库已有真实 WeatherV2 Wind canonical 资产:`platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-weatherv2/ecmwf-hres-wind-20260807/`。 +- WeatherV2 源树只读位于 `/root/sourceserver/weatherV2`;其 NSFT v1 语义以 `src/dev/core/nsft.py` 和当前生产 pipeline 为准。不要修改 WeatherV2。 + +## 4. 数据来源与可追溯性 +先调查 `/root/sourceserver/weatherV2` 当前实际输出,优先从已存在的 NSFT v1 `field_pmtiles`/`/fields` 等本地数值产物生成 fixture,不联网、不重新跑完整生产 pipeline。 + +至少为以下四类场确认一个可用 source/refTime/validTime/区域: +- Wind +- Rain +- Wave +- Tide/Current + +优先选日本及周边、相邻两个实际 validTime,便于 T0/T1/MIX。若某层当前本地没有可用正式 NSFT 数值产物,不得伪称“真实 WeatherV2”;应在报告明确说明并使用最小可追溯 synthetic/decoder-derived fallback,同时保留该层的业务语义。Wind 已有真实 canonical 基线可直接复用/升级。 + +不得把旧 QBIN 当作正式未来 transport。若为数据核验读取旧 BIN,只能作为来源调查/对照,最终 fixture 必须输出 v2 canonical planes。 + +## 5. 新建通用 fixture/converter 工具 +在 `tools/` 下新增 provider-neutral 或 WeatherV2-to-canonical 的离线工具,具体目录名由实际实现确定,但不得只为 Wind 特化。 + +工具至少应支持把已解码/NSFT 数值场规范化为: +- `metadata.json` +- `validity.u8` +- optional `scalar.f32` +- optional `vector.f32`(interleaved U/V) +- `conversion_summary.json` + +约束: +- float32-le;validity uint8;NW-origin row-major;rows north→south;cols west→east。 +- U eastward,V northward。 +- 每个输出文件记录 byteLength、SHA-256、grid、bounds、validTime、generation、source provenance。 +- vector DIRECTION 必须在 valid cell 上接近 unit length;允许原始量化误差,转换后可规范化,但近零/非 finite 必须 invalid/fail-closed,不得任造方向。 +- shared validity 必须由源有效性与所有存在 channel 的有限性共同约束;invalid cell 的具体 payload 值可为 0,但 validity=0 才是权威。 +- metadata 中不允许 Wind/Wave/Rain/Tide capability enum;`fieldId` 可包含可读 fixture 名称但 Core 不依赖它。 + +## 6. 四类 fixture 目录 +新增仓库内固定测试资产,建议: +`integration/particle-v3-fields/` + +至少: +- `wind-real/`:两个相邻 validTime,vector-only VELOCITY。 +- `rain-real/`:两个相邻 validTime,scalar-only。 +- `wave-real/`:两个相邻 validTime,scalar+vector DIRECTION。 +- `tide-real/`:两个相邻 validTime,vector-only VELOCITY。 +- `wave-synthetic-independence/`:两个兼容时次或至少足以验证 T0/T1 的 fixture。 + +若某 `*-real` 因本机数据缺失只能 fallback,目录名不得误用 real;改用 `*-decoder-fixture` 或等价明确命名,并在 dataset manifest/报告写明来源。 + +每组 fixture 提供 `dataset.json`,列出 frame 顺序、metadata/plane 路径、expected FieldValueSource、particle expected on/off、vector semantics、建议 advectionScale(Wave synthetic 可指定非 1 值用于验证 scale 独立性)。 + +## 7. Synthetic Wave Independence 固定语义 +必须实现并静态验证: +- scalar(wave height)西侧低、东侧高,形成明显 E→W 或 W→E 的横向数值梯度; +- vector 在所有 valid cell 固定向北 `(u=0, v=1)`; +- `vectorSemantics=DIRECTION`; +- Field source=`SCALAR`; +- Particle 方向应只由 vector 决定为北向;scalar 改变不得改变粒子方向/推进 magnitude; +- 改 `advectionScale` 只改变 DIRECTION visual movement magnitude,不改变 field scalar。 + +增加无 Android 的数据 oracle/脚本验证该 fixture:scalar 的东西梯度与 vector 的北向独立,不允许只靠人工阅读 JSON。 + +## 8. RN Test Host 升级到 `.3` 正式 API +只修改 Test Host 的 JS/TS 与离线 assets/config,不执行 Gradle。 + +要求: +- REAL WEATHER/V3 验证入口从内部 `WeatherRuntimeBridge.submitWindField` 迁移到 `@navsea/maplibre-particle-rn` 的 `FieldRuntime`。 +- 不从 package 内部路径 import bridge。 +- 通过 app cache/local file URI 提交 metadata + plane refs;不得 JS number[] / base64 / giant JSON。 +- 提供四类场选择/加载能力,至少能在 UI/fixture runner 中选择 Wind/Rain/Wave/Tide 和 synthetic Wave。 +- 每次切换 dataset 前 `clearFields()`,设置正确的 `setFieldValueSource()`、Particle visibility、`setParticleAdvectionScale()`,再顺序提交 T0/T1。 +- 对每个 dataset 明确断言:T0=`ACCEPTED_PENDING_INITIAL_WINDOW`,T1=`INITIAL_WINDOW_READY`,runtime state=`READY`。 +- Rain 必须 Particle OFF;Wind/Tide Particle ON;Wave Particle ON 且 DIRECTION 数据不被改成 VELOCITY。 +- Field/Particle visibility 仍独立。 +- 不在 JS 保存 T0/T1/MIX 第二份 Runtime 状态;JS 只保存 fixture/选择 UI 与调用结果。 +- 现有 GSI/base map 与已验证 UI 可保留,不做无关重构。 + +## 9. Core/Linux 回归 +新增最小 C++/数据 contract 测试,重点不是复制完整 Android bridge,而是验证: +- 四类 canonical shape 能被 `LocalParticleFieldFeed` / `RasterArraySource` 接受。 +- Wind/Tide vector-only VELOCITY compatible T0/T1。 +- Rain scalar-only compatible T0/T1。 +- Wave scalar+vector DIRECTION compatible T0/T1。 +- channel presence/semantics mismatch 继续 fail-closed。 +- synthetic Wave 的 scalar/vector independence oracle。 +- normal T0→T1 / next snapshot 不修改 Runtime 算法。 + +如果现有 test 已覆盖底层形状,不重复堆测试;新增 fixture-specific contract test 即可。 + +## 10. 本 Step 不做 +- 不执行 Android Gradle/AAR/APK/ADB/设备测试。 +- 不执行 Jest。 +- 不修改 NavSea。 +- 不修改 WeatherV2 源项目。 +- 不联网下载天气数据。 +- 不引入 provider/network/cache/manifest runtime 到 C++ Core。 +- 不改 v2 schema、Timeline/Playback、shader 数学、DIRECTION 算法。 +- 不发布 npm/tgz/tag;`.3` 仅保持源码版本。 +- 不修改 STEP-062 两处现有用户 dirty 文档,不清理历史 untracked/build/cache。 + +## 11. 必须验证 +至少执行并记录: +1. fixture converter/validator 自检 PASS,含 plane byteLength/SHA/grid/bounds/time/finite/validity。 +2. synthetic Wave independence oracle PASS。 +3. package `.3` TypeScript `tsc --noEmit` PASS。 +4. RN Test Host TypeScript/typecheck PASS(使用项目现有命令,不跑 Jest)。 +5. RN TurboModule codegen schema 检查 PASS,确保 STEP-069 ABI 未被破坏。 +6. 受影响 Core 精确对象编译 PASS。 +7. 相关 C++ fixture/Source 过滤测试 PASS。 +8. `git diff --check` PASS。 + +所有 noisy build 使用 `$build-output-throttle`。若完整 `mbgl-test/runner` 已经最新可直接使用;若因改 C++ test 触发重链,允许 Linux 重链。 + +## 12. 文档输出 +必须生成/更新: +- 本任务书。 +- `docs/50_Step记录/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_v1.md` +- `docs/报告/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex完成报告_v1.md` +- 追加 `粒子开发记录.md` +- 新工具 README / fixture dataset manifest 必须中文说明为主。 + +状态上限:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。不得标记 Android CLOSED/PASS。 + +## 13. Git 收尾 +所有本任务实际修改必须 scoped commit 并 push `origin/particle/main`。 +- 开始/结束都核对 branch、HEAD、origin。 +- 严禁 `git add .` / `git add -A`。 +- 不提交 STEP-062 两处用户 dirty、历史任务 MD、build/cache/tgz。 +- 可使用 1 个实现 commit + 1 个文档 handoff commit;必要时再用一个纯 Git 元数据收尾 commit。 +- 完成报告记录 implementation/handoff commit、push result、remote branch、Final HEAD、`HEAD == origin/particle/main`。 +- push 失败不得声称完成。 diff --git a/docs/40_任务/STEP-071A_V3-07_Linux综合冻结_测试Fixture修正_Codex任务_v1.md b/docs/40_任务/STEP-071A_V3-07_Linux综合冻结_测试Fixture修正_Codex任务_v1.md new file mode 100644 index 000000000..974512e9b --- /dev/null +++ b/docs/40_任务/STEP-071A_V3-07_Linux综合冻结_测试Fixture修正_Codex任务_v1.md @@ -0,0 +1,40 @@ +# STEP-071A V3-07 Linux 综合冻结:测试 Fixture 修正 + +## 基线 +- 仓库:`/mnt/sda1/MapLibreParticleLab` +- 分支:`particle/main` +- 基线 HEAD:`fcc533dc9518395d0c25794131f53f0fae20d51c` +- 只允许修改:`test/style/source.test.cpp` + +## 已确认问题 1:Host duplicate 测试数据错误 +`HostFieldSubmissionRejectsInvalidWithoutChangingLastValidField` 当前用同一份 `uniform-east` payload 提交 T0/T1。 +Runtime 先按 GPU content identity 返回 `Duplicate` 是正确行为;测试预期 `InitialWindowReady` 已过时。 + +修正要求: +- T0 使用 `uniform-east/field.f32`。 +- T1 使用兼容但内容不同的 `uniform-north/field.f32`。 +- 后续 invalid/non-monotonic 检查使用不会与当前 lastValid 重复的 payload;尤其 non-monotonic 必须真正到达 non-monotonic 判定,而不是先命中 Duplicate。 +- 不修改 `sameFieldGpuContent`、Duplicate 顺序或任何 Runtime 生产逻辑。 + +## 已确认问题 2:Timeline 测试时间重复 +`RasterArrayTimelineCandidatesReachReadyThroughFieldAdapter` 使用 east/north/vortex 三份旧 fixture;三份 metadata 的 `timeIndex` 均为 0。 +当前 `OfflineFixtureParticleAdapter` 保留 `validTime = timeIndex + 1`,因此旧测试会形成重复时间,Timeline 正确 fail-closed,而测试无限等待。 +修正要求: +- 不改共享 fixture 文件。 +- 仅在该测试内部把 north/vortex metadata 的 `timeIndex` 调整为严格递增值,例如 east=0、north=1、vortex=2。 +- 保持 grid/bounds/vector semantics/encoding 不变。 +- 保持测试仍通过真实 `OfflineFixtureParticleAdapter` / Source preload / Timeline 路径验证,不改成绕过适配器的伪测试。 +- 若需要小型局部 helper,只能放在 `test/style/source.test.cpp`,不得扩散生产 API。 + +## 禁止事项 +- 不修改 `src/`、`include/`、shader、JNI、Android、RN、WeatherV2、NavSea。 +- 不修改 STEP-062 用户 dirty 文件。 +- 不执行编译、测试、代码审计、搜索、Android Gradle、ADB、Jest。 +- 不执行 git add/commit/push。 +- 不创建额外完成报告;验证与 Git 收尾由 ChatGPT 在 tmux `rdc` 中直接完成。 + +## 完成输出 +修改完成后只报告: +1. 修改了 `test/style/source.test.cpp` 哪两个测试; +2. 每处如何避免 Duplicate / 重复 validTime; +3. 是否修改了任何生产代码(必须为“否”)。 diff --git a/docs/40_任务/STEP-071A_V3-07_Linux综合冻结_测试Fixture修正_Codex任务_v2.md b/docs/40_任务/STEP-071A_V3-07_Linux综合冻结_测试Fixture修正_Codex任务_v2.md new file mode 100644 index 000000000..2d8c11186 --- /dev/null +++ b/docs/40_任务/STEP-071A_V3-07_Linux综合冻结_测试Fixture修正_Codex任务_v2.md @@ -0,0 +1,27 @@ +# STEP-071A V3-07 Linux 综合冻结:测试 Fixture 修正 v2 + +## 基线与现状 +- 当前基线:`fcc533dc9518395d0c25794131f53f0fae20d51c` + 未提交的 v1 测试修正。 +- v1 的 Host duplicate 修正已实测 PASS,不得回退。 +- 只允许修改:`test/style/source.test.cpp`。 + +## 唯一问题 +`RasterArrayTimelineCandidatesReachReadyThroughFieldAdapter` 中 v1 使用: +`northMetadata.find("\"timeIndex\":0")` +实际共享 fixture metadata 格式是: +`"timeIndex": 0` +因此 `find()` 返回 `npos`。 + +## 必做修正 +- 保留 v1 已完成的 east/north distinct payload 修正。 +- 将 north/vortex 的测试内 `timeIndex` 替换逻辑改为匹配实际 JSON 文本格式。 +- north 必须得到 `timeIndex = 1`,vortex 必须得到 `timeIndex = 2`。 +- 不修改共享 fixture 文件。 +- 不修改任何生产代码或 Runtime 行为。 + +## 禁止 +- 不编译、不测试、不搜索/审计、不运行 Git。 +- 不修改其他文件。 +- 不运行 Android/ADB/Jest。 + +完成后只报告修改内容和是否修改生产代码。 \ No newline at end of file diff --git a/docs/40_任务/STEP-072A_V3-08_RNTestHost移除Legacy双Bridge_Codex任务_v1.md b/docs/40_任务/STEP-072A_V3-08_RNTestHost移除Legacy双Bridge_Codex任务_v1.md new file mode 100644 index 000000000..e07e6a322 --- /dev/null +++ b/docs/40_任务/STEP-072A_V3-08_RNTestHost移除Legacy双Bridge_Codex任务_v1.md @@ -0,0 +1,50 @@ +# STEP-072A V3-08 RN Test Host 移除 Legacy 双 Bridge + +## 基线 +- 仓库:`/mnt/sda1/MapLibreParticleLab` +- 分支:`particle/main` +- 已提交 HEAD:`dd356a277db05ce0075bee43f431ddeb2aba19cc` +- 当前工作区已有 STEP-072 未提交修改,必须保留,不得回退或覆盖。 +- 只处理 RN Test Host legacy bridge 重复 autolink 问题。 + +## 已确认事实 +- App 已只从 `@navsea/maplibre-particle-rn` 使用 `FieldRuntime`。 +- Expo autolinking 当前同时链接: + - `@maplibre-particle/weather-runtime-bridge` + - `@navsea/maplibre-particle-rn` +- 两者都注册 Native module `WeatherRuntimeBridge`,codegen library 都是 `WeatherRuntimeBridgeSpec`。 +- Test Host 根 `react-native.config.js` 主动注册 `./modules/weather-runtime-bridge`。 +- `tsconfig.json` 仍 include `modules/weather-runtime-bridge/src`。 +- V3-08 必须只保留正式 package `.3` 的 bridge。 + +## 必做修改 +1. 删除整个 tracked legacy 目录: + `integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/` + - 只删除其中 Git tracked 源码/配置;不要清理、提交或处理其 untracked `android/build/`。 +2. 删除 Test Host 根: + `integration/react-native/MapLibreParticleRNTestHost/react-native.config.js` + - 该文件目前唯一用途是注册 legacy bridge;删除后不要新造替代配置。 +3. 修改: + `integration/react-native/MapLibreParticleRNTestHost/tsconfig.json` + - include 收敛为 `App.tsx` 与 `src`,移除 legacy module src。 +4. 更新 Test Host README 当前 V3-08 章节: + - 明确 legacy local `@maplibre-particle/weather-runtime-bridge` 已移除。 + - `@navsea/maplibre-particle-rn@13.4.1-particle.3` 是唯一 `WeatherRuntimeBridge` 提供者。 + - 历史 STEP-061/062 章节可保留为历史说明,不得改写历史事实。 +## 禁止事项 +- 不得运行 Android Gradle、AAR/APK build、publish、install 或 ADB。 +- 不得运行或加入 Jest。 +- 不得修改 `packages/maplibre-particle-rn/`、MapLibre Core/JNI/WeatherRuntime、fixture、NavSea 或 release manifest。 +- 不得删除/清理任何 untracked/generated 文件;尤其不得处理 legacy 目录下现有 `android/build/`。 +- 不得执行 `git add .`、`git add -A`、commit、push、reset、clean、checkout 或 stash。 +- 不得修改 STEP-062 dirty 文档和其它历史 dirty/untracked 文件。 + +## 完成后必做报告 +逐项报告“已完成 / 未完成 / 无法验证”,并说明原因: +1. tracked legacy bridge 源码/配置是否全部删除。 +2. 根 `react-native.config.js` 是否删除。 +3. `tsconfig.json` 是否只包含 `App.tsx` 与 `src`。 +4. README 是否明确正式 package 为唯一 bridge provider。 +5. 明确说明未执行 Android/ADB/Jest/Git。 + +不要自行做 Android 验证;完成文件修改后停止。 \ No newline at end of file diff --git a/docs/40_任务/STEP-072B_V3-08_RN_Kotlin_ReadableMap编译修复_Codex任务_v1.md b/docs/40_任务/STEP-072B_V3-08_RN_Kotlin_ReadableMap编译修复_Codex任务_v1.md new file mode 100644 index 000000000..15ca1edd6 --- /dev/null +++ b/docs/40_任务/STEP-072B_V3-08_RN_Kotlin_ReadableMap编译修复_Codex任务_v1.md @@ -0,0 +1,32 @@ +# STEP-072B V3-08 RN Kotlin ReadableMap 编译修复 Codex 任务 v1 + +## 背景 +V3-08 Win11 正式 npm 消费链已验证到 Android Kotlin 编译阶段。`@navsea/maplibre-particle-rn@13.4.1-particle.3` 的 `WeatherRuntimeBridgeModule.kt` 在 `setWeatherColorStops()` 中使用 `ReadableMap`,但源码 import 区缺少 `com.facebook.react.bridge.ReadableMap`。 + +Win11 错误: +- `Unresolved reference 'ReadableMap'` +- 后续多个 `getDouble` unresolved 均为级联错误。 + +## 唯一允许的代码修改 +仅修改: +`packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt` + +在 React Native bridge import 区加入: +`import com.facebook.react.bridge.ReadableMap` + +除此之外不得修改任何生产代码、接口、行为或格式。 +## 禁止项 +- 不得运行 Android Gradle、APK/AAR build、ADB、Jest。 +- 不得修改版本号、Maven 坐标、npm package metadata、TurboModule spec、README、CHANGELOG 或 release manifest。 +- 不得修改 Win11 临时 worktree。 +- 不得执行 Git add/commit/push/reset/clean。 +- 不得触碰 STEP-062 dirty 文件和任何无关 untracked/build 目录。 + +## Codex 完成要求 +1. 读取目标 Kotlin 文件并确认缺失 import。 +2. 只增加 `ReadableMap` import。 +3. 输出 `git diff -- <目标文件>` 供人工审计。 +4. 不执行任何编译或测试。 + +## 后续由 ChatGPT 执行 +ChatGPT 将直接完成 Linux 静态检查、重新 npm pack/publish、Win11 dependency/build 验证、文档与 Git 收尾。 diff --git a/docs/40_任务/STEP-072_V3-08_Android_Native_v2与单次综合验收准备_Codex任务_v1.md b/docs/40_任务/STEP-072_V3-08_Android_Native_v2与单次综合验收准备_Codex任务_v1.md new file mode 100644 index 000000000..328dfdb7e --- /dev/null +++ b/docs/40_任务/STEP-072_V3-08_Android_Native_v2与单次综合验收准备_Codex任务_v1.md @@ -0,0 +1,81 @@ +# STEP-072 V3-08 Android Native v2 与单次综合验收准备 + +## 基线 +- 仓库:`/mnt/sda1/MapLibreParticleLab` +- 分支:`particle/main` +- 基线 HEAD:`dd356a277db05ce0075bee43f431ddeb2aba19cc` +- 当前 RN 包:`13.4.1-particle.3` +- 当前旧 Native AAR:`13.4.1-particle.1` + +## 已确认事实 +- Maven Local 仅存在 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`。 +- 该 AAR 的 `WeatherRuntime` 只有 v1 `submitWindField(String, byte[])`,不含 STEP-069 的 v2 multi-plane `submitField(...)`。 +- 当前源码已包含 v2 scalar/vector/shared validity、`clearFields`、`setFieldValueSource`、`setParticleAdvectionScale`。 +- 因此 V3-08 不得继续消费 Native `.1`,也不得覆盖 `.1`。 + +## 目标 +准备新的 Android Native artifact 坐标 `13.4.1-particle.2`,并让 RN `.3` 与 RN Test Host 全部指向该新坐标;只做源码/配置/文档准备,不执行 Android 构建或发布。 + +## 必做修改 +1. `platform/android/VERSION` + - 改为 `13.4.1-particle.2`。 +2. `packages/maplibre-particle-rn/android/build.gradle` + - Native dependency 改为 `.2`。 +3. `packages/maplibre-particle-rn/app.plugin.js` + - MLRN dependency substitution 改为 `.2`。 +4. `integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/build.gradle` + - Native dependency 改为 `.2`。 +5. `integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js` + - dependency substitution 改为 `.2`。 +6. 不修改或提交生成目录 `integration/react-native/MapLibreParticleRNTestHost/android/`。 + +## RN `.3` 文档关系 +更新 `packages/maplibre-particle-rn/README.md` 与 `CHANGELOG.md`: +- RN/TS 为 `13.4.1-particle.3`。 +- 对应目标 Native artifact 为 `13.4.1-particle.2`。 +- `.2` Native 是 STEP-069 v2 multi-plane 所需的新 artifact,不能复用/覆盖 `.1`。 +- 当前只完成坐标准备;Android AAR 尚未由用户手工构建/发布/验收,因此不得写成已发布或 PASS。 + +## Release manifest +更新 `release/maplibre-particle/release-manifest.json` 为当前 `.3` 开发/验收准备状态: +- release:`13.4.1-particle.3` +- releaseState:明确为等待 Android 手工构建/发布/验收,不得使用 `released` +- reactNativePackage.version:`13.4.1-particle.3` +- nativeArtifact.version:`13.4.1-particle.2` +- canonicalField.schema:`maplibre-particle-field/v2` +- transport 描述为 metadataJson + validity local ref + optional scalar/vector local refs +- 不伪造 AAR SHA、发布时间、release tag、Android PASS。 +- 保留可追溯到 `.2` 已发布 RN 版本的 previousBaseline 信息;不要抹掉历史验证语义。 + +## Test Host README +更新 `integration/react-native/MapLibreParticleRNTestHost/README.md` 的当前版本/构建说明: +- 当前 V3-08 目标为 RN `.3` + Native `.2`。 +- 用户必须先手工 build/publish `.2` Native AAR,再 prebuild/build Test Host。 +- 历史 STEP-061/062 可保留为历史章节,但不能让顶部“当前版本”继续写旧 `.1`/step061。 +- Metro 当前 binary asset extension 为 `.f32` + `.u8`。 + +## 新建 Android 单次验收文档 +新建:`docs/60_测试/STEP-072_V3-08_Android单次综合验收_v1.md`。 +只写用户手工执行流程,不执行命令。必须使用 `--console=plain`,ADB 命令固定显式 `adb -s 192.168.1.146 ...`。 + +验收文档至少覆盖: +- Native `.2` Release OpenGL AAR build + Maven Local publish;核对 AAR SHA 与 Maven Local artifact 一致。 +- `javap` 核对 `.2` 的 `WeatherRuntime.submitField`、`clearFields`、`setFieldValueSource`、`setParticleAdvectionScale` 存在。 +- Test Host clean prebuild/build/install/start,一次 Android 周期完成。 +- Wind real、Rain scalar-only、Wave scalar+DIRECTION、Tide VELOCITY、Wave synthetic independence 五组数据逐组加载。 +- 每组要求 T0=`ACCEPTED_PENDING_INITIAL_WINDOW`、T1=`INITIAL_WINDOW_READY`、最终 Runtime=`READY`。 +- PLAY/PAUSE、HALF/NORMAL/DOUBLE、seek T0/T1、AUTO/30/60 FPS、Field/Particle 独立显隐。 +- Wave synthetic 检查 scalar 色场与 direction particle 独立;Rain 不显示粒子。 +- logcat 观察 v2 submit / initial window ready / 无 fatal bridge/JNI 错误。 + +## 禁止事项 +- 不修改 Core/Runtime/JNI/renderer 业务实现。 +- 不执行 Gradle Android build、AAR/APK build、Maven publish、Expo Android build、ADB、安装或真机操作。 +- 不执行 Jest。 +- 不修改 STEP-062 用户 dirty 文件或历史 untracked 文件。 +- 不执行 git add/commit/push。 +- 不修改生成的 `integration/react-native/MapLibreParticleRNTestHost/android/`。 +- 不创建或覆盖 Native `.1` artifact。 + +## 完成后报告 +只报告修改文件、Native `.2` 坐标同步情况、验收文档路径,以及“Android build/publish/ADB 未执行”。 diff --git a/docs/40_任务/STEP-077_QBIN2_V4_Android_JNI_RN桥接只读调查_Codex任务_v1.md b/docs/40_任务/STEP-077_QBIN2_V4_Android_JNI_RN桥接只读调查_Codex任务_v1.md new file mode 100644 index 000000000..aabcc88e1 --- /dev/null +++ b/docs/40_任务/STEP-077_QBIN2_V4_Android_JNI_RN桥接只读调查_Codex任务_v1.md @@ -0,0 +1,170 @@ +# STEP-077 — QBIN2 V4 Android / JNI / RN 桥接只读调查 Codex 任务 v1 + +状态:完成 +日期:2026-09-13 + +## 1. 任务性质 + +本任务是只读架构调查,不实现桥接,不修改 production code。 + +当前 `particle/main` / `origin/particle/main` 基线: + +`0759a06104e1fb30f036db2efb6ef34ca5859759` + +STEP-073~076 已完成 V4 Core、Decoder/Ingress、Binding/Projection Identity、`RasterArraySource` C++ Host API。 +## 2. 调查目标 + +回答:V4 的以下能力应如何最小、稳定地穿过现有 Android/JNI/RN 链路: + +```text +submitEncodedField +setColorBinding +setParticleBinding +setRequestedForecastTime +setForecastPlaybackState +setForecastPlaybackSpeed +clearEncodedField / clearAllEncodedFields +``` + +Host 仍不得控制 T0/T1/MIX。 +## 3. 固定架构边界 + +必须保护: + +```text +Test Host / NavSea JS/TS:下载、缓存、重试、gzip 解压、Manifest/Time Index +MapLibreParticle:只从 Host 已准备好的 raw QBIN bytes 开始 +Core:选择 per-field T0/T1/MIX +Renderer:不解析 QBIN,不知道 provider/network/profile 业务 +``` + +真实服务器 QBIN 不在本调查中下载或使用。 +## 4. 必查实际链路 + +必须基于当前真实代码逐层记录: + +```text +packages/maplibre-particle-rn/src/FieldRuntime.ts +packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts +packages/maplibre-particle-rn/src/types.ts +WeatherRuntimeBridgeModule.kt +WeatherRuntime.java +MapLibreMap.java +NativeMapView.java +native_map_view.hpp / native_map_view.cpp +RasterArraySource V4 Host API +``` + +不得只引用旧 STEP 文档替代源码调查。 +## 5. QBIN 传输形态调查 + +重点比较并给出推荐: + +```text +A. RN 传 qbinFileUri,本地 Android 层读取后交给 Java/JNI +B. RN 直接传 byte[] / number[] +C. 其他现有可复用的本地 binary seam +``` + +必须结合当前 TurboModule/codegen、Kotlin 文件读取、Java byte[]、JNI array copy 与 64 MiB 限制分析。 + +默认倾向不得由 JS 构造大 number array / Base64;但最终结论必须以现有代码和成本为依据。 +## 6. RuntimeMetadata / ProfileContract / Binding ABI 调查 + +分别给出跨层最小 ABI 推荐: + +```text +EncodedFieldRuntimeMetadata +ResolvedFieldProfileContract + requiredPlanes[] +ColorBinding +ParticleBinding +Forecast playback state/speed/time +``` + +比较:typed primitive parameters、primitive arrays、JSON string、ReadableMap/ReadableArray 等可行形态。 + +结论必须说明每一层在哪里解析、在哪里只做边界校验、如何避免 Server/SDK/Host/Native 多处重复业务规则。 +## 7. 兼容性与命名调查 + +必须明确: + +```text +现有 V3 FieldRuntime.submitField 是否保留 +V4 是否新增独立 submitEncodedField,而不是替换旧 submitField +WeatherRuntime 现有 setForecastTime / setPlaybackState / setPlaybackSpeed 是否可复用或必须新增 V4 语义 API +clearFields 与 clearEncodedFields 是否必须分离 +旧 Renderer/V3 timeline 是否可能被 V4 命令误操作 +``` + +不得为了表面统一而让 V4 clear/time/playback 复用会修改 V3 timeline 的旧实现。 +## 8. 生命周期与线程模型 + +必须调查并说明: + +```text +WeatherRuntime 与 MapLibreMap / MapView 的所有权 +Fabric reactTag → MLRNMapView → MapLibreMap 的现有解析路径 +Java 主线程约束 +NativeMapView / Style Source 查找与生命周期 +style reload / map detach / runtime dispose 后 V4 状态如何失效 +``` + +不得引入第二套 Map registry 或长期保存 reactTag/map pointer 的新机制。 +## 9. 错误模型调查 + +必须把 C++ `RasterArrayEncodedFieldSubmitResult` 与 RN 可见结果的推荐映射列全: + +```text +AcceptedInserted +AcceptedReplacedRevision +Duplicate +RejectedDecode +RejectedContract +RejectedInvalidFieldKey +RejectedIdentityConflict +RejectedStaleRevision +RejectedIncompatible +RejectedCapacity +``` + +同时说明文件不可读、参数非法、runtime disposed、source missing/style not ready 等桥接层错误与 Core submit result 如何分层,不得混成一个 UNKNOWN。 +## 10. 验证能力调查 + +不执行 Android 编译、APK、ADB 或实体机操作。 + +调查当前可在 Linux 完成的静态验证入口,包括: + +```text +TurboModule codegen schema +TypeScript typecheck +JNI C++ 精确编译或 Linux 可编译部分 +Java/Kotlin 是否有无需 Android Gradle 的现成静态验证入口 +现有 WeatherRuntime JVM tests 的边界 +``` + +给出后续实现 Step 的分层验收清单,不在本任务实际执行大规模 build。 +## 11. 输出文档 + +必须生成: + +```text +docs/20_调查/STEP-077_QBIN2_V4_Android_JNI_RN桥接调查报告_v1.md +docs/报告/STEP-077_QBIN2_V4_Android_JNI_RN桥接调查_Codex完成报告_v1.md +``` + +并追加 `粒子开发记录.md`。 + +调查报告必须包含当前链路图、推荐 V4 API 表、每层参数形态、错误映射、生命周期、兼容策略、实施文件清单、风险与后续实现拆分建议。 +## 12. 禁止项 + +不得修改 production code、不得新增桥接 API、不得修改 package version、不得修改 Test Host、不得进入真实 QBIN 联调、不得修改 Renderer/GPU。 + +任务开始前已有 dirty/untracked 内容必须保持原样,不得清理、覆盖、暂存或提交。 + +## 13. Git 收尾 + +本任务会生成调查文档,因此完成后只提交本任务新增/修改的文档,commit 后 push `origin/particle/main`。 + +完成报告必须记录 commit hash、commit message、push result、remote branch、Final HEAD;push 失败不得声称任务完成。 + +完成后立即停止,不进入 STEP-077 实现。 diff --git a/docs/40_任务/STEP-081_iOS_Metal_Renderer与真机Smoke入口_Codex任务_v1.md b/docs/40_任务/STEP-081_iOS_Metal_Renderer与真机Smoke入口_Codex任务_v1.md new file mode 100644 index 000000000..4871e6d67 --- /dev/null +++ b/docs/40_任务/STEP-081_iOS_Metal_Renderer与真机Smoke入口_Codex任务_v1.md @@ -0,0 +1,254 @@ +# STEP-081 — iOS Metal Renderer 与真机 Smoke 入口 Codex 任务 v1 + +状态:计划 +日期:2026-09-17 + +## 1. 任务性质与执行机器 + +本任务在 `Mac-mini-tei.lan` 的 `/Users/tei/dev/MapLibreParticleLab` 执行,是 Android 粒子系统完成后的第一张 iOS/Metal 实现任务。 + +当前代码事实: + +- branch:`particle/main` +- 起点 HEAD:`f23f4143b9642de0e8d6661dd567e791494a0a0b` +- `origin/particle/main`:同一 commit +- 当前 Android/RN 代码版本:`13.4.1-particle.5` +- iOS 官方基线:`ios-v6.28.0`,与 Android 共用 core `18545ca10d296704b2cba675b182ff2c8da24588` +- iOS 当前 renderer:Metal +- `ParticleGpuValidationShader` 目前只有 OpenGL ShaderSource,Metal 尚未实现/注册。 + +Codex 是本任务的大规模代码实现者;**Codex 禁止执行任何 iOS/Xcode/CMake 编译、链接、安装、Simulator 或真机操作**。Codex 完成后停止,由 RDC 把结果交给 GPT 审查;审查通过后,实际编译和真机验证由 GPT + RDC 完成。 +## 2. 前置必读 + +开始修改前必须读取并遵守: + +1. `AGENTS.md` +2. `PARTICLE_BASELINE.md` +3. `docs/00_项目规范/` 全部当前文件 +4. `docs/00_项目总则/README.md` 及其列出的当前有效总则 +5. `docs/00_项目总则/当前基线与开发路线_v2.md` +6. `packages/maplibre-particle-rn/CHANGELOG.md` 与 `README.md` +7. 当前 `RasterParticleLayer`、`WeatherColorLayer`、QBIN V4 Projection/Materialization 代码 +8. Metal backend、Metal shader group/program、iOS app CMake 与 `MLNMapView` private Core seam + +若旧总则中出现“Simulator 验收”或“Codex 负责构建”的描述,以本任务和用户最新规则为准:**iOS 不做 Simulator 测试;Codex 不编译;RDC 编译;运行验收只用真机。** + +## 3. 本 Step 目标 + +实现第一条可被 iOS 真机触发的 Metal 粒子渲染路径: + +```text +QBIN V4 / canonical field +→ RasterArraySource V4 renderer projection +→ WeatherColorLayer + RasterParticleLayer +→ ParticleGpuValidationShader (Metal) +→ MapLibre Metal gfx backend +→ iOS 原生 Test App 真机 Smoke 入口 +``` + +本 Step 不接 React Native iOS bridge。 +## 4. 固定架构边界 + +必须保持 Android 已冻结的核心语义不变: + +- 不改变 QBIN V4 文件格式、Decoder、FieldProfile/Binding 语义。 +- 不改变 `RasterArraySource` 对 T0/T1/MIX、Forecast Clock、boundary reuse 的所有权。 +- 不改变 `WeatherColorLayer` 与 `RasterParticleLayer` 分离原则。 +- 不新增 iOS 专用 Field 数据模型,不复制一套 Particle Runtime。 +- 不把 provider、HTTP、manifest、下载、缓存或 NavSea 业务逻辑放进 Native/iOS。 +- 粒子颜色继续独立于 WeatherColor ramp;不得为了 Metal 顺手改变视觉产品语义。 +- iOS/Metal 应消费与 Android/OpenGL 相同的 C++ RenderLayer、UBO、field snapshot 和 renderer-facing V4 projection。 + +后端差异只允许落在 Metal Shader/资源绑定/必要的 backend thin-hook 与 iOS Test App smoke seam。 + +## 5. 必做项 A — Metal Particle Shader + +新增 `ParticleGpuValidationShader` 的 Metal `ShaderSource`,语义必须与当前 OpenGL 版本逐 pass 对齐,至少覆盖: + +- State A/B update +- Trail fade +- Trail particle line draw +- Composite / FrozenComposite +- WeatherColor pass(当前 `u_pass.x > 4.5`) +- T0/T1 `timeMix` +- field bounds / respawn bounds +- VELOCITY 与 DIRECTION 两种 advection semantics +- particleColor / trailColor / speed-color optional path +- WeatherColor stops/ramp +- invalid alpha / invalid sample 的 fail-closed 行为 +- world/map matrix projection +Metal 实现必须复用现有 `ParticleGpuValidationUBO`,不得另建 iOS 专用 UBO。必须核对 Metal `alignas(16)`/buffer layout 与当前 `sizeof(ParticleGpuValidationUBO) == 31 * 16` 契约。 + +必须从当前 shader IDs / texture IDs / attribute IDs 推导 Metal binding,不得凭 OpenGL location 猜测。 + +建议文件形态遵循现有 Metal shader 规范,例如: + +```text +include/mbgl/shaders/mtl/particle_gpu_validation.hpp +src/mbgl/shaders/mtl/particle_gpu_validation.cpp +``` + +具体文件名可按仓库现有命名契约调整,但不得把 MSL 字符串塞进 renderer layer 源文件。 + +## 6. 必做项 B — Metal backend 注册与构建图接线 + +必须完成: + +- 在 Metal renderer backend include 新 shader。 +- 将 `BuiltIn::ParticleGpuValidationShader` 注册进 Metal shader registry。 +- 补齐 Metal shader `attributes`、`textures` 与 UBO/buffer binding。 +- 如当前 CMake/Bazel/source list 不是自动 glob,补齐最小构建图接线。 +- 不修改 OpenGL shader 语义来“迁就”Metal;若共享 ID 定义确有缺口,只做 backend-neutral 最小修正并在报告中单列原因。 + +必须静态检查当前 `RenderRasterParticleLayer` / `RenderWeatherColorLayer` 使用的 gfx API 是否存在明显 OpenGL-only 假设。能通过现有 gfx 抽象工作的代码保持不动;只有明确阻断 Metal 的点才允许最小修改。 +## 7. 必做项 C — iOS 原生真机 Smoke 入口 + +在现有 `platform/ios/app` 内增加一个最小、隔离、可明确触发的 Particle/QBIN V4 Smoke 场景,用于后续 RDC 编译安装后在真实 iPhone/iPad 上触发 Metal shader runtime compilation。 + +要求: + +- 使用现有 iOS Test App,不新建第二个产品工程。 +- 场景应能从 App 内明确进入,名称应能识别为 Particle / QBIN V4 Smoke。 +- 使用仓库现有 Wind Golden QBIN T0/T1 作为本地 fixture;不得联网下载。 +- fixture 可通过 CMake resource copy 或其他现有 iOS app 资源机制进入 bundle;不得复制出新的数据格式。 +- 通过 iOS Objective-C++ / private test seam 取得当前 `mbgl::Map` / Style,并调用现有 C++ Host API;不得新增另一套 runtime。 +- 建立 `raster-array` source、`weather-color` layer、`raster-particle` layer,并提交 Wind T0/T1。 +- Smoke 默认应同时显示 WeatherColor 与 Particle,便于真机确认 Metal 两种 pass 都被实际触发。 +- 至少提供 T0 / midpoint / T1 三个固定时间状态或等价的可操作入口,使后续真机能验证 `mix`。 +- 提供 clear/reload 最小入口或可重复进入场景的方式,以便观察资源释放/重建。 + +Smoke 代码只能存在于测试 App/测试 seam,不得进入公开 iOS SDK API,也不得成为 NavSea 正式接口。 + +## 8. 非目标 + +本 Step 明确不做: + +- React Native iOS bridge / TurboModule +- Expo plugin iOS 接线 +- NavSea iOS 接入 +- npm/iOS artifact 发布 +- Android AAR/RN package 版本升级 +- Vulkan 实现 +- 新粒子算法、视觉重设计、性能调优 + +## 9. 允许修改范围 + +优先限制在: + +```text +include/mbgl/shaders/mtl/ +src/mbgl/shaders/mtl/ +src/mbgl/mtl/renderer_backend.cpp +必要的 CMake/Bazel/source list +src/mbgl/renderer/layers/(仅 Metal 阻断的 backend-neutral 最小修正) +platform/ios/app/ +integration/react-native/MapLibreParticleRNTestHost/assets/qbin-golden/wind/(只读 fixture 来源) +docs/30_设计/ +docs/50_Step记录/ +docs/报告/ +粒子开发记录.md +``` + +若必须超出以上 production 范围,先判断是否仍属于明确的 Metal thin-hook;若需要改 QBIN/Core contract、RasterArraySource API 或 Android bridge,立即停止并在报告标为 BLOCKED,不得自行扩大任务。 + +## 10. 禁止修改范围 + +禁止: + +- `docs/00_项目总则/` +- `docs/00_项目规范/` +- QBIN V4 格式/Decoder/Profile/Binding contract +- Android JNI/Kotlin/RN bridge +- Android 版本号与公开 artifact coordinate +- `packages/maplibre-particle-rn` 公共 API +- upstream remote/tag/history +- 与本 Step 无关的已有文件/未跟踪文件 + +## 11. Codex 验证规则 — 严禁编译 + +Codex 本任务只允许静态验证,例如: + +```text +git diff --check +git status --short +git grep / rg / find +必要的文本/结构一致性检查 +``` + +**禁止 Codex 执行任何可能进入 iOS/Native 构建、链接、shader runtime 编译、安装或运行的命令**,包括但不限于: + +```text +xcodebuild +cmake / ninja / make / bazel build +clang / clang++ / metal / metallib 的编译调用 +pod install / swift build +simctl +任何 iOS Simulator 命令 +任何真机安装/启动命令 +``` + +也不要用 Android/Linux build 代替 iOS 验证。本 Step 的编译状态在 Codex 报告中必须写为 `NOT_RUN_BY_DESIGN`。 + +不加入 Jest,不执行 Jest。 + +## 12. GPT/RDC 后续验收接口 + +Codex 必须在完成报告中明确列出,供 GPT/RDC 后续逐项执行: + +1. iPhoneOS device build 所需的推荐 CMake/Xcode 入口与 target 名称。 +2. Smoke 场景在 iOS Test App 中的进入方法。 +3. 真机预期出现的关键日志事件,至少覆盖 shader/layer creation、field ready、particle resource create。 +4. T0 / midpoint / T1 与 clear/reload 的人工操作步骤。 +5. 若 Metal shader runtime compile 失败,应优先检查的源文件/函数/绑定表。 + +## 13. 必做输出 + +Codex 必须实际生成/更新: + +```text +docs/30_设计/STEP-081_iOS_Metal_Renderer与真机Smoke入口设计_v1.md +docs/50_Step记录/STEP-081_iOS_Metal_Renderer与真机Smoke入口_v1.md +docs/报告/STEP-081_iOS_Metal_Renderer与真机Smoke入口_Codex完成报告_v1.md +粒子开发记录.md +``` + +设计文档必须记录 OpenGL → Metal pass 对照、Metal UBO/attribute/texture binding 表、Metal 注册位置、Smoke 场景数据链和后续 RN iOS bridge 的明确边界。 + +Step 状态在 Codex 结束时只能根据事实写:代码实现完成但未编译时,写“进行中 / WAITING_RDC_BUILD_REVIEW”或等价明确表述;**不得写 iOS PASS、Metal PASS 或真机 PASS**。 + +完成报告必须至少包含: + +- 执行结论:PASS/PARTIAL/BLOCKED/FAIL(仅针对 Codex 代码任务本身) +- branch、Start HEAD、End HEAD、工作树状态 +- 实际修改/新增文件 +- Metal shader/pass/binding 实现摘要 +- iOS Smoke 入口说明 +- 与任务书差异 +- 构建记录:明确 `NOT_RUN_BY_DESIGN — Codex prohibited from building` +- 静态检查记录 +- 已知风险/未完成项 +- 给 GPT/RDC 的编译与真机验收清单 +- commit hash、commit message、push result、remote branch、Final HEAD + +## 14. Git 收尾 + +本任务会修改项目文件,因此 Codex 必须: + +1. 只提交本任务相关文件,不得夹带既有无关 dirty/untracked。 +2. 使用 scoped commit message,例如:`feat: bring up particle renderer on ios metal`。 +3. commit 后 push 到 `origin/particle/main`。 +4. push 失败不得声称完成。 +5. 完成后不得继续下一 Step,不得自行开始 RN iOS bridge。 + +## 15. 停止条件 + +出现以下任一情况必须停止扩大修改并在报告中写明 BLOCKED/PARTIAL: + +- Metal 实现需要改变 QBIN V4/Core contract 才能继续。 +- 需要建立 iOS 独立 Particle Runtime 或复制 Android Runtime 逻辑。 +- 必须修改 Android bridge/API 才能让 iOS smoke 工作。 +- 发现现有 gfx RenderTarget/Texture/Lines 在 Metal 根本不具备所需能力,且修复将变成大规模 gfx backend 重构。 +- 需要升级 Xcode、MapLibre baseline、CMake、SDK 或第三方依赖。 +- 无法以现有 iOS Test App/private test seam 触发 Core Host API,且需要新增公开 SDK API。 + +完成本任务后立即停止,等待 RDC 将 Codex 结果交给 GPT 审查。GPT 审查通过后,才由 RDC 执行 iPhoneOS device build,并只在真实 iPhone/iPad 上做运行验收。 \ No newline at end of file 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-087_QBIN_V3解码与13.4.1-particle.9升级_Codex任务_v1.md b/docs/40_任务/STEP-087_QBIN_V3解码与13.4.1-particle.9升级_Codex任务_v1.md new file mode 100644 index 000000000..d1f3f1bbc --- /dev/null +++ b/docs/40_任务/STEP-087_QBIN_V3解码与13.4.1-particle.9升级_Codex任务_v1.md @@ -0,0 +1,198 @@ +# STEP-087 QBIN V3 解码与 13.4.1-particle.9 升级 Codex 任务 v1 + +## 1. 任务目标 + +在 Mac 专用工作树 `/Users/tei/dev/MapLibreParticleLab-qbv3-p9` 上完成 QBIN V3(Magic=`QBV3`, version=3)正式解码支持,并将统一 Native / RN 源码版本升级到 `13.4.1-particle.9`。 + +本任务必须以以下设计稿为权威依据: +- `docs/30_设计/WeatherV2_QBIN_V3通用天气场二进制格式与React独立Render_Binding设计_v2.2_最终冻结稿.md` + +该设计稿是用户提供原文,必须保留,不得改写、覆盖或删减。实现中如发现规范真实矛盾,只能在完成报告中记录并停止相关 ABI 决策,不得自行修改规范来迁就代码。 + +## 2. 当前基线 + +执行前必须核对: +- 工作树分支:`particle/qbv3-p9` +- 基线必须包含 `release/particle-8` 的两笔发布提交,至少到 `77744dd9f` +- `platform/android/VERSION` 与 `packages/maplibre-particle-rn/package.json` 当前均应为 `13.4.1-particle.8` +- 不得修改其他 Mac 工作树 +- 主线目标仍为 `origin/particle/main` + +先阅读项目根目录 AGENTS.md(如存在)、docs/00_项目总则、docs/00_项目规范,以及 STEP-073~085 中与 QBIN ingress、FieldProfile、发布和 iOS device-arm64 相关的最新记录。## 3. Phase 0:只读现状调查 + +在写代码前完成一次只读调查并在报告中记录结论,至少确认: +- `QbinV2Decoder`、`EncodedFieldIngress::decodeAndValidate()`、`DecodedFieldFrame`、`FieldProfileValidator` 的真实接口。 +- QBV2 source/test 在 Bazel/CMake/平台构建中的注册位置。 +- RN / Android / iOS 当前 encoded-field 入口是否只是透传 bytes,确认 QBV3 不需要新增 JS/TS 公共业务 API。 +- `.8` 版本号实际出现在哪些“当前版本”文件中;禁止全仓替换历史文档中的旧版本号。 +- 现有 Wind / Rain / Wave / Current QBIN Golden Fixture 的来源、尺寸与可复用性。 + +结论应坚持: +- V2 与 V3 只在 Encoded Field Ingress 前分流。 +- Decoder 后继续输出同一 `DecodedFieldFrame / Plane Registry`。 +- Runtime / Binding / Renderer 不感知 QBV2/QBV3。 +- 不把 Provider、Weather Service、HTTP、Manifest、缓存、React 业务逻辑放进 Native Decoder。 + +## 4. Block Geometry 冻结 + +v2.2 规范只剩 `BLOCK_WIDTH / BLOCK_HEIGHT` 未冻结,本任务必须在正式 QBV3 Decoder 固化前完成选择。 + +使用现有四类真实 Golden 数据: +- `integration/react-native/MapLibreParticleRNTestHost/assets/qbin-golden/wind/` +- `rain/` +- `wave/` +- `current/` + +优先复用 QBV2 Decoder 得到 canonical quantized integer grid + validity,再用测试/benchmark 专用 deterministic QBV3 encoder/prototype 比较 `16×16` 与 `32×32`。该 encoder 仅用于测试、fixture 和 benchmark,不得演变成 Weather Provider/生产 Writer。比较必须使用同一 predictor / ZigZag / Rice 规则,至少记录: +- encoded bytes +- block count / non-empty block count +- encode time +- entropy decode time +- total decode time +- CRC time(若可独立测) +- peak temporary bytes(若现有工具不能可靠测量,报告中明确说明,不得伪造) + +选择原则: +1. 首先保证规范一致和 bit-exact round-trip。 +2. 压缩率差异很小时,优先更低 decode/encode 开销、更少 block、实现更简单的一项。 +3. 最终必须只留下一个 production Block Geometry。 +4. production Decoder 不得同时“猜测”16×16/32×32,也不得根据解码是否成功试两遍。 +5. 测试代码可以保留两个候选参数用于 benchmark,但正式 QBV3 ABI 常量必须唯一。 + +把选择依据写入: +- `docs/30_设计/MapLibreParticle_QBIN_V3_Decoder与BlockGeometry实施决策_v1.md` + +该文档是粒子侧实施补充,不改写用户提供的 WeatherV2 v2.2 原文。 + +## 5. 正式 QBV3 Decoder + +新增清晰独立的 QBV3 decoder 文件,优先采用: +- `include/mbgl/particle/ingress/qbin_v3_decoder.hpp` +- `src/mbgl/particle/ingress/qbin_v3_decoder.cpp` + +如现有构建结构要求轻微调整可据实处理,但禁止把 V3 逻辑塞成 `qbin_v2_decoder.cpp` 内的大量 version 分支。实现必须逐项遵守 v2.2 normative 主体,重点包括: +- 112-byte Header、32-byte Descriptor、Little Endian。 +- Magic=`QBV3`、version=3;FRAME 固定 layout/projection/sampleOrder/gridRegistration/tile 字段。 +- Version 3 limits:planeCount 32、cellCount 2,097,152、file 64 MiB、decoded frame 32 MiB。 +- Header bounds finite、Web Mercator 范围、binary64 数值零禁止 -0.0。 +- Numeric dtype 只允许 INT16/UINT16/UINT8/INT8;FLOAT32 与 Numeric BITSET reject。 +- `semantic==3 ⇔ valueClass==VALIDITY ⇔ dtype==BITSET`,且只允许一个 Shared Validity Plane。 +- Validity LSB-first;tail unused high bits 必须为 0。 +- unknown fieldType(包括 0)只作为 opaque hint,不得单独 reject。 +- Numeric `scale > 0` finite,offset finite;数值为 0 时只接受 binary32 +0.0。 +- Parser 顺序保持:结构与 budget → CRC → padding/Validity → Rice decode。 +- CRC-32/ISO-HDLC,CRC field 76..79 计算时视为 0。 +- all-invalid 合法:Validity 全 0、Numeric byteLength=0、decoded raw 全 0。 +- zero-length Plane 可以共享 canonical byteOffset;只对真实非零 range 做 overlap 判断。 +- Numeric Plane byteLength 是各 non-empty block encoded bytes 之和,不含 align16 padding。 + +Rice / predictor 必须严格实现: +- Predictor 在全 Grid 连续,不在 block 边界 reset。 +- invalid cell 不消费 Rice symbol,materialize raw=0。 +- ZigZag 使用无 UB 的 widened fixed-width arithmetic。 +- Rice bitstream 与 remainder 都按 byte 内 LSB-first。 +- riceK 允许 0..31;Decoder permissive,不因非最优 k 单独 reject。 +- unary quotient、MAX_Q、residual、raw reconstruction 全部 checked;非法立即 fail-closed。 +- block padding high unused bits 必须为 0,symbolCount 必须精确。 +- Direction 使用 canonical float32(360/N)、N=floor(360/scale+0.5)、raw∈[0,N)。 +- Direction predictor 必须 normalize;half-circle tie 固定 `-N/2`。## 6. Ingress 与兼容性 + +`EncodedFieldIngress::decodeAndValidate()` 必须按 bytes 中的 magic/version 做显式 dispatch: +- `QBV2 / version=2 → QbinV2Decoder` +- `QBV3 / version=3 → QbinV3Decoder` +- 其他组合 fail-closed + +要求: +- QBV2 frozen behavior 不得改变。 +- V3 decode 成功后继续走现有 `FieldProfileValidator`。 +- Binary-valid 与 Profile-valid 继续分层;V3 Decoder 不得根据 wind/wave/rain/current 名称要求业务 Plane。 +- V2/V3 decoded Plane 的 ContentIdentity 必须继续使用项目既有 canonical identity 语义,避免 Runtime 因容器版本产生无意义 identity 漂移。 +- 如需要抽取共享 helper,可做小范围重构,但不得把公共 API 扩大到 Renderer/Binding。 + +## 7. 测试与 Fixture + +新增 C++ 定向测试,不新增或运行 Jest。 + +至少覆盖: +- Wind / Rain / Wave / Current integer + validity bit-exact round-trip。 +- INT16 / UINT16 / INT8 / UINT8。 +- all-valid、部分 invalid、all-invalid。 +- zero-length shared offset。 +- bad magic/version/endian、reserved、bounds、negative zero、descriptor order/duplicate、packing、CRC。 +- malformed Rice、unterminated unary、riceK>31、symbolCount 少/多、nonzero block padding、MAX_Q/residual/raw overflow。 +- non-optimal but otherwise-valid riceK 必须 accept。 +- fieldType=0 与 unknown fieldType 必须 accept。 +- Direction 359↔0、N-1↔0、half-circle tie、predictor wrap。 +- WEB_MERCATOR + NODE sample 公式的定位 Golden Test。 + +最终冻结 Geometry 后生成唯一正式 QBV3 Golden Fixture 集,至少覆盖四类场;实验 16/32 双候选 fixture 不得混入 production fixture 目录。## 8. 构建与验证 + +本任务在 Mac 上执行 Native/C++ 开发和精确编译验证。 + +必须: +- 运行与 QBV2/QBV3 decoder 直接相关的 C++ 定向测试。 +- 运行必要的 Mac 原生精确编译,确认新增 source/header 被真实构建目标消费。 +- 对 iOS 只验证 iPhone/iPad device arm64 路径;不得恢复 simulator slice。 +- 如执行 noisy compile/test,遵循当前 Codex 全局输出节流规则。 +- `git diff --check` 必须 PASS。 + +禁止: +- Android Gradle build +- AAR/APK 构建或安装 +- ADB +- 启动 Android App / 清理数据 +- Android 实机/虚拟机冒烟 + +Android 最终构建安装仍由用户手工执行。本任务应在报告中给出最少的用户手工验证入口,但不要代替用户执行。 + +## 9. 升级到 13.4.1-particle.9 + +以已纳入的 `.8` 为基线,将当前有效版本升级为: +`13.4.1-particle.9` + +按 `.8` 的真实发布结构更新所有“当前版本”源文件/配置,包括 Native VERSION、RN package、插件/Android 依赖引用、CHANGELOG/README 中的当前安装示例,以及 release pipeline 需要的当前版本元数据。 + +禁止: +- 全仓替换历史 STEP / 报告中的 `.1~.8`。 +- 覆盖既有 `.8` artifact、tag 或发布目录。 +- 未经用户明确要求发布外部 Maven/npm/CDN artifact。 +- 未经用户明确要求创建正式 release tag。## 10. 文档输出 + +除本任务书和用户提供设计稿外,任务必须生成/更新: +- `docs/30_设计/MapLibreParticle_QBIN_V3_Decoder与BlockGeometry实施决策_v1.md` +- `docs/50_Step记录/STEP-087_QBIN_V3解码与13.4.1-particle.9升级_v1.md` +- `docs/报告/STEP-087_QBIN_V3解码与13.4.1-particle.9升级_Codex完成报告_v1.md` + +完成报告必须写清: +- 起始 HEAD / 最终 HEAD +- 选定的 Block Geometry 与 benchmark 证据 +- 新增/修改的主要代码文件 +- QBV2 回归结果 +- QBV3 测试结果 +- Mac 原生精确编译结果 +- 未执行的 Android 手工门槛 +- 版本号变更位置 +- 仍存在的风险或未完成项 + +如同主题报告已存在,按 v2/v3 递增,不覆盖旧报告。 + +## 11. Git 收尾 + +完成规定代码、测试、版本和文档后: +1. 只提交本任务相关变更。 +2. 不提交无关 untracked 文件或其他工作树内容。 +3. commit message 使用清晰英文,例如:`feat: add QBIN V3 decoder for particle.9`。 +4. 将最终结果 push 到公开远端 `origin/particle/main`。 +5. push 失败不得声称完成。 +6. 完成报告必须记录最终 commit hash、commit message、push result、remote branch、Final HEAD。 +7. 最终再次确认本地 HEAD 与 `origin/particle/main` 一致。 + +## 12. 停止条件 + +如果实际 benchmark 无法用现有真实 fixture 对 16×16/32×32 做可信选择,或 v2.2 存在导致合法文件无法唯一解释的真实矛盾: +- 不得猜测 production Geometry。 +- 完成可独立验证的代码/调查部分后停止。 +- 在 `docs/报告/` 生成接口/规范阻塞说明,明确缺什么数据或决定。 +- 不得把支持两种 production Geometry 的“兼容 decoder”当成完成方案。 + +除此之外,应完成实现,不要因为普通工程细节反复向用户确认。 \ No newline at end of file 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_任务/STEP-089_静态渲染Revision与Invalidation基础层_Codex任务_v1.md b/docs/40_任务/STEP-089_静态渲染Revision与Invalidation基础层_Codex任务_v1.md new file mode 100644 index 000000000..8f1fd67b5 --- /dev/null +++ b/docs/40_任务/STEP-089_静态渲染Revision与Invalidation基础层_Codex任务_v1.md @@ -0,0 +1,144 @@ +# STEP-089 静态渲染 Revision 与 Invalidation 基础层 Codex 任务 v1 + +## 目标 + +为 STEP-090 Static CPU Fast Path 建立可靠、可验证的静态状态失效判据。 + +STEP-089 不负责跳过 static source/tile/render-tree/prepare,也不负责降低 CPU;只建立“静态前缀当前是否仍可安全复用”的 revision / invalidation 基础,确保后续 fast path 不会复用过期静态状态。 + +当前基线: + +- branch: `particle/main` +- baseline HEAD: `07a17b4c701cd22c8e8b516ad84f4044f291f9fa` +- STEP-088 retained underlay 已完成: + - GPU static draw cache + - map-scoped runtime Cache ON/OFF + - Release 最小 diagnostics + - Android/iOS/RN runtime API + - Android NavSea 真实 PBF 性能场景 +- 实测证明主要剩余成本是 Particle continuous repaint 下 PBF/vector/static renderer pipeline 被重复 processing。 +- STEP-090 才负责 CPU fast path。 + +## 必做项 + +- [ ] 引入单调递增的 static/retained revision 机制,能表达“静态前缀相关输入已变化”。 +- [ ] retained cache build 时记录所依据的 revision。 +- [ ] reuse 前必须确认当前 revision 与 build revision 一致。 +- [ ] 下列变化必须使 revision 前进或明确 invalidate: + - [ ] camera / transform + - [ ] viewport / size / pixel ratio + - [ ] style reload / style mutation + - [ ] layer add/remove/reorder/property mutation + - [ ] source add/remove/change + - [ ] tile / drawable 内容更新 + - [ ] image / sprite / glyph 变化 + - [ ] placement / symbol fade 状态变化 + - [ ] render context / surface / backend resource reset + - [ ] debug render options + - [ ] terrain / 3D / custom layer 等当前不能安全证明静态的场景 +- [ ] symbol fade / placement transition 活跃时不得复用 stale static cache。 +- [ ] unsupported / uncertain 情况继续 fail closed 到 full renderer。 +- [ ] Cache OFF/ON runtime 行为保持不变。 +- [ ] Release diagnostics 保持低成本;允许新增只读 revision 字段,但禁止 per-frame Release 日志。 +- [ ] Debug 日志可增加 revision / invalidation reason,便于 RDC 验证。 +- [ ] 原有 LayerGroup ordinal、dynamic boundary、transparent underlay、legacy ordering 语义不得回退。 +- [ ] 不实现 STEP-090 的 static CPU processing skip。 +- [ ] 不新增 React 管理的 renderer cache。 +- [ ] 不把 provider/network/NavSea Store 逻辑引入 MapLibre Core。 +- [ ] 不使用 Jest。 + +## Revision 设计约束 + +优先采用单调 `uint64_t` revision / generation: + +- revision 只增不减; +- 不依赖 wall clock; +- 不要求每种事件都维护独立缓存副本; +- 可以保留 reason/category 作为诊断,但 correctness 依赖 revision,而不是字符串 reason; +- revision wraparound 可按现实上不可达处理,但类型必须明确。 + +建议最少区分: + +- 当前 static revision; +- retained underlay build revision; + +如现有 Renderer/RenderOrchestrator 的生命周期更适合其他命名,可调整,但不要把模型复杂化。 + +## Invalidation 范围原则 + +STEP-089 首选“安全偏保守”。 + +如果某类状态变化是否会影响 static prefix 无法可靠判断: + +- 直接 bump revision / invalidate; +- 不做复杂局部 dependency graph; +- 不为了提高 hit rate 提前实现 STEP-090 级别的细粒度 source/layer dependency tracking。 + +目标是让 STEP-090 有一个可信的 fast-path gate,而不是在 089 追求最高 cache reuse。 + +## Placement / Symbol 特别规则 + +已有 Android 真机证据: + +- Particle 放到全部 63 层上方时,symbol-heavy prefix 会出现持续 `static-placement` / `static-transition`。 +- BEFORE_SYMBOL_CLUSTER(fill/line 在下,symbol cluster 在上)是目前最稳定边界。 + +因此: + +- placement/fade active 必须阻止旧 static state 被错误复用; +- fade/placement 稳定后允许重新 build 并记录新 revision; +- 不得简单忽略 symbol placement 来提高 TOP 场景 reuse。 + +## Runtime API / Diagnostics + +保持现有: + +- `setRetainedUnderlayEnabled` +- `getRetainedUnderlayDiagnostics` + +允许在 diagnostics 增加: + +- `staticRevision` +- `buildRevision` + +如增加,Android/iOS/RN 三端需一致;如果为了控制 STEP-089 范围决定暂不公开,也至少在 Debug Native 日志中可观察。 + +不要新增第二套 runtime module / map registry。 + +## 测试要求 + +新增/扩展 Native directed tests,至少覆盖: + +1. revision 单调递增; +2. build revision 与 current revision 相等时允许 reuse; +3. camera/viewport/style/source 变化后 revision 改变并禁止旧 cache reuse; +4. placement/fade active 时不 reuse stale cache; +5. Cache OFF -> invalidate;重新 ON -> 下一次重新 build; +6. unsupported / resource failure 保持安全 fallback; +7. 原有 RetainedDynamicBoundary / LayerGroupRange / RetainedUnderlayCache 测试继续通过。 + +不要求 Jest。 + +## Codex 执行规则 + +- 先完整阅读本任务、AGENTS.md、STEP-088 v3 Step/报告,以及 retained underlay 当前实现。 +- 不需要提交实施计划。 +- 直接实现。 +- 不运行 Gradle / Android build / ADB / Xcode。 +- 不运行大型 noisy build/test;编译和最终验证由 RDC 负责。 +- 可以做必要的静态代码检查,但不要替代 RDC 编译。 +- 不 commit/push,等 RDC 验证后统一收尾。 +- 不触碰用户既有无关 dirty/untracked。 +- 不使用 worktree。 +- 无法完成的项明确说明,不得静默跳过。 + +## 完成时必须报告 + +逐项按“已完成 / 未完成 / 无法验证”报告上述必做项,并列出: + +- 修改文件; +- revision 的实际所有者与生命周期; +- 每类 invalidation 在哪里触发; +- 新增测试名; +- RDC 应执行的精确 OpenGL / Metal / Android Validation 命令; +- 明确说明没有实现 STEP-090 fast path。 diff --git a/docs/40_任务/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex任务_v1.md b/docs/40_任务/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex任务_v1.md new file mode 100644 index 000000000..a792d8b5d --- /dev/null +++ b/docs/40_任务/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex任务_v1.md @@ -0,0 +1,239 @@ +# STEP-090 Particle-only Static CPU Fast Path Codex 任务 v1 + +## 目标 + +基于 STEP-088 retained underlay GPU cache 与 STEP-089 static revision/invalidation gate,真正减少 Particle continuous repaint 下静态 PBF/vector 的 CPU processing。 + +当前已实测: + +- PURE PBF idle:RenderThread ~0% +- PURE PARTICLE 2048 / FPS30:RenderThread ~10.2% +- production Style + 60 个 PBF layer hidden + Particle:RenderThread ~25.5% +- production PBF + Particle:RenderThread ~96~100% +- STEP-088 仅省 static GPU draw,RenderThread 仍 ~96% +- STEP-089 已保证: + - steady state staticRevision/buildRevision 稳定; + - camera/style/tile 等真实变化会 revision bump; + - transient drawable/topology churn 不误触发。 + +因此 STEP-090 的目标是: + +> 当当前帧仅由 Particle animation 触发,且静态状态 revision/topology/camera/viewport/placement 等均稳定时,跳过不必要的静态 PBF/vector CPU pipeline,复用上一帧已准备好的 render state / RenderTree,并只执行 Particle 所需的动态 render。 + +## 基线 + +- branch: `particle/main` +- baseline HEAD: `98e1d2d977ba9e007ba313c14d63bfa54ab9d23b` +- STEP-089: CLOSED / PASS + +## 核心约束 + +### 1. Fast path 必须在 createRenderTree 之前判定 + +当前: +`Renderer::render(UpdateParameters)` +→ `RenderOrchestrator::createRenderTree()` +→ `renderTree->prepare()` +→ `Renderer::Impl::render()` + +如果 fast path 只放在 `Renderer::Impl::render()`,已经无法节省 createRenderTree/source update/prepare,不满足 STEP-090。 + +### 2. 仅允许 Particle-only frame + +必须内部区分: + +- Particle-only animation frame; +- 其他任何会影响 map/render tree 的更新。 + +Fast path 只在能够证明“除了 Particle animation,没有其他更新需要处理”时使用。 + +不得新增 Host/RN 手工调用: +- 不要 `particle.renderFastFrame()` +- 不要 `cacheMapNow()` +- 不要 `invalidateBasemap()` + +这是 MapLibre Native 内部自动行为。 + +### 3. 安全 gate + +至少要求: + +- retained underlay runtime enabled; +- retained cache valid; +- retained buildRevision == current staticRevision; +- camera / transform unchanged; +- viewport / pixelRatio unchanged; +- styleLoaded; +- dynamic boundary unchanged; +- topologyRevision unchanged; +- no static transition; +- no placement/fade transition affecting correctness; +- no source/tile/image/glyph/style/layer mutation; +- no context reset/debug/3D/custom unsupported; +- no pending change request that would alter render tree; +- no non-particle layer transition/repaint; +- no fading tile; +- no still image request; +- no map mode / special render path that cannot safely reuse。 + +任何不确定情况必须 full path。 + +### 4. RenderTree reuse + +优先实现一个最小、明确的 cached prepared RenderTree / render state 生命周期: + +- full frame 成功创建并 prepare 后,可作为下一次 particle-only fast frame 的候选; +- fast frame 不重新 createRenderTree; +- fast frame 不重新调用 full `RenderTree::prepare()`; +- 复用对象生命周期必须明确,不能悬挂引用; +- full path 创建新 tree 后替换旧缓存; +- revision/context/style reset 时失效。 + +若 RenderTree 当前设计无法安全长期持有,允许提取一个专用 immutable prepared frame state,但不要大规模重构。 + +### 5. 动态部分 + +Particle fast frame必须仍然: +- 正常 advance simulation; +- 正常 render Particle; +- composite retained underlay; +- 正常 present; +- 保持 particle cadence/targetFps 语义。 + +对于 Particle 之外的动态 layer: +- 如果存在其他 `needsRepaint()` / transition / placement / fading tile,则禁止 fast path,走 full path。 +- 不要在本步骤尝试支持 AIS/Ownship 等所有未来动态 layer;先确保 Particle-only fast frame 安全。 + +WeatherColor: +- 若本身不需要 repaint 且输入未变,可与静态 prepared state 一起复用; +- 若其状态变化,full path。 + +### 6. Source/PBF processing + +Fast path 命中时,目标是明确不执行: +- full `createRenderTree()` +- static PBF/vector source update +- tile pyramid normal per-frame update +- static layer prepare +- static placement orchestration +- static layer group rebuild + +允许仍执行 fast frame 所必须的最小 upload/tweaker/render,但要避免重新走静态 source/layer CPU pipeline。 + +### 7. Runtime ON/OFF + +继续使用现有 `retainedUnderlayEnabled` 作为整体优化 gate: + +- ON:允许 retained cache + CPU fast path; +- OFF:完全回到原 full-render baseline,用于 A/B; +- 不新增第二个用户开关,除非内部 debug-only 诊断确实必要。 + +这样同一个 Release 构建仍可通过 RN RendererRuntime 做完整 A/B。 + +### 8. 诊断 + +Debug 增加最小 counters/log: + +- `ParticleFastFrameHit` +- `ParticleFastFrameMiss` +- `ParticleFullFrame` +- miss reason(debug only) + +建议周期日志继续并入 `ParticleRetained:` 或新增稳定前缀 `ParticleFastPath:`,字段至少: + +```text +hit= +miss= +full= +reason=<...> +staticRevision= +buildRevision= +``` + +Release 不做 per-frame 日志。 + +如现有 diagnostics 适合,可在 Release pull snapshot 中增加累计 hit/miss,但不是强制;不要扩大 RN 高频接口。 + +### 9. 性能验收目标 + +不预设固定百分比门槛,但 Android NavSea PBF Test App 必须证明: + +- AFTER_CLIP / BEFORE_SYMBOL_CLUSTER 固定 Camera; +- Cache/FastPath ON 时 fast hit 持续增长; +- full frame 不随 Particle frame 一比一增长; +- Particle 仍连续运动; +- static draw 继续不随 particle frame 增长; +- RenderThread CPU 相比 STEP-089 / Cache OFF 有明显下降。 + +关键基线用于比较: + +```text +PURE PARTICLE ~10.2% RenderThread +PBF hidden + Particle ~25.5% +PBF visible + Particle ~96~100% +``` + +目标方向是让 PBF visible + Particle 大幅向前两者靠近,而不是只减少几个百分点。 + +### 10. 回退验证 + +必须验证: + +- pan / zoom / rotate → fast path 立即 miss/full; +- tile load/change → full; +- style/layer/source change → full; +- cache OFF → full; +- static state重新稳定后 → fast hit恢复; +- TOP symbol-heavy / placement active → 保守 full/fallback; +- resize/context reset → full; +- no crash / stale map / ordering regression。 + +## 不做 + +- 不新增 Host API 触发 fast frame。 +- 不把 provider/network/vendor 逻辑放入 Native。 +- 不改 RN Weather Service 边界。 +- 不做 STEP-091 级别的多动态层通用调度。 +- 不做复杂 dependency graph。 +- 不做 Jest。 +- 不使用 worktree。 + +## Native 测试 + +新增 directed tests,至少覆盖: + +1. particle-only + stable revision → fast candidate true; +2. revision mismatch → false; +3. camera/viewport mismatch → false; +4. non-particle transition/repaint → false; +5. placement/fade/fading tile → false; +6. cache disabled → false; +7. context/style/source/tile mutation后 → false; +8. full frame重新稳定后 → fast candidate true; +9. cached RenderTree 生命周期/替换不悬挂; +10. 原 12 个 retained tests继续通过。 + +## Codex 执行规则 + +- 先完整读取 AGENTS.md、STEP-088 v3、STEP-089 task/Step/报告、当前 renderer.cpp / render_orchestrator / renderer_impl / RenderTree。 +- 不提交实施计划,直接调查并实现。 +- 不运行 Gradle/ADB/Xcode/大构建。 +- 不 commit/push。 +- 不触碰无关 dirty/untracked。 +- 不使用 worktree。 +- 不加 Jest。 +- 如果发现当前 RenderTree 不能安全缓存,先在当前架构内采用最小安全方案,不进行无边界重构。 +- 如果实现范围明显膨胀到新的架构层,停止并报告阻塞点,不强行完成。 + +## 完成时必须报告 + +逐项说明: +- fast path gate 实际位于哪里; +- 如何判断 particle-only; +- 哪些 full-frame 工作被跳过; +- cached RenderTree / prepared state 生命周期; +- 所有 miss/fallback 条件; +- 修改文件; +- 新增 tests; +- RDC 精确验证命令; +- 明确没有新增 Host 手工 fast-frame API。 diff --git a/docs/40_任务/STEP-091_ZoomOut_Viewport_Particle_Reseed_Codex任务_v1.md b/docs/40_任务/STEP-091_ZoomOut_Viewport_Particle_Reseed_Codex任务_v1.md new file mode 100644 index 000000000..caafc020b --- /dev/null +++ b/docs/40_任务/STEP-091_ZoomOut_Viewport_Particle_Reseed_Codex任务_v1.md @@ -0,0 +1,225 @@ +# STEP-091 Zoom Out 后粒子 Viewport 一次性重播种 Codex 任务 v1 + +## 目标 + +修复正式 RasterParticleLayer 在“大 Zoom → 小 Zoom(Zoom Out)”后,旧粒子短时间聚集在原视野小区域,必须等待粒子生命周期/运动逐渐扩散后才恢复正常密度的问题。 + +用户实机现象: +- Zoom Out 完成后,粒子集中成一小团/小矩形; +- 风场和粒子仍继续运动; +- 等旧粒子跑远或寿命到期后,整个新视野才逐渐恢复正常铺开; +- Zoom In 暂未观察到同类问题。 + +本任务必须以当前实际源码为准修复,不改 Field/T0/T1/Timeline 数据契约,不引入第二套粒子系统。 + +## 基线 + +- repo: MapLibreParticleLab +- branch baseline: `origin/particle/main` +- baseline HEAD: `297b9126200cea3da3bdef2faacedcd9845f6d08` +- 实施工作树: `/Users/tei/dev/MapLibreParticleLab-step091` +- 实施分支: `step091-zoom-out-reseed` +- STEP-090 已 CLOSED / PASS,retained underlay + Particle-only CPU fast path 必须保持。 +## 已确认根因 + +重点读取: +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- OpenGL / Metal particle shader +- 当前 camera invalidation、respawnBounds、State/Trail seed 逻辑。 + +当前正式路径已经存在: +- `TrailCameraStableDelay = 150ms` +- camera 变化时 `trailCameraInvalidated=true` +- camera stable 后重新启用 viewport density +- `viewportRespawnDomain(...)` +- `respawnBounds` +- shader 的 `outsideRespawnBounds(position)` +- `stateSeeded / trailSeeded` +- initial seed 时由 shader 使用 `respawnSeed(...)` 在 `respawnBounds` 内播种。 + +问题是 Zoom Out 后新的 respawnBounds 变大,旧粒子仍位于扩大后的 bounds 内,因此不会命中 outsideRespawnBounds,只能等待 age/invalid/运动触发 respawn。 + +## 修复原则 + +Camera 稳定后,如果本次手势/连续 camera 变化的最终结果是明确 Zoom Out,则执行一次 viewport density reseed: +- 下一次 State pass 把全部粒子重新播种到“新的、已经计算完成的 respawnBounds”; +- 同时清理/重新 seed Trail,避免旧小视野 trail 与新粒子叠加; +- 之后立即恢复正常连续 simulation。 +这不是完整 Simulation reset。禁止为了 Zoom Out: +- 调用 `resetSequence("ZOOM_OUT")`; +- 重建 State/Trail GPU resource; +- 重新上传 Field texture; +- reset Timeline / T0 / T1 / MIX; +- style reload; +- source reload; +- Host/RN 手工 reset API。 + +优先复用现有 `stateSeeded=false` / `trailSeeded=false` 和 shader initial-seed 机制;若实际代码证明还需一个极小内部状态,可最小新增。 + +## Camera 手势状态 + +需要记录“一次 camera invalidation 开始前”的稳定 Zoom,不能每帧覆盖。 + +建议最小状态语义: +- camera gesture / invalidation 开始时保存 `startZoom`; +- camera 变化期间仍沿用现有 invalidated 行为; +- 150ms stable 时比较最终 `endZoom`; +- 仅当 `endZoom < startZoom - epsilon` 才请求 viewport reseed; +- epsilon 仅防浮点/微小抖动,建议约 0.05,实际值可按现有 double/zoom 语义选择并写明理由; +- Zoom In、纯 Pan、Rotate、Pitch 不触发 full reseed; +- 新一轮 camera invalidation 必须重新建立 startZoom,不得复用上一轮。 +## 执行顺序要求 + +必须保证 reseed 使用新的 viewport domain: + +1. camera 变化 → invalidated; +2. camera 连续变化期间不触发 reseed; +3. stable debounce 到期; +4. 计算当前 `viewportRespawnDomain(transformState, viewport, field)`; +5. 得到当前 `viewportDensityActive` 和新的 `respawnBounds`; +6. 仅在本轮最终为 Zoom Out 且 viewport respawn domain 有效时执行一次: + - `stateSeeded = false` + - `trailSeeded = false` + - 清除 pending 标记; +7. 下一次 State/Trail pass 使用现有 initial seed 流程; +8. 后续帧继续正常 simulation。 + +若 stable 时 respawn domain fallback/无效: +- 不允许留下会在未来无关 Pan 时突然触发的 pending reseed; +- 必须安全消费/取消本轮请求并记录原因; +- 不得伪造 viewport domain。 + +## Trail + +Zoom Out viewport reseed 必须同时让 Trail 重新 seed/清理。 +不得保留旧小视野 trail 作为新 viewport 的历史。 +不得改变正常连续播放时的 trail fade、T0/T1 overlap、simulationRebuild=false 语义。 +## Shader / UBO 边界 + +当前 GL/Metal shader 已有: +- initial seed; +- respawnSeed; +- respawnBounds; +- alpha/validity 防止 respawn jump 连线。 + +本任务默认不修改 shader、UBO layout 或公共 Layer API。 +只有实际源码证明现有 initial-seed 无法正确完成 viewport reseed 时,才允许最小修改,并在完成报告说明原因。 + +绝对禁止: +- partial reseed ratio 新协议; +- per-particle CPU readback; +- JS/RN 粒子重播种; +- 新增 Host camera callback 驱动粒子; +- 将 provider/network/vendor 逻辑放入 Renderer。 + +## STEP-089 / STEP-090 保护 + +Camera 变化期间原本就应使 fast path miss/full;稳定后应恢复 fast hit。 +本次修复不得破坏: +- retained underlay cache; +- Particle-only static CPU fast path; +- staticRevision/buildRevision 语义; +- camera/style/tile mutation fail-closed; +- normal continuous particle cadence; +- WeatherColor 与 RasterParticle 分层; +- Runtime Field Window/T0/T1/MIX。 +## 诊断日志 + +新增或扩展最小 Debug 日志,至少能确认一次 Zoom Out reseed: + +建议稳定前缀: +`ParticleDensity: event=VIEWPORT_RESEED reason=ZOOM_OUT ...` + +字段至少包含: +- startZoom +- endZoom +- respawnBounds +- stateReseed=true +- trailReseed=true +- fieldResubmit=false +- timelineReset=false +- gpuResourceRebuild=false + +fallback/无效 domain 时输出一次 skip/cancel 原因即可。 +禁止 per-frame spam。 + +## 定向测试 + +不得加入 Jest。 + +至少验证: +1. Zoom Out 超过 epsilon → stable 后请求一次 reseed; +2. Zoom In → 不请求; +3. 纯 Pan → 不请求; +4. Rotate/Pitch 且 Zoom 未下降 → 不请求; +5. Zoom Out 后 fallback domain → 不留下延迟 pending; +6. 一次 gesture 只 reseed 一次; +7. 下一轮 gesture startZoom 正确刷新; +8. 现有 respawn/State/Trail 生命周期测试继续通过(如已有可复用测试)。 +测试实现优先使用小型 C++ helper / directed native test;不要为了测试扩大公共 API。 +若当前测试结构无法直接覆盖 private renderer 状态,允许测试提取出的纯判定 helper,但不得引入复杂新模块。 + +## Mac 验证 + +Codex 需要: +- 读取根目录 `AGENTS.md` 与相关 STEP-055、STEP-089、STEP-090 记录/报告; +- 执行与修改直接相关的 Mac 原生精确编译/定向 C++ 测试; +- noisy build 遵循全局 `$build-output-throttle`; +- `git diff --check` PASS; +- 不运行 Android Gradle/APK/ADB; +- 不运行 Jest。 + +## Android Test App + +本任务默认不新增 UI 控件。 +现有 Particle Core Layer Validation / 正式 RasterParticleLayer 场景应足以通过 ADB 手势验证。 +如果现有 Test App 无法可靠触发正式 viewport respawn path,才允许最小增加 debug-only validation 入口,禁止改产品 Host API。 + +## 实机验收预期(由 RDC 在 Codex 完成后执行) + +- 编译 OpenGL Debug Test APK; +- 安装到当前可用 Android 测试设备; +- 启动 `ParticleCoreLayerValidationActivity`; +- 使用正式 RasterParticleLayer 场景与较高粒子数; +- 做明显 Zoom In 后再快速 Zoom Out; +- 日志出现一次 VIEWPORT_RESEED; +- Zoom Out 后不再等待 10 秒级生命周期才铺满新视野; +- 无 crash / ANR / GL/EGL error; +- camera stable 后 ParticleFastPath fast hit 恢复增长。 +## 文档与 Git + +Codex 完成后必须: +- 更新 `粒子开发记录.md`; +- 生成 `docs/50_Step记录/STEP-091_ZoomOut_Viewport_Particle_Reseed_v1.md`; +- 生成 `docs/报告/STEP-091_ZoomOut_Viewport_Particle_Reseed_Codex完成报告_v1.md`; +- 完成报告逐项写“已完成 / 未完成 / 无法验证”,不要写实施计划; +- 记录实际修改文件、实际测试命令与结果; +- 记录是否改 shader/UBO/API; +- 记录 STEP-090 fast path 是否保持。 + +提交本任务相关变更到当前工作分支,并 push: +`git push origin HEAD:particle/main` + +不得提交任何与本任务无关的改动。 +push 失败不得声称任务完成。 + +完成报告必须记录: +- implementation commit hash +- commit message +- push result +- remote branch +- Final HEAD / origin/particle/main + +## 完成标准 + +只有同时满足以下条件才可判定 Codex 阶段 PASS: +- 根因按当前代码被修复; +- Zoom Out stable 后只做一次 viewport State/Trail reseed; +- 不做 full simulation reset / field resubmit / timeline reset / GPU resource rebuild; +- Zoom In/Pan/Rotate/Pitch 不误触发; +- 定向 C++ 测试与 Mac 精确编译通过; +- git diff --check 通过; +- 文档完成; +- commit + push 到 `origin/particle/main` 成功。 + +Android APK 构建、安装与设备手势验收由后续 RDC 串行执行,不属于 Codex 自己的完成条件。 diff --git a/docs/40_任务/STEP-092_ContinuousRange可退化分段RetainedCache_Codex任务_v1.md b/docs/40_任务/STEP-092_ContinuousRange可退化分段RetainedCache_Codex任务_v1.md new file mode 100644 index 000000000..f1314342e --- /dev/null +++ b/docs/40_任务/STEP-092_ContinuousRange可退化分段RetainedCache_Codex任务_v1.md @@ -0,0 +1,249 @@ +# STEP-092 Continuous Range 可退化分段 Retained Cache — Codex 任务 v1 + +## 0. 目标 +在当前 MapLibreParticleLab / Particle V11 Renderer 上实现一个 explicit Continuous Range。 +正式架构定义: +App 用正常 Style 定义唯一 Z-order,并指定两个现有 Style Layer ID 作为闭区间 Continuous Range; +Renderer 将两个 ID 解析为连续 layerIndex,根据 Continuous Range 在 Style 中的位置自然形成 0~2 个 Retained Range Cache; +最终可以是 1、2 或 3 个 Render Range,绝不是固定“三层三明治”。 + +本任务只支持一个 Continuous Range,不做多个 Range 或任意 N Segment Planner。 + +## 1. 环境与起始确认 +工作目录:/Users/tei/dev/MapLibreParticleLab +目标分支:particle/main +历史参考 HEAD:b29e564944197072a0effaec36ca95c48fe0649b,但不得据此强制回退。 +修改前必须: +1. git fetch origin +2. 确认当前分支、HEAD、origin/particle/main +3. 记录起始 HEAD +4. 检查 git status --short +5. 不覆盖、不删除、不 reset/clean、不提交无关 dirty/untracked +6. 若远端更新,以当前真实 origin/particle/main 为基线 +## 2. 唯一 Z-order 事实来源 +Style Layer 顺序是唯一 Z-order 事实来源: +App/RN 负责业务语义和 Style Layer 顺序; +MapLibre Style 保存正式 Z-order; +C++ Renderer 只消费最终连续 layerIndex。 +Renderer 禁止硬编码 NavSea 业务 Layer ID(AIS/Alarm/Wind/Wave 等)。 +禁止建立 BOTTOM/CONTINUOUS/TOP 第二套 metadata 分类表。 +禁止增加 AnchorLayer、ContinuousStartLayer、ContinuousEndLayer、RenderAnchorLayer、空白 Marker Layer。 + +## 3. Continuous Range API 语义 +App 直接指定两个已有正常 Style Layer: +startLayerId +endLayerId +公开语义为闭区间 [startLayerId, endLayerId]。 +Native 按当前 Style 解析成 startIndex/endIndex。 +合法条件必须是 startIndex <= endIndex。 +必须正式支持 startLayerId == endLayerId,单 Layer Continuous 完全合法。 +App/RN 不传数字 index,RN 不计算 layerIndex。 + +内部建议转半开区间: +continuousBegin = startIndex +continuousEndExclusive = endIndex + 1 +Bottom=[0,continuousBegin) +Continuous=[continuousBegin,continuousEndExclusive) +Top=[continuousEndExclusive,layerCount) +Range 存在判断统一为 begin < endExclusive。 +## 4. 四种合法结构与 Cache 数量 +必须支持: +1. Bottom + Continuous + Top → 2 retained caches +2. Bottom + Continuous → 1 Bottom cache +3. Continuous + Top → 1 Top cache +4. Continuous only → 0 retained caches + +Continuous only 当 startIndex==0 且 endIndex==layerCount-1,是完全合法状态,不是 error/fallback。 +不存在的 Range: +不创建 cache; +不分配 texture; +不分配 framebuffer/render target; +不 build; +不 composite; +不参与 cache-valid gate。 + +当前 RetainedUnderlayCache 应合理泛化为可复用 retained range cache 能力。 +具体命名服从当前源码,但不得固定创建 bottomCache+topCache 占位资源。 +Bottom/Top GPU 资源必须 lazy 创建。 + +## 5. Final Render 顺序 +如果 Bottom 存在:build/reuse/composite Bottom +然后 render Continuous +如果 Top 存在:build/reuse/composite Top +顺序只能来自 Style,自然退化为 3/2/1 段。 +## 6. global layerIndex / pass 语义 +分 Range 后仍必须保持整个 Style 的: +原始 layerIndex; +原始 ordinal; +parameters.currentLayer; +depthRangeSize。 +不得给 Bottom/Continuous/Top 各自从 0 重新编号。 +Opaque 继续 reverse traversal; +Translucent 继续 forward traversal。 + +## 7. Visibility +Range topology 来自 Style/orderedLayers,不来自当前 visible/renderable Layers。 +hidden start/end 仍必须参与 Range index 解析。 +Layer ON/OFF 是内容变化,不是 Range topology 变化。 +例如 start=10 hidden、11 visible、end=12 hidden,Range 仍是 10..12,不得缩成 11..11。 + +## 8. Style mutation +必须支持正常 Style mutation。 +Continuous 内新增/删除普通成员正常工作。 +Bottom/Top 内增删使对应实际存在 cache invalid/rebuild。 +同一 Range 内 reorder 使 cache 内容 invalid/rebuild。 +Layer 跨 Range 移动:重新解析 start/end index → topology revision → invalidate affected caches → invalidate fast prepared state → rebuild。 +startLayerId/endLayerId 自身删除:Range 立即 invalid,清除 stale cache/index/prepared state并安全 fallback。 +Layer 后续重新出现且顺序合法时可恢复 explicit Range。 +## 9. Top Cache 正确性 +Top retained texture 必须是透明 RGBA,并正确使用 premultiplied alpha。 +目标支持 Particle 属于 Continuous、land-area 属于 Top Retained 时,Top texture 正确覆盖 Particle。 +不得新增 LandMask RT、Wave/Tide mask system 或 Native land-mask 业务判断。 +land-area 仍只是普通 Style Z-order。 + +## 10. Legacy compatibility +当前 WeatherColor/RasterParticle 可能通过 startsRetainedDynamicRange() 参与 legacy boundary。 +规则: +explicit Continuous Range 已配置且有效 → STEP-092 Range 模式; +未配置 explicit Range → 保持当前 legacy retained-underlay 行为。 +一旦 explicit Range 生效,startLayerId/endLayerId 是唯一 topology/boundary 来源。 +禁止 explicit Range 与 startsRetainedDynamicRange() 混合生成第二套 boundary。 + +## 11. RN / Native API +在现有 RendererRuntime 增加稳定入口,目标语义: +setContinuousLayerRange(reactTag,startLayerId,endLayerId) +要求:ID 非空;允许相同;map-scoped;thread-safe;monotonic config revision; +Range 修改触发 renderer update/invalidation。 +Android/iOS bridge 不理解业务;C++ 按当前 Style 解析 index。 +保留 setRetainedUnderlayEnabled() 与 getRetainedUnderlayDiagnostics() 兼容。 +## 12. Invalid / fallback +至少处理: +start 不存在; +end 不存在; +startIndex > endIndex; +Style 未 ready; +未配置 explicit range。 +要求:不 crash;不猜 index;不复用 stale index/cache/prepared state; +不根据 Particle/WeatherColor 类型偷偷修正 explicit Range; +安全进入 legacy/full renderer; +diagnostics 说明 fallback 原因。 +Continuous-only 不是 invalid/fallback。 + +## 13. Particle-only CPU Fast Path +现有 Particle-only CPU fast path 不得退化。 +三段:Bottom reuse → Continuous render → Top reuse +只有 Bottom:Bottom reuse → Continuous render +只有 Top:Continuous render → Top reuse +0 cache:Continuous render +不存在的 cache 不能成为 Fast Path miss 原因。 +Bottom 不存在时不得要求 bottomCache.valid=true;Top 同理。 +Fast gate 只检查实际存在 retained ranges。 +Continuous Range 本身必须通用,不依赖 RasterParticleLayer/Weather Layer ID。 +若 Continuous 中存在当前 Fast Path 无法证明安全的未知 Layer:Fast Path fail closed,走正常 full CPU frame,Range Z-order/cache 仍正确。 +不要为此重写整个 RenderGraph。 +## 14. Invalidation +继续 correctness-first、偏保守模型,至少覆盖: +camera/transform/viewport/resize/pixel ratio/style reload; +layer add/remove/reorder/property; +source mutation/tile update/drawable update; +image/sprite/glyph; +placement/symbol fade/transition; +context reset/debug options/unsupported 3D/legacy ordering/resource/compositor failure。 +无法判断只影响 Bottom 还是 Top 时,可同时 invalidate 所有实际存在 retained caches。 +本任务不做复杂 dependency graph。 +topology 变化还要 invalid Range index resolution、retained prepared state、Particle fast prepared state。 + +## 15. Symbol / Placement +Top 可能包含 symbol/label/Navigation/AIS/Alarm。 +correctness 优先。 +placement/symbol fade/transition 无法证明 reuse 安全时,invalidate/full fallback,稳定后 rebuild。 +不重写 Placement 系统。 + +## 16. Diagnostics +至少增加/暴露: +rangeConfigured, rangeValid, +startLayerId,endLayerId,startIndex,endIndex, +bottomPresent,bottomBuild,bottomReuse,bottomInvalidation, +topPresent,topBuild,topReuse,topInvalidation, +fallback,lastReason。 +bottomPresent=false 且 topPresent=false 可以只是 Continuous-only,不能自动判 error。 +Release 保持 pull-based,不增加 per-frame 高频日志。 +## 17. GPU 生命周期与双后端 +实际 retained texture 数量必须严格等于实际存在 retained ranges: +Continuous only=0; +Bottom+Continuous=1; +Continuous+Top=1; +三段=2。 +稳定 Continuous frame 不得每帧重建 retained texture/framebuffer/compositor resources。 + +Mac 上完成当前工程允许的精确 Native 验证。 +OpenGL 重点:Bottom/Top target 创建复用、Top RGBA/premultiplied alpha、0/1/2 cache 无空资源、Continuous-only、Fast Path。 +Metal 重点:同等 Range/cache topology、透明 Top composite、0/1/2 cache 生命周期、explicit Range/fallback 与 OpenGL 一致。 +不得用 Linux Core/OpenGL 构建替代 Mac 基线。 +若 Android 实机/Gradle/ADB 项未执行,报告必须明确列为“未执行 Android 项”,不得伪造通过。 + +## 18. Native directed tests(不加入 Jest) +至少覆盖: +start==end; +startend invalid; +start missing; +end missing; +hidden start/end; +Style reorder; +start/end 删除; +四种 Range 结构; +Bottom empty/Top empty/both empty; +不存在 Bottom 时不因 bottom cache missing 而 Fast Path miss; +不存在 Top 时不因 top cache missing 而 Fast Path miss。 +还应验证: +explicit Range 有效时不混用 legacy boundary; +explicit Range invalid 不复用 stale cache; +single-layer Continuous 正常; +hidden endpoint 不改变 topology; +reorder 后重新解析 index; +endpoint 删除 fail safe; +0/1/2 retained cache resource 数量与 topology 一致; +Opaque/Translucent traversal 保持 global layerIndex 语义。 + +## 19. 明确不做 +不做:任意 N Segment Planner、多个 Continuous Range、LOW/MEDIUM/HIGH 分类、 +自动分析 Layer 更新频率、自动调整 Z-order、Anchor/Marker Layer、新 Style Layer 类型、 +NavSea AIS 专项优化、Weather provider、Route ETA、Native land-mask 业务、 +Wave/Tide 业务判断、第二套 Z-order、NavSea Layer ID 硬编码、 +复杂 per-source dependency graph、PBF 重构/重生成、通用 RenderGraph、 +Placement 重写、Particle 专属 Continuous 机制、Jest。 +如发现必须突破边界才能继续,停止扩大范围并记录阻塞。 + +## 20. 必做文档 +生成: +docs/50_Step记录/STEP-092_ContinuousRange可退化分段RetainedCache_v1.md +docs/报告/STEP-092_ContinuousRange可退化分段RetainedCache_Codex完成报告_v1.md +追加:粒子开发记录.md +同主题已存在则递增版本,不覆盖旧文件;所有文档中文。 +完成报告必须包含: +起始 HEAD、最终 HEAD、修改文件清单; +C++ Renderer、RN API、Android API/bridge、iOS API/bridge; +Range config 所有者、ID→layerIndex 解析位置机制; +single-layer range、1/2/3 Render Range、0/1/2 retained cache; +invalidation、legacy compatibility、Fast Path、diagnostics、GPU lifecycle; +OpenGL、Metal、Native tests、未执行 Android 项、known limitations; +commit SHA/message、push result、remote branch、origin/particle/main Final HEAD、最终 git status --short。 + +## 21. Git +仅 scoped staging。 +禁止 git add -A、git add .、git reset --hard、git clean。 +不得触碰无关 dirty/untracked。 +结束前:git diff --check;核对变更;只 stage 本任务文件;scoped commit;push origin particle/main。 +push 后必须确认: +git rev-parse HEAD +git rev-parse origin/particle/main +git status --short +要求 HEAD == origin/particle/main。 +push 失败不得声明完成。 + +## 22. PASS Gate +只有架构、四种 Range、0/1/2 cache、global layerIndex、Opaque/Translucent、Fast Path、 +diagnostics、RN/Android/iOS API、Native directed tests、OpenGL/Metal 规定验证、 +文档、scoped commit、push、远端 HEAD 一致全部满足,才可判 STEP-092 PASS。 +关键项无法完成必须明确 PARTIAL/BLOCKED 和原因,禁止伪造 PASS。 diff --git a/docs/40_任务/STEP-093_ContinuousRange运行态综合验收_Codex任务_v1.md b/docs/40_任务/STEP-093_ContinuousRange运行态综合验收_Codex任务_v1.md new file mode 100644 index 000000000..7a1a033f8 --- /dev/null +++ b/docs/40_任务/STEP-093_ContinuousRange运行态综合验收_Codex任务_v1.md @@ -0,0 +1,652 @@ +# STEP-093:Continuous Range 运行态综合验收 + +## 1. 任务目标 + +在 STEP-092 已完成并关闭的 Continuous Range / Retained Cache 架构基础上,进入真实 Renderer 运行态综合验收。 + +本任务不重新设计 STEP-092 架构,不引入新的 Z-order 体系,不重做 retained cache 结构。目标是通过可重复、可观察、可诊断的运行态场景,确认以下核心事实在真实渲染过程中成立: + +1. Style Layer 顺序仍然是唯一 Z-order 真相。 +2. `[startLayerId, endLayerId]` 能稳定解析为一个 Continuous Range。 +3. Bottom / Continuous / Top 四种合法退化结构均能正确工作。 +4. 实际 retained cache 数量严格为 0 / 1 / 2,不存在的 range 不创建无意义 GPU 资源。 +5. Particle 连续播放时,稳定的普通 Style/PBF 内容能够命中 retained cache,而不是每帧重复完整绘制。 +6. Top range 能真实覆盖 Continuous 中的 Particle,验证最终画面顺序为:`Bottom → Continuous → Top`。 +7. Camera、viewport、style、layer/source 等变化发生时能够正确 invalidation、rebuild,并重新进入稳定 reuse。 +8. Fast Path 失败时必须安全退回 full CPU frame,但不得破坏 Continuous Range、retained cache 和 Z-order。 +9. Android/OpenGL 与 iOS/Metal 最终表现一致;若本阶段只能先完成 Android,则 iOS 可明确记录为后续待验收,不得虚报通过。 + +--- + +## 2. 当前基线 + +开始任务前必须先检查真实仓库,不得只依赖任务书中的 SHA。 + +预期起始基线: + +- Repository: `MapLibreParticleLab` +- Branch: `particle/main` +- STEP-092 最终状态:`CLOSED / PASS(Mac Native 验证闭环)` +- STEP-092 最终已知 HEAD:`8a5d88008c142e2a80e0ff602d228e0e67de2d83` +- `HEAD == origin/particle/main` +- 工作区应为 clean + +STEP-092 已完成的关键内容包括: + +- Continuous Range 显式配置; +- Bottom / Continuous / Top 三段模型; +- 四种合法退化拓扑; +- 0 / 1 / 2 retained cache; +- Bottom/Top retained cache; +- Top 透明 RGBA + premultiplied alpha 合成; +- Style mutation 后重新解析; +- invalid explicit range 的安全 fallback; +- Particle Fast Path 与 retained cache 的协同; +- Android / iOS Bridge 接口; +- OpenGL Native build; +- iOS Metal-only native build; +- 定向 Native tests 29/29 PASS。 + +本 STEP 不重新证明上述静态设计,而是证明它们在真实运行态画面与资源生命周期中成立。 + +--- + +## 3. 开始前必读 + +Codex 开始前必须读取: + +1. 仓库根目录 `AGENTS.md` +2. 当前粒子系统总规则 / 架构约束文档 +3. `docs/50_Step记录/STEP-092_ContinuousRange可退化分段RetainedCache_v2.md` +4. `docs/报告/STEP-092_ContinuousRange可退化分段RetainedCache_Codex完成报告_v2.md` +5. STEP-092 相关实现文件与 diagnostics 实现 +6. 当前 Android/iOS Particle Test App / validation 页面 / Host runtime 实现 +7. 当前可用的测试按钮、场景、日志和 diagnostics 展示入口 + +如实际仓库结构与本任务书描述不一致,以当前真实代码为准,并在报告中写明差异。 + +--- + +## 4. 本任务职责划分 + +### 4.1 Codex 负责 + +Codex 负责: + +- 调查当前可用的运行态测试入口; +- 为 STEP-093 补齐必要的 Test/Validation UI、场景切换、diagnostics 展示和最小测试辅助代码; +- 只在确有必要时补充可观察性; +- 修复在准备阶段发现的明确、局部、与 STEP-093 验收直接相关的问题; +- 为 RDC / 用户准备清晰、可逐项执行的运行态验收入口; +- 更新中文 STEP 记录、完成报告和开发记录; +- 提交并 push 自己完成的代码与文档。 + +### 4.2 RDC / 用户负责 + +实际 Android / iOS 编译、安装、启动、真机或模拟器运行、视觉操作与最终画面确认由 RDC / 用户执行。 + +Codex 不得仅依据静态代码、日志或自己推测就把运行态测试标记为 PASS。 + +如果没有实际执行某个运行态场景,必须写为 `NOT RUN`、`WAITING_RUNTIME_VALIDATION` 或其他明确的未完成状态,不得写成 PASS。 + +--- + +## 5. 严格保护的 STEP-092 架构 + +本 STEP 禁止借“测试方便”修改以下既定架构: + +1. Style 顺序是唯一 Z-order 真相。 +2. App/RN 只提供 `startLayerId`、`endLayerId`。 +3. Native 自行解析 index。 +4. Continuous Range 为闭区间 `[startLayerId, endLayerId]`。 +5. 内部:`continuousBegin = startIndex`,`continuousEndExclusive = endIndex + 1`。 +6. 三段:Bottom `[0, begin)`、Continuous `[begin, endExclusive)`、Top `[endExclusive, layerCount)`。 +7. `start == end` 合法。 +8. Continuous-only 合法,不是 fallback。 +9. 不允许加入 anchor / marker layer。 +10. 不允许增加第二套 BOTTOM / CONTINUOUS / TOP metadata 真相源。 +11. 不允许 Renderer 硬编码 NavSea/AIS/Alarm/Wind/Wave 等业务 Layer ID。 +12. Visibility 不改变 topology。 +13. explicit range 有效时不得再混用旧 `startsRetainedDynamicRange()` 作为 topology boundary。 +14. 不允许把普通 style layer 改造成新的特殊 layer type 来解决测试问题。 +15. 不做任意 N-range planner。 +16. 不做多个 Continuous Range。 +17. 不做频率分类系统。 +18. 不做 PBF 通用重构。 +19. 不做 RenderGraph 重构。 +20. 不加入 Jest。 + +--- + +# 6. STEP-093 必测场景 + +## 6.1 场景 A:Bottom + Continuous + Top + +构造至少:Bottom 为普通 Style/PBF Layer,Continuous 为 Particle / Weather 动态 Layer,Top 为普通 Style/PBF Layer。 + +必须验证: + +- explicit range valid; +- Bottom present = true; +- Top present = true; +- retained cache 实际数量 = 2; +- Bottom cache 可 build/reuse; +- Top cache 可 build/reuse; +- 最终画面顺序 `Bottom → Continuous → Top`; +- Top 内容能够真实覆盖 Particle; +- 无明显黑边、错误透明、旧 cache 或层序颠倒。 + +## 6.2 场景 B:Bottom + Continuous + +Continuous 结束于当前 Style 最顶层。 + +必须验证: + +- Bottom present = true; +- Top present = false; +- retained cache 实际数量 = 1; +- 不得创建 Top texture/FBO/compositor; +- 不存在的 Top 不得阻塞 Fast Path; +- 画面顺序正确。 + +## 6.3 场景 C:Continuous + Top + +Continuous 从当前 Style 最底层开始。 + +必须验证: + +- Bottom present = false; +- Top present = true; +- retained cache 实际数量 = 1; +- 不得创建 Bottom texture/FBO/compositor; +- 不存在的 Bottom 不得阻塞 Fast Path; +- Top 可正确覆盖 Particle。 + +## 6.4 场景 D:Continuous only + +Continuous 覆盖全部 Style Layer。 + +必须验证: + +- Bottom present = false; +- Top present = false; +- explicit range valid = true; +- retained cache 实际数量 = 0; +- 这是合法 Continuous-only,不得被识别为 fallback; +- 不创建 retained texture/FBO/compositor; +- Particle/continuous renderer 正常运行。 + +## 6.5 场景 E:Single-layer Continuous + +设置 `startLayerId == endLayerId`。 + +必须验证: + +- range valid; +- startIndex == endIndex; +- continuousEndExclusive == startIndex + 1; +- 正常参与 Bottom/Top 切分; +- 不得 fallback; +- 不得被误认为空 range。 + +--- + +# 7. Z-order 核心视觉验收 + +这是 STEP-093 的最高优先级运行态验收。 + +必须构造一个肉眼可以清楚确认覆盖关系的场景: + +1. Bottom 中放一个明显底图/填充层; +2. Continuous 中运行 Particle; +3. Top 中放一个能够明显覆盖 Particle 的普通 Style Layer。 + +优先使用已有普通 Style Layer;如果测试环境缺少合适图层,可添加纯测试用普通 Style Layer,但不得引入新的特殊 layer type 或业务硬编码。 + +建议 Top 验证至少覆盖:opaque fill、半透明 fill、line;如当前测试环境可行,再加入 symbol/text。 + +必须确认: + +- Top 真正绘制在 Particle 上方; +- Particle 不会穿透 opaque Top; +- 半透明 Top 与 Particle 合成正确; +- 无黑边; +- 无颜色异常变暗; +- 无错误双重 alpha; +- 无 cache 旧内容残留。 + +--- + +# 8. Stable Frame / Cache Reuse 验收 + +在 Camera、viewport、Style、普通 PBF/source 内容都不变化,仅 Particle 持续动画时,必须观察: + +- Bottom/Top retained cache build 后持续 reuse; +- 不应每个 Particle frame 重建 retained cache; +- 不应每 frame 重建 retained texture; +- 不应每 frame 重建 FBO/offscreen texture; +- 不应每 frame 重建 composite group; +- Fast Path 在满足条件时能够持续命中; +- diagnostics 能够区分 build / reuse / invalidation。 + +如果现有 diagnostics 无法证明资源生命周期,可增加最小 debug-only counter / reason 字段。禁止为了测试引入生产环境高频日志风暴。 + +--- + +# 9. Camera / Viewport invalidation + +至少验证:Pan、Zoom、Rotate、Pitch、viewport resize/orientation 或等价尺寸变化,以及测试环境可实际触发时的 pixel ratio 变化。 + +每项检查: + +- 旧 retained cache 被正确 invalidation; +- 下一帧或正确时点 rebuild; +- rebuild 后重新进入 reuse; +- 不出现 stale cache; +- 不出现错位; +- 不出现黑屏; +- 不出现 Particle 与地图不同步; +- topology 不应无故改变。 + +--- + +# 10. Style Mutation / Topology 重新解析 + +必须提供可重复测试入口,至少覆盖: + +### 10.1 Continuous 内部变更 + +添加 Layer、删除非 endpoint Layer、reorder Continuous 内部 Layer。 + +预期:重新解析必要状态;range 仍有效时保持正确 topology;cache 根据需要 invalidation;不产生第二套 Z-order。 + +### 10.2 Bottom 内变更 + +添加、删除、reorder,检查 Bottom cache invalidation / rebuild / reuse。 + +### 10.3 Top 内变更 + +添加、删除、reorder,检查 Top cache invalidation / rebuild / reuse。 + +### 10.4 Layer 跨边界移动 + +将某个普通 Layer 做有代表性的 Bottom → Continuous、Continuous → Top 或 Top → Bottom 跨 range 移动。 + +必须验证: + +- ID → index 重新解析; +- topology/cache/prepared state 正确 invalidation; +- 新 Z-order 正确; +- 不使用旧 index; +- 不出现 stale cache。 + +--- + +# 11. Endpoint 删除与恢复 + +删除 start endpoint 时:explicit range 立即 invalid;不 crash;不猜测新 start;不继续使用 stale index/cache;进入既定安全 fallback;diagnostics 有明确 lastReason。 + +恢复 start endpoint 时:Native 能重新解析;explicit range 恢复 valid;正确重建需要的 cache;不需要重建整个 Map instance。 + +对 end endpoint 做同等验证。 + +--- + +# 12. Visibility 不改变 topology + +对 start endpoint、end endpoint 或 Continuous 内普通 Layer 做 visible → hidden → visible。 + +必须验证: + +- topology 仍以 Style 顺序解析; +- startIndex/endIndex 不因 visibility 改变; +- hidden endpoint 仍是 endpoint; +- 不自动收缩 Continuous Range; +- ON/OFF 只作为内容变化处理; +- 如需 cache invalidation,应是内容 invalidation,不得变成新的 topology 规则。 + +--- + +# 13. Invalid Explicit Range + +至少验证: + +1. startLayerId 不存在; +2. endLayerId 不存在; +3. startIndex > endIndex; +4. Style 尚未 ready 时配置; +5. 空 ID / 未配置(按实际 API 合法性处理)。 + +预期:不 crash;不猜测;不复用 stale cache;不按 Layer 类型自动修正;进入明确安全 fallback;diagnostics 显示 invalid 原因;条件恢复后能够重新进入 explicit range。 + +--- + +# 14. Particle Fast Path + +## 14.1 可命中场景 + +Continuous 内只保留当前 Fast Path 已支持的动态内容。 + +检查:Fast Path 可持续命中;Bottom/Top 的存在与否只影响实际存在的 retained cache;0 cache 的 Continuous-only 不得因为“没有 retained cache”而错误阻止 Fast Path。 + +## 14.2 Fail Closed 场景 + +在 Continuous 中加入当前 Fast Path 不支持的普通/未知 Continuous Layer。 + +必须验证: + +- Fast Path 退回 full CPU frame; +- 不 crash; +- 不错误跳过该 Layer; +- Continuous Range 本身保持 valid; +- Bottom / Top retained cache Z-order 仍正确; +- 不允许为了 Fast Path 强行把未知 Layer 移出 Continuous。 + +--- + +# 15. Top Alpha / Premultiplied Alpha 验收 + +必须重点看 Top retained cache 合成:transparent background、opaque fill、semi-transparent fill、line,以及可行时 symbol/text。 + +检查:透明区域正确露出 Particle;半透明区域正常叠加;opaque 区域完整覆盖;无黑色边缘;无 halo;无颜色被错误乘两次;无残影;OpenGL 与 Metal 设计保持一致。 + +--- + +# 16. 长时间运行与资源稳定性 + +在测试环境允许情况下进行持续运行测试,至少包括:Particle 连续播放、多次 Pan/Zoom/Rotate、多次 range 配置变化、endpoint 删除/恢复、visibility 切换、style mutation、再次稳定运行。 + +观察: + +- 无持续 GPU resource 增长迹象; +- retained texture/FBO 不因 stable frame 每帧新建; +- 无明显内存持续增长; +- 无黑屏; +- 无闪屏; +- 无 stale cache; +- 无偶发层序颠倒; +- 无 Particle 停止后恢复不了的问题。 + +如果当前工程缺少可靠 GPU memory 数字来源,不要求伪造数值;允许使用资源创建/释放 counter、diagnostics 和稳定运行现象作为当前阶段证据,并明确限制。 + +--- + +# 17. Diagnostics 要求 + +STEP-093 的测试入口必须能清楚观察至少以下信息: + +### Range + +- configured +- valid +- startLayerId +- endLayerId +- startIndex +- endIndex +- continuousBegin +- continuousEndExclusive +- fallback / lastReason + +### Bottom + +- present +- cache state +- build count +- reuse count +- invalidation count +- last invalidation reason + +### Top + +- present +- cache state +- build count +- reuse count +- invalidation count +- last invalidation reason + +### Fast Path + +- eligible / active +- fallback reason +- full CPU frame count 或等价诊断(若现有已有) +- unknown continuous layer 导致 fail-closed 的可观察原因 + +如当前已有字段足够,不得重复造一套 diagnostics。 + +--- + +# 18. Test UI / Playground 要求 + +优先复用现有 Particle Test App / validation UI。 + +如果需要新增 STEP-093 面板,要求: + +- 不影响生产 MapLibre API 语义; +- 不引入 NavSea 业务逻辑; +- 场景按钮命名明确; +- 能一键切换四种 topology; +- 能设置 single-layer range; +- 能触发 invalid range; +- 能隐藏/显示 endpoints; +- 能删除/恢复 endpoint; +- 能触发代表性的 reorder / cross-boundary move; +- 能显示当前 diagnostics; +- 能恢复默认测试场景。 + +测试 UI 本身不应成为复杂新框架。 + +--- + +# 19. Android 运行态验收 + +Android 作为本 STEP 首要完整运行态验收平台。 + +Codex 必须准备: + +- 明确的 build/install/run 手工命令; +- STEP-093 测试入口位置; +- 每项场景的操作步骤; +- 需要采集的日志/diagnostics; +- PASS/FAIL 判定。 + +实际 Android Gradle / APK / ADB / 真机或模拟器操作由 RDC / 用户执行。 + +不得由 Codex 仅凭代码静态分析代替 Android 实机/模拟器结论。 + +--- + +# 20. iOS / Metal 运行态验收 + +Android 完整通过后,再进行 iOS / Metal 对等测试。 + +至少需要验证:四 topology、Single-layer Continuous、Top 覆盖 Particle、Stable cache reuse、Camera invalidation、endpoint missing/recovery、visibility、Fast Path fail-closed、Top alpha。 + +如果本 STEP 暂未执行 iOS runtime: + +- 报告必须明确写 `WAITING_IOS_RUNTIME_VALIDATION`; +- Android PASS 不得自动推导为 iOS PASS; +- Metal native build PASS 也不能替代 Metal runtime rendering PASS。 + +--- + +# 21. 编译与测试原则 + +1. MapLibre Native 的 Android/iOS 开发与编译统一使用 Mac。 +2. 不安排 Linux Native 编译。 +3. noisy native build 遵循 Mac/Codex 全局构建输出节流规则。 +4. 不加入 Jest。 +5. 可运行已有 Native 精确测试;如代码变更影响 STEP-092 核心逻辑,应重跑相关现有 Native tests。 +6. 不为了“让测试绿”删除或弱化 STEP-092 已有覆盖。 + +--- + +# 22. 发现问题时的处理规则 + +### 22.1 小型、事实明确、局部问题 + +例如测试 UI 接线错误、diagnostics 漏展示、明确计数器错误、局部 invalidation reason 错误、编译错误、测试工具小缺陷,允许直接修复,并重新验收受影响项目。 + +### 22.2 架构级问题 + +如果发现需要修改 Continuous Range 数学定义、Style 顺序唯一 Z-order、0/1/2 retained cache 模型、renderer 大范围遍历结构、Fast Path 核心模型、第二套 layer metadata、新特殊 layer type、新 RenderGraph 或大规模 GPU cache redesign,必须停止当前修复扩张,先生成明确的“STEP-093 架构阻塞报告”,说明实际问题、复现方式、为什么 STEP-092 架构无法满足、建议方案和影响范围。 + +不得在运行态测试任务中擅自重构架构。 + +--- + +# 23. 验收矩阵 + +最终报告必须至少给出以下矩阵: + +| 类别 | 场景 | Android | iOS/Metal | 关键证据 | 结论 | +|---|---|---|---|---|---| +| Topology | Bottom+Continuous+Top | | | | | +| Topology | Bottom+Continuous | | | | | +| Topology | Continuous+Top | | | | | +| Topology | Continuous-only | | | | | +| Range | Single-layer Continuous | | | | | +| Z-order | Top covers Particle | | | | | +| Cache | Stable reuse | | | | | +| Invalidation | Pan/Zoom/Rotate/Pitch | | | | | +| Mutation | Continuous internal mutation | | | | | +| Mutation | Cross-range move | | | | | +| Endpoint | start missing/recovery | | | | | +| Endpoint | end missing/recovery | | | | | +| Visibility | hidden endpoint topology | | | | | +| Invalid | missing/reversed range | | | | | +| Fast Path | eligible | | | | | +| Fast Path | unknown layer fail-closed | | | | | +| Alpha | Top premultiplied composite | | | | | +| Stability | repeated operations / long run | | | | | + +状态只允许使用清楚枚举,例如:PASS、FAIL、NOT RUN、BLOCKED、WAITING_ANDROID_RUNTIME_VALIDATION、WAITING_IOS_RUNTIME_VALIDATION。 + +--- + +# 24. STEP-093 完成条件 + +只有满足下面条件,才允许把 STEP-093 标记为 `CLOSED / PASS`: + +1. Android 运行态核心矩阵实际执行并通过; +2. 四种 topology 实际验证; +3. Single-layer Continuous 实际验证; +4. Top 覆盖 Particle 实际验证; +5. 0/1/2 cache 与 diagnostics 一致; +6. Stable reuse 实际验证; +7. Camera invalidation → rebuild → reuse 实际验证; +8. endpoint missing/recovery 实际验证; +9. visibility 不改变 topology 实际验证; +10. invalid range fallback 实际验证; +11. Fast Path eligible + fail-closed 实际验证; +12. Top alpha 无明显错误; +13. 发现的阻塞性缺陷已解决并回归; +14. 文档与 Git 收尾完整; +15. `HEAD == origin/particle/main`; +16. 工作区 clean。 + +如果 Android 已全部通过但 iOS 尚未运行,可记录为:`ANDROID CLOSED / PASS + IOS WAITING_RUNTIME_VALIDATION`。不得把未运行的平台写成 PASS。 + +--- + +# 25. 必做输出 + +本任务所有需要 ChatGPT/RDC 后续审查的文件必须由 Codex 在任务中实际生成,不得在结束后临时补要。 + +## 25.1 STEP 记录 + +生成: + +`docs/50_Step记录/STEP-093_ContinuousRange运行态综合验收_v1.md` + +如果同名主题已有版本,递增为 v2/v3……,不得覆盖旧版。 + +内容至少包括:起始 Git 基线、调查到的实际测试入口、新增/修改内容、Android/iOS 实际执行状态、验收矩阵、问题与修复、未执行项、当前状态、下一步。 + +## 25.2 Codex 完成报告 + +生成: + +`docs/报告/STEP-093_ContinuousRange运行态综合验收_Codex完成报告_v1.md` + +同主题已有版本时递增版本号。 + +报告至少包含:任务目标、修改文件、测试入口、diagnostics、build/test 实际结果、Android runtime 实际结果、iOS runtime 实际结果、PASS/FAIL/WAITING 状态、Git commit、push、Final HEAD、origin/particle/main、git status、未执行事项、风险与已知限制。 + +## 25.3 开发记录 + +追加: + +`粒子开发记录.md` + +只追加,不覆盖历史内容。 + +--- + +# 26. Git 规则 + +严格遵守项目 Git 规则: + +- 只提交本任务相关文件; +- 禁止 `git add -A`; +- 禁止 `git add .`; +- 禁止 `git reset --hard`; +- 禁止 `git clean`; +- 不触碰无关 dirty/untracked; +- 功能代码与文档可按合理 scoped commit 拆分; +- push 到 `origin/particle/main`; +- push 后确认 `HEAD == origin/particle/main`; +- 最终确认 `git status --short`; +- push 失败不得标记任务完成。 + +--- + +# 27. Codex 执行终态规则 + +Codex 本轮结束时必须给出真实状态,而不是默认写 PASS。 + +建议状态: + +- `IMPLEMENTED / WAITING_RUNTIME_VALIDATION` +- `PARTIAL / WAITING_ANDROID_RUNTIME_VALIDATION` +- `ANDROID PASS / WAITING_IOS_RUNTIME_VALIDATION` +- `CLOSED / PASS` +- `BLOCKED` +- `FAILED` + +如果只是完成测试工具与代码准备,而 RDC/用户尚未真正运行 Android,则只能标记等待运行态验收。 + +--- + +# 28. 明确不做 + +STEP-093 不做: + +- 任意 N-range planner; +- 多 Continuous Range; +- frequency class 自动分类; +- 自动 Z-order; +- anchor/marker layer; +- 第二套 BOTTOM/CONTINUOUS/TOP metadata; +- NavSea AIS 专用优化; +- Alarm 专用优化; +- Weather provider; +- Route ETA; +- LandMask 业务系统; +- Wave/Tide 业务系统; +- PBF 通用重构; +- RenderGraph 大改; +- 新粒子算法; +- 粒子视觉美化; +- unrelated performance tuning; +- Jest。 + +--- + +# 29. 最终核心判定 + +STEP-093 最终不是检查“按钮能不能点”,而是验证 STEP-092 的核心架构在真实渲染中确实成立: + +> 普通地图内容可以被合理拆成 Bottom / Top retained cache,Particle 所在 Continuous Range 保持高频实时绘制;Style 顺序仍然决定最终 Z-order;不存在的 retained range 不产生资源;缓存只在需要时失效,并在稳定状态持续复用。 + +只要这个结论还没有通过真实运行态证据建立,STEP-093 就不能写成最终 `CLOSED / PASS`。 diff --git a/docs/40_任务/STEP-094_通用RenderSegment分段渲染与Style分类诊断_Codex任务_v1.md b/docs/40_任务/STEP-094_通用RenderSegment分段渲染与Style分类诊断_Codex任务_v1.md new file mode 100644 index 000000000..b8a737700 --- /dev/null +++ b/docs/40_任务/STEP-094_通用RenderSegment分段渲染与Style分类诊断_Codex任务_v1.md @@ -0,0 +1,599 @@ +# STEP-094 通用 Render Segment 分段渲染与 Style 分类诊断 — Codex 任务 v1 + +## 0. 任务目标 + +从已冻结的 Particle V12 `Bottom Retained / Continuous / Top Retained` 单 Continuous Range 模型,演进到最终通用模型: + +```text +Style Z-order + ↓ +连续 layerIndex + ↓ +若干连续 Render Segment + ↓ +每个 Segment 的执行策略仅为: +CONTINUOUS 或 RETAINED +``` + +Style 顺序继续是唯一 Z-order 事实来源。Renderer 不理解 LOW / MEDIUM / HIGH,也不理解 AIS、Route、地图、Weather 等业务语义。 + +本 STEP 同时必须建立新的 pull-based Segment Diagnostics,能够取回“当前 Style → Segment → Layer”的真实分类快照,供后续人工判断 Segment 划分是否合理。 + +## 1. 当前冻结基线 + +工作目录:`/Users/tei/dev/MapLibreParticleLab` +目标分支:`particle/main` + +当前已知 V12 冻结基线: +- HEAD:`7eaff6e12360e26591dceb5e011129eeab7ba4c7` +- Commit:`release: finalize unified particle.12` +- Tag:`maplibre-particle/rn/13.4.1-particle.12` +- `HEAD == origin/particle/main` +- 工作区 clean + +开始前必须重新执行 fetch/status/HEAD 检查,以实际仓库为准。禁止 reset/clean,禁止回退、移动、覆盖 V12 Tag。STEP-094 是 V12 之后的新开发,不修改 V12 已发布事实。 + +必须先读取: +- 根目录 `AGENTS.md` +- `ARCHITECTURE.md` +- STEP-092 / STEP-093 任务、Step 记录与完成报告 +- V12 当前 Continuous Range、Retained Cache、Fast Path、Android/iOS/RN bridge 实现 +- 当前 `RendererRuntime.ts`、`types.ts`、`NativeWeatherRuntimeBridge.ts` + +## 2. 最终架构原则 + +1. Style Layer 顺序是唯一 Z-order 真相。 +2. 整个 Style 必须按连续 layerIndex 被完整划分为 1..N 个 Render Segment。 +3. 每个 Segment 只声明 `RETAINED` 或 `CONTINUOUS`。 +4. 允许多个 RETAINED、多个 CONTINUOUS,且可按 Style 顺序任意交错。 +5. 相邻同策略 Segment 不得自动合并。 +6. 每个 RETAINED Segment 独立拥有 cache / revision / invalidation / rebuild / GPU resource。 +7. CONTINUOUS Segment 不创建 retained texture/FBO/compositor。 +8. Renderer 不自动分析 Layer 更新频率,不自动调整 Z-order。 +9. App 通过 Style 顺序 + Segment boundary 表达分类。 +10. 不建立 LOW/MEDIUM/HIGH 第二套业务分类。 +11. 不硬编码 NavSea Layer ID。 +12. 不增加空白 Anchor / Marker Layer。 + +V12 三明治只是本模型的一个特例:`RETAINED → CONTINUOUS → RETAINED`。 + +## 3. 命名约束 + +MapLibre 现有代码已经大量使用 shader/bucket 的 `Segment` / `SegmentVector`。本任务禁止新增裸名称 `Segment` 造成语义冲突。 + +新模型命名必须明确属于 Renderer execution topology,例如: +- `RenderSegmentStrategy` +- `RenderSegmentDefinition` +- `RenderSegmentPlan` +- `ResolvedRenderSegment` +- `RenderSegmentDiagnostics` +- `RetainedSegmentCache` + +具体文件名可按当前代码结构调整,但必须避免与 geometry/shader Segment 混淆。 + +## 4. Segment Plan 公共语义 + +推荐公共模型: + +```text +RenderSegmentDefinition + strategy: RETAINED | CONTINUOUS + startLayerId?: string +``` + +规则: +- definitions 至少 1 个。 +- 第 0 个 Segment 从 Style index 0 开始,`startLayerId` 必须省略/为空。 +- 第 1..N-1 个 Segment 必须提供非空 `startLayerId`。 +- startLayerId 必须引用当前 Style 中已经存在的普通 Layer。 +- Native 根据当前 Style 解析真实 layerIndex。 +- App/RN 不传数字 index。 + +例如: + +```text +[ + { strategy: RETAINED }, + { strategy: CONTINUOUS, startLayerId: "rn-weather-color" }, + { strategy: RETAINED, startLayerId: "land-area" }, + { strategy: RETAINED, startLayerId: "ais-layer" } +] +``` + +解析后: +- Segment 0 = `[0, index(rn-weather-color))` +- Segment 1 = `[index(rn-weather-color), index(land-area))` +- Segment 2 = `[index(land-area), index(ais-layer))` +- Segment 3 = `[index(ais-layer), layerCount)` + +Segment 2/3 虽然都是 RETAINED,也必须保持两个独立 cache,不得合并。 + +这种定义保证整个 Style 完整覆盖;插入普通 Layer 后依据 Style 位置自然进入对应 Segment;最顶端新增 Layer自动属于最后一个 Segment。 + +## 5. Plan 合法性与 resolve + +有效计划必须满足: +- Style ready; +- 所有 boundary Layer 存在; +- boundary index 严格递增; +- 无重复 boundary; +- 每个 resolved Segment 非空; +- 最终覆盖 `[0, layerCount)`; +- 不重叠、不留 gap。 + +hidden / visibility=none 的 boundary Layer 仍参与 topology。Visibility 变化不得改变 Segment membership。 + +以下情况 plan invalid: +- boundary missing; +- boundary 顺序反转或重复; +- Style 未 ready; +- 空 definitions; +- 非法 strategy; +- JSON/schema 非法。 + +invalid 时不得 crash、不得猜 boundary、不得复用 stale cache。必须释放/隔离失效 topology 的 retained resource,安全走 full renderer,并在 diagnostics 给出明确 reason。Style 后续恢复合法时允许自动重新 resolve。 + +必须有 monotonic `configRevision`。Style add/remove/reorder 后重新 resolve;实际 begin/end 变化时推进 `topologyRevision`。Visibility 变化不得推进 topologyRevision。 + +## 6. Renderer 执行模型 + +Resolved Segment 按 Style 顺序执行: + +```text +for segment in resolvedSegments: + RETAINED -> build/reuse cache -> composite + CONTINUOUS -> direct render +``` + +逻辑上按 Segment 顺序组合,但实际实现必须继续满足 MapLibre 原有 RenderPass 语义。 + +必须保持: +- 全 Style 原始 global layerIndex; +- 原始 ordinal; +- `parameters.currentLayer`; +- `depthRangeSize`; +- Opaque reverse traversal; +- Translucent forward traversal。 + +禁止每个 Segment 从 0 重新编号,禁止为了 Segment 模型重新排序 Style Layer。 + +## 7. RETAINED Segment 独立 Cache + +每个 RETAINED Segment 必须拥有独立: +- cache state; +- content revision; +- build/reuse/invalidation count; +- last invalidation reason; +- texture/compositor create count; +- resource release count。 + +GPU resource 必须 lazy 创建。CONTINUOUS Segment 绝不能因为占位而创建 retained texture。 + +Plan/topology 改变后,不能证明安全复用的旧 cache 必须释放并重建;stale segment cache 绝不能错误 composite。本 STEP 不要求复杂 cache migration,correctness-first。 + +## 8. Invalidation 粒度 + +目标是 Segment 级 invalidation。 + +能够明确定位影响 Segment 时,只 invalidate 受影响 RETAINED Segment;其他 RETAINED Segment 保持 valid/reuse。 + +LayerMutation 若明确属于某 Segment,应只使该 Segment dirty。Source/Tile mutation 若能从现有 RenderLayer/Source 关系安全确定影响的多个 Segment,应 invalidate 对应集合。 + +若无法可靠确定影响范围,允许保守 invalidate 全部 RETAINED Segment。本 STEP 不建立复杂通用 dependency graph。 + +camera/transform/viewport/resize/pixel ratio、style reload、image/sprite/glyph、placement/symbol fade/transition、context reset、debug options、unsupported 3D、resource/compositor failure 等无法证明局部安全时可 invalidate 全部 retained。 + +完成报告必须区分“已实现的 segment-local invalidation”和“仍采用全 retained invalidation 的原因”。 + +## 9. Cache ON/OFF + +现有 Cache ON/OFF 总开关继续有效。 + +Cache OFF: +- Segment Plan 仍然 resolve; +- Diagnostics 仍能看到 Style/Segment/Layer 分类; +- RETAINED Segment 不错误复用 retained GPU cache; +- Renderer 安全走非 retained 路径。 + +Cache ON: +- 每个 RETAINED Segment 独立 build/reuse; +- CONTINUOUS Segment 直接绘制。 + +关闭所有 CONTINUOUS Layer 的 visibility 不得删除 Segment topology。 + +## 10. V12 兼容入口 + +不得破坏现有: +- `setRetainedUnderlayEnabled()` +- `setContinuousLayerRange(startLayerId,endLayerId)` +- `getRetainedUnderlayDiagnostics()` + +新内部实现应尽量只有一套 Segment topology。 + +`setContinuousLayerRange(start,end)` 应作为 compatibility adapter,转换成等价 RenderSegmentPlan: +- start 前存在 → RETAINED +- start..end → CONTINUOUS +- end 后存在 → RETAINED + +自然退化成 V12 原有 1/2/3 Range。 + +旧 API 与新 Segment Plan 不得同时形成两套 Renderer topology。最后一次显式 topology 配置必须只有一个 active plan,并从 diagnostics 可见 `configSource`。 + +旧 `getRetainedUnderlayDiagnostics()` 保留兼容摘要;新的多 Segment 事实以新 Diagnostics API 为准。 + +## 11. 新 RN / Native API + +新增稳定公共语义: + +```text +RendererRuntime.setRenderSegmentPlan(reactTag, plan) +RendererRuntime.getRenderSegmentDiagnostics(reactTag) +``` + +建议 TS: + +```text +RenderSegmentStrategy = "RETAINED" | "CONTINUOUS" + +RenderSegmentDefinition = { + strategy: RenderSegmentStrategy + startLayerId?: string +} + +RenderSegmentPlan = { + schemaVersion: 1 + segments: readonly RenderSegmentDefinition[] +} +``` + +RN public API 必须是 typed object。TurboModule ABI 可使用 JSON string 作为 plan/snapshot 内部传输,避免可扩展嵌套数组变成脆弱 parallel arrays。若采用 JSON,必须有 schemaVersion 与严格校验;public TS 不暴露 raw JSON。 + +必须沿当前真实链路扩展: +`RendererRuntime → NativeWeatherRuntimeBridge/TurboModule → Android/iOS package bridge → MapLibre platform API → Core Renderer state`。 + +不得新建第二套独立 RN bridge。 + +## 12. Segment Diagnostics — 强制要求 + +这是 STEP-094 的强制验收项,不是附加日志。 + +新增 pull-based `getRenderSegmentDiagnostics()`,必须能够取回当前真实 Style 的: +1. Style 全 Layer 顺序; +2. 当前 resolved Segment 顺序; +3. 每个 Segment 的 strategy; +4. 每个 Segment 的 begin/end; +5. 每个 Segment 内实际包含的 Layer; +6. 每个 RETAINED Segment 的 cache 状态与统计。 + +Diagnostics 只能按需读取。禁止 per-frame 打印完整 Style/Segment/Layer,禁止每帧构造大 JSON/大字符串。 + +## 13. Diagnostics Snapshot 最低字段 + +顶层至少: + +```text +schemaVersion +enabled +configured +valid +configSource +configRevision +topologyRevision +styleLayerCount +segmentCount +retainedSegmentCount +continuousSegmentCount +coverageComplete +fallback +lastReason +fastPathEligible +fastPathActive +fastPathFallbackReason +fastPathHit +fastPathMiss +fullCpuFrame +``` + +`configSource` 至少区分: +- `explicit-render-segment-plan` +- `continuous-range-compat` +- `unconfigured/legacy`(若仍存在) + +必须提供完整 ordered `styleLayers[]`,用于 plan invalid 时仍可人工分析 Style。 + +## 14. Style Layer Diagnostics + +每个 `styleLayers[]` / Segment 内 `layers[]` 至少返回: + +```text +layerIndex +layerId +layerType +visible +``` + +如当前 Style/Core 可低成本稳定取得,还应返回: +- `sourceId` +- `sourceLayer` + +不适用时可为空/null,禁止伪造。Layer Diagnostics 只描述通用 Style 信息,禁止加入 AIS/Route/Wind 等业务判断。 + +## 15. 每个 Segment Diagnostics + +每个 Segment 至少: + +```text +segmentIndex +strategy +configuredStartLayerId +beginIndex +endExclusive +resolvedStartLayerId +resolvedEndLayerId +layerCount +layers[] +``` + +如果 `strategy=RETAINED`,还必须有: + +```text +cacheState +contentRevision +build +reuse +invalidation +lastInvalidationReason +textureCreate +compositeCreate +resourceRelease +``` + +如果 `strategy=CONTINUOUS`,不得伪装成 disabled retained cache。retained-only 字段应为 null/not-applicable,或由 TS discriminated union 排除。 + +## 16. Diagnostics 一致性 + +当 plan valid: +- `styleLayers.length == styleLayerCount` +- Segment 按 segmentIndex/beginIndex 严格递增 +- 所有 `segment.layers[]` 拼接后必须与 `styleLayers[]` 完全一致 +- 每个 Style Layer 必须且只能属于一个 Segment +- `segment.layerCount == segment.layers.length` +- `coverageComplete=true` + +当 plan invalid: +- 仍尽可能返回当前 ordered styleLayers +- segments 可为空或只返回已安全解析部分,但必须 `valid=false` +- 禁止把猜测结果标成 valid + +## 17. DEBUG 使用目标 + +必须能取回类似: + +```text +Segment 0 RETAINED [0..18) + background + water + coastline + +Segment 1 CONTINUOUS [18..20) + rn-weather-color + rn-weather-particles + +Segment 2 RETAINED [20..27) + land-area + navigation-marks + route + +Segment 3 RETAINED [27..30) + ais + radar + alarm +``` + +此信息只用于人工分析 Style 分类是否合理。Renderer 不根据 Debug 结果自动重新分类。 + +设计文档必须给出至少一个完整 JSON/TS snapshot 示例。 + +## 18. Particle Fast Path + +V12 已有 Particle Fast Path 不得无声退化。 + +至少保证: +- 对“恰好一个 CONTINUOUS Segment + 其余为 RETAINED”的 V12 等价拓扑,且 Continuous 内容满足现有支持条件时,现有 Fast Path 能继续工作。 +- 不存在某个 retained cache 时不得成为 miss 原因。 + +对于多个 CONTINUOUS Segment,若当前 Fast Path 无法安全支持,允许 fail closed: +- `fastPathActive=false` +- 明确 fallback reason +- 正常 full CPU frame +- Segment topology / retained cache / Z-order 仍正确 + +禁止为了 STEP-094 一次性重写整个 RenderGraph 或实现所有 N-Continuous Fast Path 优化。 + +## 19. Style Mutation + +必须验证: +- Segment 内新增/删除/reorder Layer +- boundary 前后插入 Layer +- boundary Layer hidden +- boundary Layer 删除/重新出现 +- boundary reorder 导致 plan invalid +- plan 更新后恢复 + +普通 Layer 插入 Segment 内后应自然成为该 Segment 成员,不要求 App 给每个 Layer 单独注册频率 metadata。 + +相邻 RETAINED Segment 即使策略相同,也必须依靠显式 boundary 保持独立。 + +## 20. GPU 生命周期 + +实际 retained GPU resource 数量应与当前 resolved plan 中 RETAINED Segment 数量一致,允许 lazy 未创建状态,但不得为 CONTINUOUS Segment 分配 retained target。 + +Plan topology 变化后 stale resource 必须正确 release。稳定帧不得重复创建 retained texture/FBO/compositor。OpenGL 与 Metal 行为必须一致。 + +## 21. Native Directed Tests(不加入 Jest) + +本任务明确不加入 Jest。 + +至少覆盖: +1. 单 RETAINED Segment 覆盖全 Style +2. 单 CONTINUOUS Segment 覆盖全 Style +3. V12 等价 R-C-R +4. C-R-R +5. R-R-C +6. R-C-R-C-R 多段交错 +7. 相邻 R-R 不自动 merge +8. hidden boundary 不改变 topology +9. boundary missing / duplicate / 顺序非法 +10. Style 内插入 Layer 后 membership 正确 +11. boundary 删除后 fail safe、恢复后重新 resolve +12. retained resource 数量匹配 RETAINED Segment 数 +13. CONTINUOUS 无 retained resource +14. Cache OFF 时 topology diagnostics 仍完整 +15. V12 `setContinuousLayerRange` 兼容映射 +16. global layerIndex / Opaque / Translucent 语义不变 +17. 多 CONTINUOUS Fast Path 安全 fallback + +Diagnostics tests 额外验证: +- styleLayers 顺序精确 +- segments 顺序精确 +- 每个 Segment 的 layers 成员精确 +- 拼接 segment.layers == styleLayers +- layerIndex 不重编号 +- layerId/layerType/visible 正确 +- sourceId/sourceLayer(若实现)正确 +- invalid plan 仍能取得 ordered styleLayers +- snapshot pull 不产生 per-frame 日志 + +## 22. 平台与 RN 验证 + +Core/C++: +- 执行规定的 Mac 原生精确编译与 Native tests +- OpenGL / Metal +- 编译输出遵循全局 build-output-throttle + +Android: +- 完成源码、JNI/Kotlin/Java/RN bridge 修改 +- Android Gradle 编译由用户手工执行,不由 Codex 运行 +- 报告列明“未执行 Android 编译/实体机”直到用户验证 + +iOS: +- 只面向 iPhone/iPad arm64 真机架构 +- 不要求 Simulator slice +- 可执行当前工程已有的 Mac/iOS arm64 精确编译验证 +- 不伪造实体机运行 PASS + +## 23. 最小 Debug/Validation 入口 + +在当前已有 Particle validation/Test Host 体系中补一个最小 pull 入口: +- Fetch Segment Diagnostics +- 展示或输出一次完整 snapshot +- 能看到 Segment 列表和每个 Segment 的 Layer 列表 + +不得增加持续轮询,不得把大 snapshot 每帧写日志。若现有 UI 已有 diagnostics panel,优先复用。 + +## 24. 性能要求 + +Segment topology/style diagnostic 数据只在 config/style topology 或相关 debug metadata 变化时更新,或在显式 diagnostics pull 时做必要轻量组装。 + +禁止因为 Particle 30/60fps: +- 每帧扫描/复制完整 Style Layer 字符串 +- 每帧 JSON stringify +- 每帧创建 Segment diagnostics 大对象 +- 每帧 Logger 输出 topology + +高频 render path 只消费预解析的轻量 segment ranges/cache state。 + +## 25. 明确不做 + +本 STEP 不做: +- LOW/MEDIUM/HIGH 枚举或自动频率分类 +- Renderer 识别 NavSea 业务 Layer +- 自动调整 Style Z-order +- Anchor/Marker Layer +- 复杂通用业务 dependency graph +- Weather provider / Route ETA / AIS 专项 renderer +- PBF 重生成 +- 通用 RenderGraph 重写 +- 自动合并同策略 Segment +- 多 Continuous 的高级 Fast Path 优化 +- per-frame Segment debug log +- Jest + +如发现必须突破这些边界才能保证 correctness,停止扩大范围并在报告明确 BLOCKED/PARTIAL。 + +## 26. 必做文档 + +新增: +- `docs/30_设计/STEP-094_通用RenderSegment模型与Style分类Diagnostics设计_v1.md` +- `docs/50_Step记录/STEP-094_通用RenderSegment分段渲染与Style分类诊断_v1.md` +- `docs/报告/STEP-094_通用RenderSegment分段渲染与Style分类诊断_Codex完成报告_v1.md` + +追加: +- `粒子开发记录.md` + +同主题已有文件则递增版本,不覆盖历史。所有文档中文。 + +设计文档必须包含最终 Segment 数据模型、config/resolve 生命周期、V12 compatibility mapping、cache ownership、invalidation、Fast Path 边界、Android/iOS/RN API、Diagnostics schema、完整 Style→Segment→Layer snapshot 示例。 + +## 27. 完成报告必须记录 + +- 起始 HEAD / 最终 HEAD +- 修改文件清单 +- RenderSegment config API +- boundary Layer ID → layerIndex 解析 +- N Segment 遍历 +- retained cache ownership/resource lifecycle +- segment-local 与 global invalidation +- V12 compatibility / Fast Path +- Diagnostics schema +- styleLayers / segment.layers 验证 +- RN/Android/iOS bridge +- OpenGL / Metal +- Native test 数量与结果 +- 未执行 Android/实体机项目 +- known limitations +- commit SHA/message +- push result / remote branch +- origin/particle/main Final HEAD +- 最终 `git status --short` + +## 28. Git 规则 + +仅 scoped staging。禁止 `git add -A`、`git add .`、`git reset --hard`、`git clean`,不得删除/覆盖无关 dirty/untracked。 + +结束前必须: +1. `git diff --check` +2. 核对 diff +3. 只 stage 本任务文件 +4. scoped commit +5. push `origin particle/main` +6. fetch/确认远端 + +最终要求: +```text +HEAD == origin/particle/main +git status --short 无本任务残留 +``` + +push 失败不得声称完成。 + +## 29. PASS Gate + +只有以下全部成立才可判 STEP-094 PASS: +- Style Z-order 仍唯一 +- 1..N 连续 Segment 完整覆盖 +- RETAINED/CONTINUOUS 可任意交错 +- 相邻同策略 Segment 不自动合并 +- 每个 RETAINED Segment 独立 cache/revision/invalidation/resource +- V12 Continuous Range 兼容 +- V12 等价 Fast Path 不无声退化 +- 新 Diagnostics 能真实返回 Style→Segment→Layer +- diagnostics pull-based、无 per-frame 大开销 +- Android/iOS/RN API 完整 +- Native directed tests 通过 +- 规定编译验证完成 +- 文档完成 +- scoped commit/push 完成 +- HEAD 与远端一致 + +关键项未完成必须明确 PARTIAL/BLOCKED,禁止伪造 PASS。 diff --git a/docs/40_任务/STEP-095_V13独立严格代码与性能审查_Codex任务_v1.md b/docs/40_任务/STEP-095_V13独立严格代码与性能审查_Codex任务_v1.md new file mode 100644 index 000000000..80456c75a --- /dev/null +++ b/docs/40_任务/STEP-095_V13独立严格代码与性能审查_Codex任务_v1.md @@ -0,0 +1,109 @@ +# STEP-095 V13 独立严格代码与性能审查 — Codex 任务 v1 + +## 0. 工作性质与模型 +- 本任务是独立、严格、全链路、以源码为最终证据的只读代码/性能调查,不是修复任务,也不是批准已有报告。 +- 执行环境:Mac-mini-tei.lan,仓库 /Users/tei/dev/MapLibreParticleLab,分支 particle/main。 +- 运行模型:gpt-6-astra,reasoning effort=high;由外部启动命令实际指定,报告记录实际模型。 +- 最终交付物必须为中文,具体源码符号、文件路径、命令、证据可保留英文。 +- 不以“编译通过”“定向单元测试通过”“已有 Codex 报告 PASS”推断 GPU 运行态、安全性、性能或跨平台一致性已经通过。 + +## 1. 源码冻结基线 +- 被审查的 V13 代码提交:ee0b89e711bf01335b3aa71d36def785cd97fec3。 +- V13 核心功能提交:016fd1fff6a33cf795c10d4f5c5413d308383677。 +- V13 发布标识:RN 13.4.1-particle.13;iOS 6.28.0-particle.13;以实际 manifest/tag/API 核实,不假设已在 NavSea 接入。 +- 前置独立审查归档提交:45c4dca84928a57a2cf0226737c34cabd7fbfbfb。 +- 审查前读取 AGENTS.md、PARTICLE_BASELINE.md、docs/00_项目规范/、docs/00_项目总则/README.md 与其中标记的当前有效总则。 +- 同步检查 git branch、HEAD、origin/particle/main、working tree、release manifest 和相关 tag;记录所有偏差。 +- 本任务自身新增文档提交不改变上述被审查的源码 commit;核查源码时固定以该提交为事实基线。 + +## 2. 审查独立性:先源码,后已有报告 +- 首先依据 Git V12→V13 改动、当前调用链及实际代码独立扫描,不从旧报告结论直接生成 findings。 +- 然后阅读 docs/20_调查/Particle_V13_严格代码审查报告_v1.md,逐项复核 F01–F10:确认、修正、否定或标记需动态验证。 +- 不能复制上一份报告来冒充独立 review。需要发现旧报告遗漏、误判、证据不足及其他模块的潜在风险。 +- 交叉阅读 STEP-088~094 的任务、设计、Step 记录、Codex 报告及相关发布/验证事实。 +- 当任务书与代码冲突,分别说明“设计意图”和“实际实现”;不能把历史数据冒充 V13 测量。 +- 可以读取 Integration repo 的 contract/status/compatibility,但不得修改该仓库;发现跨平台契约问题只提升级建议。 + +## 3. 强制审查范围 A:Renderer 与 V13 Segment +- 通用 RenderSegment plan、JSON parser/resolver、配置切换、missing/reorder/hidden boundary、valid/invalid/recovery。 +- Style 全局 Z-order、layerIndex、LayerGroup ordinal、opaque reverse、translucent forward、depth/stencil/blending、debug/legacy/3D 与 offscreen composite 的一致性。 +- 任意 R/C 交错:全 R、全 C、R-C-R、C-R-R、R-R-C、R-C-R-C-R、相邻 R-R;缓存 OFF 和资源失败的 full fallback。 +- Retained cache 独立生命周期、build/reuse/invalidate/release、topology generation、content fingerprint、diagnostics counters 的归属。 +- V12 连续区间兼容映射、单 Continuous Fast Path、多个 Continuous 安全 fallback,注意 silent fallback 与正常渲染的真实成本。 +- 特别核查 Segment pass 分割后的 alpha/depth/stencil 语义、source/layer mutation 与下一帧 stale GPU object 风险。 +## 4. 强制审查范围 B:GPU、粒子和时间场 +- OpenGL 与 Metal OffscreenTexture、Framebuffer、Texture、render pass、commit/present/wait、resource destroy/context loss;区分通用 Core 与平台实现。 +- 是否存在 CPU/GPU 同步等待、重复 GPU allocation、每帧 upload/UBO/drawable 重建、纹理引用失效和资源峰值。 +- Particle State A/B、Trail、Field T0/T1/MIX、spatial sampling、forecast/playback、zoom-out reseed、viewport bounds 和 resize/orientation。 +- Wind/Wave/Current/Rain 等 canonical FieldProfile;V3/V4 encoded/decoded field、QBIN decoder、projection/materialization、binding/validity/missing data。 +- 检查异常或损坏输入、极大网格/时间、浮点 NaN/Inf、整数溢出、文件/缓冲区生命周期、重复/乱序/过期 field、安全失败和内存占用。 +- 需要从 Source→Runtime→RenderLayer→Renderer→GPU 按真实调用链追踪,不接受只搜索名称的浅层判断。 + +## 5. 强制审查范围 C:高频、线程和 Host API +- 每帧 CPU 分配/深复制/锁竞争/字符串或 JSON 构造、LayerGroup 遍历复杂度、静态 CPU Fast Path 命中与错过原因。 +- style/source/tile/image/glyph/placement 失效范围是否正确,segment-local 与全局 revision 的边界是否一致。 +- map-scoped lifetime、prepared RenderTree、Runtime/renderer/control state 的并发、线程归属、异步回调、销毁后引用和重复 attach/detach。 +- Android Java/Kotlin/JNI 与 iOS ObjC++/Metal 的桥接接口、错误归一化、RN TurboModule typed API、ABI/codegen 与版本锁。 +- Test Host 的依赖及 lockfile、构建结果的可复现性、release manifest/npm/AAR/XCFramework 的版本一致性。 +- 不将业务 provider、HTTP、Route/ETA 带入 Shared Core;不建议建立新的隐藏私有桥接或第二套协议。 + +## 6. 强制审查问题清单 +- 是否存在 V13 上线后能够导致画面层级错误、透明度/遮挡错误、丢帧、黑屏、闪烁、过期 cache composite、GPU/CPU 泄漏、崩溃或错误数据的实际源码路径? +- 非法配置是否真正 fail closed?同一 index 换不同 Segment 时旧 cache/diagnostics 归属是否错误? +- Retained contentDirty 是否被误当作 GPU allocation invalid?Metal 逐 cache wait 是否可证实?需要哪些 profiler 证据才能决定修改? +- 高频 Fast Path 是否复制 full diagnostics、扫描 Style 或反复线性搜索?有无更重的热点在既有报告中遗漏? +- 多段 retained 是否存在全 viewport depth/stencil 过度分配;是否有峰值预算、安全回退及 resource accounting? +- source/tile 局部更新是否引起不必要全 retained rebuild?若建议局部化,必须给出不破坏 correctness 的依赖证据。 +- 单 Continuous 与多 Continuous 的 Frame 处理在相同真实 Style 下有什么可测的差异?不能外推 STEP-090 历史 A/B 数字。 +- release、Test Host、RN/iOS/Android 版本与实际产物是否能被独立复现?尚未测试的必须标 NOT RUN。 + +## 7. 审查方法 +1. 建立 V12→V13 变更与模块地图,阅读入口、类型定义、状态机、调用方和析构路径。 +2. 针对每个怀疑的问题制作最短触发条件和源码调用链;尝试找反例,禁止只因为名称相似就报告漏洞。 +3. 按已证实缺陷、源码确定的成本、真实运行影响待验证、显式设计限制、文档/环境偏差分类。 +4. 为每条 finding 写严重度 P0/P1/P2/P3、置信度、源文件精确行范围、触发场景、影响、最小安全修复方向和验收方法。 +5. 独立比较上一份 F01–F10,标为 CONFIRMED / QUALIFIED / DISPROVED / NEEDS_RUNTIME,说明依据和新增发现。 +6. 对潜在收益只给复杂度/估算表达和建议基准测试,不提供无 profiler 支持的 FPS/CPU/GPU 节省百分比。 +7. 所有未亲自执行的测试必须标 HISTORY / NOT RUN;已有报告的 PASS 只能列为历史证据,不替代当前独立执行。 + +## 8. 运行与修改边界 +- 禁止修改任何生产源码、shader、公共 API、构建配置、package/lockfile、release manifest、tag、发布产物或 Integration 文件。 +- 禁止运行耗时 CMake/Xcode/Gradle 编译、安装、设备操作和大型测试;由 RDC/用户另行验证。 +- 允许只读 git show/log/diff、文件检索、源码静态分析和查看既有日志、测试源码、报告。 +- 如判断必须借助 profiler、真机或编译才可定论,写精确验证方案而不是擅自实施。 +- 仅允许新增本 STEP 中文调查/Step/完成报告,按规定追加根目录粒子开发记录;不要顺手修复发现的问题。 +## 9. 完成报告格式(必须实际落盘) +- 执行摘要:哪些是确定缺陷、哪些是性能成本、哪些只能动态验证;不能只给结论口号。 +- 基线与范围:审查 commit、分支、release manifest、审查的主要文件/代码路径、未覆盖范围。 +- Finding 总表:编号、严重度、证据类别、问题、触发、影响、代码定位、修复方向、验证方法。 +- 逐条完整分析:至少包含最短可复核调用链及确认或推翻依据;新问题与旧报告重复项区分。 +- V13 与 V12 架构/性能成本比较:不确定的写待测;不要虚构性能数值。 +- F01~F10 独立复核矩阵:原结论、当前意见、依据、是否需补充/修正。 +- 缺失测试与运行态矩阵:GPU correctness、CPU/GPU frame time、allocation/peak memory、30/60 FPS、长时间稳定性、各平台状态。 +- 优先级清单:分 correctness gate、无需设备的安全优化、GPU 资源/同步、较大架构改造。 +- 具体建议的下一轮 Codex 修复批次(只建议,不创建修复任务,不改源码)。 +- Git:起始/最终 HEAD、docs-only diff 清单、commit SHA/message、push 结果、remote HEAD、最终 status。 +- 结论状态:调查 PASS/PARTIAL/BLOCKED 与 V13 产品运行态是否通过必须分开。 + +## 10. 必做输出和固定路径 +- 调查主文档:docs/20_调查/STEP-095_V13独立严格代码与性能调查_v1.md。 +- 阶段记录:docs/50_Step记录/STEP-095_V13独立严格代码与性能审查_v1.md。 +- Codex 完成报告:docs/报告/STEP-095_V13独立严格代码与性能审查_Codex完成报告_v1.md。 +- 追加:根目录 粒子开发记录.md,短而具体地记录本次调查变化、现状及下一步,不覆盖历史。 +- 若同主题相同文件已经存在,改用递增版本号并在报告写明,不覆盖旧报告。 +- 以上文件全部中文;任何报告必须明确区分事实、推断和待验证。 + +## 11. Git 收尾 +- 先确保用户现有 dirty/untracked 不被覆盖;只有本任务新增/追加文档允许 staged。 +- 不执行 git reset --hard、git clean、force push、tag 移动或向 upstream push。 +- 对所有新增文档进行路径、目录、内容、交叉引用检查;git diff --check。 +- 仅 git add 上述具体文档和本次追加的开发记录;scoped commit 后 push origin particle/main。 +- push 失败或 HEAD 与远端不一致时,不得写 PASS;明确失败与恢复建议。 +- 记录源代码相对于原 V13 源码提交没有改变,禁止把 docs-only commit 误写成新 V13 软件发布。 +- 最终由 RDC 独立检查报告证据、Git 与任务边界,Codex 不自行宣布正式产品验收。 + +## 12. 停止条件 +- 无法确认当前 commit、源代码状态与所审查发布版本对应时,停止并报告 BLOCKED。 +- 遇到跨项目协议变更/需要修改业务源码时,只形成接口阻塞或修复建议,不扩大任务。 +- 证据不足时明确 NEEDS_RUNTIME,不应为了凑数量虚构问题。 +- 重大 P0 或明确破坏性问题优先直接写进报告,但本任务仍只读,不自动修复。 diff --git a/docs/40_任务/STEP-096_V13隔离盲审_Codex任务_v1.md b/docs/40_任务/STEP-096_V13隔离盲审_Codex任务_v1.md new file mode 100644 index 000000000..71976ddfa --- /dev/null +++ b/docs/40_任务/STEP-096_V13隔离盲审_Codex任务_v1.md @@ -0,0 +1,59 @@ +# STEP-096 V13 隔离盲审 — Codex 任务 v1 + +## 0. 工作性质与隔离目标 + +这是针对已冻结 V13 源码的全新、只读、源码优先的代码与性能审查。发现必须来自当前隔离目录内的源代码和规范,不允许从任何既有审查结论、聊天记录或外部仓库导入问题清单。不得为凑数量制造 finding。 + +- 执行模型:全新 Codex TUI 会话,`gpt-6-astra`,reasoning effort=`high`;RDC 负责实际启动与记录,Codex 报告仅记录能核实的启动事实。 +- 唯一工作目录:`/Users/tei/dev/MapLibreParticleV13Blind096`。 +- 目标源码版本:`ee0b89e711bf01335b3aa71d36def785cd97fec3`;由 RDC 从该 Git 对象导出**无 .git 元数据**的源码快照。当前隔离目录不是主仓库、不是 Git worktree;不得在此运行 git,也不得用其他路径反查仓库历史。 +- 唯一任务说明:本文件。用户和外部 RDC 将在审查开始前检查隔离树不含已有调查、阶段记录、完成报告、任务历史或开发日志。 +- 禁止访问隔离目录之外的所有源码、文档、Git metadata、会话日志、终端回放、网页、连接器、用户历史或其他 AI 输出;不要尝试恢复缺失文档。对任何未在快照内的证据写 NOT AVAILABLE。 +- 不得读取、搜索或比较其他轮次的审查材料;**本任务只有盲审,不包含解除盲态后的交叉比较。** 完成后的文件由 RDC 先冻结散列,再独立移交正式仓库。 + +## 1. 只读入口与事实层级 + +先读取隔离目录的 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目规范/`、`docs/00_项目总则/README.md`、其中列出的当前有效总则和本任务。隔离快照故意不提供历史 Step、调查/完成报告;读取要求在盲审范围内以当前有效规范和实际源码为准,不得向目录外补读。规范包含历史发布坐标时,以本快照的 release manifest、源码和目标 SHA 为实现事实,区别说明规范意图与实际代码。 + +允许读取:隔离树内的 C++/GL/Metal/Android/iOS/RN 代码、头文件、shader、构建配置、测试源码、release manifest,以及当前有效规范/架构设计。历史测试输出和运行日志不可用;阅读测试源码不等于执行测试。不得浏览 `docs/20_调查`、`docs/50_Step记录`、`docs/报告`、既有历史任务或开发日志(这些输入在隔离树内也应不存在)。不引用主仓库的当前 HEAD 作为 V13 源码 SHA。 + +## 2. 审查目标与探索方法 + +从目录和公开 API 识别实际架构,自行建立模块和调用链地图;不要按照预设缺陷名称凑检索结果。对每个候选问题至少追踪入口、状态所有权、调用方、正常路径、错误路径、销毁路径,并寻找反例。分别审查: + +1. Style 顺序和分段解析/更新/绘制语义;任意连续渲染策略交错、透明/深度/裁剪及缓存一致性。 +2. Renderer→gfx→OpenGL/Metal 的目标、命令、资源和生命周期;稳定/变化帧、失败恢复、尺寸变化及上下文切换。 +3. GPU 粒子、颜色场、时空插值与场数据链;输入合法性、数值边界、时间状态和显示状态。 +4. Source/Runtime/Field/QBIN、数据快照、线程与内存所有权;重复、乱序、异常或极大输入与 fail-closed。 +5. Android/iOS/RN 的公开 API、错误传播、生命周期和版本/产物引用;区分源码可证事实与安装环境需要的验证。 +6. 高频执行成本:CPU、锁、分配、遍历、复制、GPU 同步/资源、UI 线程以及资源上界。查找实际热点路径,但没有 profiler 不得断言运行时热点排序或节省百分比。 +7. 兼容与工程边界:既有行为、上游薄接入、Style Z-order 唯一事实来源;业务下载/HTTP/provider 属于 Host,不进入 Native。 + +如同一问题已有安全 fallback、缓存稳定复用或数据校验,必须写出反例和限制,而不是仅报告表面上可疑的符号。源代码路径和行号必须在隔离快照内真实存在。 + +## 3. 每条 finding 的证据契约 + +- 按发现顺序自行编号 `B01`、`B02`……;不要采用其他报告的任何编号。 +- 列:P0/P1/P2/P3、类型(确定缺陷/源码确定成本/设计限制/环境或文档偏差/需运行验证)、置信度、触发条件、最短源码调用链与精确路径/行号、反例、影响边界、安全修复方向、最小验证方案。 +- 区分“代码中的风险路径已经证实”和“用户画面、GPU、CPU、OOM、崩溃、性能收益已经实测”;后者本轮一概不可假定。 +- 如果推断尚缺关键调用链,降为 HYPOTHESIS/NEEDS_RUNTIME,不以 P0/P1 明确事故冒充。 +- 不要求 finding 数量、也不要求与任何报告相同;实际不存在证据就明确无此类 finding。 +- 不复制包含商业/个人机密的数据;路径与定位信息足够即可。 + +## 4. 验证边界与停止条件 + +本任务只允许静态读取、轻量文件搜索、源码/语法/引用自检。禁止生产代码、测试代码、shader、manifest、package/lock、工具链改动;禁止 CMake/Xcode/Gradle、Codegen、Jest、设备、安装、性能测试、发布、Git 写入。历史构建/测试结果没有暴露,不得编造。所有未运行门槛标 `NOT RUN`,运行效果标 `NEEDS_RUNTIME`。如必须依赖平台设备、profiler 或竞态注入,写可执行验证矩阵,停止在证据边界。 + +只能写以下三份中文文件,写完确认三份存在、正文和相对链接有效、所引用路径/行号有效、无错误引用和行尾空格后退出: + +1. `docs/20_调查/STEP-096_V13隔离盲审调查_v1.md`:架构地图、独立 finding 总表与证据、反例、风险、性能模型、未覆盖范围、运行验证矩阵、分批建议。开头明确声明仅使用隔离输入。 +2. `docs/50_Step记录/STEP-096_V13隔离盲审_v1.md`:状态、输入版本、检查范围、只读工作过程、结果、三份文件清单、未运行验证、阻塞与下一步。 +3. `docs/报告/STEP-096_V13隔离盲审_Codex完成报告_v1.md`:执行结果 PASS/PARTIAL/BLOCKED(仅表示盲审文档交付)、finding 索引、证据质量、输入限制、构建测试 NOT RUN、源码未变、交付/移交方案。 + +本隔离树无 Git metadata,**不要在报告内伪造 Git commit/push/remote/clean 状态**。正式仓库移交、`粒子开发记录.md` 追加、四个文件的 scoped commit/push、最终 SHA 和远端核对均由 RDC 在盲审文件冻结之后执行。Codex 不得自行打开正式仓库进行移交,避免在写稿前接触比较材料。 + +## 5. 盲审冻结与移交验收(由 RDC 执行,不是 Codex 自行执行) + +Codex 停止后,RDC 验证隔离树的文件清单,计算三份原始报告 SHA-256 并记录时间;冻结后不得由同一会话基于其他资料重写 finding。随后独立把三份报告完整复制到正式 `particle/main` 同路径,追加根目录 `粒子开发记录.md`,执行引用、边界、git diff --check、scoped commit、push `origin particle/main`、远端 HEAD/clean 核查。禁止修改已冻结正文,若必须校正移交元数据,应写入独立移交记录,不得改写发现表。 + +文档交付 PASS 不等于 V13 Android/iOS GPU 运行态或性能 PASS。若 Codex 无法访问输入、丢失上下文或额度阻断,停止、保留已成品并标 PARTIAL/BLOCKED;不得使用其他会话已有审查结果补齐。 diff --git a/docs/40_任务/STEP-097_V13三轮审查问题合并修复_Codex任务_v1.md b/docs/40_任务/STEP-097_V13三轮审查问题合并修复_Codex任务_v1.md new file mode 100644 index 000000000..bbdff1878 --- /dev/null +++ b/docs/40_任务/STEP-097_V13三轮审查问题合并修复_Codex任务_v1.md @@ -0,0 +1,153 @@ +# STEP-097 V13 三轮审查问题合并修复 — Codex 任务 v1 + +状态:**计划 / 待执行**。本文件只定义后续代码修复任务;创建本文件不代表任何缺陷已经修复、编译通过或 V13 运行验收通过。 + +## 0. 目标、基线及本轮决策 + +目标:对 V13 三轮源码调查的问题去重、复核、按安全依赖顺序修改;保留全部原始编号和证据归属,不以“多份报告都说有问题”取代源码验证。先消除渲染与天气数据的正确性风险,再处理输入安全、Host 生命周期和可证明的热路径成本。保留通用连续 Render Segment、Style Z-order 唯一顺序、每个 RETAINED 独立 cache;不得退回旧 Bottom/Continuous/Top 固定三明治模型。 + +本次用户决定:**暂不执行 V13 正式运行态确认**。本任务结束的最高状态只能是 \`IMPLEMENTED / WAITING_RDC_BUILD_AND_RUNTIME_VALIDATION\`;不能写 Android/iOS GPU、FPS、长期稳定性、正式发布或 NavSea 接入 PASS。本任务不要求 Codex 编译、安装、操控设备或执行性能测试。运行态集中验证清单由 Codex准备,之后单独授权执行,避免小变动引发多次 Android 编译/安装。 + +实际源码基线以执行时 \`particle/main\`、\`origin/particle/main\` 和 clean worktree 为准;编写任务书时 HEAD 为 \`b5c1c4879a9d36608242e788afab5012bd9388f2\`,V13 软件冻结源码为 \`ee0b89e711bf01335b3aa71d36def785cd97fec3\`,功能提交 \`016fd1fff6a33cf795c10d4f5c5413d308383677\`。当前 \`.13\` 软件已发布,所有修复在后续开发提交中完成,**不得原地覆盖、移动或重新发布 \`.13\` 的 tag/manifest/产物**。若起始 HEAD 已前进,先核实源代码差异并报告,不用旧行号机械覆盖。 + +审查输入,必须逐份读取: +1. \`docs/20_调查/Particle_V13_严格代码审查报告_v1.md\`(F01–F10,含历史实测)。 +2. \`docs/20_调查/STEP-095_V13独立严格代码与性能调查_v1.md\`(N01–N07、C01–C07、O01、E01/E02)。 +3. \`docs/20_调查/STEP-096_V13隔离盲审调查_v1.md\`(B01–B13;**已冻结的独立盲审原文,不得因修复结果修改其 findings**)。 +4. STEP-094、095、096 的任务/Step/完成报告与 release manifest,按真实性区分历史 PASS 与本轮 NOT RUN。 + +开始前读根 \`AGENTS.md\`、\`PARTICLE_BASELINE.md\`、\`docs/00_项目规范/\`、\`docs/00_项目总则/README.md\` 指定的四份有效总则。通过 RDC 确认 \`navsea-particle-integration\` 已同步到最新 \`main\`,并只读检查 \`README.md\`、\`WORKFLOW.md\`、\`coordination/TEAM_MODEL.md\`、\`contracts/\`、\`rfc/\`、\`adr/\`、\`status/\`、\`compatibility/\`。创建任务时 Integration main 为 \`39ad8415fbdb76c1822460bb09f3a3f35fe141e0\`,C-001 / RFC-001 为既有合同,不把其中历史 \`.5\` 误认为当前软件发布 \`.13\`。本任务**不修改 Integration、NavSea 仓库**,新增跨项目公共语义须先有协调/RFC/ADR/Contract 决定。 + +## 1. 去重修复总表与执行分类 + +状态枚举:\`CODE_FIX\`=本任务可实施;\`CONDITIONAL\`=只有满足设计/证据门槛才动;\`DESIGN_ONLY\`=不作为既有缺陷盲改;\`COORDINATION\`=需跨项目负责人协调。Codex 应将表中每项逐条写入实施矩阵,落实文件、测试入口、commit、实际状态;确认某项不成立也必须提供反证,不能直接删除。 + +| 合并问题 | 三轮原编号 | 初始分类 | 目标与安全边界 | +| --- | --- | --- | --- | +| 01 跨 render target 的 stencil CPU/GPU 状态失配 | N01 / B01 | CODE_FIX | target/pass 世代一致;首帧、R-R/R-C-R、cache ON/OFF | +| 02 Metal depth/stencil clear 与附件不完整 | N02 / B02 | CODE_FIX | 全部 RenderPass descriptor/combined attachment;不得只改颜色 | +| 03 LayerGroup render 异常后缓存错误标成功 | N03 / B12 | CODE_FIX | 失败不得 \`markBuildSucceeded\` 或 reuse;允许安全恢复 | +| 04 encoded PLAY 缺失唯一 production clock 驱动 | N04 / B06 | CODE_FIX | map/source owner 单一单调推进;无双重 tick | +| 05 GPU 粒子场身份未覆盖 V/validity | B13 | CODE_FIX | 两时间端点完整内容身份;正确复用和局部上传 | +| 06 8-bit 零向量偏差 | B07 | CONDITIONAL | 修复零表达和编码/解码一致性,不引入未验证的格式破坏 | +| 07 Mercator 东向位移缺纬度系数 | B08 | CODE_FIX | U/V 相同物理单位下正确度量,保留既定时间倍率 | +| 08 JSON 嵌入 NUL/重复键被接受 | N05 | CODE_FIX | 长度感知解析;非法计划清旧状态、fail-closed | +| 09 坏数值场先入库、物化失败替换好投影 | N06 | CODE_FIX | 事务式校验/发布;允许合法 no-data/validity=0 | +| 10 Android 重复 attach listener 积累 | N07 | CODE_FIX | 幂等 attach/detach、移除监听、对象释放 | +| 11 热路径完整 Diagnostics 深复制 | F01 / C01 | CODE_FIX | 内部轻快照与对外完整 pull 分离,线程一致性 | +| 12 Metal retained/普通 State/Trail 同步等待 | F02 / C02 / B03 | CONDITIONAL | 先记录同步点/依赖;不可直接删除 wait | +| 13 内容 dirty 导致相同尺寸 GPU allocation 重建 | F03 / C03 / B04 | CODE_FIX | contentDirty 与 resourceInvalid 分离;清屏正确性先通过 | +| 14 retained GPU 资源无总预算 | F04 / C04 | CONDITIONAL | 安全估算、分配失败完整 fallback;独立 R 不合并 | +| 15 encoded store 缺总 key/byte 预算 | C04 / B11 | CONDITIONAL | 保留绑定窗口和旧读者;若拒绝结果需新公共语义则停止 | +| 16 ordinal 前缀 O(S×G)/fast 扫全层 | F07 / C05 | CODE_FIX | topology generation 缓存索引,不能改变全局顺序 | +| 17 相同 time/mix-only 重物化和 UI 同步处理 | C06 / B05 / B10 | CODE_FIX(异步部分 CONDITIONAL) | 先幂等和不可变端点复用;owner-thread 安全发布 | +| 18 drawable/tweaker 重建与窗口端点复制 | C07 | CONDITIONAL | 只改可证明稳定的对象生命周期,避免新 stale 引用 | +| 19 旧 cache 的 release 记入新 topology | F09 / O01 | CODE_FIX | identity/generation 归属、历史/当前计数分离 | +| 20 Test Host RN \`.10\` / native \`.13\` 错配 | F05 / E01 | CODE_FIX | 默认 package/lock 与插件精确锁定;不把旧 TS 报错误归因 | +| 21 多 Continuous 退出 CPU Fast Path | F06 | DESIGN_ONLY | 保留安全 full CPU fallback,后续真实 A/B 再立项 multi-C | +| 22 source/tile 等保守全局失效 | F08 | DESIGN_ONLY | 没有完整依赖证明不得缩窄,后续 profiling/依赖设计 | +| 23 Integration compatibility/status 历史口径 | F10 / E02 | COORDINATION | 向粒子负责人提交接入证据请求,不擅改或声称 NavSea 支持 | +| 24 正式共享地理 Ocean Mask 尚未形成 | STEP-095 §7 / B09 | COORDINATION | 跨 Host/Shared Core/双平台显示契约需单独 RFC/ADR,不用合成 fixture 冒充正式实现 | + +注意:B13 与 N06 **不是同一缺陷**;GPU 内容身份遗漏与 encoded ingress 事务发布必须分别修复和验证。B07/B08 是粒子数值正确性,不等于 V13 分段回归。所有 P1/P2 是调查建议优先级,不是运行事故证明。 + +## 2. 固定顺序与不可跨越的检查点 + +严格按 \`基线复核 → A正确性 → B输入/生命周期 → C安全性能 → D交付\` 串行进行。前一阶段没有明确代码完成和静态检查结果,禁止推进下一阶段;失败、进程超时、状态未知不能当作成功。任一阶段发现需要更改冻结公共 API、QBIN/FieldProfile 语义或破坏跨平台 ABI,**停在该问题**,记录接口阻塞和建议,不自行加入 Adapter/Normalizer、隐藏通道或不兼容 fallback。不得为了凑齐总表自动进行大架构重构。 + +### 阶段 0:基线与定点反证 + +1. 确认 branch/HEAD/origin、dirty/untracked、submodule、manifest、V13 tag;对照三轮行号回读实际源码,建立 \`问题→当前源码→最短复现→反例→修复文件→目标回归\` 表。 +2. 确认 GL/Metal/RN/Test Host 的实际入口和平台条件,核对是否已有后续修改解决某项;有明确反证者记 \`NOT_REPRODUCIBLE_ON_CURRENT_HEAD\` 并附定位,不重复修复。 +3. 固定公开语义:\`fieldId/generation/validTime\`、U 东向/V 北向、T0/T1/MIX 与 OLD_T1→NEW_T0 reuse、Style Z-order、每个 R 独立 cache、颜色与粒子分离、天气获取由 Host 所有。必要的内部对象身份可以新增,禁止创造第二套公共场协议。 +4. 在 \`docs/30_设计/STEP-097_V13三轮问题去重与修复矩阵_v1.md\` 先写入逐项状态和安全不变量;后续随源码实现持续更新。这不是重新撰写第四轮审查。 + +### 阶段 A1:Renderer 正确性(01/02/03/19) + +- **01 stencil**:明确记录属于当前 render target/pass,切换附件时重置/分代 CPU mask/ID 记录;GPU clear 与 CPU 映射同世代。保留同一 target 中安全的 tile mask 复用,不能中途全局清掉仍需使用的 stencil。针对相同 tiles 的 R-R、R-C-R、C-R-R、首次 build/rebuild/reuse/cache OFF 加最小可执行回归入口。 +- **02 Metal**:从 gfx RenderPass descriptor 到 MTL descriptor 完整传递 color/depth/stencil 的 load/clear/store;正确处理 combined depth-stencil 和 device/simulator 附件存在性,创建失败显式不发布可用缓存。不能只依赖默认 loadAction 或 color isRenderable。补 descriptor/附件状态与 fallback 可观察测试入口;不宣称 iOS runtime PASS。 +- **03 错误缓存**:使 layer-group 绘制失败结果贯穿 opaque/translucent、observer、finishBuild;部分绘制不得发布为 valid,不得在下一帧 reuse。正常 fallback/retry 能恢复;保留已有资源准备失败回退,不吞异常或改成进程崩溃。 +- **19 计数归属**:cache 创建/析构使用稳定 identity + topology generation,旧资源 release 不记入同 index 的新段;聚合统计与当前 topology 统计分离。连续模式切换、R→C、相同 index 换边界均有验证入口。诊断错误不夸大为纹理本体泄漏。 +- 本阶段有任何 stencil/Metal/异常正确性未闭合时,**不得开始资源复用/异步 GPU 性能改造**。 + +### 阶段 A2:Field/粒子正确性(04/05/06/07) + +- **04 PLAY**:只允许一个 map/source owner 推进 forecast clock,基于单调时间处理 PLAY/PAUSE、速度、恢复前后台、clear/destroy;平台 timer 仅调度/唤醒,不允许 WeatherColor 与 Particle 各推进一次。只 setTime 一次后 PLAY 自动移动 time/mix;保持 Host 显式 setTime 路径语义。 +- **05 B13**:在两个端点的 GPU 更新门槛覆盖 U、V、validity、grid、encoding/解释语义及来源版本;generation、fieldID、dataVersion 不能代替完整内容身份。只变 mix 不应上传场,真正跨窗时旧 T1→新 T0 安全复用,只上传需要的新端点,不能每 tick 重建;无效/旧 revision 不可污染有效窗口。 +- **06 B07**:先确认当前 GL/Metal 纹理格式、GPU 精度与 CPU/shader 编解码约束;以零、微风、正负抵消及 NaN/validity fixture 定量约束误差。选择可准确表达零的**内部**表示或明确等效方案,GL/Metal 统一;如果必须改变公共 encoding、需要不支持的图形格式或改变已发布输入 ABI,则不强行实施,记录 \`BLOCKED_FORMAT_DECISION\`。 +- **07 B08**:按 eastward U / northward V m/s 推导 Web Mercator 度量,对 U/V 一致应用纬度修正、符号、极区夹紧和既有 visual time scale。建立 0°/45°/60°、东/西/南/北及极区有限值的解析对照;不得更改 Host 输入向量单位。 + +### 阶段 B:输入与平台生命周期(08/09/10/20) + +- **08 N05**:JSON strategy/startLayerId 必须使用长度感知字符串;拒绝内嵌 NUL、重复键、未知 schema/strategy、缺失/逆序边界。无效配置 fail-closed、旧计划清除与恢复语义保持,Android/iOS/RN 错误归一化不分叉。 +- **09 N06**:在已解码 frame 真正替换 Store 前,按既有 profile/validity/no-data 语义验证有效 cell 的有限 U/V 和 scale/offset 运算不溢出;事务式提交,失败返回明确错误并保留好旧帧、投影及 GPU 显示。不把合法 validity=0 或规范允许的 noData 当成错误;不同时间/高 revision 的合法行为不改变。 +- **10 N07**:同 View 重复 attach/detach 幂等;map listener 有明确 remove/dispose owner 和 map/destroy 世代。测试入口覆盖 100 次复用、切 view、销毁中的异步回调,不产生悬挂访问或多重 dispose。 +- **20 E01**:核实真实发布坐标、Host \`package.json\` 和 lockfile、plugin 注入的 Native 版本;将**默认** Test Host 消费组合对齐经核对的 \`.13\` 精确配对,不将临时 node_modules/本地软链当作成功。若需要 install/codegen/Gradle,则准备 RDC/用户命令并标 \`NOT RUN\`;不发布、不覆盖现有 \`.13\` 产物、不改 NavSea。 + +### 阶段 C:有边界的性能与资源(11–18) + +严格先做只读热点/资源依赖记录,再实施不会改变正确性和公共契约的优化,优先把同一阶段兼容变更集中,避免每个小点都要求一次 Android APK。 + +1. **11 轻量 Diagnostics**:内部 fast/render 热路径只取同一 generation 的轻量一致状态;完整 Style/Segments 深复制仅在外部 pull 时构造。避免用若干无同步原子值组成不一致快照;低频 pull schema 兼容。 +2. **16 ordinal/扫描**:预计算 global ordinal 和粒子/待更新 group 列表,topology/style reorder 后失效重建;保持 opaque reverse、translucent forward、隐藏层及全局 Z-order,不能用局部 ordinal 替代全局编号。 +3. **17 物化与 UI**:相同 requestedTime 和相同不可变端点早退;mix-only 仅更新 mix,复用完整内容 identity(U/V/validity/grid/encoding),与 B13 修复共用身份。双 binding 共享安全的不可变端点,不能跨 generation 复用旧数据。Android/iOS 解码和 CRC 若转 worker,必须保存 map/source 世代、owner-thread 事务发布、有限队列、取消和 destroy race;若涉及跨平台公共异步返回语义或不能证明生命周期,先完成同步路径早退,异步部分标 \`DEFERRED_DESIGN\`。 +4. **13 retained allocation**:仅在 A1 的 target clear/错误语义已经正确后,分离内容 dirty 与资源 key invalid;同尺寸、同 gfx context/format 可清除重绘;resize/context loss/关闭明确释放。相邻 R-R 仍是独立 cache。 +5. **14/15 预算**:显式估算 retained color+depth/stencil+pending 与 fieldStore key/byte 上界,处理整数溢出和在途旧快照。无法分配必须走原设计的安全 fallback 或拒绝;不得静默丢弃仍绑定窗口。若新增拒绝状态无法用现有 public error contract 表达,保留安全上界/诊断并提交接口阻塞,禁止私改 RN/Native 双方错误语义。 +6. **18 Drawable**:仅在能证明版本/target/UBO 生命周期正确时保留可复用 drawable/tweaker、减少 field endpoint 深复制;区分 tick、reseed、resize、context loss 与窗口轮换。未能证明不产生 stale resources 时不做大重构。 +7. **12 Metal wait**:仅盘点 retained build、State/Trail 等实际同步点、命令和资源依赖,准备 Metal capture/性能基准。未经设备/renderer 验证**不得直接删 \`waitUntilCompleted\`、改成无法证明安全的异步 present**。可在现有顺序中去除明确重复且不改变依赖的等待,但须提供静态资源所有权证据;否则标 \`DEFERRED_PROFILE_AND_RUNTIME\`。 + +## 3. 本任务明确不进行的架构/协调事项 + +- **F06 multi-C**:保留既有安全 full CPU fallback 和 GPU retained reuse,不为达到指标改变 Style 顺序或暗中合并 C/R;新的可组合 multi-C fast path 单独设计、后续 A/B 验证。 +- **F08 精细失效**:保留 source/tile/glyph/image/placement 的保守全局 invalidation,直到存在完整依赖与变更序列回归;不因性能冲动将其裁窄。 +- **B09 Ocean Mask**:现有 synthetic fixture 不是正式共享地理 Mask。本任务只形成 Host→Shared Core→两层消费及跨平台一致性需求、最小数据边界和阻塞报告,交粒子负责人按 Integration C-001/RFC/ADR 决定;**不借任务顺便建立新的公开 mask API、导入 provider 或让 MapLibre 理解 NavSea 业务**。 +- **F10/E02**:只输出“需 Integration 更新 Compatibility/Status”的事实与建议,并提供当前软件版本与真正验证状态;不修改 Integration 文档或宣称 NavSea 已支持。 +- 不扩大为 renderer 全架构重写、QBIN 格式版本升级、RN 第二套桥、全量新主题、NavSea业务变动、新软件版本发布或复原旧三明治设计。 + +## 4. 验证准备、执行责任与验收矩阵 + +Codex 负责按实际代码添加**现有框架下必要的定向 C++/平台测试源码或最小 fixture**、静态核对、命令和期望结果;仅有调用说明但没有针对关键回归的测试入口,不能把相应问题标代码完成。可按现有项目测试结构写回归,但本任务**不包含 Jest,也不运行 Jest**。不修改旧断言来掩盖真实失败,不删除测试、不降低测试门槛。 + +| 验证集合 | 必备情形/最低证据 | +| --- | --- | +| Renderer A1 | 同 source/same tiles R-R/R-C-R、主/离屏 stencil;Metal clear/load/store、combined attachment;异常后禁止 reuse;R→C/拓扑更换计数 | +| Field A2 | 一次 setTime 后 PLAY/PAUSE/rate;T0/T1 boundary reuse;同 identity 仅 V 或 validity 变化;零/微速量化、0°/45°/60° Mercator | +| 输入 B | NUL/重复键/invalid→valid;好旧场→坏高 revision 且旧场保留;重复 attach/销毁;Host 默认版本与 lockfile | +| CPU C | pull-only diagnostics、reorder/ordinal、相同时间/仅 mix、窗口换端点、UI owner 世代安全 | +| 资源 C | mutation vs resize vs context loss、allocation fail、R×viewport×fieldKey 阶梯、清理恢复、GL/Metal resource ownership | +| 集中运行态(后续) | Android OpenGL + iOS Metal 各自像素/数值/时间/资源复现;30/60 FPS、p50/p95/p99、峰值/长稳、方向/缩放、状态恢复;按平台独立记录 | + +**现在不执行** CMake、Xcode、Gradle、Codegen、Test Host install、APK/AAR、ADB、安装、真实设备、GPU capture、profiler、NavSea 集成或正式 V13 运行确认。Codex 结束时给出可复制的 RDC 验证命令(以本仓现有 runner/build 入口为准);Android Win11 手工 Gradle/ADB 和 iOS 设备验证依后续授权,不在本任务自动触发。不得把 STEP-094 历史 43/43、旧报告 69/69 或任何已发布 manifest PASS 写成本任务结果。 + +RDC 在每个代码批次之后独立审查文件/静态 diff;未来获授权再集中编译/运行,而不是在本轮分阶段频繁请求重装 APK。未来完整 Android 编译应遵守 \`tools/particle_android_validation/build_validation_apk.sh\` 当前权威流水线与用户手工运行安排。Metal 修改必须留给 Mac 同 commit 验证,Android 结果不得替 iOS PASS。 + +## 5. 允许修改与明确禁止 + +允许(仅与已复核问题直接相关,实际存在的路径以源码核对为准): +- \`src/mbgl/renderer/\`、\`src/mbgl/gl/\`、\`src/mbgl/mtl/\`、\`include/mbgl/\`、所需 gfx backend; +- \`src/mbgl/particle/\`、\`src/mbgl/style/sources/raster_array_source.cpp\`、\`src/mbgl/renderer/layers/\`、对应 GL/Metal shader及其标准生成源; +- Android Java/Kotlin/JNI、iOS ObjC++、RN 公共实现及 Test Host 本任务限域依赖文件; +- 对应定向测试源码/fixture、下述交付文档和开发记录。 +涉及 shader generator 必须改其权威源而非只手改自动生成产物,并附原生成/校验命令由 RDC 后续执行。 + +禁止:修改 Integration、NavSea、项目总则、旧三份调查/任务/报告、已发布 manifest/tag/tgz/AAR/XCFramework、无关 package/lock、官方上游基线;禁止 \`git reset --hard\`、\`git clean\`、force push、push upstream;禁止把公开 API/FieldProfile/QBIN breaking 变更悄悄塞进修复。不能自行把 V13 版本重命名为下一发布版,下一版本号属于独立发布任务。 + +## 6. 必做输出与报告路径 + +执行时必须建立、更新且全部中文: +1. \`docs/30_设计/STEP-097_V13三轮问题去重与修复矩阵_v1.md\`:保留表中全部 24 行、旧编号、实际源码定位、修复/不修理由、源码状态、测试入口、commit、平台影响、运行待测项。分阶段附可量化的变更前/后操作次数模型,不虚构 FPS/内存收益。 +2. \`docs/50_Step记录/STEP-097_V13三轮审查问题合并修复_v1.md\`:阶段 0/A1/A2/B/C/D 检查点、状态、实际改动、阻塞、下一步。每一阶段结束后追加短而具体的事实,未完成不得勾选。 +3. \`docs/60_测试/STEP-097_V13修复后集中验证清单_v1.md\`:按 Android GL、iOS Metal、通用 Core/测试入口、后续 GPU 性能分层记录**尚未执行**的精确命令、fixtures、预期输出和复核责任。 +4. \`docs/报告/STEP-097_V13三轮审查问题合并修复_Codex完成报告_v1.md\`:按规定详列开始/结束 HEAD、每批提交、文件清单、各问题状态、跨项目阻塞、静态检查实际命令和结果、所有构建/运行 \`NOT RUN\`、软件发布 \`NOT RUN\`、下一步集中 RDC 验证。 +5. 在根目录 \`粒子开发记录.md\` **仅追加**实际进展;不得覆盖历史。 +6. 若实际触发跨 Contract 阻塞,额外写 \`docs/报告/STEP-097_公共接口阻塞报告_v1.md\`,明确现有 C-001/RFC-001、拟议语义、为什么无法安全局部修、Android/iOS/NavSea 各自责任;本地只记录并提升级建议,不假装协调决定已被接受。 + +完成状态只能细分为 \`CODE_IMPLEMENTED_STATIC_CHECKED\`、\`DEFERRED_DESIGN\`、\`BLOCKED_CONTRACT\`、\`NOT_REPRODUCIBLE_WITH_EVIDENCE\`、\`WAITING_RDC_BUILD_AND_RUNTIME\`;没有执行的验证永远写 \`NOT RUN\`,产品级 \`PASS\` 禁用。 + +## 7. Git 收尾与停止条件 + +在当前 \`particle/main\` 上执行,先保护所有已有 dirty/untracked;只 stage 本任务变更。按 A1、A2、B、C 各自边界形成 scoped commit,文档随阶段更新;阶段失败不能越过到下一阶段,不可混入不相关工作。每个成功阶段允许 push \`origin particle/main\` 供 RDC 直接核对,失败必须确认进程/状态并修复,不能以 timeout 当作已失败后盲目重复 push。最终 \`git diff --check\`、提交内容、远端 HEAD、clean status 明确记录。push 失败不得宣称交付完成;提交并不代表运行验收。 + +触发以下任一条件则停止相应实现并生成阻塞事实:需要新增/改变公共契约而无 Integration 决定;必须升级工具链/MapLibre;不能证明跨 GL/Metal 正确或资源所有权;修复需要删除已有功能/测试或强制绕过安全 fallback;当前 HEAD 与所需源码不符且不能安全合并;既有用户改动会被覆盖;任一关键问题没有明确退出条件。必要时整任务停在已完成的 scoped checkpoint,报告 PARTIAL,不靠继续扩大代码量冒充完成。 + +交付后由 RDC 独立审查并按后续授权集中执行构建、回归和设备验收。**本任务不发布下一版软件,不提前关闭 V13 GPU 运行态。** diff --git a/docs/40_任务/STEP-098_QBIN_V4单帧首显与八帧滚动缓存_Codex任务_v1.md b/docs/40_任务/STEP-098_QBIN_V4单帧首显与八帧滚动缓存_Codex任务_v1.md new file mode 100644 index 000000000..70b949476 --- /dev/null +++ b/docs/40_任务/STEP-098_QBIN_V4单帧首显与八帧滚动缓存_Codex任务_v1.md @@ -0,0 +1,141 @@ +# STEP-098:QBIN V4 单帧首显与按 fieldKey 独立的八帧滚动缓存 — Codex 任务 v1 + +状态:**任务书 / 待契约闸门通过后执行**。本任务书不代表代码已经修改、编译或双平台运行验收通过。 + +## 0. 目标与执行环境 + +本任务落实已讨论的目标模型:**单帧可独立显示;第二帧到达后支持正常时间插值;后续时间片在 Native 内部进入每个 fieldKey 独立的、按 validTime 升序排序的固定 8 帧滚动缓存。** 满 8 帧提交新的合法时间片时应自动淘汰,不应因正常满额返回 `REJECTED_CAPACITY`。 + +当前用户维护和修改的源码以 **Mac** 为准:`/Users/tei/dev/MapLibreParticleLab`,分支 `particle/main`。编写本任务书时 Mac HEAD / origin HEAD 均为 `0d13552bed84f427dd43cae9856391bd26622179`。执行前重新查询 Git 事实,不以该 SHA 或本文行号覆盖后续提交;不要拿 Linux 旧工作树代替 Mac 代码。任务 MD 的目标归档位置为 `docs/40_任务/STEP-098_QBIN_V4单帧首显与八帧滚动缓存_Codex任务_v1.md`;不要创建 `docs/任务/`。 + +本阶段以 **Shared Core 公共语义和现有 RN V4 公共接口**为实施目标,不在 NavSea/Weather Server 中实施下载策略;Android 与 iOS 使用同一共享语义,平台专项编译/实体机验收分别另行完成。不得因 Mac 为编辑机而建立 iOS 专属 Core 行为。 + +### 固定边界 + +- RN:负责获取预报目录、QBIN 文件、本地下载缓存、用户选择的预报时间、异步命令 latest-wins 和 UI。RN 已提供 `FieldRuntime.submitEncodedField(...)`、`FieldRuntime.setEncodedForecastTime(...)`,**先 await 前者成功,再 await 后者**;无需新增组合提交 API。 +- Shared Core/Native:负责 QBIN 校验、存储、每 fieldKey 内排序/淘汰/复用、单帧/双帧解析、颜色与粒子投影、GPU 资源正确性。 +- 不增加 Native→RN 索取数据回调;Native 不接入 provider、HTTP、manifest、下载、缓存文件管理或 NavSea 业务语义。 +- 不修改 QBIN 文件格式、Rice 解码语义、现有公开方法签名、Style Z-order 或 V3 路径,不在 JS 伪造第二帧,也不在 Host 手动清空 8 帧来实现正常拖动。 + +## 1. 首要闸门:Integration 公共契约 + +1. 先经 RDC 检查并同步 `/Users/tei/dev/navsea-particle-integration` 的最新 `origin/main`(`git fetch origin main` + 确认可 fast-forward;尊重现有本地改动)。读取该仓库 `README.md`、`WORKFLOW.md`、`coordination/TEAM_MODEL.md`、`contracts/`、`rfc/`、`adr/`、`status/`、`compatibility/`、`coordination/active/COORD-004/` 和 `instructions/PARTICLE_PROJECT_INSTRUCTIONS.md`。 +2. 编写本任务书时 Integration HEAD 为 `318f8ccf911ac3136c3086a1d14e61f83e2d4bce`;`RFC-003_编码场两帧首显与增量补帧语义.md` 仍处于 DRAFT,其第一条要求至少两帧、满额返回容量拒绝,与本任务新目标**明确冲突**。本任务书不是已经接受的跨平台 RFC。 +3. **开始改公共源码前,必须由粒子负责人/Integration 按既定流程正式更新 RFC-003、COORD/Contract 或 ADR,接受下述单帧和滚动淘汰语义,明确 Android/iOS 共用。** Codex 不得自行把 DRAFT 当 ACCEPTED,也不得静默更改 Integration 文档替代负责人决定。 +4. 如果执行时仍没有已接受且不冲突的正式依据:先在 `docs/报告/STEP-098_公共契约阻塞报告_v1.md` 写出当前 SHA、冲突条目、拟实施语义与所需协调决策,**停止功能修改及 push**,状态记为 `BLOCKED_CONTRACT`,交回粒子负责人。不得绕过闸门做 Android/iOS 两套实现。 +5. 若 Integration 已更新且批准,记录精确文档路径、版本与 commit SHA,后续实现以正式版本为准;如正式结论与本文不同,应停止并回报,不自行择一。 + +## 2. 开始前必须核对的 Mac 实际基线 + +先读根项目规范、`PARTICLE_BASELINE.md`(如存在)、`docs/00_项目规范/`、`docs/00_项目总则/README.md` 指定总则、与 STEP-097 的正在实施范围。读取并建立文件/函数对照表: + +- `include/mbgl/particle/field/field_runtime.hpp`、`src/mbgl/particle/field/field_runtime.cpp`:`FieldSeries`、`EncodedFieldStore`、`resolve`、容量及 revision/identity。 +- `src/mbgl/particle/field/field_binding.cpp`、`src/mbgl/particle/field/field_projection_materialization.cpp`:单/双帧投影、独立绑定和物化。 +- `src/mbgl/style/sources/raster_array_source.cpp` 及相关头文件:`submitEncodedField`、`setRequestedForecastTime`、`refreshV4RendererState`、现有 projection equality/notification。 +- `src/mbgl/renderer/layers/render_weather_color_layer.cpp`、`render_particle_validation_layer.cpp` 及 shader/texture 资源:单帧/双帧下颜色、粒子、轨迹和 GPU 复用。 +- `packages/maplibre-particle-rn/src/FieldRuntime.ts`、`NativeWeatherRuntimeBridge.ts`、Android Kotlin bridge/JNI、iOS `WeatherRuntimeBridge.mm`/`MLNParticleFieldRuntime.mm`:确认两个现有公开调用完整传递结果。 +- `test/particle/field_runtime.test.cpp`、`test/particle/field_binding.test.cpp`、`test/source/raster_array_v4_host_api.test.cpp` 及现有 Renderer 定向验证入口。 + +编写任务书时源码事实:`FieldSeries::submit` 对第 9 个不同时间直接 `RejectedCapacity`;`resolve` 对 `frames.size()<2` 返回空;`submitEncodedField` 已会刷新投影但不修改 forecast time。以上仅供定位,若新 HEAD 有变化应以实际代码为准。若 STEP-097 仍在改相同的 runtime/projection/Renderer 文件,先确认依赖顺序,不得并行覆盖或回滚已完成的修复;尤其保留 STEP-097 的输入校验、forecast clock、GPU 身份、资源生命周期等新修正。 + +## 3. 正式目标语义 + +### 3.1 缓存所有权、容量与排序 + +- `EncodedFieldStore` 按 `fieldKey` 分别持有 `FieldSeries`。`wind`、`wave`、`rain`、`tide` 使用不同 key 时分别最多 8 帧;同 key 的多个 Layer/Binding 共享该序列。不能把四类天气合为“全局 8 帧”。 +- 每帧按 `frameContentIdentity.validTimeMs` **升序**保存,提交顺序不影响排序;不得假设固定一小时/三小时间隔。 +- 已有相同 `validTime`:先走现有 Duplicate、identity conflict、stale、合法 revision 替换语义;不额外占一个槽,不因为满额淘汰别的帧。 +- 不同时间且不足 8 帧:先完整校验,按时间有序插入。 +- 不同时间且已满 8 帧:在成功通过解码、profile、grid/plane、身份和相邻兼容性校验后,选择淘汰一帧并原子提交新有序集合。正常满额替换成功返回现有 `ACCEPTED_INSERTED`(或正式合同要求的**既有**成功语义),不返回 `REJECTED_CAPACITY`。为了 TypeScript/ABI 向后兼容可以保留旧结果枚举,但正常容量=8的有效插入路径不再产生它;不可偷偷更名公共结果。 +- 淘汰只作用于正在提交的 `fieldKey`,不影响其他天气种类、Bindings、requestedForecastTime、V3 数据或 Style。 + +### 3.2 双向淘汰,按新提交帧时间取中心 + +由于既定调用顺序是先 `submitEncodedField()`、后 `setEncodedForecastTime()`,**提交阶段不能依赖尚未下发的新目标时间**。本版使用“新提交帧 validTime 为中心,淘汰时间距离最远的端点”的确定性规则;新帧本身必须留下。对于已升序的满额序列,只在最早端或最晚端候选之间选择;若距离相同,按正式合同规定稳定决胜,允许优先避免淘汰当前显示端点,但不可牺牲新提交帧。不得永远只删除最旧帧。 + +必须覆盖: + +- 原有示意 `13:00 ... 21:00` 共 8 帧;提交 `22:00` → 删除最早端 `13:00`,插入 `22:00`,最终仍 8 帧。 +- 已向后滚动后提交 `12:00` → 删除最远的最晚端,重新保留 `12:00`;向过去拖动同样成立。 +- 已满时提交位于内部的新时刻 → 仍保持有序、8 帧和确定的端点淘汰。 +- 今天跳到后天某一时刻 → 接收该时刻 QBIN,而不是要求 RN 先清空旧窗口或填齐 8 帧。之后逐帧提交,Native 自行滚动。 +- 乱序提交、Duplicate、同时间新 revision 不应造成意外淘汰。 +- 插入不兼容数据或解码失败时,**零淘汰、零 Store/Projection/Clock 修改**;不得先删除旧帧再验证。 + +### 3.3 单帧首显与双帧升级 + +- 当请求时间精确对应某个已验证 `validTime`,即使整个 Store 只有一帧,或它与其余 7 帧相隔很远无法形成合理双帧窗口,也应能够显示该帧:颜色为这一帧的值;有有效 U/V(或规范允许的方向向量)时粒子可持续运动,**天气时间不变不等于粒子停止**;仅标量帧不能伪装成可平流向量。 +- 单帧在运行时具有真实单端点语义。可在内部使用同一 Snapshot 作为 T0/T1、`mix=0` 来复用现有双纹理 shader,但不得向 RN 要求伪造第二个 QBIN 或捏造第二个不同 validTime;优先共享解码结果、物化 Snapshot 和 GPU 纹理,避免同一单帧无意义地双份常驻。 +- 第二帧补入后,如果目标时间被合法的相邻端点覆盖,则自动升级为双帧插值,保持既有 `T0/T1/MIX`,端点、mix 与实际预报时间对应;如果并未覆盖,保持合法单帧或 fail-closed,不能强行把不相关的旧帧插值进去。 +- 目标时间落在两帧之间且有合法覆盖时执行正常时间插值;目标时间既不精确匹配可用单帧、也没有合法双帧覆盖时返回明确 unavailable,不使用“最近旧帧”冒充该时刻。RN 若选择最近预报帧首显,必须以该帧实际 `validTime` 作为已显示时刻;用户日历选中时间与实际显示时间不可混称。例如用户初始选择 13:30,只有最近的 12:00 QBIN 时,RN 可先提交该帧并调用现有 `setEncodedForecastTime(12:00)` 显示 12:00 静态预报,仍将 13:30 保留为业务层待完成目标;等 15:00 帧到达后再设置 `setEncodedForecastTime(13:30)` 进入插值。不得在 Native 悄悄修改 `requestedForecastTime`,也不得将临时 12:00 画面标成 13:30 的插值天气。 +- 不能把跨 dataset/reference/Profile 的帧混合成一个时间序列;沿用已有权威身份契约。不凭固定时间间隔猜测有效帧,也不能用任意远离的旧时间片推断新的天气。若当前元数据不足以明确区分“同一批次的合法双端点”与“跨批次伪兼容”,作为契约阻塞项报告,不能自行新造隐含协议。 +- 明确 `fieldKey` 之间可有不同 validTime 覆盖。颜色与粒子在同一全局请求时刻分别 resolve,各自可处于单帧、双帧或 unavailable;不得因一个 fieldKey 无数据清空其他 key 的合法投影。 + +### 3.4 两个现有 RN API 的行为不变 + +正常时间轴操作为: + +1. RN 从本地缓存或服务器取得对应 QBIN,先 `await FieldRuntime.submitEncodedField(...)`。 +2. 仅在 `ACCEPTED_INSERTED`、`ACCEPTED_REPLACED_REVISION` 或可确认的 `DUPLICATE` 等成功结果后,`await FieldRuntime.setEncodedForecastTime(reactTag, targetTimeMs)`。 +3. Native 的 submit **不擅自修改目标时间**,setTime 不触发网络下载。已缓存时间直接 setTime 即可;无需每次重新 submit。 +4. 两调用非原子,可能在两次调用之间短暂显示旧时间、单帧或暂时不可用,**不可对外保证无闪烁**。不得把旧时刻天气伪标为新目标。RN 侧 latest-wins、目标版本和展示状态属于后续 Host 接入范围;本任务不私改 NavSea。 +5. 不新增 `submitAndSetTime`、Native→RN 数据请求事件、逐帧删除 RN API 或新的 provider 参数;保留现有 `clearEncodedField()` / `clearAllEncodedFields()` 作为显式重置能力,而不是正常滚动手段。 + +### 3.5 Renderer 与资源安全 + +- 正常向前/向后补帧且当前已选 pair 和 mix 未变化时,避免无条件 rebuild、重复 GPU 上传、清空 trail 或发布无意义 Source revision;复用 existing `sameProjection`/GPU content identity。 +- 单帧→双帧、双帧窗口变化和 revision 更新必须正确刷新,shader 无非法 T1/null;若相同 Snapshot 对应 T0/T1,避免重复上传同一内容。支持 GL/Metal 共享语义,不能只修 Android Renderer。 +- 淘汰旧帧不能销毁仍被当前 projection/GPU 更新流程持有的不可变 Snapshot;通过正常引用计数和资源所有权安全释放。切换不同窗口时遵守现有 trail/particle reset 规则,不错误承诺跨任意远跳跃保留轨迹。 +- 坏帧不允许覆盖已验证的好投影;生命周期关闭、clear、style reload、map 销毁与异步提交有明确所有权,不使旧请求重新点亮已 OFF 的画面。 + +### 3.6 内存边界 + +- 8 帧是**每 fieldKey 的数量上限**,不是无限制内存保证。每个 decoded frame 由原始 Plane 字节、validity bitset 和元数据构成;参与显示的端点另有 Float32 物化数组、图像、纹理和粒子/trail 资源。不得错误地用压缩 QBIN 大小表示运行内存。 +- 不在本任务未经批准添加任意固定全局 MiB 上限或第二套容量公开错误码。为风/浪/雨/潮各 8 帧、单端点/双端点和跳跃替换提供 CPU/GPU 常驻及峰值测量入口或现有计数;记录可计算的 decoded byte 占用、资源复用/释放及最坏边界风险。 +- 评估单帧 T0=T1 情况下重复物化/重复上传;保证淘汰与资源 release 后无持续增长。若测量显示 32 帧和显示资源会触及移动端内存风险,报告下一步“全局字节预算/多 fieldKey 优先级”设计建议,**不可把内存申请失败伪装成正常 REJECTED_CAPACITY**。 + +## 4. 实施顺序与测试矩阵 + +阶段 A:只读复核与契约闸门 → 阶段 B:Core Store 滚动淘汰及单帧 resolver → 阶段 C:Binding、物化、Renderer 单/双帧和资源复用 → 阶段 D:已有 RN Bridge 端到端语义检查 → 阶段 E:文件、报告、Git 收尾。前一阶段未明确通过不进入后一阶段;命令 timeout/进程未知不能算通过。发生公共 ABI、QBIN、profile、dataset 身份或无法证实跨平台一致性的阻塞时停止扩大范围。 + +至少具备以下定向验证;优先扩展既有 C++ 源级测试和可复用 Test Host 场景,不新建无关测试框架: + +| 编号 | 场景 | 必须得到的结果 | +|---|---|---| +| T01 | 只有一帧,target=validTime | 投影合法;色场可显示;有 U/V 时粒子可动;实际时刻正确 | +| T02 | 首帧后补入第二帧,target 在两者中间 | 单帧→双帧,MIX 正确;不用伪造第二帧 | +| T03 | 8 帧未满乱序提交 | 按 validTime 升序;各 key 相互隔离 | +| T04 | 满 8 帧,向未来提交新帧 | 淘汰最早端、保留新帧,计数仍 8,无正常容量拒绝 | +| T05 | 满 8 帧,向过去提交新帧 | 淘汰最晚端、保留新帧,计数仍 8 | +| T06 | 满 8 帧,提交中间时刻 | 确定性端点淘汰;排序/唯一性不变 | +| T07 | 今天→后天单帧→后续补帧→返回今天 | 新时刻单帧可用,后续升级双帧;旧数据需 RN 重交时可正常滚动,不越界、不混批 | +| T08 | Duplicate、同时间合法新 revision、stale、identity conflict | 保留现有判定;重复/替换不淘汰其他时刻 | +| T09 | 错 QBIN、Profile/Grid/Plane 不兼容、无效向量 | 拒绝且 Store、forecast clock、原投影不被破坏 | +| T10 | 目标时间位于已缓存相邻两帧之间 | 正常插值,mix 首/中/末精确 | +| T11 | 目标不被合法帧覆盖也不精确匹配 | unavailable,不把最近帧冒充目标时间 | +| T12 | 新帧在当前 pair 外围、当前投影不变 | requestedTime、当前 pair/mix 保持;避免无意义资源重建 | +| T13 | 新帧改变当前 bracket,或当前端点被淘汰 | 正确更新;不沿用失效旧 GPU 数据、错误轨迹 | +| T14 | `wind` 满额后提交 `wave`;同 key Color+Particle | 独立 8 帧;其他 key 不受影响;同 key 不复制 Store | +| T15 | 单/双端点 Materialization/GL/Metal 资源 | 单帧无需两份独立 QBIN/CPU/GPU 内容;复用、release 与上下文丢失恢复正确 | +| T16 | 连续 100+ 次双向滚动、反复 clear、切 Runtime | 帧上限不破、无持续内存增长和悬挂引用 | +| T17 | RN 两步调用成功与失败分支 | 既有 API/TypeScript/Android/iOS bridge 类型兼容,失败不继续 setTime;不新增公开 API | + +注意:Native 静态/定向测试只能证明对应源码路径,不等于手机 GPU 首帧或 NavSea UI 通过。对尚未执行的 Android/iOS 真机、滚动拖动、跨天回退、内存峰值必须标 `NOT RUN / WAITING_USER_VALIDATION`。 + +## 5. 修改范围与明确禁止项 + +允许根据当前真实源码在上述 Shared Core/Source/Renderer 及对应现有测试文件做必要的最小修改;仅在证明是既有 API 传递障碍时修改 Android/iOS bridge,保持方法签名和结果语义。可在 `docs/30_设计/` 记录淘汰不变量与单帧/双帧选择表,在 `docs/50_Step记录/` 记录实施进度。 + +禁止:修改 NavSea、Server、天气 provider、下载缓存、Manifest、日历 UI;重新生成 PBF、修改 Style Z-order、强行更改 QBIN/QBV3 格式;新建第二套 FieldStore/Timeline、额外 JS Normalizer/Wrapper、私有 Native 通道、自动请求 RN 数据、提交单帧伪造第二份不同时间、给四类天气合用 8 帧、让正常满额仍返回容量拒绝。不允许顺带完成 STEP-097 的未授权项,也不允许覆盖现有发布 tag/manifest 或把源码通过称为正式发布通过。 + +## 6. 构建、交付与 Git + +- 本次编译、Gradle/Pod/Xcode 构建、APK/IPA 安装、ADB/设备操作及实际 GPU 验收由用户人工执行。Codex 可做源码、`git diff --check`、格式/静态检查及环境许可的低成本 C++ 定向测试;**不得擅自启动完整 Android/iOS 编译或设备操作**,也不得把未运行项目写成 PASS。 +- 准备一份 Windows 11 可直接执行的 Android 手动验证清单,ADB 命令如需涉及当前虚拟机必须显式 `adb -s 192.168.200.139 ...`;iOS 只列 Mac 用户手工构建/设备验收条件。合并一轮验证 T01–T17,不为单个按钮或日志要求反复重装。 +- 必做输出:`docs/30_设计/STEP-098_QBIN_V4单帧与八帧滚动缓存设计_v1.md`、`docs/50_Step记录/STEP-098_QBIN_V4单帧与八帧滚动缓存_v1.md`、`docs/报告/STEP-098_QBIN_V4单帧与八帧滚动缓存_Codex完成报告_v1.md`。目录不存在先创建;同主题报告已存在则递增版本,不覆盖旧报告。全部中文。 +- 报告明确记录:当前 HEAD、获接受的 Integration 协议版本/commit、真实改动列表、API/ABI 是否变化、每项 T01–T17 执行状态、`REJECTED_CAPACITY` 新语义、单帧显示实际范围、CPU/GPU 内存测量与未验证项、下一步 Android/iOS/Host 责任。最多只能报告 `IMPLEMENTED / WAITING_PLATFORM_RUNTIME_VALIDATION`;契约闸门未过则 `BLOCKED_CONTRACT`。 +- 完成可实施阶段且检查通过后,只提交本次任务相关变更到 **Mac 当前项目 `particle/main`**,并 push 到公开 `origin/particle/main`。提交前检查未跟踪/用户既有文件,不得 `git add -A` 混入无关变更;先核验 `origin` 是否前进,冲突时停下处理而非强推。完成报告必须写最终 commit hash、commit message、push 结果和远端 branch/HEAD;push 失败不能声称完成。若被契约闸门阻塞,不提交或 push 功能代码。 + +## 7. 最终验收口径 + +用户能在 RN 维持两个现有 API 和顺序:第一帧即显示其真实有效时间;第二帧具备合法覆盖时升级插值;后续任意顺序提交,Native 每 key 自动保持最多 8 帧。向未来提交淘汰最早,向过去提交淘汰最晚;正常已满不会 `REJECTED_CAPACITY`;四类天气互不挤占。无效帧零副作用、时间语义不造假、Core 不掌握下载/日历。Renderer 不因正常补帧无谓清屏或持续泄漏。Android 和 iOS 分别通过用户实际设备验证后,才能由 Integration 更新兼容性及正式状态。 diff --git a/docs/40_任务/STEP-100_SourceScoped_RenderSegment局部失效_Codex任务_v2.md b/docs/40_任务/STEP-100_SourceScoped_RenderSegment局部失效_Codex任务_v2.md new file mode 100644 index 000000000..dadb8a1de --- /dev/null +++ b/docs/40_任务/STEP-100_SourceScoped_RenderSegment局部失效_Codex任务_v2.md @@ -0,0 +1,690 @@ +# STEP-100 Source → RenderSegment 局部 Revision / Invalidation / Readiness 与独立 Fallback — Codex 任务 v2 + +## 0. 任务性质 + +这是 STEP-099 之后的正式修复任务。 + +STEP-099 已经完成纯诊断并 CLOSED / PASS,已确认 V17 的核心问题: + +1. 动态 GeoJSON Source/Tile mutation 会推进 global retained content revision; +2. 即使 R0 不依赖该 GeoJSON,R0 retained cache 仍跟着 invalid/build; +3. GeoJSON 更新期间约 200 ms 的 transient `isLoaded=false` 会通过 global `RenderOrchestrator::isLoaded()` 把整个 retained path 拉成 `style-not-loaded`; +4. 当前 renderer 还存在 `useRetainedRanges` all-or-none gate:一个 RETAINED Segment fallback 时,会使其他本来可安全 reuse 的 RETAINED Segment 一起退出 retained path; +5. STEP-099 diagnostics 已能正确给 source mutation 计算 `affectedSegmentIndices`,但该依赖关系仍只是诊断,不参与正式 invalidation。 + +本任务目标是把 **Source/Tile 变化对 RETAINED Segment 的影响正式局部化**。 + +本任务不是 V18 发布任务。Codex 只负责实现与源码级说明;Android/iOS 构建、APK、ADB、设备测试、最终 Git push、V18 release 由 RDC/ChatGPT 在 Codex 完成后独立执行。 + +未经用户明确要求,本任务 **不加入、不运行 Jest**。 + +--- + +## 1. 冻结基线 + +工作目录: + +`/Users/tei/dev/MapLibreParticleLab-step100` + +分支: + +`step100-segment-local-source-invalidation` + +开始基线必须重新核对,当前预期: + +- `origin/particle/main` +- HEAD:`c4b481fd2c5b5da20d31f6ae54011f0552d7dbd0` +- STEP-099 implementation:`8a5214d92dd2f8899db76d2194da42999fa0c87d` +- V17 frozen tag:`maplibre-particle/rn/13.4.1-particle.17` +- V17 frozen commit:`e8b451fe70054107773e82484c80e90df3889147` + +禁止修改、移动、覆盖 V17 tag / release worktree。 + +STEP-100 是 V17 之后的新开发,最终只有 RDC 验收通过后才允许进入 V18 发布。 + +开始前必须读取: + +- 当前仓库实际 HEAD / status / diff; +- 全局 Codex AGENTS; +- `ARCHITECTURE.md`(若存在); +- STEP-092 / 094 / 099 的任务、Step 记录、完成报告; +- 当前: + - `renderer_impl.cpp` + - `render_orchestrator.*` + - `retained_underlay_cache.*` + - `retained_underlay_runtime_state.hpp` + - `render_segment.*` + - STEP-099 diagnostics 与 Android fixture; +- 当前 RenderSegment / retained cache / particle fast path tests。 + +不得凭历史印象修改。 + +--- + +## 2. 最终架构原则 + +长期 RenderSegment 模型继续保持通用模型: + +```text +Style Z-order + ↓ +R / C / R / C / R / ... +``` + +其中: + +- R = RETAINED +- C = CONTINUOUS + +本任务只用 **R-C-R** 做最小验证,不把 R-C-R 固化成最终架构。 + +必须保持: + +1. Style layer 顺序是唯一 Z-order 真相; +2. Renderer 不理解 AIS / Route / Weather / GeoJSON 等业务名; +3. Source → Segment 依赖由当前实际 Style / RenderLayer / Source 关系推导; +4. 一个 Source 可依赖多个 Segment; +5. source-less layer 不制造假 dependency; +6. unknown/global dependency 必须 fail closed; +7. 相邻 Segment、多个 C、多个 R 的通用模型不能被破坏; +8. 本任务不实现 multi-continuous Fast Path。 + +--- + +## 3. 本任务必须实现的核心目标 + +### 3.1 Source/Tile mutation 局部化 + +假设: + +```text +R0 depends [staticSource] +C depends [particleField] +R1 depends [dynamicGeoJson] +``` + +当: + +```text +dynamicGeoJson mutation +``` + +正式 renderer 必须达到: + +```text +R0: + content/revision 不因该 source mutation 改变 + cache 不 invalid + cache 不 rebuild + 持续 reuse + +R1: + revision/invalidation 正常变化 + 按需要 build/reuse/fallback + +C: + 正常 continuous render +``` + +禁止继续出现: + +```text +dynamicGeoJson mutation + ↓ +global revision 变化 + ↓ +R0 + R1 一起 rebuild +``` + +### 3.2 保留 global revision 兼容信号 + +STEP-100 不要求删除现有 global `retainedUnderlayContentRevision`。 + +为了保持: + +- diagnostics; +- 现有 fast-path correctness; +- 旧 API / compatibility; +- conservative fallback; + +允许并推荐继续维护 global revision。 + +但 **RETAINED Segment cache 的正式内容 revision / invalidation 不能再只依赖这个 global source/tile revision**。 + +当前 `retainedUnderlayRevisionTracker` 同时消费 global content revision 与 `renderTreeParameters.loaded`。STEP-100 必须明确拆成两种用途: + +```text +global retained revision / tracker + -> compatibility / Fast Path / diagnostics + +segment-local retained content state + -> full CPU renderer 的每个 RETAINED Segment Build / Reuse / Fallback +``` + +**Segment-local cache content state 禁止继续直接消费:** + +- global source/tile content revision; +- global `renderTreeParameters.loaded`。 + +否则即使 source-scoped revision 已实现,R0 仍会被无关 GeoJSON transient unloaded 或 global revision 连带失效。 + +允许 global revision / global tracker继续按原逻辑变化;禁止为了让测试 PASS 而停止 global revision 增长或放宽 Fast Path。 + +### 3.3 per-RETAINED-Segment readiness + +不得修改任何 Source `isLoaded()` 定义。 + +不得修改: + +```cpp +renderTreeParameters->loaded = updateParameters->styleLoaded && isLoaded(); +``` + +这个 global loaded 仍作为 MapLibre 现有全局兼容事实。 + +但 retained segment 的 cache Build/Reuse/Fallback 必须有自己的安全 readiness 判断。 + +最低语义: + +- Style 本身未 ready:fail closed; +- image manager / sprite / glyph 等无法安全定位到单 Segment 的全局前置条件未 ready:fail closed; +- Segment 已知依赖的 source 中任一缺失或 `isLoaded()==false`:仅该 Segment fail closed; +- Segment 已知依赖的 source `hasFadingTiles()==true`:仅该 Segment视为 unstable; +- **不被该 Segment 引用的 source unloaded 或 fading,不得让该 Segment cache fallback/rebuild**; +- `retainedRangeIsStable(begin,end)` 的 source fading 检查必须按该 range 的真实 source dependency 缩小,不能继续遍历全部 `renderSources` 作为 full-CPU retained gate; +- Fast Path 原有 global fading gate 保持不变; +- source ID 无法解析、dependency 缺失或真正无法证明安全时 fail closed。 + +必须区分: + +1. **已知 Source dependency**; +2. **symbol/global prerequisite**(glyph/image/placement 等); +3. **真正 unknown dependency**。 + +STEP-099 的 `unknownDependencies` 目前会把 symbol 相关条件标为 unknown。STEP-100 不得机械地把“存在 symbol”解释成永久 fallback;symbol 所需 glyph/image/placement 应作为明确的 global prerequisite/fail-closed 条件处理。 + +因此 T3 中一个: + +```text +no layer +enabled=false +isLoaded=false +``` + +的 RasterArray source 可以继续让 global `RenderOrchestrator::isLoaded()==false`,但不得因此让与它无依赖关系的 R0/R1 cache 全部 `style-not-loaded`。 + +本 Step 的运行态硬验收范围是 **MapMode::Continuous**(粒子正式实时渲染模式)。当前非 Continuous / still 路径如果在 global unloaded 时提前返回,本 Step 不强行改变其兼容语义;完成报告必须明确该适用范围。 + +### 3.4 RETAINED Segment 独立决策 + +当前 V17 路径存在类似: + +```text +useRetainedRanges = all retained segments supported +``` + +的 all-or-none 语义。 + +STEP-100 必须改为每个 RETAINED Segment 独立拥有: + +- readiness/support decision; +- Build / Reuse / Fallback; +- content/source revision; +- invalidation reason。 + +如果: + +```text +R0 = Reuse +R1 = Fallback +``` + +则本帧必须允许: + +```text +R0 composite cached texture +C live render +R1 live render fallback +``` + +而不是退化为“整个 Style 全部 live render”。 + +如果某 retained cache 自身 build 失败,应优先只 fallback 该 Segment;只有无法证明局部安全的 context/global resource failure 才允许全 retained fail closed。 + +--- + +## 4. Source-scoped revision 设计要求 + +Codex 必须基于当前代码选择最小、稳定设计,并在完成报告说明实际方案。 + +推荐方向但不强制具体类名: + +- 为 Source/Tile mutation 维护 source-scoped monotonic revision; +- keyed by stable sourceId,但必须处理 source 删除后同 ID recreate,不能让新 source 错误继承可复用的旧 cache identity;可用 generation/epoch 或保留单调 tombstone 语义实现; +- source diff、feature-state mutation、tile changed 等所有当前会进入 Source/Tile retained content invalidation 的正式路径都必须覆盖; +- 能定位 sourceId 的 mutation 只推进对应 source revision;无法定位 sourceId 的 mutation继续 global fail closed; +- Segment content revision 由: + - 该 Segment 的 dependent source revisions; + - 当前 Segment layer fingerprint / layer mutation; + - 必须全局影响 retained 的 global revision 类别; + - topology / camera / viewport 等既有安全条件 + 共同决定。 + +Source/Tile mutation 不应推进“所有 Segment 都消费的 retained cache revision”。 + +必须避免: + +- 用屏幕位置猜 dependency; +- 在 renderer 中硬编码 source/layer ID; +- 从 STEP-099 diagnostics JSON 反向驱动正式 renderer; +- 建立 diagnostics 与 production 两套互相漂移的 dependency 真相。 + +如果需要提取 production-grade dependency helper,应让 diagnostics 和正式逻辑共享同一只读推导源。正式 renderer 不得从 diagnostics JSON 或 `RetainedUnderlayRuntimeState` 反向取 dependency。 + +Source-scoped revision 属正式 correctness state,不得沿用 STEP-099 diagnostics 的“best-effort 记录失败可忽略”语义。正式 revision/dependency state 若无法可靠更新,必须 conservative global invalidation/fail closed。 + +--- + +## 5. Global mutation 仍可保守处理 + +本任务只要求 **Source/Tile mutation** 与明确 Source readiness 局部化。 + +以下无法安全局部化的事件,本轮允许继续 invalidate/fail closed 全 retained: + +- context reset; +- style reload / topology change; +- image mutation; +- glyph mutation; +- sprite/image manager 全局 readiness; +- placement / symbol fade / glyph/image/sprite 全局状态; +- camera / transform / viewport / resize / pixel ratio; +- debug mode; +- 3D / unsupported / legacy ordering; +- 无法定位 dependency 的 mutation; +- 真正 unknown/global dependency。 + +注意:**Source 自身的 loaded / fading 不属于这里的 global fail-closed 项**;若 source dependency 可定位,必须按 Segment 局部判断。Fast Path 的 global source/fading gate除外,本轮保持原状。 + +完成报告必须列出: + +- 已局部化的 mutation 类型; +- 仍为 global 的 mutation 类型; +- 为什么保守处理。 + +--- + +## 6. Fast Path 本轮严格不改 + +STEP-099 已确认 Fast Path 仍有 global gates: + +- global update signature(sources/layers/images 等); +- global content revision; +- global fade; +- pendingChanges; +- placement/light/non-particle transition/repaint; +- annotation mutation; +- topology/boundary/revision/cache validity; +- `singleContinuousSegment`。 + +STEP-100 **禁止修改这些 gate 的语义**。 + +允许: + +- Source mutation 后 Fast Path miss; +- transient source readiness 时 Fast Path fail closed; +- full CPU renderer 被调用。 + +但进入 full CPU renderer 后,必须能做到: + +```text +R0 retained cache reuse +R1 rebuild/fallback +``` + +这正是本任务验收目标。 + +Multi-continuous Fast Path 明确不在本任务。 + +--- + +## 7. Render 顺序与混合 Build/Reuse/Fallback + +实现 per-segment mixed decisions 时必须保持原 MapLibre 绘制语义: + +- Style Z-order; +- global layerIndex; +- opaque reverse traversal; +- translucent forward traversal; +- currentLayer; +- depthRangeSize; +- legacy item 不双绘; +- debug overlay 不重复; +- retained texture composite 不改变 segment 顺序。 + +推荐的逻辑行为: + +```text +for segment in Style order: + RETAINED + Build: + build cache + composite cache + RETAINED + Reuse: + composite cache + RETAINED + Fallback: + live render only this range + CONTINUOUS: + live render this range +``` + +实际代码可按现有 RenderPass 安全实现,不要求机械照搬伪代码。 + +必须特别检查: + +- 一个 R fallback 不得导致另一个 R live redraw; +- resource prepare 在 Build 前失败时,必须先把该 Segment 本帧 decision 改为 Fallback,再进入主 buffer 绘制; +- retained build 中途失败时,必须 invalidate/标记该 Segment失败并把该 Segment 本帧 decision 改为 Fallback,禁止 composite 旧纹理; +- 如果失败意味着 context / shared GPU state 已不可信,允许全局 fail closed; +- fallback range 的 legacy items 只绘一次,不得因 per-segment 主循环重复 render; +- debug overlay 全帧只绘一次; +- Segment 间 Opaque/Translucent 语义不得被破坏; +- 相邻 R、多个 C、透明叠加与 legacy scene 不能因 mixed execution 出现漏绘/双绘。 + +推荐执行顺序:先完成各 Segment resource prepare 与必要离屏 Build;再 clear 主 buffer;最后按 Style Segment 顺序逐段选择 cached composite 或 live range render。 + +--- + +## 8. Diagnostics 必须继续可证明局部性 + +保留 STEP-099 diagnostics,并 additive 增加足够字段,使 RDC 能验证正式行为。 + +每个 RETAINED Segment 至少应能观察: + +- dependentSourceIds; +- segment readiness / support; +- segment-local source/content revision 或等价可证明值; +- Build / Reuse / Fallback 的**最终本帧 execution decision**; +- build/reuse/fallback/invalidation count; +- last invalidation/fallback reason。 + +Source mutation 继续输出: + +- sourceId; +- sequence/reason/action; +- affectedSegmentIndices。 + +要求: + +```text +step099-dynamic mutation +affected=[R1] +R0 revision unchanged +R0 cache reuse++ +R0 build unchanged +R0 invalidation unchanged +``` + +Diagnostics 仍是 pull/event low-frequency,不允许逐帧高频 spam。 + +--- + +## 9. Android Test App + +复用 STEP-099,不另建独立 App。 + +至少保留并完善: + +### T1 STATIC + +验证静态 R-C-R: + +- loaded settle; +- R0/R1 build 一次后稳定 reuse; +- 不产生持续 rebuild。 + +### T2 GEOJSON_1HZ + +两个 GeoJSON source 交错,各约 1 Hz。 + +重点: + +- global loaded transient false window 可以继续存在; +- 只引用某 source 的 retained Segment 才允许因该 source mutation 改变; +- 无关 retained Segment build/invalidation 不得跟随增长。 + +### T3 PERMANENT_UNREADY + +保留一个: + +- no layer; +- enabled=false; +- isLoaded=false; +- no field submission + +的 RasterArray source。 + +预期: + +- global renderOrchestratorLoaded 仍可为 false; +- 但无关 R0/R1 retained cache 不应因它持续 `style-not-loaded` fallback; +- 能安全判断依赖的 retained Segment应继续 build/reuse。 + +### T4 R-C-R(主验收) + +必须保持清晰依赖: + +```text +R0 -> dedicated static source only +C -> WeatherColor/Particle field +R1 -> dedicated dynamic GeoJSON source +``` + +运行动态 GeoJSON 约 1 Hz。 + +必须调整 fixture,避免 STEP-099 中 R1 再复用 R0 的 static source: + +- R0 static source 只出现在 R0; +- R1 dynamic source 只出现在 R1; +- 若 MapLibre 内建 annotation source/layer 自动进入 R1,必须在 diagnostics 中列明,并保证验收窗口内 annotation 不发生 mutation; +- 不允许用业务 ID 特判 renderer。 + +RDC 验收区间必须支持“settled baseline → 60 s pure dynamic-source mutation window → final”对比。每个 aggregate sample 同时记录 mutation source/sequence、各 Segment local revision、readiness、最终 execution decision 与 cache counters。 + +--- + +## 10. STEP-100 硬验收标准 + +### A. R0 locality + +T4 settle 后记录 baseline。 + +随后至少 60 s dynamic GeoJSON: + +- R0 `buildDelta == 0` +- R0 `invalidationDelta == 0` +- R0 `reuseDelta > 0` +- R0 segment-local revision/content state 不因 R1 dynamic source mutation 改变 +- 即使某采样点 global `renderOrchestratorLoaded=false`,只要 R0 自身 prerequisites/dependencies ready,R0 final decision 仍可为 Reuse + +如果为了安全出现明确 global event,应在日志中可解释并重新选择纯动态 mutation 区间;不能把 global event 混入后声称 PASS。 + +### B. R1 reacts + +同一时间窗: + +- R1 必须能观察到 source mutation; +- R1 revision / build / invalidation / fallback 中至少有与动态 source 对应的实际变化; +- `affectedSegmentIndices` 必须只包含真实依赖 Segment。 + +### C. global revision compatibility + +- global `retainedUnderlayContentRevision` 允许并预期继续增长; +- 不能通过停止 global revision 增长来制造 R0 PASS。 + +### D. readiness locality + +T2 transient unloaded 或 T3 permanent unrelated unloaded 时: + +- global loaded 可 false; +- R0 不得仅因无关 source 的 unloaded 而 invalid/rebuild/fallback; +- R0 cache 必须继续可复用。 + +### E. correctness + +- 无 crash / ANR; +- 无 stale cache composite; +- 无 Segment Z-order 错乱; +- 无 fallback range 双绘; +- Cache OFF 仍安全走 live renderer; +- plan invalid 仍 fail closed。 + +任一硬门槛失败,STEP-100 不得宣布 PASS,不得进入 V18。 + +--- + +## 11. Native 非 Jest 测试要求 + +Codex 必须新增或扩展现有 C++ 测试源码,至少覆盖: + +1. source A mutation 只改变依赖 A 的 Segment revision; +2. source A 被多个 Segment 共享时,对应多个 Segment 都变化; +3. unrelated Segment revision 不变; +4. source-less Segment 不制造 dependency; +5. unknown/global dependency fail closed; +6. unrelated source unloaded 不使安全 Segment readiness false; +7. dependent source unloaded 使对应 Segment readiness false; +8. R0 Reuse + R1 Fallback 的 mixed decision; +9. 单 Segment build failure 不 composite stale cache; +10. query diagnostics 不改变 revision/cache state; +11. global non-source invalidation 仍影响所有 retained; +12. source 删除后同 ID recreate 不误复用旧 Segment cache identity; +13. dependent source fading 只影响对应 Segment;unrelated source fading 不影响安全 Segment; +14. Cache OFF / invalid plan 行为不回归; +15. 至少有一组能证明实际 mixed render 没有漏绘/双绘的 renderer-level draw-count、pixel 或现有等价测试入口;不能只用 decision unit test 代替。 + +不得修改测试断言去适应错误实现。 + +--- + +## 12. 允许修改范围 + +允许: + +- RenderOrchestrator source mutation/readiness bookkeeping; +- RenderSegment dependency production helper; +- Renderer retained per-segment revision/readiness/decision; +- RetainedUnderlayCache/RuntimeState 为局部状态增加必要字段; +- additive diagnostics JSON/TS; +- Android Validation Test App STEP-099 fixture/log; +- 相关 Native C++ tests; +- STEP-100 Step 记录/完成报告/开发记录。 + +禁止: + +- NavSea; +- Integration; +- V17 tag/release worktree; +- Source `isLoaded()` 定义; +- Particle field/provider/network; +- Fast Path gate semantic; +- `singleContinuousSegment`; +- multi-continuous Fast Path; +- 自动改变 Style Z-order; +- provider/business-specific source/layer ID; +- unrelated cleanup/refactor; +- Jest。 + +--- + +## 13. Codex 执行边界 + +Codex 本轮负责: + +1. 读取实际源码和任务书; +2. 实现代码; +3. 编写 Native 非 Jest 测试源码; +4. 更新 Android Test App 验证入口; +5. 执行轻量静态检查,如 `git diff --check`; +6. 生成中文完成报告。 + +Codex **不得执行**: + +- Android Gradle / APK; +- ADB / 安装 / 实机; +- Xcode / iOS device; +- V18 release; +- tag; +- push; +- Jest。 + +本轮 Codex 完成报告固定: + +`docs/报告/STEP-100_SourceScoped_RenderSegment局部失效_Codex完成报告_v1.md` + +Step 记录固定: + +`docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md` + +完成报告必须写清: + +- 实际 Codex model / reasoning effort; +- start HEAD; +- 修改文件; +- 生产设计; +- source revision/readiness 规则; +- mixed retained decision 规则; +- Fast Path 明确未改; +- 测试源码新增情况; +- 实际执行 / NOT RUN; +- 风险与 RDC 验收点; +- 未 commit / 未 push(如果确实如此)。 + +Codex 不 commit、不 push。由 RDC 验证通过后统一 Git 收尾。 + +--- + +## 14. RDC / ChatGPT 后续独立验收 + +Codex 返回后由 RDC 负责: + +1. source/diff 审计; +2. Native Mac 编译; +3. Native focused + broad related C++ tests; +4. Android x86_64 OpenGL validation APK 单次构建; +5. Mac ADB 直连 FZG1-2; +6. 自动跑 T1/T2/T3/T4; +7. 解析 R0/R1 build/reuse/invalidation/revision/readiness; +8. 确认硬验收标准 A~E; +9. 若失败,回到 STEP-100 修复,不发布; +10. 若全部 PASS,完成 STEP-100 Git push; +11. 再按现有统一发行流程升级到: + `maplibre-particle/rn/13.4.1-particle.18` +12. V18 build/release/public artifact 验证通过后才宣布 V18 发布完成。 + +--- + +## 15. 停止条件 + +Codex 遇到以下任一情况必须停止并报告,不得扩大范围: + +- 要修改 Source `isLoaded()` 才能继续; +- 需要放宽 Fast Path gate; +- 必须改变 singleContinuous / multi-continuous; +- production dependency 无法可靠从现有 Style/RenderLayer/Source 推导; +- per-segment fallback 会破坏 RenderPass/Z-order 且无法在当前范围安全解决; +- 发现需要 NavSea/Integration 配合; +- 需要业务 layer/source ID; +- 需要改变 V17 release。 + +--- + +## 16. 本任务最终原则 + +STEP-100 的目标不是让“全局世界看起来 loaded”。 + +目标是: + +> **即使某个 GeoJSON Source 正在变化或短暂 unready,只要 R0 与它没有依赖关系,R0 retained cache 就必须继续安全 reuse,不能被迫一起刷新。** + +Fast Path 是否还能 miss,是后续独立问题。 + +只有这一点经过 Native + Android R-C-R 实测成立,才允许进入 V18 发布。 diff --git a/docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v1.md b/docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v1.md new file mode 100644 index 000000000..9f9db984e --- /dev/null +++ b/docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v1.md @@ -0,0 +1,405 @@ +# STEP-101 RenderOrchestrator teardown 生命周期修复 — Codex 任务 v1 + +## 0. 任务目的 + +修复 Particle V18 Android 在正常 Renderer teardown / MapView reset 时发生的 Native SIGABRT。 + +现场已在 NavSea Android x86_64 设备稳定复现: + +```text +Fatal signal 6 (SIGABRT) +thread: RenderThread 11 + +FORTIFY: +pthread_mutex_lock called on a destroyed mutex + +libmaplibre.so +std::__ndk1::mutex::lock() +... +mbgl::android::MapRenderer::resetRenderer() +``` + +V17 正常,V18 接入后稳定在启动约 9~11 秒触发。 + +本任务只修 Particle Native 生命周期;禁止在 NavSea 增加绕路、private wrapper、禁止阻止正常 MapView reset。 + +未经用户明确要求:不加入、不执行 Jest。 + +--- + +## 1. 冻结基线 + +工作树: + +`/Users/tei/dev/MapLibreParticleLab-step101` + +分支: + +`step101-render-orchestrator-teardown` + +起点必须是: + +`origin/particle/main = d29e84c1a90daeea104b389d28b000c4f017d08d` + +即正式 V18 Final HEAD。 + +V18 已发布,不得覆盖、重发或移动: + +- `maplibre-particle/rn/13.4.1-particle.18` +- Android `13.4.1-particle.18` +- iOS `6.28.0-particle.18` + +修复通过后由 RDC/ChatGPT 发布新不可变版本 V19。 + +--- + +## 2. 已知根因证据 + +V18 STEP-100 新增: + +```cpp +RenderSegmentRevisionBook retainedSegmentRevisions; +mutable std::mutex readinessDiagnosticsMutex; +uint64_t readinessEventSequence; +std::unordered_map sourceReadinessEvents; +RenderMutationDiagnostics lastReadinessMutation; +``` + +多个 RenderSource / Tile observer callback 会进入: + +```cpp +advanceRetainedSourceRevision(...) +recordReadinessMutation(...) +onTileChanged(...) +onTileAction(...) +buildReadinessDiagnostics(...) +retainedSegmentSourceState(...) +``` + +并 lock: + +```cpp +readinessDiagnosticsMutex +``` + +V18 当前成员声明中: + +```text +renderSources +renderLayers +... +retainedSegmentRevisions +... +readinessDiagnosticsMutex +sourceReadinessEvents +... +threadPool +``` + +C++ 成员按声明反序析构,因此 readiness state / mutex 会早于 renderSources 销毁。 + +而 RenderSource 在创建时: + +```cpp +renderSource->setObserver(this); +``` + +observer 指向 RenderOrchestrator。 + +当前 `RenderOrchestrator::~RenderOrchestrator()` 只: + +1. contextLost 时标记 layer; +2. `threadPool.waitForEmpty()`; + +没有在 readiness state 仍有效时显式 detach RenderSource observer / 清理 renderSources。 + +因此 V18 存在明确生命周期危险: + +```text +readinessDiagnosticsMutex destroyed + ↓ +renderSources still destroying/alive + ↓ +late tile/source callback + ↓ +RenderOrchestrator::onTile* + ↓ +lock destroyed mutex + ↓ +FORTIFY SIGABRT +``` + +--- + +## 3. 修复原则 + +不要只依赖调整成员声明顺序。 + +本任务应建立显式 teardown contract: + +```text +RenderOrchestrator destructor begins + ↓ +stop / drain deferred scheduler work + ↓ +detach observer from every RenderSource + ↓ +destroy/clear renderSources while all orchestrator state is still alive + ↓ +only then continue member teardown +``` + +同时 `RenderSource::setObserver(nullptr)` 必须安全恢复到 NullObserver。 + +目标是即使未来成员声明顺序变化,Source callback 也不能在 owner teardown 后访问 RenderOrchestrator。 + +--- + +## 4. 必做实现 + +### 4.1 RenderSource nullptr observer 语义 + +当前: + +```cpp +void RenderSource::setObserver(RenderSourceObserver* observer_) { + observer = observer_; +} +``` + +改成与 Style Source / Layer 一致的安全语义: + +```cpp +observer = observer_ ? observer_ : &nullObserver; +``` + +要求: + +- constructor 仍默认 NullObserver; +- `setObserver(nullptr)` 是合法 detach; +- onTileChanged / onTileError / onTileAction 不需要加 null 分支; +- 不吞掉正常有 observer 时的回调。 + +必须加非 Jest C++ 回归测试验证 nullptr detach 后 callback 不 crash、不调用旧 observer。 + +### 4.2 RenderOrchestrator 显式 teardown + +在 `RenderOrchestrator::~RenderOrchestrator()` 中,在 readiness mutex/state 仍确定存活时: + +1. 保留现有 contextLost layer handling; +2. 等待已有 deferred work 完成; +3. 对所有 `renderSources` 执行 observer detach; +4. 显式 `renderSources.clear()`; +5. 必要时对 glyph/image observer 也做已有 API 支持的 detach,但不要扩大范围; +6. 不允许 teardown callback 再进入 RenderOrchestrator source callback。 + +顺序必须结合当前 TaggedScheduler / RenderSource / TilePyramid 的真实生命周期检查,而不是机械照抄上述编号。 + +如果存在“clear renderSources 本身可能产生新的 scheduler work”,需要设计第二次 drain 或更安全顺序,并在报告解释。 + +### 4.3 生命周期状态可选保护 + +如果仅 detach + explicit clear 仍无法证明并发安全,允许增加最小 teardown flag,例如 atomic bool,但只作为二次防线。 + +禁止用 flag 代替 observer detach。 + +禁止 catch FORTIFY;destroyed mutex abort 不是 C++ exception。 + +--- + +## 5. 不得修改 + +- STEP-100 Source→Segment locality 语义; +- per-segment revision; +- per-segment readiness; +- mixed Build/Reuse/Fallback; +- Fast Path gate; +- singleContinuousSegment; +- Source::isLoaded; +- Style Z-order; +- provider/network; +- NavSea; +- MapRenderer::resetRenderer 正常生命周期; +- Android Activity 为避免 reset 而加 workaround。 + +--- + +## 6. 必做测试 + +### 6.1 Native 非 Jest + +至少覆盖: + +1. RenderSource 默认 NullObserver 可安全接 callback; +2. setObserver(real) 正常转发; +3. setObserver(nullptr) 后不再调用旧 observer; +4. detach 后 onTileChanged / onTileError / onTileAction 安全; +5. RenderOrchestrator teardown 有至少一个含 RenderSource 的场景; +6. source callback 与 teardown 边界不访问已销毁 readiness state; +7. STEP-100 locality 相关 focused tests 不回归。 + +如果现有测试设施不方便直接构造完整 RenderOrchestrator teardown,允许提取最小可测试 teardown helper,但禁止为了测试重构大范围生产结构。 + +### 6.2 Android Test App + +在现有 ParticleCoreLayerValidationActivity / Validation App 增加专门 STEP-101 teardown 场景。 + +场景必须真实触发 Native Renderer teardown,而不是只 clear field。 + +至少提供: + +```text +T1 RESET_ONCE +T2 RESET_LOOP_20 +T3 RESET_WITH_DYNAMIC_GEOJSON +T4 RESET_WITH_DIAGNOSTICS +``` + +目标: + +- recreate/destroy MapView 或等价真实 renderer.reset; +- 每轮等待足够让 source/tile work 进入正常运行; +- T3 同时保持动态 GeoJSON/source mutation; +- T4 在 reset 前调用一次 RendererRuntime diagnostics,模拟 NavSea probe 时序; +- 20 次循环无 SIGABRT / crash / ANR; +- reset 后新 renderer 能继续正常显示 / 获取 diagnostics。 + +不得通过延迟 reset、禁用 observer、关闭 GeoJSON 来逃避问题。 + +--- + +## 7. NavSea 现场等价验收 + +RDC 在 Codex 完成后独立执行。 + +Android 设备: + +`192.168.200.139:5555` + +必须显式 `adb -s`。 + +最终至少执行: + +1. Validation App STEP-101 T1~T4; +2. crash buffer 检查; +3. 20 次 reset/recreate; +4. 安装 V19 进 NavSea 后做真实启动; +5. 保留 NavSea 当前 `key={map-style-generation-...}` 正常 reset 触发条件; +6. 保留/启用一次 RenderSegment diagnostics probe A/B; +7. 连续至少 5 次冷启动,每次观察 >30 秒; +8. 不允许出现: + - `pthread_mutex_lock called on a destroyed mutex` + - SIGABRT RenderThread + - `MapRenderer::resetRenderer` teardown crash。 + +--- + +## 8. STEP-100 回归验收 + +修复 teardown 后必须确认 V18 的核心 locality 没被破坏。 + +至少复测 T4 R-C-R: + +无关 R0: + +```text +buildDelta = 0 +invalidationDelta = 0 +reuseDelta > 0 +``` + +动态 R1 正常 build/invalidation。 + +禁止通过删除 mutex/readiness/revision 功能来修 crash。 + +--- + +## 9. Codex 执行边界 + +Codex负责: + +- 只读检查真实 V18 代码; +- 实现最小 Native 修复; +- 编写/更新非 Jest C++ tests; +- 更新 Android Validation STEP-101 场景; +- 静态检查; +- 生成报告。 + +Codex不负责: + +- NavSea 修改; +- V19 release; +- tag; +- publish; +- 覆盖 V18; +- Jest。 + +Mac Native 开发/编译原则保持;不要使用 Linux Native 环境。 + +--- + +## 10. 完成报告 + +生成: + +`docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md` + +Step 记录: + +`docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md` + +报告必须写: + +- root cause; +- start HEAD; +- 修改文件; +- destructor teardown 顺序; +- observer detach 语义; +- 为什么不依赖成员声明顺序; +- tests added; +- 实际运行 / NOT RUN; +- STEP-100 locality 是否保持; +- 风险; +- 未发布 V19(Codex 阶段)。 + +Codex阶段不 push,由 RDC 验收后统一 commit/push。 + +--- + +## 11. V19 发布门槛 + +仅当: + +- Native compile PASS; +- focused C++ tests PASS; +- Android Validation teardown loop PASS; +- STEP-100 locality regression PASS; +- NavSea V19 真实 5 次冷启动 >30s PASS; +- crash buffer 无 destroyed mutex; + +才允许发布: + +- RN / Android:`13.4.1-particle.19` +- iOS:`6.28.0-particle.19` +- tag:`maplibre-particle/rn/13.4.1-particle.19` + +V18 保持不可变,不覆盖、不删除。 + +--- + +## 12. 最终原则 + +正常: + +```text +MapView unmount +Style generation replacement +renderer.reset() +Activity recreate +``` + +都是合法生命周期。 + +Particle Native 必须做到: + +> Renderer teardown 永远不能因为 observer late callback 访问已销毁 mutex/state 而杀掉宿主 App。 diff --git a/docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v2.md b/docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v2.md new file mode 100644 index 000000000..ee36696a3 --- /dev/null +++ b/docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v2.md @@ -0,0 +1,510 @@ +# STEP-101 RenderOrchestrator teardown 生命周期修复 — Codex 任务 v1 + +## 0. 任务目的 + +修复 Particle V18 Android 在正常 Renderer teardown / MapView reset 时发生的 Native SIGABRT。 + +现场已在 NavSea Android x86_64 设备稳定复现: + +```text +Fatal signal 6 (SIGABRT) +thread: RenderThread 11 + +FORTIFY: +pthread_mutex_lock called on a destroyed mutex + +libmaplibre.so +std::__ndk1::mutex::lock() +... +mbgl::android::MapRenderer::resetRenderer() +``` + +V17 正常,V18 接入后稳定在启动约 9~11 秒触发。 + +本任务只修 Particle Native 生命周期;禁止在 NavSea 增加绕路、private wrapper、禁止阻止正常 MapView reset。 + +未经用户明确要求:不加入、不执行 Jest。 + +--- + +## 1. 冻结基线 + +工作树: + +`/Users/tei/dev/MapLibreParticleLab-step101` + +分支: + +`step101-render-orchestrator-teardown` + +起点必须是: + +`origin/particle/main = d29e84c1a90daeea104b389d28b000c4f017d08d` + +即正式 V18 Final HEAD。 + +V18 已发布,不得覆盖、重发或移动: + +- `maplibre-particle/rn/13.4.1-particle.18` +- Android `13.4.1-particle.18` +- iOS `6.28.0-particle.18` + +修复通过后由 RDC/ChatGPT 发布新不可变版本 V19。 + +--- + +## 2. 已知根因证据 + +V18 STEP-100 新增: + +```cpp +RenderSegmentRevisionBook retainedSegmentRevisions; +mutable std::mutex readinessDiagnosticsMutex; +uint64_t readinessEventSequence; +std::unordered_map sourceReadinessEvents; +RenderMutationDiagnostics lastReadinessMutation; +``` + +多个 RenderSource / Tile observer callback 会进入: + +```cpp +advanceRetainedSourceRevision(...) +recordReadinessMutation(...) +onTileChanged(...) +onTileAction(...) +buildReadinessDiagnostics(...) +retainedSegmentSourceState(...) +``` + +并 lock: + +```cpp +readinessDiagnosticsMutex +``` + +V18 当前成员声明中: + +```text +renderSources +renderLayers +... +retainedSegmentRevisions +... +readinessDiagnosticsMutex +sourceReadinessEvents +... +threadPool +``` + +C++ 成员按声明反序析构,因此 readiness state / mutex 会早于 renderSources 销毁。 + +而 RenderSource 在创建时: + +```cpp +renderSource->setObserver(this); +``` + +observer 指向 RenderOrchestrator。 + +当前 `RenderOrchestrator::~RenderOrchestrator()` 只: + +1. contextLost 时标记 layer; +2. `threadPool.waitForEmpty()`; + +没有在 readiness state 仍有效时显式 detach RenderSource observer / 清理 renderSources。 + +因此 V18 存在明确生命周期危险: + +```text +readinessDiagnosticsMutex destroyed + ↓ +renderSources still destroying/alive + ↓ +late tile/source callback + ↓ +RenderOrchestrator::onTile* + ↓ +lock destroyed mutex + ↓ +FORTIFY SIGABRT +``` + +--- + +## 3. 修复原则 + +不要只依赖调整成员声明顺序。 + +本任务应建立显式 teardown contract: + +```text +RenderOrchestrator destructor begins + ↓ +stop / drain deferred scheduler work + ↓ +detach observer from every RenderSource + ↓ +destroy/clear renderSources while all orchestrator state is still alive + ↓ +only then continue member teardown +``` + +同时 `RenderSource::setObserver(nullptr)` 必须安全恢复到 NullObserver。 + +目标是即使未来成员声明顺序变化,Source callback 也不能在 owner teardown 后访问 RenderOrchestrator。 + +--- + +## 4. 必做实现 + +### 4.1 RenderSource nullptr observer 语义 + +当前: + +```cpp +void RenderSource::setObserver(RenderSourceObserver* observer_) { + observer = observer_; +} +``` + +改成与 Style Source / Layer 一致的安全语义: + +```cpp +observer = observer_ ? observer_ : &nullObserver; +``` + +要求: + +- constructor 仍默认 NullObserver; +- `setObserver(nullptr)` 是合法 detach; +- onTileChanged / onTileError / onTileAction 不需要加 null 分支; +- 不吞掉正常有 observer 时的回调。 + +必须加非 Jest C++ 回归测试验证 nullptr detach 后 callback 不 crash、不调用旧 observer。 + +### 4.2 RenderOrchestrator 显式 teardown + +在 `RenderOrchestrator::~RenderOrchestrator()` 中,在 readiness mutex/state 仍确定存活时: + +1. 保留现有 contextLost layer handling; +2. 等待已有 deferred work 完成; +3. 对所有 `renderSources` 执行 observer detach; +4. 显式 `renderSources.clear()`; +5. 必要时对 glyph/image observer 也做已有 API 支持的 detach,但不要扩大范围; +6. 不允许 teardown callback 再进入 RenderOrchestrator source callback。 + +顺序必须结合当前 TaggedScheduler / RenderSource / TilePyramid 的真实生命周期检查,而不是机械照抄上述编号。 + +如果存在“clear renderSources 本身可能产生新的 scheduler work”,需要设计第二次 drain 或更安全顺序,并在报告解释。 + +### 4.3 生命周期状态可选保护 + +如果仅 detach + explicit clear 仍无法证明并发安全,允许增加最小 teardown flag,例如 atomic bool,但只作为二次防线。 + +禁止用 flag 代替 observer detach。 + +禁止 catch FORTIFY;destroyed mutex abort 不是 C++ exception。 + +--- + +## 5. 不得修改 + +- STEP-100 Source→Segment locality 语义; +- per-segment revision; +- per-segment readiness; +- mixed Build/Reuse/Fallback; +- Fast Path gate; +- singleContinuousSegment; +- Source::isLoaded; +- Style Z-order; +- provider/network; +- NavSea; +- MapRenderer::resetRenderer 正常生命周期; +- Android Activity 为避免 reset 而加 workaround。 + +--- + +## 6. 必做测试 + +### 6.1 Native 非 Jest + +至少覆盖: + +1. RenderSource 默认 NullObserver 可安全接 callback; +2. setObserver(real) 正常转发; +3. setObserver(nullptr) 后不再调用旧 observer; +4. detach 后 onTileChanged / onTileError / onTileAction 安全; +5. RenderOrchestrator teardown 有至少一个含 RenderSource 的场景; +6. source callback 与 teardown 边界不访问已销毁 readiness state; +7. STEP-100 locality 相关 focused tests 不回归。 + +如果现有测试设施不方便直接构造完整 RenderOrchestrator teardown,允许提取最小可测试 teardown helper,但禁止为了测试重构大范围生产结构。 + +### 6.2 Android Test App + +在现有 ParticleCoreLayerValidationActivity / Validation App 增加专门 STEP-101 teardown 场景。 + +场景必须真实触发 Native Renderer teardown,而不是只 clear field。 + +至少提供: + +```text +T1 RESET_ONCE +T2 RESET_LOOP_20 +T3 RESET_WITH_DYNAMIC_GEOJSON +T4 RESET_WITH_DIAGNOSTICS +``` + +目标: + +- recreate/destroy MapView 或等价真实 renderer.reset; +- 每轮等待足够让 source/tile work 进入正常运行; +- T3 同时保持动态 GeoJSON/source mutation; +- T4 在 reset 前调用一次 RendererRuntime diagnostics,模拟 NavSea probe 时序; +- 20 次循环无 SIGABRT / crash / ANR; +- reset 后新 renderer 能继续正常显示 / 获取 diagnostics。 + +不得通过延迟 reset、禁用 observer、关闭 GeoJSON 来逃避问题。 + +--- + +## 7. NavSea 现场等价验收 + +RDC 在 Codex 完成后独立执行。 + +Android 设备: + +`192.168.200.139:5555` + +必须显式 `adb -s`。 + +最终至少执行: + +1. Validation App STEP-101 T1~T4; +2. crash buffer 检查; +3. 20 次 reset/recreate; +4. 安装 V19 进 NavSea 后做真实启动; +5. 保留 NavSea 当前 `key={map-style-generation-...}` 正常 reset 触发条件; +6. 保留/启用一次 RenderSegment diagnostics probe A/B; +7. 连续至少 5 次冷启动,每次观察 >30 秒; +8. 不允许出现: + - `pthread_mutex_lock called on a destroyed mutex` + - SIGABRT RenderThread + - `MapRenderer::resetRenderer` teardown crash。 + +--- + +## 8. STEP-100 回归验收 + +修复 teardown 后必须确认 V18 的核心 locality 没被破坏。 + +至少复测 T4 R-C-R: + +无关 R0: + +```text +buildDelta = 0 +invalidationDelta = 0 +reuseDelta > 0 +``` + +动态 R1 正常 build/invalidation。 + +禁止通过删除 mutex/readiness/revision 功能来修 crash。 + +--- + +## 9. Codex 执行边界 + +Codex负责: + +- 只读检查真实 V18 代码; +- 实现最小 Native 修复; +- 编写/更新非 Jest C++ tests; +- 更新 Android Validation STEP-101 场景; +- 静态检查; +- 生成报告。 + +Codex不负责: + +- NavSea 修改; +- V19 release; +- tag; +- publish; +- 覆盖 V18; +- Jest。 + +Mac Native 开发/编译原则保持;不要使用 Linux Native 环境。 + +--- + +## 10. 完成报告 + +生成: + +`docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md` + +Step 记录: + +`docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md` + +报告必须写: + +- root cause; +- start HEAD; +- 修改文件; +- destructor teardown 顺序; +- observer detach 语义; +- 为什么不依赖成员声明顺序; +- tests added; +- 实际运行 / NOT RUN; +- STEP-100 locality 是否保持; +- 风险; +- 未发布 V19(Codex 阶段)。 + +Codex阶段不 push,由 RDC 验收后统一 commit/push。 + +--- + +## 11. V19 发布门槛 + +仅当: + +- Native compile PASS; +- focused C++ tests PASS; +- Android Validation teardown loop PASS; +- STEP-100 locality regression PASS; +- NavSea V19 真实 5 次冷启动 >30s PASS; +- crash buffer 无 destroyed mutex; + +才允许发布: + +- RN / Android:`13.4.1-particle.19` +- iOS:`6.28.0-particle.19` +- tag:`maplibre-particle/rn/13.4.1-particle.19` + +V18 保持不可变,不覆盖、不删除。 + +--- + +## 12. 最终原则 + +正常: + +```text +MapView unmount +Style generation replacement +renderer.reset() +Activity recreate +``` + +都是合法生命周期。 + +Particle Native 必须做到: + +> Renderer teardown 永远不能因为 observer late callback 访问已销毁 mutex/state 而杀掉宿主 App。 + +--- + +# 13. Codex 6-Sol High 架构评审修订(v2,优先级高于前文冲突表述) + +只读评审结论:`APPROVE_WITH_CHANGES`。 + +## 13.1 根因表述修正 + +当前已经**代码层证实**存在生命周期漏洞:`readinessDiagnosticsMutex` / readiness state 的析构顺序早于 `renderSources`,而 `RenderSource` observer 仍指向 `RenderOrchestrator`;source/tile teardown 可同步或延迟回调进入 `onTileAction/onTileChanged` 并访问 mutex。 + +该漏洞与现场 `pthread_mutex_lock called on a destroyed mutex` 高度吻合;在没有完整符号化 crash stack 精确到具体 callback 前,不把“唯一现场根因”写成数学意义上的唯一证明。 + +## 13.2 teardown 必须双 drain + +最低安全顺序调整为: + +```text +contextLost layer handling + ↓ +第一次 threadPool.waitForEmpty() + ↓ +detach RenderSource observers + ↓ +detach GlyphManager / ImageManager observers + ↓ +renderSources.clear() // readiness mutex/state 此时仍存活 + ↓ +第二次 threadPool.waitForEmpty() + ↓ +继续既有 backend/context render-job teardown + ↓ +成员析构 +``` + +原因:source/tile destruction 本身可能投递新的 tagged cleanup/deferred work,第一次 drain 不能覆盖 clear 过程中产生的新任务。 + +同时必须审计并在报告说明:`TaggedScheduler::waitForEmpty()` 不等于清空 `runOnRenderThread` 队列;后者依赖现有 backend/context render-job 生命周期。不得声称双 drain 自动证明所有 render-thread job 已清空。 + +## 13.3 Observer detach 范围 + +除了所有 `RenderSource`: + +- `glyphManager->setObserver(nullptr)`; +- `imageManager->setObserver(nullptr)`; + +也应在 owner teardown 时显式 detach(前提是现有 API 的 nullptr 已安全回 NullObserver)。 + +`RenderSource::setObserver(nullptr)` 必须改为 NullObserver fallback。 + +不得直接给 `TilePyramid` / `Tile` observer 写 nullptr,除非其 API 本身有同等安全语义;本任务不扩展到这一步。 + +## 13.4 teardown flag + +默认不增加 teardown atomic flag。 + +若实现审计发现真实并发 callback 不受 renderer thread / tagged scheduler 串行保证约束,必须先报告;atomic flag 只能二次防护,不能替代 observer detach、drain 和可等待取消。 + +## 13.5 Android Validation 证据升级 + +T1~T4 每一轮必须能证明发生了**真实 Native renderer reset**: + +- 旧 MapView detach / SurfaceView detach; +- `nativeReset()`; +- `MapRenderer::resetRenderer()` 进入/完成; +- 新 renderer style ready; +- 新 renderer diagnostics 可用。 + +不能把 `setStyle()`、clear field、只读 diagnostics 当成 teardown 测试。 + +推荐按代次销毁并重建 MapView,或 Activity recreate;T2 至少 20 次已确认 reset。 + +T3:每轮 reset 前保持动态 GeoJSON/source mutation。 + +T4:每轮 reset 前调用一次 Native diagnostics,模拟 NavSea 8 秒 probe 的等价条件。 + +每轮应等待 source/tile 确实进入运行态,而不是只按固定 sleep 机械计数。 + +## 13.6 测试必须覆盖真实 source teardown + +空 `RenderOrchestrator` 析构测试不够。 + +非 Jest C++ 测试至少需要一个含 RenderSource / tile callback 的 teardown 场景,使 source cancellation / callback 能在销毁边界发生,并验证旧 observer 不再收到回调。 + +## 13.7 正常 source erase / clearData 审计 + +必须只读检查 `clearData()` 和正常 source erase/replacement 路径,因为它们也会销毁 RenderSource。 + +本任务主要修 owner teardown;不得未经证明把 destructor 修复泛化为“所有 source replacement 异步问题均已解决”。若发现相同 observer 生命周期漏洞在正常 erase 路径存在,应做最小同源修复或明确 BLOCK,不得隐藏。 + +## 13.8 V19 候选验收顺序 + +V19 发布门槛修正为: + +```text +STEP-101 source code PASS +→ 构建未公开的 V19 candidate artifacts +→ Validation App 实机 PASS +→ NavSea 使用 candidate 做 5 次冷启动/teardown 验收 +→ 公开发布不可变 V19 +→ public readback +→ finalize/tag +``` + +不得先公开 V19 再做 NavSea 验收。 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/40_任务/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex任务_v1.md b/docs/40_任务/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex任务_v1.md new file mode 100644 index 000000000..89ba8387a --- /dev/null +++ b/docs/40_任务/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex任务_v1.md @@ -0,0 +1,39 @@ +# UPSTREAM-001 MapLibre 官方升级兼容性架构审计(只读代码,2026-10-09) + +## 授权与模式 +- 用户明确授权 Mac 使用 **GPT-6 Astra High** 做以未来跟随官方升级为目的的架构审计。 +- Codex 执行器须显式指定 `gpt-6-astra`、`model_reasoning_effort=high`,实际配置从 Codex JSONL 开始事件核实;如不一致,报告 BLOCKED,不能伪称。 +- 只读源代码。仅允许生成/修改本任务书和 `docs/报告/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex完成报告_v1.md`;禁止改 C++、RN、Java、ObjC、构建脚本和依赖。 +- 仓库为独立干净审计 worktree,基于正式远端 `origin/particle/main` 的 `.21`,不可操作其他已有脏工作树。无需构建 APK、AAR、Xcode,无需 Jest。不能伪造运行验收。 +- 不修改 Integration COORD/RFC/ADR/Contract;如建议跨平台契约变化仅列出待协调项。 + +## 权威输入 +- `/root/sourceserver/navsea-particle-integration` 为 Linux Integration;已由 RDC 同步到 `80667d6c343411d03a760425438e3d292f452118`。Mac 上可读取到的合同以已存在文件为准,不要猜测。 +- 阅读本仓库 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/10_架构/20_粒子系统总体架构设计_v1.md`、`docs/00_项目总则/粒子系统项目规则与设计决策_v1.md`、`docs/30_设计/STEP-094_通用RenderSegment模型与Style分类Diagnostics设计_v1.md` 和相关 STEP-098/099/100/101/102/103 实现记录。 +- 自定义现行发布:`630ec657687bad4200c059bbccbb950d9c6f7ce9` (`13.4.1-particle.21`)。 +- 初始 upstream Core:`18545ca10d296704b2cba675b182ff2c8da24588` (见 `PARTICLE_BASELINE.md`);验证实际 merge-base。 +- 官方 MapLibre Native Android `android-v13.6.1` peeled commit `c7506d6aff3800e946f89e1a51432fc22d1a6534`。 +- 官方 MapLibre Native iOS `ios-v6.31.0` peeled commit `01791ad02c1bfe3b22c42a97619b972aba962f82`。 +- 官方 MapLibre React Native `v11.5.0`;现有 Particle RN `11.3.6` peer/dev 锁及 Android AAR replacement、iOS SPM/Podspec 需独立审计。不能混淆 RN 11.5.0 与 Native 13.6.1。 + +## 必查问题 +1. 原始目标“官方可持续升级、自研粒子和 RenderSegment 可重复移植”离目前真实代码有多远?用逐文件证据分类:**纯新增扩展**、**不可避免的官方接入 hook**、**可外提的官方修改**、**深耦合必须重构**,另列构建/平台桥接。不要用“62 个修改文件 = 62 个冲突”。 +2. 在 `git diff` 与 `git merge-tree` dry-run 基础上,对 Android 13.6.1 和 iOS 6.31.0 分别量化冲突文件与交叉文件、可自动合并与疑似行为冲突。绝不可真实 merge/rebase 任何生产分支。优先核对 `renderer_impl.cpp`、`render_orchestrator.cpp`、`render_target.cpp`、`render_layer.hpp`、`render_source.cpp`、`paint_parameters.cpp`、RenderSegment/Retained cache、GPU backend、Style/Layer factory、JNI/Android/iOS 对接代码。 +3. 以源代码里的函数/类名证明现有 Particle Core/Field/WeatherColorLayer/GPU 模块是否独立;列出真正只需 hook 的文件和未来易碎调用。用修复方案区分 thin hook、抽象接口、适配器、source-to-segment dependency、offscreen target/placement 等,不得建议一刀切“重写”。 +4. RenderSegment 与 Particle Fast Path 的数据依赖、分层失效、Style 顺序、Opaque/Translucent、Placement、Context loss、资源所有权、Top/Bottom Retained、multi-segment 未来兼容约束。区分已实现功能和目前 fail-closed 限制。 +5. 核查 `packages/maplibre-particle-rn` 的 `package.json`、`app.plugin.js`、`MapLibreParticleRN.podspec`、Native API 等;给出 RN 11.5.0 受官方 Android 13.6.1/iOS 6.31.0 依赖推动的单 SDK 依赖收敛风险、代码适配点、功能验证清单。 +6. 准备升级顺序和可回滚 Gate:什么必须先重构、什么可以直接合并、哪些要不同平台分别验证,什么情况下必须停止并回 Integration 新 RFC/Contract。建议未来 upstream 版本跟踪工作流和“patch footprint budget”;明确对长期维护代价影响。 +7. 逐项目给出 P0/P1/P2 表,包含路径、当前耦合证据、方案、风险、工作量相对估计、验收标准、Android/iOS影响;结论为 GO / CONDITIONAL GO / BLOCKED,不得把未编译当 PASS。 + +## 调查方法与客观限制 +- 最少执行 `git merge-base`、`git diff --name-status`、`git merge-tree --write-tree --messages` 对两个官方 Native 目标作三方模拟;记录命令、exit code、冲突路径。merge-tree 会在 Git object db 创建对象但不改源码或分支,允许。 +- 深入看源码定义和调用,不只扫描关键词;尽量提供可读且可复核的 `path:line` 和 commit hash。 +- 官方 tag 比较若产生无共同祖先、shallow、无法下载等障碍,如实记录并提出可复现替代方案。 +- 不强行运行常规编译、Android 实机、iOS 设备或 Jest。 +- 对官方 RN 版本差异使用 release changelog、已有源码;未下载的 RN 文件不得假称已审查。 +- 审计报告必须中文,章节包含:结论摘要、核对的版本与 SHA、方法和数据可信度、两条 Native 升级线冲突统计与具体文件、Particle 耦合清单、RenderSegment 耦合清单、RN 11.5.0 适配风险、改造优先级矩阵、阶段性迁移与验收、阻塞和未知、实际模型和文件修改清单。 + +## 必做输出和 Git 收尾 +- 写入 `docs/报告/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex完成报告_v1.md`;如不存在 `docs/报告/` 则创建。 +- 工作树中仅允许审计任务 MD 和审计报告两份新增文档;完成 `git diff --check`、路径与报告完整性检查。 +- **不要自行 commit 或 push**;执行器将先独立核对报告,再将仅这两份文档以隔离分支提交、确认快进后 push 到 `origin/particle/main`,最终回填 Git 状态。若未收到执行器的最终 Git 回执,报告必须写 `Git 收尾待 RDC 验收`,不能自称已推送。 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 diff --git a/docs/50_Step记录/README.md b/docs/50_Step记录/README.md new file mode 100644 index 000000000..7eaa9ccad --- /dev/null +++ b/docs/50_Step记录/README.md @@ -0,0 +1,11 @@ +# Step 记录目录 + +保存各阶段已发生事实和阶段权威状态。历史 Step 不因后续版本升级而改写。 + +## 当前 Android / RN 操作入口 + +当前 QBIN V4 / Android `.3` 的构建、pbf 公网发布、Win11 Test Host、Metro/ADB 与人工验收流程: + +`STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md` + +`STEP-080 ... v1` 及 STEP-060~072 中的 MavenLocal、`.1/.2`、旧 Win11 路径/端口均保留为历史证据,不作为当前命令来源。 diff --git a/docs/50_Step记录/STEP-000_完整环境初始化_v1.md b/docs/50_Step记录/STEP-000_完整环境初始化_v1.md new file mode 100644 index 000000000..0bb3d88b7 --- /dev/null +++ b/docs/50_Step记录/STEP-000_完整环境初始化_v1.md @@ -0,0 +1,27 @@ +# STEP-000 完整环境初始化 v1 + +## 状态 +进行中 + +## 目标 +建立 MapLibreParticleLab 完整 Linux/Android 开发环境与 Codex 中文工程规范。 + +## 必做项 +- [x] MapLibre 正式 release 基线 +- [x] 全 submodule +- [x] upstream / origin +- [x] particle/main +- [x] 中文文档目录和命名规范 +- [x] Codex 协作规范 +- [x] 构建规范 +- [x] Step 规范 +- [x] Node/npm 环境 +- [x] Android SDK/NDK +- [x] Linux Native 构建 +- [x] Android OpenGL 构建 +- [x] Android Vulkan 构建 +- [x] 初始化完成报告 + +## 必做输出 +- PARTICLE_BASELINE.md +- 环境初始化报告.txt diff --git a/docs/50_Step记录/STEP-040_Android离线Fixture正式粒子最小闭环_v1.md b/docs/50_Step记录/STEP-040_Android离线Fixture正式粒子最小闭环_v1.md new file mode 100644 index 000000000..76d9f6258 --- /dev/null +++ b/docs/50_Step记录/STEP-040_Android离线Fixture正式粒子最小闭环_v1.md @@ -0,0 +1,39 @@ +# STEP-040 Android 离线 Fixture 正式粒子最小闭环 v1 + +## 状态 + +完成。40-01 Stage B Android OpenGL x86_64 手工验收已完成,正式状态为 `PASS`。 + +## 目标与基线 + +- 目标:建立 `RasterArraySource -> RenderRasterArraySource -> RasterParticleLayer -> RenderRasterParticleLayer` 的第一条正式离线粒子闭环,并修复 STATIC recreation 的零步 bootstrap。 +- 基线:`343cecc778c3ffaa00adf94f82d7ad5f494a3877`,分支 `particle/main`。 +- 前置:30-01、30-02、30-03 均为 PASS;EGL context recreation 的独立实机证据仍是已接受风险。 + +## 必做项 + +- [x] `RasterArraySource` 以 FileSource byte boundary 读取 fixture metadata 与 field,生成 immutable `ParticleFieldSnapshot`。 +- [x] `RenderRasterArraySource`、`RasterParticleLayer`(`Source::Required`)和 `RenderRasterParticleLayer` 进入 Style/RenderTree 链。 +- [x] 正式 RenderLayer 复用既有 GPU field、State A/B、Trail A/B、mask、camera/world-wrap/antimeridian 与 resource lifecycle;没有第二套 simulation。 +- [x] Android Test App 从 APK `asset://integration/particle-fixtures/...` 提供正式 source 输入;Linux smoke helper 仍可读取仓库 fixture。 +- [x] NoData cell 作为 GPU field texture alpha=0 上传;shader 遇到 invalid sample 确定性回到 seed position,不把 NoData 当速度。 +- [x] STATIC recreation 仍走 zero-step State/Trail/bootstrap composite,commit 不增加 `simulationFrame`。 +- [x] Android OpenGL x86_64 APK 手工验收:GLOBAL/ACTIVE/STATIC、fixture 切换、Rotation、Camera、world wrap、antimeridian、mask、disable/enable 与快速切换均 PASS。 + +## 允许修改 + +隔离 particle field/source/layer/render source/render layer、最小 LayerManager/Style Source/shader 注册、Android Test App fixture assets 与本 Step 文档。 + +## 禁止修改 + +不得运行 Gradle、安装 APK、ADB/T1-T12;不得新增 Overlay、Android UI/Core 耦合、在线数据、第二套 GPU simulation,或声称 Android/Vulkan/iOS PASS。 + +## 构建与检查 + +- Linux OpenGL 受影响 Core 与 `source.test.cpp.o` 均在各轮修复后精确编译:PASS(`-Werror`);完整 `mbgl-test` 不属于本任务的精确验证范围。 +- `clang-format --dry-run --Werror`、`git diff --check`、shader generator/source-header parity 与生成器幂等检查:PASS。 +- adapter smoke:`uniform-east`、`uniform-north`、`vortex` 均 valid;`nodata` valid 且有 1 个 invalid cell。Android APK OpenGL x86_64 手工验收:PASS。 + +## 当前结果、阻塞与下一步 + +正式 `RasterArraySource -> RasterParticleLayer` 闭环已通过 Android OpenGL x86_64 Stage B。历史调试中先后修复 OfflineDownload 枚举接入、enabled RasterArray source 空 RenderItem SIGSEGV、旧 APK/.cxx native 缓存、ACTIVE/STATIC transient reset、formal Composite 未显示 Trail、shader source/generated header 不一致、opaque RenderTarget clear 下的 State seed、source 重建的 field identity generation collision,以及 STATIC Rotation/Viewport 自动恢复。最终实机已确认:east/north/east 切换方向正确、ACTIVE/STATIC lifecycle 正确、Camera 与 Rotation 正确、GLOBAL/OCEAN A/OCEAN B 与 shared mask reuse 正确。相同 RenderThread/resource generation 内三种 mask consumer 均复用同一 `maskGeneration` 且 `reuse=true`;graphics resource 重建后的 generation 递增属于新 GPU resource,不是同 generation 内重复创建。40-01 正式关闭为 `PASS`。iOS、Vulkan、真实在线数据、生产 coastline/mask 和性能结论不在本 Step 范围。 diff --git a/docs/50_Step记录/STEP-041_Android双时间片GPU时间插值_v1.md b/docs/50_Step记录/STEP-041_Android双时间片GPU时间插值_v1.md new file mode 100644 index 000000000..45b485b61 --- /dev/null +++ b/docs/50_Step记录/STEP-041_Android双时间片GPU时间插值_v1.md @@ -0,0 +1,25 @@ +# STEP-041 Android 双时间片 GPU 时间插值 + +- 状态:`CLOSED / PASS` +- 前置:STEP-040(40-01)已在 Android OpenGL 手工验收为 PASS。 +- 目标:在唯一正式链路 `RasterArraySource -> RasterParticleLayer -> GPU Simulation -> Trail -> Composite` 中,把单 Field 扩展为 T0/T1 两个独立 GPU Field,并在 State pass 对速度作 GPU `mix`。 +- 实现提交:`4675a8a29dd6fc7b684bfb64913a8c9075ad8583`(`feat: add GPU particle time interpolation`)。 +- 40-02C 最终修正:原实现的 MIX fallback、GPU identity 与 NORTH Mercator displacement 问题由 STEP-043/043A/043B 修正;Android T1--T10 已完成并全部 PASS。 + +## 本轮实现 + +- `RasterArraySource` 支持可选 `next-fixture`、`next-metadata`、`next-field`,以同一 FileSource byte boundary 加载 T0/T1;两份快照必须具有相同 grid、bounds、NoData、Field 类型及 display domain。 +- `RasterParticleLayer` 新增常量 paint property `raster-particle-time-mix`(有限值,内部 clamp 至 `[0,1]`)。它仅更新既有 UBO,不能触发 Field upload 或 State/Trail 重建。 +- `RenderRasterParticleLayer` 维持一套既有 State A/B、Trail A/B、Mask 与 Composite,只将 Field GPU resource 扩为 T0/T1 两个 texture slot。窗口推进时会按 generation + fixtureID + dataVersion 匹配旧 slot;A/B -> B/C 复用 B、只上传 C,并不 reset simulation/trail。完全无重叠的新窗口仍走既有安全 reset/reseed。 +- State shader 分别 decode T0/T1 RG velocity 后使用 `mix(velocity0, velocity1, timeMix)`;没有 CPU 生成中间完整 Field。NoData alpha 在任一端无效时保持现有确定性 respawn。 +- Android Test App 新增 T0 EAST/T1 NORTH、MIX 0/50/100 与 ADVANCE B->C 入口;保留既有 ACTIVE/STATIC、mask、Camera、world-wrap 与 antimeridian 入口。 + +## 验证边界 + +- Linux OpenGL 受影响 Core 与 source test object:通过。 +- shader generator、二次生成稳定性、C++ format、`git diff --check`:通过。 +- Codex 未运行 Android Gradle、ADB、APK 安装或设备测试;用户已在 Android x86_64 OpenGL 完成 T1--T10,全部 PASS。 + +## 不在本 Step 范围 + +iOS/Metal、Vulkan、网络/真实气象数据、完整 forecast cache、Jest、最终性能基线均不在本任务范围。40-02 在 T1--T10 通过前不得标记 PASS。 diff --git a/docs/50_Step记录/STEP-042_Android粒子Validation测试界面紧凑化_v1.md b/docs/50_Step记录/STEP-042_Android粒子Validation测试界面紧凑化_v1.md new file mode 100644 index 000000000..e5b51e362 --- /dev/null +++ b/docs/50_Step记录/STEP-042_Android粒子Validation测试界面紧凑化_v1.md @@ -0,0 +1,23 @@ +# STEP-042 Android 粒子 Validation 测试界面紧凑化 + +- 状态:`完成` +- 前置:40-02 为 `IMPLEMENTED / WAITING FOR ANDROID VALIDATION`。 +- 目标:只缩小 `Particle Core Layer Validation` 的控制区,扩大 MapView/粒子观察区;不改变任一粒子业务或按钮 action。 +- 实现提交:`f4d519ca789c0488d6b9d8259427ac070b15eb63`(`ui: compact particle validation controls`)。 + +## 已实现 + +- 底部控制区从 7 个默认高度行收为 4 行,每行 34dp;状态行 36dp,外侧 padding 4dp,总高度约 180dp。 +- 全部 16 个现有 button ID 与 Activity click binding 保留,按 Field/时间片、Mix/Runtime、Enable/Mask、地理场景重新分组。 +- 状态文字改为单行紧凑表达:mask、ACTIVE/STATIC/DISABLED、Field/T0/T1 与 mix;不读取或新增 Core 状态。 +- MapView 继续全屏布局,控制区压缩直接将空间归还给地图/粒子显示。 + +## 检查与边界 + +- 16 个 XML button ID 与现有 Kotlin binding 静态对应检查:通过。 +- Java 17 标准 XML parser、16 个 button ID/binding 核对与 `git diff --check`:通过;未运行 Android Gradle/ADB/设备。 +- 禁止修改项均未触及:C++ Core、shader、GPU Field、simulation、trail、composite、mask、ABI 与 Gradle 均未修改。 + +## 当前结果与下一步 + +状态应为 `IMPLEMENTED / WAITING FOR ANDROID UI VALIDATION`。用户需以横屏 Android OpenGL APK 验收 UI-1 至 UI-5;通过前不得标记 PASS。 diff --git a/docs/50_Step记录/STEP-043A_40-02C源码审查后最小修正_v1.md b/docs/50_Step记录/STEP-043A_40-02C源码审查后最小修正_v1.md new file mode 100644 index 000000000..c74439871 --- /dev/null +++ b/docs/50_Step记录/STEP-043A_40-02C源码审查后最小修正_v1.md @@ -0,0 +1,7 @@ +# STEP-043A 40-02C 源码审查后最小修正 + +- 基线:`5dd4dbf4967c1238e1bde8fb66fe3dbd2c4fe52a` +- 状态:`CLOSED / PASS` +- 修正:GPU texture reuse 保留旧 backing 但消费 wanted snapshot revision;MIX_REJECTED 不再改变 timeMix;增加 Layer/Resource/Field upload/reuse/reset 生命周期日志;所有 Style reload 调用点有具体 reason。 +- 检查:shader generator、Linux Core/source-test object、format、diff check 通过;未运行 Android Gradle、ADB 或设备。 +- Android 设备验证发现 Java peer lookup 对自定义 raster-particle 不适用,STEP-043B 以最小 Core property bridge 接续修正;最终 T1--T10 均 PASS。 diff --git a/docs/50_Step记录/STEP-043B_40-02C_Android到Core_MIX属性桥接_v1.md b/docs/50_Step记录/STEP-043B_40-02C_Android到Core_MIX属性桥接_v1.md new file mode 100644 index 000000000..215318e96 --- /dev/null +++ b/docs/50_Step记录/STEP-043B_40-02C_Android到Core_MIX属性桥接_v1.md @@ -0,0 +1,11 @@ +# STEP-043B Android 到 Core MIX 属性桥接 + +- 状态:`CLOSED / PASS` +- 根因:Android `Style.getLayer()` 需要自定义 layer 的 Java peer;raster-particle Core layer 无 peer,导致 false `LAYER_NOT_READY`。 +- 实现:最小 `NativeMapView.setStyleLayerFloatProperty(layerId, propertyName, value)`,JNI 通过 `Style::getLayer()` 与正式 `Layer::setProperty()` 更新 Core 属性;没有完整 RasterParticle Java peer。 +- Host:MIX 不再查询 Java Layer peer,bridge 成功后才更新 `timeMix`。 +- Codex 未运行 Android Gradle/ADB;用户已在 Android x86_64 OpenGL 完成 T4--T10,全部 PASS。MIX bridge 成功且无 MIX 引起的 Style reload、Field upload 或 simulation reset。 + +## Android 编译修正 + +首次 Android 编译发现 `Layer::setProperty(..., static_cast(value))` 触发未支持的 `Convertible` / `ConversionTraits`。Core 已有强类型 `RasterParticleLayer::setTimeMix(float)`;bridge 现确认 `raster-particle-time-mix` 与实际 layer 类型后直接调用该 setter,不新增 conversion trait。Kotlin `setTimeMix()` 不含 `Style.getLayer()`、`layer.setProperties()` 或 `PaintPropertyValue` 的 MIX 写入,bridge 成功后才更新 `timeMix`。 diff --git a/docs/50_Step记录/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_v1.md b/docs/50_Step记录/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_v1.md new file mode 100644 index 000000000..05f9f74e1 --- /dev/null +++ b/docs/50_Step记录/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_v1.md @@ -0,0 +1,9 @@ +# STEP-043 40-02C Android 双时间片 GPU 时间插值最终修正 + +- 状态:`CLOSED / PASS` +- 修正:MIX hard property-only guard、revision/content identity 分离、A/B→B/C content reuse、Mercator Y displacement 与 north-positive V 的边界转换。 +- 规则:generation 仍是 source revision;GPU reuse 比较 fixtureID、dataVersion、已验证 `fieldSha256` 和 grid compatibility。MIX 未就绪只记录 `MIX_REJECTED`,不 reload style。 +- 检查:shader generator、Linux Core/source test object、format、diff check 通过;未运行 Android Gradle/ADB。 +- Android:用户已完成 x86_64 OpenGL T1--T10,全部 PASS;MIX property-only、B→C reuse 与 lifecycle 均无异常 reset。 +- STEP-043A 源码审查发现 reuse revision、MIX reject state、diagnostics 与 Style reason 缺口,均已最小修正;状态不变。 +- STEP-043B 处理 Android Java Layer peer 缺失导致的 false LAYER_NOT_READY,改用 Android→Core property bridge;状态不变。 diff --git a/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v1.md b/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v1.md new file mode 100644 index 000000000..1f6d9ca38 --- /dev/null +++ b/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v1.md @@ -0,0 +1,39 @@ +# STEP-044 Android OpenGL 粒子数量容量基线 + +- 状态:`IMPLEMENTED / WAITING FOR ANDROID VALIDATION` +- 前置:STEP-041(40-02)已 `CLOSED / PASS`。 +- 目标:在唯一正式链路 `RasterArraySource -> RasterParticleLayer -> GPU State A/B -> Trail A/B -> Composite` 上,为 Android OpenGL 提供可重复的 State 粒子容量档位;本 Step 不宣称任何容量实测结果。 + +## 实现 + +- 正式 `RasterParticleLayer` 新增常量 paint property `raster-particle-particle-count`,默认 `256`。Core 只接受 `256..1048576` 的平方数,保证 Renderer 总能将其映射为合法的正方形 State texture。 +- 数量的实际承载点是正式 GPU State A/B render target:默认 `16x16=256`,可切换为 `32x32=1024`、`64x64=4096`、`128x128=16384`、`256x256=65536`、`512x512=262144`(Core 支持的中间平方值)和页面最高 `1024x1024=1048576`。每个 State texel 继续运行既有 State shader,故 effective count 即该网格面积。 +- Trail 仍只读取四个确定性 State texel 作为低开销视觉健康指示;这不是第二个 simulation,也不会改变 State A/B、Trail A/B、Composite 或 40-02 Field/MIX 语义。 +- Android 验证页提供 `256 / 1K / 4K / 16K / 64K / 1M` 档位。请求通过既有 `MapLibreMap -> NativeMapView -> JNI -> Style::getLayer()` 薄桥,并经强类型 `RasterParticleLayer::setParticleCount(uint32_t)` 生效;没有 Java raster-particle Layer peer。 +- 切换数量只替换 State A/B,并执行一次 `SIMULATION_RESET reason=PARTICLE_COUNT_CHANGE` 以 seed 新网格、清理旧 Trail。Field T0/T1、GPU content reuse、MIX、interpolation、mask、viewport 逻辑和 layer instance/resource generation 均不因该属性本身改变。 + +## 观测契约 + +- Android host:`PARTICLE_COUNT_REQUEST`、`PARTICLE_COUNT_APPLIED`、拒绝原因;状态行显示当前 count。 +- Core/JNI:`STYLE_PROPERTY_SET result=SUCCESS|INVALID_PARTICLE_COUNT`。 +- Renderer:`PARTICLE_RESOURCES_CREATE|REBUILD|RELEASE`,每条含 requested/effective count、State grid 和 resource generation;现有低频 schedule/pass/commit 继续证明 simulation、trail 与 composite 持续工作。 +- State target 创建失败时记录 `PARTICLE_COUNT_REJECTED reason=STATE_RESOURCE_CREATE_FAILED`,旧资源/旧 effective count 保持。 + +## Android 待验收矩阵 + +| 粒子数量 | 是否持续运行 | 画面/帧表现 | 内存趋势 | GL/GPU 错误 | 生命周期异常 | 结论 | +|---:|---|---|---|---|---|---| +| 256 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | +| 1,024 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | +| 4,096 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | +| 16,384 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | +| 65,536 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | +| 1,048,576 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | + +## 本地验证边界 + +- Linux/OpenGL:`raster_particle_layer.cpp.o`、`render_particle_validation_layer.cpp.o`、`style_layer.test.cpp.o` 精确编译通过;新增 Layer 测试覆盖默认、合法及非法 count 的 Core guard。 +- `clang-format` 与 `git diff --check` 通过。 +- 未运行 Android Gradle、ADB、APK 安装或设备测试;未加入/执行 Jest。 + +Android 用户完成各档长时间测试、GL/GPU 日志和矩阵后,才可判定稳定区间、性能下降区间和压力边界。 diff --git a/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v2.md b/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v2.md new file mode 100644 index 000000000..8b00f7a9c --- /dev/null +++ b/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v2.md @@ -0,0 +1,37 @@ +# STEP-044 Android OpenGL 粒子数量容量基线 v2 + +- 状态:`IMPLEMENTED / WAITING FOR ANDROID VALIDATION` +- 基线:`c0d1ad43c49ed6d526c3fd8b982c04840326d0d0` 的容量档位实现。 +- 范围:补齐正式 RasterParticle 的 full particle draw;不是新 Step,也不是容量结论或性能优化。 + +## v1 的实际缺口 + +v1 已让 State A/B 容量随 `raster-particle-particle-count` 改为 256 至 1,048,576,但 Trail fragment 只读取四个固定 State UV。因此当时只能证明 N 个 State 同时 simulation,不能证明 N 个粒子同时 Trail/render/composite;Android 容量验收据此暂停。 + +## v2 补齐 + +- `effectiveParticleCount=N -> State N -> Trail draw N -> Composite` 已进入唯一正式 `RasterArraySource -> RasterParticleLayer -> RenderRasterParticleLayer` 链。 +- 每个粒子在 State A/B 中对应一条静态 GPU vertex:`particleIndex -> x=index%stateGridWidth, y=index/stateGridWidth`。vertex buffer 仅在 resource create 或 particle-count change 重建;顶点 shader 用 `(x+0.5,y+0.5)/stateGrid` 采样当前 State write texture,位置从未 CPU 生成或 readback。 +- Trail write target 先由全屏 Trail fade quad 消费前一 Trail,再追加 full point draw。每粒子一个 point,`trailDrawCount=N`;Composite 继续消费同一 Trail A/B write target。 +- 现有 16-bit index 路径每 segment 最多 65,536 points;更高 count(包括 1M)被分为多个 segment,仍使用同一 static index buffer、同一 shader 和同一 State texture,非第二 simulation。 +- legacy `ParticleValidationLayer` 仍保留四个 deterministic sample 诊断;正式 RasterParticle Layer 的路径不再使用它们作为 draw 数量限制。 + +## 生命周期与冻结语义 + +- particle-count change 继续只触发合理的 State A/B 和 full-draw resource rebuild,以及 `SIMULATION_RESET reason=PARTICLE_COUNT_CHANGE` / reseed。 +- Field T0/T1、MIX、interpolation、Field reuse、mask、A/B→B/C 及既有 State/Trail ping-pong 没有改变。MIX 仍不能导致 draw resource rebuild、Style reload、Field upload 或 reset。 +- State seed 仍是每个 State texel 的 `v_uv`,所以 full draw 的首次 256/1K 粒子已经 deterministic 地分布在完整 grid,而非重叠于一个点。 + +## 观测日志 + +资源 create/rebuild/release 日志含 `effectiveParticleCount`、`stateGrid`、`trailDrawCount`、`particleIndexCount`、`fullParticleDraw=true` 和 `resourceGeneration`。`PARTICLE_DRAW_REJECTED` 明确报告缺失 draw resource 或 drawable finalize failure。 + +## 待 Android 验收 + +| 用例 | 要求 | 状态 | +|---|---|---| +| T-FULL-01 | 256:`16x16`、`trailDrawCount=256`、明显粒子群并持续运动 | 待测 | +| T-FULL-02 | 1K:`32x32`、`trailDrawCount=1024`、密度明显大于 256、Trail/Composite 正常 | 待测 | +| T-FULL-03 | 256→1K→256:draw count 同步变化,无 crash、Style reload、Field upload 或 MIX 改变 | 待测 | + +只有 T-FULL-01~03 通过,才恢复 4K / 16K / 64K / 1M 的容量矩阵。Codex 未运行 Android Gradle、ADB、安装或设备测试;未加入 Jest。 diff --git a/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v3.md b/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v3.md new file mode 100644 index 000000000..f3722d543 --- /dev/null +++ b/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v3.md @@ -0,0 +1,45 @@ +# STEP-044 Android OpenGL 粒子数量容量基线 v3 + +- 关闭日期:2026-09-03 +- 状态:`CLOSED / PASS` +- 验收方式:用户完成 Android OpenGL x86_64 实机验证;Codex 本收尾未执行 Android Gradle、ADB、安装或设备操作。 + +## 基线与历史缺口 + +- `c0d1ad43c49ed6d526c3fd8b982c04840326d0d0`,`feat: add Android particle capacity baseline`:完成 State A/B 容量扩展,但 Trail/render 仍只消费少量固定 State sample。因此该提交只能证明 N 个 State 同时 simulation,原 Android 容量验收曾正确暂停;它不是 40-03 的最终完成状态。 +- `a0aae48ec8cb8a379909b88df1aeef45663f2985`,`feat: draw all raster particle states`:正式 Trail/render 改为消费全部有效 State,建立 `effectiveParticleCount=N -> trailDrawCount=N` 的 full particle draw,使容量测试具备真实意义。 + +## 最终链路 + +`State A/B N 个有效 texel -> GPU simulation -> N 个 full particle Trail point draw -> Trail A/B -> Composite`。 + +particle index 在 resource create 或 count change 时一次性映射 State texel;稳定帧 vertex shader 直接从当前 State write texture 读取位置。没有每 frame CPU particle loop、GPU→CPU readback、第二 simulation 或 overlay。 + +## Android 实机证据 + +所有下列日志均确认 `fullParticleDraw=true`。 + +| 档位 | State Grid | effectiveParticleCount | trailDrawCount | 结果 | +|---|---:|---:|---:|---| +| 256 | 16×16 | 256 | 256 | PASS | +| 1K | 32×32 | 1024 | 1024 | PASS | +| 4K | 64×64 | 4096 | 4096 | PASS | +| 16K | 128×128 | 16384 | 16384 | PASS | +| 64K | 256×256 | 65536 | 65536 | PASS | +| 1M | 1024×1024 | 1048576 | 1048576 | PASS;用户确认运行 OK | + +## T-FULL-01~03 + +- T-FULL-01:256 State → draw 256,PASS。 +- T-FULL-02:1K State → draw 1024,PASS。 +- T-FULL-03:256 → 1K → 256:State grid 与 draw count 正确变化,full draw resources 正确释放/重建,无 stale resource、crash、由数量切换引起的 Field upload 或 Style reload,PASS。 + +## 40-02 语义保持 + +实际日志证明 count change 只作用 State/draw resources。MIX 继续是 property-only,不触发 Field upload 或 simulation reset;时间窗 Field reuse 继续工作,`simulationRebuild=false`、`trailCleared=false`。40-02 已冻结语义未被破坏。 + +## 最终结论 + +当前 Android OpenGL 正式 GPU 粒子链已实际验证 1,048,576 个 full-draw 粒子可正常运行。已验证容量 **≥ 1M**;该容量已经满足 MapLibreParticleLab / NavSea 当前实际需求,本阶段不继续测试 2M / 4M / 8M 或寻找理论压力边界。 + +1M 是本验证设备上的已验证容量,不是硬编码产品默认值、所有 Android 设备保证值或理论最大容量。 diff --git a/docs/50_Step记录/STEP-045_Runtime_Field动态更新接口_v1.md b/docs/50_Step记录/STEP-045_Runtime_Field动态更新接口_v1.md new file mode 100644 index 000000000..de358865c --- /dev/null +++ b/docs/50_Step记录/STEP-045_Runtime_Field动态更新接口_v1.md @@ -0,0 +1,42 @@ +# STEP-045 Runtime Field 动态更新接口 + +- 状态:`IMPLEMENTED / WAITING FOR ANDROID VALIDATION` +- 前置:40-03 / STEP-044 已 `CLOSED / PASS`。 +- 范围:唯一正式链路 `RasterArraySource -> RasterParticleLayer -> GPU State/Trail/Composite` 的低频内存 Field 时间窗推进;不接网络或真实天气 Provider。 + +## 实现 + +- `RasterArraySource::updateRuntimeFieldWindow(ParticleFieldSnapshot)` 是正式强类型入口。它验证 Source 已加载、Snapshot 有效、grid/bounds/NoData 等插值契约兼容且 runtime generation 单调递增;成功时只把 Source 的权威窗口从 `T0/T1` 原子推进为 `T1/newT1`,并发出 `onSourceChanged()`。 +- 已有的 `RasterArraySource::Impl::field/nextField` 继续是正式 T0/T1 权威状态。Kotlin 不保存 T0/T1,JNI 不保存窗口或管理 renderer/GPU;Android 仅以 source id 调用薄桥 `advanceRasterArraySourceFieldWindow()`。 +- 为离线 Android 验证,Style 的 `runtime-fields` 在 Source 内预加载 C=`vortex`、D=`nodata`。`advanceRuntimeFieldWindow()` 只从该 Source-owned 队列取候选;候选资源完成顺序不影响 revision,因为 candidate 进入权威 T1 时才取得 process-wide generation。 +- A=`uniform-east`、B=`uniform-north`、C=`vortex`、D=`nodata`。因此两次按钮推进是 A/B→B/C→C/D;不调用 `setStyle()`,不重建 Layer/Source。 +- Renderer 继续使用已有 content identity 与双 Field slot:旧 T1 成为新 T0 时记录 `FIELD_REUSE from=OLD_T1 to=NEW_T0`;只有新 T1 按 `FIELD_UPLOAD reason=NEW_CONTENT` 上传。窗口有重叠时既有逻辑保持 `simulationRebuild=false`、`trailCleared=false`。 +- `raster-particle-time-mix` 未改,仍仅更新 Core paint property/UBO;它不上传 Field、不清 trail、不 reset simulation、不 reload Style。 + +## 失败与重复语义 + +- 当前 T1 内容重复提交:`NOOP`,不发出 Source change、upload 或 reset。 +- 缺失/非法 Snapshot、未加载 Source、不兼容 grid、非单调 generation、候选未完成或耗尽:`FIELD_WINDOW_UPDATE_REJECTED reason=...`,当前有效窗口和 simulation 原样保留。 +- 更新是低频事件;没有每帧 Field copy、hash、Snapshot 创建或 texture upload。 + +## Android 待验收 + +1. 点击 `TIME WINDOW` 建立 A/B,检查 MIX 0/50/100。 +2. 第一次 `ADVANCE`:检查 B→新 T0 reuse、只上传 C、没有 style/layer/source recreate、`simulationRebuild=false`、`trailCleared=false`。 +3. 第二次 `ADVANCE`:同样检查 C→新 T0 reuse、只上传 D。 +4. 在 C/D 再检查 MIX 0/50/100;Field upload/reset/trail clear 计数不应因 MIX 改变。 +5. 第三次 `ADVANCE` 应明确拒绝 `NO_PRELOADED_FIELD`,不中断当前窗口。 + +## 本地验证边界 + +- Linux/OpenGL 精确编译:`raster_array_source.cpp.o`、`style/conversion/source.cpp.o`、`source.test.cpp.o`。 +- 新增 Source 定向测试对象覆盖 A/B→B/C→C/D、重复 Snapshot NOOP、无效 Snapshot fail-closed,并已精确编译;当前 build tree 缺少大量无关测试依赖,未把完整 `mbgl-test` 二进制执行误报为通过。 +- 未运行 Android Gradle、ADB、APK 安装、设备测试或 Jest。 +- 实现提交:`45502a53f4fc486bd24cb0008d1ab0ff221e7e63`(`feat: add runtime particle field updates`);文档收尾另行提交。 + +## T-RUNTIME-05 Duplicate NOOP 补验收入口 + +- 用户已完成 T-RUNTIME-01~04 与 T-RUNTIME-06 Android 手工验证;本轮只补 T-RUNTIME-05 的真实触发入口,状态仍为 `IMPLEMENTED / WAITING FOR ANDROID VALIDATION`。 +- `DUPLICATE T1` 按钮经 `MapLibreMap -> NativeMapView -> JNI -> RasterArraySource::repeatRuntimeFieldWindow()` 重提 Source 当前 T1;它不调用 `setStyle()`、不推进 candidate queue,也不改变 Kotlin 的 MIX/窗口状态。 +- `repeatRuntimeFieldWindow()` 只把已存在的 `Impl::nextField` 交给既有 `updateRuntimeFieldWindow()`。在 `T0=vortex/T1=nodata`,content identity 命中既有 T1,正式 API 返回 `Noop`,不发 Source change,因而不触发 Field upload、GPU slot 交换、simulation reset 或 trail clear。 +- 新增 Source test assertion 在 C/D 后调用该入口并验证返回 `Noop`、T0/T1 未变、`sourceChangedCount` 未增加。Linux/OpenGL 的 `raster_array_source.cpp.o` 与 `source.test.cpp.o` 以 `-Werror` 精确编译;Android Gradle、ADB、设备和 Jest 未执行。 diff --git a/docs/50_Step记录/STEP-045_Runtime_Field动态更新接口_v2.md b/docs/50_Step记录/STEP-045_Runtime_Field动态更新接口_v2.md new file mode 100644 index 000000000..786fb4d04 --- /dev/null +++ b/docs/50_Step记录/STEP-045_Runtime_Field动态更新接口_v2.md @@ -0,0 +1,51 @@ +# STEP-045 Runtime Field 动态更新接口 + +- 状态:`CLOSED / PASS` +- 最终 Android 验证日期:2026-09-03 +- 当前最终功能基线:`4e31163516ad77473df672e162f554223cd2f09e` + +## 目标与冻结接口 + +40-04 使正式 `RasterParticleLayer` 数据链在运行中接收新的内存 Field 时间片,不依赖 Style reload、Source/Layer recreate 或 simulation restart。 + +- 正式更新接口:`RasterArraySource::updateRuntimeFieldWindow(ParticleFieldSnapshot)`。 +- Duplicate 验证接口:`RasterArraySource::repeatRuntimeFieldWindow()`;它重新提交 Source 当前 T1,并经正式 update API 返回 `Noop`。 +- `RasterArraySource::Impl::field/nextField` 是正式 T0/T1 的唯一权威状态;JNI 只是薄桥,Android Validation 只触发测试动作,Kotlin 不持有正式 T0/T1。 +- Renderer 负责 GPU Field resource reuse/upload;`RasterParticleLayer` 继续负责既有 simulation、Trail 与 Composite。 + +## Android OpenGL 最终验收:PASS + +| 验收项 | 结果 | 最终证据 | +|---|---|---| +| T-RUNTIME-01 | PASS | 初始 T0=uniform-east、T1=uniform-north;MIX 0/50/100 正常,`TIME_MIX_UPDATE` 期间 Field upload/reset 不增加。 | +| T-RUNTIME-02 | PASS | A/B→B/C(east/north→north/vortex):`FIELD_REUSE from=OLD_T1 to=NEW_T0`,仅上传 vortex,`simulationRebuild=false`、`trailCleared=false`。 | +| T-RUNTIME-03 | PASS | B/C→C/D(north/vortex→vortex/nodata):再次复用 OLD_T1,仅上传 nodata,证明连续推进未写死一次。 | +| T-RUNTIME-04 | PASS | C/D 下 MIX 50/100/0:`fieldUploadCount=5`、`resetCount=0` 始终不变,MIX 保持 property-only。 | +| T-RUNTIME-05 | PASS | C/D 下连续 `DUPLICATE T1`:每次 `FIELD_WINDOW_UPDATE_APPLIED result=NOOP` 与 duplicate bridge `result=NOOP`;无 upload/reset/style load,T0/T1 不变。 | +| T-RUNTIME-06 | PASS | candidate 耗尽返回 `FIELD_WINDOW_UPDATE_REJECTED reason=NO_PRELOADED_FIELD`;随后 MIX 仍显示 C/D,`fieldUploadCount=5`、`resetCount=0`,fail-closed 成立。 | + +## 最终运行时契约 + +```text +Runtime / Validation Host + -> RasterArraySource::updateRuntimeFieldWindow(...) + -> Source-owned T0/T1 + -> A/B -> B/C -> C/D + -> OLD_T1 -> NEW_T0 GPU reuse + -> upload only new T1 + -> ongoing RasterParticleLayer simulation / Trail / Composite +``` + +- 正常推进不重载 Style、不重建 Layer/Source、不 reset simulation、不 clear Trail;日志确认 `simulationRebuild=false`、`trailCleared=false`。 +- 初始 `STYLE_LOAD reason=TIME_WINDOW` 是用户建立 A/B Validation window 的允许初始化行为。之后 A/B→B/C、B/C→C/D、Duplicate 和 candidate 耗尽 reject 全程 `styleEpoch=2` 不变。 +- MIX property 更新产生的 `LAYER_CHANGED` 是正常 RenderLayer change 通知,不是 Layer recreate;最终 `renderLayerInstanceId=1` 保持稳定。 + +## 未改变的冻结能力与范围外内容 + +40-04 未改变 `U=east-positive`、`V=north-positive`、Mercator shader 边界 `dy=-v`、T0/T1 GPU interpolation、MIX property-only,或 40-03 的 full particle draw(N State texels→N simulated particles→N actual particle draw)与已验证 capacity ≥ 1,048,576。 + +本 Step 未实现真实天气 HTTP、GRIB、Server、forecast cache、vendor/provider 协议、NavSea/React Native 正式接入、粒子形状/旋转/箭头、颜色系统、WeatherColorLayer、Vulkan、iOS/Metal、生产 Land/Ocean Mask 或 APK packaging/ZIP 技术债。 + +## 收尾边界 + +本收尾只更新文档和开发记录;未修改功能代码,未运行新的 Linux/OpenGL 编译、Android Gradle、ADB、实机测试或 Jest。 diff --git a/docs/50_Step记录/STEP-046_Android_OpenGL正式粒子视觉与运行时渲染控制_v1.md b/docs/50_Step记录/STEP-046_Android_OpenGL正式粒子视觉与运行时渲染控制_v1.md new file mode 100644 index 000000000..08f1c8e91 --- /dev/null +++ b/docs/50_Step记录/STEP-046_Android_OpenGL正式粒子视觉与运行时渲染控制_v1.md @@ -0,0 +1,72 @@ +# STEP-046 Android OpenGL 正式粒子视觉与运行时渲染控制 + +日期:2026-09-03 +状态:CLOSED / PASS + +## 目标 + +在已关闭的 40-04 Runtime Field 链路上,将正式 `RasterParticleLayer` 的 full particle draw 从固定蓝色 point 扩展为 GPU 速度驱动的短 streak,并一次性提供运行时视觉属性和 Android 验收入口。 + +正式链路保持唯一: + +```text +RasterArraySource T0/T1 + -> GPU time interpolation + -> State A/B + -> Trail A/B + -> GPU full streak draw + -> Composite +``` + +## 实现 + +- 每个 State texel 仍代表一颗正式粒子。静态 VBO 仅保存该 texel 的两个 line endpoint 编码;不包含 live position、direction 或 color。 +- vertex shader 从当前 State write texture 和 T0/T1 Field textures 获取位置与 `mix` 后的速度,按既有边界将 `V north-positive` 转换为 Mercator `dy=-v`,生成 canonical map-space 短 streak。无效/NoData/NaN/近零速度会 fail-closed,不绘制该 streak。 +- fragment shader 用最终插值速度 magnitude 走稳定的蓝→绿→橙 ramp;COLOR 控制可关闭 ramp 回退为固定蓝色。相同 magnitude 的 EAST/NORTH 保持同色,vortex 可显示局部速度差异。 +- 新增 Core visual paint properties: + - `raster-particle-line-width` + - `raster-particle-particle-length` + - `raster-particle-particle-opacity` + - `raster-particle-trail-opacity` + - `raster-particle-trail-fade` + - `raster-particle-speed-color-enabled` +- 所有属性是 Render/Visual property,不是 Field 或 simulation state。其运行时更新经既有 `MapLibreMap -> NativeMapView -> JNI -> RasterParticleLayer` boolean thin bridge,调用强类型 Core setter;不通过 Android Java Layer peer。 +- Android Validation 页面加入 VISUAL DEFAULT、SIZE、LENGTH、OPACITY、TRAIL、FADE 与 COLOR 控制,另有直接 VORTEX fixture 与 256K capacity 档;状态栏保留当前可见参数;Activity recreation 保存这些值。 + +## 不变量与观测 + +- visual property 更新只更新 RenderLayer/UBO;不会 reload Style、reload Source、上传 Field、重建 T0/T1、reset State/Trail 或清 Trail。 +- 40-04 的 A/B→B/C→C/D、`OLD_T1 -> NEW_T0` GPU reuse、Duplicate→NOOP、`NO_PRELOADED_FIELD` fail-closed 保持不变。 +- `PARTICLE_DRAW_RESOURCES_*` 记录 `particleDrawCount=N` 与 `streakVertexCount=2N`,明确两个顶点仍是一颗粒子的线段,不改变 40-03 已验证容量 `>= 1,048,576` 的含义。 +- `VISUAL_PROPERTY_APPLIED` 记录参数、renderLayerInstanceId、resourceGeneration、fieldUploadCount、resetCount;不按 frame 高频输出。没有可靠 GPU timer,因此不虚构 GPU ms。 + +## 静态验证 + +- `node shaders/generate_shader_code.mjs` 连续运行两次,生成物稳定。 +- Linux OpenGL 精确编译:`raster_particle_layer.cpp.o`、`render_particle_validation_layer.cpp.o`、`style_layer.test.cpp.o`:PASS。 +- 新增 `RasterParticleVisualProperties` C++ unit-level coverage,检查默认、运行时 setter 和 visual 参数边界。 +- `clang-format` 与 `git diff --check`:PASS。 +- 未运行 Android Gradle、ADB、真机或 Jest。 + +## Android 最终验收 + +实施提交:`e48fa087e01e9710c5d0c18ab062726226bbb6bd`(`feat: add particle visual runtime controls`)。用户已完成 Android OpenGL APK 编译、安装、实体机/虚拟机操作和完整 logcat 收集;本次收尾不重新运行 Android Gradle、ADB 或 Jest。 + +- T1 正式粒子方向视觉:PASS。UNIFORM EAST、UNIFORM NORTH、VORTEX 的 streak 正常显示,未见明显方向错误、黑屏或渲染异常。 +- T2 Runtime Visual Properties:PASS。VIS DEFAULT、SIZE、LENGTH、OPACITY、TRAIL、FADE、COLOR 都经正式 Runtime Property 路径生效;它们不是 Field reload 或 simulation rebuild。 +- T3 Speed Color / MIX:PASS。T0 EAST/T1 NORTH 的 MIX 0/50/100 与 speed color 正常;日志确认 MIX property-only,未重载 Field Window 或 reset simulation。 +- T4 Runtime Field Window:PASS。连续窗口推进、`OLD_T1 -> NEW_T0` reuse、新 T1 上传、Duplicate NOOP、fail-closed 都正常;推进未重载 Style、未无意义 reset simulation 或清 Trail。 +- T5 Camera / Projection:PASS。pan、zoom、bearing、DATELINE、WORLD WRAP 后正式粒子与地图组合正常。 +- T6 Lifecycle:PASS。DISABLE/ENABLE 与屏幕/生命周期恢复后无需额外按钮即可恢复显示。 +- T7 Mask:PASS。GLOBAL、OCEAN A、OCEAN B 均未发现 streak/speed color 绕过既有 mask 语义。 +- T8 Capacity Regression:PASS。256、1K、4K、16K、64K、256K、1M 全部实际运行;新的 streak + speed color 路径未发生 crash、黑屏、Renderer 中断、明显资源错误或结构性 full-particle-draw 失败。 + +256/1K 适合单 streak 的视觉正确性检查。当前 Validation viewport 下 4K 以上的粒子/Trail 已非常密集并接近铺满屏幕;这是 Validation 可读性现象,不是功能失败。64K、256K 和 1M 主要验证容量、稳定性与正式 full draw 路径,不要求肉眼分辨每一颗粒子。1M 继续只代表当前验证设备的已验证容量,不是理论最大值、产品默认值或所有 Android 设备保证。 + +logcat 的 `TimeStats: RenderEngineTimes are already at its maximum size[64]` 属于 Android/RenderEngine/虚拟机环境的日志噪声。同期粒子 Renderer 持续 `State -> Trail -> Composite` 且 `hazard=SAFE`,未观察到关联 crash 或中断,不能判为 STEP-046 Renderer FAIL。 + +## 最终结论 + +40-05 / STEP-046 已完成 Android 验收并正式 `CLOSED / PASS`。下一阶段如继续,可单独规划 40-06 时间序列 Playback/preload/cache;高密度 Validation 可读性可作为低优先级测试工具改进,不阻塞本阶段。 + +本阶段不包含 WeatherColorLayer、网络/GRIB/provider、React Native、Vulkan、iOS/Metal、生产 Land/Ocean mask、粒子箭头贴图或 APK packaging 技术债。 diff --git a/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v1.md b/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v1.md new file mode 100644 index 000000000..9fa478d7b --- /dev/null +++ b/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v1.md @@ -0,0 +1,27 @@ +# STEP-047 Runtime Timeline 与 Playback + +状态:IMPLEMENTED / WAITING FOR ANDROID VALIDATION + +## 目标与边界 + +40-06 在已冻结的 40-04 Runtime Field Window 与 40-05 正式 streak 渲染之上增加 Core-owned Timeline/Playback。它不接入网络、GRIB、provider、HTTP 或 Android 定时器;Android 只通过 JNI 发送低频命令。 + +`RasterArraySource` 持有有界(最多 8 个)不可变 Field 快照、严格递增的逻辑时间、当前 T0/T1、cursor、播放状态、速度和 gap/invalid/recover 状态。渲染层每帧只取得 Core 给出的 T0/T1/mix;不会获知文件、provider 或网络。 + +## 实现 + +- Validation timeline 使用 6 个 fixture snapshot:east、north、vortex、nodata、east、north;逻辑间隔为 4 秒。 +- `PLAY/PAUSE`、0.5x/1x/2x、STEP PREV/NEXT、SEEK START/MID/END、GAP、INVALID、RECOVER 均为 Source Core 命令,经薄 JNI bridge 调用。 +- 自动边界维持 `OLD_T1 → NEW_T0` 资源重用语义;只在窗口 revision 改变时交给既有 Renderer Field reuse/upload 路径处理。正常 MIX 是 Core clock 的 scalar 更新,不会触发 Style reload。 +- Gap/invalid 到达边界时 fail-closed 保持当前有效 T0/T1;recover 后可继续操作;末尾进入 END。 +- Validation UI 改为状态栏与 `TIME | VIS | COUNT | MAP | MORE` 五个菜单入口,原动作仍可由菜单触达。 + +## 不变契约 + +- U=east-positive、V=north-positive、Mercator `dy=-v` 不变。 +- 40-02 MIX property-only、40-03 full particle draw、40-04 runtime window reuse/noop/fail-closed 不变。 +- 无未来 GPU 纹理预上传;仅 Source 内有界 CPU snapshot cache。 + +## 验证状态 + +已执行 Linux/OpenGL Core 精确编译与 Source 单元测试构建;未运行 Android Gradle、ADB、Jest 或实体机。Android x86_64 手工验收仍待执行。 diff --git a/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v2.md b/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v2.md new file mode 100644 index 000000000..200a0a809 --- /dev/null +++ b/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v2.md @@ -0,0 +1,24 @@ +# STEP-047 Runtime Timeline 与 Playback(集中修复) + +状态:IMPLEMENTED / WAITING FOR ANDROID RE-VALIDATION + +## Android Validation Round 1 + +Round 1 结论:PARTIAL / NEEDS FIX。 + +已通过:Core Timeline READY、自动 MIX、自动 window advance、`OLD_T1 → NEW_T0` reuse、紧凑控制台、MAP/MORE 基础回归、VIS Core property 调用链。 + +失败或无法充分验证:Pause/Resume UX、速度可视性、Seek 状态语义、GAP/INVALID/RECOVER 入口、播放中 VIS、COLOR 可达性、Preload/Cache 可观测性。 + +## 本轮修复 + +- Validation 逻辑时间窗口从 4 秒扩展到 40 秒,6 snapshot 总时长约 200 秒;Core clock 不使用 Android timer。 +- PLAYING→PAUSED 和 PAUSED→PLAYING 保留 cursor;END 时 PLAY 明确从 start restart;END/WAITING 拒绝日志明确。 +- PLAYING 的非 END seek 保持 PLAYING;PAUSED seek 保持 PAUSED;Seek END 进入 ENDED;日志记录状态、cursor、目标窗口和 mix。 +- TIME 菜单直接提供 NORMAL、GAP、INVALID NEXT、RECOVER NEXT;GAP/INVALID 在边界 fail-closed,recover 恢复此前播放/暂停语义。 +- VIS 把 COLOR 放到第二项;Validation 调节步长扩大,状态行即时显示 W/L/O/T/F/C;状态行同时显示 Timeline state/speed。 +- 预加载仍是 Source 的有界 CPU snapshot cache(容量 8,绝不预上传未来 GPU Field);READY 输出 TIMELINE_PRELOAD,窗口推进输出 TIMELINE_CACHE_HIT。 + +## 不变项 + +自动 MIX、GPU T0/T1 window、`OLD_T1 → NEW_T0` reuse、只上传新 T1、无 Style reload/simulation reset/trail clear 的正常推进路径,及 STEP-046 visual property-only 均未改动。 diff --git a/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v3.md b/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v3.md new file mode 100644 index 000000000..6744a06aa --- /dev/null +++ b/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v3.md @@ -0,0 +1,15 @@ +# STEP-047 Playback Tick 根因修复 + +状态:IMPLEMENTED / WAITING FOR ANDROID RE-VALIDATION + +## Android Validation Round 2:PARTIAL / NEEDS FIX + +PLAY 和 speed JNI command 均返回 SUCCESS;短暂的 `TIME_MIX_UPDATE` 后 mix 停止,但 State/Trail/Composite 与 RenderThread 持续运行。根因是 `RasterArrayTimelineRuntime::tick()` 只在 `RenderRasterParticleLayer::update()` 中调用;该函数只在 render tree/update 生命周期进入,不是每个已构建 drawable frame。 + +## 修复 + +将 Source-owned timeline tick 接入既有 `ParticleGpuValidationTweaker::execute()` 的 State pass。State drawable 每个正式粒子 simulation frame 执行一次;Trail、Composite 与 world copy 不 tick。因此不需要 Android Timer、每帧 JNI、新线程或循环。Tweaker 将本帧 Core mix 写回现有 resources UBO;现有 layer update 仍负责资源/窗口变更。 + +新增约每秒一次 `PLAYBACK_TICK` heartbeat,含 state、speed、cursor、mix、window、fieldUploadCount、simulationResetCount。 + +Round 3 P1--P4 待用户 Android 验证。未执行 Android Gradle、ADB 或 Jest。 diff --git a/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v4.md b/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v4.md new file mode 100644 index 000000000..ba64cb506 --- /dev/null +++ b/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v4.md @@ -0,0 +1,28 @@ +# STEP-047 Runtime Timeline 与 Playback 最终验收 + +状态:CLOSED / PASS + +功能代码最终 Android 验证基线:`1343929474796b1313489ee9fecbf23b949580a7`(`fix: keep particle timeline playback ticking`)。本文件后续的文档收尾提交不是功能实现提交。 + +## 历史与最终结论 + +- 初次实现后 Android Round 1 为 `PARTIAL / NEEDS FIX`:主链和 reuse 通过,但 Timeline 过短、状态机交互、异常入口和可观测性需要修正。 +- 集中修复后 Android Round 2 仍发现 tick 仅在非连续 layer update 中执行;`0609199c...` 修正 playback 验收控制,`134392947...` 将 tick 接入连续 State drawable frame。 +- Android 最终复验完成,STEP-047 `CLOSED / PASS`。 + +## Android 最终验收 PASS + +- 持续 Playback Tick、Pause/Resume、0.5x/1x/2x、Seek、Step Prev/Next 均通过。PAUSE 冻结 cursor/mix 而粒子 simulation 继续;Resume 不补算暂停时间。 +- PLAYING seek 保持 PLAYING,PAUSED seek 保持 PAUSED,Seek END 进入 ENDED,ENDED seek 非末尾可回到可继续状态;跨非连续 window 的可解释 reset 合法。 +- GAP 在 boundary `WAITING/GAP_HOLD`、mix=1 时保持最后有效 Field 且不黑屏;RECOVER 后继续。INVALID NEXT 以 `TIMELINE_REJECTED reason=INVALID_NEXT` fail-closed,当前有效窗口与 Renderer 保持,Recover 后继续。 +- 自动推进保持 40-04 契约:`OLD_T1 → NEW_T0` reuse,只上传 NEW_T1,`simulationRebuild=false`、`trailCleared=false`;正常播放不 reset simulation、不 clear trail。 +- Preload/Cache PASS:`TIMELINE_PRELOAD` 与 `TIMELINE_CACHE_HIT` 可见。Validation timeline 为 snapshots=6、cacheCapacity=8,未出现 CACHE_EVICT 是容量未满的正常结果。 +- VIS runtime、MAP/MORE 与 Compact Validation UI 均通过。 + +## 非阻塞记录 + +Validation 状态栏对自动 MIX、WAITING/INVALID 等状态的可观测性仍可优化,分类为 Validation UI observability 的 `NON-BLOCKING` 技术债;不是 Timeline Core 或 Renderer failure,不阻塞本阶段关闭。 + +`TimeStats: RenderEngineTimes are already at its maximum size[64]` 是 Android/RenderEngine 环境噪音;未伴随 crash、ANR、GL resource failure 或 Renderer failure,不判定为粒子系统失败。 + +本收尾未修改功能代码,未执行 Android Gradle、ADB 或 Jest。 diff --git a/docs/50_Step记录/STEP-049_Provider-neutral_Field_Data_Adapter与Runtime输入契约_v1.md b/docs/50_Step记录/STEP-049_Provider-neutral_Field_Data_Adapter与Runtime输入契约_v1.md new file mode 100644 index 000000000..e7346dec7 --- /dev/null +++ b/docs/50_Step记录/STEP-049_Provider-neutral_Field_Data_Adapter与Runtime输入契约_v1.md @@ -0,0 +1,40 @@ +# STEP-049 Provider-neutral Field Data Adapter 与 Runtime 输入契约 + +- 状态:进行中(等待 Android 综合验收) +- 目标:建立不包含 provider/network/vendor 语义的 C++ Field descriptor、payload adapter 与 immutable Snapshot 输入边界,并复用既有 Runtime Field Window。 +- 基线:`f0954476783b60c6ad4fdde7229657abed2e70f6`,`particle/main`,与 `origin/particle/main` 一致。 + +## 实际实现结构 + +新增 `ParticleFieldDescriptor`、`ParticleFieldSnapshot`、`ParticleFieldDataAdapter` 与 `ParticleFieldAdapterResult`,位于 `include/mbgl/particle/field/field_data_adapter.hpp` / `src/mbgl/particle/field/field_data_adapter.cpp`。descriptor 仅描述 field identity、类型/域、有效时间、网格 bounds、U/V canonical layout、format、stride/byte length、no-data、版本和可选 checksum;不包含 URL、token、provider 或网络对象。 + +`ParticleFieldDataAdapter::decode()` 校验 descriptor、`interleaved-uv` + `float32-le`、尺寸/stride、payload 长度和 finite data,生成拥有自有 `std::vector` 的 Snapshot。`OfflineFixtureParticleAdapter` 仍保留 `fixture-v1` schema、fixture whitelist、metadata 与 checksum 校验,但最终通过正式 adapter 输出 Snapshot;因此 fixture 不是 production adapter 的限制。 + +## 错误与接线 + +结构化错误包括 `InvalidDescriptor`、`UnsupportedFormat`、`DimensionMismatch`、`PayloadSizeMismatch`、`InvalidTimestampOrIdentity`、`NonFiniteData` 和 `DecodeValidationFailure`,错误码与 message 分离。`RasterArraySource` 继续只调用 `updateRuntimeFieldWindow()`;没有第二套 T0/T1 状态机,`OLD_T1 → NEW_T0`、正常只上传 NEW_T1、Duplicate NOOP、MIX property-only 和 fail-closed 语义未改。 + +Renderer 仅将日志字段从 fixture identity 改为 neutral `fieldID`,未引入 provider/network/vendor、decode、Timeline 或缓存业务。Timeline、Playback、WeatherColorLayer、Android JNI 和 GPU simulation 未改。 + +## 验证与待验收 + +- Linux OpenGL `mbgl-test-runner` 构建:PASS。 +- 定向测试:`Source.ParticleFieldDataAdapter*`、`Source.RasterArray*` 共 11 项,PASS。 +- `git diff --check`:PASS。 +- Android Gradle/APK/ADB/设备测试:按任务要求未执行,等待用户一次综合验收,覆盖 EAST/NORTH/VORTEX、Playback、neutral valid/invalid、payload mismatch、reuse、MIX、no black screen。 +- Jest:未新增、未执行。 + +## 必做项 + +- [x] neutral descriptor + immutable Snapshot +- [x] C++ Data Adapter 与 structured error +- [x] fixture wrapper 复用正式 adapter +- [x] RasterArraySource 继续复用 Runtime Field Window +- [x] 未新增 Provider Framework / network / vendor 逻辑 +- [x] Linux 定向构建与测试 +- [x] Step、开发记录与完成报告 +- [ ] Android 综合验收与 `CLOSED / PASS` + +## 阻塞与下一步 + +当前无代码阻塞;正式关闭依赖用户执行任务书规定的一次 Android 综合验收。Codex 不执行 Android build/install/ADB。 diff --git a/docs/50_Step记录/STEP-049_Provider-neutral_Field_Data_Adapter与Runtime输入契约_v2.md b/docs/50_Step记录/STEP-049_Provider-neutral_Field_Data_Adapter与Runtime输入契约_v2.md new file mode 100644 index 000000000..92a669a2a --- /dev/null +++ b/docs/50_Step记录/STEP-049_Provider-neutral_Field_Data_Adapter与Runtime输入契约_v2.md @@ -0,0 +1,46 @@ +# STEP-049 Provider-neutral Field Data Adapter 与 Runtime 输入契约 — Timeline 回归修复 + +- 状态:CLOSED / PASS +- Starting HEAD:`dcedbe7394e74b80d91f00315a9956dfaea35229` +- 分支:`particle/main` + +## Android 回归与根因 + +STEP-049 后 Android 的 Runtime Window 和 Renderer 正常,但 Timeline 没有 READY。失败链路定位为 `RasterArraySource::tryLoadRuntimeField()` 调用 fixture wrapper 时传入 generation `0`;正式 `ParticleFieldDataAdapter` 正确拒绝 `generation == 0`。Runtime Window 初始 field 使用已分配的全局 generation,因此未受影响。Timeline 原本计划在 `tryInitializeTimeline()` 中才给 candidate 分配 generation,顺序晚于 adapter 校验。 + +## 修复点与语义 + +candidate 在 metadata/payload 完整后、调用正式 adapter 前分配 `++nextRasterArrayFieldGeneration`,因此每个进入 Core 的 Snapshot 都满足非空 identity 与 `generation > 0`。Timeline 构建阶段不再重复改写 generation。没有放宽 adapter contract,没有新增 adapter/Timeline/Window,也没有改变 Timeline 状态机、Runtime Field Window、Renderer 或 Android Timeline ownership。 + +低频 rejection 路径补充 error code、fieldId、generation、validTime 和 candidate index 上下文,不输出 payload。 + +## 测试 + +新增 `RasterArrayTimelineCandidatesReachReadyThroughFieldAdapter`,实际加载 EAST/NORTH 初始窗口和 VORTEX candidate,覆盖 fixture wrapper → formal adapter → candidate preload → Timeline READY,并断言 candidate identity/generation 合法、PLAY 成功。原有 generation=0/invalid descriptor 结构化拒绝测试继续通过。 + +- Linux OpenGL `mbgl-test-runner`:PASS +- 定向测试:12/12 PASS +- `git diff --check`:PASS +- Android/APK/ADB/Jest:未执行 + +## Validation UI + +仅修改 Android Validation UI 展示:Window 与 Timeline 分离显示,Timeline 使用 `TL NOT READY`、`TL READY`、`TL PLAY`、`TL PAUSE`、`TL WAIT`、`TL INVALID`,MIX 保持显示。未修改 Core 状态机。 + +## 待复验与阻塞 + +代码无已知阻塞。Android 最终综合复验已通过,STEP-049 正式关闭。 + +## Android 最终复验 + +- Timeline READY / PLAY:PASS;auto MIX Core 与 Playback Tick 持续推进:PASS。 +- Runtime Window Boundary:PASS;A/B → B/C → C/D、`OLD_T1 → NEW_T0` reuse、only `NEW_T1` upload:PASS。 +- 正常连续播放:`simulationRebuild=false`、`trailCleared=false`;Window 内 MIX 的 field upload/reset/style reload delta 均为 0:PASS。 +- Pause / Resume:PASS;Pause 冻结 forecast timeline、粒子 simulation 继续,Resume 后恢复。 +- Step Prev / Next、Seek Start / Mid / End:PASS;非连续 Seek 引起的 `FIELD_NO_OVERLAP` 属允许语义。 +- Gap Hold / Recover:PASS;保持最后有效 Field、Renderer 持续、不黑屏。 +- Invalid Next / Recover:PASS;`INVALID_NEXT` 后 fail-closed,保持 Field,Recover 后继续 boundary/reuse。 +- Duplicate T1 NOOP:PASS;不增加 Field Upload、Simulation Reset 或 Style Reload。 +- Renderer fail-closed / 不黑屏 / 无 Particle crash:PASS。 + +Validation UI 的 MIX 百分比文本可能滞后,但 Core 日志中的 MIX 正常推进;分类为 `NON-BLOCKING observability`,不构成 Timeline、Playback、Window、Renderer 或 Adapter failure。 diff --git a/docs/50_Step记录/STEP-051_Local_Production-like_Field_Feed与Core_Snapshot_Submit_Bridge_v1.md b/docs/50_Step记录/STEP-051_Local_Production-like_Field_Feed与Core_Snapshot_Submit_Bridge_v1.md new file mode 100644 index 000000000..515506a2e --- /dev/null +++ b/docs/50_Step记录/STEP-051_Local_Production-like_Field_Feed与Core_Snapshot_Submit_Bridge_v1.md @@ -0,0 +1,85 @@ +# STEP-051 Local Production-like Field Feed 与 Core Snapshot Submit Bridge + +- 状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION` +- Starting HEAD:`885e7aebef7fb5d02acaed57f9dcf98b8d9e9ba3` +- 分支:`particle/main` + +## 范围与数据链路 + +本阶段建立了不依赖网络、provider 或 vendor 的本地 production-like 入口: + +`local metadata + binary payload → ParticleFieldDescriptor → ParticleFieldDataAdapter → ParticleFieldSnapshot → RasterArraySource → Core Timeline/Runtime Window`。 + +入口类为 `mbgl::particle::LocalParticleFieldFeed`(`include/mbgl/particle/field/local_particle_field_feed.hpp`)。它只解析 provider-neutral metadata 和内存中的 payload,不负责文件系统、asset、HTTP 或缓存。 + +新增的 `maplibre-particle-field/v1` metadata contract 位于 `particle-production/field-00.json` 至 `field-03.json`,Android 对应资源位于 `platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/`。字段包括 `fieldId`、`generation`、`validTime`、`dataVersion`、`bounds`、`grid.width/height`、`vector.layout/encoding/noData` 和 `payload.byteLength`。payload 使用既有 canonical float32 binary asset 作为最小离线载荷,但不再经过 fixture schema/wrapper。 + +## Ownership 与 Core bridge + +- `fieldId`:producer 在 metadata 中稳定赋值;Renderer、Timeline、Android 不生成。 +- `generation`:producer/source 赋值。`ParticleFieldDescriptor.generation` 必须非零,并且必须与 Adapter 参数一致;Adapter 不把 0 改成 1,Timeline/Runtime 不猜测默认值。 +- `validTime`:producer 赋值,`LocalParticleFieldFeed` 要求非零;Timeline 直接消费 Snapshot 的真实时间并要求严格递增。 +- `ParticleFieldSnapshot` 继续拥有 immutable canonical field 内容和 Core metadata,不含 provider/network/GPU ownership。 + +`RasterArraySource` 新增 `RasterArrayLocalFieldDescriptor` 和 local resource candidate。`submitInitialRuntimeFieldWindow(T0, T1)` 是薄的 Core submit bridge:检查 Snapshot、兼容性和严格时间顺序后建立 authoritative 初始窗口;后续仍通过现有 `updateRuntimeFieldWindow()`,保持 `OLD_T1 → NEW_T0` reuse、only `NEW_T1` upload 和 Duplicate NOOP。没有复制 Timeline 或 Runtime Window。 + +Initial T0/T1 由同一 Source 的 local candidates 形成;Timeline preload 使用所有 Snapshot 的 `validTime`。正常连续 playback 仍保持 `simulationRebuild=false`、`trailCleared=false`,MIX 为 property-only。 + +## Android validation + +TIME/NORMAL validation 入口现可选择 production-like source(UI 状态标出 `PRODUCTION` 和 `TL` 状态);旧 fixture EAST/NORTH/VORTEX 路径保留。Android Host 只提交 style/resource 命令,不实现解析、Timeline 或 Window 状态机。 + +本阶段未执行 Android Gradle、APK build/install、ADB 或 App 测试,等待用户一次综合验收。MIX 文本滞后仍是 `NON-BLOCKING observability`;Rotation/Viewport 自动恢复仍保留为技术债,本阶段未触及 lifecycle/surface 路径。 + +## 测试与边界 + +新增 `Source.LocalProductionFeedSubmitsSnapshotsWithProducerTime` 和 `Source.LocalProductionFeedRejectsMissingGeneration`,覆盖四个 production-like 时间片、Adapter、初始 submit、Timeline READY/PLAY 及正式非法校验。原有 READY/PLAY、Pause/Resume、Window Boundary、Gap、Invalid、Recover、Duplicate NOOP 测试继续运行。 + +未修改 Renderer 业务边界、WeatherColorLayer、Timeline state machine、Runtime Field Window 语义;未引入 Weather Service、Provider Framework、Network、第二套 Adapter/Timeline/Window。 + +## 验证结果 + +- Linux OpenGL `mbgl-test-runner` 精确构建:PASS。 +- STEP-051 及既有相关定向测试:14/14 PASS。 +- `git diff --check`:PASS(提交前执行)。 +- Jest:未执行。 + +当前状态只能记为:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。 + +## Android Validation Host 入口补充 + +在现有 Particle Core Layer Validation 页面的 MORE 菜单新增: + +- `PRODUCTION FEED`:切换到 STEP-051 已有 `production-fields` source,加载四个 local metadata asset 与既有 binary payload,继续进入 Core Adapter、Snapshot、initial T0/T1、Timeline READY/PLAY 路径。 +- `INVALID PROD`:加载 `field-invalid.json`,其中 `fieldId`、`generation`、`validTime` 明确非法,用于观察正式 Adapter rejection/fail-closed;不放宽 contract。 + +旧 `UNIFORM EAST`、`UNIFORM NORTH`、`VORTEX` 与 TIME 控件仍保留。Host 仅触发低频 style/resource 命令;没有新增 JNI command、Android Timer、Timeline、Window 或 MIX state。状态栏在 production 模式显示 `PRODUCTION · TL ...`,日志仍由 Core 输出 `PRODUCTION_FEED_LOAD`、`SNAPSHOT_SUBMIT` 和 Timeline 事件。 + +本次没有修改 C++ Core、Timeline、Runtime Field Window、Renderer、Adapter contract、MIX observability 或 Rotation/Viewport。Android Gradle/APK/ADB/实机验证仍由用户执行;状态继续为 `IMPLEMENTED / WAITING_ANDROID_VALIDATION`。 + +## Android Validation Host Kotlin 编译修复 + +- 错误:`ParticleCoreLayerValidationActivity.kt:418` 的 `Unresolved reference 'reason'`。 +- 根因:`loadValidationStyle()` 调用 `validationStyle()` 时未透传已有的 `reason` 参数。 +- 修复:将调用改为 `validationStyle(active, enabled, reason)`,并将函数签名增加 `reason: String`。 +- `PRODUCTION FEED → field-00.json` 与 `INVALID PROD → field-invalid.json` 的判断逻辑保持不变。 +- 未修改 Core、Timeline、Runtime、Renderer 或 production metadata contract;Android build 尚待用户手工复验。 +- STEP-051 状态仍为:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。 + +## Android 最终复验与关闭 + +用户已在 Windows 11 / Android 设备 `192.168.1.146` 基于最新 Host 修复版本完成手工复验,结论如下: + +- production-like T0~T3 加载:PASS;`PRODUCTION_FEED_LOAD` 可见。 +- initial T0/T1 submit:PASS;`SNAPSHOT_SUBMIT kind=INITIAL_T0_T1` 可见。 +- Timeline READY、Playback、自动 MIX:PASS。 +- 至少两次 Window Boundary:PASS;`OLD_T1 → NEW_T0` reuse、only NEW_T1 upload:PASS。 +- 正常 Boundary:`simulationRebuild=false`、`trailCleared=false`:PASS。 +- Duplicate T1:PASS,结果为 NOOP,upload/reset 计数不增加。 +- Invalid production metadata:PASS;Adapter reject、fail-closed、保持最后有效 Field,Renderer 持续运行。 +- Renderer 连续运行、无黑屏、无 crash:PASS。 +- Pause/Resume:沿用 STEP-049 已通过回归基线;本轮日志未作为独立阻塞项重新执行。 + +Validation UI 的 MIX 文本可能滞后、WAITING/INVALID 可视提示仍不够强,继续标记为 `NON-BLOCKING observability`,本次不修复、不重新触发 Android build/install。Runtime Window、Core-owned Timeline/Playback、Pause、Gap/Invalid fail-closed 与 Renderer 边界保持不变。 + +最终状态:`CLOSED / PASS`。 diff --git a/docs/50_Step记录/STEP-053_Windy-style_Canonical_Field与Host_MapLibre_Submission_Contract_v1.md b/docs/50_Step记录/STEP-053_Windy-style_Canonical_Field与Host_MapLibre_Submission_Contract_v1.md new file mode 100644 index 000000000..6cf056ff7 --- /dev/null +++ b/docs/50_Step记录/STEP-053_Windy-style_Canonical_Field与Host_MapLibre_Submission_Contract_v1.md @@ -0,0 +1,152 @@ +# STEP-053 Windy-style Canonical Field 与 Host→MapLibre Submission Contract + +- 状态:`CLOSED / PASS` +- Starting HEAD:`285d23a58f6ecb16f360748df688d9d03dad6b3d` +- 分支:`particle/main` +- 目标:让已经取得并规范化规则二维 U/V Field 的外部 Host,以低频 `metadata + contiguous binary payload` 提交给 MapLibre Native,并复用 STEP-049/051 的 Adapter、Snapshot、Timeline、Runtime Window 与 Renderer。 + +## 设计结果 + +正式入口为: + +`Host metadata + binary UV bytes → RasterArraySource::submitHostField() → LocalParticleFieldFeed → ParticleFieldDataAdapter → ParticleFieldSnapshot → existing initial window / Timeline → RasterParticleLayer` + +没有新增第二套 Descriptor、Snapshot、Timeline、Runtime Window 或 Field normalizer。`ParticleFieldDescriptor`、`ParticleFieldDataAdapter`、`ParticleFieldSnapshot` 继续是唯一 canonical 数据契约;`LocalParticleFieldFeed` 继续只负责把 `maplibre-particle-field/v1` metadata 映射到 Descriptor 并调用正式 Adapter。 + +新增 `raster-array` style validation seam: + +```json +{"type":"raster-array","host-field-submission":true} +``` + +该 Source 不读取 URL、asset 或网络。Host 通过 Android `MapLibreMap.submitRasterArraySourceHostField(sourceId, metadata, byte[])` 或 Core `RasterArraySource::submitHostField(metadata, payload)` 一次提交一个独立时间片。 + +## Canonical Field Contract + +第一版 canonical contract 固定为: + +- 规则二维 Grid,`cell = y * width + x`,不为每个 cell 重复携带 lat/lon。 +- grid origin 为 north-west;row 为 north-to-south;column 为 west-to-east。 +- payload 为 contiguous `float32-le`、`interleaved-uv`;每 cell 8 bytes。 +- `velocity[2 * cell] = U`,U 为 eastward-positive。 +- `velocity[2 * cell + 1] = V`,V 为 northward-positive。 +- bounds、width/height、stride/noData、expected byte length 继续由既有 Descriptor 表达。 +- `fieldId`、`generation`、`validTime`、`dataVersion` 均由 Host/producer 在进入 Adapter 前拥有并稳定赋值;MapLibre 不猜默认值。 +- 同一提交序列的 `validTime` 使用同一时间基准并严格递增;Timeline 按毫秒差值推进。 +- payload 不使用巨大 JSON、字符串 float list 或 `Array`;Android 当前直接使用 `byte[]`,为 STEP-054 的 ArrayBuffer/TypedArray/native buffer/file reference 保留二进制边界。 + +`ParticleFieldDescriptor.fieldSha256` 仍是可选字段,但当前 `maplibre-particle-field/v1` Host metadata 未启用 checksum 校验;本阶段没有伪造 checksum mismatch PASS。若后续正式启用,必须在 Adapter ingress 增加明确校验和负向测试。 + +## Submission Routing + +- 第一个合法 Snapshot:暂存为 pending T0,返回 `ACCEPTED_PENDING_INITIAL_WINDOW`。 +- 第二个合法 Snapshot:调用既有 `submitInitialRuntimeFieldWindow(T0,T1)`,建立唯一 authoritative initial window 和 Core-owned Timeline,返回 `INITIAL_WINDOW_READY`。 +- 后续合法 Snapshot:受 Timeline 既有 capacity=8 约束,通过线程安全 append 加入同一个 Timeline,返回 `ACCEPTED_NEXT_SNAPSHOT`。 +- Duplicate:按既有 canonical GPU content identity 返回 `DUPLICATE_NOOP`,不追加、不通知 Renderer、不上传 Field。 +- Invalid:Adapter、compatibility、generation/validTime monotonicity 或 capacity 失败均结构化拒绝;pending/Timeline/最后有效 Field 不变。 + +Timeline append 仅扩展低频 Snapshot arrival;cursor、speed、pause、gap/invalid、playback clock 和每帧 tick 状态机均未改。已结束的尾窗口收到后续 Snapshot 时回到 Paused tail,可由 Host 的低频 PLAY 命令继续,不从 T0 重建第二套 Timeline。 + +## STEP-051 兼容与冻结边界 + +- 原 `production-fields` asset 路径、fixture 路径和 Validation 入口保留。 +- `OLD_T1 → NEW_T0` reuse、only NEW_T1 upload、Duplicate NOOP、Invalid fail-closed、MIX property-only 保持。 +- 正常 boundary 的 `simulationRebuild=false`、`trailCleared=false` 语义未改。 +- Timeline/Playback 继续 Core-owned;Android 不持有 cursor/window,不做 Timer/Handler/Coroutine per-frame JNI。 +- `RasterParticleLayer` 未加入 provider、network、vendor、manifest、下载、缓存或解析逻辑。 +- `WeatherColorLayer` 与 `RasterParticleLayer` 继续分离。 + +## Android Validation Host + +MORE 菜单新增: + +- `HOST FIELD SUBMIT`:Validation Host 自己读取 STEP-051 的 T0~T3 metadata 和 canonical binary bytes,然后逐片调用 Host submission API。数据顺序为 uniform east、uniform north、vortex、uniform east,可观察 U/V 和时间变化。 +- `HOST DUPLICATE`:重新提交当前最后一个 Host Field,预期 `DUPLICATE_NOOP`。 +- `INVALID HOST`:提交既有 `field-invalid.json`,预期 Adapter reject 且保持最后有效 Field。 + +旧 `PRODUCTION FEED`、`INVALID PROD`、fixture、TIME、VIS、COUNT、MAP 等入口不删除。Validation Host 只模拟未来 React Native Host 已经持有数据,不 fetch 网络,不实现 Timeline。 + +## 实际修改文件 + +- `include/mbgl/particle/field/field_data_adapter.hpp` +- `include/mbgl/style/sources/raster_array_source.hpp` +- `src/mbgl/style/sources/raster_array_source.cpp` +- `src/mbgl/style/sources/raster_array_source_impl.hpp` +- `src/mbgl/style/conversion/source.cpp` +- `test/style/source.test.cpp` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java` +- `platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp` +- `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` +- `platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_particle_core_layer_validation.xml` +- 本 Step、`粒子开发记录.md` 与中文完成报告。 + +没有新增或修改 canonical dataset asset;用户正式测试数据尚未提供,本阶段按任务书复用 STEP-051 T0~T3 binary validation dataset。 + +## Linux/Core/OpenGL 验证 + +- `mbgl-test-runner` 精确构建:PASS。 +- `mbgl-render` 精确构建:PASS。 +- `mbgl-render-test-runner` 精确构建:PASS。 +- 新增 Host/canonical 定向测试:4/4 PASS。 +- `Source.*` 回归:44/44 PASS。 +- `Layer.RasterParticleCountProperties`、`Layer.RasterParticleVisualProperties`:2/2 PASS。 +- JNI Java/interface/native declaration/registration 与 Validation resource ID 静态核对:PASS。 +- `xmllint`:环境未安装,未执行;不伪报 PASS。 +- Android Gradle/APK/ADB/App/设备:未执行。 +- Jest:未执行。 + +## Android 待验收 + +用户需在 Windows 11 对设备 `192.168.1.146` 完成一次 OpenGL APK 综合验收:Host T0~T3 submit、Timeline READY/PLAY、east/north 方向、MIX、至少两次 boundary、reuse/new-T1-only、Duplicate、Invalid fail-closed、Renderer continuity、无黑屏、无 crash。用户正式数据若后续提供,仍需用同一 contract 单独完成视觉验收。 + +## 非目标与技术债 + +未实现 Weather Service、HTTP/provider、React Native binding、Manifest/Catalog、下载/重试/cache、GRIB/NetCDF/vendor decoder、WeatherColorLayer、第二套状态机。Validation UI MIX/WAITING/INVALID observability 继续为 NON-BLOCKING;Rotation/Viewport 技术债未处理。 + +当前状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。 + +## Android Validation Host 接线补充 + +Android Validation Host 原有 STEP-053 控件和 JNI/API 已存在,但 MORE 菜单将入口显示为 `HOST FIELD SUBMIT`,未紧邻 `PRODUCTION FEED`,且 Kotlin 在读取本地 canonical dataset 后没有单独输出 dataset-load 事件。该可观察性不足会使手工验收难以一眼区分 STEP-051 的 `PRODUCTION FEED` 与 STEP-053 Host submission。 + +- MORE 菜单现在按 `PRODUCTION FEED → HOST FIELD → INVALID PROD → HOST DUPLICATE → HOST INVALID` 显示相关入口;旧 `PRODUCTION FEED` 和 `INVALID PROD` 的语义未改。 +- `HOST FIELD` 创建 `{"type":"raster-array","host-field-submission":true}` source,并在 style ready 后读取既有 T0~T3 metadata/binary resources,通过 `MapLibreMap.submitRasterArraySourceHostField()` 逐片提交。 +- Kotlin 对每个时间片输出低频 `HOST_FIELD_DATASET_LOAD`,带 metadata resource、payload resource 和 payload bytes;Native 既有 `HOST_FIELD_SNAPSHOT_READY`/`HOST_FIELD_INITIAL_WINDOW_READY`/`HOST_FIELD_NEXT_SUBMIT` 继续输出 fieldId、generation、validTime 和结果。 +- `HOST DUPLICATE` 重新提交 Host dataset 的最后一个 Field,复用同一 Host submit API,预期 `DUPLICATE_NOOP`;`HOST INVALID` 在已存在有效 Host window 时提交 `field-invalid.json`,经同一 API 结构化拒绝并保持 last valid field。两者均不通过 style reload 冒充 Host submission。 +- 未新增 JNI forwarding,复用既有 `MapLibreMap → NativeMapView → RasterArraySource::submitHostField()` 薄桥。未修改 Adapter、Timeline、Runtime Field Window、Renderer 或任何 provider/network 逻辑。 + +Android Gradle、APK、ADB 和设备验收仍由用户手工执行;状态继续为:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。 + +## Android Host Submission 最终验收 + +用户已在 Android Validation Host 完成综合技术验收:`STEP-053 Android Host Submission Validation: PASS`。 + +| 验收项 | 结果 | +| --- | --- | +| `HOST FIELD` 与 STEP-051 `PRODUCTION FEED` 路径分离 | PASS | +| canonical `production-wind-t00` ~ `production-wind-t03` | PASS | +| Host Submission → Adapter → Snapshot → Timeline → Runtime Window → GPU Renderer | PASS | +| T0/T1 Initial Window、T2/T3 Next Submission | PASS | +| `PLAYBACK_PLAY`、Automatic MIX | PASS | +| production→production Window Boundary(至少 2 次) | PASS | +| `OLD_T1 → NEW_T0` reuse、only NEW_T1 upload | PASS | +| 正常 boundary `simulationRebuild=false`、`trailCleared=false` | PASS | +| `HOST DUPLICATE` / `DUPLICATE_NOOP` | PASS | +| `HOST INVALID` / Adapter fail-closed / last valid field retained | PASS | +| Renderer continuity(`ParticleRender stateSeed=CONTINUE`)、无黑屏、无 crash | PASS | + +两次已确认的连续 boundary 为: + +`production-wind-t00 / production-wind-t01 → production-wind-t01 / production-wind-t02`,以及 +`production-wind-t01 / production-wind-t02 → production-wind-t02 / production-wind-t03`。 + +正常 boundary 的 `fieldUploadCount` 从 `4 → 5 → 6`,每次只增加一个 NEW_T1 upload。`simulationResetCount=1` 仅来自旧 fixture `uniform-east / uniform-east` 首次切换到 Host `production-wind-t00 / production-wind-t01` 的允许初始化(该切换可出现 `simulationRebuild=true` 与 `trailCleared=true`);它不是 production→production boundary 回归。 + +用户正式风场数据集为 `NOT PROVIDED / NOT VALIDATED`。这不阻止本 Step 关闭:STEP-053 的目标是冻结并验证 Host→MapLibre canonical Field Submission Contract,已由本地 canonical T0~T3 驱动实际 Timeline、Runtime 和 GPU Renderer 完成验证。后续正式数据只是在同一 contract 下替换或追加验证。 + +Validation UI 的 MIX 文本可能滞后、WAITING/INVALID 视觉不够明显,继续归类为 `NON-BLOCKING observability`,不是 Core、Timeline、Runtime 或 Renderer 缺陷。本次仅文档收尾,未修改功能代码,也未执行 Android Gradle/APK/ADB/Jest。 + +最终状态:`CLOSED / PASS`。 diff --git a/docs/50_Step记录/STEP-054_真实WeatherV2风数据提取与Canonical测试数据生成_v1.md b/docs/50_Step记录/STEP-054_真实WeatherV2风数据提取与Canonical测试数据生成_v1.md new file mode 100644 index 000000000..a52df121f --- /dev/null +++ b/docs/50_Step记录/STEP-054_真实WeatherV2风数据提取与Canonical测试数据生成_v1.md @@ -0,0 +1,58 @@ +# STEP-054 真实 WeatherV2 风数据提取与 Canonical 测试数据生成 + +- 状态:`IMPLEMENTED / WAITING_NEXT_ANDROID_INTEGRATION_VALIDATION` +- Starting HEAD:`5fc922785d8ec007127d48cd3ab5dadce270b947` +- 分支:`particle/main` +- 目标:将 WeatherV2 ECMWF HRES 的真实 `WVECBIN1` 风场无损转换为已冻结的 `maplibre-particle-field/v1` Android 测试 asset;不实现网络、Provider、React Native Binding 或 Android 综合绘制。 + +## 实际数据与格式依据 + +读取 WeatherV2 源目录: + +`/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-hres/wind/2026080600` + +按既定时次生成 T0~T3:`2026080700`、`2026080706`、`2026080712`、`2026080718`。四帧均实际存在,均为日本及周边的完整区域 grid:`513×769`、bounds `112.5, 0.0, 157.5, 55.77657301866769`,单源文件 `5,128,989` bytes。 + +WeatherV2 `src/dev/core/bin_export.py` 的 `_export_vector_target()` 先以 `decode_wind()` 从原 Windy `wm_grid_257` tile 解码 U/V,再按 `WVECBIN1 + uint32-le header length + JSON header + U float32-le plane + V float32-le plane + speed float32-le plane + valid uint8 plane` 写入 `wind_vectors.bin`。数组 row-major;Web Mercator tile y 向南增加,因此 row 是 north-to-south、column 是 west-to-east。 + +`src/dev/layers/wind.py` 明确读取 `decoded_r` 为 U、`decoded_g` 为 V;writer metadata 说明 U eastward-positive、V northward-positive、screen y 使用 `-v`。QBIN exporter 和 NSFT exporter 都重新使用同一 `decode_wind()` Field:QBIN `u_q/v_q` 的 quantization metadata 明确为 `m/s`(`scale=0.01`),NSFT wind 也从该 Field 量化。因此本次原始 U/V 单位已由 writer/下游 decoder 确认为 `m/s`,没有 kt 换算、scale/offset 还原或额外量化。 + +`wind_vectors.bin` 是 WeatherV2 pipeline 对完整已拼接 U/V grid 的 particle/vector export;QBIN 是同源单 tile量化分发格式,NSFT 是同源单 tile量化 PMTiles 格式。三者共享 decoded U/V、valid 和 grid/bounds 语义,但本任务直接读取完整 WVECBIN1,不经 QBIN/NSFT 拼瓦片。 + +## 转换工具与输出 + +新增独立工具: + +- `tools/weatherv2_wind_canonical/convert_weatherv2_wind.py` +- `tools/weatherv2_wind_canonical/README.md` + +工具参数为 `--run-dir`、`--valid-time`、`--output`;源文件仅只读。它严格校验 magic、embedded header 与 sidecar 一致性、fields/dtype、长度、bounds 和有效 U/V,再输出既有 `maplibre-particle-field/v1` metadata 与 `float32-le interleaved-uv` payload。输出 `validTime` 是真实 UTC epoch milliseconds;`generation` 和 `dataVersion` 同为 producer-owned epoch milliseconds,四帧严格递增。 + +输出目录: + +`platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-weatherv2/ecmwf-hres-wind-20260807/` + +其中 `dataset.json` 只是开发期四帧索引,不是 Weather Manifest,也不进入 Snapshot。每个 validTime 子目录包含 `metadata.json`、`field.f32`、`conversion_summary.json`。 + +未重采样、未裁剪;每个 canonical payload 保留原完整 `513×769` 范围,`3,155,976` bytes。有效 U/V 逐 float32 bit-exact 复制;source `valid=0` 或非有限 U/V 会映射为既有 `noData=-9999` 的 U/V pair,绝不写成 `(0,0)`。当前四个实际 frame 均为 `394,497` valid、`0` invalid,但转换器仍保留该 fail-closed mapping。 + +## 生成结果与验证 + +| validTime | SHA-256 | U min/max (m/s) | V min/max (m/s) | speed min/max (m/s) | +| --- | --- | --- | --- | --- | +| 2026080700 | `363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283` | -35.1724 / 22.5011 | -25.4723 / 28.2009 | 0.0037 / 35.2783 | +| 2026080706 | `ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451` | -37.1826 / 24.1255 | -28.2276 / 25.0581 | 0.0105 / 37.7632 | +| 2026080712 | `d86a6a51fd5216cf9368756962a5366fd32a627afd40cfde235ba7358c02c5a0` | -37.3931 / 24.7693 | -28.2666 / 27.8099 | 0.0244 / 37.4291 | +| 2026080718 | `970785b0334a9d1511b8897874a81f681814a32b1e0b90e37cf64b77ca9fa39b` | -31.9560 / 28.5575 | -26.1394 / 27.1316 | 0.0210 / 33.6781 | + +- 四帧 metadata 可解析、payload length 与 `width×height×8` 一致,validTime 严格递增,四个 SHA-256 均不同:PASS。 +- 每帧中心、四角附近和内部有效样本 source→canonical U/V 的最大绝对误差均为 `0.0`:PASS。 +- T0 重复转换的 SHA-256 与已生成 payload 相同:PASS。 +- 转换器 Python syntax、正常转换和无效 source 的非零失败路径:PASS。 +- 既有 Core canonical/Adapter/Host ingress 定向测试:5/5 PASS。 + +## 冻结边界与未执行项 + +未修改 `ParticleFieldDescriptor`、`ParticleFieldSnapshot`、`ParticleFieldDataAdapter`、Timeline、Playback、Runtime Field Window、Renderer、Android Kotlin/JNI 或 WeatherV2 原始数据。未将 Weather Service、HTTP、Provider、QBIN/NSFT 网络、缓存或目录扫描加入 MapLibre Runtime。 + +未执行 Android Gradle、APK、ADB、设备/模拟器测试或 Jest。下一阶段才使用本 asset 做 MapLibre map + WeatherColorLayer + RasterParticleLayer 综合 Android 验收;本 Step 不提前进入该范围。 diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md new file mode 100644 index 000000000..acb0b48f5 --- /dev/null +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md @@ -0,0 +1,47 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 + +状态:IMPLEMENTED / WAITING_ANDROID_VALIDATION + +Starting HEAD:`da71c8784ec20ed830f5d23880718647d666cf2f` + +## 目标与数据边界 + +本阶段只复用 STEP-054 已生成的真实 WeatherV2 canonical 资产:`integration/particle-weatherv2/ecmwf-hres-wind-20260807/` 下的 2026080700、0706、0712、0718。数据保持原始 513×769 网格、`interleaved-uv/float32-le`、U=eastward、V=northward、单位 m/s 和原始 validTime;未重新生成、重采样、裁剪或伪造数据。 + +## 实现 + +- 新增独立 `weather-color` Style/Render Layer。其仅从现有 `RasterArraySource` 的 canonical T0/T1 snapshot 读字段,拥有独立的颜色 GPU texture/draw pass;不合并到 `RasterParticleLayer`。 +- 颜色 shader 对同一位置的 T0/T1 先还原 canonical U/V、按 Core mix 插值,再计算 `hypot(U,V)`;色带固定为 0–30 m/s,不做自动拉伸。 +- 字段 texture 以每个 snapshot 的显式 signed velocity scale 编码/还原,避免真实 m/s 数据被旧 `[-1,1]` 编码截断。 +- 粒子 GPU 字段采样改为 metadata bounds 的 Web Mercator UV。字段区域外保持 fail-closed;State 初始 seed 和越界恢复均使用字段 bounds,因此真实日本区域不再被当作全球 UV。 +- Android MORE 新增 `REAL WEATHER`:一次 style 初始化后,经既有 STEP-053 Host Submission Contract 依次提交真实 T0–T3;样式层级为 OSM 无 Key raster basemap → `weather-color` → `raster-particle` → Android HUD,并按 metadata bounds 自动 fit camera。 +- `REAL WEATHER` 状态随 Activity 保存/恢复。重建后自动再次以相同 metadata、payload 与 validTime 提交,不要求用户按按钮恢复;粒子 Trail FBO 仍由既有 viewport resize 路径重建。 + +## 所有权与冻结边界 + +Core 仍拥有 Timeline、Playback 和 Runtime Field Window。颜色层不 tick Timeline,`RasterParticleLayer` 仍是唯一的 playback tick 消费者。未修改 Adapter contract、Snapshot、Window A/B→B/C→C/D、Duplicate NOOP、Invalid/Gap fail-closed、Renderer provider/network 边界或 WeatherColorLayer/RasterParticleLayer 分离。 + +正常连续边界继续依赖既有 `OLD_T1 → NEW_T0 reuse` 与 `only NEW_T1 upload`;颜色层相同地复用其旧 T1 texture,仅为新的 T1 建立新 texture。正常 playback 的粒子 `simulationRebuild=false`、`trailCleared=false` 语义不变。 + +## 可观察性 + +新增低频日志:`REAL_WEATHER_DATASET_LOAD`、`REAL_WEATHER_INITIAL_WINDOW_READY`、`WEATHER_COLOR_FIELD_READY`、`REAL_WEATHER_COMPOSITE_READY`。现有 Host submit、Timeline、Window Boundary、FIELD_REUSE、FIELD_UPLOAD 和 ParticleRender 日志继续可用。没有新增每帧 Android JNI 或 UI timer。 + +## 验证与待验收 + +Codex 未执行 Android Gradle、APK build/install、ADB、App 启动、设备/模拟器测试或 Jest。Linux/OpenGL 受影响 translation unit 精确编译与相关定向测试结果见完成报告。 + +Android 仍需用户一次性手工综合验收,步骤见 `docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md`。已知 Validation UI 的 MIX 文本滞后以及 WAITING/INVALID 可见性仍为 NON-BLOCKING,不在本阶段单独修复。 + +## 首轮 Android 视觉修复(仍待复验) + +首轮实机确认真实 WeatherV2 T0–T3 Host submission、513×769 canonical Field、WeatherColorLayer 与 REAL WEATHER composite 均可工作;本轮只修复 Validation 显示与粒子物理位移表达。 + +- REAL WEATHER 的 raster basemap 从 OSM 换为 GSI 标准地图:`https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png`。该 source 与 weather/particle source 独立,GSI tile 不可用时仍为 fail-soft;`REAL_WEATHER_COMPOSITE_READY` 明确记录 `basemap=GSI failSoft=true`。 +- 底部入口变为 `TIME | VIS | COUNT | MAP | REAL WEATHER | MORE`。REAL WEATHER 不再位于 MORE:首次点击使用既有 Host Submission 提交真实 T0–T3、fit bounds、开启专属低频 `PARTICLE TUNING` 菜单;已激活时再次点击只显示/关闭菜单,不 reload Style、Dataset、Field 或 Timeline。激活状态以按钮 selected/yellow 显示。 +- REAL WEATHER 专属默认值为 P=2048、W=0.75、L=0.0040、O=0.75、T=0.55、F=0.88;合成场景退出时恢复进入前的旧 validation visual state,故未改变 synthetic 场景默认值。HUD 与调参菜单读取同一实际 runtime state,二级菜单关闭后 HUD 仍显示。 +- Tuning 用低频离散 `-/+` 操作覆盖 particleCount、lineWidth、streakLength、particleOpacity、trailOpacity、trailFade,并提供 RESET DEFAULT。非 particleCount 参数仅通过既有 layer property 更新;不 Style reload、不重新 submit Field、不 reset Timeline/Window。particleCount 允许仅重建 State/draw resources。 +- 为支持任务规定的 2048/512/8192 档,Core 明确接受这些 validation presets,State texture 使用 `ceil(sqrt(count))`,只为实际 count 建 particle vertices;并将 visual property 下限扩到 lineWidth=0.25、streakLength=0.0005。 +- 旧 shader 用无单位 `velocity * frame * 0.00004`,使真实 m/s 没有可审计的 geography conversion。现改为 U/V m/s → Web Mercator world displacement:`dx=U*dt/40075016.6856`、`dy=-V*dt/(40075016.6856*cos(latitude))`。`dt` 是稳定 wall delta(1/240–0.05s)乘 6000 simulation-seconds/real-second 的 Validation visual clock;它不改 canonical U/V、validTime、Timeline mix 或 Playback ownership,只使日本 zoom 下真实 m/s 有可见流线速度。低频 `ParticleMotion` 日志记录 sampleU/V、dt、visualSpeedScale、估算 world delta 和 simulationFrame,无 GPU readback。 + +本轮未修改 STEP-054 assets、Adapter、Host canonical contract、Timeline、Runtime Field Window、Renderer provider/network 边界或 WeatherColorLayer 数据链。状态继续为 **IMPLEMENTED / WAITING_ANDROID_VALIDATION**;新人工验收见 `docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v2.md`。 diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v2.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v2.md new file mode 100644 index 000000000..8a80369f1 --- /dev/null +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v2.md @@ -0,0 +1,30 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 v2 + +状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +Starting HEAD:`37c9aa31da1d2b3495c8802e6fd2f6b999562b06` + +## 本轮问题与根因 + +Android 已确认真实 WeatherV2 Field、Host submission、WeatherColorLayer、Timeline State A/B 调度、非零 U/V 与非零 m/s→Web Mercator 位移均存在,但 Timeline Pause 下粒子仍肉眼静止。 + +根因是 Particle State A/B 与 Trail A/B 共用 `createValidationTarget()` 的 `UnsignedByte` target。State RG 保存的是 Web Mercator world position;RGBA8 的一个量化步长约为 `1/255=0.00392 world`,远大于日志中每个真实 Weather simulation step 的 `0.000003–0.000008 world`,写回后不能累积有效位移。 + +## 修复 + +- State A/B target 改为 `TextureChannelDataType::Float`,包括初次创建、context/resource recreation 和 particleCount 变更的 replacement targets。 +- Trail A/B 保持 `TextureChannelDataType::UnsignedByte`,继续是 viewport-sized 颜色/透明度 target;没有升级 WeatherColorLayer 或任意无关 full-screen target。 +- `RenderTarget` 新增 `isRenderable()`,在 Particle target 创建时立即驱动现有 `OffscreenTexture → GL texture → framebuffer` 创建/检查。Android OpenGL 已有映射链为 `Float → RGBA32F + GL_FLOAT → glFramebufferTexture2D → checkFramebuffer()`;无需新增 backend 抽象。 +- Float State FBO 不能创建时不回退到 UByte:`ParticleStateTarget: event=CREATE_FAILED ... dataType=Float` 后停止该 State resource 创建;count change 同样明确返回 `STATE_RESOURCE_CREATE_FAILED dataType=Float`。 +- 每个成功 target 输出 `CREATE_SUCCESS`,Trail pair 就绪时另输出一次 `ParticleStateTarget: stateAType=Float stateBType=Float trailAType=UnsignedByte trailBType=UnsignedByte`。 +- Shader 注释校正为 State RG 保存 `Web Mercator world position`,不是 field-normalized position。 + +## 冻结边界 + +未修改 canonical Field、STEP-054 assets、Host Submission Contract、Timeline、Playback、Runtime Field Window、WeatherColorLayer、U/V=m/s 或 `visualSpeedScale=6000`。继续保持 Timeline Pause 只冻结 forecast 时间、Particle simulation 在当前 Field 中运行;Window reuse、NEW_T1-only、MIX property-only、normal boundary `simulationRebuild=false` / `trailCleared=false` 不变。 + +## 验证与待验收 + +已执行 shader 生成、Linux OpenGL 受影响 Core 精确编译、相关测试源精确编译和 `git diff --check`;未执行 Android Gradle/APK/ADB/App/设备或 Jest。 + +下一轮人工验收的第一优先项是:进入 REAL WEATHER、保持 Timeline Pause,确认 Float State 类型日志后粒子中心位置连续运动。若仍完全不动,停止调节 6000,改以单独任务调查 GPU State readback / State Update→Draw 链。 diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v3.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v3.md new file mode 100644 index 000000000..3198e36ec --- /dev/null +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v3.md @@ -0,0 +1,26 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 v3 + +状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +Starting HEAD:`f8027312e6e06d539e9ad222b60ac9a496b1758d` + +## Float State fail-closed 小修复 + +`createValidationTarget()` 现在将 RenderTarget 的创建、FBO validation、sampler 设置和 LayerGroup 接线置于局部 `try` 边界内。 + +- `std::exception`(包括 `std::runtime_error` 和 `std::bad_alloc`)记录 `stage=exception error=...` 后返回空 target。 +- 未知异常记录 `stage=unknown_exception` 后返回空 target。 +- validation 失败记录 `stage=validation` 后返回空 target。 +- 没有回退到 `UnsignedByte`:State A/B 仍为 `Float`,Trail A/B 仍为 `UnsignedByte`。 + +调用方已有空 target 检查,因而 Float framebuffer 创建异常会 fail-closed,不继续向 Renderer 上层传播异常。 + +## 不变边界 + +未修改 canonical Field、Timeline、Runtime Field Window、WeatherColorLayer、REAL WEATHER UI 或 `visualSpeedScale=6000`。 + +## 验证 + +Linux OpenGL 精确编译 `render_particle_validation_layer.cpp` 及既有非 Jest `style_layer.test.cpp` 均通过;`git diff --check` 通过。未执行 Android Gradle、APK、ADB、设备测试或 Jest。 + +下一步仍是用户 Android 手工验收 Float State target 创建与 Pause 下粒子运动。 diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v4.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v4.md new file mode 100644 index 000000000..fac8840e2 --- /dev/null +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v4.md @@ -0,0 +1,29 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 v4 + +状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +Starting HEAD:`97aa019f6ff07d1b91e17c46ad0e6e6c0cd339d5` + +## 已确认问题 + +Float State 已使真实粒子恢复运动,但旧正式路径会在无效或越界时回固定 seed;同时 particle draw 以固定 world `streakLength` 构造短棍,viewport-sized Trail 又按全球 world 坐标使用。结果是密度向边缘集中、缩放后出现粗短线与闪烁,且每帧固定 fade 受 FPS 影响。 + +## 本轮修复 + +- State A/B 保持 Float,Trail A/B 保持 UnsignedByte。 +- State B 保存 simulation age;越界、无效、零速或 `3600` simulation seconds 到期时,在当前有效 Field bounds 内以 particle texel/旧位置哈希进行最多三次确定性重生。State A=0 作为 respawn 标记,下一次正常 advection 才设回 A=1。 +- 正式 particle segment 改为 `StateRead.previousPosition → StateWrite.currentPosition`;respawn 标记帧丢弃 segment,避免跨屏连线。REAL WEATHER 中 `streakLength` 保留兼容 UI/API,不再决定真实 segment 长度。 +- particle segment 通过当前 `matrixForTile()` 投影进入 viewport Trail;正式 Composite 改为一次 fullscreen view-space Trail 合成,而非把 viewport texture 当作全球 world texture。 +- 相机 pan/zoom/rotate/pitch/resize 变化时只释放并重建 Trail A/B,记录 `ParticleTrail: event=CAMERA_RESET`;State、Field、Timeline、Runtime Window 均保留。正常 T0/T1 forecast boundary 不触发该路径。 +- `trailFade` 保持“60 FPS 每帧保留率”UI 含义,实际每帧 fade 为 `pow(trailFade, wallDeltaSeconds * 60)`,因此以 wall time 一致衰减。 +- visual clock 从 `6000` 改为 `1200` simulation seconds / real second;60 FPS 时约为 20 simulation seconds / frame。5 m/s≈100 m/frame(z9≈0.65 px),30 m/s≈3.9 px,避免旧 6000 的高风速离散跳跃。 + +## 冻结边界 + +未修改 canonical Field、WeatherV2 assets、Host submission、Timeline、Runtime Field Window、WeatherColorLayer 或 Android UI。Timeline Pause 仍仅冻结 forecast;Particle State 在当前 Field 内继续更新。正常 window advance 仍维持 reuse、only NEW_T1 upload、`simulationRebuild=false` 与 `trailCleared=false`。 + +## 验证与待验收 + +已执行 shader generator、Linux OpenGL 受影响 C++ 精确编译、既有非 Jest `style_layer.test.cpp` 精确编译和 `git diff --check`。未执行 Android Gradle、APK、ADB、设备测试或 Jest。 + +下一步:由用户按 v5 指令进行一次 Android 综合验收,重点观察 Pause 下 2–3 分钟密度、缩放后的连续轨迹、相机 Trail reset、time-based fade、T0→T3 boundary 与 Rotation。 diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v5.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v5.md new file mode 100644 index 000000000..3f11d43a3 --- /dev/null +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v5.md @@ -0,0 +1,16 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 v5 + +状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +Starting HEAD:`c7acefcb21840bc1db736a5f061ba39b18ba2c36` + +## 生命周期错峰小修复 + +- `ParticleLifecycleSeconds` 从 `3600` 改为 `14400` simulation seconds;`VisualSimulationSecondsPerRealSecond=1200` 未变,因此基础生命周期为 12 real seconds。 +- 首次 State seed 的 age 改为 `particleHash(v_uv + vec2(71.19, 29.47)) * maxAge`,不再统一为零。 +- 每个 slot 的 `maxAge = base * mix(0.75, 1.25, particleHash(v_uv + vec2(17.31, 83.17)))`;普通 respawn 仍重置 age=0,initial seed 保留错峰 age。 +- `ParticleLifecycle` 日志使用 `configuredSlots`,并标示 `initialAge=HASH_STAGGERED`、`maxAge=HASH_JITTERED_0.75_1.25` 与 `readback=disabled`。 + +State Float、Trail UByte、previous→current segment、view-space Trail、time-based fade、canonical Field、Timeline、Runtime Window、WeatherColorLayer 和 REAL WEATHER UI 均未修改。 + +shader generator、Linux OpenGL 精确编译、既有非 Jest style layer 精确编译和 `git diff --check` 均通过;未执行 Android 或 Jest。 diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v6.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v6.md new file mode 100644 index 000000000..542fbe962 --- /dev/null +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v6.md @@ -0,0 +1,16 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 v6 + +状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +Starting HEAD:`067bfdc38baa32d6e474a502d67bc0910d18e247` + +## 低缩放可见性与 Trail 稳定性修复 + +- visual scale 使用 `effectiveScale = clamp(1200 * 2^(9 - zoom), 300, 153600)`;simulation step 再限制为最多 `2400` seconds/frame。典型 60 FPS:z2≈2400(cap)、z5≈320、z9≈20 simulation seconds/frame。 +- canonical U/V=m/s 与 m/s→Web Mercator 物理换算未改;只是 validation visual clock 依据 zoom 保持可见的屏幕位移。 +- 生命周期改为 real seconds:base=12,hash jitter 后为 9–15 real seconds;State B 使用 wall delta,不再使用 simulation delta,因此 zoom、Timeline speed、Timeline Pause 不改变寿命。 +- 普通 camera transform 首次触发 `CAMERA_INVALIDATE`,暂停 Trail segment 累积而 State 继续;连续操作不重复重建 texture;150ms 稳定后 `CAMERA_STABLE_RESUME`。仅真正 viewport size 改变才 `VIEWPORT_RESIZE` 并重新分配 Trail A/B。 + +State Float、Trail UByte、真实 previous→current segment、respawn/hash stagger、time-based fade、canonical、Timeline、Runtime Window、WeatherColorLayer 和 REAL WEATHER UI 保持不变。 + +shader generator、Linux OpenGL 精确编译、既有非 Jest style layer 精确编译和 `git diff --check` 已通过;Android 与 Jest 未执行。 diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v7.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v7.md new file mode 100644 index 000000000..ab7814497 --- /dev/null +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v7.md @@ -0,0 +1,28 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 v7 + +状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +Starting HEAD:`d9a9c8ed57605abca86893ba7fb3b5265db3c0dc` + +## Zoom 下 Viewport 粒子密度调整 + +- `configured particleCount` 不随 zoom 改变,仍由既有用户设置控制;本次没有按 zoom 重建 State slot,也没有新增均匀网格铺点。 +- C++ 使用当前 viewport 四角的 unwrapped Web Mercator AABB,水平和垂直各扩展 15%,再与 canonical Field bounds 取交集,作为稳定相机时的 `respawnBounds`。 +- shader 保留 `u_field_bounds` 作为唯一 Field 采样/UV 映射范围;新增独立 `u_respawn_bounds` 和 `viewportDensityActive`(`u_field_scale.z`),防止把可视范围误作数据有效范围。 +- camera moving 期间 `viewportDensityActive=false`,普通 invalid/age 生命周期仍在 full Field 复生,State 不 reset、Field 不重提、Timeline 不重置。稳定至少 150ms 后才启用 viewport domain。 +- stable 后,位于 expanded viewport 外的 slot 成为额外 respawn 条件;新出生 slot `age=0`、alpha=0,因此不会产生跨屏 segment。viewport 内的正常粒子不被统一重置。 +- 若四角 AABB 无效、交集为空或 extent 过小,安全回退 full Field bounds;日志为 `ParticleDensity mode=FIELD_FALLBACK fallback=true`,不会产生 NaN/inverted bounds。 + +## 保持的边界 + +State A/B=Float、Trail A/B=UnsignedByte、previousPosition→currentPosition、zoom-aware visual speed、wall-time lifecycle、time-based fade 与 camera Trail invalidate/stable resume 保持。canonical U/V=m/s、WeatherV2 assets、Host Submission、Timeline、Runtime Field Window、WeatherColorLayer 与 REAL WEATHER UI 未修改。 + +## 验证 + +- shader generator:PASS。 +- Linux OpenGL 精确编译 `render_particle_validation_layer.cpp`:PASS。 +- 既有非 Jest `style_layer.test.cpp` 精确编译:PASS。 +- `git diff --check`:PASS。 +- Android Gradle/APK/ADB/设备和 Jest:未执行。 + +下一步由用户按人工验收 v7 检查 z2–3/z5/z8–9 密度、相机稳定后的补充行为和 Timeline boundary 回归。 diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v8.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v8.md new file mode 100644 index 000000000..e8568d4f9 --- /dev/null +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v8.md @@ -0,0 +1,20 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 v8 + +状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +Starting HEAD:`4645280fc59ac8e356f4339bb7fad5d73b4a413a` + +## v9 Android C++ 编译遗漏修复 + +v9 为 `ParticleGpuValidationUBO` 新增 `respawnBounds` 后,`render_weather_color_layer.cpp` 的 designated initializer 未同步,Android OpenGL 编译在 `-Wmissing-designated-field-initializers -Werror` 下失败。 + +WeatherColorLayer 不参与 particle viewport respawn,因此其 initializer 已按字段声明顺序补齐:`fieldBounds`、`respawnBounds`、`fieldScale`、`matrix`。`respawnBounds` 取与 `fieldBounds` 相同的中性 Field 范围;色场采样、颜色计算与 opacity 语义未变。 + +搜索确认当前 C++ 中仅有两个 `ParticleGpuValidationUBO` initializer: + +- `render_particle_validation_layer.cpp`:已有 v9 respawn bounds; +- `render_weather_color_layer.cpp`:本次补齐。 + +shader generator、Linux OpenGL 两个 renderer translation unit 精确编译、既有非 Jest `style_layer.test.cpp` 精确编译和 `git diff --check` 通过。未执行 Android Gradle/APK/ADB/设备或 Jest。 + +viewport-aware respawn、zoom-aware speed、wall-time lifecycle、Trail camera debounce、canonical U/V、Timeline/Runtime Window、particleCount 与 WeatherColorLayer 实际颜色场行为均未修改。 diff --git a/docs/50_Step记录/STEP-056_RN天气渲染桥接现状调查_v1.md b/docs/50_Step记录/STEP-056_RN天气渲染桥接现状调查_v1.md new file mode 100644 index 000000000..085f75391 --- /dev/null +++ b/docs/50_Step记录/STEP-056_RN天气渲染桥接现状调查_v1.md @@ -0,0 +1,17 @@ +# STEP-056 React Native 天气渲染桥接现状调查 + +状态:`INVESTIGATED / READY_FOR_HOST_BRIDGE_FOUNDATION`。 + +Starting HEAD:`44a50e6e4a4e79fae7ddf36af007b5e35e969a92`。 + +本阶段为只读调查,未修改 C++、Kotlin/Java、JNI、shader 或 Android UI;未执行 Android Gradle、APK、ADB、设备测试或 Jest。 + +结论:仓库没有现成 React Native/TurboModule/Fabric/JSI binding。可复用的稳定 seam 是 Android `MapLibreMap.submitRasterArraySourceHostField()` → JNI → `RasterArraySource::submitHostField()`;它已严格复用 canonical Descriptor、Adapter、Snapshot、Core Timeline 与 Runtime Window。 + +当前 `byte[]` Host path 适合 RN v1 先以 ArrayBuffer/Uint8Array materialize 为 byte[] 后接入;单帧 513×769 U/V 为 3,155,976 bytes,存在 Java/C++ Snapshot 多次 copy,direct ByteBuffer/zero-copy 仅在 profiling 后另行优化。 + +下一实现建议为 STEP-057 Android Host Weather Runtime Bridge Foundation:补齐 typed lifecycle/clear、timestamp time control、visibility/style、structured low-frequency result/event,以及 dynamic WeatherColor stops 和独立 particle/trail color contract。RN package 本身因本仓库没有 RN host/dependency 不应凭空创建;未来 React 层薄包裹该 facade。 + +`targetFps` 应另列性能批次:现有约 60 FPS 来自两层 `needsRepaint()` → Map update/requestRender;`MapView#setMaximumFps()` 是全图上限,不是天气专属节流点。STEP-057 不应改变既有 Renderer scheduling。 + +调查报告:`docs/20_调查/STEP-056_RN天气渲染桥接现状调查_v1.md`。 diff --git a/docs/50_Step记录/STEP-057D_WeatherRuntime接口收尾与RN桥接前冻结_v1.md b/docs/50_Step记录/STEP-057D_WeatherRuntime接口收尾与RN桥接前冻结_v1.md new file mode 100644 index 000000000..45a400987 --- /dev/null +++ b/docs/50_Step记录/STEP-057D_WeatherRuntime接口收尾与RN桥接前冻结_v1.md @@ -0,0 +1,31 @@ +# STEP-057D WeatherRuntime 接口收尾与 RN 桥接前冻结 + +状态:`IMPLEMENTED / READY_ANDROID_COMPILE_CHECK` + +基线:`01295df4b814562f569375e7e7cf1f48ce35388f`,分支 `particle/main`。 + +## 目标与范围 + +本 Step 只收紧 Android Host `WeatherRuntime` 公共接口,为后续 RN 薄桥接冻结边界;不实现 RN/TurboModule/JSI,不修改 provider、Timeline、Runtime Field Window、canonical field、shader、UBO 或 renderer 架构。 + +## 已完成 + +- 新增 `WeatherRuntime.setParticleColor(r,g,b,a)` 与 `setTrailColor(r,g,b,a)`,分别通过 JNI 进入 Core 的独立 `RasterParticleLayer::setParticleColor` / `setTrailColor`。两者不提交 field、不影响 WeatherColor ramp、不重置 Timeline 或粒子模拟。 +- 保留 `setParticleColors(...)` 作为兼容 helper,继续使用原子成对底层提交;新 Host/RN 契约使用两个独立 typed 方法。Core 既有 `particleColor=(0.08,0.55,1,1)`、`trailColor=(1,1,1,1)` 默认值不在 facade 重建或猜测。 +- REAL WEATHER 数据提交改用 `WeatherRuntime.FieldSubmitResult` 与 `accepted()`/enum 名称,不再用 `getCode()` 或整数比较组织正常 Host 路径。 +- REAL WEATHER playback/visual 正常路径遇到不支持命令或 property 时不再回退到 `MapLibreMap` 低层调用。旧 fixture、HOST、diagnostic time-mix/field-window 入口保留为 legacy/diagnostic 路径。 +- 增加独立颜色 API 的现有 JUnit/Mockito 定向源码覆盖,包含独立调用、参数隔离和 native rejection fail-closed。 + +## 低层入口审计 + +- A 类(REAL WEATHER):field submit、play/pause/speed、已支持粒子视觉属性均经 `WeatherRuntime`;独立 particle/trail color 也经 `WeatherRuntime`。 +- B 类(legacy/diagnostic):fixture/HOST INVALID/DUPLICATE、ADVANCE WINDOW、time-mix 及旧诊断 property 仍可直接使用 `MapLibreMap`,仅供既有验证工具,不构成未来 RN contract。 + +## 验证与未执行 + +- 已执行 `git diff --check` 与源码静态审计(typed API、`getCode()` 使用、REAL WEATHER fallback 边界)。 +- 未执行 Android Gradle/APK、ADB、设备/虚拟机测试或 Jest;Android compile check 留待后续统一授权。 + +## 剩余风险与下一步 + +JNI/Android Java 编译及 Mockito 配置仍需 Android compile check;设备上的独立颜色视觉效果和 style reload/资源生命周期需后续人工验收。代码边界已冻结,可进入 RN 薄桥接前独立审计。 diff --git a/docs/50_Step记录/STEP-057_AndroidHost天气运行时接口基础_v1.md b/docs/50_Step记录/STEP-057_AndroidHost天气运行时接口基础_v1.md new file mode 100644 index 000000000..44492a365 --- /dev/null +++ b/docs/50_Step记录/STEP-057_AndroidHost天气运行时接口基础_v1.md @@ -0,0 +1,37 @@ +# STEP-057 Android Host 天气运行时接口基础 + +状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。 + +Starting HEAD:`faf7548220e9a1282275711c236987f7faedfa03`,分支 `particle/main`。 + +已实现:Android `WeatherRuntime` typed facade;canonical byte[] submit result enum;clear fields;timestamp seek;play/pause/0.5/1/2x;独立 visibility;OFF unload;WeatherColor 8-stop dynamic ramp;独立 particle/trail RGBA;Android→JNI→C++ 接线;REAL WEATHER Test App submit/playback 改走 facade。 + +Core 保持 canonical U/V、Timeline、Runtime Field Window、T0/T1 interpolation 和 Renderer 架构边界。正常窗口边界的 GPU content reuse 路径未改变;normal seek 不改变 window revision,跨窗口仍由既有 revision 驱动 reuse。 + +OFF 会停止 Timeline、clear Host ownership,并在 Render 线程的空 Source path 清理 layer drawables、particle State/Trail/draw targets、field textures 和 WeatherColor resources。结构化日志包括 `TIMELINE_STOP`、`FIELD_WINDOW_CLEAR`、`SNAPSHOT_CLEAR`、`PARTICLE_RESOURCE_RELEASE`、`TRAIL_RESOURCE_RELEASE`、`WEATHER_COLOR_RESOURCE_RELEASE`。 + +本地验证:shader generator、Linux OpenGL `mbgl-core`、`style_layer.test.cpp` 与 `source.test.cpp` 精确编译;`git diff --check`。Android Gradle/APK/ADB/设备测试和 Jest 未运行。 + +未完成:Android 手工 T1–T13 验收,特别是 OFF resource log、ON 后重新 T0/T1、dynamic ramp/colour 无 field upload/particle reset、timestamp mid-point 和窗口边界回归。 + +## STEP-057A 审计修正(2026-09-07) + +状态保持 `IMPLEMENTED / WAITING_ANDROID_VALIDATION`。基于已推送 `efaa3e61c` 审计修正,不进入 STEP-058。 + +- 删除正式 `WeatherRuntime.setParticleVisual(String, float)`;改为 particle count、line width、length、particle/trail opacity、trail fade、speed-color enabled 的明确 typed 方法。Test App 的 fixture-only generic property 调试仍只留在低层路径。 +- 修正 facade 状态:READY 重复 enable 幂等;DISABLED enable 进入 WAITING;ERROR 收到 pending T0 恢复 WAITING;initial window ready 进入 READY;clear/disable 只在对应 Core 成功时转入 WAITING/DISABLED。新增 Android 既有 JUnit state transition 定向测试源码,未按本轮限制执行 Gradle。 +- `RenderOrchestrator` 不再对所有 hidden RasterArray layer 执行额外 prepare/update。`RenderLayer` 默认无 cleanup;仅仍持有资源、Host-field Source 已空的 RasterParticle/WeatherColor renderer 显式 opt-in,完成一次空场 GPU cleanup。普通 hidden layer 语义不变。 +- Host stop capacity 改为由 JNI 读取 Core `MaxWeatherColorStops`;Host API 继续接受 `ColorStop` list,未暴露 UBO。严格验证与 particle/trail 独立性保持。 +- timestamp 测试改为 epoch milliseconds(2024-01-01 UTC 起的 64-bit ms),覆盖 exact T0、midpoint、exact T1、上下界 out-of-range 与 clear 后 fail-closed。 + +验证:output-throttle 下 shader generator、Linux OpenGL `mbgl-core`、`mbgl-render`、`mbgl-test-runner`、相关精确 Source/Style compile 和定向非 Jest tests 均 PASS;`git diff --check` PASS。Android Gradle/APK/ADB/设备/Jest 未运行。 + +## STEP-057B 状态语义修正(2026-09-07) + +状态:`IMPLEMENTED / WAITING_CODE_REVIEW`。 + +基于 `dad6876c72f29e2d5671677ee15e13cd25e653a1`,本轮只修改 `WeatherRuntime` 与状态测试/记录:普通 `REJECTED_ADAPTER`、`REJECTED_INCOMPATIBLE`、`REJECTED_NON_MONOTONIC`、`REJECTED_CAPACITY` 和空 metadata/payload 保持当前 state;`REJECTED_SOURCE_MODE`、`UNKNOWN` 才进入 ERROR。pending T0、initial window、next snapshot、duplicate 的成功/保持语义不变。 + +`READY + enable(true)`、`WAITING_FOR_FIELDS + enable(true)` 为幂等 no-op;`DISABLED + enable(true)` 成功恢复到 WAITING;ERROR enable 明确返回 REJECTED 并保持 ERROR。clear/disable 成功与失败转换保持原有契约,DISABLED 重复 disable 仍为成功 no-op。 + +`WeatherRuntimeStateTest` 已覆盖普通 rejection 保持 READY/WAITING、真正错误、所有 enable 分支、clear/disable,以及 public flow 的 invalid submit 和 ERROR enable。public flow 测试源码已加入现有 Mockito/JUnit 体系,但本轮未运行 Android Gradle;因此仍需代码审计后统一 Android 验收。 diff --git a/docs/50_Step记录/STEP-058_WeatherLocalTargetFps与RepaintGate_v1.md b/docs/50_Step记录/STEP-058_WeatherLocalTargetFps与RepaintGate_v1.md new file mode 100644 index 000000000..6a08f8626 --- /dev/null +++ b/docs/50_Step记录/STEP-058_WeatherLocalTargetFps与RepaintGate_v1.md @@ -0,0 +1,42 @@ +# STEP-058 Weather-local Target FPS 与 Repaint Gate + +状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION` + +Starting HEAD:`6d514e3aae68f579af41b10f85efe0f3b28784b2`,分支 `particle/main`。 + +## 目标 + +为天气粒子与 WeatherColor 建立 source-local AUTO/30/60 cadence 和 repaint gate,不限制全局 Map FPS,不进入 React Native package。 + +## 已完成 + +- `WeatherRuntime.TargetFps`、`setTargetFps()`、`getTargetFps()` typed Host API,经 `MapLibreMap → NativeMapView → JNI → RasterArraySource` 接线。 +- `RasterArrayTimelineRuntime` 持有单一 target/deadline authority;Particle 与 WeatherColor 共用 due 判定,真实 `steady_clock` elapsed 驱动 Timeline、particle lifecycle 与 Trail fade。 +- 未到 deadline 的地图高频帧不推进天气 State/Trail/Timeline;Android facade 使用既有 `triggerRepaint()` 低频 delayed wake-up,不调用全局 `setMaximumFps()`。 +- REAL WEATHER popup 增加 FPS AUTO/30/60,HUD 显示当前选择;两层 visibility、PLAY/PAUSE、clear/disabled 的持续 wake-up 规则已收敛。 +- 增加 deterministic cadence 测试,覆盖 AUTO、30/60 deadline、target 切换与单 authority。 + +## 保护与禁止范围 + +canonical field、field bytes、submit result、timestamp、playback、WeatherColor ramp、particle/trail color、OFF unload、hidden cleanup、Runtime Field Window、OLD_T1→NEW_T0 reuse、only NEW_T1 upload、simulation/trail reset 保护均未重写;未新增 RN/provider/route/ETA/Jest,也未使用全局 Map FPS cap。 + +## 验证 + +- throttle 下 Linux/OpenGL `mbgl-test-runner` 与 `mbgl-render-test-runner` 增量构建:PASS。 +- throttle 下 deterministic cadence 测试 `Source.RasterArrayTimelineCadenceUsesOneSharedDeterministicDeadline`:PASS。 +- throttle 下 `Source.RasterArray*` 定向测试:PASS。 +- `git diff --check`:PASS。 +- 未执行 Android Gradle、APK、ADB、设备/虚拟机测试或 Jest。 + +## 剩余风险 + +Android JNI/Java compile 与设备上 30/60 tick、camera 并发、visibility/off wake-up 仍需统一人工验收;天气 camera skipped-frame composite 的具体 GPU 视觉连续性需实机确认。 + +## STEP-058A Shared Cadence 与 WeatherRuntime 生命周期修正 + +状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。 + +- 以 `RasterArrayTimelineRuntime::advanceIfDue()` 取代 renderer 单独 `tick()`,在 timeline 锁内原子完成 deadline claim、revision 标记和 wall-time 推进。WeatherColor-only + PLAY 可独立推进 MIX;同一 deadline Particle 后到仅消费相同 frame/revision,不会双推进。 +- WeatherColor-only + PAUSE 不主动持续 repaint;Particle visible + PAUSE 保持粒子 cadence;两层 hidden、clear/disable 不再由 facade 保留 delayed wake-up。 +- 新增幂等 `WeatherRuntime.dispose()`;disposed 后取消 callback、旧 Runnable 不 repaint/不重排,控制 API 拒绝或 fail-fast。Test App 在 style reload、leave REAL WEATHER、Activity destroy 前显式释放旧 Runtime。 +- Linux/OpenGL 增量 build、`Source.RasterArray*` deterministic/source 定向测试和 `git diff --check` 通过;Android Gradle/APK/ADB/设备/Jest 未执行。 diff --git a/docs/50_Step记录/STEP-060A_RNTestHost编译前修正_v1.md b/docs/50_Step记录/STEP-060A_RNTestHost编译前修正_v1.md new file mode 100644 index 000000000..d15da8750 --- /dev/null +++ b/docs/50_Step记录/STEP-060A_RNTestHost编译前修正_v1.md @@ -0,0 +1,23 @@ +# STEP-060A RN Test Host 编译前修正 + +状态:完成(`READY_ANDROID_VALIDATION`) + +## 基线与目标 + +Starting HEAD:`6a99f1a1424ce7e30c26e825251df956a46bf4c4`,分支 `particle/main`。本 Step 只修正 STEP-060 的 Codegen Kotlin override blocker 与 npm/package 文档契约;不进入 STEP-061,未改 RN ownership、MapLibre renderer、timeline 或 field contract。 + +## 完成项 + +- [x] 以当前 RN `0.85.2` 安装源码的 `@react-native/codegen/lib/generators/modules/GenerateModuleJavaSpec.js` 核对:必选方法生成 `public abstract`,Promise 末尾追加 `Promise promise`。 +- [x] 为 JS `NativeWeatherRuntimeBridge.ts` 的 20 个方法全部补齐 Kotlin `override`;签名逐项脚本核对,未发现额外 Spec mismatch。 +- [x] Test Host 统一为 `npm@10.9.4` 声明与 `package-lock.json`;不新增 yarn.lock。 +- [x] README/人工验收命令统一 `npm ci`;实际 `npm ci --ignore-scripts` 可通过,不需要 `--legacy-peer-deps`。 +- [x] 文档明确 Android toolchain 数值目前是验收目标;首次 Expo prebuild 后才检查实际 Gradle/AGP/Kotlin/SDK/NDK/Hermes/New Architecture。 + +## 验证与边界 + +执行 `npm ci --ignore-scripts --no-audit --no-fund`、throttle 下 `npx tsc --noEmit`、Node config syntax、package/lock 检查、Spec/override 一一检查、yarn 残留搜索与 `git diff --check`。未执行 Android Gradle、custom publish、Expo prebuild、APK、ADB、设备测试或 Jest。 + +Bridge 继续按 `reactTag → Fabric UIManager → MLRNMapView → MapLibreMap → map-owned WeatherRuntime` 工作;无 Adapter/Normalizer/第二 registry,未实现 Binary Field。 + +STEP-060B 补充:Test Host prebuild 观察到的 Gradle/AGP/NDK/SDK 值只描述其生成工程;Gate C 封存 Linux 环境继续是 current custom OpenGL AAR 的唯一构建基线,Maven Local 是两者之间的 handoff。 diff --git a/docs/50_Step记录/STEP-060B_GateC封存环境与CustomAAR接入修正_v1.md b/docs/50_Step记录/STEP-060B_GateC封存环境与CustomAAR接入修正_v1.md new file mode 100644 index 000000000..471370893 --- /dev/null +++ b/docs/50_Step记录/STEP-060B_GateC封存环境与CustomAAR接入修正_v1.md @@ -0,0 +1,22 @@ +# STEP-060B Gate C 封存环境与 Custom AAR 接入修正 + +状态:完成(`IMPLEMENTED / WAITING_USER_GATEC_AAR_BUILD_AND_ANDROID_VALIDATION`) + +## 基线与目标 + +Starting HEAD:`80f360be7849be3e63557c4e074ee3a0fc02e15f`,分支 `particle/main`。Gate C 是 MapLibre Native custom AAR 的封存 Linux 编译环境;STEP-060 的 Maven Local 是 Test Host handoff,两者不互相改写。 + +## 完成项 + +- [x] publication 源码确认:group `org.maplibre.gl`、version `13.4.1`、OpenGL Release component 与 `openglrelease` publication,精确任务为 `publishOpenglreleasePublicationToMavenLocal`。 +- [x] MLRN 11.3.6 由 `nativeVariant/nativeVersion` 解析 `android-sdk-opengl:13.4.1`;Bridge 的直接依赖相同。 +- [x] Maven Local 位于首位且 Maven Central 排除唯一 custom module `org.maplibre.gl:android-sdk-opengl`。AAR 缺失 fail-closed,同时不阻断 MLRN 的 turf/annotation/scalebar 辅助 artifacts。 +- [x] 文档要求 Gate C 环境从当前 HEAD 生成 OpenGL Release AAR,核对 mtime/SHA/classes,再用精确 publication task 发布。 +- [x] 旧 2026-08-29 AAR SHA `5c67d275…f85e5c` 仅作历史 Gate C 证据,不得用于含 WeatherRuntime 的当前验证。 + +## 未执行与下一步 + +未执行 Android Gradle、AAR build/publish、Expo prebuild、APK/ADB/设备或 Jest。用户必须先在 Gate C 环境完成 current HEAD AAR build → Maven Local publication → clean prebuild → Test Host Android compile/install → Phase A/B/lifecycle 验收,之后才可进入 STEP-061。 +## STEP-060C 后续记录(2026-09-07) + +Gate C 已生成当前 custom OpenGL AAR,但精确 Maven Local publication 曾在 Vanniktech `signAllPublications()` 要求 signatory 时失败。该问题只属于 Maven Local handoff,并不要求重建已核对的 AAR。STEP-060C 将签名 required 条件隔离为 Maven Central task graph;Local 保持正式坐标并可在没有 PGP signatory 时发布,Central 正式路径仍要求签名。2026-09-07 人工执行已通过(sign task `SKIPPED`、publication `BUILD SUCCESSFUL`);最终 Local/build-output AAR SHA 为 `2e73e25cfd31bb181f75ba2d84974afb949640f72de46bc0234d4579abe3ba38`。这只关闭 Maven Local handoff,不代表 RN Test Host Android 验收完成。 diff --git a/docs/50_Step记录/STEP-060C_MavenLocal签名隔离修正_v1.md b/docs/50_Step记录/STEP-060C_MavenLocal签名隔离修正_v1.md new file mode 100644 index 000000000..bdffae096 --- /dev/null +++ b/docs/50_Step记录/STEP-060C_MavenLocal签名隔离修正_v1.md @@ -0,0 +1,35 @@ +# STEP-060C Maven Local 签名隔离修正 + +## 目标 + +修正 custom OpenGL AAR 的 Maven Local handoff:保持正式坐标 `org.maplibre.gl:android-sdk-opengl:13.4.1` 与精确 task `:MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal`,但不要求本地拥有 PGP signatory;Maven Central 正式发布仍必须签名。 + +## 实现 + +- 保留 Vanniktech `signAllPublications()`,使 Central publication 继续具备签名任务和语义。 +- `SigningExtension.required` 使用延迟 `Provider` 检查 task graph 是否含 `*PublicationToMavenCentralRepository`:Local graph 为 false,Central graph 为 true。 +- 因而 Maven Local 的 Sign task 可正常跳过,不读取 signatory;Central task graph 仍会执行且要求签名。 +- 未改变 AAR、artifactId、version、publication 名称、custom repository 来源或 RN bridge。 + +## 人工 Maven Local 验收(2026-09-07) + +用户在 Gate C 封存 Linux 环境实际执行: + +```bash +cd /mnt/sda1/MapLibreParticleLab/platform/android +./gradlew :MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain +``` + +- `:MapLibreAndroid:signOpenglreleasePublication SKIPPED`;未出现 `no configured signatory`。 +- `:MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal` 完成,`BUILD SUCCESSFUL in 3m 33s`(57 actionable tasks:49 executed、8 up-to-date)。 +- Maven Local 已有 AAR、POM、module metadata,坐标为 `org.maplibre.gl:android-sdk-opengl:13.4.1`;没有 `.asc`,符合 Local publication 不签名的设计。 +- 发布 AAR 与 `platform/android/MapLibreAndroid/build/outputs/aar/MapLibreAndroid-opengl-release.aar` SHA-256 一致,均为 `2e73e25cfd31bb181f75ba2d84974afb949640f72de46bc0234d4579abe3ba38`。 +- AAR `classes.jar` 含 `org/maplibre/android/maps/MapLibreMap.class`、`org/maplibre/android/maps/WeatherRuntime.class`,并含 `arm64-v8a`、`armeabi-v7a`、`x86`、`x86_64` 四 ABI。 + +`47646fb708d096cf5ee0c3b8eeaeb7e6083081279c79f4a642db554efe88fad7` 是 publication 前曾核对的中间 AAR SHA;publication 重新执行 `bundleOpenglReleaseAar` 后已由上述最终 SHA 取代,不得再作为当前 Maven Local artifact 引用。 + +## 验证与状态 + +- 已完成:Maven Local signing isolation、Local publication、AAR/POM/module 存在与坐标核对、build output 与 Local AAR SHA 一致、两项 Host class 与四 ABI 核对。 +- 未完成:RN Test Host Android compile/install、STEP-060 Phase A/B、style reload/map remount/stale reactTag/detach lifecycle、Binary Field 与 STEP-061;这些均不属于本 Step 的 PASS 范围。 +- 状态:`CLOSED / PASS`(仅限上述 Maven Local signing isolation 与 publication 验收)。 diff --git a/docs/50_Step记录/STEP-060D_WeatherRuntimeBridge_attach精确诊断_v1.md b/docs/50_Step记录/STEP-060D_WeatherRuntimeBridge_attach精确诊断_v1.md new file mode 100644 index 000000000..75bcc4af3 --- /dev/null +++ b/docs/50_Step记录/STEP-060D_WeatherRuntimeBridge_attach精确诊断_v1.md @@ -0,0 +1,40 @@ +# STEP-060D WeatherRuntimeBridge attach 精确诊断 + +状态:进行中(`IMPLEMENTED / WAITING_CARBON7I_ATTACH_DIAGNOSTICS`) + +## 目标与基线 + +在 `particle/main` 的 `d1b1e890acdc291011d52e337a52c19a8f665e08` 基线上,将 STEP-060 attach 的笼统 weather 对象缺失错误升级为可定位 source、两个 layer 与最终 Style 快照的稳定诊断。未进入 STEP-061 或 binary U/V transport。 + +## 完成项 + +- [x] `attach()` 分别计算 `rn-weather-fields`、`rn-weather-color`、`rn-weather-particles` 是否存在。 +- [x] 任一对象缺失时以 `WEATHER_OBJECTS_NOT_READY` 返回 `reactTag`、MapView/MapLibreMap/Style present 状态及三个对象的独立 present/missing 状态。 +- [x] 诊断含 `styleSourceCount`、`styleLayerCount` 和有界 source/layer ID 摘要;每类最多 24 个 ID,weather ID 优先保留,余项以 `…+N` 明示截断。 +- [x] 早期分支保持 `MAP_VIEW_NOT_FOUND`、`MAP_NOT_READY`、`STYLE_NOT_READY`;三对象都在时仍走原有 `replaceWeatherRuntime()` 成功路径,未增加重试、timer 或新的控制语义。 +- [x] 临时 Maven version 改为 `13.4.1-step060d`,避免新 AAR 复用旧 `13.4.1` 缓存;Test Host 的既有 version wiring 同步到该坐标,未改变业务流程。 +- [x] Linux OpenGL Release 四 ABI `make apackage` 通过,Maven Local publication 通过;AAR/POM/module 坐标一致。 + +## 交付 AAR + +- group:`org.maplibre.gl` +- artifact:`android-sdk-opengl` +- version:`13.4.1-step060d` +- build output:`platform/android/MapLibreAndroid/build/outputs/aar/MapLibreAndroid-opengl-release.aar` +- Maven Local:`/root/.m2/repository/org/maplibre/gl/android-sdk-opengl/13.4.1-step060d/android-sdk-opengl-13.4.1-step060d.aar` +- SHA-256(两者一致):`7391440f02e976f295caa265bc15e895a7c0183883a0e2cb08deef4cf42b0096` + +## 验证与限制 + +- [x] `JOBS=3 RENDERER=opengl BUILDTYPE=Release make apackage`:PASS。 +- [x] `:MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain`:PASS。 +- [x] `git diff --check`:PASS。 +- [ ] `:maplibre-particle_weather-runtime-bridge:compileDebugKotlin`:当前 untracked Expo Android 生成工程没有 `NativeWeatherRuntimeBridgeSpec`,标准 `:app:generateCodegenArtifactsFromSchema` 也为 `SKIPPED`,故无法独立验证 bridge Kotlin。失败为已有 codegen 产物缺失而非本轮诊断源码的类型错误;本轮不修改生成工程、codegen 契约或 RN bridge 架构来掩盖它。 + +## 禁止范围确认 + +未修改 WeatherRuntime、MapLibreMap、Core renderer/shader、固定 weather ID、React Native 业务流程或 binary U/V transport;未执行 Windows Maven Local 更新、dependencyInsight、APK build/install、ADB 或 Android 运行验证。 + +## 下一步 + +将本记录的唯一 AAR 坐标/SHA 提供给 CARBON7i:更新其 Maven Local,确认 dependencyInsight 后由用户手工 Android 运行 attach,收集 `WEATHER_OBJECTS_NOT_READY` 精确诊断。仍属于 STEP-060;不得据此进入 STEP-061。 diff --git a/docs/50_Step记录/STEP-060E_CoreOnlyLayer存在性检查修复_v1.md b/docs/50_Step记录/STEP-060E_CoreOnlyLayer存在性检查修复_v1.md new file mode 100644 index 000000000..ac31ce280 --- /dev/null +++ b/docs/50_Step记录/STEP-060E_CoreOnlyLayer存在性检查修复_v1.md @@ -0,0 +1,37 @@ +# STEP-060E CoreOnly Layer 存在性检查修复 + +状态:`IMPLEMENTED / WAITING_CODE_REVIEW` + +## 目标与基线 + +在 `particle/main` 的 Win11 已验证 RN Test Host 基线 +NaN2d2375cd532468821c6e3c158ee1a275f4522044` 上,修正 STEP-060D 的 attach +存在性检查假阴性。本轮仍属于 STEP-060,未进入 STEP-061。 + +## 根因与修正 + +- `raster-particle` 与 `weather-color` 在 Android `LayerManagerAndroid` 中以 + `addLayerTypeCoreOnly(...)` 注册。Core 可以解析、持有和渲染它们,但不创建 Java + `Layer` peer。 +- 因此 `Style.getLayer(id)` 经 `createJavaLayerPeer(...)` 后会对真实存在的 CoreOnly + layer 返回 null;`Style.getLayers()` 也不能作为 Core layer 真值。 +- 新增窄范围公开 API `MapLibreMap.hasStyleSource(String)` 与 + `MapLibreMap.hasStyleLayer(String)`,经 `NativeMap`、`NativeMapView`、JNI 到 + `NativeMapView`,分别直接调用 Core `Style::getSource/getLayer`。 +- RN `WeatherRuntimeBridge.attach()` 保持 fully-loaded `Style` 前置条件,但三项存在性 + 判断统一改走上述 Core 真值;成功时原有 `replaceWeatherRuntime()`、snapshot 与 detach + listener 不变。 +- `WEATHER_OBJECTS_NOT_READY` 仍给出三个固定对象的独立状态,诊断改为输出 + `Style.getUri()`(JSON style 为 ``),不再枚举 Java source/layer peer。 + +## 版本与验证边界 + +- Maven 临时坐标已同步为 + `org.maplibre.gl:android-sdk-opengl:13.4.1-step060e`。 +- `AAR_BUILD_NOT_EXECUTED_BY_CODEX`:本轮不构建 AAR、不发布 Maven Local。 +- Android APK、ADB、设备/模拟器验证尚未执行;必须在人工构建并发布该新版本 AAR 后进行。 + +## 结果 + +CoreOnly 架构未改变、未新增 Java weather layer peer、未引入 attach retry,且没有修改 +WeatherRuntime 状态机、binary field transport 或 STEP-061 范围。 diff --git a/docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v1.md b/docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v1.md new file mode 100644 index 000000000..678d0261e --- /dev/null +++ b/docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v1.md @@ -0,0 +1,24 @@ +# STEP-060 RN Test Host 与 WeatherRuntime 薄桥接 + +状态:完成(`IMPLEMENTED / WAITING_ANDROID_VALIDATION`) + +## 目标与基线 + +基于 `particle/main` 的 `b535d98263af58ac86845f15bb7c51bd85b59519`,在仓库内建立独立 RN Test Host,证明 `reactTag → MLRNMapView → MapLibreMap → per-map WeatherRuntime` 的低频控制链路;不进入 STEP-061 Binary Field 或 NavSea 正式接入。 + +## 完成项 + +- [x] 冻结 RN 0.85.2 / React 19.2.5 / Expo 56.0.4 / MLRN 11.3.6 / TypeScript 5.9.3 基线。 +- [x] 以 MLRN 实际 11.3.6 源码核对 `nativeVersion/nativeVariant` 与 reactTag resolver;custom OpenGL SDK 只从 Maven Local 解析。 +- [x] Test Host style 明确含 host-field RasterArraySource、WeatherColor 与 RasterParticle layers。 +- [x] Phase A:getState、enabled、两层 visible、targetFps。 +- [x] Phase B:forecast timestamp、playback/speed、ramp、独立 particle/trail color、粒子视觉与 WeatherColor opacity。 +- [x] map-owned WeatherRuntime、style replacement、View detach 与 React unmount dispose 路径。 + +## 边界 + +Bridge 每次由 Fabric UIManager 解析 reactTag,不复制 MLRN 私有 `mViews`,不建立 global/current-map 或 timeline/playback registry。`MapLibreMap.replaceWeatherRuntime` / `disposeWeatherRuntime` 是为消除桥接 registry 所作的最小上游 Host ownership seam;它不改 WeatherRuntime 状态机或 C++ renderer。不存在 field payload、provider/network、RN JS state machine、iOS bridge 或 NavSea 业务代码。 + +## 验证与剩余 + +已执行 throttle 下 `npx tsc --noEmit`(PASS)、package-lock/config 一致性检查(PASS)、bridge boundary 搜索(PASS)与 `git diff --check`(PASS)。Android Gradle、artifact publish、APK、ADB、设备和 Jest 明确留给手工验收;本 Step 不宣称 Android 已验证。 diff --git a/docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v2.md b/docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v2.md new file mode 100644 index 000000000..a26b28892 --- /dev/null +++ b/docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v2.md @@ -0,0 +1,33 @@ +# STEP-060 RN Test Host 与 WeatherRuntime 薄桥接 + +状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION` + +## 目标与基线 + +本记录在 v1 基础上追加 STEP-060F 收尾:修正 Android WeatherColor opacity 的 typed 路径,固定 Test Host 的 Metro reverse 兼容与横屏交接;仍属于 STEP-060,不进入 STEP-061 Binary U/V Field Transport。 + +## 已完成项 + +- [x] STEP-060E Android attach 已人工 PASS;CoreOnly source/layer 真值查询使 `WEATHER_OBJECTS_NOT_READY` 消失。 +- [x] `WeatherRuntime.setWeatherColorOpacity()` 不再误走仅适用于 `RasterParticleLayer` 的 float property setter。 +- [x] 新增 `MapLibreMap.setWeatherColorOpacity(String, float)`,经 NativeMap、NativeMapView、JNI 直达 Core `WeatherColorLayer::setOpacity()`;不存在、类型不符或非 finite 输入均返回 false,未创建 Java Layer peer。 +- [x] RasterParticle 的 float-property whitelist、`RasterParticleLayer` 类型语义及 Timeline/Playback 均未扩大或修改。 +- [x] 纳入受控 `scripts/05-install-and-run.ps1`:保留 `8091 -> 8091`,并在安装前增加 `8081 -> 8091`。 +- [x] 保持 `app.json` 的 `orientation: landscape`;下次 Android prebuild/build 应生成 `MainActivity android:screenOrientation="landscape"`。 +- [x] 临时 Maven 坐标切换为 `org.maplibre.gl:android-sdk-opengl:13.4.1-step060f`。 + +## 验证边界与当前结果 + +- [x] 已作 JNI Java/C++ 声明、实现与 registration 静态一致性检查;WeatherColor opacity 唯一路径直达 `WeatherColorLayer::setOpacity()`。 +- [x] 已检查双 reverse、版本接线、landscape JSON 与源码 diff。 +- [ ] Android AAR、Maven Local、APK、Gradle、ADB/设备与 Jest:本轮未执行,等待用户按人工验收文档验证。 + +无 Binary Field 时 `WAITING_FOR_FIELDS / PAUSE` 下的 Timeline 命令仍可返回 `TIMELINE_NOT_READY`;这是预期 fail-closed 行为。最终 Android hard gate 尚需确认 `weather opacity: ACCEPTED` 且不再有 `STYLE_PROPERTY_SET result=UNSUPPORTED_PROPERTY`。 + +## 禁止范围确认 + +未改 `RasterParticleLayerFactory` / `WeatherColorLayerFactory` CoreOnly 注册、NavSea、Bridge JS state、Timeline 语义或 STEP-061 内容。 + +## 下一步 + +用户手工构建并发布 `13.4.1-step060f` OpenGL AAR,核对 Linux/Win11 SHA-256 与两条 dependencyInsight,随后 Android clean prebuild、APK build、双 reverse 安装和设备验收。只有所有 Android hard gate 通过后,STEP-060 才可标记 CLOSED / PASS。 diff --git a/docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v3.md b/docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v3.md new file mode 100644 index 000000000..1cbfeaf78 --- /dev/null +++ b/docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v3.md @@ -0,0 +1,27 @@ +# STEP-060 RN Test Host 与 WeatherRuntime 薄桥接 + +状态:`CLOSED / PASS` + +## 最终结论 + +STEP-060 已完成 React Native Test Host → MLRN → custom MapLibre Android → WeatherRuntime 的低频控制面闭环,并于 2026-09-08 完成 STEP-060F Android 最终实测。 + +- CoreOnly `WeatherColorLayer` / `RasterParticleLayer` 保持不变,未新增 Java weather layer peer。 +- RN bridge attach 通过 Core Style source/layer 真值检查,不再依赖 Java `Style.getLayer/getLayers`。 +- `WeatherRuntime.setWeatherColorOpacity()` 已使用独立 typed Java/JNI/Core 路径,直达 `WeatherColorLayer::setOpacity()`。 +- RasterParticle float-property whitelist 与 Timeline/Playback fail-closed 语义未扩大或改变。 +- Test Host 固定横屏;安装脚本同时建立 `8091 -> 8091` 与 `8081 -> 8091` Metro reverse。 + +## Android 最终验收 + +- OpenGL Release AAR `13.4.1-step060f` 使用 `JOBS=1` 构建成功,四 ABI 均存在。 +- Linux build output 与 Maven Local AAR SHA-256 一致:`53dfcd43f3479ae43a3c0fd91c10ccbfec7cf1c84ae22ae3435c94393806da6d`。 +- CARBON7i 下载后的 AAR SHA-256 与 Linux 一致,Win11 Maven Local 安装完成。 +- Bridge 与 MLRN 两条 `dependencyInsight` 均解析到 `org.maplibre.gl:android-sdk-opengl:13.4.1-step060f`。 +- Test Host APK `assembleDebug`:`BUILD SUCCESSFUL`。 +- Android 设备 `192.168.1.146`:`SCREEN_ORIENTATION_LANDSCAPE`。 +- 运行时:`WeatherRuntime attached`,`runtime: WAITING_FOR_FIELDS / PAUSE / AUTO`。 +- 完整验证 UI:`weather opacity: ACCEPTED`。 +- 当前 App PID 日志未发现 `WEATHER_OBJECTS_NOT_READY`、`UNSUPPORTED_PROPERTY`、`FATAL EXCEPTION`、`SIGSEGV` 或 `JNI DETECTED`。 + +无 Binary Field 时 Timeline 命令继续允许因 `TIMELINE_NOT_READY` 返回 `REJECTED`;这是预期行为,不阻塞 STEP-060 关闭。 diff --git a/docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v1.md b/docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v1.md new file mode 100644 index 000000000..ba164e698 --- /dev/null +++ b/docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v1.md @@ -0,0 +1,41 @@ +# STEP-061 Binary U/V Field Transport + +- 状态:`进行中 / WAITING_RDC_REVIEW` +- 起始 HEAD:`e3c36880960ddb74abeafefe5c5662e1a61b46dc` +- 分支:`particle/main` + +## 目标 + +以既有 `WeatherRuntime.submitWindField(metadataJson, byte[])` 为唯一 Core ingress,完成 RN Test Host 的 canonical U/V 原始二进制 file-reference transport,并保留 Core 对 metadata、byteLength、Timeline 和 Runtime Field Window 的 fail-closed 所有权。 + +## 已完成 + +- [x] 调查确认 HEAD 与 `origin/particle/main` 一致;既有 Java/JNI/Core Host ingress 可复用。 +- [x] TurboModule 增加 `clearWindFields(reactTag)` 与 `submitWindField(reactTag, metadataJson, payloadFileUri)`。 +- [x] Kotlin 只读取受限的本地 `file://` URI 或绝对路径为原始 `byte[]`,再调用既有 WeatherRuntime;相对路径、非本地、空、不可读、`IOException` 及超过 64 MiB 文件均 fail-closed,metadata 上限按 UTF-8 bytes 计算。 +- [x] Test Host 用 Expo FileSystem 56.0.11 写入两个 4×3 `float32-le/interleaved-uv` canonical WIND/GLOBAL frame;`DataView.setFloat32(..., true)` 明确 little-endian。 +- [x] Test Host 初始/每次提交均为 PENDING,严格断言 invalid/T0/T1/Native state 依次为 `REJECTED_ADAPTER`/`ACCEPTED_PENDING_INITIAL_WINDOW`/`INITIAL_WINDOW_READY`/`READY`;不符即 ERROR。 +- [x] Runtime UI 区分 Core evidence 与 producer identity;MIX 标为 Core-owned/not exposed,最终 T0/T1 真值留给既有 Android logcat 事件验证。 +- [x] 坐标已准备为唯一 `13.4.1-step061`(`platform/android/VERSION`、plugin、bridge dependency、README);未覆盖 `step060f`,本轮未 build/publish。 +- [x] 本轮修正后的 `npm run typecheck`、TurboModule Codegen(输出至 `/tmp`)、plugin `node --check`、禁止 transport 审计与 `git diff --check` 通过。 + +## 允许修改 + +RN Test Host、weather-runtime-bridge、其 README/package 直接依赖,以及本 Step/完成报告/开发记录。 + +## 禁止修改 + +不修改 NavSea、provider/network/download/cache 业务、Particle/Core Timeline 数据模型或视觉参数;不提交 build 产物;不加入或运行 Jest。 + +## 当前结果 + +RDC NEEDS FIX 的 UI、I/O 与版本坐标修正已实现并通过本轮静态验证。Runtime snapshot 当前只公开 State/Playback/FPS;`INITIAL_WINDOW_READY + READY` 是 Core evidence,界面中的 generation/time 明确只是 producer identity,MIX 继续由 Core 独占且未新增观测 API。 + +## 未完成/阻塞 + +- [ ] RDC 对真实 diff 审查。 +- [ ] 审查通过并获后续明确指令后,在指定 `particle-codex:step061` tmux window 构建并 Maven Local publish 唯一 `13.4.1-step061` AAR,再继续 Android Gradle/APK 和实机端到端验收;本次按任务要求未执行。 + +## 下一步 + +停在工作区,等待 RDC 审查后再进行任务书限定的端到端流程。 diff --git a/docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v2.md b/docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v2.md new file mode 100644 index 000000000..b96a5f14b --- /dev/null +++ b/docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v2.md @@ -0,0 +1,53 @@ +# STEP-061 Binary U/V Field Transport v2 + +状态:`CLOSED / PASS` +日期:2026-09-09 + +## 最终结论 + +STEP-061 已完成 RN Test Host → WeatherRuntime → Android Java/JNI → RasterArraySource/Core 的正式 Binary U/V Field Transport,并完成 Android OpenGL 端到端实机验收。 + +- 正式 transport 为 `metadataJson + local file reference`;payload 为 raw `float32-le / interleaved-uv` bytes。 +- 未使用 `number[]`、payload JSON、base64、hex,也未引入 provider/network/vendor 逻辑到 Native/Core。 +- 继续复用唯一 `WeatherRuntime.submitWindField(metadataJson, byte[])` ingress;未新建第二套 Adapter、Timeline 或 Runtime Field Window。 +- Core 保持 schema、byteLength、generation、validTime、Timeline、Runtime Window 与 fail-closed 的最终所有权。 + +## 构建与制品 + +- Source commit:`4467adcce02db7f1bbbea1f4a829023c53085e39`。 +- AAR:`org.maplibre.gl:android-sdk-opengl:13.4.1-step061`。 +- AAR 固定使用 `JOBS=1 RENDERER=opengl BUILDTYPE=Release make apackage` 构建,输出确认 `--max-workers=1`,最终 `BUILD SUCCESSFUL in 7m 10s`。 +- Maven Local publish:`BUILD SUCCESSFUL in 20s`。 +- Linux build output 与 Maven Local AAR SHA-256 一致:`40ec19724cbe0807c09c6acf9b7e6e0dac2f2498f1269b6bacdd35740a6cd77a`。 +- CARBON7i 通过 `curl.exe -L` 加不同 `?v=` 下载 AAR/POM/module;Win11 AAR SHA-256 与 Linux 完全一致。 +- Win11 Maven Local 安装完成;Bridge 与 MLRN 两条 `dependencyInsight` 均解析到 `13.4.1-step061` 并 `BUILD SUCCESSFUL`。 +- Test Host APK `assembleDebug`:`BUILD SUCCESSFUL in 1m 50s`。 + +## Android 最终验收 + +设备固定为 `192.168.1.146`,横屏运行;双 reverse 为 `8091 -> 8091`、`8081 -> 8091`。 + +重复提交并重新读取 Native state 后,结果稳定为: + +- `runtime: READY / PAUSE / AUTO` +- `Binary Transport: READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV` +- `Invalid Probe: REJECTED_ADAPTER` +- `T0 Submit Result: ACCEPTED_PENDING_INITIAL_WINDOW` +- `T1 Submit Result: INITIAL_WINDOW_READY` +- `Runtime Field Window: Core evidence: INITIAL_WINDOW_READY + runtime READY` +- producer identity:`T0=1@1767225600000`、`T1=2@1767229200000` +- MIX:`Core-owned / not exposed` + +当前 App PID 为 `24476`;日志 hard gate 中 `FATAL EXCEPTION`、`SIGSEGV`、`WEATHER_OBJECTS_NOT_READY`、`JNI DETECTED ERROR`、`UnsatisfiedLinkError`、`NoSuchMethodError` 均为 0。 +## 可观测性说明 + +Release AAR 中既有 `HOST_FIELD_SNAPSHOT_READY`、`HOST_FIELD_INITIAL_WINDOW_READY` 为 `Log::Info`,本轮设备 logcat 未观察到这两条 INFO 事件;未把不存在的日志写成 PASS 证据。负向 `HOST_FIELD_SUBMIT_REJECTED` Warning 可见,并明确给出 `PAYLOAD_SIZE_MISMATCH`。 + +正向 hard gate 采用 bridge 直接返回的 Core submit result,加上重新执行 GETSTATE 后的 Native `READY` 状态;这两类结果均来自现有 WeatherRuntime/Core 链,不由 JS 自行推导 T0/T1/MIX。 + +## 收尾 + +- 未启用 `07-enable-full-validation-ui.ps1`,因此没有临时 STEP-060 validation UI 需要回退;legacy runtime `App.tsx` 与当前 Git 工作副本 `App.tsx` SHA-256 完全一致,baseline 已保持。 +- `/mnt/nas/pbf` 中本 Step 临时公开的 AAR/POM/module 已全部删除。 +- 未进入 NavSea,未加入或运行 Jest。 +- STEP-061 正式关闭,状态为 `CLOSED / PASS`。 diff --git a/docs/50_Step记录/STEP-062_RNTestHost真实WeatherV2底图粒子综合验证_v1.md b/docs/50_Step记录/STEP-062_RNTestHost真实WeatherV2底图粒子综合验证_v1.md new file mode 100644 index 000000000..5d19c9199 --- /dev/null +++ b/docs/50_Step记录/STEP-062_RNTestHost真实WeatherV2底图粒子综合验证_v1.md @@ -0,0 +1,40 @@ +# STEP-062 RN Test Host 真实 WeatherV2 底图粒子综合验证 v1 + +状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +## 目标与冻结基线 + +- 分支:`particle/main`;开工 HEAD:`364596574a620037fa854cfea174075f8107a504`。 +- Test Host 固定消费 `org.maplibre.gl:android-sdk-opengl:13.4.1-step061`;没有修改、构建或发布 AAR。 +- 范围仅 `MapLibreParticleRNTestHost`,不进入 NavSea,未改 Native/Core/JNI/WeatherRuntime/STEP-061 ingress,未加入或运行 Jest。 + +## 必做项 + +- [x] GSI 标准 raster source/layer(256、maxzoom 18、国土地理院 attribution)置于 WeatherColor/RasterParticle 下方。 +- [x] 初始 Camera 聚焦 `[135, 32]`、zoom `3.9`,直接覆盖日本及相邻 WeatherV2 区域。 +- [x] 原样复制 ECMWF HRES `2026080700/06/12/18` dataset、metadata、raw field.f32 到 Test Host 受控 assets。 +- [x] `expo-asset` 直接依赖与仅增加 `.f32` 的 Metro asset 配置。 +- [x] `LOAD REAL WEATHER T0-T3`:clear → materialize 四个 `file://` URI → validTime 递增 submit → Native `READY` 硬断言。 +- [x] 显示 GSI/loading error、dataset identity、T0–T3 submit result、Runtime 与 FILE_REF/FLOAT32_LE/INTERLEAVED_UV transport 状态。 +- [x] 接线 playback/speed、timestamp seek、weather-local FPS、双层 visibility、冻结 stops、独立颜色和粒子视觉参数。 +- [x] Test Host README、开发记录、中文完成报告。 + +## 资产与 transport + +每帧 grid=`513×769`,raw payload=`3,155,976 bytes`,canonical contract=`maplibre-particle-field/v1` / `float32-le` / `interleaved-uv`(U eastward、V northward)。Expo Asset 只输出其 cache 中的本地 `file://` URI;payload 不进入 JS number array、JSON、base64 或 hex。Expo/OS 管理 cache,代码没有另建或累积 `.f32` 临时文件。 + +预期 Core submit 顺序严格为:T0 `ACCEPTED_PENDING_INITIAL_WINDOW`、T1 `INITIAL_WINDOW_READY`、T2 `ACCEPTED_NEXT_SNAPSHOT`、T3 `ACCEPTED_NEXT_SNAPSHOT`,最终 `READY`。T0/T1/MIX runtime window 仍由 Core 独占,JS 只保留 producer timestamp 供 `setForecastTime(timestampMs)` 调用。 + +## 静态验证 + +- `npm run typecheck`:PASS(经 build-output-throttle,log 位于 `/tmp/step062-build-logs/`,未加入工作树)。 +- `node --check metro.config.js` 与实际 config `.f32` extension:PASS。 +- `npx expo export --platform android`:PASS;Metro 实际导出四个 `3,155,976`-byte `.f32` assets。 +- dataset、四个 metadata、四个 field.f32 的源/目标 SHA-256:逐文件一致,四个 field 复用相同 Git blob。 +- transport static audit:PASS;源码层数组顺序为 `rn-basemap → gsi-standard → rn-weather-color → rn-weather-particles`;`git diff --check`:PASS。 + +## 当前结果、阻塞与下一步 + +代码状态为 **IMPLEMENTED / WAITING_ANDROID_VALIDATION**,绝不等同 Android PASS。Codex sandbox 的 `.git/index.lock` 只读限制已由 RDC 接管收尾;功能实现已提交并推送:`a91e3601f37f3adfe5d8b7b6a6c3f7f04fcadb8f`(`feat: add STEP-062 real weather test map`)。 + +后续 CARBON7i APK-only 周期需在同一冻结 `13.4.1-step061` AAR 下验证:GSI tile 实际加载与 attribution、四帧 Asset file URI、T0–T3 硬断言/Native READY、WeatherColor/Particle 地理对齐、pan/zoom/bearing、seek/playback/FPS/visibility 独立性,以及连续提交下 OOM、卡死和异常 GC。Codex 未执行 Android Gradle、assembleDebug、ADB、安装或设备测试。 diff --git a/docs/50_Step记录/STEP-063_MapLibre粒子Android_v1发布工程化_v1.md b/docs/50_Step记录/STEP-063_MapLibre粒子Android_v1发布工程化_v1.md new file mode 100644 index 000000000..5b8e3d890 --- /dev/null +++ b/docs/50_Step记录/STEP-063_MapLibre粒子Android_v1发布工程化_v1.md @@ -0,0 +1,28 @@ +# STEP-063 MapLibre 粒子 Android v1 发布工程化 v1 + +状态:`IMPLEMENTED / WAITING_MANUAL_RELEASE_VALIDATION` + +## 目标与基线 + +- 实际 MapLibre Native Android 基础版本:`13.4.1`;core:`18545ca10d296704b2cba675b182ff2c8da24588`;renderer:OpenGL。 +- 正式版本:`13.4.1-particle.1`;npm:`@navsea/maplibre-particle-rn@13.4.1-particle.1`;Maven:`cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`。 +- STEP-061 的 `13.4.1-step061` 是开发期已验收坐标,不作为正式 release/package/build config。 + +## 已完成 + +- [x] 建立机器可读 release manifest,registry 均明确为 `null`。 +- [x] Android publication 配置改为正式 group/artifact/version,保留 OpenGL publication postfix。 +- [x] 建立正式 RN facade package,含 JS/TS source、类型、Android bridge、autolinking、Expo plugin、README、CHANGELOG、LICENSE。 +- [x] package plugin 将 MLRN 的单一 OpenGL SDK dependency substitution 到正式 NavSea AAR,避免双 Native AAR。 +- [x] 建立只声明正式 npm 包的 Consumer Test Host;不引用源码 package、Native tree 或复制 bridge。MLRN 采用 peer dependency,避免嵌套 native module 与 Windows codegen 长路径。 +- [x] Windows 预发布 Consumer 首次 APK 编译暴露嵌套 MLRN codegen 路径超过 260 字符;已修正式包依赖模型并重新打包,等待新 tarball 重装后复验。 +- [x] `npm pack`、tarball 内容审计、package JSON、plugin syntax、package TypeScript check、`git diff --check` 静态验证。 +- [x] 设计、Step、完成报告和开发记录。 + +## 禁止项与未执行项 + +未改 Particle/Core/JNI/WeatherRuntime 业务功能,未接入 NavSea,未执行 Android Gradle、AAR build/publish、Maven Local publish、APK、ADB 或 Jest。未创建 release tag,未决定 npm/Maven server。 + +## 下一步 + +用户手工构建并在 Maven Local 或临时 staging 准备 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`;以 npm tarball 安装 Consumer Host,手工编译/安装 APK 并验证 GSI、WeatherColor、Particle 与 Runtime READY。全部通过后才可进入 release freeze/tag 任务。 diff --git a/docs/50_Step记录/STEP-063_MapLibre粒子Android_v1发布工程化_v2.md b/docs/50_Step记录/STEP-063_MapLibre粒子Android_v1发布工程化_v2.md new file mode 100644 index 000000000..10bd0a30d --- /dev/null +++ b/docs/50_Step记录/STEP-063_MapLibre粒子Android_v1发布工程化_v2.md @@ -0,0 +1,109 @@ +# STEP-063 MapLibre 粒子 Android v1 发布工程化 v2 + +状态:`CLOSED / PASS` +日期:2026-09-09 + +## 最终结论 + +STEP-063 已完成 MapLibre Particle Android v1 的正式发布工程化,并完成“正式 npm package → 正式 Maven AAR → React Native Consumer → 真实 WeatherV2 → WeatherColor / Particle”的 Android 实机消费验证。 + +正式版本冻结为: + +```text +MapLibre Native upstream : 13.4.1 +npm package : @navsea/maplibre-particle-rn@13.4.1-particle.1 +Android Maven artifact : cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1 +Maven repository : https://pbf.jpcn.ccwu.cc/maven +renderer : OpenGL +``` + +STEP-061 的 `13.4.1-step061` 只保留为历史开发制品,不再作为正式 Consumer、package 或 Maven 发布依赖。 + +## 正式制品 + +- 正式 Android AAR:166,304,929 bytes;SHA-256 `63c976c5a73fe8f132f428afecca40b09fa2dbdedaa13721586c472ea7eb279d`。 +- 正式 npm 预发布 tarball:`navsea-maplibre-particle-rn-13.4.1-particle.1.tgz`,9,100 bytes。 +- tarball SHA-1:`247e2db901f6588c9e48bc3156a448b97d203d0d`。 +- tarball SHA-256:`31335791a5a6e553a538f84816322e8cd119f16d7c2b15436ce785aa497da6a8`。 + +## Consumer 依赖模型修正 + +Windows 首次 Consumer APK 编译曾因 MLRN 被安装在: + +```text +@navsea/maplibre-particle-rn/node_modules/@maplibre/maplibre-react-native +``` + +导致 RN codegen/CMake 路径超过 Windows 260 字符限制。正式包随后将 `@maplibre/maplibre-react-native@11.3.6` 改为 peer dependency,并保留 devDependency 仅供包自身开发;修正提交为 `fa431ba6fd1f182b8b77345b50a9d74e225f17d4`。 + +重新安装后实际验证: + +```text +root MLRN = present +nested MLRN = absent +``` + +Gradle `dependencyInsight` 同时确认: + +```text +org.maplibre.gl:android-sdk-opengl:13.4.1 +→ cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1 +``` + +没有回退到官方 Native AAR、Maven Local 或 `step061`。 + +## 真实 WeatherV2 Package Consumer 验证 + +Consumer Test Host 已由 2×2 smoke fixture 升级为真实 WeatherV2 T0–T3 验收 Host;实现提交:`3ac678a3c8164082452bd77e61989ccc928ed8bd`。 + +真实数据固定为 ECMWF HRES 2026-08-07: + +```text +T0 2026080700 +T1 2026080706 +T2 2026080712 +T3 2026080718 +``` + +每帧: + +```text +grid = 513 × 769 +payload = 3,155,976 bytes +schema = maplibre-particle-field/v1 +encoding = float32-le +layout = interleaved-uv +U = eastward-positive m/s +V = northward-positive m/s +bounds = west 112.5 / south 0.0 / east 157.5 / north 55.77657301866769 +transport = metadataJson + local file:// payload +``` + +Linux 静态验证已通过:TypeScript、根层 peer dependency、禁止旧 bridge / direct MLRN import 审计、Metro Android export;Metro 实际识别并导出 4 个 `.f32` assets。 + +Win11 Consumer 编译环境按 NavSea 基线对齐:Gradle Wrapper 8.14.3、buildTools 36.0.0、minSdk 24、compile/target 36、NDK 27.1.12297006、Kotlin Plugin 2.1.20、顶层声明 AGP 8.5.0、RN Gradle Plugin 请求 AGP 8.12.0、JDK 17、New Architecture/Hermes 开启、edge-to-edge 关闭。 + +用户随后手工完成 `assembleDebug`、APK 安装与实体机运行,并明确确认当前正式 Package Consumer APK 正常。结合本轮真实 WeatherV2 Consumer 的设计,STEP-063 的人工 release validation 门槛已通过。 + +## 最终边界 + +本 Step 没有修改 Particle/Core/JNI/WeatherRuntime 业务算法,没有接入 NavSea,没有加入或执行 Jest。正式 package Consumer 不引用 MapLibreParticleLab source path、旧本地 bridge、Maven Local 或手工复制 AAR。 + +当前 release manifest 可以进入“已验证、等待最终 release freeze/tag”状态,但仍然: + +```text +npmRegistry = null +releaseTag = null +``` + +因此 `CLOSED / PASS` 表示 STEP-063 发布工程化与 Consumer 验收完成,不等于 npm 已上传或 release tag 已创建。 + +## React 使用文档 + +React Native 正式调用、生命周期、提交 API 以及 canonical 显示数据结构详见: + +`docs/30_设计/MapLibre粒子ReactNative使用说明_v1.md` + +其中明确冻结:规则二维 NW-origin row-major Grid、`float32-le / interleaved-uv`、每 cell 8 bytes、U eastward / V northward、metadata 字段、byteLength 公式、file-reference transport、Timeline 与 React/Core 责任边界。 + +下一阶段如需正式创建 tag,应单独执行 Release Freeze;不要在本 Step 文档收尾中隐式创建 tag。 diff --git a/docs/50_Step记录/STEP-064_RN纯FieldRenderer接口V2_v1.md b/docs/50_Step记录/STEP-064_RN纯FieldRenderer接口V2_v1.md new file mode 100644 index 000000000..f6d00f4d1 --- /dev/null +++ b/docs/50_Step记录/STEP-064_RN纯FieldRenderer接口V2_v1.md @@ -0,0 +1,51 @@ +# STEP-064 RN 纯 Field Renderer 接口 V2 + +状态:冻结(13.4.1-particle.2 正式发布完成) + +## 目标 + +将 `@navsea/maplibre-particle-rn` 升级为 `13.4.1-particle.2` 纯 Field Renderer 公共 API。React/宿主负责业务、WeatherV2、provider、网络、数据选择和文件管理;SDK 只向既有 Native ingress 提交 canonical Field 并控制图场/粒子渲染。 + +## 已完成 + +- 新增根入口 `FieldRuntime`,以薄映射保留既有 Native bridge ABI,未保留旧 API wrapper。 +- 公共类型使用 `FieldColorStop.value`;保留无业务含义的 Runtime、Playback、FPS 与结果类型。 +- 移除 package 内 `src/weatherV2/` 与根导出;不再转导出 MLRN。 +- 新增不含底图的 `particleFieldIds` 和 `createParticleFieldStyleFragment()`。 +- `TurboModuleRegistry.get()` 使无 Native module 的 import 安全;实际调用时给出明确错误。 +- Consumer Host 直接从官方 MLRN 导入 Map/Camera,仅从 NavSea 包导入 Field Renderer API;本地真实 fixture 仍由宿主提交 canonical metadata 与 file URI。 +- release manifest 明确 RN `.2` 对应冻结 Native `.1`。 + +## 验证 + +- package TypeScript typecheck:通过。 +- Consumer TypeScript typecheck:通过(使用临时类型解析配置,仅用于检查,不进入 Consumer 产品依赖)。 +- `npm pack`:通过,`.2` tarball 仅生成在 `/tmp/step064-pack/`,未触碰仓库既有未跟踪 `.1` tarball。 +- tarball 内容审计:通过;不含 `src/weatherV2/`、WeatherV2 HTTP/decoder/loader、Test Host、node_modules 或构建缓存。 +- 根导出与业务语义审计、`git diff --check`:通过;旧名称仅存在于内部 Native ABI/codegen 接缝,未由根入口公开。 +- Android Gradle、AAR、APK、ADB、Jest:按边界未执行。 +- RN 0.85.2 Codegen parser 已核对支持 `TurboModuleRegistry.get()` 与 `getEnforcing()`;当前 import-safe `get()` 方案保留。 +- 收尾复审修正:`.2` Consumer validation 明确为 pending,`.1` PASS 仅作为 previous baseline;FieldColorStop 文档示例改为满足至少 2 个 stop 的既有 Native 契约。 + +## 待办与边界 + +- 待 Consumer 安装最终 `.2` tarball 并实际运行验证;此项完成前不得标记 RELEASED 或 CLOSED/PASS。 +- 未修改 Native/Core/JNI/shader/Android AAR,Native artifact 继续 `13.4.1-particle.1`。 +- 未进入 NavSea。 + +## 2026-09-10 Consumer 正式验证 + +- Win11 固定 Consumer 安装 `13.4.1-particle.2` tgz,MLRN 唯一解析为 11.3.6,TypeScript 通过。 +- 首轮实测暴露 RN helper 无效属性 `raster-particle-opacity`,导致 `rn-weather-particles` 缺失;仅在 RN helper 改为 Native `.1` 已验证的 `raster-particle-active`,未改 Native。 +- 修正后 `FieldRuntime attached; style ready`;T0–T3 依次返回 Pending / InitialWindowReady / NextSnapshot / NextSnapshot,Runtime READY。 +- `Field OFF + Particle ON` 与 `Field ON + Particle OFF` 均实测通过,并恢复两者 ON。 +- hard gate 未见 WEATHER_OBJECTS_NOT_READY、FATAL、SIGSEGV、JNI DETECTED ERROR、NoSuchMethodError、UnsatisfiedLinkError 或非法 layer property。 + +## 正式发布关闭 + +- Release source commit:`e43b58707cfd0f87965424477ed3f95625b3f724`。 +- Release tag:`maplibre-particle/rn/13.4.1-particle.2`,已 push。 +- 正式 HTTPS:`https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.2.tgz`。 +- 正式 tgz:10,660 bytes;SHA256 `ace9e0401824839d18a93cc4201c1f5d4cccbff0636570cfde82fd80b60779b5`;公网 HTTP 200 且下载字节完全一致。 +- Win11 Consumer 已从正式 HTTPS URL 安装 `.2`,TypeScript 通过,MLRN 11.3.6 单份 dedupe。 +- STEP-064 正式冻结;后续修改必须升 `13.4.1-particle.3`,不得覆盖 `.2` tgz 或移动 release tag。 diff --git a/docs/50_Step记录/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_v1.md b/docs/50_Step记录/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_v1.md new file mode 100644 index 000000000..6f81610c6 --- /dev/null +++ b/docs/50_Step记录/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_v1.md @@ -0,0 +1,25 @@ +# STEP-065 MapLibreParticleLab V3 通用 Scalar / Vector Field 架构冻结 + +状态:冻结 + +## 目标与基线 + +在 `2e3ab262` 对当前真实实现做只读调查,冻结 V3.0 generic Scalar/Vector Contract 和迁移路线;不实现。开始与结束均为 `particle/main`,开始时 `HEAD == origin/particle/main == 2e3ab262`。 + +## 必做项结果 + +- [x] 读取全部规范、STEP-064 基线,复核 branch/HEAD/origin/dirty worktree。 +- [x] 调查 descriptor/snapshot/adapter、WIND/GLOBAL、Timeline/MIX/window reuse、WeatherColor `speedMps`、Particle velocity m/s、RN→Android→JNI→Core。 +- [x] 只读核对 WeatherV2 Wind/Tide/Rain/Wave NSFT 语义。 +- [x] 冻结 0/1 scalar + 0/1 vector + shared validity、`FieldValueSource`、`VectorSemantics`、独立 plane file refs。 +- [x] 冻结 `maplibre-particle-field/v2`、v1 thin normalize compatibility、分批路线和单次 Android 综合验收。 +- [x] 生成调查、设计、Step、报告并追加开发记录。 +- [x] 确认未修改生产代码、WeatherV2、NavSea、STEP-062 或历史无关 untracked 文件。 + +## 允许/禁止与验证 + +仅新增/更新任务指定文档;不改 C++、shader、JNI、Android、RN API 或 fixture,不生成 AAR/tgz。未执行 Android Gradle、APK、ADB、Jest,原因是任务明确禁止。执行只读代码/文本审计与 `git diff --check`/精确 staged 文件审计;不宣称构建或设备 PASS。 + +## 当前结果与下一步 + +详见调查及设计文档。下一任务建议:`STEP-066 V3-02 Core Contract + Scalar Snapshot/Adapter`,只实现 v2/v1 normalize 与 Core fixture/验证,先不进入 GPU 或 Android。未解决风险:v2 具体 Kotlin/JNI ABI 形态、plane 总内存预算、direction 视觉 scale 默认值须在实施前用目标设备约束量化。 diff --git a/docs/50_Step记录/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_v1.md b/docs/50_Step记录/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_v1.md new file mode 100644 index 000000000..99c85391d --- /dev/null +++ b/docs/50_Step记录/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_v1.md @@ -0,0 +1,35 @@ +# STEP-066 MapLibreParticleLab V3-02 Core Contract 与 Scalar Snapshot Adapter + +状态:完成 + +## 目标与基线 + +在 `particle/main` 的 STEP-065 冻结设计上,仅实现 Core Contract、Snapshot、Adapter 与最小 Core 测试。开工时 `HEAD == origin/particle/main == d23ae2079dcfef3ce48517f734fe487207e28d23`;工作区已有 STEP-062 两处用户修改及历史 untracked/build 内容,均未纳入本 Step。 + +## 完成内容 + +- [x] 阅读规范、STEP-065、任务书与当前实际源码。 +- [x] 定义通用 `ParticleFieldPayloadBundle`、v2 plane descriptor 与 `VectorSemantics::Velocity|Direction`。 +- [x] Snapshot 升级为 optional scalar、optional interleaved U/V vector、shared `valid`;移除 Core 对 `WIND/GLOBAL` 的 capability hardcode。 +- [x] 实现 v2 vector-only、scalar-only、scalar+vector;neither、plane presence/长度/encoding、validity、semantics 与 finite/noData 错误 fail-closed。 +- [x] 保留 v1 单 payload parser;在 ingress 派生 shared validity,并归一为 vector-only + `VELOCITY`。 +- [x] 收紧 T0/T1 compatibility 至 grid/bounds、scalar/vector presence 与 vector semantics;不改变 Source-owned Timeline、T0/T1/MIX、pause 或 rollover ownership。 +- [x] `sameFieldGpuContent()` 逐值比较 decoded scalar/vector/validity plane,不依赖 fieldId/dataVersion/legacy hash 猜测内容。 +- [x] 最小 renderer 适配:速度纹理读取改为 generic vector 成员;scalar-only 输入清空/不创建 drawables,V3-03 前 fail-closed;未改 shader、纹理编码、GPU 计算或粒子运动语义。 +- [x] `source.test.cpp` 覆盖 v1 normalize、三种 v2 合法组合、neither、三种长度错误、missing semantics、DIRECTION without vector、NaN/Inf、invalid finite placeholder、compatibility 与三 plane identity。 + +## 验证 + +- `ninja -C build-linux-opengl -j3 mbgl-test`:PASS。 +- `ninja -C build-linux-opengl -j3 test/CMakeFiles/mbgl-test.dir/style/source.test.cpp.o`:PASS。 +- `ninja -C build-linux-opengl -j3 mbgl-test-runner`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter=Source.ParticleFieldV3*`:PASS(退出码 0)。 +- `git diff --check`:PASS。 + +全部 build/test 输出经 `build-output-throttle` 限流。未执行 Android Gradle、AAR/APK、ADB 或 Jest。 + +## 范围确认、阻塞与下一步 + +没有 Android/JNI/RN、WeatherV2、NavSea、shader 或 Test Host JS/TS 修改;STEP-062 用户修改和历史 untracked 内容未暂存。Scalar GPU texture、generic `FieldValueSource`、DIRECTION advection、v2 JNI/RN ingress 均未实现,按冻结路线留给 V3-03/V3-04/V3-05;scalar snapshot 在现有 renderer 为 unavailable/fail-closed。 + +下一步:STEP-067(V3-03 Scalar GPU Texture + Generic Field Renderer)。 diff --git a/docs/50_Step记录/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_v1.md b/docs/50_Step记录/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_v1.md new file mode 100644 index 000000000..350d63bcb --- /dev/null +++ b/docs/50_Step记录/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_v1.md @@ -0,0 +1,37 @@ +# STEP-067 MapLibreParticleLab V3-03 Scalar GPU 与 Generic Field Renderer + +状态:完成(CLOSED / PASS) + +## 目标与基线 + +在 `particle/main` 的 `9dabb95e4425ffbcb863c520923f5d4b537a3feb` 上实现 V3 scalar GPU field bundle 的 WeatherColor 消费,保持 vector magnitude 的 Wind 色场和既有粒子 Velocity 语义。 + +## 完成内容 + +- [x] `WeatherColorLayer` 新增显式 `FieldValueSource::Scalar|VectorMagnitude`,默认 `VectorMagnitude`。 +- [x] WeatherColor 单一 GPU renderer 根据明确 source 编码 scalar 或 U/V,shared validity 始终写入 alpha;T0/T1 均无效或缺少所选 channel 时清理 drawable 并 fail-closed。 +- [x] shader 对 scalar 或 vector magnitude 分别先作 T0/T1 时间插值,再进入同一 generic color ramp;没有 scalar 伪装为 U/V。 +- [x] `WeatherColorStop.speedMps` 收敛为 generic `value`,校验为 finite 且严格递增;日志、UBO 注释和 shader 去除固定 m/s 语义。 +- [x] Particle renderer 明确仅接受 vector present 且 `VectorSemantics::Velocity`;scalar-only 与 `Direction` 立即清理其 GPU resource 并 fail-closed。 +- [x] WeatherColor rollover 仍以 decoded bundle content identity 复用 OLD_T1→NEW_T0;切换 value source 时不误复用不同 GPU 编码,只有新内容上传。 + +## 允许修改 + +WeatherColor style/render、Particle renderer guard、实际 GLSL/生成的 OpenGL shader header、对应 UBO 注释和 C++ layer test。 + +## 禁止修改确认 + +未修改 Android/JNI/Java/Kotlin、RN package/API/version、Test Host、WeatherV2、NavSea、Timeline/Playback/Runtime Window;未实现 DIRECTION advection、Wave physics、v2 JNI/RN ingress;未运行 Gradle、AAR、APK、ADB 或 Jest。 + +## 验证 + +- `node shaders/generate_shader_code.mjs`:PASS(经 build-output-throttle)。 +- 精确受影响对象:`render_weather_color_layer.cpp.o`、`render_particle_validation_layer.cpp.o`、`style_layer.test.cpp.o`:PASS(经 build-output-throttle;ccache 临时目录使用 `/tmp/maplibre-particle-ccache-tmp`)。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='Source.ParticleFieldV3*:Layer.WeatherColorUsesIndependentFixedOpacity'`:PASS,退出码 0。 +- `git diff --check`:PASS。 +- 用户于当前 `HEAD`(`06fcbef9ccb820e595f06acbd356950de7e3a73f`)手工执行 `ninja -C build-linux-opengl -j3 mbgl-test mbgl-test-runner`,输出为 `ninja: no work to do.`。确认 `mbgl-test` 与 `mbgl-test-runner` 两个完整目标及其依赖均已是最新,没有待处理的构建或重链接工作。 +- 上述结果仅确认目标构建/重链接状态;本次没有重新执行全量测试用例,不能将其表述为全量测试用例重跑 PASS。 + +## 阻塞与下一步 + +无功能阻塞。本 Step 的 Linux 全量目标重链收尾已确认;Android 验收按 V3-08 的单独周期执行,且不属于本 Step 的关闭条件。 diff --git a/docs/50_Step记录/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_v1.md b/docs/50_Step记录/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_v1.md new file mode 100644 index 000000000..1c0544889 --- /dev/null +++ b/docs/50_Step记录/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_v1.md @@ -0,0 +1,61 @@ +# STEP-068 MapLibreParticleLab V3-04 DIRECTION 粒子推进 + +状态:完成(CLOSED / PASS) + +## 目标与基线 + +基线为 `particle/main` / `89c72554c5fb5cd469bdd8ba06ff4b03a1a67ff9`,与 `origin/particle/main` 一致。实现通用 `DIRECTION` 向量推进及独立 renderer `advectionScale`,不引入任何 Wave、WeatherV2 或 provider 分支。 + +## 已完成 + +- [x] RasterParticleLayer 新增 `raster-particle-advection-scale` 与 C++ `getAdvectionScale()/setAdvectionScale()`;默认 `1.0`,非 finite 或非正值 fail-closed。 +- [x] State fragment 与正式 line vertex 共享同一语义:分别解码 T0/T1、分量 MIX、检查有限性;DIRECTION 对混合结果归一化并乘 scale,长度 `<= 1e-5` 无效;VELOCITY 保留原始分量 MIX 与 magnitude。 +- [x] UBO 新增明确 `advection` vec4(Direction flag、scale、epsilon),C++ struct/static_assert、两份 GLSL 和 generated OpenGL header 同步。 +- [x] renderer 接受 vector VELOCITY 与 DIRECTION;scalar-only、缺 semantics 或 T0/T1 semantics 不同仍清理并 fail-closed。 +- [x] `advectionScale` 仅更新 renderer uniform,不上传 field texture、不重置 Timeline/State/Trail。 +- [x] C++ oracle 测试覆盖 east/north/diagonal、east/west MIX 归零、scale 改变 magnitude,以及 VELOCITY 不归一化且不消费 scale;Layer 测试覆盖默认、set/get 与非法值。 + +## 禁止范围确认 + +未修改 Android、JNI、Java/Kotlin、RN、WeatherV2、NavSea、Timeline/Playback/Runtime Window、STEP-062 用户修改或历史无关文件;未执行 Gradle、AAR/APK/ADB 或 Jest。 + +## 验证 + +- [x] `node shaders/generate_shader_code.mjs`:PASS(输出节流)。 +- [x] 第一轮受影响对象 `render_particle_validation_layer.cpp.o`、`raster_particle_layer.cpp.o`、`style_layer.test.cpp.o`:PASS(输出节流;发生在新增 CPU oracle 后的最终重编译前)。 +- [x] 既有过滤测试 `Layer.RasterParticle*:Source.ParticleFieldV3*`:PASS(输出节流;发生在新增 CPU oracle 后的最终重链前)。 +- [x] 最终受影响精确对象:`render_particle_validation_layer.cpp.o`、`render_weather_color_layer.cpp.o`、`raster_particle_layer.cpp.o`、`source.test.cpp.o`、`style_layer.test.cpp.o`:PASS。 +- [x] `ninja -C build-linux-opengl -j3 mbgl-test mbgl-test-runner`:PASS,完整目标重编译/重链完成(本轮约 899.5 秒)。 +- [x] STEP-068 相关过滤测试 `Source.ParticleFieldV3*:Layer.RasterParticle*`:8/8 PASS;其中包含 DIRECTION east/north/diagonal、MIX northeast、opposite fail-closed、VELOCITY 不归一化及 advectionScale 属性。 +- [x] `git diff --check`:PASS。 +- [x] shader generator:此前本 Step 已 PASS;WeatherColor UBO 最小修复未修改 shader/generator 输入,无需再次生成。 + +## 2026-09-11 全量 Linux 重编译发现的 UBO 初始化遗漏 + +全量 Linux 重编译在 `src/mbgl/renderer/layers/render_weather_color_layer.cpp:108` 以 +`-Wmissing-field-initializers` 发现 `ParticleGpuValidationUBO::advection` 新增后 +WeatherColor 的 designated aggregate initializer 未同步。已仅在该 color-only pass +显式补入安全中性值 `{Direction flag=0, scale=1, epsilon=1e-5, reserved=0}`;WeatherColor +不执行 advection,色场取样和颜色路径不变。 + +搜索确认 C++ 中仅有两个 `ParticleGpuValidationUBO` 聚合初始化器:Particle renderer +已初始化 `advection`,WeatherColor 为本次唯一遗漏。此项修复后的 shader generator +不需要重跑(未修改 shader/generator 输入)。修复后的 WeatherColor 精确对象编译与 +`git diff --check` 均已 PASS;编译日志按输出节流保存。随后外部终端完成完整 +`mbgl-test` / `mbgl-test-runner` 重编译与重链并 PASS。 + +## 独立既有测试问题与下一步 + +全量目标构建 PASS 后,额外扩大过滤范围时发现当前 `HEAD` 已存在的 `Layer.WeatherColorUsesIndependentFixedOpacity` 断言错误:测试先成功调用 `setColorStops({{-2,...},{3,...}})` 改变状态,最后却仍断言等于更早的 `{0,10,20}`。`git show HEAD:test/style/style_layer.test.cpp` 已确认该错误在 STEP-068 开工前即存在,且本 Step 对该测试的 diff 只有新增 RasterParticle 测试。为保持任务隔离,本 Step 不修改该既有 WeatherColor 测试,也不把它作为 DIRECTION 关闭门槛。 + +STEP-068 相关实现、精确对象、完整 Linux 目标重链、8 个相关过滤测试与 `git diff --check` 均已通过,因此本 Step 正式 `CLOSED / PASS`。Android Gradle/AAR/APK/ADB 与 Jest 仍按任务要求未执行。下一步进入 V3-05:Android/JNI/RN generic v2 multi-plane ingress。 + + +## Git 收尾 + +- 实现提交:`f3c33c67275b24f61b80924abbb9d10d68b2cb7c` +- commit message:`feat: add direction particle advection` +- push:成功 +- 远端分支:`origin/particle/main` +- 实现提交 push 后:`HEAD == origin/particle/main == f3c33c67275b24f61b80924abbb9d10d68b2cb7c` +- STEP-062 两处用户修改及历史 untracked/build/cache/tgz 均未暂存、未提交。 diff --git a/docs/50_Step记录/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_v1.md b/docs/50_Step记录/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_v1.md new file mode 100644 index 000000000..954c8b629 --- /dev/null +++ b/docs/50_Step记录/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_v1.md @@ -0,0 +1,23 @@ +# STEP-069 MapLibreParticleLab V3-05 Android/JNI/RN v2 多 Plane 入口 + +状态:完成(IMPLEMENTED / WAITING_ANDROID_VALIDATION) + +## 已完成 + +- [x] RN 包升为 `13.4.1-particle.3`,导出 `FieldPayloadRefs`、`FieldValueSource`;v2 主入口只接受 refs。 +- [x] 保留 v1 `submitWindField` ABI,并提供明确的 `submitLegacyVectorField` 公共兼容入口。 +- [x] Native TurboModule Spec 改为 codegen 支持的三个明确 string plane 参数;公共 `FieldRuntime.submitField(..., FieldPayloadRefs)` 保持不变,空 scalar/vector string 仅为 Native ABI 的 optional marker。Kotlin 在读取前校验 validity、local file、非空和三 plane aggregate 64 MiB;Java/JNI 用 presence flags 和长度恢复 optional bundle。 +- [x] Java `WeatherRuntime.submitField` 给 nullable scalar/vector 标注 `@Nullable`,并独立拒绝 total 超过 64 MiB,避免绕过 Kotlin 后分配 oversized packed array。 +- [x] `RasterArraySource` v1/v2 decode 后共用同一 duplicate、monotonic、compatibility、initial-window 与 Timeline 路径。 +- [x] 桥接 `SCALAR|VECTOR_MAGNITUDE` 与 positive finite `advectionScale`,只更新既有 renderer config。 +- [x] Core source 测试覆盖三种合法 v2 bundle、缺 validity/no channel 与 semantics incompatibility,以及 v1 legacy 与 compatible v2 vector-only 进入同一 `RasterArraySource` Timeline。 + +## 范围与验证 + +未改 WeatherV2、NavSea、Timeline/T0/T1/MIX/rollover/lifecycle,也未改 STEP-062 dirty 或无关 untracked 内容。以下均经 build-output-throttle:外部复核指定的 `combine-js-to-schema-cli.js /tmp/step069-schema.json packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts` 实际 PASS;`./node_modules/.bin/tsc --noEmit -p packages/maplibre-particle-rn/tsconfig.json` PASS;`source.test.cpp.o` 精确编译 PASS;`ninja -C build-linux-opengl -j3 mbgl-test` 重链 PASS;`Source.HostFieldV2BundleUsesTheSameTimelineIngress:Source.HostFieldLegacyAndCompatibleV2VectorOnlyShareOneTimeline` 2/2 PASS。`git diff --check` PASS。Android Gradle/AAR/APK/ADB、Jest 均未执行;Android compile 留给 V3-08,不能标记 Android PASS。 + +## Git 收尾 + +- implementation commit:`148bf80fe41ba566de6c1d4b439cfe372a8a453c`(`feat: add v2 multi-plane field ingress`)。 +- 本 Step 仅显式暂存实现文件;STEP-062 两处用户 dirty、历史 untracked、`.codex-build-logs/`、旧 tgz/build/cache 均未纳入。 +- 文档 handoff commit 与 push/Final HEAD 在完成报告最终提交后回填。 diff --git a/docs/50_Step记录/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_v1.md b/docs/50_Step记录/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_v1.md new file mode 100644 index 000000000..21e92c6a1 --- /dev/null +++ b/docs/50_Step记录/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_v1.md @@ -0,0 +1,38 @@ +# STEP-070 MapLibreParticleLab V3-06 四类场 Fixture 与 RN 通用回归 + +状态:IMPLEMENTED / WAITING_ANDROID_VALIDATION + +## 目标与边界 + +在 `particle/main` `3b012759` 上增加离线 V3 fixture、转换/校验工具、RN Test Host `.3` 公共 API 接线和最小 Core contract test。不修改 Timeline/Playback/T0/T1/MIX/rollover、schema、shader、WeatherV2、NavSea 或 STEP-062 两份用户 dirty 文档;不运行 Android Gradle/AAR/APK/ADB/Jest。 + +## 完成内容 + +- [x] `tools/particle_v3_fixture` 生成并校验 v2 metadata、shared validity、可选 scalar/vector、长度与 SHA-256。validator 对每一个 validity=1 的 cell 检查所有存在 plane:finite 且不等于 noData。 +- [x] `wind-real` 从本地 canonical v1 `field.f32` 重新封装;shared validity 严格派生为 `finite(U) && finite(V) && U != -9999 && V != -9999`。 +- [x] `wave-real` 由 WeatherV2 `ecmwf-wam/waves/2026090912/wave_bin` 的正式 `2026090918/2026090919` BIN 和 header sidecar 解码:scalar=`height_m`,vector=规范化 `unit_to_u/unit_to_v`,语义 `DIRECTION`;`period_s` 和原始 degree 不进入 canonical。 +- [x] `tide-real` 由 WeatherV2 `cmems/tide/2026090900/current_bin` 的正式 `2026090901/2026090902` BIN 和 header sidecar 解码为 vector-only `VELOCITY`。 +- [x] Wave/Tide 每帧 summary 记录正式源 BIN 与 sidecar SHA-256、target_time、referenceTime、bounds、grid、projection、included/excluded channels 和 validity 派生规则;两者 generation 为小且严格递增的 `1/2`。 +- [x] Rain 保持 `rain-decoder-fixture`,manifest/provenance 明确为非真实 field values,因为本 Step 未实现 PMTiles reader;fallback generation 为严格递增的 `1/2`。 +- [x] Test Host 的 `fieldDatasets` 已指向 `wave-real` 和 `tide-real`,继续仅经 package `FieldRuntime` 公共 API 提交 local file refs。 +- [x] 保留并验证 Wave synthetic scalar/vector independence;新增 C++ provider-neutral 四类 shape contract test。 + +## 验证 + +- [x] `python3 tools/particle_v3_fixture/generate_fixtures.py --write-reference-fixtures --validate`:PASS。 +- [x] package `.3` `tsc --noEmit`:PASS。 +- [x] RN Test Host `npm run typecheck`:PASS(不含 Jest)。 +- [x] RN TurboModule `combine-js-to-schema-cli.js`:PASS。 +- [x] 使用 `CCACHE_DIR=/tmp/step070-ccache`,重新编译 `source.test.cpp.o`:PASS。 +- [x] `mbgl-test` / `mbgl-test-runner` 完整 Linux OpenGL 重编译/重链:459/459 完成;`Source.ParticleFieldV3*` 过滤测试 6/6 PASS;验证链退出码 0。 +- [x] `git diff --check`:PASS(提交前复核)。 +- [ ] Android Gradle/AAR/APK/ADB/Jest:按任务范围未运行。 + +## 当前结果、阻塞与下一步 + +fixture 数据契约、TypeScript/codegen、Linux 完整重链与 V3 Source 过滤测试均已完成。Android/JNI 真实编译和设备视觉验收仍由后续统一周期完成。Rain 要升级为真实数值场,需要另行实现只读 PMTiles reader;不得把现有 fallback 改称 real。 + +## Git 收尾 + +- implementation:`8588e6fcb4f3f5d874159c2e42619c5227cb7921`(`feat: add V3 field fixtures and RN regression host`),已 push 至 `origin/particle/main`。 +- docs-only Git handoff 在 `git add` 创建 `.git/index.lock` 时因只读文件系统被阻断,未提交、未推送;STEP-062 两份用户 dirty 文档与历史 untracked/build/cache/tgz 均未暂存。 diff --git a/docs/50_Step记录/STEP-071_MapLibreParticleLab_V3-07_Linux综合冻结_v1.md b/docs/50_Step记录/STEP-071_MapLibreParticleLab_V3-07_Linux综合冻结_v1.md new file mode 100644 index 000000000..5a86c2df3 --- /dev/null +++ b/docs/50_Step记录/STEP-071_MapLibreParticleLab_V3-07_Linux综合冻结_v1.md @@ -0,0 +1,37 @@ +# STEP-071 — MapLibreParticleLab V3-07 Linux 综合冻结 + +状态:CLOSED / PASS(Linux) +日期:2026-09-11 +基线:`fcc533dc9518395d0c25794131f53f0fae20d51c` + +## 目标 +在进入 V3-08 Android 统一验收前,对 V3-02~V3-06 的 canonical field、Host ingress、Timeline、RasterParticle 与 RN 公共桥接做一次 Linux 综合冻结。 + +## 冻结链结果 +- [x] canonical fixture validator:PASS。 +- [x] `packages/maplibre-particle-rn` TypeScript:PASS。 +- [x] RN Test Host `npm run typecheck`:PASS。 +- [x] RN TurboModule codegen:PASS。 +- [x] `mbgl-test` / `mbgl-test-runner` Linux OpenGL target:PASS / up-to-date。 +- [x] V3 综合过滤测试:21/21 PASS。 +- [x] `git diff --check`:PASS。 +- [ ] Android Gradle/AAR/APK/ADB/Jest:本 Step 不运行。 + +## 综合测试覆盖 +过滤范围:`Source.ParticleField*`、`Source.HostField*`、`Source.RasterArrayTimeline*`、`Source.RasterArraySharesCanonicalSnapshotsWithWeatherColorLayer`、`Layer.RasterParticle*`。 +最终 18 个 Source + 3 个 Layer,共 21 项全部 PASS。 + +## 冻结中发现并修正的测试问题 +1. `HostFieldSubmissionRejectsInvalidWithoutChangingLastValidField` 旧测试用同一 `uniform-east` payload 构造 T0/T1,正确 Runtime 会先返回 Duplicate。测试改为 east→north 两份兼容但不同内容。 +2. `RasterArrayTimelineCandidatesReachReadyThroughFieldAdapter` 使用的 east/north/vortex 旧 fixture 均为 `timeIndex=0`;当前 Adapter 保留 `validTime=timeIndex+1`,导致 Timeline 正确 fail-closed。测试仅在局部 metadata 副本中改为 0/1/2,未修改共享 fixture。 +3. 两处均为 test fixture 陈旧,不修改 Runtime、Duplicate 判定顺序、Timeline fail-closed 或生产 API。 + +对应 Codex 任务: +- `docs/40_任务/STEP-071A_V3-07_Linux综合冻结_测试Fixture修正_Codex任务_v1.md` +- `docs/40_任务/STEP-071A_V3-07_Linux综合冻结_测试Fixture修正_Codex任务_v2.md` + +## 边界 +- 未修改 `src/`、`include/`、shader、JNI、Android、RN runtime、WeatherV2 或 NavSea。 +- 已知既有 `Layer.WeatherColorUsesIndependentFixedOpacity` 断言债不属于本 Step,不修、不作为 V3-07 gate。 +- STEP-062 用户 dirty 与历史 untracked/build/cache/tgz 均保持隔离。 +- 下一步:V3-08,一次 Android 编译/安装/实体机综合验收。 diff --git a/docs/50_Step记录/STEP-072_V3-08_Android_Native_v2与单次综合验收准备_v1.md b/docs/50_Step记录/STEP-072_V3-08_Android_Native_v2与单次综合验收准备_v1.md new file mode 100644 index 000000000..1de803265 --- /dev/null +++ b/docs/50_Step记录/STEP-072_V3-08_Android_Native_v2与单次综合验收准备_v1.md @@ -0,0 +1,57 @@ +# STEP-072 — V3-08 Android Native v2 与单次综合验收准备 + +状态:IMPLEMENTED / WAITING_ANDROID_VALIDATION +日期:2026-09-11 +基线:`dd356a277db05ce0075bee43f431ddeb2aba19cc` + +## 目标 +在不执行 Android 编译/安装的前提下,完成 V3-08 前的 Native/RN 版本坐标准备、Test Host 单 Bridge 收口与单次实体机验收流程冻结。 + +## 已完成 +- [x] Native Android `VERSION`:`13.4.1-particle.1 → 13.4.1-particle.2`。 +- [x] RN package `.3` 的 Android dependency 与 Expo plugin 统一指向 Native `.2`。 +- [x] RN Test Host plugin 统一指向 Native `.2`。 +- [x] release manifest 升为 RN `.3` + Native `.2`,Android 状态保持 `not_run`,并保留 `.2` 已发布历史追溯。 +- [x] 删除 Test Host legacy local `@maplibre-particle/weather-runtime-bridge` tracked 源码/配置。 +- [x] 删除 Test Host 根 `react-native.config.js`,`tsconfig.json` 只保留 `App.tsx` 与 `src`。 +- [x] Expo autolinking 复核:只剩正式 `@navsea/maplibre-particle-rn` 提供 `WeatherRuntimeBridge`。 +- [x] 建立 `docs/60_测试/STEP-072_V3-08_Android单次综合验收_v1.md`。 +## 静态验证 +- [x] RN Test Host `npm run typecheck`:PASS。 +- [x] package TypeScript:PASS。 +- [x] RN TurboModule codegen:仅生成 `NativeWeatherRuntimeBridge`,PASS。 +- [x] canonical fixture validator:PASS。 +- [x] Expo autolinking:3 dependencies;包含 MLRN、正式 particle package、Expo;不再包含 legacy bridge。 +- [x] 两个 Expo plugin `node --check`:PASS。 +- [x] release manifest JSON:PASS。 +- [x] `git diff --check`:PASS。 + +## 关键发现与修正 +准备阶段发现 Test Host 会同时 autolink legacy local bridge 与正式 package bridge;二者 Native module name 都是 `WeatherRuntimeBridge`、codegen library 都是 `WeatherRuntimeBridgeSpec`。STEP-072A 删除了旧本地 bridge 注册与源码,使 V3-08 clean prebuild 前只剩正式 package bridge。 + +验收文档同时修正了 AAR `javap` 方法、Makefile `MLN_ANDROID_GRADLE` 的命令行覆盖方式、Linux→Win11 Maven Local handoff,以及 Win11 `gradlew.bat`/Metro/固定 ADB 设备流程。 + +## 未执行 +- Android Gradle / AAR build / Maven publish / APK build / ADB / 实体机验收:未执行。 +- Jest:未执行。 +- 因此本 Step 不声明 Android PASS,也不创建 `.3` release tag。 + +## 下一步 +用户按 `docs/60_测试/STEP-072_V3-08_Android单次综合验收_v1.md` 只执行一次 Android build/install 周期,覆盖 Wind、Rain、Wave、Tide 与 synthetic Wave 五组场数据。 +## Git 实现基线 +- implementation/preparation commit:`11cf58d344e64e6aff77deceb2c34c8eda045f1b`(`chore: prepare V3-08 android validation`)。 +- 已成功 push 到 `origin/particle/main`;push 后实现基线 HEAD 与远端一致。 + +## 2026-09-11 V3-08 最终关闭 + +状态:**CLOSED / PASS**。 + +Win11/CARBON7i 使用 Gradle 8.14.3、AGP 8.12.0、Kotlin 2.1.20、JDK 17.0.18、NDK 27.1.12297006、compileSdk/targetSdk 36、minSdk 24 完成正式 npm 消费链与 Debug APK 构建。dependencyInsight 最终解析到 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2`。 + +实体机 `192.168.1.146` 上五组数据均满足 T0=`ACCEPTED_PENDING_INITIAL_WINDOW`、T1=`INITIAL_WINDOW_READY`、最终 Runtime=`READY`:Wind real、Rain scalar-only、Wave real、Tide/Current、Wave synthetic independence。 + +控制面 PLAY/PAUSE、HALF/NORMAL/DOUBLE、FPS AUTO/30/60、seek T0/T1、Weather/Particle 独立显隐、Field stops、Particle color、Trail color、Apply visual 均返回 `ACCEPTED`;最终 FATAL/JNI/UnsatisfiedLinkError 扫描为空。 + +测试期间发现 fixture 位于 Metro project root 外,临时 Win11 `D:\V308` 验收 worktree 通过 `watchFolders` 纳入 `integration/react-native/particle-v3-fields` 后 bundle 成功。该临时 Metro 修改不进入正式仓库,不属于 Native/RN Runtime 缺陷。 + +最终产物:Native `.2` AAR SHA-256 `f890f834692d3cacb8a281b2d0f82cf4a7f8ba7756132092be800505b961e9a2`;RN `.3` tgz SHA-256 `16e5dab86aac9832f7a48d8c35dd9821f80f52e7e53ff4e0e6c98724781eda3a`;APK SHA-256 `49824062CAB72771282294114B5A818D2067F9B786B8697D2D2A983BF0B743E2`。 diff --git a/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v1.md b/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v1.md new file mode 100644 index 000000000..dc5de6ec3 --- /dev/null +++ b/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v1.md @@ -0,0 +1,39 @@ +# STEP-073 — QBIN2 V4 Core 与通用 Field Profile Validator + +状态:完成 + +日期:2026-09-13 + +## 目标 + +建立不接入 Decoder、Renderer、Android/JNI/RN 或 V3 生产路径的 QBIN2 V4.4 provider-neutral Core primitive,并以数据驱动 Contract Schema v1 验证 `DecodedFieldFrame`。 + +## 完成项 + +- [x] 建立 `ResolvedFieldProfileContract`、`DecodedFieldFrame`、`FieldSeries`、`EncodedFieldStore` 与独立 `ForecastClock`。 +- [x] 以独立 Validity descriptor 加共享只读 payload 表达单一 Frame validity domain;descriptor 与 payload 使用同一 `shared_ptr`,不复制位图。 +- [x] 实现纯数据驱动 `FieldProfileValidator`:schema、REQUIRED 双向一致、额外 plane、shared-validity 能力、时间关系及 Core 单位/速度组规则均返回结构化结果。 +- [x] 保持普通 Scalar 的 semanticID/unitID opaque;无 profileID 产品分支。 +- [x] 保持同 validTime 的 Duplicate、identity conflict、revision replacement 与相邻插值兼容性语义;fieldKey 独立 cadence/window。 +- [x] 增加 C++ 自动化测试,并通过 Linux OpenGL 构建与 V3 Field/Timeline 定向回归。 + +## 允许修改 + +`include/mbgl/particle/field/`、`src/mbgl/particle/field/`、根/测试 CMake、`test/particle/`、本 Step、开发记录与完成报告。 + +## 禁止修改确认 + +未修改 Renderer、shader、`platform/android/`、RN/integration、Style JSON/source schema;未新增 QBIN decoder、JNI、Java/Kotlin 或 React API;旧 V3 Timeline/`submitField(...)` 路径未切换。 + +## 验证 + +- `cmake --build build-linux-opengl --target mbgl-test -j3`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='ParticleFieldRuntime.*:ParticleFieldProfileValidator.*'`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*'`:PASS。 +- `git diff --check`:PASS。 + +以上构建/测试均通过输出节流脚本执行。未执行 Android Gradle、APK、ADB 或真机操作。 + +## 阻塞与下一步 + +无本 STEP 阻塞。下一步必须等待独立审计后再决定是否进入 QBIN2 Encoded Ingress / Decoder;本 STEP 未自行开展该工作。 diff --git a/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v2.md b/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v2.md new file mode 100644 index 000000000..b481d9e19 --- /dev/null +++ b/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v2.md @@ -0,0 +1,40 @@ +# STEP-073 — QBIN2 V4 Core 与通用 Field Profile Validator 审计修正 v2 + +状态:完成 + +日期:2026-09-13 + +## 目标 + +仅修正 STEP-073 v1 审计确认的 QBIN2 V4 Core/通用 Validator 三项问题:DEGREE Unit ID、Plane `(semanticID, groupID)` 唯一性与 ForecastClock HALF tick 舍入漂移。 + +## 完成项 + +- [x] `FieldUnitDegrees` 改为冻结 QBIN V2 registry 的 numeric ID `4`;`NONE=0`、`MPS=1` 未变。Direction `unitID=4` 可接受,`unitID=2` 返回 `InvalidCoreUnit`。 +- [x] 实际 Frame 与 Contract `requiredPlanes[]` 均以 `(semanticID, groupID)` 判重;component 改为 descriptor 属性。semantic/group 已定位但 component 不同返回 `PlaneDescriptorMismatch`。 +- [x] ForecastClock 以整数 half-millisecond remainder 累积,HALF 不再逐 tick 向上取整;seek 清除 remainder,溢出 fail-closed。 +- [x] U/V 继续使用各自 semanticID,并按既有通用 U_EAST/V_NORTH velocity group 规则通过。 +- [x] 新增对应 C++ 自动化覆盖并完成 Linux 定向验证与 V3 回归。 + +## 允许修改 + +仅修改 `include/mbgl/particle/field/`、`src/mbgl/particle/field/`、`test/particle/field_runtime.test.cpp`、本 Step、开发记录及完成报告。 + +## 禁止修改确认 + +未修改 Renderer、shader、`platform/android/`、RN/package/integration、现有 V3 ingress/timeline 生产路径;未实现 QBIN2 Decoder、Binding/Projection、JNI 或 Android/RN 接入。 + +## 验证 + +所有构建/测试均经 `build-output-throttle` 的 `run_limited.py` 执行: + +- `cmake --build build-linux-opengl --target mbgl-test -j3`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='ParticleFieldRuntime.*:ParticleFieldProfileValidator.*'`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*'`:PASS。 +- `git diff --check`:PASS。 + +未执行 Android Gradle、APK、ADB、真机操作或 Jest;未执行 iOS/Metal 验证。 + +## 阻塞与下一步 + +本审计修正无阻塞。后续工作须另行批准;本 STEP 在此停止,不进入 QBIN2 Decoder 或任一接入阶段。 diff --git a/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v3.md b/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v3.md new file mode 100644 index 000000000..fe7af8f64 --- /dev/null +++ b/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v3.md @@ -0,0 +1,40 @@ +# STEP-073 — QBIN2 V4 Core 与通用 Field Profile Validator 审计修正 v3 + +状态:完成 + +日期:2026-09-13 + +## 目标 + +仅关闭独立审计发现的 Generic Field Profile Validator 协议遗漏:Native 已知 Core semantic 不得被 Contract 与 Frame 联合伪装,且 Direction Circular Quantization 必须有完整的 provider-neutral 结构约束。 + +## 完成项 + +- [x] 在 Validator 内建立静态、数据表驱动的 Native Core semantic contract:`1=VELOCITY_COMPONENT/MPS/U_EAST`、`2=VELOCITY_COMPONENT/MPS/V_NORTH`、`3=VALIDITY/NONE/NONE/group0`、`10/11=DIRECTION_ANGLE/DEGREE/NONE/group0`。表不依赖 profile/product 名称。 +- [x] Contract 与实际 Frame 都在原有 descriptor 比较前核对该表;冲突返回结构化 `SemanticContractMismatch`。 +- [x] Unknown `SCALAR` semanticID 和 unitID 仍为 opaque,继续经过 Contract/Frame descriptor 一致性检查而不加入 Native unit 白名单。 +- [x] 所有 `DIRECTION_ANGLE` 强制 `DEGREE(4)`、`NONE`、`groupID=0`、`UINT16`、`offset=0`、`scale=360/65536`(允许极小 float32 表示误差);违反返回 `InvalidCoreUnit` 或已知 semantic 的 `SemanticContractMismatch`。 +- [x] 覆盖已知 semantic 伪装、正确 Direction、Direction component/group/dtype/scale/offset/unit 拒绝、opaque Scalar、既有 plane identity、DEGREE registry 与 ForecastClock HALF 行为。 + +## 允许修改 + +仅修改 `include/mbgl/particle/field/field_profile_validator.hpp`、`src/mbgl/particle/field/field_profile_validator.cpp`、`test/particle/field_runtime.test.cpp`、本 Step、完成报告与开发记录。 + +## 禁止修改确认 + +未修改 QBIN2 Decoder、Binding/Projection、Renderer、shader、JNI、`platform/android/**`、`packages/maplibre-particle-rn/**`、`integration/react-native/**` 或现有 V3 生产入口;未实现 parser/header/CRC/packing。 + +## 验证 + +所有 noisy 命令均经 `build-output-throttle` 的 `run_limited.py` 节流执行: + +- [x] `cmake --build build-linux-opengl --target mbgl-test -j3`:PASS。 +- [x] `build-linux-opengl/mbgl-test-runner --gtest_filter='ParticleFieldRuntime.*:ParticleFieldProfileValidator.*'`:PASS。 +- [x] `build-linux-opengl/mbgl-test-runner --gtest_filter='Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*'`:PASS。 +- [x] `git diff --check`:PASS。 + +未执行 Android Gradle、APK、ADB、真机、Jest 或 iOS/Metal。 + +## 当前结果、阻塞与下一步 + +本轮所有范围内项完成,无阻塞。本 STEP 在此停止,等待 ChatGPT 独立审计;不进入 STEP-074 或任何 QBIN2 ingress/Decoder 工作。 diff --git a/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v4.md b/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v4.md new file mode 100644 index 000000000..45800018e --- /dev/null +++ b/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v4.md @@ -0,0 +1,45 @@ +# STEP-073 — QBIN2 V4 Core 与通用 Field Profile Validator 最终关闭修正 v4 + +状态:完成 + +日期:2026-09-13 + +## 基线 + +- 开始 HEAD:`0b66a5cd30e91423cbc864a327a53bed06cae91c`。 +- 开始工作区含 STEP-062 文档修改及多项历史 untracked 文件;本 Step 未修改、暂存或提交它们。 + +## 目标 + +关闭独立审计在实际重链 `mbgl-test-runner` 后发现的三项问题:Frame 的唯一共享 validity domain、实际已知 Core semantic 的错误优先级,以及 ForecastClock 测试的 pause/resume 语义。 + +## 完成项 + +- [x] `DecodedFieldFrame` 允许空 numeric `planes[]`,完整 designated validity 仍结构合法;所缺 required numeric plane 由 Validator 返回 `MissingRequiredPlane`。 +- [x] designated validity 强制 `Validity/Bitset/NONE/group0/None/REQUIRED`、cell count 与 bit payload 长度一致,并与 `validityBits` 使用同一 `shared_ptr`;任一违反均为 `InvalidFrame`。 +- [x] 禁止 `planes[]` 出现第二张 Validity plane,确保一帧只有唯一共享 validity domain。 +- [x] 保留静态数据表驱动的 Core semantic contract,并在 required-plane descriptor 匹配前检查实际 plane;semantic 1 Scalar 伪装、semantic 10 component/group 冲突均返回 `SemanticContractMismatch`。 +- [x] unknown Scalar 及 opaque unit 的可扩展行为保持;Direction circular quantization v3 规则未变。 +- [x] ForecastClock 测试在 `pause()` 后显式 `play()`,未改变生产 `pause()` 语义。 + +## 允许修改 + +仅修改 Field Runtime Core、通用 Validator、对应 C++ 测试和本 Step 文档/报告/开发记录。 + +## 禁止修改确认 + +未修改 QBIN2 Decoder、Binding/Projection、Renderer、shader、JNI、`platform/android/**`、`packages/maplibre-particle-rn/**`、`integration/react-native/**`、Style JSON/source schema 或 V3 `submitField(...)` 路径;未执行 Android Gradle、APK、ADB、真机、Jest 或 iOS/Metal。 + +## 验证 + +构建/测试完整输出均落入 `/tmp` 限流日志(仓库未提供 build-output-throttle 指定的 helper script,仅在失败时读取有限末尾窗口)。 + +- [x] `cmake --build build-linux-opengl --target mbgl-test-runner -j3`:实际重编译并重链 PASS。 +- [x] `mbgl-test-runner --gtest_list_tests`:确认包含 `ParticleFieldProfileValidator.` 与 `ParticleFieldRuntime.`。 +- [x] `mbgl-test-runner --gtest_filter='ParticleFieldRuntime.*:ParticleFieldProfileValidator.*'`:实际运行 11 tests,11 PASS,0 failed;绝非 0 tests。 +- [x] `mbgl-test-runner --gtest_filter='Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*'`:实际运行 11 tests,11 PASS,0 failed。 +- [x] `git diff --check`:PASS。 + +## 当前结果、阻塞与下一步 + +STEP-073 v4 的关闭条件已满足,无范围内阻塞。停止于此,等待 ChatGPT 独立复验;不进入 STEP-074。 diff --git a/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v1.md b/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v1.md new file mode 100644 index 000000000..793339cdd --- /dev/null +++ b/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v1.md @@ -0,0 +1,9 @@ +# STEP-074 — QBIN2 V4 Encoded Ingress 与 Decoder v1 + +状态:完成(Core 范围)。开始 HEAD:`b8a629b7b65067f7f561db473312484bd8f0451d`。开始工作区已有 STEP-062 文档修改及历史 untracked 文件,未清理、暂存或提交。 + +新增独立 `mbgl::particle::ingress::QbinV2Decoder` 和无状态 `EncodedFieldIngress`。其顺序为 QBIN structural decode、构造 `DecodedFieldFrame`、再调用既有 generic `FieldProfileValidator`;未接入 Store、Renderer、Binding、Host 或平台 bridge。 + +Decoder fail-closed 校验 QBV2 LE header/descriptor、保留字段、FRAME/WebMercator/node 限制、时间/边界/容量、精确 canonical packing 与零 padding、ISO-HDLC CRC(CRC 字段虚拟清零)、single shared validity、invalid-cell raw-zero,以及 per-plane local content identity。numeric payload 保持原始 bytes。 + +真实 CMake runner 已加入 `ParticleQbinV2Decoder`、`ParticleEncodedFieldIngress`;fixture 仅在 test 范围,涵盖 2×2 与 513×769 构造。 diff --git a/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v2.md b/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v2.md new file mode 100644 index 000000000..dcbe84bd2 --- /dev/null +++ b/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v2.md @@ -0,0 +1,36 @@ +# STEP-074 — QBIN2 V4 Encoded Ingress 与 Decoder v2 + +状态:完成 + +日期:2026-09-13 + +## 基线 + +- 开始 HEAD:`83a84452f23d019cbb143630eac04631552fef89`。 +- 开始前已有 STEP-062 文档修改及历史 untracked 文件;未清理、覆盖、暂存或提交。 + +## 完成项 + +- [x] per-plane `contentIdentity` 改为 raw bytes 加 descriptor 的明确 Little Endian canonical byte stream;不含 `byteOffset`。 +- [x] 64 MiB 为整份 QBIN 上限,32 MiB 为各 Plane raw `byteLength` 总和上限;canonical padding 不计入 decoded data。 +- [x] BITSET 与 `VALIDITY` 双向强制;构造后以 `DecodedFieldFrame::isStructurallyValid()` 作最终 fail-closed 自检。 +- [x] test-only fixture writer 对整数、float32、float64 明确写 Little Endian。 +- [x] decoder/ingress runner 扩为实际 18 tests,包含 header、descriptor、limits、packing、padding、CRC、validity、invalid-cell、identity、513×769 与 Contract 矩阵。 +- [x] unknown Scalar opaque unit、unknown field semantic 与 `validTimeUtc < referenceTimeUtc` 的 decoder 容器行为均通过。 + +## 验证 + +- `cmake --build build-linux-opengl --target mbgl-test-runner -j3`:PASS(实际重编译/重链)。 +- `mbgl-test-runner --gtest_list_tests`:PASS,确认 suites 已注册。 +- `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`:18/18 PASS。 +- `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`:STEP-073 11/11 PASS。 +- `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*`:既有 V3 11/11 PASS。 +- `git diff --check`:PASS。 + +## 范围确认 + +只修改 QBIN decoder/header 与 C++ test,未进入 Store submission、Binding/Projection、Renderer/shader、JNI、Android、RN、Test Host、URL/HTTP/download/gzip、iOS/Metal 或 Jest。 + +## 阻塞与下一步 + +无范围内阻塞。本 Step 到此关闭;提交并 push 后停止,不进入下一 Step。 diff --git a/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v3.md b/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v3.md new file mode 100644 index 000000000..b78530093 --- /dev/null +++ b/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v3.md @@ -0,0 +1,49 @@ +# STEP-074 — QBIN2 V4 Encoded Ingress 与 Decoder v3 + +状态:完成 + +日期:2026-09-13 + +## 目标 + +在不扩展 STEP-074 生产范围的前提下,收口 Decoder 入口错误、Little Endian fail-closed 与 32 MiB raw decoded-data 上限的最终测试覆盖。 + +## 基线 + +- 开始 HEAD:`56b8dab90f914438e6fcaec6278146c96c8df638`。 +- 开始前已有 STEP-062 文档修改及历史 untracked 文件;均未清理、覆盖、暂存或提交。 + +## 必做项 + +- [x] 空 span、1 byte、111 bytes 都经 production `QbinV2Decoder::decode` 返回 `InputTooSmall`。 +- [x] 112-byte 非 `QBV2` 输入返回 `BadMagic`;空 `fieldID`、零 `generation` 返回 `InvalidRuntimeMetadata`。 +- [x] 明确验证 `dataVersion=0` 的合法 frame 仍 Accepted。 +- [x] Header width 写成 big-endian `00 00 00 02` 并重算 CRC 后以 `LimitExceeded` 结构化拒绝。 +- [x] Descriptor count 写成 mixed-endian `00 00 00 04` 并重算 CRC 后以 `InvalidDescriptor` 结构化拒绝。 +- [x] raw sum 为 31,712,633 bytes、canonical payload 为 31,712,648 bytes 的 1447×1449 FRAME Accepted;15 bytes padding 未计入 32 MiB decoded limit。 + +## 允许修改 + +- `test/particle/qbin_v2_decoder.test.cpp`。 +- 本 Step 记录、完成报告与开发记录。 + +## 禁止修改 + +- Host、网络、JNI、Android/RN、Binding/Projection、Renderer、Store submission 与 production Decoder。 + +## 构建与测试 + +- `cmake --build build-linux-opengl --target mbgl-test-runner -j3`:PASS(经输出限流器,实际重编译/重链)。 +- `mbgl-test-runner --gtest_list_tests`:PASS,Decoder/Ingress suite 已注册。 +- `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`:24/24 PASS。 +- `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`:STEP-073 11/11 PASS。 +- `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*`:既有 V3 11/11 PASS。 +- `git diff --check`:PASS。 + +## 当前结果 + +仅补 6 个 production decode 覆盖测试,未发现生产 Decoder 缺陷,故未修改生产实现。STEP-074 由 v2 的 18 tests 扩至实际 24 tests,全部通过。 + +## 阻塞与下一步 + +无范围内阻塞。按任务要求完成 scoped commit 与 `origin/particle/main` push 尝试后停止,不进入 STEP-075。 diff --git a/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v4.md b/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v4.md new file mode 100644 index 000000000..e67cfd452 --- /dev/null +++ b/docs/50_Step记录/STEP-074_QBIN2_V4_EncodedIngress与Decoder_v4.md @@ -0,0 +1,46 @@ +# STEP-074 — QBIN2 V4 Encoded Ingress 与 Decoder v4 + +状态:完成 + +日期:2026-09-13 + +## 目标 + +仅修正 v3 `AcceptsRawLimitWithCanonicalPaddingExcluded` 的边界 fixture,使它严格证明 decoded raw 数据不超过 32 MiB、canonical packed payload 超过 32 MiB、整个 QBIN 文件不超过 64 MiB 时 production Decoder 仍接受。 + +## 基线 + +- 开始 HEAD:`f4a495ff38b77f586f97853493dda02ab067b26d`。 +- 开始前已有 STEP-062 文档修改及历史 untracked/build 文件;均未清理、覆盖、暂存或纳入本 Step。 + +## 必做项 + +- [x] `AcceptsRawLimitWithCanonicalPaddingExcluded` 改为 1101×1890(2,080,890 cells)、17 plane:1 个 single shared validity BITSET 与 16 个 UINT8 numeric plane;descriptor 依 semantic 3、4–19 canonical 排序。 +- [x] 测试直接计算并断言 `rawSum=33,554,352 <= 33,554,432`、`payloadLength=33,554,448 > 33,554,432`、`payloadLength > rawSum`、`totalQbinBytes=33,555,104 <= 67,108,864`。 +- [x] fixture 经 production `QbinV2Decoder::decode(...)` 返回 Accepted。 +- [x] cells、planes、canonical packing、single shared validity、descriptor 与 64 MiB 文件限制均在构造及断言内满足。 + +## 允许修改 + +- `test/particle/qbin_v2_decoder.test.cpp`。 +- 本 Step 记录、完成报告与粒子开发记录。 + +## 禁止修改 + +- Production Decoder、Host、网络、JNI、RN、Binding/Projection、Renderer、Store submission、Android/iOS 平台路径。 + +## 构建测试 + +- `cmake --build build-linux-opengl --target mbgl-test-runner -j3`:PASS(输出节流器;实际目标已重新检查并确认最新)。 +- `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`:实际 Running 24,24/24 PASS(XML:Decoder 22、Ingress 2)。 +- `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`:实际 Running 11,11/11 PASS。 +- `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*`:实际 Running 11,11/11 PASS。 +- `git diff --check`:PASS。 + +## 当前结果 + +旧 fixture 的 payload 仍低于 32 MiB,不能捕获把 packed payload 误当 decoded raw limit 的实现错误。新 fixture 以 16 个每个 6-byte canonical padding 的 numeric plane 将 payload 推至 limit 以上 16 bytes,同时 raw sum 比 limit 小 80 bytes;production Decoder 接受,未发现生产缺陷,故未修改 production 代码。 + +## 阻塞与下一步 + +无范围内阻塞。未执行 Android Gradle/APK/ADB/Jest;按任务要求停止,不进入下一 Step。 diff --git a/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v1.md b/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v1.md new file mode 100644 index 000000000..1c3036a3a --- /dev/null +++ b/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v1.md @@ -0,0 +1,36 @@ +# STEP-075 — QBIN2 V4 Binding 与 Projection Content Identity v1 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 开始 HEAD:`31783f25a8153f48000f19ac7cc2f2113cf47818`。 +- 仅实现 provider-neutral Core Binding/Projection;没有接入 Renderer、GPU、JNI、Android、RN、NavSea 或网络。 + +## 必做项 + +- [x] 新增独立 `ColorBinding`、`ParticleBinding`、`ColorProjectionContentIdentity`、`ParticleProjectionContentIdentity` 与 `BindingProjectionResolver`。 +- [x] Color 可选择 Scalar/Velocity/Direction numeric plane;unit 只作 equality assertion。 +- [x] Particle 支持同 fieldKey/window 的 U/V vector 与 DirectionAngle 两种模式;没有定义速度大小或 Renderer 行为。 +- [x] 每次 resolve 以 binding 的 fieldKey 独立调用 `EncodedFieldStore::resolve`,保留 Core-owned requested time 与每 field window。 +- [x] SampleState 仅保存 requestedForecastTimeMs/mix;content identity 不纳入 mix、播放状态或视觉参数,且保留只读 frame/storage shared_ptr。 +- [x] 结构化 fail-closed 返回 InvalidBinding、FieldSeriesMissing、WindowUnavailable、PlaneMissing、Class/Unit/Component/Group mismatch 与 InterpolationIncompatible。 +- [x] 测试 wind `00/01/02` 与 wave `00/03/06` 在 `01:30` 使用各自窗口;MIX `0/.25/.5/.99` 保持 identity/storage;Scalar-only revision 仅改变 Color identity;覆盖 Direction 成功及 unit/selector failure。 + +## 验证 + +- Linux OpenGL `mbgl-test-runner` 真实重建:PASS(输出节流)。 +- 规定过滤组 XML:50 tests、0 failures、0 errors:Binding 4,Runtime/Profile 11,QBIN Decoder/Ingress 24,V3 Source 11。 +- `git diff --check`:PASS。 + +## 边界与未执行 + +- FieldSeries 在提交阶段已拒绝不可插值相邻 frame;Resolver 仍保留 `InterpolationIncompatible` 结构化保护,正常 Store 无法形成该窗口。 +- 未执行 Android Gradle/APK/ADB/Jest、iOS/Metal 或任何 Host/Renderer 验证;它们不属于本 Step,不能视为 PASS。 +- 任务开始前既有 STEP-062 dirty、历史 untracked、build logs 与 RN 输出均未清理、暂存或提交。 + +## 下一步 + +STEP-075 关闭;按任务要求停止,不进入 STEP-076。 diff --git a/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v2.md b/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v2.md new file mode 100644 index 000000000..702170c0a --- /dev/null +++ b/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v2.md @@ -0,0 +1,46 @@ +# STEP-075 — QBIN2 V4 Binding 与 Projection Identity v2 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 基于 STEP-075 v1(`3616daec30079395847cc6a538fbd963641b7e12`)完成独立审计收口。 +- 仅修改 provider-neutral Core Binding/Projection、其离线单元测试与本 Step 文档;没有进入 STEP-076。 + +## 必做项 + +- [x] 新增 `ProjectionFrameCoordinate`,只以 `fieldID + validTimeMs` 表达 T0/T1 window coordinate;完整 `FrameContentIdentity` 仍保留在 resolved frame 中作 revision/runtime 诊断。 +- [x] Color 和 Particle identity 均统一比较 binding、T0/T1 coordinate、各自 selected plane content identity 及 T0/T1 shared validity content identity。 +- [x] 移除 Resolver 公共 `InterpolationIncompatible` 错误码;`FieldSeries::submit` 已拒绝不兼容相邻 frame,`resolve` 只返回已验证 window。 +- [x] 通过 Store 的 same-valid-time `ReplacedRevision` 测试 Scalar-only、U/V-only、validity-only revision:分别为 Color-only change、Particle-only change、两者均 change。 +- [x] 保持各 binding 独立 fieldKey/cadence、MIX 不进入 identity、in-window storage stable,以及 DirectionAngle success/failure coverage。 + +## 允许修改 + +- `include/mbgl/particle/field/field_binding.hpp` +- `src/mbgl/particle/field/field_binding.cpp` +- `test/particle/field_binding.test.cpp` +- 本 Step v2、完成报告 v2 与 `粒子开发记录.md` + +## 禁止修改 + +- 未修改 Decoder、FieldSeries、Profile Validator、Renderer、Host/JNI/RN、Android/iOS 或网络数据路径。 +- 未执行 Android Gradle/APK/ADB/Jest、iOS/Metal、Renderer/GPU 或真实 QBIN Golden 数据。 + +## 验证与当前结果 + +- Linux OpenGL `cmake --build build-linux-opengl --target mbgl-test-runner -j3` 真实重建 PASS(输出节流)。 +- 规定 GTest XML `/tmp/step075-gtest.xml`:50 tests、0 failures、0 errors。Binding 4;Runtime/Profile 11;QBIN Decoder/Ingress 24;V3 Source 11。 +- `git diff --check` PASS。 + +## Git + +- scoped commit:`90d05f596a42c157502f9a2a5f20425c999cebab`,`fix: close STEP-075 projection identity audit`。 +- 已成功 push:`3616daec3..90d05f596 particle/main -> particle/main`。 + +## 阻塞与下一步 + +- 无实现阻塞。Android/Jest/iOS/Metal 等禁止范围没有验证,不能视为 PASS。 +- STEP-075 已关闭;按任务书停止,不进入 STEP-076。 diff --git a/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v3.md b/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v3.md new file mode 100644 index 000000000..0658880d8 --- /dev/null +++ b/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v3.md @@ -0,0 +1,49 @@ +# STEP-075 — QBIN2 V4 Binding 与 Projection Identity v3 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 基于 `653e6f2f1598b7817b7912a39877690f248d6f25`,仅关闭 inactive selector 污染 `ParticleBinding` identity 的审计问题。 +- 不进入 STEP-076;测试仅使用离线 C++ fixture。 + +## 必做项 + +- [x] `ParticleBinding::operator==` 改为 mode-aware:VectorComponents 只比较 U/V selector;DirectionAngle 只比较 direction selector;两者均继续比较 mode、fieldKey 与 expectedUnitID。 +- [x] selector identity 继续仅为 `(semanticID, groupID)`;component 保持为 resolved Plane descriptor assertion,不进入 identity。 +- [x] VectorComponents 覆盖:仅变更 inactive `directionPlane` 时 binding equality 与 resolved `ParticleProjectionContentIdentity` 均相同;变更 active U selector 则 binding 不同且 `PlaneMissing` fail-closed。 +- [x] DirectionAngle 覆盖:仅变更 inactive U/V selectors 时 binding equality 与 resolved identity 均相同;变更 active direction selector 则 binding 不同且 `PlaneMissing` fail-closed。 + +## 允许修改 + +- `include/mbgl/particle/field/field_binding.hpp` +- `src/mbgl/particle/field/field_binding.cpp` +- `test/particle/field_binding.test.cpp` +- 本 Step、完成报告 v3 与 `粒子开发记录.md` + +## 禁止修改 + +- 未修改 Decoder、Runtime、Renderer、Host/JNI/RN/Android,未进入 STEP-076。 +- 未执行 Android Gradle/APK/ADB/Jest、iOS/Metal,未使用真实服务器 QBIN。 + +## 验证与当前结果 + +- Linux OpenGL:`cmake --build build-linux-opengl --target mbgl-test-runner -j3`,真实重建 PASS(全局 build-output-throttle)。 +- `ParticleFieldBinding.*`:6/6,0 failure。 +- `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`:11/11,0 failure。 +- `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`:24/24,0 failure。 +- `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*`:11/11,0 failure。 +- `git diff --check` PASS。 + +## Git + +- scoped commit:`2645bd725f63c4da12ab48a0cc11ff034c9d9878`,`fix: close STEP-075 binding selector identity audit`。 +- push result:`653e6f2f1..2645bd725 particle/main -> particle/main`;remote branch:`origin/particle/main`。 +- Final HEAD(scoped implementation delivery):`2645bd725f63c4da12ab48a0cc11ff034c9d9878`。 + +## 阻塞与下一步 + +- 无实现阻塞。 +- STEP-075 v3 已关闭;按任务书停止,不进入 STEP-076。 diff --git a/docs/50_Step记录/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_v1.md b/docs/50_Step记录/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_v1.md new file mode 100644 index 000000000..bf5adc20c --- /dev/null +++ b/docs/50_Step记录/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_v1.md @@ -0,0 +1,34 @@ +# STEP-076 — QBIN2 V4 Core Host API 与 RasterArraySource 接线 v1 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 开始 HEAD:`b148fb6a0c6711182194f6778d80c8f8a60eb037`。 +- 任务前 STEP-062 两处 dirty、历史 untracked 和 build 输出保持原样,未暂存、覆盖或提交。 +- 本 Step 仅在 C++ Core / style source 接入 QBIN V4;V3 timeline、renderer、JNI/Java/RN/Test Host 均不变。 + +## 完成项 + +- [x] `RasterArraySource` 新增隔离 V4 owner 状态:`EncodedFieldStore`、`ForecastClock`、可选 Color/Particle binding;与 V3 `RasterArrayTimelineRuntime` 生命周期和数据完全分离。 +- [x] 新增 `submitEncodedField`,严格按 fieldKey/runtime metadata → `QbinV2Decoder::decode` → `FieldProfileValidator::validate` → `EncodedFieldStore::submit` 顺序执行;失败在 Store 提交前返回,结果稳定区分 insert/replace/duplicate/decode/contract/key/identity/stale/incompatible/capacity。 +- [x] 新增 binding、requested forecast time、playback state/speed、clock advance、独立 color/particle resolve、单 field / all field clear API;没有 Host 可写 T0/T1/MIX。 +- [x] binding 只保存 Host intent;resolve 使用既有 `BindingProjectionResolver` fail-closed。clear 保留 binding intent,且绝不调用 V3 `clearHostFields()`。 +- [x] 新增离线 QBIN deterministic suite:submission reject 后旧 window 可继续 resolve;wind `00/01/02` 与 wave `00/03/06` 各自解析 window/mix,clock in-window advance 只改变 mix 不改变 content identity,pause 和 HALF/NORMAL/DOUBLE 语义覆盖;clear/re-submit 生命周期覆盖。 + +## 验证 + +- 先通过全局 `build-output-throttle` 配置,再真实执行 `cmake --build build-linux-opengl --target mbgl-test-runner -j3`。构建环境的默认 ccache 位于只读目录,故仅将本次 ccache 临时目录置于 `/tmp`;重建成功。 +- `Source.RasterArrayV4HostApi*`:3/3,0 failure。 +- `ParticleFieldBinding.*`:6/6,0 failure。 +- `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`:11/11,0 failure。 +- `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`:24/24,0 failure。 +- `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*`:11/11,0 failure。 +- `git diff --check`:PASS。 + +## 边界、阻塞与下一步 + +- 未执行 Android Gradle/APK/ADB/Jest、iOS/Metal;未修改 Android/JNI/RN、Renderer、shader、Test Host;未使用真实服务器 QBIN,也未新增网络/缓存/gzip 逻辑。 +- 无实现阻塞。本 Step 到 C++ Host API 为止,按任务书停止,不进入 Android/JNI/RN bridge。 diff --git a/docs/50_Step记录/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_v2.md b/docs/50_Step记录/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_v2.md new file mode 100644 index 000000000..2b8defefb --- /dev/null +++ b/docs/50_Step记录/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_v2.md @@ -0,0 +1,39 @@ +# STEP-076 — QBIN2 V4 Core Host API 与 RasterArraySource 接线 v2 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 开始 HEAD:`ae03d8bfd2813eb5e65074db16018e37c7cff1c4`。 +- 任务开始前检测到 STEP-062 两处已有 dirty,以及历史 untracked/build 输出;均未修改、暂存、提交或清理。 +- v1 production 接线保持不变;本 v2 只补 V4 Host API public-path 测试证明与任务文档。 + +## 完成项 + +- [x] 针对 `RejectedDecode`、`RejectedContract`、`RejectedInvalidFieldKey`、`RejectedStaleRevision`,先由公开 `RasterArraySource` API 建立可 resolve 的 V4 Particle projection;每次 reject 后重新 resolve,并验证 content identity、T0/T1 coordinate、U/V selected-plane storage 与 shared validity storage 仍为同一内容身份。 +- [x] 保留 v1 三个 Source V4 测试,并新增 `RasterArrayV4HostApiRejectsKeepPublicProjectionStable`;该 suite 已为 4 个测试。 +- [x] clear 测试先通过公开 V3 `submitInitialRuntimeFieldWindow(...)` 和既有离线 fixture 建立 V3 `field/nextField`。`clearEncodedField`、`clearAllEncodedFields` 后均核验 V3 双端 identity/generation/validTime 不变;V4 resolve 按 fail-closed 返回,重新提交 V4 后既有 binding intent 再次 resolve。 +- [x] 未调用 `clearHostFields()`;原子性证明未访问 V4 `encodedFieldStore` 私有成员或私有 helper。 + +## 修改范围 + +- `test/source/raster_array_v4_host_api.test.cpp` +- 本 Step、完成报告与 `粒子开发记录.md` +- 未修改 production API、Renderer/shader、JNI、Android/RN/Test Host、QBIN decoder 或 Field Runtime/Binding production 代码。 + +## 验证 + +- [x] 真实执行 `cmake --build build-linux-opengl --target mbgl-test-runner -j3`,通过全局 `build-output-throttle`,PASS。 +- [x] `Source.RasterArrayV4HostApi*`:Running 4,0 failure。 +- [x] `ParticleFieldBinding.*`:Running 6,0 failure。 +- [x] `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`:Running 11,0 failure。 +- [x] `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`:Running 24,0 failure。 +- [x] `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*`:Running 11,0 failure。 +- [x] `git diff --check`:PASS。 + +## 边界、阻塞与停止条件 + +- 未执行 Android 编译、Gradle/APK、ADB、实体机、Jest 或 iOS/Metal。 +- 无阻塞;仅完成本任务书的审计收口,停止,不进入下一 Step。 diff --git a/docs/50_Step记录/STEP-077_QBIN2_V4_Android_JNI_RN桥接只读调查_v1.md b/docs/50_Step记录/STEP-077_QBIN2_V4_Android_JNI_RN桥接只读调查_v1.md new file mode 100644 index 000000000..36f064c8d --- /dev/null +++ b/docs/50_Step记录/STEP-077_QBIN2_V4_Android_JNI_RN桥接只读调查_v1.md @@ -0,0 +1,29 @@ +# STEP-077 QBIN2 V4 Android / JNI / RN 桥接只读调查 v1 + +状态:完成 + +日期:2026-09-13 + +## 目标与结果 + +- [x] 基于实际 RN、Kotlin、Java、JNI、`RasterArraySource` 源码完成 V4 桥接调查。 +- [x] 给出 file URI 传输、small control-plane ABI、V3/V4 timeline/clear 隔离、结果和 lifecycle 映射建议。 +- [x] 生成调查报告、完成报告并追加开发记录。 + +## 范围 + +只读调查;未修改 production code、package version、Test Host、Renderer/GPU,未下载真实 QBIN,未执行 Android 编译、APK、ADB 或实体机操作。 + +## 核心结论 + +V4 必须新增独立 `submitEncodedField` / binding / forecast / clear 命名空间。V3 `submitField`、`clearFields`、forecast/playback API 当前会进入 V3 Host field / timeline,不能复用。RN 传 `qbinFileUri`,Kotlin 以现有 64 MiB local-file guard 读取,再走 Java `byte[]` 和 JNI copy;不要使用 JS number array 或 Base64。 + +## 验证 + +- [x] 对要求的全部实际源码路径、JNI registration、Core public V4 API、package codegen/typecheck config 和 `WeatherRuntimeStateTest` 做静态审计。 +- [x] `git diff --check`(仅本任务文档)将在提交前执行。 +- [ ] Android/Gradle/APK/ADB/实体机:任务明确不执行。 + +## 阻塞与下一步 + +无调查阻塞。后续只能按报告中拆分实现 Step,先做 Spec/ABI 与 V4-only facade/JNI 隔离测试;本 Step 到此停止。 diff --git a/docs/50_Step记录/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_v2.md b/docs/50_Step记录/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_v2.md new file mode 100644 index 000000000..0f7cb52ee --- /dev/null +++ b/docs/50_Step记录/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_v2.md @@ -0,0 +1,44 @@ +# STEP-078 — QBIN2 V4 Android / JNI / RN 桥接实现 v2 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 开始 HEAD:`4b5192e224613a885c1d038594fe06c5eb58df50`。 +- 保留 v1 未提交 bridge 草稿,并修正其已确认的 ABI/契约问题。 +- 开始时已有 STEP-062 两处 dirty、历史 untracked 与 `.codex-build-logs/`;未清理、改写、暂存或提交它们。 + +## 完成项 + +- [x] RN → TurboModule → Kotlin → Java → MapLibreMap → NativeMap/NativeMapView → JNI → `RasterArraySource` 完整贯通独立 V4 submit、两类 binding、forecast time/state/speed 与两类 clear API。 +- [x] `EncodedColorBindingInput.expectedUnitID` 端到端传递;不在 bridge 默认为 NONE。 +- [x] `ParticleBinding` ABI 固定 U/V/direction 三组 primitive selector slot;Vector 只接受 active U/V,Direction 只接受 active direction,inactive slot 仅允许 `(0,0)`。 +- [x] forecast playback state 与 speed 分别调用 `setForecastPlaybackState`、`setForecastPlaybackSpeed`,没有互相覆盖。 +- [x] frozen `contractJson` 使用 `contractSchemaVersion`、`requiredFlag` 等 Core 同名字段;新增无 Android/JNI 依赖的纯 C++ parser,JNI 仅调用该 parser。 +- [x] parser 单测覆盖合法 schema、缺字段、未知 schema、整数越界、重复 selector、非法 enum、requiredPlanes 上限及非法 profileID。 +- [x] QBIN 仍仅通过绝对路径或 `file://` local file URI 进入 Kotlin,沿用读取前后 1..64 MiB guard;没有 URL、HTTP、Base64 或 JS number array。 +- [x] Core 十种 `RasterArrayEncodedFieldSubmitResult` 保持一一 `.name` resolve;source 缺失/类型不符只在私有 JNI `0` sentinel 转为 `SOURCE_MISSING` Promise rejection,不伪装 Core result。 + +## 实际修改范围 + +- RN/TurboModule/Kotlin:`FieldRuntime.ts`、`types.ts`、`NativeWeatherRuntimeBridge.ts`、`WeatherRuntimeBridgeModule.kt`。 +- Java/JNI:`WeatherRuntime`、`MapLibreMap`、`NativeMap`、`NativeMapView`、`native_map_view.hpp/.cpp`。 +- 新增 provider-neutral parser:`field_profile_contract_json.hpp/.cpp` 及 `field_profile_contract_json.test.cpp`,并注册 Core/Test CMake。 +- 未修改 Renderer、GPU、shader、Test Host、NavSea、STEP-073~076 已冻结 production 语义。 + +## Linux 与静态验证 + +- [x] 在 `particle-codex` tmux session 中通过全局 `build-output-throttle` 重建 `mbgl-test-runner`。 +- [x] parser 单测、STEP-076 Source V4、STEP-075 Binding、STEP-074 QBIN/Ingress、STEP-073 Runtime/Profile 与 V3 Source 过滤回归:PASS。 +- [x] package TypeScript `tsc --noEmit`:PASS。 +- [x] TurboModule schema combine/codegen:PASS(仅 `NativeWeatherRuntimeBridge` Spec)。 +- [x] Java native declarations 与 C++ JNI registration V4 name set 静态一一核对:PASS。 +- [x] `git diff --check`:PASS。 + +## 未执行、阻塞与停止 + +- Android Kotlin/Java/JNI 真实编译、Gradle、APK、ADB、实体机:按任务约束未执行,待后续一次性 Android 验收。 +- 未执行 iOS/Metal 或 Jest。 +- 无实现阻塞;本 Step 到此停止,不进入 STEP-079。 diff --git a/docs/50_Step记录/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_v3.md b/docs/50_Step记录/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_v3.md new file mode 100644 index 000000000..26d9e4228 --- /dev/null +++ b/docs/50_Step记录/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_v3.md @@ -0,0 +1,44 @@ +# STEP-078 — QBIN2 V4 Android / JNI / RN 桥接实现 v3 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 基线:`73779b89bcce1094eb344f4727d6d23904f667f3`。 +- 仅关闭 V4 bridge 的 QBIN transport 三层 64 MiB guard 与受限 ID 的 UTF-8 byte-length guard;不进入 STEP-079。 +- 任务开始前 STEP-062 两处 dirty、历史 untracked、`.codex-build-logs/` 与 Test Host generated/android/.expo/tgz 均未清理、改写、暂存或提交。 + +## 完成项 + +- [x] Java `WeatherRuntime.submitEncodedField(...)` 在 map/JNI 前拒绝空及超过 64 MiB QBIN,并以 UTF-8 1..64 bytes 检查 fieldKey、fieldID 和 contract JSON 中 profileID。 +- [x] JNI 在读取 `jbyteArray` 长度后、复制到 `std::vector` 前重复检查 1..64 MiB;无效 transport 直接返回既有 `RejectedDecode`,不产生 oversized vector。 +- [x] Kotlin local-file 既有 1..64 MiB read guard 保持;Kotlin fieldKey、fieldID、profileID 改为 `toByteArray(Charsets.UTF_8).size` 的 1..64 bytes 检查。 +- [x] RN/TS 改用 `TextEncoder` UTF-8 bytes,fieldKey、runtime fieldID 与 profileID 在 33 个 CJK 字符(99 bytes)时 fail-fast。 +- [x] 新增 provider-neutral C++ transport-size helper/test、JDK-only Java guard test 与 TS directed test;未改变 QBIN decoder 的 64 MiB file / 32 MiB decoded-raw 语义、Core 十种 submit result、binding 或 forecast state/speed。 + +## 修改范围 + +- RN:`FieldRuntime.ts` 与 private `EncodedFieldGuards.ts`、其 directed test。 +- Kotlin/Java:`WeatherRuntimeBridgeModule.kt`、`WeatherRuntime.java` 与 JDK-only V4 guard/helper test。 +- JNI:`native_map_view.cpp`;Core only 增加 transport-size header 和 Linux unit test。 +- CMake/test registration、本 Step、本报告、`粒子开发记录.md`。 + +## 验证 + +- [x] 在 `particle-codex` tmux session,经全局 `build-output-throttle`:package `tsc --noEmit` PASS、TS UTF-8 directed test PASS、TurboModule schema combine/codegen PASS。 +- [x] JDK-only Java guard test PASS:64 MiB accepted,0 与 64 MiB+1 rejected,33 CJK 字符 rejected。 +- [x] `mbgl-test-runner` 实际 rebuild PASS;`ParticleFieldProfileContractJson.*`、`Source.RasterArrayV4HostApi*`、`ParticleFieldBinding.*`、`ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`、`ParticleEncodedFieldTransport.*`、`ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`、`Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*` 合并过滤 PASS。 +- [x] Java declaration ↔ C++ JNI registration V4 name-set 静态核对、Kotlin/Java UTF-8 guard 静态核对、`git diff --check`:PASS。 + +## 未完成、阻塞与下一步 + +- Android Kotlin/Java/JNI 真实编译、Gradle、APK、ADB、设备验证按任务约束未执行;需要后续一次性 Android 验收。iOS/Metal 与 Jest 同样未执行。 +- 无实现阻塞。本 Step 在 scoped commit/push 后立即停止,不进入 STEP-079。 + +## Git 收尾 + +- 实现 commit:`75a974e77503c56ec5bd2823ac1b689225ed80a7`(`fix: close STEP-078 V4 bridge guard audit`)。 +- push:PASS,`73779b89b..75a974e77 particle/main -> particle/main`;remote branch 为 `origin/particle/main`。 +- 本记录及完成报告的 Git 收尾补充将以独立、仅文档的 STEP-078 scoped commit 推送;其 Final HEAD 记录于该次文档收尾的提交输出,之后立即停止。 diff --git a/docs/50_Step记录/STEP-078_QBIN2_V4_ColorBinding_VelocityComponent接口修订_v4.md b/docs/50_Step记录/STEP-078_QBIN2_V4_ColorBinding_VelocityComponent接口修订_v4.md new file mode 100644 index 000000000..882fed316 --- /dev/null +++ b/docs/50_Step记录/STEP-078_QBIN2_V4_ColorBinding_VelocityComponent接口修订_v4.md @@ -0,0 +1,57 @@ +# STEP-078 — QBIN2 V4 ColorBinding VELOCITY_COMPONENT 接口修订 v4 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 开始 HEAD:`dd1d7014710819fe55857968558fcc9538f28d9c`。 +- 开始 `origin/particle/main`:`dd1d7014710819fe55857968558fcc9538f28d9c`。 +- 仅解除 STEP-079 已确认的 ColorBinding public API blocker:让 `FieldRuntime.setEncodedColorBinding(...)` 能表达既有 Core `VELOCITY_COMPONENT`;不进入 STEP-079。 +- 任务开始时已有 STEP-062 两处 dirty、历史 untracked 与 build 日志;均未清理、改写、暂存或提交。 + +## 完成项 + +- [x] `EncodedColorBindingInput.expectedValueClass` 新增 `VELOCITY_COMPONENT`,TS public fail-fast validation 同步允许该值。 +- [x] Kotlin `WeatherRuntimeBridgeModule` 将 `SCALAR` / `VELOCITY_COMPONENT` / `DIRECTION_ANGLE` 精确映射为 Core value class `0` / `1` / `2`。 +- [x] JNI `setRasterArraySourceEncodedColorBinding(...)` 仅允许 `0..2`;`VALIDITY=3` 与其它非法值继续 fail-closed。 +- [x] Core 回归证明以真实 `VelocityComponent` U-east plane 的 ColorBinding 可 resolve,并证明 `Validity` ColorBinding 返回 `InvalidBinding`。 +- [x] TurboModule primitive string ABI 与 Java `WeatherRuntime.setEncodedColorBinding(...)` 的参数数量、顺序及 primitive 形状均未修改。 + +## 修改范围 + +- `packages/maplibre-particle-rn/src/types.ts` +- `packages/maplibre-particle-rn/src/FieldRuntime.ts` +- `packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt` +- `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp` +- `test/particle/field_binding.test.cpp` +- 本记录、完成报告与 `粒子开发记录.md`。 + +未修改 Renderer/GPU/shader、QBIN container/decoder、Store、ForecastClock、Projection identity、ParticleBinding、Test Host V4 或 NavSea。 + +## 验证 + +所有 noisy command 均在 `particle-codex` tmux session 内经 `build-output-throttle` 执行,日志位于 `/tmp/step078v4-build-logs`: + +- [x] package `tsc --noEmit`。 +- [x] TurboModule schema combine/codegen。 +- [x] Linux OpenGL `mbgl-test-runner` rebuild。 +- [x] `ParticleFieldBinding.*`。 +- [x] `Source.RasterArrayV4HostApi*`。 +- [x] `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`。 +- [x] `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`。 +- [x] `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*`。 +- [x] Java `nativeSetRasterArraySourceEncodedColorBinding` 6-parameter declaration、C++ method signature 和 registration 静态一致性核对。 +- [x] task-scope `git diff --check`。 + +## 未完成、阻塞与下一步 + +- Android Kotlin/Java/JNI 真实编译、Gradle、APK、install、ADB、设备/模拟器验证均按任务约束未执行;Windows 11 Android Test Host 编译仍待用户手工验收。Jest、iOS/Metal 亦未执行。 +- 无实现阻塞。完成 scoped Git 收尾后立即停止;STEP-079 由 ChatGPT 独立复验本修订后重启。 + +## Git 收尾 + +- implementation commit:`e02353820aafa1834ce3a2ab3afe9261fadd83bc`(`fix: expose V4 velocity component color binding`)。 +- implementation push:PASS,`dd1d70147..e02353820 particle/main -> particle/main`;remote branch 为 `origin/particle/main`。 +- implementation Final HEAD:`e02353820aafa1834ce3a2ab3afe9261fadd83bc`。本记录、完成报告和开发记录将以独立、仅文档 scoped commit 推送;完成后立即停止。 diff --git a/docs/50_Step记录/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_v1.md b/docs/50_Step记录/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_v1.md new file mode 100644 index 000000000..060449885 --- /dev/null +++ b/docs/50_Step记录/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_v1.md @@ -0,0 +1,46 @@ +# STEP-079 — 真实 QBIN Golden Fixture 接入 RN Test Host v1 + +状态:阻塞 + +日期:2026-09-13 + +## 目标 + +把 WeatherV2 冻结的 QBIN V2 Golden Fixture 通过 STEP-078 的 V4 public API 接入 `MapLibreParticleRNTestHost`;本 Step 不执行 Android Gradle、APK、安装或 ADB。 + +## 起始基线 + +- `HEAD`:`2ea8f03064b2379d5220ce67806e3543e9cba0a7` +- `origin/particle/main`:`2ea8f03064b2379d5220ce67806e3543e9cba0a7` +- 开始前存在 STEP-062 dirty、历史 untracked、`.codex-build-logs/`、Test Host generated Android/Expo/modules 与 package tgz;均未清理、暂存或提交。 + +## 已完成的只读核对 + +- [x] 核对冻结来源 `/root/sourceserver/weatherV2/_artifacts/MapLibre_QBIN_V2_单元测试数据_v1/`、说明、原始 manifest、profile/semantic/unit registry。 +- [x] 核对 8 个 raw `.qbin` 的目标(Wind/Wave/Current/Rain,各 T0/T1)及 descriptor:Wind/Current U/V 都是 `VELOCITY_COMPONENT`,semantic `1/2`、group `1`、unit `1`。 +- [x] 核对 STEP-078 V4 TypeScript、TurboModule/Kotlin bridge 与 Core ColorBinding resolver。 +- [x] 确认不存在可在不改变冻结 public API 语义的前提下为 Wind/Current 提交正确 ColorBinding 的路径。 + +## 阻塞 + +Wind/Current 的任务合同要求选择 U 或 V 做 ColorBinding;其正确 `expectedValueClass` 是 `VELOCITY_COMPONENT`。然而冻结的 RN public API 在 `FieldRuntime.setEncodedColorBinding` 只接受 `SCALAR` 或 `DIRECTION_ANGLE`,Kotlin bridge 同样只映射这两者。将 U/V 伪装为 `SCALAR` 虽能通过 JS/Kotlin 参数白名单,但 Core resolver 会以 `PlaneClassMismatch` 拒绝真实 velocity plane。 + +因此无法同时满足“所有 dataset 至少有 ColorBinding”“所有 binding 仅通过冻结 V4 public API”“不得改变 STEP-078 public API 语义”。按任务的接口阻塞条款,未创建 Test Host wrapper、未修改 package/Core/Renderer,也未保留半成品 fixture 复制。 + +完整证据与解阻条件见 `docs/报告/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_接口阻塞报告_v1.md`。 + +## 构建与验证 + +- 未执行 TypeScript/codegen/C++ build 或测试:在实现开始前已得到不可绕过的 public-API 合同矛盾,继续执行不会验证一个可交付实现。 +- 未执行 Android Gradle、APK、install、ADB、模拟器、实体机或 iOS/Metal 操作。 +- `git diff --check` 与 staged `git diff --cached --check`:在 `particle-codex` tmux session 执行,PASS。 + +## Git 收尾 + +- 阻塞记录提交:`afcc74066f1395678621fa2b1a70d10bc722a623`,`docs: record STEP-079 V4 fixture API blocker`。 +- 已推送至 `origin/particle/main`;该提交只含本 Step 的 Step 记录、接口阻塞报告和开发记录,不含 fixture、Test Host 或既有 dirty/untracked 文件。 + +## 允许/禁止范围结果 + +- 未修改 Renderer/GPU/shader、Core/QBIN/Binding/Host API、Android bridge、RN package public API、NavSea、网络、下载或 gzip。 +- 未进入下一 Step;等待具备明确授权的 STEP-078 API 补充/修订后重新开始本 Step。 diff --git a/docs/50_Step记录/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_v2.md b/docs/50_Step记录/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_v2.md new file mode 100644 index 000000000..b62141705 --- /dev/null +++ b/docs/50_Step记录/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_v2.md @@ -0,0 +1,49 @@ +# STEP-079 — 真实 QBIN Golden Fixture 接入 RN Test Host v2 + +状态:完成(IMPLEMENTED / WAITING FOR WINDOWS 11 ANDROID VALIDATION) + +日期:2026-09-13 + +## 目标与基线 + +- 开始 HEAD / `origin/particle/main`:`a83ab9cc94a14426913f971786eae6b13a6f1702`。 +- 将 WeatherV2 冻结来源的 Wind、Wave、Current、Rain QBIN V2 原始 T0/T1 fixture 接入现有 RN Test Host 的 V4 public API;保留全部 V3 测试区。 +- 开始前的 STEP-062 dirty、历史 untracked、`.codex-build-logs/`、Test Host generated `android/.expo/modules`、tgz 和旧任务书均未清理、暂存或提交。 + +## 完成项 + +- [x] 从 `/root/sourceserver/weatherV2/_artifacts/MapLibre_QBIN_V2_单元测试数据_v1/` 逐字节复制 8 个 raw `.qbin` 到 Test Host 自有 `assets/qbin-golden/`;运行时不读取该来源、不使用 HTTP/CDN/NAS/download/gzip。 +- [x] 新增 V4 fixture contract:稳定 `fieldKey`、`fieldID`、generation/dataVersion、真实 validTime/midpoint、profile contract、source 相对路径、bytes 与 SHA-256。 +- [x] Wind/Current 使用真实 U ColorBinding:semantic 1 / group 1 / `VELOCITY_COMPONENT` / unit 1,并使用 U/V `VECTOR_COMPONENTS` ParticleBinding;未伪装为 `SCALAR`。 +- [x] Wave 使用 height ColorBinding(20/0/`SCALAR`/2)及 direction ParticleBinding(10/0/unit 4);Rain 使用 precipitation ColorBinding(30/0/`SCALAR`/5),Particle 明确 N/A。 +- [x] 沿用 `expo-asset` 将 bundled asset materialize 为 fail-closed 的本地 `file://` URI,再调用 `FieldRuntime.submitEncodedField(...)`;Metro 仅追加 `qbin` asset extension。 +- [x] 保留 V3 区域;新增 V4 dataset 选择、load、分开的 T0/T1 submit 结果、binding 结果、T0/midpoint/T1 seek、PLAY/PAUSE、正数 speed、clear selected/clear all 控制。load 固定顺序为 clear → binding → T0 → T1 → midpoint,T0/T1 非 `ACCEPTED_INSERTED` 会显示失败;midpoint 仅显示 command accepted,不宣称观测到 mix=0.5。 +- [x] 新增开发期 fail-closed fixture verifier,逐个同时核验冻结 source 与 bundled copy 的 raw bytes/SHA-256。 + +## 修改范围 + +- 仅修改 `integration/react-native/MapLibreParticleRNTestHost/` 下的 V4 fixture、assets、materialize/UI、Metro、README 与 verifier,以及本 Step、完成报告、`粒子开发记录.md`。 +- 未修改 Core、QBIN decoder、JNI、Android bridge、RN package public API、Renderer/GPU/shader、NavSea 或网络层。 + +## 验证 + +以下均在 `particle-codex` tmux session 内执行;所有 build/typecheck/codegen/test 经 `build-output-throttle`,日志位于 `/tmp/step079-build-logs`: + +- [x] Test Host `npm --prefix integration/react-native/MapLibreParticleRNTestHost run typecheck`:PASS。 +- [x] package `npx tsc --noEmit -p packages/maplibre-particle-rn/tsconfig.json`:PASS。 +- [x] TurboModule schema combine 与 Android modules codegen:PASS。 +- [x] `scripts/verify-qbin-golden-fixtures.py`:PASS(8 个 raw source/copy 文件 bytes + SHA-256)。 +- [x] Linux OpenGL `cmake --build build-linux-opengl --target mbgl-test-runner -j3`:PASS(当前无 Core 变更,target 已最新)。 +- [x] `ParticleFieldProfileContractJson.*`、`ParticleEncodedFieldTransport.*`、`Source.RasterArrayV4HostApi*`、`ParticleFieldBinding.*`、`ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`、`ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`、`Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*` 合并过滤:PASS。 +- [x] `git diff --check` 与 task-scope cached `git diff --check`:PASS。 + +## 未完成、无法验证与下一步 + +- Android Gradle/APK/install/ADB/模拟器/真机均未在 Linux 执行;Jest、iOS/Metal 也未执行。 +- 无法在 Linux 证明 Android Test Host 实际读取 Expo bundled `.qbin`、Kotlin/JNI/Renderer consumer 接受 fixture,或观察 Android 画面/播放效果;这些必须由 Windows 11 用户手工验收。 +- 无 implementation blocker。下一步仅为 Windows 11 Android 手工验收;本 Step 不进入 Renderer consumer、NavSea 正式接入或下一 Step。 + +## Git 收尾 + +- implementation commit:`f1ea88b0ce9c6995f05a8d2ae2eb2696d72e5f28`,`feat: add V4 QBIN golden fixture test host`。 +- implementation Final HEAD:`f1ea88b0ce9c6995f05a8d2ae2eb2696d72e5f28`。本 Step/报告/开发记录使用独立且仅文档的 scoped commit 收尾并 push 到 `origin/particle/main`;该文档收尾后立即停止。 diff --git a/docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v1.md b/docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v1.md new file mode 100644 index 000000000..94725ed07 --- /dev/null +++ b/docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v1.md @@ -0,0 +1,69 @@ +# STEP-080 — Android AAR 与 RN Test Host 标准构建流水线 v1 + +> **历史版本。** 当前正常公网发布/Win11 验证流程以同目录 `STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md` 为准;v1 的本地 bundle/MavenLocal 流程仅作历史/离线参考。 + + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 开始 HEAD / `origin/particle/main`:`d490578b973508d267c590dd604b033507c9b859`。 +- 固定交付版本:RN package / Native AAR 均为 `13.4.1-particle.3`,Maven coordinate 为 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3`。 +- 任务开始前已有 Native `VERSION` 升级、JNI 头文件修正、STEP-062 文档 dirty、历史 untracked task/build/cache/tgz 和 Test Host generated Android/Expo/modules;均未清理、暂存或提交。 + +## 必做项 + +- [x] 新增 Linux producer `tools/particle_android_pipeline/linux_build_publish.sh`:报告 HEAD/branch/dirty;版本完全相等校验;SDK 解析;OpenGL Release 四 ABI 完整构建与 Maven Local publish;Maven artifact、旧 `.2` 保护、AAR/tgz bytes/SHA-256 校验;版本隔离 bundle/manifest。 +- [x] Linux 所有 noisy native build、Gradle publish、RN TypeScript typecheck 与 npm pack 均强制经 build-output-throttle;runner 不可达即 fail-closed。 +- [x] 默认 Linux 流程会生成可传输 AAR/POM/module/tgz 与 `artifacts/maplibre-particle//build-manifest.json`;目录已有 manifest 或不同 partial file 时 fail-closed,不覆盖历史 handoff。 +- [x] 明确提供 `--reuse-existing`:仅能消费已完整存在的 Maven Local 发布物并在 manifest 标识 `PASS_REUSED_EXISTING_MAVEN_LOCAL`;不会冒充重新 AAR 构建。`--preflight` 无写入。 +- [x] 新增 Windows consumer `tools/particle_android_pipeline/windows_build_testhost.ps1`:manifest 是唯一 artifact/version 输入,校验 SHA、安装 AAR/POM/module、同步 clean Test Host tracked source、固定 tgz 与 Maven coordinate、显式 prebuild/Debug APK build、输出 APK bytes/SHA-256。 +- [x] Windows 默认不做安装、启动或 ADB;只有 `-Install` 与 `-Install -Run` 才使用固定设备 `192.168.1.146`。支持 `-DryRun`,先打印版本、AAR、tgz、Host、Maven Local 与 Gradle command。 +- [x] 新增中文 README;版本升级必须先同步 `platform/android/VERSION` 与 RN `package.json`,pipeline 只校验,绝不暗改版本。 +- [x] `.gitignore` 忽略 `/artifacts/maplibre-particle/`,生成 AAR/tgz/manifest 不进入 scoped commit。 + +## 允许修改 + +- `tools/particle_android_pipeline/` 的 Linux/Windows 脚本及说明。 +- `.gitignore`、本 Step、完成报告与 `粒子开发记录.md`。 +- 运行时写入 Git ignore 的 `local.properties`、`.codex-build-logs/`、`artifacts/`。 + +## 禁止修改 + +- Core、Renderer、QBIN、Binding、JNI 业务语义。 +- Native/RN 版本值、既有 dirty/untracked、历史 Maven Local `.2` 发布物。 +- Windows APK/ADB/设备、Jest、iOS/Metal;本 Step 不重复完整 Linux AAR 编译。 + +## 构建与验证 + +以下 Linux 验证均在 `particle-codex` tmux session 内完成: + +- [x] `bash -n tools/particle_android_pipeline/linux_build_publish.sh`:PASS。 +- [x] `linux_build_publish.sh --preflight`:PASS;SDK 解析为 `/root/Android/Sdk`,无 build/package/artifact 写入。 +- [x] `linux_build_publish.sh --reuse-existing`:PASS;不运行完整 AAR build/publish,验证现存 `.3` Maven Local 的 AAR/POM/module,保护 `.2` 字节不变;经限流完成 RN package typecheck 与 npm pack。 +- [x] manifest JSON contract:AAR/POM/module/tgz 的存在、绝对路径、bytes、SHA-256、版本和 Maven coordinate 全部 PASS。 +- [x] tarball `package/package.json` version 与 Native `13.4.1-particle.3` 一致:PASS。 +- [x] `git diff --check -- .gitignore`:PASS。 +- [ ] Windows PowerShell parser/static validation:本 Linux 环境无 `pwsh`,明确 `POWERSHELL_PARSE_UNAVAILABLE_NO_PWSH`;未伪称 PASS。 + +## 当前结果 + +本轮 bundle 已在 Git ignore 路径生成: + +```text +artifacts/maplibre-particle/13.4.1-particle.3/build-manifest.json +``` + +manifest 记录构建 HEAD、分支、dirty 状态、版本、四 ABI、OpenGL、Maven coordinate、AAR/POM/module/tgz 的 path/bytes/SHA-256 和 Linux validation result。验证使用的 existing AAR SHA-256 为 `9ef03eb0d0c03457adc04847cfa69fb67d85191daecd64d3b76db2ad8cc688cd`,tgz SHA-256 为 `6c8af6ff19adb4e25785b54bea3edd1dc7d0502d9158cf1c519d84b69d08ee55`。 + +## 未完成与阻塞 + +- 未重复执行完整四 ABI AAR 构建与 Maven Local publish:任务明确禁止为验证重复完整 AAR 编译;默认脚本已固化该受限命令,验证仅复用现存完整 `.3` publication。 +- Linux 无 `pwsh`,因此未能本机解析 Windows PowerShell。Windows `-DryRun`、npm/prebuild/Gradle APK、安装、启动和设备验收均未执行,须在 Windows 11 使用该 manifest 完成;这不是 Linux 流水线实现阻塞。 +- Jest、Windows APK/ADB/设备、iOS/Metal 均未执行。 + +## 下一步 + +Windows 11 将完整 Linux bundle 复制到本机后,先运行 `windows_build_testhost.ps1 -ManifestPath -DryRun`,核对路径与 SHA;再无设备开关地运行同一脚本完成 Debug APK。只有用户明确选择时才附加 `-Install` 或 `-Run`。 diff --git a/docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md b/docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md new file mode 100644 index 000000000..4ef4171b0 --- /dev/null +++ b/docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md @@ -0,0 +1,535 @@ +# STEP-080 — Android AAR 与 RN Test Host 标准构建流水线 v2 + +状态:已按真实 Linux → pbf → Windows 11 → Android 模拟器链路验证通过 + +日期:2026-09-14 + +## 1. 目的 + +本文固定 MapLibreParticleLab Android Native AAR 完成后,到 Windows 11 构建 RN Test Host、启动 Metro、安装 App、执行 QBIN V4 验证的标准流程。 + +以后正常验证不得再临时复制 AAR、临时改 MavenLocal、在 Windows 重编 Native AAR,或凭经验猜 Metro 端口。 + +## 2. 最终权威链路 + +```text +Linux MapLibreParticleLab +→ 编译 Native OpenGL AAR +→ 发布到 /mnt/nas/pbf/maven +→ 公网 https://pbf.jpcn.ccwu.cc/maven + +Linux RN package +→ npm pack +→ 发布到 /mnt/nas/pbf/npm +→ 公网 https://pbf.jpcn.ccwu.cc/npm + +Windows 11 RN Test Host +→ npm install 公网 tgz +→ Gradle 从公网 Maven 自动取得 Native AAR +→ Expo prebuild +→ assembleDebug +→ Metro :8091 +→ adb reverse 8081 → 8091 +→ 安装并启动 App +→ QBIN V4 真机/模拟器验证 +``` + +## 3. Linux 与 Windows 的职责边界 + +### Linux 负责 + +- MapLibre Native Android OpenGL AAR 编译。 +- Native AAR 版本管理。 +- RN package 打包。 +- AAR/POM/module 与 RN tgz 发布到 pbf。 +- 公网回读并校验 SHA-256。 + +### Windows 11 负责 + +- 从 pbf npm 安装 RN package。 +- 通过 Gradle 从 pbf Maven 消费对应 Native AAR。 +- Expo prebuild。 +- Test Host Debug APK 编译。 +- Metro、ADB reverse、安装、启动和人工验证。 + +### 禁止 + +- Windows 正常流程中重新编译 MapLibre Native AAR。 +- Windows 正常流程中使用 Maven Local 替代 pbf Maven。 +- Linux 与 Windows 之间通过局域网临时复制 AAR 作为正式验证路径。 +- 覆盖旧 Maven 版本坐标。 + +## 4. 版本一致性硬门槛 + +以当前已验证版本为例: + +```text +RN package = 13.4.1-particle.3 +Native AAR = 13.4.1-particle.3 +``` + +以下位置必须全部一致,否则停止构建: + +1. `platform/android/VERSION` +2. `packages/maplibre-particle-rn/package.json` +3. `packages/maplibre-particle-rn/android/build.gradle` +4. `packages/maplibre-particle-rn/app.plugin.js` +5. `integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js` + +## 5. Linux:AAR 构建与发布 + +推荐使用仓库内标准脚本: + +```bash +cd /mnt/sda1/MapLibreParticleLab +tools/particle_android_pipeline/linux_build_publish.sh +``` + +如果只是检查版本、SDK、路径和发布条件: + +```bash +tools/particle_android_pipeline/linux_build_publish.sh --preflight +``` + +Linux Android SDK 当前实际路径: + +```text +/root/Android/Sdk +``` + +AAR 必须使用新的版本目录发布,不能覆盖历史 `.2`: + +```text +/mnt/nas/pbf/maven/cc/navsea/maplibre/android-sdk-particle-opengl// +``` + +当前 `.3` 目录为: + +```text +/mnt/nas/pbf/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.3/ +``` + +至少包含: + +```text +android-sdk-particle-opengl-13.4.1-particle.3.aar +android-sdk-particle-opengl-13.4.1-particle.3.pom +android-sdk-particle-opengl-13.4.1-particle.3.module +``` + +RN package 使用 `npm pack` 生成,并发布到: + +```text +/mnt/nas/pbf/npm/navsea-maplibre-particle-rn-.tgz +``` + +## 6. Linux:把 AAR 正式发布到公网 pbf + +`https://pbf.jpcn.ccwu.cc/` 的服务器文件根目录固定映射为: + +```text +/mnt/nas/pbf +``` + +因此 Maven 公网根: + +```text +https://pbf.jpcn.ccwu.cc/maven/ +``` + +对应 Linux: + +```text +/mnt/nas/pbf/maven/ +``` + +npm/tgz 公网根: + +```text +https://pbf.jpcn.ccwu.cc/npm/ +``` + +对应 Linux: + +```text +/mnt/nas/pbf/npm/ +``` + +发布不需要 SSH 到其他服务器,也不需要从 Win11 上传;Linux 直接写 `/mnt/nas/pbf` 即完成公网发布。 + +### 6.1 发布 Native AAR + +先定义版本: + +```bash +VERSION=13.4.1-particle.3 +GROUP_PATH=cc/navsea/maplibre/android-sdk-particle-opengl +SRC="$HOME/.m2/repository/$GROUP_PATH/$VERSION" +DST="/mnt/nas/pbf/maven/$GROUP_PATH/$VERSION" +``` + +正常发布必须使用一个从未发布过的新版本。先保护旧版本: + +```bash +test ! -e "$DST" || { + echo "目标版本已经存在,禁止覆盖:$DST" >&2 + exit 1 +} +``` + +确认本地 Maven publication 完整: + +```bash +ls -lh "$SRC" +sha256sum "$SRC/android-sdk-particle-opengl-$VERSION.aar" +``` + +创建公网 Maven 版本目录并复制: + +```bash +mkdir -p "$DST" +cp -a "$SRC"/. "$DST"/ +``` + +至少确认: + +```bash +test -f "$DST/android-sdk-particle-opengl-$VERSION.aar" +test -f "$DST/android-sdk-particle-opengl-$VERSION.pom" +test -f "$DST/android-sdk-particle-opengl-$VERSION.module" +``` + +### 6.2 公网回读验证 AAR + +公网 URL 规则: + +```text +https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl//android-sdk-particle-opengl-.aar +``` + +发布后必须从公网重新下载,不允许只检查 `/mnt/nas/pbf` 本地文件: + +```bash +LOCAL_AAR="$DST/android-sdk-particle-opengl-$VERSION.aar" +PUBLIC_AAR="https://pbf.jpcn.ccwu.cc/maven/$GROUP_PATH/$VERSION/android-sdk-particle-opengl-$VERSION.aar" +TMP_AAR="$(mktemp)" + +curl -fL "${PUBLIC_AAR}?nocache=$(date +%s)" -o "$TMP_AAR" +sha256sum "$LOCAL_AAR" "$TMP_AAR" +cmp -s "$LOCAL_AAR" "$TMP_AAR" +``` + +`cmp` 必须返回 0。只有公网回读字节完全一致,才算 AAR 发布完成。 + +同时可验证 POM/module HTTP 200: + +```bash +curl -fI "https://pbf.jpcn.ccwu.cc/maven/$GROUP_PATH/$VERSION/android-sdk-particle-opengl-$VERSION.pom" +curl -fI "https://pbf.jpcn.ccwu.cc/maven/$GROUP_PATH/$VERSION/android-sdk-particle-opengl-$VERSION.module" +``` + +### 6.3 发布 RN package 到公网 npm 目录 + +在仓库中生成 tgz: + +```bash +cd /mnt/sda1/MapLibreParticleLab/packages/maplibre-particle-rn +npm pack +``` + +生成文件示例: + +```text +navsea-maplibre-particle-rn-13.4.1-particle.3.tgz +``` + +正式发布路径: + +```bash +TGZ="navsea-maplibre-particle-rn-$VERSION.tgz" +PUBLIC_TGZ_DIR="/mnt/nas/pbf/npm" + +test ! -e "$PUBLIC_TGZ_DIR/$TGZ" || { + echo "公网同版本 tgz 已存在,禁止覆盖;请升级版本" >&2 + exit 1 +} + +cp "$TGZ" "$PUBLIC_TGZ_DIR/$TGZ" +``` + +公网 URL: + +```text +https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-.tgz +``` + +发布后必须公网回读并比较 SHA: + +```bash +TMP_TGZ="$(mktemp)" +curl -fL "https://pbf.jpcn.ccwu.cc/npm/$TGZ?nocache=$(date +%s)" -o "$TMP_TGZ" +sha256sum "$TGZ" "$TMP_TGZ" +cmp -s "$TGZ" "$TMP_TGZ" +``` + +正常发布不得重新覆盖同一版本的 tgz。今天 `.3` 的覆盖属于修复历史错误包的例外,不应成为日常流程。 + +## 7. Windows 11:从公网取得 RN package 与 AAR + +Windows 正常流程不直接下载 AAR 文件。AAR 由 Gradle 根据 RN package 中的 Maven coordinate 自动从 pbf Maven 获取。 + +当前 Test Host 建议工作目录: + +```text +D:\React\MapLibreParticleRNTestHost-V4 +``` + +不要在 `V:\` 盘符根目录执行 Expo prebuild;本次实测 Expo autolinking 在盘符根目录会出现 `Couldn't find package.json up from path V:\`。 + +### 7.1 安装 RN package + +```powershell +cd D:\React\MapLibreParticleRNTestHost-V4 + +npm install --force ` + https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.3.tgz +``` + +正常情况下新版本不需要清 npm cache;只有确认同版本曾被错误覆盖时才做: + +```powershell +npm cache clean --force +``` + +安装后必须核对实际依赖: + +```powershell +Get-Content node_modules\@navsea\maplibre-particle-rn\package.json +Select-String ` + -Path node_modules\@navsea\maplibre-particle-rn\android\build.gradle ` + -Pattern 'android-sdk-particle-opengl' +``` + +必须看到目标版本,例如: + +```text +cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3 +``` + +### 7.2 Expo prebuild 与 Android SDK + +执行: + +```powershell +cd D:\React\MapLibreParticleRNTestHost-V4 +npx expo prebuild --platform android +``` + +Windows Android SDK 当前实际路径: + +```text +C:\Users\taibe\AppData\Local\Android\Sdk +``` + +如果 Gradle 报 `SDK location not found`,优先在当前 PowerShell 显式设置: + +```powershell +$env:ANDROID_HOME='C:\Users\taibe\AppData\Local\Android\Sdk' +$env:ANDROID_SDK_ROOT='C:\Users\taibe\AppData\Local\Android\Sdk' +``` + +并写入: + +```text +D:\React\MapLibreParticleRNTestHost-V4\android\local.properties +``` + +内容: + +```properties +sdk.dir=C\:\\Users\\taibe\\AppData\\Local\\Android\\Sdk +``` + +### 7.3 构建 Debug APK + +```powershell +cd D:\React\MapLibreParticleRNTestHost-V4\android +$env:ANDROID_HOME='C:\Users\taibe\AppData\Local\Android\Sdk' +$env:ANDROID_SDK_ROOT=$env:ANDROID_HOME +.\gradlew.bat :app:assembleDebug +``` + +成功产物: + +```text +D:\React\MapLibreParticleRNTestHost-V4\android\app\build\outputs\apk\debug\app-debug.apk +``` + +## 8. Metro、ADB reverse、安装与启动 + +Test Host 的固定规则是: + +```text +Metro 实际监听端口:8091 +App 默认访问端口:8081 +ADB reverse:tcp:8081 → tcp:8091 +``` + +### 8.1 启动 Metro + +```powershell +cd D:\React\MapLibreParticleRNTestHost-V4 +npx expo start --port 8091 --clear +``` + +### 8.2 配置 reverse + +另开一个 PowerShell: + +```powershell +adb -s 192.168.1.146 reverse --remove-all +adb -s 192.168.1.146 reverse tcp:8081 tcp:8091 +adb -s 192.168.1.146 reverse --list +``` + +必须看到: + +```text +tcp:8081 tcp:8091 +``` + +### 8.3 安装并启动 + +```powershell +adb -s 192.168.1.146 install -r ` + D:\React\MapLibreParticleRNTestHost-V4\android\app\build\outputs\apk\debug\app-debug.apk + +adb -s 192.168.1.146 shell am force-stop org.maplibre.particle.rntesthost +adb -s 192.168.1.146 shell am start ` + -n org.maplibre.particle.rntesthost/.MainActivity +``` + +## 9. V3 fixture 与 Metro watchFolders + +当前 Test Host 同时保留旧 V3 数据集和 V4 QBIN 数据集。 + +独立 Windows Host 使用: + +```text +D:\React\MapLibreParticleRNTestHost-V4 +D:\React\particle-v3-fields +``` + +`src/fieldDatasets.ts` 使用 `../../particle-v3-fields/...`,因此这些 fixture 位于 Metro 项目根之外。 + +此时 `metro.config.js` 必须把外部 fixture 目录加入 `watchFolders`,例如独立 Windows Host: + +```js +const path = require("path"); +const { getDefaultConfig } = require("expo/metro-config"); + +const config = getDefaultConfig(__dirname); +config.resolver.assetExts = [...new Set([...config.resolver.assetExts, "f32", "u8", "qbin"])]; +config.watchFolders = [ + path.resolve(__dirname, "..", "particle-v3-fields"), +]; + +module.exports = config; +``` + +修改 `metro.config.js` 后必须重启 Metro,普通 Reload 不足以保证配置重载。 + +如果看到: + +```text +Unable to resolve ../../particle-v3-fields/... +``` + +先确认文件实际存在,再检查 `watchFolders`;不要重复复制同一批 fixture 或误判为 QBIN 问题。 + +## 10. QBIN V4 验收确认 + +V4 测试区必须走: + +```text +assets/qbin-golden/*.qbin +→ FieldRuntime.submitEncodedField(...) +→ WeatherRuntimeBridgeModule.submitEncodedField(...) +→ WeatherRuntime.submitEncodedField(...) +→ JNI native_map_view.cpp +→ RasterArraySource::submitEncodedField(...) +→ QbinV2Decoder::decode(...) +``` + +这与旧 V3 `metadata.json + .f32 + .u8 → submitField(...)` 是两条不同链路。 + +至少验证: + +- V4 Wind:T0/T1 load、Color Binding、Particle Binding、T0/midpoint/T1 seek。 +- V4 Wave:同上。 +- V4 Current:同上。 +- V4 Rain:Color Binding、T0/midpoint/T1;Particle Binding 为 N/A。 +- PLAY / PAUSE / playback speed。 +- `clearEncodedField` / `clearAllEncodedFields`。 + +人工确认 V4 功能正常后,才可判定 Windows Android QBIN V4 验证通过。 + +## 11. 本次 `.3` 已验证发布基线 + +Native AAR: + +```text +cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3 +SHA-256: 9ef03eb0d0c03457adc04847cfa69fb67d85191daecd64d3b76db2ad8cc688cd +``` + +最终修正后的 RN tgz: + +```text +https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.3.tgz +SHA-256: abdd5b5f313b6dc2bc2f2b00848d1901be72fed6a6e1fa15899efb10df9f923f +``` + +Windows 11 Debug APK 已于 2026-09-14 成功构建、安装并由用户完成功能验证。 + +## 12. 本次实际踩过的弯路与固定处理 + +### A. Win11 不要重编 Native AAR + +症状:为了验证 Test Host,又在 Windows 尝试重建 MapLibre Native。 + +固定规则:Native AAR 只在 Linux 生产;Windows 只通过 pbf Maven 消费。 + +### B. 不要用局域网复制 AAR 作为正常路径 + +固定规则:Linux 发布到 `/mnt/nas/pbf/maven`,Windows Gradle 从 `https://pbf.jpcn.ccwu.cc/maven` 获取。 + +### C. RN package 版本号相同但内部仍依赖旧 AAR + +必须检查 `android/build.gradle`、`app.plugin.js`、Test Host plugin 的 Maven coordinate,不能只看 `package.json` 版本号。 + +### D. Android Kotlin 必须实编译 + +Linux TypeScript/Core 检查无法替代 Android Kotlin 编译。本次实际发现 `value >= if (...)` 在 Kotlin 编译器下失败,最终改为显式括号与 `Double` 常量。 + +### E. Expo 不要在 `V:\` 根目录 prebuild + +本次实际出现 `Couldn't find package.json up from path V:\`。固定使用普通子目录,例如 `D:\React\MapLibreParticleRNTestHost-V4`。 + +### F. App 报 localhost:8081,而 Metro 在 8091 + +这是正常设计,不要把 Metro 改回 8081。正确映射是: + +```text +adb reverse tcp:8081 tcp:8091 +``` + +### G. fixture 文件存在但 Metro 仍 Unable to resolve + +优先检查 `watchFolders`,不要重复复制 fixture。仓库 Test Host 的 `metro.config.js` 已加入仓库布局/独立 Windows 布局自动探测。 + +## 13. 文档优先级 + +本文件 `v2` 覆盖 STEP-080 v1 中“Linux bundle → Windows MavenLocal”的正常发布结论。 + +`tools/particle_android_pipeline/` 中旧脚本保留用于历史/离线用途;正常联网验证以本文的 pbf 公网链路为准。 diff --git a/docs/50_Step记录/STEP-081_iOS_Metal_Renderer与真机Smoke入口_v1.md b/docs/50_Step记录/STEP-081_iOS_Metal_Renderer与真机Smoke入口_v1.md new file mode 100644 index 000000000..7426e9193 --- /dev/null +++ b/docs/50_Step记录/STEP-081_iOS_Metal_Renderer与真机Smoke入口_v1.md @@ -0,0 +1,64 @@ +# STEP-081 iOS Metal Renderer 与真机 Smoke 入口 v1 + +状态:CLOSED / PASS + +## 目标 + +为既有 C++ RasterArray/QBIN V4 → WeatherColor + RasterParticle path 增加 Metal shader registry 接线,并在 iOS Test App 中提供 Wind Golden QBIN 的真机 smoke 入口。 + +## 本轮结果 + +- [x] 新增 Metal `ParticleGpuValidationShader` source、属性/纹理表和 registry 注册。 +- [x] 保持 `ParticleGpuValidationUBO` 为唯一 shared UBO,MSL 静态契约为 `31 * 16`。 +- [x] 在 Test App Runtime Styling 菜单加入 T0/midpoint/T1/clear private smoke seam。 +- [x] CMake 将既有 Wind Golden T0/T1 复制到 app bundle。 +- [x] 仅做静态检查;未执行 Jest。 +- [x] iPhoneOS arm64 Test App 无签名编译通过,`BUILD SUCCEEDED`。 +- [x] Pleiades(iPad mini 5 / iPadOS 26.5.2)完成签名安装、真实 Metal runtime 与视觉验收。 +- [x] Deterministic Uniform East 向右、Uniform North 向上、Vortex 可见并具有连续轨迹。 +- [x] QBIN T0 / MID / T1 / Clear / Clear 后重新 T0 均通过。 + +## 允许范围与限制 + +修改仅涉及 Metal thin-hook、iOS Test App、CMake Metal/app 资源接线以及本 Step 文档。未修改 QBIN V4/Decoder/Profile/Binding、Android/RN API、版本坐标或项目总则。现有 gfx 代码使用 `gfx::RenderTarget`、`Texture2D`、`Lines` 与 shader registry;静态审查未见 layer 内的直接 OpenGL 调用。 + +## 真机验收入口 + +在 iOS Test App 中,`Particle Core Validation (Fixtures)` 用 Uniform East / Uniform North / Vortex 验证正式 Host ingress → RasterParticleLayer → Metal 路径;`Particle / QBIN V4 Smoke (T0)` 用 T0 / midpoint / T1 / Clear 验证真实 Wind Golden QBIN。QBIN T0/MID/T1 对应 1789239600000 / 1789241400000 / 1789243200000。 + +## 构建与阻塞 + +`Codex` 未执行任何编译;GPT 审查后由 RDC 完成 CMake/Xcode iPhoneOS arm64 build。最终签名 device build `BUILD SUCCEEDED`,Bundle ID `jp.eyaku.maplibreparticlelab.step081`,Wind T0/T1 QBIN 均进入 `ParticleSmokeFixtures/`。随后在 Pleiades 真机完成安装、启动、Metal shader runtime 与视觉 smoke。 + +## GPT / RDC 独立审查(2026-09-17) + +Codex 结束后由 RDC 读取完整 diff、设计文档和完成报告,GPT 独立审查通过;编译仍未执行。审查中做了 3 个小范围、事实明确的修正: + +- State A/B RenderTarget 明确为 `TextureChannelDataType::Float`,Metal fragment 输出由 `half4` 改为 `float4`,避免 Web Mercator 粒子状态在 shader 输出阶段降为半精度。 +- Metal `resolveAdvection()` 的 VELOCITY 零向量行为与 OpenGL fragment fail-closed 对齐:长度 `<= 0.00001` 返回 false。 +- Smoke 选择器由 iPad 需要 popover anchor 的 ActionSheet 改为 Alert,保证 iPhone/iPad 真机都可直接进入。 + +`git diff --check` 通过;未发现 QBIN V4/Core contract、Android/RN API 或版本坐标越界修改。下一步允许 RDC 执行 iPhoneOS device build;仍禁止 Simulator。 +## RDC 编译验证(2026-09-17) + +- `cmake --preset ios-metal` 配置成功,Xcode 26.3 / iPhoneOS 26.2 / AppleClang 17。 +- Darwin style codegen 需要 Bazel;按仓库 `.bazelversion` 使用 Bazel 8.6.0 后通过。 +- 首次签名构建被 `org.maplibre.app` provisioning 阻塞,因此改用 `CODE_SIGNING_ALLOWED=NO` 做 device-architecture 编译验证;未使用 Simulator。 +- 编译中只对 Test App 做小范围修正:使用 `${CMAKE_SOURCE_DIR}/src` 暴露 private smoke 所需内部头;`weakSelf` 改为显式 `MBXViewController *`;fixture path 使用 `NSString *`。 +- 最终 `app` target `BUILD SUCCEEDED`;`particle_gpu_validation.cpp` 和 `MBXViewController.mm` 均通过 arm64 编译。 +- 当前登记 iPhone/iPad 均为 `unavailable`,因此状态进入 `WAITING_DEVICE_SMOKE`,不能声明真机/Metal runtime PASS。 + + +## RDC 真机收口(2026-09-18) + +- 设备:Pleiades,iPad mini (5th generation) / iPad11,2,iPadOS 26.5.2;使用 iPhoneOS device build,未使用 Simulator。 +- 确认 Metal render-pipeline cache key 缺少 attachment pixel format:State RGBA32Float / Trail RGBA8Unorm / 主 renderable BGRA8Unorm 之间可能错误复用 pipeline。`src/mbgl/shaders/mtl/shader_program.cpp` 已把 color/depth/stencil format 合入 cache key。 +- Trail target 强制写测试在修复前全零、修复后 RGBA8 有效写入,证明 cache-key 根因。 +- State GPU 与 CPU/视觉验证确认 canonical U/V、T0/T1、State ping-pong、vertex-stage texture sampling 和 z0 matrix 投影正确。 +- 实测正式 previous→current 线段平均约 0.065 px、最大约 0.24 px,全部 `<0.5 px`;Metal line primitive 无稳定覆盖。保留真实 line,同时增加 Metal-only 1 px current-endpoint quad coverage,不改变物理位移。 +- Deterministic:Uniform East 向东/右,Uniform North 向北/上,Vortex 粒子可见、移动连续、有轨迹感。 +- QBIN Golden fixture:grid 513×769,bounds=112.5E..157.5E / 0..55.776573N;Smoke 相机定位约 27.8883N / 135E、zoom 3 后,T0 / MID / T1 / Clear / Clear 后重进全部人工确认正常。 +- 中间 `STEP081_*` GPU readback / fixed-line / point fallback 诊断均已删除;错误的 Point coverage 曾导致整屏绿色,最终未保留。 +- `git diff --check` 在最终清理后通过;无关 `MBXAppDelegate.m` 与 vendor/submodule dirty 不纳入 STEP-081 commit。 + +- 功能与真机验收实现已提交并成功推送:`f1c0f92b2b`(`fix: close STEP-081 iOS Metal particle rendering`),`origin/particle/main` 已同步。 diff --git a/docs/50_Step记录/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_v1.md b/docs/50_Step记录/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_v1.md new file mode 100644 index 000000000..c22e0be32 --- /dev/null +++ b/docs/50_Step记录/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_v1.md @@ -0,0 +1,40 @@ +# STEP-082 iOS Native FieldRuntime Facade 与 XCFramework 发行基线 v1 + +状态:CLOSED / PASS + +## 目标与基线 + +起始分支 `particle/main`,起始 HEAD `3e03949d3411c830cfff4bf320cd350f5b6a7527`。本 Step 只新增 iOS public V4 facade 和 XCFramework distribution 基线;不重做 STEP-081,也不进入 STEP-083/RN。 + +## 执行清单 + +- [x] iOS public Native FieldRuntime facade。 +- [x] MapView-owned replace/current/dispose 生命周期。 +- [x] V4 submit、binding、forecast、clear 转发。 +- [x] style reload/source missing fail-closed 与 public header 无 Core/STL 泄漏。 +- [x] XCFramework public header 构建图接线。 +- [x] distribution-ready 模板和中文设计文档。 +- [x] macOS XCFramework 成功构建、Headers/symbol/zip SHA-256 验证。 +- [x] `git diff --check` 已通过;本次文档收尾后的 scoped closeout commit/push 由宿主执行并回填最终 hash。 + +## 允许与禁止 + +允许修改 iOS public header/implementation、Bazel public export、release 模板以及指定中文文档。禁止改 QBIN V2、FieldProfile、BindingProjection、renderer/Metal physics、STEP-081 smoke、Android/RN/NavSea;本 Step 未修改这些范围。 + +## 最终构建记录与关闭依据 + +宿主已在真实实现提交 `136ed726cf749c24b3673c046581a38ef48be390`(`fix: close ios xcframework packaging blockers`)上完成阻塞修复和 macOS 验证。`bazel build //:generated_style_code` PASS;`bazel build //platform/ios:MapLibre.dynamic` PASS,最终日志 `/tmp/step082-host-maplibre-dynamic-final2.log` 含 `Build completed successfully`。 + +产物为 `bazel-bin/platform/ios/MapLibre.dynamic.xcframework.zip`,大小 `24639200` bytes,SHA-256 为 `64faa174c5c0dc0a2da3e289f0beaf2dedf7298665e3345f39836729b668e239`。`ios-arm64` 与 `ios-arm64_x86_64-simulator` slice 均导出 `Headers/MLNParticleFieldRuntime.h`,两份 `Mapbox.h` 均 import `MLNParticleFieldRuntime.h`;`MLNMapView_Private.h` 的 public export count 为 0。两 slice binary 均存在 `_OBJC_CLASS_$_MLNParticleFieldRuntime` 和 `_OBJC_CLASS_$_MLNParticleFieldRuntimeConfiguration`;device 架构为 arm64,simulator 架构为 x86_64+arm64。 + +本次仅完成工作树文档收尾;Git closeout commit/push 将由宿主在本文件和 v2 完成报告生成后执行,最终 hash 以宿主回填/最终回复为准。 + +## 下一步 + +STEP-082 已关闭。远端发布仍未执行,manifest 的 artifact URL 保持 `LOCAL_BUILD`;后续发布必须另行上传并以发布后的 URL/校验结果更新 release 元数据。STEP-083 不因本记录自动开始。 + +## STEP-083 post-close correction(2026-09-18) + +本段追加纠正,不改写上方当时的历史记录。后续真实 RN device 验证确认,上方使用裸 `bazel build //platform/ios:MapLibre.dynamic` 取得的默认 OpenGL artifact(`24639200` bytes,SHA-256=`64faa174c5c0dc0a2da3e289f0beaf2dedf7298665e3345f39836729b668e239`,source commit `136ed726…`)只有色场、没有 Particle render,故作废且不再作为 iOS Particle 发行基线。 + +iOS Particle 发行固定改用 `bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic`。Metal XCFramework 为 `24943196` bytes,SHA-256=`ae88975fc20f4d9961b95079638d0e9c0d8e61d4394d5c0a8ffcc48d173a827f`,source commit=`bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd`。其 device binary 含 `MLNMapView`、Metal/`MTLDevice`/`ParticleGPU` 标记且无 OpenGL marker;Pleiades iPad mini 5 RN Test Host signed Release 安装后,用户已确认粒子恢复。artifact 仍为本地构建,URL 保持 `LOCAL_BUILD`。 diff --git a/docs/50_Step记录/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_v1.md b/docs/50_Step记录/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_v1.md new file mode 100644 index 000000000..8746bcda5 --- /dev/null +++ b/docs/50_Step记录/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_v1.md @@ -0,0 +1,38 @@ +# STEP-083 iOS RN 统一 FieldRuntime / TurboModule / Fabric 与 XCFramework 消费 v1 + +状态:`CLOSED / PASS` + +## 基线与范围 + +- branch:`particle/main`;开始 HEAD:`bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd`。 +- STEP-082 public facade 与 `//platform/ios:MapLibre.dynamic` Bazel build 已由 RDC 真实验证 PASS。 +- 本批次仅增加 RN package iOS bridge/pod/codegen/autolinking、Test Host iOS identity 和记录;没有修改 Android、Shared Core、QBIN contract、renderer、NavSea 或 frozen TS public API。 + +## 已实现 + +- `WeatherRuntimeBridge` 是 RN 0.85.2 codegen TurboModule:`NativeWeatherRuntimeBridgeSpecJSI`、Promise `resolve/reject` ABI 及 package `ios.modulesProvider` 均使用当前本地 RN 0.85.2 source 的真实 pattern。 +- 每个 Map command 先校验正整数 `reactTag`,再通过 RN 注入的当前 Fabric/UIManager `viewRegistry_DEPRECATED` 进入 UI block,重新确认 `MLRNMapViewComponentView` → `contentView` → `MLRNMapView` → public `MLNMapView`。不缓存 tag、map、Core pointer 或建立第二 registry。 +- V3、V4、runtime state、weather/particle visibility、timeline、binding 与所有视觉命令均映射到现有 public `MLNParticleFieldRuntime`。V3 `clearWindFields` 的 Android 语义本身等价 `clearFields`。 +- V3/QBIN file ingress 均只接收 absolute path 或 `file://`,要求 readable regular file、1..64 MiB,并比较读前后 length;metadata、contract、UTF-8 identifier、safe integer 与 inactive `(0,0)` selector 同 Android guard。 +- pod 只 dependency `MapLibreReactNative 11.3.6`,不声明 MapLibre pod/framework/SPM product。MLRN 11.3.6 的真实 `$MLRN_SPM_SPEC` 将唯一 `MapLibre` product 注入 Pods 和 app target。 + +## RDC 实际 iOS 验证(2026-09-18) + +- RDC 已实际执行 npm dependency install(`npm ci`)、typecheck、RN codegen、Expo prebuild、Pod integration、device arm64 build、signed Release build 与安装。MapLibre.framework 签名成功。 +- Test Host 消费的是 single MapLibre SPM product;未发现第二份 MapLibre binary。新 XCFramework 必须以 Metal 命令 `bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic` 生成。默认裸 Bazel 命令的 OpenGL artifact 只有色场、无粒子,已由 STEP-082 post-close correction 作废。 +- Pleiades iPad mini 5 上 signed Release 安装运行。一次 V4 Wind load 实际观察到 `clearAllEncodedFields` → color binding → particle binding → T0 `ACCEPTED_INSERTED` → T1 `ACCEPTED_INSERTED` → midpoint seek;用户确认粒子恢复。 + +## 已执行检查 + +- `ruby -c packages/maplibre-particle-rn/MapLibreParticleRN.podspec`:成功。 +- `npm ci`、typecheck、RN codegen、Expo prebuild、Pod integration、device arm64 build、signed Release build/installation:均由 RDC 实际执行;Release 日志为 `BUILD SUCCEEDED`。 +- `npm pack --dry-run --json --cache /private/tmp/step083-npm-cache`:成功,tgz 清单包含 podspec、`ios/WeatherRuntimeBridge.h/.mm`、既有 Android 与 `src`。 +- package `codegenConfig.ios.modulesProvider.WeatherRuntimeBridge` 静态检查和 Native Spec 34 个方法与 ObjC++ 34 个实现逐项计数完成。 + +## Final control regression 真机关闭 + +`integration/react-native/MapLibreParticleRNTestHost/App.tsx` 新增唯一按钮 `RUN V4 FINAL CONTROL REGRESSION`,仅在当前 `reactTag` 有效时执行,且运行中禁用以避免重复并发。它复用 V4 加载的 `prepareV4` 公共 API 路径:clearAll → color binding →(如有)particle binding → T0/T1 `ACCEPTED_INSERTED` → midpoint seek。 + +Pleiades iPad mini 5 真机已由用户用该单按钮完成最终控制序列:`prepare#1 → PLAY → 约500ms → PAUSE → clear selected → prepare#2 → clear all → detach → attach → prepare#3 → refresh`。用户明确反馈“FINAL CONTROL REGRESSION PASS”;最后一次 prepare/refresh 后粒子恢复并保持可见。此前同一真机的 V4 Wind 验收中,用户也已确认“粒子恢复了”。 + +上述结果关闭此前唯一未完成的逐控制面真机验收项;STEP-083 正式 `CLOSED / PASS`。回归按钮保留为后续入口,未改动其逻辑。 diff --git a/docs/50_Step记录/STEP-084_正式粒子速度着色默认关闭与红头修复_v1.md b/docs/50_Step记录/STEP-084_正式粒子速度着色默认关闭与红头修复_v1.md new file mode 100644 index 000000000..03fd11fa1 --- /dev/null +++ b/docs/50_Step记录/STEP-084_正式粒子速度着色默认关闭与红头修复_v1.md @@ -0,0 +1,50 @@ +# STEP-084 正式粒子速度着色默认关闭与红头修复 v1 + +状态:`完成(等待 STEP-085 后由 RDC 统一执行 Native/真机验收)` + +## 1. 目标 + +修复 NavSea 天气正式粒子在 Android/iOS 均出现橙红色“头部”的问题。正式 `RasterParticleLayer` 默认应严格使用宿主显式提供的 `particleColor`;速度着色只作为显式 opt-in 能力保留,不再作为正式粒子的默认行为。 + +## 2. 基线 + +- 机器:Mac-mini-tei.lan。 +- 工作仓库:`/Users/tei/dev/MapLibreParticleLab-step084`,独立 worktree,避免触碰主 Mac 工作树既有 dirty/untracked。 +- branch:`step/084-particle-speed-color-default-off`。 +- start commit:`91d4859121afef98e05efac923797b31835a49df`(tag `maplibre-particle/rn/13.4.1-particle.7`)。 +- Integration repo 已同步并核对 `main`;本修正不改变 RN 公共 API、QBIN/Field contract 或 NavSea/Particle 责任边界。 + +## 3. 根因 + +正式 fragment/Metal shader 在 `speedColorEnabled=true` 时,会对当前粒子段覆盖 `particleColor`,使用内置速度色带;高速端为橙红色。`RasterParticleLayer::Impl` 当前默认值为 `true`,因此 Host 未显式设置该属性时,Android OpenGL 与 iOS Metal 都会出现同样的红头。 + +## 4. 必做项 + +- [x] 将正式 `RasterParticleLayer` 的 `speedColorEnabled` 默认值改为 `false`。 +- [x] 将 `ParticleGpuResources` 初始默认值同步为 `false`,避免资源首次创建后、视觉属性同步前出现单帧速度着色。 +- [x] 保留 shader 中速度着色逻辑和显式 setter,不删除 Validation/Test 能力。 +- [x] 更新 `Layer.RasterParticleVisualProperties` 回归测试:默认必须为 OFF,显式打开后仍可为 ON,再可关闭。 +- [x] 默认 OFF → 显式 ON → 再 OFF 的 Core Layer 回归语义已由定向测试源码断言覆盖;未运行测试二进制。 +- [x] `ParticleGpuResources` 初始值、RenderLayer visual-property 同步和 shader UBO 开关的静态调用链已复核。 +- [x] `git diff --check` 通过。 +- [x] scoped commit 并 push 到 `origin/particle/main`(commit 与远端确认见本轮最终交付)。 +- [ ] Mac Metal 精确编译、Android Native/AAR 编译与 Android/iOS 设备验收:`BUILD_NOT_RUN_BY_CODEX — WAITING_FOR_RDC`。任务书要求与 STEP-085 性能修复合并验收,故本 Step 不单独声称平台 PASS。 + +## 5. 禁止范围 + +- 不修改 QBIN V4、FieldProfile、Runtime Window、时间插值或天气数据。 +- 不增加 iOS/Android 专用公共协议,不增加 RN 平台分叉。 +- 不修改 NavSea。 +- 不删除 speed-color 能力;只改变正式层默认值。 +- 不执行 Android Gradle/AAR/APK 编译、安装或 ADB 操作;Android 构建仍由用户手工执行。 + +## 6. 验收语义 + +默认情况下 `current ? particleColor : trailColor` 选择完成后,不再进入 `particleSpeedColor(...)` 覆盖分支;只有调用方显式打开 speed-color 时才允许速度色覆盖当前粒子段。由于 OpenGL GLSL 与 Metal shader 共用同一 `speedColorEnabled`/UBO 语义,本修正应在重新构建各平台 Native 后同时生效。 + +## 7. Codex 静态收尾 + +- 远端 `origin/particle/main` 在本 Step 开始时与 start commit 一致;现有 dirty 仅含任务书列出的三处源码/测试修改及本 Step 记录,未混入 STEP-085 性能代码。 +- `RasterParticleLayer::Impl` 默认值与 `ParticleGpuResources` 的资源首帧默认值均为 `false`;显式 setter 及 shader speed-color 分支保持不变。 +- `Layer.RasterParticleVisualProperties` 明确覆盖 default OFF、set ON 与 set OFF,因此正式宿主未设属性时不再选择内置速度色带,诊断/显式 opt-in 仍然可用。 +- 未执行任何 Native 编译、链接、artifact、Gradle、ADB、iOS 安装或真机工作;这些项目由 RDC 在 STEP-085 后统一验收。 diff --git a/docs/50_Step记录/STEP-085_V4_Renderer性能修复_v1.md b/docs/50_Step记录/STEP-085_V4_Renderer性能修复_v1.md new file mode 100644 index 000000000..85a9c272f --- /dev/null +++ b/docs/50_Step记录/STEP-085_V4_Renderer性能修复_v1.md @@ -0,0 +1,35 @@ +# STEP-085 V4 Renderer 性能修复 v1 + +状态:`完成(BUILD_NOT_RUN_BY_CODEX — WAITING_FOR_RDC)` + +## 基线 + +- 机器:Mac-mini-tei.lan。 +- worktree:`/Users/tei/dev/MapLibreParticleLab-step085`。 +- branch:`step/085-v4-render-efficiency`。 +- STEP-085 Start HEAD:`bc6b8dd84b4c10e5263d61832661508c02d84d88`。 +- STEP-084 Final HEAD:`bc6b8dd84b4c10e5263d61832661508c02d84d88`,已进入 `origin/particle/main`。 + +## 本轮结果 + +- [x] V4 WeatherColor PAUSE/静态 window 不再因缺少 legacy timeline 永久 `needsRepaint()`。 +- [x] V4 Particle 使用 Source-owned backend-neutral cadence;30/60 gate simulation,AUTO 保持 animation-frame 语义。 +- [x] Forecast PAUSE 与 Particle visual simulation 解耦;camera redraw 未到 deadline 时只 composite。 +- [x] V4 Renderer prepare 改持 shared immutable snapshot handle,稳定 window 不深复制大数组。 +- [x] WeatherColor 稳定 wrap/resource 状态复用 drawable,mix/opacity/stops 由 tweaker UBO 更新。 +- [x] 加入 DEBUG/Validation cumulative counters。 +- [x] Android/iOS existing Runtime thin seam 为 V4 30/60 提供 deadline wake;无新 RN API 或平台专用 Core。 +- [x] 自审将 platform wake 收紧为“已绑定的 fieldKey 已收到 accepted V4 submit”;clear 非粒子 field 不会错误停止仍可运行的粒子,disable 后 re-enable 可恢复既有 V4 field 的 wake。 +- [x] 审查 `advanceForecastClock` production owner;当前未发现,已明确记录而未扩大重构。 +- [x] `git diff --check` 通过。 +- [ ] Native C++/Metal/Android 编译、链接、codegen、artifact、安装、设备和性能采样:`BUILD_NOT_RUN_BY_CODEX — WAITING_FOR_RDC`。 + +## 禁止范围核对 + +未修改 NavSea、provider/network、QBIN V4、FieldProfile/Binding 对外 contract、RN 公共 API、C-001、`detach()` 语义或项目总则。未新增第二套 runtime/timeline,也未引入平台专用 Particle Core。 + +## 已知风险与下一步 + +V4 ForecastClock PLAY 的自动生产推进仍无调用 owner,因此未声称 PLAY 自动推进已经修复。RDC 必须先按完成报告进行 Mac C++/Metal、Android Native/AAR、shader/build graph 编译;成功后用统一颜色+性能真机矩阵验证 30/60/AUTO、PAUSE、speed-color、camera 和 debug counters。 + +本 Step 只完成源码和静态自审;Android Java/Objective-C++ wake 状态调整尚未经过目标平台编译。 diff --git a/docs/50_Step记录/STEP-087_QBIN_V3解码与13.4.1-particle.9升级_v1.md b/docs/50_Step记录/STEP-087_QBIN_V3解码与13.4.1-particle.9升级_v1.md new file mode 100644 index 000000000..757c4e3e4 --- /dev/null +++ b/docs/50_Step记录/STEP-087_QBIN_V3解码与13.4.1-particle.9升级_v1.md @@ -0,0 +1,34 @@ +# STEP-087 QBIN V3 解码与 13.4.1-particle.9 升级 + +## 状态 + +CLOSED / PASS + +## 已完成 + +- 已将用户提供的《WeatherV2 QBIN V3 通用天气场二进制格式与 React 独立 Render Binding 设计 v2.2 最终冻结稿》保存到 `docs/30_设计/`,原文件内容与 SHA-256 保持一致。 +- 新增独立 `QbinV3Decoder`,生产 Block Geometry 冻结为 16×16。 +- `EncodedFieldIngress` 改为 QBV2 / QBV3 显式 magic 分流;QBV2 兼容链保留。 +- `RasterArraySource::submitEncodedField()` 改为统一走 `EncodedFieldIngress::decodeAndValidate()`。 +- CMake / Bazel Core 已注册 V3 decoder;CMake test 已注册 V3 decoder tests。 +- 新增 8 个 Current / Rain / Wave / Wind QBIN V3 Golden Fixtures、manifest 与中文校验脚本。 +- 新增 C++ 定向测试,覆盖 block boundary、Direction wrap、all-invalid、非最优 Rice-K、CRC/descriptor/validity corruption、Rice padding、残差越界、Node grid 与 ingress 分流。 +- Mac clang 定向构建并运行 8 个 V3 tests:8/8 PASS。 +- Native / RN 版本声明已统一升级到 `13.4.1-particle.9`。 + +## Block Geometry + +真实 8 帧 benchmark:16×16 数值 payload 为 2,470,419 bytes;32×32 为 2,514,288 bytes,32×32 增加约 1.7758%。32×32 的 decode 总时间仅减少约 2.67 ms / 8 帧,因此最终冻结 16×16。详见 `docs/30_设计/MapLibreParticle_QBIN_V3_Decoder与BlockGeometry实施决策_v1.md`。 + +## 最终验证与发布 + +- `verify-qbin-v3-golden-fixtures.py`:PASS(8 files,block=16×16)。 +- QBIN V3 C++ 定向 tests:PASS 8 / FAIL 0。 +- Android Release 四 ABI:armeabi-v7a、arm64-v8a、x86、x86_64 全部构建通过。 +- iOS Metal `MapLibre.dynamic`:PASS;device-arm64 XCFramework 已生成并发布。 +- RN tgz:已生成并发布。 +- Android Maven / RN tgz / iOS XCFramework 公网 readback:PASS。 +- SwiftPM `6.28.0-particle.9`:已发布。 +- Release tag:`maplibre-particle/rn/13.4.1-particle.9`。 +- Release Final HEAD:`63047540cba703c8efeae0df71e759e165315f85`。 +- Android APK / ADB 仍按项目规则由用户手工执行,不属于本次自动发布门槛。 diff --git a/docs/50_Step记录/STEP-088_静态底图缓存PoC与Renderer观测_v1.md b/docs/50_Step记录/STEP-088_静态底图缓存PoC与Renderer观测_v1.md new file mode 100644 index 000000000..08ec1e5fa --- /dev/null +++ b/docs/50_Step记录/STEP-088_静态底图缓存PoC与Renderer观测_v1.md @@ -0,0 +1,122 @@ +# STEP-088 静态底图缓存 PoC 与 Renderer 观测 v1 + +## 状态 + +进行中 + +`IMPLEMENTED / WAITING FOR ANDROID VALIDATION` + +## 目标 + +在固定 Camera、Style/Tile 稳定、2D 且存在 WeatherColor/RasterParticle 动态层时,把动态边界之前的连续普通 LayerGroup 前缀缓存为 retained underlay。Particle Tick 仍进入现有 MapLibre update/createRenderTree/prepare/upload 主链,但绘制阶段复用静态底图纹理,只实时绘制动态边界及其上方内容。 + +## 基线 + +- 分支:`particle/main` +- 起始 HEAD:`f47469d2ab77cdb959ecfe4081dc571fa1ab78f9` +- 实现提交:`8b9ab303228c307612c47f7f3379ad06206ea14e` +- 当前版本坐标:`13.4.1-particle.10` + +## 必做项 + +- [x] Renderer-internal dynamic boundary。 +- [x] LayerGroup before/from、forward/reverse 与 original ordinal。 +- [x] Renderer-owned underlay cache 与 OpenGL/Metal compositor。 +- [x] full-render fallback 与 legacy ordering 保护。 +- [x] 7 个 Renderer debug counters。 +- [x] Native 定向测试代码与 CMake 注册。 +- [x] Mac OpenGL/Metal 编译及 4/4 定向测试由 RDC 完成。 +- [ ] Android/NavSea 实机视觉、counters 与性能验证。 + +## 允许与禁止范围 + +允许修改 Renderer/Core/gfx backend/shader/CMake/Native test 和本 Step 文档;必要的 `test/gl/gl_functions.test.cpp` macOS 平台条件小修纳入本次。禁止 Android/NavSea/React 业务修改、Host API、C-001、STEP-089、STEP-090、production 全量 invalidation 与 animation-only fast path。 + +## 当前问题 + +STEP-086 已确认 Particle/WeatherColor 的 `needsRepaint()` 会唤醒完整 MapLibre frame。固定 Camera 下仍重复执行 source/layer orchestration、prepare、LayerGroup opaque/translucent draw 与 present;复杂 NavSea Style 因而持续重复提交大量静态 fill/line/symbol/PBF draw。 + +## 实现结构 + +- `RenderLayer` 新增仅 Renderer 内部使用的 `startsRetainedDynamicRange()`;WeatherColor 与 RasterParticle 返回 true,不增加 Host/public API。 +- `RenderOrchestrator` 保存最小动态 boundary,并提供 before/from、forward/reverse 的 LayerGroup 分段 visitor;callback 保留完整序列 original ordinal。 +- Renderer-owned `RetainedUnderlayCache` 持有 RGBA offscreen texture、depth/stencil renderable、compositor LayerGroup、key、valid 状态和 build/reuse/fallback/invalidation 数据。 +- 新增独立 `RetainedUnderlayCompositeShader`,OpenGL/Metal 均为 fullscreen textured quad,只采样 premultiplied RGBA,不做 map transform、color ramp 或 Particle 业务。 +- OpenGL OffscreenTexture 补齐可选 depth/stencil attachment;通用 gfx Context 的四参数 offscreen 接口由 OpenGL、Metal、Vulkan、WebGPU 后端显式覆盖。 +- legacy `RenderLayer::render()` 当前仅 LocationIndicator 需要保护;若它位于 boundary 以下则 full-render fallback。 + +## Boundary + +- WeatherColor 与 RasterParticle 都是 candidate。 +- 两者同时存在时取实际 ordered layer 序列中的最小 index。 +- only Particle 时使用 Particle index。 +- 无 candidate 时 retained path disabled,完整原 Renderer 路径不变。 +- 不依赖 React layer ID/name,不由 Host/NavSea 传入。 + +## Underlay Cache Flow + +1. 保留 `Renderer::render → createRenderTree`、source update/prepare、layer prepare、`updateLayers/processChanges`、LayerGroup tweaker/upload。 +2. 先按原顺序处理 independent RenderTargets。 +3. cache invalid 时,把 boundary 前 LayerGroups 以原始 ordinal/depth 映射分别执行 opaque reverse 与 translucent forward,绘制到透明 clear 的 offscreen。 +4. main framebuffer 仍按原 `backgroundColor/depth/stencil` 语义 clear;background optimized-as-color 路径未改。 +5. composite cached underlay,再按完整序列 original ordinal 绘制 boundary 及之后的 opaque/translucent groups。 +6. legacy items、debug overlay、postRender 与 present 保持原位置。 +7. fixed key 且安全条件连续成立时复用;viewport、camera matrix 或 boundary 改变时重新 build。 + +## Fallback + +以下状态不复用缓存:无 boundary、静态前缀为空、style 尚未 loaded、3D、任意 debug options、静态层 transition/repaint、placement/fade、boundary 以下 legacy ordering、offscreen/compositor 创建失败。不能建立安全资源时计数并回到完整绘制路径。 + +## Renderer 观测 + +在既有 debug-only counters 中新增: + +- `FullRendererFrame` +- `RetainedUnderlayBuild` +- `RetainedUnderlayReuse` +- `RetainedUnderlayFallback` +- `RetainedUnderlayInvalidation` +- `RetainedUnderlayStaticGroupDraw` +- `RetainedDynamicGroupDraw` + +`NDEBUG` 下仍为 no-op;未增加 Host API,也未增加逐帧日志。 + +## 构建与测试 + +以下由 RDC / 普通 Terminal 在当前 STEP-088 实现上完成: + +- `cmake --build build-step088-opengl --target mbgl-test-runner -j3`:成功。 +- STEP-088 定向测试:4/4 成功,覆盖 boundary、LayerGroup range/original ordinal、build/reuse/resize/boundary invalidation、3D/resource failure fallback。 +- `cmake --build build-ios --config Debug --target mbgl-core -j3`:iOS/Metal device arm64 `BUILD SUCCEEDED`。 +- `git diff --check`:成功。 +- RDC 为 Mac OpenGL test-runner 链接补充 `test/gl/gl_functions.test.cpp` Apple 平台条件:macOS 不引用仅 iPhone/Simulator 提供的 `glInvalidateFramebuffer` / `glInvalidateSubFramebuffer`。 + +Codex 收尾阶段按新规则未再启动任何 build/test。Android Gradle、AAR/APK、ADB、Android 设备、NavSea 实机与 Jest 均未执行。 + +## PoC 限制 + +- 本 Step 只消除稳定场景下静态前缀的重复 GPU draw;没有跳过 Map update、createRenderTree、source/TilePyramid update、prepare、tweaker 或 upload。 +- production 级 style/layer/source/tile/bucket/drawable、sprite/image/glyph、context/surface 等完整 invalidation 属于 STEP-089;本 PoC 只允许在固定 Camera、Style/Tile 稳定条件下判断 reuse。 +- 未做 animation-only CPU fast path;STEP-090 未执行。 +- 未做通用任意 static/dynamic RenderGraph,也未修改 NavSea、React 或 C-001。 + +## 用户 Android/NavSea 手工验证项 + +1. 固定 Camera、复杂真实 Style、Particle 约 30 FPS,确认视觉与原路径一致。 +2. 观察 `RetainedUnderlayBuild ≈ 1`、`RetainedUnderlayReuse` 和 `ParticleSimulationAdvance` 持续增长。 +3. 确认建 cache 后 `RetainedUnderlayStaticGroupDraw` 不随 Particle FPS 增长,`RetainedDynamicGroupDraw` 持续增长。 +4. 分别验证 only Particle、WeatherColor + Particle 的两种 layer 顺序。 +5. pan/zoom/resize 后确认 invalidation/rebuild,不能出现 stale underlay。 +6. 验证 3D、debug 与 boundary 以下 legacy ordering 会安全回到 full render。 +7. 对比 Particle OFF/ON 的 draw calls、RenderThread CPU、main/JS CPU 与画面叠放顺序。 + +## 当前结果与下一步 + +STEP-088 实现与 Mac OpenGL/Metal、Native 定向测试已完成;Android/NavSea 运行态证据尚缺。因此当前不满足进入 STEP-089 的验收条件,不得进入 STEP-089/090。下一步仅由用户/RDC 完成上述 Android/NavSea 手工验证并回传事实。 + +## 必做输出与阻塞 + +- Step 记录:本文。 +- Codex 完成报告:`docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v1.md`。 +- 开发记录:已追加 `粒子开发记录.md`。 +- 阻塞:Android/NavSea 实机验证尚未执行;这是关闭 STEP-088 和进入 STEP-089 的唯一当前验收阻塞。 diff --git a/docs/50_Step记录/STEP-088_静态底图缓存PoC与Renderer观测_v2.md b/docs/50_Step记录/STEP-088_静态底图缓存PoC与Renderer观测_v2.md new file mode 100644 index 000000000..ac4e69ab0 --- /dev/null +++ b/docs/50_Step记录/STEP-088_静态底图缓存PoC与Renderer观测_v2.md @@ -0,0 +1,139 @@ +# STEP-088 静态底图缓存 PoC 与 Renderer 观测 v2 + +## 状态 + +进行中 + +`ANDROID TEST APP PASS / WAITING FOR NAVSEA VALIDATION` + +STEP-088 尚未关闭,也不得据此进入 STEP-089/090。 + +## 本版目的 + +本文在 v1 实现记录基础上,记录 Android MapLibreAndroidTestApp 实机发现的 placement fallback 问题、Renderer 修正、Mac 最终复验和 Android Test App 运行态验收。v1 中的 retained underlay 架构、范围和 PoC 限制继续有效。 + +## 基线 + +- 仓库:`/Users/tei/dev/MapLibreParticleLab` +- 分支:`particle/main` +- v1 实现提交:`8b9ab303228c307612c47f7f3379ad06206ea14e` +- debug counters 日志提交:`e7d109246a225eb1c1b2f177aa30e3c7fcdf658e` +- Android placement-scoping 修正提交:`1d1a7915e0886c46be40f4fc9a2eff13192f804b` +- 当前版本坐标:`13.4.1-particle.10` + +## Android 首轮问题 + +较早安装的 STEP-088 APK 在 fixed camera / only Particle 场景持续 full-render fallback: + +```text +frame=1200 build=0 reuse=0 fallback=1199 invalidation=1199 +staticDraw=0 dynamicDraw=0 particleAdvance=1199 +``` + +根因是 retained eligibility 无条件受全局 placement transition、`placementChanged` 和 `symbolFadeChange` 约束。即使 retained static prefix 没有 symbol/placement layer,动态范围或全局 placement 状态也会阻止 cache build/reuse。 + +## Renderer 修正 + +- `RenderOrchestrator::retainedUnderlayUsesPlacement(boundary)` 只检查 boundary 之前的 retained static prefix。 +- static prefix 不使用 placement 时,全局 placement transition、`placementChanged` 和 `symbolFadeChange` 不再阻止 retained cache。 +- static prefix 真正使用 placement 时,上述 placement stability 保护仍然生效并安全 fallback。 +- 原有 3D、debug、legacy ordering、style-not-loaded、static transition、resource/compositor failure 等 fallback 不变。 +- debug-only `ParticleRetained` 周期日志保留 `reason=`,用于区分 `first-frame`、`camera-or-boundary`、`style-not-loaded`、`static-transition`、`static-placement` 等原因;Release/NDEBUG 语义不变。 + +## Android Validation 构建辅助 + +`platform/android/MapLibreAndroidTestApp/build.gradle.kts` 新增测试专用 property: + +```text +maplibre.validation.stripNativeDebugSymbols=true +``` + +默认 Debug 行为不变,仍保留 native debug symbols。只有 Validation 构建显式启用该 property 时才允许 strip native symbols;x86_64 Debug APK 由约 375 MB 降至约 57 MB。该改动不改变 Renderer、debug counters 或 `NDEBUG` 语义。 + +## RDC Mac 最终复验 + +- `cmake --build build-step088-opengl --target mbgl-test-runner -j3`:PASS / no work to do;RDC 已确认当前 placement-scoping 源码已编入 object。 +- STEP-088 定向测试:4/4 PASS: + - `RetainedDynamicBoundary.SelectsFirstCandidate` + - `RetainedLayerGroupRange.PreservesForwardReverseOriginalOrdinals` + - `RetainedUnderlayCache.BuildReuseAndInvalidation` + - `RetainedUnderlayCache.UnsupportedAndResourceFailureFallBackSafely` +- `cmake --build build-ios --config Debug --target mbgl-core -j3`:`BUILD SUCCEEDED`。 +- `git diff --check`:PASS。 + +上述命令均由 RDC 执行;Codex 文档与 Git 收尾阶段未重新执行 build/test。 + +## Android Test App 环境 + +- 设备:`192.168.200.139:5555` +- 设备标识:FZG1-2 +- 系统:Android 13 +- ABI:x86_64 +- App:MapLibreAndroidTestApp OpenGL Debug x86_64 +- Validation strip property:启用 +- 构建、安装、ADB 与 Activity 启动:均成功 + +## Android Test App 运行态结果 + +### Fixed camera / only Particle + +```text +frame=840 build=1 reuse=836 fallback=2 invalidation=3 +reason=first-frame staticDraw=2 dynamicDraw=5022 particleAdvance=839 +``` + +后续 `reuse` 持续增长,`staticDraw` 保持不变,证明 retained static prefix 首次 build 后被复用。 + +### Camera DATELINE + +- 变化前:`build=1`、`staticDraw=2`。 +- camera transition 后:`build=2`、`staticDraw=4`。 +- 随后 `reuse` 重新持续增长。 +- 观测原因:`reason=camera-or-boundary`。 + +结果符合 camera change → transition 期间安全 fallback/invalidate → 稳定后 rebuild 一次 → reuse。 + +### REAL WEATHER + +- `REAL_WEATHER_INITIAL_WINDOW_READY` 正常。 +- `WEATHER_COLOR_FIELD_READY` 正常。 +- GSI raster + WeatherColor + Particle 正常进入 retained path。 +- 代表数据:`frame=6960 build=3 reuse=6907 fallback=49 invalidation=52 staticDraw=6 particleAdvance=6959`。 +- fixed camera 后 `reuse` 持续增长,`staticDraw` 保持 6。 +- `TARGET_FPS_CHANGED target=FPS_30` 成功,缓存继续 reuse。 + +### Resize / rotation + +viewport 完成 `1280x680 → 800x1160 → 1280x680`。过渡期间出现安全的 `style-not-loaded` fallback;稳定后 rebuild 并继续 reuse。设备最终恢复 `accelerometer_rotation=1`、`user_rotation=0`。 + +### STATIC / ACTIVE + +- STATIC 时 RenderThread 约 0%。 +- ACTIVE 后恢复 continuous render,retained reuse 继续增长且 `staticDraw` 保持不变。 +- 旧 Test App 设备上 ACTIVE RenderThread 约 96%~107%,仅作为 Test App 对照,不代表 NavSea 最终性能。 + +### 异常扫描 + +未发现 `FATAL EXCEPTION`、`SIGSEGV`、`SIGABRT`、ANR、`GL_INVALID`、`EGL_BAD` 或 OOM。 + +## 当前验收边界 + +Android MapLibreAndroidTestApp 运行态验证为 PASS,但它不能替代 NavSea 复杂真实 Style 的最终验证。以下仍待 NavSea 完成: + +1. 真实复杂 Style 的视觉与 layer ordering。 +2. 大量 PBF/fill/line/symbol 下 retained static prefix 的 `staticDraw` 稳定性。 +3. Particle OFF/ON 的 RenderThread、main、JS CPU 对比。 +4. pan/zoom/resize/fallback 后无 stale underlay。 +5. only Particle 与 WeatherColor + Particle 在真实产品 layer stack 中的一致性。 + +## PoC 边界 + +- 未实现 STEP-089 production 全量 invalidation。 +- 未实现 STEP-090 animation-only CPU fast path。 +- 未修改 NavSea、React、Host API 或 C-001。 +- Android Test App PASS 不构成 STEP-088 overall CLOSED/PASS。 +- NavSea 验证完成前不得进入 STEP-089/090。 + +## 当前结论 + +placement-scoping 修正解决了 Android 上 static prefix 不含 placement 时的每帧 fallback。Mac OpenGL/Metal、Native 定向测试及 Android Test App 运行态均已通过;STEP-088 当前唯一剩余 Gate 是 NavSea 复杂真实 Style 验证。 diff --git a/docs/50_Step记录/STEP-088_静态底图缓存PoC与Renderer观测_v3.md b/docs/50_Step记录/STEP-088_静态底图缓存PoC与Renderer观测_v3.md new file mode 100644 index 000000000..2db3c371d --- /dev/null +++ b/docs/50_Step记录/STEP-088_静态底图缓存PoC与Renderer观测_v3.md @@ -0,0 +1,65 @@ +# STEP-088 静态底图缓存 PoC 与 Renderer 观测 v3 + +## 状态 + +**ANDROID / NATIVE VALIDATION PASS;STEP-088 收尾。** + +STEP-088 已验证 retained underlay 能停止静态 LayerGroup 的重复 GPU draw,并补充了 Release 可用的运行时 Cache ON/OFF、最小诊断接口、Android/iOS Native API、React Native RendererRuntime,以及 Android Test App 的真实 NavSea PBF 性能基线工具。 + +## 本轮新增能力 + +- Retained Underlay 默认开启,可按 Map 实例运行时开启/关闭。 +- 关闭时立即失效缓存并恢复原始 full-render;重新开启后在满足条件时重新 build/reuse。 +- Release 保留最小 pull-based diagnostics:enabled/build/reuse/fallback/invalidation/lastReason。 +- Debug 继续保留详细 ParticleRetained 周期日志。 +- RN 新增 RendererRuntime: + - setRetainedUnderlayEnabled(reactTag, enabled) + - getRetainedUnderlayDiagnostics(reactTag) +- Android Test App 新增: + - NavSea 正式 63 层 PBF Style; + - Particle 位置 BOTTOM / AFTER_SEA / AFTER_CLIP / BEFORE_SYMBOL_CLUSTER / TOP; + - runtime Cache ON/OFF; + - PBF ON/OFF; + - PURE PARTICLE; + - PURE PBF; + - CUSTOM PARTICLE COUNT,范围 1..10000。 + +## 关键 Android 性能证据 + +测试设备:Android 13 / x86_64 / FZG1-2,固定唐津 Camera,Particle target FPS 30。 + +- PURE PARTICLE,2048 粒子,无 PBF: + - RenderThread ≈ 10.2% + - Process ≈ 13.7% + - Main ≈ 2.5% +- PURE PBF,63 层正式 Style,无 Particle: + - RenderThread ≈ 0% + - Process ≈ 0% +- NavSea Style 保留、60 个 navsea_delivery layer 全隐藏、Particle 2048: + - RenderThread ≈ 25.5% + - Process ≈ 29.1% +- PBF + Particle,AFTER_CLIP: + - Cache ON RenderThread ≈ 96.5%,Process ≈ 102.2% + - Cache OFF RenderThread ≈ 99.5%,Process ≈ 111.8% +- PBF + Particle,BEFORE_SYMBOL_CLUSTER: + - Cache ON RenderThread ≈ 96.5%,Process ≈ 102.8% + - Cache OFF RenderThread ≈ 100.2%,Process ≈ 114.2% +- TOP 因 static-placement/static-transition 持续 fallback,不能作为稳定 retained 场景。 + +结论:STEP-088 能消除静态 GPU draw,但主要 CPU 成本仍来自 Particle continuous repaint 下 PBF/vector/static renderer pipeline 被反复处理。该证据支持下一阶段先做 STEP-089 invalidation/revision 基础,再进入 STEP-090 Static CPU Fast Path。 + +## 验证结果 + +- Android x86_64 stripped Debug Validation APK:BUILD SUCCESSFUL,约 57 MB,实机通过。 +- Mac OpenGL mbgl-test-runner:PASS。 +- STEP-088 directed tests:5/5 PASS。 +- iOS Metal mbgl-core:BUILD SUCCEEDED。 +- iOS ios-sdk-static:BUILD SUCCEEDED。 +- RN package TypeScript:PASS。 +- RN Test Host TypeScript:PASS。 +- RN iOS bridge 单独编译:被 Test Host 仍引用旧 STEP-083 本地 SwiftPM 包阻塞;旧包缺少本轮新增 MLNMapView retained-underlay API。当前源码 iOS SDK 已通过编译,因此该项记录为测试宿主产物过期,不阻塞 STEP-089。 +- git diff --check:PASS。 + +## 下一步 + +进入 STEP-089:建立静态内容 invalidation/revision 可靠判据,只做 fast-path 前置安全基础,不提前实现 STEP-090 的 CPU fast path。 diff --git a/docs/50_Step记录/STEP-089_静态渲染Revision与Invalidation基础层_v1.md b/docs/50_Step记录/STEP-089_静态渲染Revision与Invalidation基础层_v1.md new file mode 100644 index 000000000..52ee2805c --- /dev/null +++ b/docs/50_Step记录/STEP-089_静态渲染Revision与Invalidation基础层_v1.md @@ -0,0 +1,117 @@ +# STEP-089 静态渲染 Revision 与 Invalidation 基础层 v1 + +## 状态 + +**CLOSED / PASS** + +STEP-089 已完成 retained underlay 的静态状态 revision / invalidation 基础层,为 STEP-090 Static CPU Fast Path 提供可靠的安全门控。 + +## 核心实现 + +- 新增单调递增的静态 revision / generation。 +- retained cache build 时记录 buildRevision。 +- reuse 前要求 buildRevision 与当前 staticRevision 一致。 +- 新增稳定 topology snapshot: + - dynamic boundary; + - static prefix 中每个 LayerGroup 的 layerIndex; + - groupName; + - groupType。 +- 真正的 LayerGroup 增删、排序、index 变化会推进 topologyRevision。 +- Renderer 内部 transient drawable churn 不再直接推进 static revision。 +- 等价的 stencil tile set 不因 shared_ptr identity 变化而误失效。 +- 支持并记录以下语义失效来源: + - camera / viewport / pixel ratio; + - style / layer / source mutation; + - tile content change; + - image / sprite / glyph; + - placement / fade; + - context reset; + - debug options; + - 3D / unsupported custom/static layer。 +- unsupported / uncertain 场景继续 fail-closed。 +- STEP-088 runtime Cache ON/OFF 行为保持不变。 +- 未实现 STEP-090 CPU fast path。 + +## Android 实机关键验证 + +测试场景: +- Android 13 / x86_64; +- NavSea 正式 63 层 PBF Style; +- Particle = AFTER_CLIP; +- 固定 Camera:Karatsu; +- Cache ON。 + +### 稳态 + +修复前曾出现: +- drawable-mutation 每帧失效; +- layer-group-mutation 每帧失效; +- staticRevision 每帧递增; +- build 每帧递增; +- reuse=0。 + +最终版本: + +```text +frame=480 build=2 reuse=302 staticRevision=21 buildRevision=21 +frame=600 build=2 reuse=422 staticRevision=21 buildRevision=21 +frame=720 build=2 reuse=542 staticRevision=21 buildRevision=21 +``` + +确认: +- staticRevision 稳定; +- buildRevision == staticRevision; +- build 不再增长; +- reuse 持续增长。 + +### Camera 变化 + +相机变化后: +- staticRevision: 41 -> 44; +- build: 18 -> 19; +- buildRevision = 44; + +随后稳定: + +```text +build=19 +reuse=3519 -> 3639 -> 3759 -> 3879 +staticRevision=44 +buildRevision=44 +``` + +确认真实变化会使 revision 前进并重建一次,稳定后恢复 reuse。 + +## Native 测试 + +Retained directed tests:12/12 PASS。 + +覆盖: +- dynamic boundary; +- layer group range; +- cache build/reuse/invalidation; +- runtime Cache ON/OFF; +- revision 单调; +- 无语义变化时 revision 不前进; +- 等价 intra-frame remove/add 不推进 topology; +- 持久 topology 变化只推进一次; +- ordering/index 变化推进 topology; +- buildRevision 匹配才允许 reuse; +- placement/fade 活跃时拒绝 stale cache。 + +## 构建验证 + +- Mac OpenGL mbgl-test-runner:PASS。 +- Metal/iOS mbgl-core:BUILD SUCCEEDED。 +- Android x86_64 stripped Debug Validation APK:BUILD SUCCESSFUL,约 57 MB。 +- Android 实机稳态 reuse:PASS。 +- Android Camera invalidation -> rebuild -> reuse:PASS。 +- git diff --check:PASS。 + +## 结论 + +STEP-089 已建立可信的 static revision gate。后续 STEP-090 可以在该 gate 为稳定状态时尝试跳过静态 PBF/vector CPU processing;089 本身不做 CPU fast path。 + +## 下一步 + +进入 STEP-090:Static CPU Fast Path。 diff --git a/docs/50_Step记录/STEP-090_Particle-only_Static_CPU_Fast_Path_v1.md b/docs/50_Step记录/STEP-090_Particle-only_Static_CPU_Fast_Path_v1.md new file mode 100644 index 000000000..6c966f606 --- /dev/null +++ b/docs/50_Step记录/STEP-090_Particle-only_Static_CPU_Fast_Path_v1.md @@ -0,0 +1,231 @@ +# STEP-090 Particle-only Static CPU Fast Path v1 + +## 状态 + +**CLOSED / PASS** + +STEP-090 已完成 Particle-only Static CPU Fast Path。该实现建立在 STEP-088 retained underlay GPU cache 与 STEP-089 static revision/invalidation gate 之上,在满足严格安全条件时,于 `Renderer::render()` 的 `createRenderTree()` 之前命中 fast path,复用上一帧已 prepare 的 RenderTree,并跳过静态 PBF/vector 的完整 CPU pipeline。 + +## 核心实现 + +- fast-path gate 位于 `Renderer::render()`,早于 `RenderOrchestrator::createRenderTree()`。 +- full frame 成功后缓存 prepared RenderTree。 +- fast frame 命中时: + - 不重新 createRenderTree; + - 不执行完整 RenderTree::prepare; + - 不执行 full static layer update; + - 只更新 Particle 动态 layer; + - 静态 underlay 继续使用 retained cache; + - 保持 Particle simulation / draw / present 正常运行。 +- full path 创建新 RenderTree 前先清除旧 prepared tree,避免引用旧 RenderLayer/RenderSource 生命周期。 +- context lost / clearData / reduceMemoryUse / style reload 时清除 prepared tree。 +- 没有新增 Host/RN 手工 fast-frame API;仍使用已有 retainedUnderlayEnabled 作为整体优化 ON/OFF A/B 开关。 + +## Particle-only 安全 gate + +只有下列条件同时满足才允许 fast path: + +- prepared RenderTree 存在; +- retained runtime enabled; +- retained cache valid; +- static/build/content/topology revision 一致; +- Camera / viewport / pixelRatio 一致; +- UpdateParameters 的非时间语义输入一致; +- style loaded; +- dynamic boundary 一致; +- static prefix stable; +- 当前 placement/fade transition 已结束; +- 无 pending renderer changes; +- 无 non-particle transition; +- 无 non-particle needsRepaint; +- 无 fading tiles; +- Particle 当前确实 needsRepaint; +- 非 still-image request; +- Continuous MapMode; +- debug rendering disabled; +- 当前场景支持 fast path; +- annotation 无 pending mutation。 + +任一不满足均立即走 full path。 + +## placement/fade 修正 + +首轮实机显示 fast path 永久被 `placement-or-fade` 阻止。调查确认 prepared RenderTree 上一 full frame 的 `placementChanged / symbolFadeChange` 是历史帧事件状态,不应永久阻止后续已稳定帧。 + +最终改为以当前 `placementController.hasTransitions(timePoint)` 为权威判据: + +- 当前 placement/fade transition 活跃:full path; +- transition 结束且 STEP-089 revision / camera / tile / style 等语义 gate 均稳定:允许 fast path。 + +这保留了 active symbol fade/placement 的安全回退,同时避免上一帧 placement pass 永久锁死 fast path。 + +## Android 性能实测 + +设备:Android 13 / x86_64 / FZG1-2。 + +场景:NavSea 正式 63 层 PBF Style,Particle=2048。 + +### AFTER_CLIP + +FastPath ON: + +```text +PROC_CPU ≈ 27.7% +MAIN_CPU ≈ 2.9% +RENDER_CPU ≈ 21.7% +``` + +同 APK、同场景、Cache/FastPath OFF: + +```text +PROC_CPU ≈ 111.4% +MAIN_CPU ≈ 2.8% +RENDER_CPU ≈ 97.0% +``` + +RenderThread 从约 97% 降至约 21.7%。 + +稳定日志: + +```text +fastHit 持续增长 +fastMiss 固定 +particleFull 固定 +fastReason=none +buildRevision == staticRevision +retained build 固定 +retained reuse 持续增长 +``` + +代表日志: + +```text +frame=2880 ... fastHit=2670 fastMiss=210 particleFull=210 fastReason=none +frame=3000 ... fastHit=2790 fastMiss=210 particleFull=210 fastReason=none +frame=3240 ... fastHit=3030 fastMiss=210 particleFull=210 fastReason=none +``` + +### BEFORE_SYMBOL_CLUSTER + +稳定后 fast path 命中。 + +```text +PROC_CPU ≈ 26.3% +MAIN_CPU ≈ 2.7% +RENDER_CPU ≈ 20.3% +``` + +说明优化不依赖单一 AFTER_CLIP 边界。 + +## 安全回退验证 + +### Cache OFF / ON + +Cache OFF: +- enabled=false; +- fastHit 停止增长; +- fastMiss / particleFull 持续增长; +- fastReason=retained-disabled; +- RenderThread 回到约 97%。 + +重新 Cache ON: +- cache rebuild 一次; +- buildRevision 恢复匹配; +- fastReason=none; +- fastHit 恢复持续增长。 + +### Camera pan + +pan 前: +- staticRevision/buildRevision 稳定; +- particleFull 固定; +- fastHit 持续增长。 + +pan 时: +- revision 前进; +- build / invalidation / fastMiss / particleFull 增加; +- full path 正常执行。 + +稳定后: +- build 固定; +- fastHit 恢复持续增长; +- particleFull 再次固定。 + +### TOP symbol-heavy + +TOP 将全部 63 层置于 Particle 下方。 + +实测: +- retained buildRevision=0; +- reason=static-transition / static-placement; +- fastHit 不增长; +- fastMiss / particleFull 持续增长; +- 保守 full path 生效。 + +说明 symbol-heavy static prefix 未被错误复用。 + +### Viewport / orientation + +原始设备设置: +- accelerometer_rotation=1; +- user_rotation=0。 + +强制旋转后: +- style/cache invalid; +- full path; +- 新 RenderThread 创建; +- cache rebuild; +- 随后 fastReason=none,fastHit 恢复。 + +恢复原始旋转设置后再次经历同样的 full → rebuild → fast-hit 恢复流程。 + +设备设置最终已恢复: +- accelerometer_rotation=1; +- user_rotation=0。 + +## 2026-09-23 长时间运行异常复测 + +此前长时间实机运行后,2026-09-23 08:23:27 曾出现一次: + +`ActivityTaskManager: Force removing ActivityRecord{...ParticleCoreLayerValidationActivity}: app died, no saved state`。 + +当时 frame counter 已约 1,868,000,fast path 长时间持续正常;同期未检出 `FATAL EXCEPTION`、`Fatal signal`、`SIGSEGV`、`SIGABRT`、ANR、OOM、`GL_INVALID` 或 `EGL_BAD`。因此该事件没有足够证据归因于 STEP-090 fast path,也不能据此判定具体 crash root cause。 + +按收尾要求重新启动 Test App,并进入 `NAVSEA PBF / AFTER_CLIP / CACHE ON` 后连续复测约 3 分钟: + +- PID 始终为 29295,Activity 始终保持 top resumed; +- `build=2`,`staticRevision=18`,`buildRevision=18`; +- `fastReason=none`; +- `particleFull=218` 全程固定; +- `fastHit` 从约 20662 持续增长到 33622; +- 6 个连续 30 秒 RenderThread 样本为 21.2% / 21.3% / 21.5% / 21.7% / 21.8% / 22.0%; +- 复测结束后再次扫描 FATAL / signal / ANR / GL / EGL / OOM / app died 关键字,均未发现异常。 + +结论:08:23 的 `app died` 记录为**长时间单次异常,当前 3 分钟短时稳定性复测未复现**。该异常不阻塞 STEP-090 关闭,但后续如安排长时间 soak,应继续观察。 + +## 构建与测试 + +- git diff --check:PASS。 +- Mac OpenGL mbgl-test-runner:PASS。 +- retained + ParticleFastPath directed tests:21/21 PASS。 +- Android x86_64 stripped Debug Validation APK:BUILD SUCCESSFUL,约 57 MB。 +- Android 安装:PASS。 +- Android A/B CPU:PASS。 +- Camera / Cache / TOP / viewport 回退:PASS。 +- 异常日志扫描: + - 无 FATAL EXCEPTION; + - 无 ANR; + - 无 SIGSEGV/SIGABRT; + - 无 GL_INVALID / EGL_BAD; + - 无 OOM。 +- Metal/iOS mbgl-core:BUILD SUCCEEDED。 +- 临时 ParticleFastPathLife / ParticleFastPathDecision 日志:已移除。 +- 未新增 Host/RN fast-frame API。 + +## 结论 + +STEP-090 达成目标:Particle continuous repaint 不再强制静态 PBF/vector 每帧重新执行完整 CPU render-tree/source/layer pipeline。 + +在真实 NavSea PBF 测试场景中,RenderThread CPU 从约 97% 降到约 20~22%,已经接近此前 PURE PARTICLE / PBF-hidden 基线级别。 + +下一阶段不应立即扩展成通用多动态层调度;应先基于当前 Android/iOS 实际集成结果决定是否需要新的 profiling / production hardening 阶段。 diff --git a/docs/50_Step记录/STEP-091_ZoomOut_Viewport_Particle_Reseed_v1.md b/docs/50_Step记录/STEP-091_ZoomOut_Viewport_Particle_Reseed_v1.md new file mode 100644 index 000000000..d5af2fa26 --- /dev/null +++ b/docs/50_Step记录/STEP-091_ZoomOut_Viewport_Particle_Reseed_v1.md @@ -0,0 +1,53 @@ +# STEP-091 Zoom Out Viewport Particle Reseed v1 + +## 状态 + +**完成(Codex)/ 等待 RDC Android 实机验收** + +## 基线 + +- 分支:`step091-zoom-out-reseed` +- Starting HEAD:`297b9126200cea3da3bdef2faacedcd9845f6d08` +- Implementation commit:`4286c16a53d4f49001f3b41ed4638d3c6edc95b3` +- STEP-090:`CLOSED / PASS`,retained underlay 与 Particle-only CPU fast path 未修改。 + +## 实现结果 + +- camera invalidation 首帧保存一次稳定 `startZoom`,连续 camera 变化不覆盖。 +- 延用 150 ms camera stable debounce;稳定后先计算当前 `viewportRespawnDomain`、`viewportDensityActive` 与新 `respawnBounds`。 +- 当且仅当 `endZoom < startZoom - 0.05` 且 viewport domain 有效时,一次性设置 `stateSeeded=false`、`trailSeeded=false`。下一次既有 State/Trail pass 使用新的 `respawnBounds` 完成全体 State initial seed 与 Trail 清理。 +- `0.05` 仅过滤 sub-step 浮点/相机抖动,明显小于一次有意 pinch 或 zoom level 变化。 +- Zoom In、Pan、Rotate、Pitch 以及 epsilon 内抖动不触发 State reseed。 +- Zoom Out 后 domain fallback 时,本轮请求立即消费并记录 `INVALID_RESPAWN_DOMAIN`,不会留到后续 Pan。 +- `resetSequence()` 会取消尚未稳定消费的 gesture 状态,避免真实 simulation reset 后残留请求。 + +## 边界保持 + +- 未调用 Zoom Out `resetSequence()`。 +- 未重建 State/Trail GPU resource,未重新上传 Field,未 reset Timeline/T0/T1/MIX。 +- 未修改 shader、UBO layout、公共 Layer/Host/RN API。 +- 未修改 WeatherColor/RasterParticle 分层、Runtime Field Window 或 continuous cadence。 +- 未修改 STEP-089 revision/invalidation 与 STEP-090 fast-path gate;camera 变化期间既有 full-path、稳定后 fast-hit 恢复语义保持。 + +## 诊断 + +成功时单次输出: + +`ParticleDensity: event=VIEWPORT_RESEED reason=ZOOM_OUT ... stateReseed=true trailReseed=true fieldResubmit=false timelineReset=false gpuResourceRebuild=false` + +fallback 时单次输出 `action=SKIP skipReason=INVALID_RESPAWN_DOMAIN`,并明确 State/Trail 未 reseed。 + +## 测试与构建 + +- Mac mini / Darwin x86_64,OpenGL Debug CMake/Ninja 配置:PASS。 +- 精确编译 `render_particle_validation_layer.cpp.o`:PASS。 +- 精确编译 `particle_viewport_reseed.test.cpp.o`:PASS。 +- 独立原生 C++ directed executable:8/8 PASS,覆盖 Zoom Out、Zoom In、Pan、Rotate/Pitch、epsilon、fallback 消费、一次 gesture 一次触发、下一 gesture 刷新 startZoom。 +- `git diff --check`:PASS。 +- 全量 `mbgl-test-runner` 冷链接不是本 Step 必需验证;首次新 build tree 在受影响 objects 已通过后停止于 182/659,未据此声称全量 runner PASS。 +- Android Gradle/APK/ADB/设备:未执行,交由 RDC 串行验收。 +- Jest:未运行、未新增。 + +## 当前结果与下一步 + +Codex 阶段满足一次性 viewport State/Trail reseed 的实现与 Mac 定向验证要求。下一步由 RDC 构建 OpenGL Debug Test APK,在正式 RasterParticleLayer 场景验证 Zoom Out 立即铺开、单次日志、无 GL/EGL/crash,以及 camera stable 后 ParticleFastPath fast hit 恢复。 diff --git a/docs/50_Step记录/STEP-092_ContinuousRange可退化分段RetainedCache_v1.md b/docs/50_Step记录/STEP-092_ContinuousRange可退化分段RetainedCache_v1.md new file mode 100644 index 000000000..a9a84891a --- /dev/null +++ b/docs/50_Step记录/STEP-092_ContinuousRange可退化分段RetainedCache_v1.md @@ -0,0 +1,80 @@ +# STEP-092 Continuous Range 可退化分段 Retained Cache v1 + +## 状态 + +**进行中:Codex 实现完成,等待 RDC 独立编译与运行验证** + +## 目标与基线 + +- 分支:`particle/main` +- 起始 HEAD:`b29e564944197072a0effaec36ca95c48fe0649b` +- 起始 `origin/particle/main`:`b29e564944197072a0effaec36ca95c48fe0649b` +- Implementation commit:`a9eaec6b65cc26aa70e03002bdc1326c46cad245`(`feat: add continuous retained render ranges`),已 push 至 `origin/particle/main`。 +- Style Layer 顺序是唯一 Z-order;App 只提交现有 `startLayerId` / `endLayerId` 闭区间。 +- 保留未配置 explicit range 时的 `startsRetainedDynamicRange()` legacy underlay 行为。 + +## 实现结构 + +- `RetainedUnderlayRuntimeState` 是 map-scoped range config 所有者;使用 mutex 保护 ID/config/resolution,config revision 单调递增。 +- `RenderOrchestrator::createRenderTree()` 基于完整 `orderedLayers`(包含 hidden layer)把 ID 解析为 Style index;不存在、倒序或 Style 未 ready 均 fail closed,不保留旧 index。 +- 内部统一使用 `[continuousBegin, continuousEndExclusive)`:Bottom、Continuous、Top 均由 Style index 自然退化。 +- 现有 cache 泛化为带 `begin/endExclusive` 与 Bottom/Top role 的 retained range cache;两个 cache 对象不等于两个 GPU cache,texture/render target/compositor 只在对应 range 实际存在时 lazy 创建。 +- explicit valid 时完全忽略 legacy boundary;explicit invalid 时不用 legacy 猜测,走完整 Renderer。 + +## 四种结构 + +| Style 结构 | Render Range | Retained GPU cache | +|---|---:|---:| +| Bottom + Continuous + Top | 3 | 2 | +| Bottom + Continuous | 2 | 1(Bottom) | +| Continuous + Top | 2 | 1(Top) | +| Continuous only | 1 | 0 | + +`startLayerId == endLayerId` 是合法 single-layer Continuous。hidden endpoint 仍决定 topology;range 是否存在只看 Style index 区间,不看当前 drawable/group 是否为空。 + +## 绘制与 global index + +- cache build 的 opaque 使用区间 reverse traversal,translucent 使用 forward traversal。 +- callback ordinal 来自完整 `layerGroupsByLayerIndex`,`parameters.currentLayer` 与 `depthRangeSize` 沿用全 Style 语义,不对子区间重新编号。 +- main 顺序固定为:Bottom composite(若有)→ Continuous opaque/translucent → Top composite(若有)。 +- Top 与 Bottom 均使用透明 RGBA offscreen、现有 direct sample compositor 和 premultiplied alpha blend;未新增 land-mask 或业务判断。 + +## Invalidation 与 fallback + +- camera/viewport/pixel ratio/style/layer/source/tile/image/glyph/placement/context/debug/3D 等继续进入 STEP-089 的 content/topology/static revision 模型。 +- range config revision、continuous begin/end、Bottom/Top topology 纳入 revision;配置变化会释放实际存在 cache 并使 prepared tree/fast gate 失配。 +- endpoint 删除、missing、倒序与 Style 未 ready:diagnostics 给出原因,cache key 变空并释放资源,full renderer 安全 fallback;后续 Style 恢复会重新解析。 +- correctness-first:无法只归因 Bottom 或 Top 时,两侧实际存在 cache 可同时失效。 + +## Particle-only CPU Fast Path + +- gate 只要求实际存在的 retained cache valid;Bottom/Top 不存在时为 vacuous success,不产生 cache-missing miss。 +- fast frame 仅遍历 Continuous 区间,仍使用全局 ordinal。 +- explicit Continuous 中只允许 Renderer 已知的 WeatherColor/RasterParticle dynamic candidate;出现未知 Layer 时 fail closed 到 full CPU frame,但 range cache/Z-order 仍保持。 +- Continuous-only 可进入 0-cache fast gate;是否最终命中仍需满足 Particle、revision、placement、scene 等既有条件。 + +## API 与 diagnostics + +- C++/Android/iOS/RN 增加 `setContinuousLayerRange(..., startLayerId, endLayerId)`;空 ID 拒绝,相同 ID 接受。 +- 保留 `setRetainedUnderlayEnabled()` 与 `getRetainedUnderlayDiagnostics()`。 +- pull diagnostics 增加 configured/valid、IDs/index、Bottom/Top present/build/reuse/invalidation;保留 aggregate fallback 与 `lastReason`,无 per-frame Release 日志。 + +## Directed tests + +已新增源码覆盖:single/multi layer、missing/reversed、hidden endpoint、reorder/delete、四种 topology、0/1/2 cache decision、无 cache 的 fast gate、config revision 与 explicit/legacy 排他。 + +## 待 RDC 执行 + +```bash +cmake --build build-step088-opengl --target mbgl-test-runner -j3 +build-step088-opengl/mbgl-test-runner \ + --gtest_filter='ContinuousLayerRange.*:RetainedDynamicBoundary.*:RetainedLayerGroupRange.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:ParticleFastPath.*' +cmake --build build-ios --target ios-sdk-static -- -quiet +git diff --check +``` + +RDC 还需独立核验 OpenGL/Metal 透明 Top composite、0/1/2 texture/FBO 生命周期和 stable frame 复用。Android Gradle/APK/ADB/设备项未由 Codex 执行。 + +## 当前结论 + +源码与 directed test 已实现并完成 scoped implementation push;在 RDC 返回 OpenGL、Metal、Native test 实际结果前,本 Step 不判 PASS。 diff --git a/docs/50_Step记录/STEP-092_ContinuousRange可退化分段RetainedCache_v2.md b/docs/50_Step记录/STEP-092_ContinuousRange可退化分段RetainedCache_v2.md new file mode 100644 index 000000000..fdaea22a6 --- /dev/null +++ b/docs/50_Step记录/STEP-092_ContinuousRange可退化分段RetainedCache_v2.md @@ -0,0 +1,99 @@ +# STEP-092 Continuous Range 可退化分段 Retained Cache v2 + +## 状态 + +**CLOSED / PASS(Mac Native 验证闭环)** + +本版在 v1 的 Codex 实现基础上补入 RDC 独立编译、测试、修复与双后端核验结果。Android Gradle/APK/ADB/实体机与 Metal GPU frame capture 未执行,不伪造为已验证。 + +## Git 基线 + +- 分支:`particle/main` +- 起始 HEAD:`b29e564944197072a0effaec36ca95c48fe0649b` +- Codex implementation:`a9eaec6b65cc26aa70e03002bdc1326c46cad245` +- Codex 文档:`b89c8c8cb4d941abdcd704b278284e209ba7399b` +- RDC 验证修复:`4908244d2`(`fix: close STEP-092 native validation issues`) +- 最终文档收尾提交在本文件提交后回读,以最终交付结果为准。 + +## 已实现架构 + +- Style Layer 顺序是唯一 Z-order 事实来源。 +- App/RN 只提交现有 `startLayerId` / `endLayerId` 闭区间。 +- Native 基于完整 `orderedLayers` 解析 index;hidden endpoint 仍参与 topology。 +- `startLayerId == endLayerId` 为合法 single-layer Continuous。 +- 内部统一为 `[continuousBegin, continuousEndExclusive)`。 +- 四种结构自然退化: + - Bottom + Continuous + Top → 3 Render Range / 2 retained caches + - Bottom + Continuous → 2 Render Range / 1 Bottom cache + - Continuous + Top → 2 Render Range / 1 Top cache + - Continuous only → 1 Render Range / 0 retained cache +- explicit valid 时不混用 legacy boundary;explicit invalid 时 fail closed,不复用 stale index/cache。 +- global layerIndex / ordinal / `currentLayer` / `depthRangeSize` 保持全 Style 语义。 +- Opaque reverse、Translucent forward 保持。 +- Particle-only Fast Path 只检查实际存在的 retained ranges。 + +## RDC 编译与测试结果 + +### OpenGL test-runner + +第一次编译失败: +- `renderer_impl.cpp:873-874` +- 原因:循环变量被收窄为 `RenderItem&` 后访问不存在的 `index`。 + +第二次编译失败: +- 改为 `LayerRenderItem&` 后,`hasRenderPass/render` 是 private override。 + +RDC 最终修复: +- 用 `LayerRenderItem` 读取 `index`; +- 通过 `RenderItem&` 调用公开虚接口。 + +第三轮: +- `cmake --build build-step088-opengl --target mbgl-test-runner -j3` +- 结果:PASS,exit code 0。 +### Native directed tests + +命令: +`build-step088-opengl/mbgl-test-runner --gtest_filter='ContinuousLayerRange.*:RetainedDynamicBoundary.*:RetainedLayerGroupRange.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:ParticleFastPath.*'` + +第一轮:28/29 PASS。 +唯一失败: +- `RetainedUnderlayCache.RuntimeControlDisablesAndReenablesMapScopedCache` +- 原因:`setEnabled(false)` 已记录 aggregate invalidation,STEP-092 range cache 同步又调用 aggregate `invalidate()`,造成重复计数。 + +RDC 修复: +- 保留 legacy aggregate invalidation 只记录一次; +- 对实际 range cache 单独记录 Bottom/Top invalidation; +- 不改变 runtime-disabled 语义。 + +第二轮:**29/29 PASS**。 + +覆盖 single/multi range、missing/reversed endpoint、hidden endpoint、reorder/delete、四种 topology、0/1/2 cache decision、explicit/legacy 排他、Fast Path、revision 与 runtime control。 + +## Metal / iOS + +- `build-ios/CMakeCache.txt`:`MLN_WITH_METAL=ON`、`MLN_WITH_OPENGL=OFF`。 +- Xcode target 编译定义包含 `MLN_RENDER_BACKEND_METAL=1`。 +- `cmake --build build-ios --target ios-sdk-static -- -quiet` +- 结果:PASS,exit code 0。 +- 警告仅包括既有 EAGL deprecated、bundle identifier 等,不构成 STEP-092 编译失败。 +## GPU resource / composite 核验 + +RDC 对实际源码路径独立核验: +- Bottom/Top holder 可常驻,但 GPU texture/render target/compositor 只在 `prepareResources()` 被调用时 lazy 创建。 +- `prepareResources()` 只在对应 cache decision 为 Build/Reuse 时调用;不存在的 Range 为 Disabled,不调用资源准备。 +- 同 viewport 且已有 `offscreenTexture` 时不重建 texture;已有 `compositeLayerGroup` 时不重建 compositor。 +- retained target clear color 为透明 `(0,0,0,0)`。 +- composite 使用 `gfx::ColorMode::alphaBlended()`,其因子为 `One / OneMinusSrcAlpha`,对应 premultiplied-alpha。 +- OpenGL 与 Metal `RetainedUnderlayCompositeShader` 均直接采样 RGBA texture。 +- Final 顺序为 Bottom composite → Continuous → Top composite。 +- 29 项 directed tests 已验证 0/1/2 cache decision 与 stable reuse/invalidation 语义。 + +## 未执行项 + +- Android Gradle / AAR / APK / ADB / 安装 / 实体机视觉:未执行。 +- Metal GPU frame capture、iOS 实机 runtime resource counter:未执行;当前仓库无现成可直接运行的 STEP-092 Metal GPU runtime test target。 +- 未执行 Jest。 + +## 结论 + +STEP-092 的 Mac Native 实现、OpenGL 编译、Native directed tests、Metal-only iOS 编译、资源生命周期代码路径、透明 Top composite 与 Git 修复闭环均已通过。未执行的平台实体机/Frame Capture 项保留为后续产品级验证,不改变本 Step 的 Mac Native CLOSED / PASS 结论。 diff --git a/docs/50_Step记录/STEP-093_ContinuousRange运行态综合验收_v1.md b/docs/50_Step记录/STEP-093_ContinuousRange运行态综合验收_v1.md new file mode 100644 index 000000000..c9591fe67 --- /dev/null +++ b/docs/50_Step记录/STEP-093_ContinuousRange运行态综合验收_v1.md @@ -0,0 +1,238 @@ +# STEP-093 Continuous Range 运行态综合验收 v1 + +## 1. 当前状态 + +**IMPLEMENTED / WAITING_RUNTIME_VALIDATION** + +Codex 已完成 Android 运行态验收入口、跨平台 diagnostics 补强、Fast Path fail-closed 原因观测、定向 Native 测试源码和 RDC 操作手册。依照仓库职责分工与本轮用户指令,Codex 未执行 Android/iOS 长时间编译、安装、ADB、模拟器/真机操作或视觉验收。因此 Android 为 `WAITING_ANDROID_RUNTIME_VALIDATION`,iOS/Metal 为 `WAITING_IOS_RUNTIME_VALIDATION`;本版不能标记 `CLOSED / PASS`。 + +## 2. 起始 Git 基线 + +- Repository:`MapLibreParticleLab` +- 机器:`Mac-mini-tei.lan` +- 路径:`/Users/tei/dev/MapLibreParticleLab` +- Branch:`particle/main` +- Starting HEAD:`8a5d88008c142e2a80e0ff602d228e0e67de2d83` +- Starting `origin/particle/main`:`8a5d88008c142e2a80e0ff602d228e0e67de2d83` +- STEP-092 基线:`CLOSED / PASS(Mac Native 验证闭环)` +- STEP-093 implementation commit:`b3ec8edc87ece404b5f7b3706f5acf2a70de6862` +- 起始工作树差异:仅有本任务书 `docs/40_任务/STEP-093_ContinuousRange运行态综合验收_Codex任务_v1.md` 为 untracked;已保留并纳入本 Step 文档提交。 + +## 3. 实际调查到的测试入口 + +Android 已有入口为 `MapLibreAndroidTestApp` 的 `ParticleCoreLayerValidationActivity`。本 Step 复用该 Activity、既有 Host-submitted particle fixture、`weather-color` 和普通 Style layer,不建立新测试框架,也不引入特殊 layer type。 + +新增底部 `STEP 093` 菜单与常驻 diagnostics 区。入口支持: + +- A:Bottom + Continuous + Top; +- B:Bottom + Continuous; +- C:Continuous + Top; +- D:Continuous only; +- E:Single-layer Continuous; +- Continuous/Bottom/Top add 与 reorder; +- 普通 layer 跨 Bottom/Continuous/Top 移动; +- start/end endpoint visibility、删除与恢复; +- missing start、missing end、reversed、style loading 期间配置、empty ID、恢复 valid; +- Camera preset 循环覆盖 pan/zoom/rotate/pitch; +- Continuous 内未知普通 layer 触发 Fast Path fail-closed; +- 1 秒一次的屏幕 pull diagnostics;仅用户点 `READ DIAGNOSTICS` 或场景切换时写一条日志,不制造逐帧日志风暴。 + +Android Activity 声明增加 `orientation|keyboardHidden|screenSize` config changes,使 RDC 可在不重建 Map/Style 的前提下用旋转或尺寸变化观察 viewport invalidation。 + +iOS 当前只有正式 `MLNMapView` diagnostics API 与 RN bridge 字段补齐,没有新增专用 STEP-093 iOS validation 页面。因此 iOS runtime 必须后续由 RDC 准备实际入口并执行,不能由 Android 结果推导。 + +## 4. 本轮新增/修改内容 + +### 4.1 Renderer diagnostics + +在既有 map-scoped diagnostics 上追加,而非另造一套状态源: + +- Range:`continuousBegin`、`continuousEndExclusive`、`rangeFallback`; +- Bottom/Top:`cacheState`、build/reuse/invalidation、last invalidation reason; +- 资源生命周期:texture create、composite create、resource release; +- Fast Path:eligible、active、fallback reason、hit、miss、full CPU frame; +- 未知 Continuous layer 的明确原因:`unsupported-continuous-layer`。 + +这些 counter 是 Map 生命周期内累计事件数,不是 GPU memory byte 数。它们用于证明 stable frame 不重复创建资源及 invalidation 后能重建/reuse;不能替代 profiler 或 GPU capture。 + +### 4.2 跨端暴露 + +- Android JNI/Java:diagnostics 字符串数组由 20 项扩为 39 项,Java 构造索引与 Native 顺序逐项一致; +- iOS ObjC++:`MLNRetainedUnderlayDiagnostics` 暴露同组字段; +- RN Android/iOS/TypeScript/Test Host:透传并显示同组字段; +- 原有 aggregate build/reuse/fallback/invalidation 字段保持,不删除兼容语义。 + +### 4.3 Android 可视场景 + +测试 Style 中仅使用普通 background/fill/line、已有 WeatherColor 与 RasterParticle: + +- Bottom:深色 background + 青色 fill; +- Continuous:WeatherColor + Particle; +- Top:半透明黄色 fill、opaque 红色 fill、白色粗 line; +- fail-closed:在 Continuous 内加入普通 fill; +- cross-range:同一个普通 fill ID 分别位于 endpoint 前、endpoint 间、endpoint 后。 + +最终 Z-order 仍完全由 Style layer 数组顺序决定。App 只调用 `setContinuousLayerRange(startLayerId, endLayerId)`;没有 anchor/marker、业务 layer metadata 或 Renderer 业务 ID 硬编码。 + +### 4.4 定向测试源码 + +新增 `RetainedUnderlayCache.RuntimeValidationDiagnosticsExposeRangeResourcesAndFastPath`,覆盖 Continuous index、cache state、资源 counter、Fast Path counter;既有 Fast Path gate 测试增加 `UnsupportedContinuousLayer` 原因断言。 + +本轮未运行 test runner。STEP-092 既有 29/29 PASS 是历史基线,不能当作新增测试已通过;RDC 运行后预期相关 filter 共 30 项,但必须以实际 runner 输出为准。 + +## 5. 静态一致性审查 + +Codex 已完成: + +- Native diagnostics 定义、runtime state、Android JNI/Java、iOS、RN 字段名与顺序逐项比对; +- 四种 topology 与 single-layer 的 Style layer 顺序/endpoint 位置审查; +- mutation、cross-range、endpoint、invalid、camera、fail-closed 菜单接线审查; +- absent Bottom/Top 仍沿用 STEP-092 `Disabled` / lazy resource 路径,没有新增固定 GPU 分配; +- Fast Path eligibility 与 decision counter 接入既有 gate,失败仍进入 full CPU frame; +- `xmllint --noout`:Android Manifest 与 layout PASS; +- `git diff --check`:PASS。 + +未执行:C++/Java/Kotlin/ObjC++/TypeScript 编译、Native tests、Gradle、APK、ADB、Android/iOS runtime。静态审查不等于运行态 PASS。 + +## 6. RDC Android 构建、安装与启动 + +在仓库根目录执行: + +```bash +./tools/particle_android_validation/build_validation_apk.sh +adb devices +adb -s install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk +adb -s shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity +``` + +可选定向日志: + +```bash +adb -s logcat -c +adb -s logcat -v threadtime ParticleValidation:I MapLibre:I '*:S' +``` + +若使用非 x86_64 真机,当前 validation script 的固定 ABI 不适用;RDC 应按项目现行 Android 构建规则选择相符 ABI,并在证据中记录实际 APK、commit、ABI、设备与 renderer backend。 + +## 7. RDC Native 精确回归命令 + +如 RDC 使用 STEP-092 的现有 OpenGL build tree: + +```bash +cmake --build build-step088-opengl --target mbgl-test-runner -j3 +build-step088-opengl/mbgl-test-runner --gtest_filter='ContinuousLayerRange.*:RetainedDynamicBoundary.*:RetainedLayerGroupRange.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:ParticleFastPath.*' +``` + +必须记录真实 test 数与结果;不能直接抄写“30/30”。 + +## 8. Android 逐项运行手册 + +### 8.1 通用观察方法 + +启动 Activity 后点 `STEP 093`。每次切场景等待 Style READY,再观察屏幕 diagnostics;需要固化证据时点 `READ DIAGNOSTICS` 并保存日志/截图。稳定性判断建议至少间隔 10–30 秒采两次快照: + +- Particle 持续运动; +- 对存在的 cache,build/texture/composite 不再增长,reuse 与 Fast hit 应增长; +- 不存在的 range 显示 `present=false`、`state=disabled`,对应资源创建数不因稳定帧增长; +- counter 为累计值,切场景时应比较增量,不要求绝对值归零。 + +### 8.2 Topology A–E + +1. A:选 `A BOTTOM + CONTINUOUS + TOP`。预期 range valid、Bottom/Top present、cache count 2;opaque 红块完整盖住 Particle,黄色半透明区域正确混合,白线在最上层,无黑边/halo/错误变暗。 +2. B:选 `B BOTTOM + CONTINUOUS`。预期 Bottom present、Top absent、cache count 1;Top state disabled,Top 资源不增长,Fast Path 不因 Top absent 被阻断。 +3. C:选 `C CONTINUOUS + TOP`。预期 Bottom absent、Top present、cache count 1;Bottom state disabled;Top 仍正确覆盖 Particle。 +4. D:选 `D CONTINUOUS ONLY`。预期 valid、Bottom/Top 均 absent、cache count 0、不是 fallback;Particle 正常运行,Fast Path 可命中。 +5. E:选 `E SINGLE-LAYER CONTINUOUS`。预期 start ID 等于 end ID、startIndex 等于 endIndex、`continuousEndExclusive = startIndex + 1`,不 fallback。 + +### 8.3 Mutation 与跨边界 + +在 `MUTATION / FAST PATH` 子菜单依次执行: + +- `CONTINUOUS ADD` → `CONTINUOUS REORDER` → `RESTORE DEFAULT`:恢复动作也等价验证非 endpoint layer 删除; +- `BOTTOM ADD` → `BOTTOM REORDER` → restore; +- `TOP ADD` → `TOP REORDER` → restore; +- `CROSS → BOTTOM` → `CROSS → CONTINUOUS` → `CROSS → TOP`。 + +每次检查 endpoint ID 重新解析出的 index、present topology、invalidation/build/reuse 增量及最终画面顺序;不得出现旧 index、旧 cache 或自动按 layer 类型修正 range。 + +### 8.4 Endpoint 与 visibility + +在 `ENDPOINT / VISIBILITY` 子菜单: + +- start visible ↔ hidden、end visible ↔ hidden:index 与 topology 不变,hidden endpoint 仍是 endpoint; +- `DELETE START`:range invalid、fallback、无 crash/stale cache;`RESTORE START` 后恢复 valid 并 rebuild/reuse; +- 对 end 执行同样步骤。 + +### 8.5 Invalid explicit range + +在 `INVALID RANGE` 子菜单依次执行 missing start、missing end、reversed、configure while style loading、empty ID、restore valid: + +- missing/reversed/style-not-ready:不得猜测 endpoint,不复用 stale cache,diagnostics 给出原因; +- empty ID:当前 API 应同步返回 `accepted=false`; +- restore 后无需重建 Map instance,应重新 valid、build、reuse。 + +### 8.6 Camera/viewport + +连续点四次 `CAMERA NEXT`,覆盖 target/zoom/bearing/pitch 组合。每次检查 invalidation → build → reuse,topology 不变,无错位、黑屏或 Particle/地图不同步。 + +旋转设备或改变模拟器窗口尺寸,确认同一 Activity/Map 上产生 viewport invalidation;如设备支持可改变 display scale/pixel ratio,并单独记录是否实际触发。 + +### 8.7 Fast Path fail-closed + +先恢复 A,稳定后确认 eligible/active 与 hit 增长。再选 `FAST FAIL-CLOSED`: + +- Continuous Range 仍 valid; +- fallback reason 为 `unsupported-continuous-layer`; +- Fast Path 不 active,full CPU frame 增长; +- 普通未知 layer 不被跳过或移出 Continuous; +- Bottom/Top 与最终 Z-order 仍正确。 + +### 8.8 长时间运行 + +建议 RDC 在可用设备上连续运行 15–30 分钟,循环 topology、camera、mutation、endpoint、visibility、invalid/restore、再次稳定:观察 texture/composite create 与 release 增量、系统内存/GPU 工具可获得的数据、黑屏/闪屏/stale cache/偶发层序/Particle 恢复。若没有可靠 GPU byte 数,明确记录限制,不伪造内存数字。 + +## 9. PASS / FAIL 判定 + +- `PASS`:必须有实际设备/模拟器操作、画面确认与 diagnostics/log 证据,并满足该行预期。 +- `FAIL`:实际结果违背预期;保留首个真实错误、场景、截图/日志和 commit。 +- `WAITING_*`:入口已准备但平台尚未执行。 +- 编译 PASS、静态检查 PASS、另一平台 PASS 均不能替代当前平台 runtime PASS。 + +## 10. 验收矩阵 + +| 类别 | 场景 | Android | iOS/Metal | 关键证据 | 结论 | +|---|---|---|---|---|---| +| Topology | Bottom+Continuous+Top | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | A 按钮、range/index、2 cache、画面截图待采集 | NOT RUN | +| Topology | Bottom+Continuous | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | B 按钮、Top disabled/资源增量待采集 | NOT RUN | +| Topology | Continuous+Top | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | C 按钮、Bottom disabled/资源增量待采集 | NOT RUN | +| Topology | Continuous-only | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | D 按钮、valid + 0 cache + 非 fallback 待采集 | NOT RUN | +| Range | Single-layer Continuous | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | E 按钮、start=end、endExclusive=start+1 待采集 | NOT RUN | +| Z-order | Top covers Particle | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | opaque/semi-transparent/line 视觉截图待采集 | NOT RUN | +| Cache | Stable reuse | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | 两次 counter 快照待采集 | NOT RUN | +| Invalidation | Pan/Zoom/Rotate/Pitch | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | camera preset 前后 invalid/build/reuse 待采集 | NOT RUN | +| Mutation | Continuous internal mutation | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | add/reorder/restore diagnostics 待采集 | NOT RUN | +| Mutation | Cross-range move | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | cross layer 三位置的 index/Z-order 待采集 | NOT RUN | +| Endpoint | start missing/recovery | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | invalid reason + restore valid 待采集 | NOT RUN | +| Endpoint | end missing/recovery | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | invalid reason + restore valid 待采集 | NOT RUN | +| Visibility | hidden endpoint topology | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | visible/hidden index 不变待采集 | NOT RUN | +| Invalid | missing/reversed range | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | fallback/lastReason/no stale cache 待采集 | NOT RUN | +| Fast Path | eligible | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | eligible/active/hit/full counter 待采集 | NOT RUN | +| Fast Path | unknown layer fail-closed | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | unsupported reason/full CPU frame 待采集 | NOT RUN | +| Alpha | Top premultiplied composite | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | transparent/opaque/semi/line 视觉证据待采集 | NOT RUN | +| Stability | repeated operations / long run | WAITING_ANDROID_RUNTIME_VALIDATION | WAITING_IOS_RUNTIME_VALIDATION | 长时间 counter/内存/画面记录待采集 | NOT RUN | + +## 11. 问题、限制与未执行项 + +- 未发现需要修改 STEP-092 数学定义、Style 顺序、0/1/2 cache 或 Fast Path 核心模型的架构阻塞。 +- diagnostics 是累计 counter,不是 per-scene reset;RDC 应使用前后增量。 +- 资源 release counter 表示 holder 释放事件,不是释放字节数。 +- Android 场景 Style reload 是可重复的测试手段;跨场景比较必须等待 READY。 +- 当前 iOS 没有专用 STEP-093 面板,只有 diagnostics API/bridge;这是明确待办,不虚报 iOS runtime 能力完成。 +- Android/iOS build、Native tests、APK、安装、ADB、真机/模拟器、视觉、长时间运行均 NOT RUN。 + +## 12. 下一步 + +1. RDC 对 implementation commit `b3ec8edc87ece404b5f7b3706f5acf2a70de6862` 执行 Native 精确回归与 Android build/runtime 矩阵。 +2. 若 Android 核心矩阵全部实际通过,可更新为 `ANDROID CLOSED / PASS + IOS WAITING_RUNTIME_VALIDATION`。 +3. 准备 iOS/Metal 对等 validation UI 或可重复操作入口,再独立执行 iOS 矩阵。 +4. 只有任务书 24 节全部完成、Git 与文档闭环后,才能更新为 `CLOSED / PASS`。 diff --git a/docs/50_Step记录/STEP-094_通用RenderSegment分段渲染与Style分类诊断_v1.md b/docs/50_Step记录/STEP-094_通用RenderSegment分段渲染与Style分类诊断_v1.md new file mode 100644 index 000000000..bf328a172 --- /dev/null +++ b/docs/50_Step记录/STEP-094_通用RenderSegment分段渲染与Style分类诊断_v1.md @@ -0,0 +1,66 @@ +# STEP-094 通用 RenderSegment 分段渲染与 Style 分类诊断 v1 + +## 状态 + +**PASS / CLOSED(RDC 代码与构建验收完成)** + +Codex 完成实现后,RDC 已独立完成 OpenGL Native 编译与定向 tests、Metal-only iPhoneOS 编译、RN TypeScript/codegen、Android Validation APK 构建与产物自检。规定的代码/编译验收门槛全部通过;Android/iOS 实体机运行与视觉验证本 STEP 未执行,明确保留为 NOT RUN。 + +## 起始基线 + +- 机器:`Mac-mini-tei.lan` +- 仓库:`/Users/tei/dev/MapLibreParticleLab` +- 分支:`particle/main` +- 起始 HEAD:`8fbb892a1236a55329ed9ef5a612640c9363d3f1` +- 起始 `origin/particle/main`:同上 +- V12 tag:`maplibre-particle/rn/13.4.1-particle.12`(`7eaff6e12`) +- 起始工作树:clean + +## 实现结果 + +- 新增 typed Core `RenderSegmentPlan` / resolved plan / diagnostics model。 +- explicit plan、V12 Continuous Range 与 legacy boundary 统一进入一个 resolver。 +- Style boundary ID 在 RenderOrchestrator 按完整 orderedLayers 解析;hidden Layer 仍参与。 +- Renderer 改为 topology-sized cache vector;每个 Retained Segment 独立 lazy GPU cache,Continuous 无 retained resource。 +- Segment 按 Style 顺序执行,global layerIndex/original ordinal/currentLayer/depthRangeSize 保持。 +- Layer Impl fingerprint 支持明确 retained Segment 内的 layer-local invalidation;Source/Tile 等仍保守全 retained invalidation。 +- 单 Continuous 保留既有 Particle Fast Path;多 Continuous fail closed 到 full CPU frame。 +- pull diagnostics 返回完整 Style→Segment→Layer、config/topology revision、coverage 与每个 retained cache 统计。 +- Android/iOS/RN 沿既有 WeatherRuntimeBridge 增加 plan 与 diagnostics JSON ABI;RN public API 为 typed object/discriminated union。 +- RN Test Host 增加 `Apply C-R plan` 与 `Fetch Segment Diagnostics`,只在用户操作时拉取一次。 + +## 定向测试源码 + +在既有 `retained_underlay_cache.test.cpp` 增加 RenderSegmentPlan/Diagnostics tests,覆盖:全 Retained、全 Continuous、C-R-R、R-R-C、R-C-R-C-R、相邻 R 不合并、missing/duplicate/reversed、hidden boundary、插入 Layer membership、boundary 删除/恢复、compat R-C-R/Continuous-only、visibility 不推进 topology revision、Cache OFF diagnostics、完整 coverage/flatten、Continuous cache=null、严格 JSON strategy/schema。 + +RDC 已执行过滤集:8 个 suite、43/43 tests PASS;其中 RenderSegmentPlan 9、RenderSegmentDiagnostics 4,并同时覆盖 V12 Continuous/Retained/Fast Path 回归。 + +## RDC 验证结果 + +- OpenGL:`mbgl-test-runner` 编译 PASS(exit 0);定向过滤集 43/43 PASS。 +- Metal/iPhoneOS:确认 `MLN_WITH_METAL=ON`、`MLN_WITH_OPENGL=OFF`;`ios-sdk-static` 编译 PASS(exit 0)。 +- RN package TypeScript:PASS;RN Test Host 使用本工作树 package typecheck PASS。 +- RN codegen:顶层 `npx react-native codegen` 因工程未安装 `@react-native-community/cli` 无法作为入口;未修改依赖,改用当前已安装 RN 0.85 的 `@react-native/codegen` combine/generate-all 正式底层入口 PASS,并确认 Android Java/JNI、C++ JSI 与 iOS ObjC++ 生成物均含 `setRenderSegmentPlanJson` / `getRenderSegmentDiagnosticsJson`。 +- Android Validation APK:标准脚本 PASS;OpenglDebug / x86_64 / strip=true;APK 60,294,298 bytes;`libmaplibre.so` 39,421,736 bytes;SHA-256 `848c3196c64cee6dfb1e9627f4af32b6e235fdd82afd1cfb8474c7fc5d591c71`。 +- Android/iOS 实体机 runtime 与视觉:NOT RUN。 +- Jest:明确不加入、不执行。 + +## RDC 验证重点 + +1. Core 编译:重点检查 rapidjson header、RenderLayer Impl metadata、JNI String ABI 与 ObjC++ selector。 +2. Tests:验证 resolver/diagnostics 新 suites 及 V12 原有 Continuous/Retained/Fast Path suites。 +3. OpenGL/Metal:确认 N retained resource、相邻 retained 不合并、稳定帧不重复创建。 +4. RN codegen:确认新增两个 primitive JSON methods 在 Android/iOS generated Spec 与实现 selector 对齐。 +5. Runtime:拉取 snapshot,核对 `styleLayers` 与 flatten(segment.layers) 完全一致;Cache OFF 仍保留 topology。 + +## 当前限制 + +- 多 Continuous Fast Path 未优化,按任务边界明确 fail closed。 +- Source/Tile mutation 无安全通用 dependency graph,仍全 retained invalidation。 +- 旧 diagnostics 仅兼容摘要;新 API 才是多 Segment 权威事实。 +- 多 Continuous Fast Path 仍按设计 fail closed,不是本 STEP 缺陷。 +- Android/iOS 真机运行和视觉结果不在本次 PASS 证据内。 + +## 下一步 + +实现代码已以 `016fd1fff`(`feat: add generic render segment model`)提交并 push。完成报告与本 Step 记录作为独立文档收尾 commit 提交;后续如需产品级 runtime/视觉确认,应另做 Android/iOS 运行态验收,不反向改变本 STEP 的代码验收结论。 diff --git a/docs/50_Step记录/STEP-095_V13独立严格代码与性能审查_v1.md b/docs/50_Step记录/STEP-095_V13独立严格代码与性能审查_v1.md new file mode 100644 index 000000000..8e01c9cd3 --- /dev/null +++ b/docs/50_Step记录/STEP-095_V13独立严格代码与性能审查_v1.md @@ -0,0 +1,50 @@ +# STEP-095 V13 独立严格代码与性能审查 v1 + +日期:2026-09-25(Asia/Tokyo)。任务:[STEP-095 任务书](../40_任务/STEP-095_V13独立严格代码与性能审查_Codex任务_v1.md)。调查:[独立源码与性能调查](../20_调查/STEP-095_V13独立严格代码与性能调查_v1.md)。报告:[Codex 完成报告](../报告/STEP-095_V13独立严格代码与性能审查_Codex完成报告_v1.md)。 + +## 1. 合同、环境与基线 + +只读独立审查 V13,先源码后旧报告。仅新增三份指定中文文档并追加开发记录;不修复源码,不执行构建、测试、codegen、打包、安装、设备或 profiler。按 AGENTS 已读当前基线、项目规范、有效总则及 STEP-088~094 相关任务/设计/记录/报告。 + +机器 `Mac-mini-tei.lan`,仓库 `/Users/tei/dev/MapLibreParticleLab`,分支 `particle/main`;起始工作树 clean,HEAD 和实际 origin 均 `05878d662308b9a92c20c433c3cd2943d74ae718`。冻结源码 `ee0b89e711bf01335b3aa71d36def785cd97fec3`,功能提交 `016fd1fff6a33cf795c10d4f5c5413d308383677`。RN/Android `13.4.1-particle.13`、iOS `6.28.0-particle.13`;manifest 源码准备提交 `ddf20dec9fe9cd05b77274827b6825829500dd5a` 至冻结点无生产源码变化。冻结点至起始 HEAD 仅有两份文档。 + +用户在本次同一会话恢复指令中明确指定 `gpt-6-astra high`,与任务要求一致。记录的是用户指定;没有独立的底层模型/effort 启动参数证明,不声称完成模型独立核验。原 285 行调查稿保留,仅校正模型记录、一个源码路径及补充精确桥接引用;不重写调查、不扩展搜索范围。 + +## 2. 本阶段交付 + +- 沿 Segment→Runtime→Renderer→GPU、Source→Field Runtime→RenderLayer→GPU 追踪,检查平台桥接、版本锁及 Integration 只读状态。 +- 新增 N01~N07:跨 target stencil、Metal depth/stencil 契约、失败 build 标成功、V4 PLAY 接线、JSON 严格性、数值入库语义、重复 attach listener。 +- C01~C07 区分源码成本与运行影响:完整快照、Metal wait、dirty 重分配、预算、线性扫描、全网格物化/UI、drawable 与场复制。 +- O01 给出旧 cache 计数污染新 topology 的具体链;E01/E02 区分 Host 错配与协调文档历史状态。 +- F01~F10 独立复核:6 项 CONFIRMED(F01/02/03/04/07/09)、4 项 QUALIFIED(F05/06/08/10)。不把设计限制、历史 TS 失败或计数错归扩大为新实测事故。 +- 给出 V12/V13 成本模型、缺失测试矩阵及后续修复批次;未创建修复任务,未修改 Integration。 + +## 3. 验证与未完成边界 + +本轮实际构建、测试、codegen、安装和设备验证全部 NOT RUN。STEP-094 构建/43 tests、旧报告 69 tests 与 TS 结果只列 HISTORY。源码查验不替代运行验证,缺陷触发的实际像素、故障注入、性能/峰值等保留 NEEDS_RUNTIME。 + +RDC 优先复核:同 source 同 tiles 的 R-R/R-C stencil;Metal attachment/load/clear;build 失败不得 reuse;只设一次时间后 PLAY 自动推进。其后验证完整 topology、invalid/recovery、30/60 FPS、峰值/长稳、销毁恢复。精确既有命令及证据要求见调查第 9 节;本轮未执行其中任何命令。 + +调查内容完成,产品运行态不判 PASS。外部模型参数、产物独立下载/重建、NavSea 实际接入、Vulkan、正式共享海陆 mask 均保留未核实范围,不作能力承诺。 + +## 4. Git 收尾 + +写稿时调查内容完成,Git 交付待提交/推送,整体暂记 PARTIAL;实际结果在本节追加,未完成 push 不预写 PASS。 + +仅四个允许路径:调查、本文、完成报告、根开发记录末尾追加。先验证引用/证据路径和 `git diff --check`,仅具体路径 staged,主提交 message 为 `docs: audit V13 source and performance for STEP-095`,push `origin particle/main`,不移动 tag、不 force push、不向 upstream push。 + +提交对象无法在自身正文中预先记录最终 SHA。采用主文档提交/推送后追加已知主提交证据,再提交收尾文档的方式;最后收尾提交 SHA、remote HEAD 和 clean status 在最终回复给出,可用 `git rev-parse HEAD` 与 `git ls-remote origin refs/heads/particle/main` 复核。 + +### 文档提交前检查 + +- 原调查仍为285行;新增Step/完成报告与开发记录追加齐备。9处相对文档链接、63处带行号源码引用的路径及行号范围检查通过;对校正引用回读源码,未执行产品测试。 +- 开发记录原内容逐字节保留,仅末尾追加;工作树只含四个允许路径,原暂存区为空。`git diff --check` exit 0;新增文档亦检查无行尾空白,staged后再执行cached检查。 + +### 实际主提交与远端回读(收尾事实) + +- 主提交:`ad5f92c48ea353c18548c79d223e9a06888330d3`,message:`docs: audit V13 source and performance for STEP-095`;仅四个允许文档,417行增加,无源码变化。 +- 提交前 `git diff --cached --check` exit 0;主提交后 `git diff --check` exit 0。一次提交遇到瞬时index.lock,回读时锁已自行消失,重试成功;没有删锁或执行破坏性Git操作。 +- `git push origin particle/main` exit 0,远端从 `05878d662` 前进至 `ad5f92c48`;随后 `git ls-remote origin refs/heads/particle/main` exit 0,返回完整主提交SHA,与本地HEAD及origin tracking一致。 +- 主提交推送核对后的 `git status --porcelain=v1` 为空:工作树clean。冻结V13至主提交只含本轮四文档和既有审查/任务两文档,无生产源码、shader、API、依赖、build、manifest或产物变化。 +- 调查内容及主交付:**PASS**;V13产品运行态仍 **NOT RUN / 未验收**。无构建、测试、安装或设备操作。 +- 本条和完成报告的结果追加由后续scoped收尾提交保存,message:`docs: record STEP-095 delivery verification`。其自身最终SHA不能预写进自身内容;最终回复记录该收尾提交、再次push/remote核对和clean status。此说明不将尚未发生的第二次push冒充已完成。 diff --git a/docs/50_Step记录/STEP-096_V13隔离盲审_v1.md b/docs/50_Step记录/STEP-096_V13隔离盲审_v1.md new file mode 100644 index 000000000..a9d5d4ecc --- /dev/null +++ b/docs/50_Step记录/STEP-096_V13隔离盲审_v1.md @@ -0,0 +1,56 @@ +# STEP-096 V13 隔离盲审 v1 + +## 状态与范围 + +状态:**完成(仅隔离盲审文档交付)**。日期:2026-09-25。 + +本阶段按[当前任务书](../40_任务/STEP-096_V13隔离盲审_Codex任务_v1.md)独立读取隔离快照源码和当前规范,形成调查、Step 与完成报告。完成不表示 V13 构建、测试、GPU 画面或性能通过。未做代码修复,后续验证全部 NOT RUN。 + +## 输入与可核实环境 + +| 项目 | 事实及限制 | +|---|---| +| 唯一工作目录 | `/Users/tei/dev/MapLibreParticleV13Blind096` | +| 机器观察值 | `Mac-mini-tei.lan` | +| 目标导出 SHA | `ee0b89e711bf01335b3aa71d36def785cd97fec3`,来自任务书;未用 Git 对象独立核验 | +| 版本声明 | [manifest](../../release/maplibre-particle/release-manifest.json#L4):revision 13,RN/Android `13.4.1-particle.13`,iOS `6.28.0-particle.13` | +| 产物 sourceCommit 声明 | `ddf20dec9fe9cd05b77274827b6825829500dd5a`,不作为本快照 HEAD | +| 当前 branch / Git HEAD / clean / remote | NOT AVAILABLE;本轮未运行 Git | +| 实际 TUI、screen、模型和 effort 启动记录 | NOT AVAILABLE;未访问目录外配置、会话日志或终端回放补证 | + +已读取 AGENTS.md、PARTICLE_BASELINE.md、`docs/00_项目规范/`、总则 README 及所列当前有效总则,包括当前基线与开发路线 v2,以及本任务。当前总则中的旧发布坐标与快照 `.13` 声明分开记录,不重写总则或历史。未打开外部路径、历史调查/Step/完成报告、既有审查材料或开发日志;本轮输出目录只用于写入和检查本轮三份新稿。 + +## 只读过程 + +1. 从公开 API 和目录建立 Style/分段、Renderer/gfx、场 ingress/runtime、平台 bridge 与 GPU 数据路径地图。 +2. 追踪正常、拒绝、fallback、失效和销毁路径,区分 owner、共享不可变快照与 GPU 资源;对候选寻找反例,不按问题数量补齐。 +3. 阅读 C++、GLSL、Metal、Android、iOS、RN、测试源码和配置;核对公开 PLAY 调用方、纹理更新条件、attachment 状态、CRC/容量、线程派发与同步点。 +4. 排除颜色投影仅看第一分量、强引用环、关闭缓存同步遗漏、纯重排遗漏等未成立候选;保留粒子独立前置更新门槛的完整证据。 +5. 写入三份中文报告;对本轮新稿进行文件存在、相对链接、引用行号和行尾空格的轻量静态检查。未启动构建、测试 runner、设备或 profiler。 + +## 结果 + +调查形成 **13 条独立 finding**:P1 四条(B01、B02、B06、B13),P2 九条;无 P0。类型为确定缺陷七条、源码确定成本四条、设计限制两条。这里的“确定”限定源码控制流/数值语义,不代表已发生用户事故;全部运行效果 NEEDS_RUNTIME。 + +主要正确性证据为跨渲染目标 stencil 记录、Metal depth/stencil clear 请求传递、encoded 播放推进、粒子完整内容身份、8-bit 零值和 Mercator 度量、retained 失败提交。成本和设计限制涉及 Metal 同步等待、资源重复分配、重复物化、UI 同步处理、共享地理 mask 与 store 总预算。每条的条件、精确路径/行号、反例、影响边界、安全修复与验证方案见调查报告。 + +## 三份交付文件 + +| 文件 | 内容 | +|---|---| +| [调查报告](../20_调查/STEP-096_V13隔离盲审调查_v1.md) | 架构地图、B01—B13 证据、反例、性能模型、覆盖缺口、验证矩阵、分批建议 | +| [本 Step 记录](STEP-096_V13隔离盲审_v1.md) | 范围、输入、只读过程、结果、未运行验证、后续责任 | +| [Codex 完成报告](../报告/STEP-096_V13隔离盲审_Codex完成报告_v1.md) | 交付状态、发现索引、证据质量、边界与移交方案 | + +## 未运行验证与阻塞 + +- CMake/Ninja/Xcode/Gradle、编译、链接、codegen、打包:**NOT RUN**。 +- C++/GL/Metal/Jest 测试、设备安装/启动、GPU capture、性能与内存测量:**NOT RUN**。 +- Git 状态、commit、push、远端核对和产物公网回读:**NOT RUN**。 +- 外部仓库、上游差异、真实安装环境、历史日志、启动配置:**NOT AVAILABLE**。 + +这些是任务约束和证据边界,不阻塞文档交付;它们阻止把本报告升级为运行态或发布验收。未确认 Vulkan 全链、全部图层组合及竞态行为,详见调查覆盖缺口。源码未写入;没有 Git diff 或导出对象逐文件比对证据,不声称已核验工作树 clean。 + +## 下一步与停止 + +Codex 在三份报告自检后停止,不继续修复、追加开发日志或解盲比较。RDC 随后冻结三份原稿的 SHA-256 与时间,独立移交正式仓库同路径、追加 `粒子开发记录.md`,执行四文件 scoped commit/push 和远端核查;这些动作本轮均未执行。新代码任务优先处理 B01/B02/B06/B13,并补 B12 失败恢复;其余数值/域语义和性能/容量工作按调查报告分批推进。验证场景和已有 runner 的定向执行命令已写入调查报告,交由后续 RDC 阶段执行。 diff --git a/docs/50_Step记录/STEP-097_V13三轮审查问题合并修复_v1.md b/docs/50_Step记录/STEP-097_V13三轮审查问题合并修复_v1.md new file mode 100644 index 000000000..fb61e209e --- /dev/null +++ b/docs/50_Step记录/STEP-097_V13三轮审查问题合并修复_v1.md @@ -0,0 +1,81 @@ +# STEP-097 V13 三轮审查问题合并修复 v1 + +状态:进行中。目标:按阶段修复三轮审查的当前源码问题,保护 C-001 与既有公开协议。基线:Mac-mini-tei.lan,`/Users/tei/dev/MapLibreParticleLab`,`particle/main`,起始 `720d309364628f85049cf9969d71c71e8dfacce9`,起始工作树 clean,子模块无偏离。当前 `.13` 发布 tag 指向 `ee0b89e71`;不修改发布坐标。 + +| 检查点 | 状态 | 实际事实、阻塞和下一步 | +| --- | --- | --- | +| 阶段 0 基线与反证 | 完成(静态) | 读取三轮审查、STEP-094~096 交付及 C-001/RFC-001 本地副本。当前 HEAD 比任务书基线只新增任务文档;远端 particle/main 经 `git ls-remote` 核对一致。建立 24 项矩阵。Integration 远端同步状态待 RDC 复核。 | +| A1 Renderer 正确性 | Android/Core 检查点完成,iOS runtime 用户豁免 | `e67e4961b` 记录 01/02/03/19 的部分代码;19 有 GTest。01/02/03 缺少任务要求的专用可执行像素/附件/故障注入测试入口;且未进行 GL/Metal 编译。故 A1 不判完成,严格停止于此,不进入 A2。 | +| A2 Field/粒子正确性 | Android/Core 验证完成;iOS runtime 用户豁免 | 04–07 在当前 GPT-6 Luna High 可见会话实现,RDC 独立验证完成;详见本记录末尾补记及 `docs/报告/STEP-097_A2_阶段验证记录_v3.md`。A2 达到 Android/Core 阶段门槛,不能据此关闭整个 STEP-097。 | +| B 输入/生命周期 | 计划 | 尚未开始。 | +| C 安全性能 | 计划 | 尚未开始;必须先完成 A1 正确性。 | +| D 交付 | 部分完成 | 已写 24 项矩阵、集中验证清单、Step、本轮完成报告和开发记录。所有构建、测试、运行、发布均 NOT RUN;后续 RDC 独立审查。 | + +允许范围与禁止范围按任务书。未完成项目不勾选。RDC 后续执行构建及运行;本轮不运行构建或 Jest。 + +## 2026-09-25 RDC/FZG1 A1 APK 实际验证补记 + +- 用户授权在 FZG1 而非 Win11 虚拟机测试。Mac RDC 以 HEAD `91c2491b0` 的 clean 基线执行标准 OpenGL x86_64 Validation APK 脚本:Gradle `BUILD SUCCESSFUL in 15m 59s`,APK 60,295,184 bytes,SHA-256 `8677197c5f83a22747cb51f0c261edffc5db4678fc25b3f366474f6ecb94dc99`。 +- 经 `adb -s 192.168.200.139:5555` 在 Panasonic FZG1-2 覆盖安装 `Success`;设备内安装 APK SHA-256 与产物一致。保存原包备份、20 张截图/UI XML、ADB 操作日志与实时 Logcat;设备实际验证见 `docs/报告/STEP-097_A1_FZG1测试APK与实机验证报告_v1.md`。 +- R-C-R、R-C、C-R、C-only、single continuous、缓存 OFF/ON、Camera、缺失 START/END 后恢复、TOP_REORDER、跨连续区与 fail-closed 均已做**既有场景冒烟**,未发现本次测试窗口内 FATAL/SIGSEGV/SIGABRT。 +- B/D 场景 top 不可见而诊断仍 topPresent=true 的现象已记录为待复核,不凭此断言确定像素错误;01 同 tile fixture、02 Metal、03 render exception 注入、19 GTest 仍未完成。因此 **A1 保持阻塞/PARTIAL,A2/B/C 未开始**。此补记更新了之前 Codex 报告中“全部测试 NOT RUN”的后续事实,不追溯修改旧报告。 + +## 2026-09-26 RDC 第 19 项 GTest 实跑补记 + +- Mac 同一 `particle/main` HEAD `0d13552bed84f427dd43cae9856391bd26622179` 将 `build-step088-opengl/mbgl-test-runner` 增量编译/链接完成:95/95 steps,exit 0,261.74 秒;构建警告记录于原始日志,不影响测试退出状态。 +- 定向 `RenderSegmentDiagnostics.OldCacheReleaseCannotChargeReplacementAtSameIndex`:**1/1 PASS、exit 0**;相关 `RenderSegmentDiagnostics.*`:**5/5 PASS、exit 0**。测试证明旧 revision 释放不污染新 topology 相同 index 的当前段计数。详细报告:`docs/报告/STEP-097_A1_第19项GTest运行验证报告_v1.md`。 +- 19 的**已存在 GTest 实测门槛通过**,但不等于 GPU 内存无泄漏或整项资源实机验收;01/02/03 专用测试入口仍缺,A1 依然 PARTIAL,不进入 A2/B/C。本补记覆盖此前 Codex 与 FZG1 报告中的历史 `GTest NOT RUN` 记录,不改写旧报告。 + +## 2026-09-26 FZG1 第 19 项设备辅助验收 + +- Mac RDC 确认 APK 源码 `91c2491b0` 到当前 `237a3e660` 只改变文档,复用原 OpenGL x86_64 APK;在 FZG1-2(ADB `192.168.200.139:5555`)执行 `install -r` 得 `Success`,设备内 APK SHA-256 `8677197c5f83a22747cb51f0c261edffc5db4678fc25b3f366474f6ecb94dc99` 与源文件相同。 +- 两轮 A/B/C/D/E/A 拓扑循环及 TOP_REORDER、CROSS_CONTINUOUS、CACHE OFF/ON、缺失 START 后恢复完成:20 张 1920×1200 截图与 20 份 UI XML;18 个有效检查点 `valid=true/fallback=false`、预期无效检查点 fail-closed。B/T `textureCreate-resourceRelease` 在现有诊断的 19 个场景均为 0 或 1;实时日志无 FATAL/SIGSEGV/SIGABRT。 +- 设备辅助冒烟 **PASS**,前述 19 GTest **1/1 PASS**;没有 on-device GTest、逐 revision GPU 内部观测或显存泄漏证明。B/D `topPresent=true` 与视觉不一致的既有疑点仍待单独核查;01/02/03 专项测试未闭合,A1 整体继续 PARTIAL。报告:`docs/报告/STEP-097_A1_第19项FZG1运行验证报告_v1.md`。 + +## 2026-09-26 Codex 继续 A1 测试入口 + +- 新增 Android 同源同 tile 的 R-R/R-C-R/C-R-R 本地 fixture 与诊断菜单;新增 Metal 附件/clear GTest 源码;新增 LayerGroup 正反范围异常、observer 与 cache 失败后重试 GTest 源码。生产 Metal clear 逻辑抽成仅内部调用的辅助函数,使测试检查同一路径,不调整公开协议。 +- 本轮只做静态检查;构建、GTest、Jest、设备像素、Metal validation 均 **NOT RUN**。01/02/03 仍需 RDC 编译与设备验证;A1 保持 PARTIAL,A2/B/C 未启动。第 19 项原有 RDC 1/1 PASS 不受影响。 + +## 2026-09-26 A1 收口,允许进入 A2 + +- A1 新 Android APK 源码 `8c006ac83`,构建/安装/设备内 SHA-256 复核成功;FZG1 18 个同源同 tile RR/RCR/CRR 检查点、中心/区域外像素采样均一致,脚本 exit 0,实时日志无崩溃。03 原生异常恢复 GTest 2/2 PASS;19 已有 GTest 1/1 和诊断组 5/5 PASS。 +- Metal host 可编译,但无 Metal device,两项测试均 SKIP;用户明确不做 iOS runtime,本轮验收按用户决定接受,不能记为 iOS 实测 PASS。B/D 的 topPresent 历史疑点保留;详见 `docs/报告/STEP-097_A1_Android专项验收报告_v1.md`。 +- A1 Android/Core 阶段完成;下一步严格 A2→B→C,每阶段保留独立 RDC 构建及验证。STEP-098 未启动、未提交;`.13` 发布冻结不动。 + +## 2026-09-26 A2 RDC 中途验证,尚未进入 B + +- Codex GPT-6-Sol medium 在 A2 四项范围内写入生产/测试源码但触及当期使用额度,未完成最终审查/提交。RDC OpenGL 原生编译 exit 0、16/16 定向 GTest PASS;Shader generator 双次 exit 0/幂等;Metal host 源码编译 exit 0、同样 16/16 Core GTest PASS,**没有 iOS runtime**。 +- A2 仍 PARTIAL;完整差异和构建/测试日志位于 `artifacts/maplibre-particle/step097-a2-rdc/`,中文事实报告:`docs/报告/STEP-097_A2_阶段验证记录_v1.md`。B/C 均未启动;用户未跟踪 STEP-098 与已发布 `.13` 未动。已安排 Codex 额度恢复后严格串行继续。 + +## 2026-09-26 A2 Core/Android 最终验证收口(B/C 未启动) + +- Codex 配置:GPT-6 Luna High,可见会话。仓库 `/Users/tei/dev/MapLibreParticleLab`,`particle/main`,验证基线 HEAD `5bb1a0e6353c5dfe8db8a61bbeba9cef447280ef`;A2 源码保持 dirty、未提交。Integration `main` 已有报告基线 `318f8ccf911ac3136c3086a1d14e61f83e2d4bce`,C-001 未改。 +- 04–07 完成:唯一 Core forecast clock 加 synchronous notification reentry guard;严格 GPU endpoint identity 与 mix-only immutable snapshot reuse;内部 RGBA8 signed zero/byte 解码;Mercator U/V 同纬度度量。旧 `sameFieldGpuContent` 的语义未改变,修正了与其 version-agnostic payload 等价定义相冲突的旧测试断言。新增 source 回归覆盖 50 次 mix ticks 不重物化、endpoint/revision 改变触发 materialize、clear fail-closed 与重入单 tick。 +- RDC 独立结果:原 58/58 完整回归 PASS;当前 broad filter OpenGL 121/121 与 Metal host 121/121 PASS;三项 A2 定向测试两平台各 3/3 PASS;shader generator 连跑两次幂等。Metal host 仅 Mac Core runner,不是 iOS 设备验证。 +- 标准 Android OpenGL Debug x86_64 APK `61,753,803` bytes,SHA-256 `27bf4a6cc191bd86c4d1c10398478d81c2999edd61fe7242e40e5188516b0ca5`;已安装到 FZG1-2(`192.168.200.139:5555`),设备 APK hash 相同。V4 Golden Wind test-only fixture 覆盖 load、seek 25/75、speed 2x/1x、PLAY 30 秒、responsive UI、PAUSE、stable pause、replay、clear、reload、再次 PLAY/PAUSE;runner exit 0、17 PNG、24 steps、ANR=0。15 秒截图可见粒子。 +- Mix-only 计数按两个稳定资源 epoch 记录:clear/reload 前 upload/reset=`6/2` 稳定;clear/reload 后合法增至 `10/4`,reload 后 playback 稳定。不能说跨 clear/reload 零上传或零 reset。APK source diff SHA-256 与 `mix-reuse-build-source.diff` 一致:`18927a5f30a88f5c1c3a00e0017473dc33b04151c227e4baf52250dc4ad2e6a2`。 +- 历史异常保留:12:43 首次 FZG1 ANR 根因为 source notification 同步重入 forecast tick,当前 `forecastTickInProgress` 与 `RasterArrayV4ClockSourceNotificationReentryDoesNotAdvanceTwice` 覆盖此项;后续有过无 ANR 但主线程高负载、PLAY/TIME 无响应的失败试跑,栈指向 forecast tick 内重复投影物化。最终 mix reuse APK 通过本次闭环。历史 debuggerd、失败试跑和最终日志均保留在 `artifacts/maplibre-particle/step097-a2-final-validation/`。 +- A1 与 A2 的 Android/Core 阶段检查完成;iOS runtime 明确按用户决定记 `USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`,不写成 iOS PASS。STEP-097 整体仍 PARTIAL:B/C 未开始,Jest 未运行,STEP-098 未运行,`.13` 未发布/覆盖。Git commit/push 留给 RDC 独立 QA 后执行。 + +### 2026-09-26 A2 源码独立 scoped commit 核验 + +- A2 的 26 个生产源码、shader、TestApp-only V4 Golden fixture 和定向测试文件已固定为 `f49ffa68a17e0a505199bdbeccf07f8543f3a05f` 并推送,`origin/particle/main` 回读一致;没有 stage 用户未跟踪 STEP-098。完整代码/设备证据仍见 `docs/报告/STEP-097_A2_阶段验证记录_v3.md`,不因 Git 提交改变原 APK 的 `git_dirty=true` 来源事实。STEP-097 整体 PARTIAL,B/C 待用户指令。 + +## 2026-09-26 B 阶段源码、Core 复核与 Native 版本阻塞(未收口) + +- Codex 两次自动任务(Android 第 10 项销毁监听、旧数值夹具纠正)均使用 `codex exec --json` + GPT-6 Luna High,均有 `turn.completed` 和 exit 0,JSONL/status.json 已持久化于 `artifacts/maplibre-particle/step097-b10-codex/` 与 `step097-b-fixture-codex/`。 +- 08 N05:长度感知 JSON/NUL/重复键/非法配置清除与恢复测试入口已实施;09 N06:提交前有限数值验证和旧有效修订保护已实施;10 N07:View owner、世代隔离与新 Map 销毁 observer 测试源码已实施;20 E01:Host package/lock 均指向正式 .13,正式 tarball SHA-256 与 SRI 已核对。 +- RDC Mac Core 构建 PASS,四项 B 定向 GTest 4/4 PASS;broad 初次 132/134,真实原因是旧四单元 16-bit 测试借用了 4-byte UInt8 storage;仅修为合法 8-byte little-endian fixture 后复编与扩大回归 **134/134 PASS**,未放宽生产校验。 +- 硬阻塞:B10 新 `MapLibreMap.addOnDestroyListener()` 不存在于已发布 .13 source commit `ddf20dec9`,现有 RN Gradle 默认正式 .13 AAR 无法提供该符号;本地新源码 Android 测试不能替代正式 RN consumer 精确配对构建。禁止借此修改 .13 发行产物。 +- Linux Android 采用远端 `b1239ac3b` 新独立 worktree,并从 Mac 传入 B scoped patch/测试,原 Linux dirty 工作区未动。首次 Gradle 仅因工作树缺少 ANDROID_HOME 失败,按既有 SDK 路径重试;结果见 B 专项报告的后续记录。 +- 当前 B = PARTIAL / BLOCKED_NATIVE_AAR_COMPATIBILITY,尚未 scoped commit/push;C 与 STEP-098 均不启动,iOS runtime 保持用户豁免。详见 `docs/报告/STEP-097_B_独立验收与版本阻塞报告_v1.md`。 + +### 2026-09-26 B 阶段 Mac RDC 最新核验(仍阻塞) +- B10 `codex exec --json` 终态 `turn.completed`、exit 0,完整 JSONL/status.json 持久化;旧 16-bit fixture 纠正任务亦同。RDC 复编后 B 定向 6/6、扩展 139/139 PASS。 +- Android `MapLibreMapDestroyListenerTest` 2/2 PASS,Native Java 编译 exit 0;隔离 Kotlin owner 4/4 PASS,均非默认 RN consumer 集成验收。日志和 XML 见 `artifacts/maplibre-particle/step097-b-rdc/`。 +- .13 release source 无新 `MapLibreMap.addOnDestroyListener()`,新 RN Kotlin 源码默认依赖 .13 AAR;B 保持 `PARTIAL / BLOCKED_NATIVE_AAR_COMPATIBILITY`。Linux 隔离 Gradle 仍待最终状态;未启动 C、STEP-098,也未修改 .13 发布资产。 + +### 2026-09-26 B scoped Git 阻塞检查点 +- Mac RDC 在独立 worktree 对 21 个 B 文件逐项 SHA-256 一致检查后,提交并推送 `step097-b-blocked-20260926`:`e463ba04ce956a54eed02b77f981eeeedd9a5534`,远端回读一致;`particle/main` 仍为 `b1239ac3b`,原 dirty/untracked(含 STEP-098)未动。 +- 该提交只冻结 PARTIAL/BLOCKED 的源码及证据,不是 B PASS;C 与 STEP-098 仍禁止启动,.13 发布资产不变。 diff --git a/docs/50_Step记录/STEP-098_QBIN_V4单帧与八帧滚动缓存_v1.md b/docs/50_Step记录/STEP-098_QBIN_V4单帧与八帧滚动缓存_v1.md new file mode 100644 index 000000000..0f9d419d1 --- /dev/null +++ b/docs/50_Step记录/STEP-098_QBIN_V4单帧与八帧滚动缓存_v1.md @@ -0,0 +1,137 @@ +# STEP-098 QBIN V4 单帧与八帧滚动缓存 + +状态:**进行中(Codex 源码交付;等待 RDC 独立构建/定向测试及平台验收)** +日期:2026-09-26(JST) + +## 目标与基线 + +- 目标:单帧真实时刻首显、合法同批双帧插值、每 `fieldKey` 最多八帧有序滚动,保留现有 RN API 两步调用。 +- 仓库:Mac `/Users/tei/dev/MapLibreParticleLab`;branch `particle/main`;开工 HEAD `b1239ac3b1554943200e6fbf14321459ccc86527`,结束时 HEAD 未变。 +- Integration:`main` / `origin/main`=`1dd576d3304e267e53ed64d31f9711d2350ce10a`;RFC-004、ADR-001、C-002、COORD-005 均已读。旧 RFC-003 阻塞由本轮明确的新接受契约解除;旧阻塞报告保留不改。 +- 当前 Android Native/RN 坐标仍为 `13.4.1-particle.13`;本次未改版本或发布资产。iOS 基础版本 `6.28.0`。 +- 工作树开工已有 STEP-097 B 修改/未跟踪文件、STEP-098 原任务与旧阻塞报告;保留原样。`git submodule status` 全部与记录 SHA 一致。 + +## 本轮完成 + +- `FieldSeries` 在 exact validTime 返回真实单帧窗口;非精确时间只解析本地相邻 pair。 +- 按 key 区分共存 schema 与插值批次身份。新批次可进入滚动序列并按精确时间单帧显示;插值要求 fieldID、reference time、resolved profile ID/version 与 grid/plane schema 全部相同。Ingress 保存已验证 profile 身份。 +- 满额候选集合先按新帧时间选择远端点,等距删除原最早端;通过 schema 邻接校验后原子 swap。拒绝帧不先淘汰。八帧以上正常插入使用既有成功结果。 +- 新增 decoded raw-plane + validity 字节计数;单帧 T0/T1 共享一份 materialized Snapshot,WeatherColor/Particle 首次 GPU 安装共享相同 endpoint 资源。 +- 扩展 Runtime、Binding、Ingress、RasterArraySource 定向测试代码,覆盖单帧/双帧/滚动/跨批次/原子拒绝/内存计数/重复清理场景;详见设计和完成报告矩阵。 +- RN TypeScript、Android Kotlin/JNI、iOS bridge 静态核对:旧 API 名称、参数、返回类型和两步顺序不变;未修改 bridge。 +- 新增中文设计文档与本报告;开发记录仅追加本轮事实。 + +## 修改范围 + +STEP-098 本轮源代码与测试: + +- `include/mbgl/particle/field/field_runtime.hpp` +- `src/mbgl/particle/field/field_runtime.cpp` +- `src/mbgl/particle/field/field_projection_materialization.cpp` +- `src/mbgl/particle/ingress/qbin_v2_decoder.cpp` +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `src/mbgl/renderer/layers/render_weather_color_layer.cpp` +- `test/particle/field_binding.test.cpp` +- `test/particle/field_runtime.test.cpp` +- `test/particle/qbin_v2_decoder.test.cpp` +- `test/source/raster_array_v4_host_api.test.cpp` + +文档/记录: + +- `docs/30_设计/STEP-098_QBIN_V4单帧与八帧滚动缓存设计_v1.md` +- `docs/50_Step记录/STEP-098_QBIN_V4单帧与八帧滚动缓存_v1.md` +- `docs/报告/STEP-098_QBIN_V4单帧与八帧滚动缓存_Codex完成报告_v1.md` +- 根目录 `粒子开发记录.md`(追加) + +未修改用户原任务书、STEP-097 的矩阵/记录/清单/桥接文件、NavSea/Server、QBIN 格式、版本锁、tag、manifest、制品。`test/particle/field_runtime.test.cpp` 与 `test/source/raster_array_v4_host_api.test.cpp` 开工前已有 B 阶段 dirty 内容;原 B 的 fixture 修正保留,本轮只叠加 STEP-098 测试,并为 Source 测试 QBIN helper 增加 forecast reference 参数。 + +## API / ABI 与限制 + +- RN 和平台 bridge 公开方法签名/枚举名称:未变。 +- `DecodedFieldFrame` 增加了解析 profile 身份字段,C++ 结构二进制布局变化;Native/RN 配套版本必须同源码重编。没有用 `.13` 旧 AAR 验证该结构。 +- `decodedByteSize()` 只计原始 plane storage 与 validity bitset,不含 profile 元数据、CPU 物化数组、图像、GPU texture、粒子/trail。没有执行 CPU/GPU 峰值或长时间泄漏测量。 +- 单帧 Particle 的实际 GPU 运动、GL/Metal texture 复用、context loss 和设备视觉尚待平台实测。 + +## RDC 分阶段执行命令(本轮未运行) + +### B:Shared Core Store / Ingress / Binding + +```bash +ninja -C build-step088-opengl mbgl-test-runner +build-step088-opengl/mbgl-test-runner --gtest_filter='ParticleFieldRuntime.*:ParticleFieldBinding.*:ParticleEncodedFieldIngress.*' +``` + +### C:Projection / Source / Renderer 与 OpenGL、Metal host 回归 + +```bash +ninja -C build-step088-opengl mbgl-test-runner +build-step088-opengl/mbgl-test-runner --gtest_filter='Particle*.*:Source.RasterArray*' +ninja -C build-step097-metal-host mbgl-test-runner +build-step097-metal-host/mbgl-test-runner --gtest_filter='Particle*.*:Source.RasterArray*' +``` + +先完成 B 并检查结果,再执行 C;Metal host GTest 不等于 iOS 设备 Metal 通过。未通过/退出未知时停在原阶段。Codex 本轮只运行 `git diff --check`,未运行上述命令、Android Gradle、iOS/Xcode、Jest、设备或发布。 + +### D:RN / 平台验证前置条件 + +RN 端确认现有顺序为成功 await `submitEncodedField(...)` 后才 await `setEncodedForecastTime(...)`;失败提交不得调用 setTime。T17 使用 V15 同源码 Native/RN 配套构建和 Host 手工验证,不能用现有 `.13` AAR 替代。 + +Windows 11 用户手工验收(待相同 V15 Test Host APK、RN 包与 Native AAR 可用后): + +```powershell +cd D:\React\MapLibreParticleRNTestHost-V4 +npm install +npx expo prebuild --platform android +cd android +$env:ANDROID_HOME='C:\Users\taibe\AppData\Local\Android\Sdk' +$env:ANDROID_SDK_ROOT=$env:ANDROID_HOME +.\gradlew.bat :app:assembleDebug +adb -s 192.168.200.139 install -r <同源码V15 Test Host APK路径> +adb -s 192.168.200.139 shell am force-stop org.maplibre.particle.rntesthost +adb -s 192.168.200.139 shell am start -n org.maplibre.particle.rntesthost/.MainActivity +``` + +在 Host 可复用的本地 QBIN 场景中一次完成:首帧单帧显示/粒子持续运动、标量无粒子、第二帧中点 mix、乱序 8 帧、未来/过去/内部插入与淘汰、跨 reference/profile 日切首帧及回退、duplicate/revision/stale/损坏帧、wind/wave key 隔离、当前 endpoint 淘汰清场、100+ 次滚动及 clear。逐项记录真实请求时间与实际显示 validTime、T0/T1/mix、提交结果、异常日志、进程内存趋势。ADB 全部显式指定 `192.168.200.139`;不得将 Test Host 冒充 NavSea 验收。 + +iOS 由 Mac 用户使用同一冻结源码和配套产物手工构建;在 Metal 设备验证单帧首显、单帧向量运动、双帧 mix、跨批次 unavailable、双向淘汰、key 隔离、坏帧不覆盖好投影、clear/context 恢复及长时间内存趋势。Metal host runner 结果不能替代该设备证据。 + +## T01–T17 状态 + +测试覆盖映射见设计文档 §6。以下“代码覆盖”仅说明测试源码中存在场景;**实际运行均为 NOT RUN**。 + +| 项 | 代码覆盖状态 | 实际运行 | +| --- | --- | --- | +| T01 | 已覆盖 | NOT RUN | +| T02 | 已覆盖 | NOT RUN | +| T03 | 已覆盖 | NOT RUN | +| T04 | 已覆盖 | NOT RUN | +| T05 | 已覆盖 | NOT RUN | +| T06 | 已覆盖 | NOT RUN | +| T07 | 已覆盖 | NOT RUN | +| T08 | 部分覆盖;现有 revision/stale 入口 + 满额分支,尚无单一 T08 全覆盖测试 | NOT RUN | +| T09 | 部分覆盖;profile/decode/numeric/grid 拒绝及原子性,错误向量仍由既有 binding 测试覆盖 | NOT RUN | +| T10 | 部分覆盖;pair mix 0.25/0.5/0.99 已覆盖,精确 T1 以单帧表示 | NOT RUN | +| T11 | 已覆盖 | NOT RUN | +| T12 | 已覆盖 | NOT RUN | +| T13 | 已覆盖 | NOT RUN | +| T14 | 已覆盖 | NOT RUN | +| T15 | 部分覆盖;materialization/renderer 源码有覆盖,GPU/context loss 未运行 | NOT RUN | +| T16 | 部分覆盖;120 次 Core Store 双向滚动/clear 及 byte count 已覆盖,Runtime 切换/GPU 泄漏未运行 | NOT RUN | +| T17 | 部分覆盖;bridge 静态检查,自动化成功/失败序列与配套消费构建未运行 | NOT RUN | + +## 当前结果与下一步 + +- 当前结果:**源码已实施,等待 RDC 阶段 B 独立构建和测试;没有 PASS 结论。** +- `git diff --check`:通过(只代表空白/patch 格式检查)。 +- 未完成:所有新增 C++ 测试执行、OpenGL/Metal host 构建、Android/RN 同版本配套构建、iOS/Metal 设备验证、窗口拖动/跨天/内存实测、V15 pipeline、commit/push。 +- 剩余阻塞:当前仅为 RDC 尚未运行本轮实现;`.13` 不能与新增 C++ Frame layout 配对,需后续同源码新版本构建。 +- 下一责任:RDC 按 B→C 顺序执行并反馈真实日志;Android、iOS 设备与 Test Host 验收在后续配套阶段完成。当前停止于 Codex 源码交付,不进入 V15 pipeline。 + + +## 2026-09-27 RDC 独立构建与定向测试补充(覆盖“本轮 NOT RUN”的后续事实) + +- Codex `turn.completed` / exit 0 后,RDC 先执行 B;初次 runner 无 ninja PATH、Source 测试访问 private store、两处 fixture Grid 不合法,均已按首个真实错误做定向修复,记录见 `docs/报告/STEP-098_RDC定向验收报告_v1.md`。 +- B OpenGL Core/Binding/Ingress 编译 PASS,19/19 GTest PASS。 +- C1 OpenGL Host 扩展 `Particle*.*:Source.RasterArray*` 119/119 PASS。 +- C2 Metal Host 编译 PASS,同 filter 119/119 PASS。 +- 源级测试不等于 Android RN consumer/iOS 真机 GPU、内存/交互通过;V15 Pipeline 此时尚未启动。下一关必须为同源码发布构建和分端实测。 diff --git a/docs/50_Step记录/STEP-099_RenderSource就绪与RenderSegment失效诊断_v1.md b/docs/50_Step记录/STEP-099_RenderSource就绪与RenderSegment失效诊断_v1.md new file mode 100644 index 000000000..6f7a67e7d --- /dev/null +++ b/docs/50_Step记录/STEP-099_RenderSource就绪与RenderSegment失效诊断_v1.md @@ -0,0 +1,175 @@ +# STEP-099 RenderSource 就绪与 RenderSegment 失效诊断 + +状态:**CLOSED / PASS** +日期:2026-10-06(JST) + +## 基线 + +- 仓库:`/Users/tei/dev/MapLibreParticleLab-step099` +- 分支:`step099-render-source-diagnostics` +- frozen release:`maplibre-particle/rn/13.4.1-particle.17` +- frozen release commit:`e8b451fe70054107773e82484c80e90df3889147` +- implementation commit:`8a5214d92dd2f8899db76d2194da42999fa0c87d` +- Integration/NavSea:未修改。 + +## 本 Step 完成内容 + +- Renderer readiness snapshot additive 暴露 style/image manager/render orchestrator/sprite readiness、fading、global content revision、per-source 状态和最近 mutation。 +- per-source diagnostics 暴露 sourceId/type/enabled/isLoaded;tile-backed source 提供 tileCount/incomplete/loaded/fading。 +- RenderSegment diagnostics 只读推导 `dependentSourceIds` / `unknownDependencies`;source-less layer 不制造 source dependency,共享 source 可出现在多个 segment。 +- mutation sequence/source/tile action/reason 只消费既有 renderer 事件;正式 revision 增长逻辑不变。 +- 正式 loaded 判定保持: + `renderTreeParameters->loaded = updateParameters->styleLoaded && isLoaded();` +- diagnostics snapshot 不进入正式 loaded/cache/fast path/segment execution。 +- RN 仅增加 diagnostics 类型字段;没有 NavSea 私有 API,没有改变业务 API。 +- Android Test App 提供 T1 STATIC、T2 GEOJSON_1HZ、T3 PERMANENT_UNREADY、T4 R-C-R。 + +## Native 验证 + +Metal host `RenderSegmentDiagnostics.*`: + +- 6/6 PASS; +- 新增 `ReadinessSnapshotMapsSourcesToSegmentsWithoutSideEffects` PASS; +- 覆盖 source readiness、tile stats、source→segment、shared source、source-less 与重复 query 无副作用。 + +最终源码再次尝试 Metal configure 时当前 Mac 缺 `libuv` pkg-config,因此第二次 host rerun 未进入 build;不虚报 PASS。最终代码已经被 Android 全量 native build 成功编译并由 T1~T4 运行覆盖。 + +Vulkan:因当前 Mac 缺 `libuv`,NOT RUN tests。 + +Jest:NOT RUN,用户未要求。 + +## Android 构建 + +首轮 Android NDK 暴露: + +```text +error: missing field 'tileCount' initializer +[-Werror,-Wmissing-designated-field-initializers] +``` + +只补全 `RenderSourceReadinessStats` 默认空字段显式初始化。 + +随后 native build 通过,但 strip 阶段因 Mac 磁盘仅剩约 2 GiB: + +```text +No space left on device +``` + +仅清理可再生 Gradle 9.5.1 cache / 失败中间产物后恢复约 12 GiB,再次增量构建: + +- `BUILD SUCCESSFUL in 4m 54s` +- 86 actionable tasks:7 executed / 79 up-to-date +- OpenGL Debug / x86_64 +- APK:61,923,024 bytes +- libmaplibre.so:39,602,856 bytes +- SHA-256: + `5de73f0358b835a78d3ae7e2ac22f4efdb2a66775b8c63654a8bb7313957d90a` + +设备: + +- `192.168.200.139:5555` +- FZG1-2 +- Android 13 / API 33 / x86_64 + +## T1~T4 结果 + +### T1 STATIC + +- 1 个启动 false window; +- avg/max = 204 ms; +- 随后 global loaded 稳定为 true; +- retained cache 可持续 reuse; +- fast path 可进入 active。 + +### T2 GEOJSON_1HZ + +- 两个 GeoJSON source 交错更新,总 update 122 次,约 61 次/源; +- 10 个 loaded=false window; +- avg 203 ms; +- max 205 ms; +- open 0; +- 约 60 s 中 false 总时长约 2.03 s,约 3.4%。 + +结论:存在 readiness thrash,但不是“几乎永远 false”。 + +### T3 PERMANENT_UNREADY + +`step099-unready-raster-array`: + +- 无 layer; +- enabled=false; +- isLoaded=false; +- 其他主要 source loaded。 + +结果: + +- global renderOrchestratorLoaded=false 持续; +- 观察结束 open false window = 21,459 ms; +- retained cache 持续 style-not-loaded fallback。 + +结论:单个与 segment 无关的 permanently unready source 足以拉低 V17 全局 loaded。 + +### T4 R-C-R + +- R0 deps=[step099-static] +- C deps=[particle-fixture] +- R1 deps=[org.maplibre.annotations, step099-dynamic, step099-static] +- dynamic update:61 次 + +结果: + +- loaded false windows:4 +- avg 203 ms +- max 207 ms +- contentRevision:1760 → 2540 +- diagnostics 对 dynamic source 正确给出 affected=[2] +- R0 不依赖 dynamic source,但: + - build 1 → 121 + - invalidation 20 → 200 + +结论:dependency diagnostics 已能表达 source-local 影响,但 V17 正式 content revision/cache invalidation 仍为 global。 + +## Q1~Q7 + +1. Q1:**YES**。permanently unloaded source 会造成全局 style-not-loaded。 +2. Q2:**YES**。1 Hz GeoJSON 会产生约 200 ms readiness false window;T2 avg 203 / max 205 ms。 +3. Q3:**NO(本次观测)**。双 1 Hz source 并未让 global loaded 几乎始终 false。 +4. Q4:**YES**。GeoJSON tile mutation 推进 global content revision,并使 retained cache 重建/失效。 +5. Q5:**YES**。不引用动态 GeoJSON 的 R0 仍受 global revision 影响;源码还确认 `retainedRangeIsStable` 全局检查所有 source fading。 +6. Q6:fast path 仍有 global update signature(sources/layers/images 等)、global content revision、global fade、pendingChanges、placement/light/non-particle transition/repaint、annotation mutation、retained revision/cache/topology/boundary,以及 singleContinuous gate。 +7. Q7 blocker: + - runtime confirmed:`PERMANENT_SOURCE_UNLOADED`、`DYNAMIC_SOURCE_READINESS_THRASH`、`GLOBAL_CONTENT_REVISION` + - source confirmed:`GLOBAL_FADE`、`GLOBAL_UPDATE_SIGNATURE` + - OTHER:pending/placement/light/repaint/annotation/topology/boundary/singleContinuous。 + +## 证据 + +忽略目录: + +`artifacts/maplibre-particle/android-validation/` + +- `build-info.txt` +- `build.log` +- `MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk` +- `step099-logcat.txt` +- `step099-device-info.txt` +- `step099-metal-gtest.log` + +STEP-099 log SHA-256: + +`0f15d75dde77e7dd2f4d98c46a4c1501a628205e8aae62130b0cb96db6b8482f` + +## 边界结论 + +本 Step 未修改: + +- retained cache 算法; +- fast path gate; +- singleContinuous / multi-continuous; +- segment-local invalidation 正式语义; +- RenderSegment execution; +- NavSea; +- Source isLoaded; +- RN 业务 API。 + +下一阶段应拆分处理 global source readiness、source/segment-local content revision/invalidation,再独立处理 fast-path global gates。 diff --git a/docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md b/docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md new file mode 100644 index 000000000..8afa14da6 --- /dev/null +++ b/docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md @@ -0,0 +1,132 @@ +# STEP-100 SourceScoped RenderSegment 局部失效记录 v1 + +状态:**CLOSED / PASS(Android runtime)** +日期:2026-10-06 + +## 基线 + +- worktree:`/Users/tei/dev/MapLibreParticleLab-step100` +- branch:`step100-segment-local-source-invalidation` +- start HEAD:`c4b481fd2c5b5da20d31f6ae54011f0552d7dbd0` +- V17 tag:`maplibre-particle/rn/13.4.1-particle.17` +- V17 frozen commit:`e8b451fe70054107773e82484c80e90df3889147` + +## 实现 + +- Source/Tile mutation 增加 source-scoped revision; +- global retained content revision 继续保留; +- production renderer 与 diagnostics 共用 source→segment dependency; +- RETAINED Segment 独立 readiness / stability / Build / Reuse / Fallback; +- unrelated source unloaded/fading 不再拖垮该 Segment; +- full CPU mixed execution 支持 R0 Reuse + C Live + R1 Fallback; +- Fast Path gate、singleContinuous、多 Continuous语义未改; +- Source::isLoaded 与 global renderTree loaded 语义未改。 + +## Android Build + +标准 validation pipeline 最终: + +```text +RC=0 +BUILD SUCCESS +variant=OpenglDebug +abi=x86_64 +apk_bytes=61956640 +libmaplibre_bytes=39636472 +sha256=86e22bf081bf7d3196f0bf0e7c56af51bde01e9f0b01e0bab238cb794273315a +``` + +RDC 另修复 validation script 的 pipefail/SIGPIPE 141: +`awk ... {print $1; exit}` → 完整消费管道。 + +## FZG1-2 + +- Android 13 +- API 33 +- x86_64 +- ADB:`192.168.200.139:5555` + +### T1 STATIC + +- false window:1 +- avg/max:203 ms +- R0/R1 build=1 后持续 reuse + +### T4 R-C-R 主验收 + +窗口: +- baseline:tick 8 +- final:tick 130 +- 约 61.6 s + +global revision: +`128 → 973` + +R0: +```text +build 1 → 1 +invalidation 20 → 20 +reuse 132 → 4042 +sourceRevision unchanged +``` + +R1: +```text +build 5 → 135 +invalidation 26 → 211 +``` + +结论: +- R0 buildDelta=0 +- R0 invalidationDelta=0 +- R0 reuseDelta=+3910 +- global revision 仍增长 +- dynamic source affected=[2] + +**主硬门槛 PASS。** + +### T2 GEOJSON 1HZ + +- 约 35 s +- falseWindowCount=6 +- avg=203 ms +- max=205 ms +- R0 build=1 / invalidation=18 保持稳定,reuse 增长 + +### T3 PERMANENT UNREADY + +global loaded=false 持续约 21.468 s。 + +unready source: +- no layer +- enabled=false +- isLoaded=false + +R0/R1: +- build=1 不变 +- invalidation=19 不变 +- reuse:11 → 1200 +- decision=reuse + +说明 global loaded 兼容事实仍为 false,但无关 retained cache 继续安全复用。 + +## Native Host Test + +Codex 已新增 `RenderSegmentLocality.*` 测试源码。 + +当前 Intel Mac 缺 `libuv`,Metal host configure 被环境阻塞。RDC 曾尝试 Homebrew 安装,但发现会连带升级 CMake/OpenSSL/Python 且 Intel 无 bottle 支持,已主动停止。 + +因此: +- Native host GTest:ENV BLOCKED / NOT RUN +- Android full native build:PASS +- Android runtime:PASS + +Jest:NOT RUN。 + +## 结论 + +STEP-100 功能目标成立: + +> GeoJSON Source 更新只刷新依赖它的 retained segment;无关 retained segment 不再一起刷新。 + +允许进入 V18 unified release。 diff --git a/docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md b/docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md new file mode 100644 index 000000000..03e00b1d8 --- /dev/null +++ b/docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md @@ -0,0 +1,54 @@ +# STEP-101 RenderOrchestrator 销毁生命周期修复记录 v1 + +状态:**CLOSED / PASS(Native + Android runtime)** +日期:2026-10-07 + +## 目标与基线 + +- 工作区:`/Users/tei/dev/MapLibreParticleLab-step101` +- 分支:`step101-render-orchestrator-teardown` +- start HEAD:`d29e84c1a90daeea104b389d28b000c4f017d08d` +- implementation commit:`64cf8f44350eb8ec4b45360db4d3f9505ac4ac0b`(`fix: make render orchestrator teardown safe`),已 push 到 `origin/particle/main` 并回读一致。 +- 工作合同:`docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v2.md`,第 13 节优先于前文冲突项。 +- V18 为不可变已发布版本;本阶段未创建、发布或标记 V19。 +- 目标:修复 RenderSource/Tile 销毁时 observer 可能回调已析构的 RenderOrchestrator readiness mutex/state 的生命周期漏洞。 + +V18 成员声明使 readiness mutex/state 早于 `renderSources` 自动析构。Tile 销毁时同步发出 `Cancelled` 回调的路径可达,且与现场 Android destroyed mutex SIGABRT 高度吻合;没有完整符号化栈与修复后等价实机结果,暂不把它写成唯一确认的现场根因。 + +## 必做项进度 + +- [x] `RenderSource::setObserver(nullptr)` 恢复 NullObserver,保留正常 observer 转发。 +- [x] Orchestrator 析构:保留 contextLost layer handling;第一次 tagged drain;detach RenderSource、GlyphManager、ImageManager observer;在 readiness state 存活时清理 sources;第二次 tagged drain。 +- [x] 静态审计独立 render-job 队列:`waitForEmpty()` 不排空该队列;OpenGL context 析构的 `runRenderJobs(true)` 属于后续 backend 清理,测试中也单独调用 `runRenderJobs()`。不能以双 drain 代替 render-job 验收。 +- [x] 新增非 Jest C++ 测试源码:默认/真实/detach observer、三种 tile callback、Cancelled、source 清理后第二次 drain、render-job 单独处理和重复 teardown。helper 像生产代码一样注册 Orchestrator observer,并断言 source 销毁前旧 observer 已被替换。 +- [x] Android Validation 增加 T1 RESET_ONCE、T2 RESET_LOOP_20、T3 RESET_WITH_DYNAMIC_GEOJSON、T4 RESET_WITH_DIAGNOSTICS;通过 Activity recreate 触发 MapView 销毁/新建,并输出 SurfaceView detach、nativeReset、resetRenderer、style ready 与 diagnostics 日志供逐次配对。 +- [x] 只读审计正常 `renderSources.erase()` / `clearData()`:两者在 Orchestrator 存活期间执行;TileCache 析构等待其延迟释放任务结束。本阶段没有扩大其行为修改,运行态异步边界仍待 RDC 验证。 +- [x] 静态核对 STEP-100 locality、Fast Path、`singleContinuousSegment`、`Source::isLoaded()` 等代码未被本次 diff 修改。 +- [x] Native:Android full native build PASS;focused Android C++ TestRunner 6/6 PASS。Mac host 因 libuv 环境阻塞,未虚报。 +- [x] Android Validation APK build PASS;T1~T4 实机 PASS;T2 完整 20 次真实 reset/recreate,crash buffer 为空。 +- [x] STEP-100 T4 R-C-R locality 回归 PASS:R0 buildDelta=0、invalidationDelta=0、reuseDelta=+3878。 +- [ ] V19 candidate、NavSea 五次冷启动与正式 V19 发布:属于后续发行 Gate,尚未执行。 + +## 允许与禁止范围 + +实际变更限于 Orchestrator/RenderSource 生命周期、对应 C++ 测试、Android Validation 场景及 Java/JNI reset 观测日志。未修改 NavSea、provider/network、STEP-100 locality 实现、发布坐标或 V18 产物。未运行 Jest。 + +## 检查与当前结果 + +本次验证仅包含源码 diff 静态审计与 `git diff --check`。构建、链接、GTest、Gradle/APK、ADB、设备、NavSea、发布均 **NOT RUN**;源码交付不能记为运行态 PASS。完成报告:`docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md`。 + +## 阻塞与下一步 + +当前没有已观测编译失败;实际编译与场景行为仍未知。由 RDC 按完成报告的命令独立编译、运行 Native 测试、Android T1~T4、STEP-100 locality,并核对每次 `RESET_REQUEST` 对应 SurfaceView detach、`nativeReset`、`resetRenderer` 和新 renderer ready。随后才进入 V19 candidate 与 NavSea 等价验收;全部门槛满足后才可考虑正式发布。 + + +## RDC 最终证据 + +- Android Validation APK:61,957,678 bytes;`libmaplibre.so` 39,637,512 bytes;SHA-256 `14cbd35ed0bc1366389ddea69925901013e3b209ed3575462421a7920c73c721`。 +- T1~T4 teardown:全部 PASS;T2 20 次 reset;动态 GeoJSON / diagnostics 条件均无 crash。 +- STEP-100 locality:global revision 62→894;R0 build 1→1、invalidation 19→19、reuse 12→3890;PASS。 +- Android C++ focused TestRunner:4 个 `RenderSegmentLocality` + 2 个 `RenderOrchestratorTeardown` 全部 PASS;status=0。 +- Mac host GTest:ENV BLOCKED(本机缺可用 libuv pkg-config),不影响 Android full native/device 闭环结论。 +- Jest:NOT RUN。 + +**STEP-101 CLOSED / PASS。** 下一阶段进入 V19 candidate → NavSea 等价冷启动 Gate → 不可变公开发行。 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..1290bd790 --- /dev/null +++ b/docs/50_Step记录/STEP-102_RenderSegment无Placement段局部复用_v1.md @@ -0,0 +1,60 @@ +# STEP-102 RenderSegment 无 Placement 段局部复用记录 v1 + +状态:**进行中(代码完成,等待独立构建与运行验证)** +日期:2026-10-08 + +## 目标与基线 + +- 工作区:`/Users/tei/dev/MapLibreParticleLab-step102` +- 分支:`step102-placement-free-retained-locality` +- 起点 HEAD:`6df21dac38f072c5ad35b8dfc7ec5217509602bc` +- 工作合同:`docs/40_任务/STEP-102_RenderSegment无Placement段局部复用_Codex任务_v1.md` +- 设计依据:`docs/20_调查/RenderSegment多Retained段全局Placement门槛_粒子管理员审查说明_v3.md` +- 发布坐标:V19/V20 工作区与发行内容冻结;本 Step 不创建版本。 + +目标是让结构上没有 symbol layer 的 Retained Segment 在其他段 placement 活跃时继续独立缓存并经 main-buffer composite。 + +## 必做项 + +- [x] 基于 style layer type 查询结构性 placement capability;不使用当前帧 `needsPlacement()` 证明 placement-free。 +- [x] full CPU 每段计算 placement gate;含 symbol 的段仍受 changed、fade 与 transition 保守门槛约束。 +- [x] `segmentSupported` 和 fallback reason 按段生成;full CPU global-live 只移除纯 aggregate placement 条件。 +- [x] 保留 3D、debug、MapMode、sprite/image、light 等真实全局门槛和既有 revision/key 失效。 +- [x] diagnostics 增加结构、逐段 gate、实际 main-buffer 执行及全局 placement/execution 字段;RN TS 只增加诊断字段。 +- [x] 新增 C++ 源码测试与 Android STEP-102 Validation fixture/日志场景。 +- [x] 静态核对 Fast Path、mixed executor、C-001、RN method surface 未修改。 +- [ ] C++ 编译与定向测试、Android 用户手工构建、设备场景、iOS/Metal 验证:尚未执行。 + +## 允许与禁止范围 + +代码修改仅在任务书允许的 renderer、RN diagnostics 类型、`retained_underlay_cache.test.cpp` 和 Android Particle Validation 文件。未修改 `visitRenderSegmentExecution()`、`tryRenderParticleFastFrame()`、`retainedRangeUsesPlacement()`、Placement 算法、NavSea、C-001、发布文件或总则。既有未跟踪 v2/v3 审查说明及任务书保持原样。 + +## 构建、测试与当前结果 + +只读源码/diff 审计及 `git diff --check` 已执行,后者通过;新增未跟踪文件另用 `git diff --no-index --check` 检查,无空白错误(差异本身使退出码为 1)。CMake/Ninja/Bazel、GTest、Android Gradle、APK/AAR、ADB、设备、Jest、NavSea、发布全部 **NOT RUN**。静态检查不构成运行态 PASS。 + +## 必做输出 + +- STEP-102 生产代码、C++ 测试源码、Android Validation 场景源码。 +- `docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md`。 +- 本记录与 `粒子开发记录.md` 追加。 +- 报告中列出精确后续验证命令、硬门槛、NOT RUN 与剩余风险。 + +## 阻塞与下一步 + +目前没有已观察到的编译或运行失败;真正结果仍未知。由 RDC/用户按报告完成 Native focused tests、用户手工 Android APK 构建和 T1/T2 设备场景,核对 R0/R1 的 `placementCapable`、`placementGatePassed`、`actualExecution` 与 build/reuse/invalidation。构建或设备失败时保留首个真实错误与日志,再按任务边界修复;本 Step 未进入后续发行阶段。 + +## RDC 独立验证补记(2026-10-08) + +- 从现有工作树、任务书、v3 审查依据和项目总则恢复审查;原 Codex 阶段的 NOT RUN 记录保留,以下为新增 RDC 事实。 +- `git diff --check`:退出码 0,PASS;无关的 `platform/windows/vendor/vcpkg` 子模块状态变化不纳入本任务。 +- 以 AppleClang 对 `test/renderer/retained_underlay_cache.test.cpp` 执行 `-std=c++20 -fsyntax-only`:退出码 0,语法检查 PASS;不是 GTest 运行结果。 +- 本地临时独立 C++ helper smoke 测试:结构性 symbol 判定、placement gate、全局 live reason 共 10 个断言,10/10 PASS;测试文件位于 `/tmp/step102-placement-smoke.cpp`,不是正式 GTest。 +- Mac CMake `cmake --preset macos-metal`:配置退出码 1,阻塞于缺少 `libuv`;未进入 GTest 编译或运行,未安装/升级依赖。日志 `/tmp/step102-cmake-config.log`。 +- 额外审查并修复 Android STEP-102 Validation 切换入口:进入该场景时停止 STEP-093 诊断轮询、清除 STEP-093/097 标志,避免旧场景轮询污染 STEP-102 证据。 +- Metal Bazel Core 构建 `bazel build -c opt --//:renderer=metal //:mbgl-core` 已完成:退出码 0,642/642 actions,耗时 875.118s,产物 `bazel-bin/libmbgl-core.a`,构建 PASS;日志 `/tmp/step102-bazel-core.log`。该 PASS 仅表示 Native Core 编译,不代表 GTest/Android/运行态 PASS。 +- Android Gradle/APK/ADB 与设备验证依任务规则均未执行;STEP-102 继续保持“进行中 / 待 Android 实际验收”,不得标记 CLOSED / PASS。 + +## V21 发布决策(2026-10-08) + +用户明确接受未执行正式 GTest、Android T1/T2、iOS/Metal 运行态验证的风险,要求 STEP-102 经源码审查即纳入 V21 发行。代码审查未发现明确阻断项;先前 Metal Core 642/642 编译通过。本步骤依然不属于运行态验收 PASS。发布须完整执行 `prepare → build → publish → finalize`;发布记录应明确验收豁免。 diff --git a/docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md b/docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md new file mode 100644 index 000000000..e014a52d7 --- /dev/null +++ b/docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md @@ -0,0 +1,73 @@ +# V15 iOS WeatherColor 纹理生命周期修复 + +状态:**进行中(RDC Mac 定向验证 PASS;等待 iPad 人工验收)** +日期:2026-09-27(JST) + +## 目标与基线 + +- 仅修复 V15 WeatherColor 的 Metal 纹理绑定生命周期,不涉及 V12/V13、JavaScript、NavSea API、Android 行为或发行版本/tag。 +- 仓库:`/Users/tei/dev/MapLibreParticleLab`;机器:Mac mini、Darwin 24.6.0、x86_64;分支:`particle/main`。 +- 开工 HEAD:`cd71640a84901212ce4681e296f027befdff4ee0`;`maplibre-particle/rn/13.4.1-particle.15` peeled commit 与 HEAD 相同;`origin/particle/main` 同步于 HEAD。 +- manifest 的 V15 `sourceCommit` 为 `1fdc5df625d405233703136eb2dc12032d932da2`;后续 finalize release 生成的 V15 tag 指向 HEAD `cd71640a84901212ce4681e296f027befdff4ee0`。这两个坐标分别表示 manifest 源码提交与后续 tag HEAD,不构成冲突;本任务相关已跟踪源码在两提交间相同,tag 与发行内容未改。 +- 开工时仅有用户提供的 V15 任务书为未跟踪文件;始终保留。现有 submodule 均已初始化,本轮未修改。 +- 执行模型/推理档位:GPT-6 Luna High。 + +## 根因与修改 + +Metal LayerGroup 在 drawable 的 `upload()` 后才执行 Tweaker。旧实现于 Tweaker 的 `execute()` 才向既有 drawable 调用 `setTexture()`;当 endpoint 纹理已在 WeatherColor `update()` 中更换、wrap 范围不变时,drawable 的上传阶段仍持有旧纹理,随后 draw 才首次绑定新纹理,可能触发 `assert(!textureDirty)`。 + +本轮修改: + +- 在 `update()` 进入资源更新时保存 drawable 原有 T0/T1 实际纹理指针;资源 endpoint 安装完成后比较新旧指针。 +- 把 wrap 区间变化或任一纹理 endpoint 指针变化统一作为重建条件;重建在 WeatherColor update 阶段执行,builder 在 drawable 上传前安装正确纹理。 +- 移除 WeatherColor Tweaker 的三处 `setTexture()`;Tweaker 只写 UBO 中的 mix、opacity、color stops、scale/bounds 等参数。 +- 新增 `WeatherColorTextureLifecycle` 定向测试,覆盖无变化、单端/双端替换、T0/T1 交叉复用、T0=T1 合并/保持、wrap 改变、隐藏期间 endpoint 替换后恢复、空资源首次创建与释放。 + +动态 mix/opacity/stops 不参与 endpoint 比较,因此这些值单独变化不会触发 drawable 重建或纹理上传。Resources 清空后下次更新会重新创建纹理和 drawables;隐藏期间不更新时,恢复后的 endpoint 比较仍在下一次 update、drawable upload 之前发生。 + +## 修改文件 + +- `include/mbgl/renderer/layers/weather_color_texture_lifecycle.hpp`(新增) +- `src/mbgl/renderer/layers/render_weather_color_layer.cpp` +- `test/renderer/weather_color_texture_lifecycle.test.cpp`(新增) +- `test/CMakeLists.txt` +- `docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md`(新增) +- `docs/报告/V15_iOS_WeatherColor纹理生命周期修复_Codex完成报告_v1.md`(新增) +- `粒子开发记录.md`(追加) + +未修改任务书、用户其他文件、公共 API/ABI、Style Z-order、shader slot、版本锁、release tag、ZIP/manifest、Android、RN/JS、NavSea、V12/V13 或粒子 advection/State/Trail/Retained 代码。 + +## 本阶段检查与状态 + +- 已完成只读源码调用链复核和 diff 静态核对准备。 +- `git diff --check`:由 Codex 执行,结果记录于完成报告。 +- RDC 首次 OpenGL runner build:**FAIL**,`weather_color_texture_lifecycle.test.cpp:60:17` 因 `nullptr` 参数无法推导模板类型;RDC 反馈 production renderer object 已编译。Codex 仅为该断言显式指定 ``。RDC 随后 OpenGL build retry exit 0、定向 GTest 6/6 PASS;Metal host build exit 0、定向 GTest 6/6 PASS。原始日志位于 `/tmp/navsea_v15_weathercolor_validation/`(`opengl_build.log`、`opengl_build_retry.log`、`opengl_test.log`、`metal_host_build.log`、`metal_host_test.log`)。Codex 未运行构建或测试。 +- iOS App/Xcode 编译、签名、安装、Metal 真机渲染和 iPad 事故场景:**等待用户手工验收**。 +- 现有仓库内未找到 `STEP_RECORD.md`;按根 `AGENTS.md` 追加 `粒子开发记录.md`,没有新建或猜造该文件。 +- RDC host 验证已完成,用户授权的 scoped commit/push 已完成;当前只等待用户手工 iPad 验收。 + +## RDC Mac 定向验证命令 + +先按 OpenGL,再按 Metal host 顺序执行: + +```bash +cmake --build build-step088-opengl --target mbgl-test-runner --parallel 3 +build-step088-opengl/mbgl-test-runner --gtest_filter='WeatherColorTextureLifecycle.*' +cmake --build build-step097-metal-host --target mbgl-test-runner --parallel 3 +build-step097-metal-host/mbgl-test-runner --gtest_filter='WeatherColorTextureLifecycle.*' +``` + +RDC 已按顺序完成 OpenGL 与 Metal host 验证,结果均 PASS(各 6/6)。Metal host 测试仅验证 host 可执行代码,不等于 iOS 设备 Metal PASS。当前等待用户手工 iPad 验收;本阶段不再构建。 + +## Git 收尾 + +- scoped 实现提交:`5af91fe7a105f0ac047e18db1dc063c9ffd69d44`(`fix: 修复 V15 WeatherColor Metal 纹理生命周期`),仅纳入本任务 7 个文件。 +- 已推送 `origin/particle/main`;`git ls-remote` 回读的远端 HEAD 为 `5af91fe7a105f0ac047e18db1dc063c9ffd69d44`。 +- 验证结果补录提交:`4712e1752af2a8238def5839c8754270de4195c9`(`docs: 记录 V15 WeatherColor RDC 验证与推送`),包含 Step、完成报告、开发记录;推送后远端 HEAD 已回读为该提交。 +- 未跟踪 V15 任务书未提交;V15 release tag 和发行内容未改。当前仍 **WAITING FOR MANUAL IPAD VALIDATION**。 + +## 2026-09-27 v16 发行构建兼容补充 + +- 统一发行 v16 的 Android 四 ABI 与 Maven Local 构建通过;首次 iOS Bazel 编译在 render_weather_color_layer.cpp 报找不到新增的 weather_color_texture_lifecycle.hpp。 +- 根因:bazel/core.bzl 的 MLN_CORE_HEADERS 为显式清单,新增头文件未列入;仅追加该头文件路径,不改 WeatherColor 算法或公开 API。 +- iOS Bazel 构建需由 RDC 在同一 v16 独立 worktree 重新验证;未通过前不得宣称 v16 发布或 iPad 修复通过。 diff --git a/docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v1.md b/docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v1.md new file mode 100644 index 000000000..d72806b1e --- /dev/null +++ b/docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v1.md @@ -0,0 +1,193 @@ +# 30-01 Core RenderLayer 接入最小技术验证报告 v1 + +日期:2026-08-29 + +当前状态:`WAITING_MANUAL_VALIDATION` + +本报告只记录阶段 A 的代码准备与静态证据。Android OpenGL 编译、安装、运行、手势和日志结果尚未由用户回填,因此不得标记 PASS。 + +## 1. 验证范围 + +本验证只回答:独立 Core Style Layer/RenderLayer 是否能进入正式 RenderTree 顺序、读取同帧 transform、在 ACTIVE 时通过 MapLibre 自身 loop 请求下一帧,并在 STATIC 时停止额外 repaint。 + +未实现速度场、粒子状态、advection、GPU ping-pong、Trail、Mask、真实天气数据或 compute shader。 + +## 2. 最小验证实现 + +- Style type:`particle-validation`。 +- Style class:`style::ParticleValidationLayer`,明确标记 `VALIDATION ONLY`。 +- Render class:`RenderParticleValidationLayer`。 +- Source:不要求 source;`LayerTypeInfo::Source::NotRequired`。 +- Pass:`RenderPass::Translucent`。 +- 状态:style paint 字段 `particle-validation-active=true/false`,只用于本验证。 +- Drawable:一个不输出像素的验证 Drawable,用于进入真实 LayerGroup/pass 顺序并在 drawable tweaker 中取得当前 `PaintParameters`;不使用 `CustomLayerHost`,不构成外部 CustomLayer 路线。 +- Android API:没有新增 Java/JNI 产品 API;Android 只把该类型注册为 JSON style 可用的 core-only layer。 + +## 3. Style / RenderTree / pass 静态确认 + +实际链路为: + +```text +Android Test App inline Style JSON +→ style::conversion::Converter> +→ LayerManagerAndroid::getFactory("particle-validation") +→ ParticleValidationLayerFactory::createLayer +→ style::ParticleValidationLayer immutable Impl +→ RenderOrchestrator layer diff/order +→ LayerManagerAndroid::createRenderLayer +→ ParticleValidationLayerFactory::createRenderLayer +→ RenderParticleValidationLayer +→ source-less LayerRenderItem +→ RenderParticleValidationLayer::update +→ source-less LayerGroup(layerIndex) +→ validation Drawable(RenderPass::Translucent) +→ main translucent pass 的 LayerGroup 顺序 +``` + +Test App style 顺序固定为: + +```text +validation-background +→ validation-below(蓝色大圆) +→ particle-validation-core +→ validation-above(黄色小圆) +``` + +验证 Drawable 的 `layerIndex` 由 RenderOrchestrator 正式 style order 分配,LayerGroup 以同一 index 注册。运行日志同时记录 `layerIndex` 和 pass 内的 `currentLayer`。实际 Android 顺序仍需用户运行日志确认。 + +## 4. 同帧 Transform 静态确认 + +验证日志在 DrawableTweaker 的 `execute(gfx::Drawable&, PaintParameters&)` 中生成。该回调发生在 MapLibre main translucent pass 对应 LayerGroup/Drawable 的真实执行路径中,直接读取: + +- `PaintParameters::state.getSize()`:viewport; +- `PaintParameters::state.getZoom()`:zoom; +- `PaintParameters::state.getBearing()`:bearing; +- `PaintParameters::state.getPitch()`:pitch; +- `PaintParameters::transformParams.projMatrix`:当前 frame projection matrix 摘要; +- `PaintParameters::frameCount`:MapLibre frame number。 + +未创建独立 Camera 状态,未从 Android UI 层复制或推算 transform。 + +## 5. Repaint 链静态确认 + +本任务增加一个薄 Core hook: + +```text +RenderParticleValidationLayer::needsRepaint() +→ RenderOrchestrator::hasTransitions() 汇总 +→ RenderTreeParameters::needsRepaint +→ RendererObserver::onDidFinishRenderingFrame(needsRepaint) +→ Map::Impl::onDidFinishRenderingFrame +→ needsRepaint=true 时 Map::Impl::onUpdate() +→ AndroidRendererFrontend::update() +→ Android MapRenderer.requestRender() +→ 下一 MapLibre render frame +``` + +所有其他 RenderLayer 默认返回 false,因此现有 Layer 行为不变。 + +- ACTIVE:验证 Layer 可见且 active 时返回 true。 +- STATIC:active=false 时返回 false;地图自己的 transition、手势和 source 更新仍可请求 frame。 +- 未使用 Android Timer、Handler 循环、JS `requestAnimationFrame` 或第二套动画 loop。 + +## 6. 日志格式 + +日志关键字统一为 `ParticleValidation`,包括: + +- Style Layer create/destroy; +- RenderLayer create/destroy; +- ACTIVE/STATIC; +- validation Drawable create/destroy; +- 首帧及每 60 帧:frame counter、MapLibre frame、状态、repaint、pass、layer order、viewport、zoom、bearing、pitch、projection matrix 摘要。 + +预期 ACTIVE 样例: + +```text +ParticleValidation: render frame=60 mapFrame=... state=ACTIVE repaint=requested pass=translucent layerIndex=... currentLayer=... viewport=... zoom=... bearing=... pitch=... proj=[...] +``` + +预期 STATIC 样例: + +```text +ParticleValidation: state=STATIC ... +ParticleValidation: render frame=1 ... state=STATIC repaint=stopped ... +``` + +地图静止后不应继续出现该 STATIC 实例的周期性 `frame=60/120/...`。 + +## 7. Codex 已执行检查 + +### 7.1 通过 + +- `git diff --check`:PASS。 +- 新增/修改 C++ 文件 `clang-format`:PASS。 +- Android Manifest、layout、values XML 使用 Python 标准库只读解析:PASS。 +- Android `MapLibreMap.getCameraPosition/setCameraPosition` API 静态确认:PASS。 +- Linux OpenGL CMake 重新配置:PASS。 +- 精确编译以下 Linux Core 对象:PASS: + - `particle_validation_layer_factory.cpp.o` + - `particle_validation_layer.cpp.o` + - `render_particle_validation_layer.cpp.o` + - `render_orchestrator.cpp.o` + - default `layer_manager.cpp.o` + +### 7.2 未完成 + +- Linux OpenGL 全量增量编译:公共 `RenderLayer` 头变化触发 321 个对象重编译;在 `41/321` 无错误时主动停止,随后改为上述任务相关对象精确编译并通过。 +- Android OpenGL Gradle/NDK/Test App 编译:按指令书禁止 Codex 执行,等待用户手工完成。 +- Android 运行、日志、pan/zoom/bearing/pitch/resize、ACTIVE/STATIC:等待用户。 + +## 8. 用户手工编译 + +在项目 Linux 环境执行: + +```bash +cd /mnt/sda1/MapLibreParticleLab +source /root/.maplibre-particle-env +cd platform/android +RENDERER=opengl BUILDTYPE=Debug JOBS=3 make android-x86-64 +``` + +预期 APK: + +```text +platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk +``` + +若目标设备不是 x86_64,请将 `android-x86-64` 换成对应现有 Make target;不要切换 Vulkan,本任务平台固定 Android OpenGL。 + +## 9. 用户手工运行步骤 + +1. 安装并打开 `MapLibre Native Android TestApp`。 +2. 在 `Custom Layer` 分类打开 `Particle Core Layer Validation`。 +3. 确认页面显示蓝色大圆和其上的黄色小圆,初始状态文字为 ACTIVE。 +4. 过滤日志关键字:`adb logcat | grep ParticleValidation`;也可使用 Android Studio Logcat 搜索同一关键字。 +5. ACTIVE 静置至少 3 秒,确认 `frame=60/120/...` 持续增长。 +6. 分别执行 pan、双指 zoom、rotate、pitch,确认地图正常,并保存变化前后的 transform 日志。 +7. 旋转设备或改变模拟器窗口尺寸,确认 `viewport` 更新且页面正常。 +8. 点击 STATIC,静置至少 3 秒;确认不再周期性增长。 +9. STATIC 下再次 pan/zoom,确认地图仍会按正常地图事件重绘;操作停止后不继续产生验证层额外帧。 +10. 再点 ACTIVE,确认连续帧恢复。 + +## 10. PASS / FAIL 判定 + +### PASS 候选证据 + +- Style Layer、RenderLayer 和 validation Drawable 均创建; +- 日志显示 `pass=translucent` 且 layer order 与 style 顺序一致; +- viewport/zoom/bearing/pitch/projection 摘要随对应操作更新; +- ACTIVE frame counter 持续增长; +- STATIC 静止后停止周期增长,但地图手势仍正常; +- 无崩溃、黑屏、永久 continuous rendering 或普通地图破坏。 + +### FAIL/停止 + +若 layer 无法创建、无法进入 pass、transform 不是同帧、ACTIVE 无法持续、STATIC 无法停止,或必须改用 Overlay/Timer/大改 Map 主循环,则记录 FAIL 并停止后续 GPU/Mask/iOS 验证。 + +## 11. 当前结论 + +静态代码和 Linux 任务相关对象编译支持该路线具备可验证性,但缺少 Android OpenGL 实际编译与运行证据。 + +当前唯一合法结论:`WAITING_MANUAL_VALIDATION`。 + +不得进入下一项 GPU State A/B + Trail A/B 验证。 diff --git a/docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v2.md b/docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v2.md new file mode 100644 index 000000000..9f71fabd8 --- /dev/null +++ b/docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v2.md @@ -0,0 +1,110 @@ +# 30-01 Core RenderLayer 接入最小技术验证报告 v2 + +日期:2026-08-29 + +当前状态:`WAITING_MANUAL_VALIDATION` + +本版本记录用户 Android OpenGL 实机运行暴露的 DrawableBuilder 生命周期错误及其最小修复。修复后的 Android 编译和实机运行尚待用户重新执行,因此不得标记 PASS。 + +## 1. 本轮范围 + +本轮只修复 `RenderParticleValidationLayer::update()` 创建验证 Drawable 时遗漏 `DrawableBuilder::flush(context)` 的问题。 + +未实现粒子、速度场、GPU ping-pong、Trail、Land/Ocean Mask、Vulkan 或 iOS;未改变 Style 注册、RenderTree、LayerGroup、Transform 或 repaint 架构。 + +## 2. Android 实机失败事实 + +根据用户提供的 Android 实机日志结论,旧实现执行顺序为: + +```text +getCurrentDrawable(true) +→ 直接配置 custom Drawable 与 Tweaker +→ clearDrawables() +→ drawables.front() +→ LayerGroup::addDrawable() +→ RenderThread SIGSEGV +``` + +`getCurrentDrawable(true)` 只创建并保留 `currentDrawable`。`clearDrawables()` 只转移 builder 的已完成 `drawables` 容器,不负责完成当前 Drawable。旧实现因此从空容器取得 `front()`,产生未定义行为,并使空或无效 Drawable 进入 `LayerGroup::addDrawable()` 路径。 + +## 3. 仓库实现对照 + +当前 `gfx::DrawableBuilder::flush(gfx::Context&)` 在存在 `currentDrawable` 时,无论验证 Drawable 是否包含普通顶点,都会把当前 Drawable 移入已完成 `drawables` 容器。 + +仓库中的 Background、Raster、Fill、Line、Symbol、Heatmap 等 DrawableBuilder 使用方式均遵循: + +```text +配置 builder/current Drawable +→ builder->flush(context) +→ builder->clearDrawables() +→ LayerGroup::addDrawable(...) +``` + +验证 Drawable 的 custom 标志、translucent pass 与 Tweaker 直接配置在当前 Drawable 上;`flush(context)` 保留这些配置并完成所有权转移。 + +## 4. 最小修复 + +修复后的顺序为: + +```cpp +auto& drawable = builder->getCurrentDrawable(true); +drawable->setIsCustom(true); +drawable->setRenderPass(RenderPass::Translucent); +drawable->addTweaker(...); +builder->flush(context); +auto drawables = builder->clearDrawables(); +group->addDrawable(std::move(drawables.front())); +``` + +没有增加空指针判断或跳过 Drawable。验证 Drawable 仍会加入真实 source-less LayerGroup,并由 Tweaker 在 translucent render pass 内取得同帧 `PaintParameters` 和 Transform。 + +## 5. Codex 验证 + +- Linux OpenGL 精确对象编译:PASS。 + +```bash +CCACHE_DISABLE=1 cmake --build build-linux-opengl \ + --target CMakeFiles/mbgl-core.dir/src/mbgl/renderer/layers/render_particle_validation_layer.cpp.o \ + -- -j3 +``` + +- `clang-format --dry-run --Werror`:PASS。 +- `git diff --check`:PASS。 +- 首次对象编译因沙箱内 `ccache` 缓存目录只读而在编译器启动前失败;设置 `CCACHE_DISABLE=1` 后同一目标通过,不是源码编译失败。 +- Android/Gradle/ADB:未执行,遵循任务书限制。 + +## 6. 用户重新验证 + +手工编译: + +```bash +cd /mnt/sda1/MapLibreParticleLab +source /root/.maplibre-particle-env +cd platform/android +RENDERER=opengl BUILDTYPE=Debug JOBS=3 make android-x86-64 +``` + +打开 Test App:`Custom Layer` → `Particle Core Layer Validation`。 + +采集日志: + +```bash +adb logcat | grep ParticleValidation +``` + +必须重新确认: + +- 页面不再在 RenderThread 崩溃; +- validation Drawable create 后 Tweaker 真正输出 `render frame=... pass=translucent`; +- ACTIVE 帧持续增长; +- STATIC 停止验证层额外帧; +- pan、zoom、bearing、pitch、resize 的同帧 Transform 日志更新; +- 普通地图渲染和 Style order 不受影响。 + +## 7. 当前结论 + +DrawableBuilder 生命周期错误已按仓库正式 `flush(context) → clearDrawables()` 用法修复,Linux 任务对象编译通过。修复后的 Android OpenGL 实机证据尚未回填。 + +当前唯一合法结论:`WAITING_MANUAL_VALIDATION`。 + +不得进入 GPU State A/B、Trail、Mask、Vulkan 或 iOS 验证。 diff --git a/docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v3.md b/docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v3.md new file mode 100644 index 000000000..ca0811323 --- /dev/null +++ b/docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v3.md @@ -0,0 +1,55 @@ +# 30-01 Core RenderLayer 接入最小技术验证报告 v3 + +日期:2026-08-29 + +当前状态:`PASS` + +本版本仅回填用户已经完成的 Android OpenGL 人工验收结果;不重新运行 30-01,不修改 30-01 代码。 + +## 1. 验证范围 + +30-01 验证独立 Core `ParticleValidationLayer/RenderParticleValidationLayer` 能否通过 MapLibre 正式 Style、RenderTree、LayerGroup 与 translucent pass 链接入;从当前 render frame 获得 Transform;并在 ACTIVE/STATIC 状态下分别持续或停止验证层额外 repaint。 + +未实现正式粒子、Field、GPU ping-pong、Trail、Mask、Vulkan 或 iOS。 + +## 2. Codex 静态确认 + +此前 v1/v2 已确认并记录: + +- 独立 source-less Core Style Layer 经 LayerManager、RenderOrchestrator、RenderParticleValidationLayer 与 LayerGroup 进入 `RenderPass::Translucent`; +- validation Drawable Tweaker 直接读取当前 `PaintParameters`、`TransformParameters` 与 `TransformState`; +- ACTIVE 经 MapLibre `needsRepaint` 链请求下一帧,STATIC 停止验证层额外请求; +- Android Test App 提供 `Particle Core Layer Validation` 入口; +- DrawableBuilder 生命周期错误已由 `builder->flush(context)` 修复。 + +这些是代码与先前检查的静态事实,不替代设备运行证据。 + +## 3. 用户实际运行确认 + +用户已明确确认下列 Android OpenGL 实际验收事实: + +- 构建成功; +- `Particle Core Layer Validation` 已实际运行; +- 30-01 验证结果为 OK; +- 独立 Core RenderLayer 接入通过; +- ACTIVE / STATIC repaint 行为达到验证目的; +- 同帧 Transform,以及 pan、zoom、bearing、pitch、resize 的 30-01 验证范围满足要求; +- 可以正式判定 30-01 为 PASS。 + +用户未提供额外逐帧数值、截图或完整 logcat,因此本报告不虚构这些证据。 + +## 4. 本次回填操作 + +- 未修改 Core、gfx、RenderLayer、Test App 或 Shader; +- 未重新编译; +- 未重新运行 Android; +- 未执行 ADB; +- 仅新增本报告、对应 Codex 完成报告,并追加开发记录。 + +## 5. 结论 + +`30-01 Core RenderLayer 接入验证 = PASS`。 + +30-02 的前置门槛现已满足,允许在用户后续明确要求时重新执行原 30-02 指令书。 + +此前 30-02 v1 的 `BLOCKED` 记录继续保留,准确反映当时 30-01 正式状态尚未回填;它不代表 GPU/gfx 架构验证失败。 diff --git a/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v1.md b/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v1.md new file mode 100644 index 000000000..7a25c038e --- /dev/null +++ b/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v1.md @@ -0,0 +1,71 @@ +# 30-02 GPU Ping-Pong 与 Context Recreation 最小技术验证报告 v1 + +日期:2026-08-29 + +当前状态:`BLOCKED` + +## 1. 阻塞结论 + +30-02 未进入 GPU 验证代码准备阶段。 + +`docs/40_任务/30_02_GPU_PingPong与ContextRecreation验证指令书_v1.md` 第 2 节规定:只有当前仓库存在 `30-01 = PASS` 的正式记录时,才允许开始 State A/B、Trail A/B、RenderTarget、Shader 或 context recreation 验证代码。 + +当前有效前置记录均为: + +- `docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v2.md`:`WAITING_MANUAL_VALIDATION`; +- `docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v2.md`:`WAITING_MANUAL_VALIDATION`; +- `粒子开发记录.md`:修复后的 Android OpenGL 编译和实机运行仍待用户。 + +仓库中不存在 30-01 正式 PASS 记录,也不存在 30-01 PASS commit。因此 30-02 前置条件未满足。 + +## 2. 本轮实际执行 + +- 完整读取 30-02 指令书并执行前置门槛检查; +- 读取当前 30-01 技术报告、Codex 完成报告和开发记录; +- 检查分支、HEAD、工作区、远端同步与 submodule; +- 生成本阻塞报告和对应 Codex 完成报告; +- 追加开发记录。 + +## 3. 未开始的验证项 + +以下项目因前置门槛阻塞,均未实现、未运行、无结论: + +- State A/B GPU resource; +- State A/B ping-pong; +- Trail A/B offscreen RenderTarget; +- previous trail → next trail pass; +- Trail composite; +- 多 pass 同步与 hazard; +- viewport resize 资源重建; +- disable/enable GPU 生命周期; +- context/resource generation; +- Android EGL/surface context recreation 运行验证。 + +不得把“未开始”解释为 PASS 或 FAIL。 + +## 4. 代码与构建 + +- 30-02 Core/gfx/renderer/shader/Test App 代码修改:无。 +- 通用 gfx 修改:无。 +- 生产代码修改:无。 +- 新依赖:无。 +- 编译:未执行。 +- Android/Gradle/ADB:未执行。 +- APK/AAR 或其他产物:无。 + +## 5. 解除阻塞条件 + +用户必须先在最新 `particle/main` 上完成 30-01 修复后的 Android OpenGL 手工编译和实机验证,并回传足够证据,使 30-01 正式报告更新为 PASS。至少需要确认: + +- validation Drawable/Tweaker 真正进入 translucent render pass; +- 同帧 viewport、zoom、bearing、pitch、projection 证据有效; +- pan、zoom、rotate、pitch、resize 正常; +- ACTIVE 持续 repaint; +- STATIC 停止验证层额外 repaint; +- 地图正常且无 RenderThread crash。 + +只有 30-01 报告与开发记录正式更新为 PASS 并提交后,才能重新执行 30-02。 + +## 6. 30-03 状态 + +不允许进入 30-03。30-02 当前为 `BLOCKED`,且其自身尚未开始。 diff --git a/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v2.md b/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v2.md new file mode 100644 index 000000000..0000f8553 --- /dev/null +++ b/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v2.md @@ -0,0 +1,112 @@ +# 30-02 GPU Ping-Pong 与 Context Recreation 最小技术验证报告 v2 + +日期:2026-08-29 + +当前状态:`WAITING_MANUAL_VALIDATION` + +## 1. 前置状态与本轮范围 + +- 30-01 已由用户完成 Android OpenGL 实机验收并正式记录为 `PASS`;PASS 回填提交为 `b34b0a3e22f91a0101b0bbd02190e3384f6ef2ae`。 +- 历史 v1 报告的 `BLOCKED` 保留不改;它记录的是当时 30-01 正式状态尚未回填,不是 GPU/gfx 失败。 +- 本轮只准备 Android OpenGL 的 30-02 validation-only Stage A;未实现正式粒子、真实 Field、Mask、Vulkan 或 iOS。 + +## 2. 静态源码确认 + +以下是 Codex 可从源码确认的实现事实,不等同于设备运行结论: + +- `RenderParticleValidationLayer` 建立固定 `16x16` 的 State A/B 两套独立 `RenderTarget`,每帧从 read slot 采样并写入另一个 slot。 +- Trail A/B 是两套与当前 viewport 同尺寸的真实 `RenderTarget`;Trail pass 读取 previous trail,执行 `0.965` fade 并加入移动 marker,随后由主 translucent pass 合成。 +- 正常帧只把 State write 与 Trail write 注册到当帧 render-target list;read target 不作为当帧附件,从结构上避免同一 texture 同时采样和写入。 +- 首帧或 Trail resize 后,新的 read target 先以空 group 清屏;注册顺序为 State 初始化/写入、Trail 初始化/写入,主 LayerGroup 随后合成最新 Trail。 +- ping-pong slot 在主 pass 的 Drawable Tweaker 到达时提交;此时当帧离屏 State/Trail pass 已按 `RenderOrchestrator` 的 render-target 顺序完成编码。 +- 每帧只更新小型 UBO;没有 CPU 回读或复制完整 State/Trail 数据。 +- viewport 改变时只重建 Trail A/B,State A/B 保持固定尺寸和当前状态。 +- layer 不可渲染时移除已注册 target 并重置 slot/frame;layer remove 会移除 target;context lost 会记录旧 generation 被丢弃,新的 RenderLayer/resource set 使用递增 generation。 +- shader 只注册到当前 OpenGL backend;没有修改 Vulkan、Metal 或 iOS shader/backend。 +- 没有修改 `RenderTarget`、`CommandEncoder` 或其他通用 gfx API;使用现有 `createRenderTarget`、render-target 顺序、pass end/present 与 texture sampling 链。 + +## 3. DrawableBuilder 生命周期 + +所有新 Drawable 都严格执行:配置 builder → `flush(context)` 完成当前 Drawable → `clearDrawables()` 转移已完成对象 → `LayerGroup::addDrawable()`。没有恢复 30-01 曾出现的“未 finalize 就 clear”错误,也没有通过空指针跳过 render pass。 + +## 4. 验证日志 + +日志关键字:`ParticleValidation30_02`。状态变化与第 1 帧/每 60 帧记录: + +- resource `generation`; +- State/Trail A/B create、resize、destroy/context-lost; +- `frame`、State read/write、Trail read/write; +- `order=State,Trail,Composite`; +- `hazard=SAFE` / `distinct=true` / `cpuStateCopy=false`; +- viewport、zoom、bearing、pitch; +- Test App ACTIVE、STATIC、DISABLED 状态。 + +日志是待用户采集的验证接口;本报告未伪造任何设备日志。 + +## 5. 静态检查结果 + +- shader manifest JSON 解析:PASS。 +- shader 源生成并确认 OpenGL generated header、BuiltIn、manifest 注册存在:PASS。 +- C++ `clang-format --dry-run --Werror`:PASS。 +- `git diff --check`:PASS。 +- shader ID、UBO、attribute、texture、GL ShaderInfo 与 GL backend 注册符号一致性检索:PASS。 +- Android XML:已人工检查结构;环境缺少 `xmllint`/Ruby XML parser,未宣称解析器 PASS。 +- 编译、Gradle、Android 构建、安装、ADB、设备运行:均未执行,遵守用户“不编译”和指令书限制。 + +## 6. 用户手工编译与安装 + +当前 Makefile 的 x86_64 Test App OpenGL 命令为: + +```bash +cd /mnt/sda1/MapLibreParticleLab +source /root/.maplibre-particle-env +cd platform/android +RENDERER=opengl BUILDTYPE=Debug JOBS=3 make android-x86-64 +``` + +预期 APK: + +```text +platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk +``` + +如需命令行安装,必须由用户手工执行: + +```bash +adb -s 192.168.1.146 install -r platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk +``` + +## 7. 用户手工运行步骤 + +1. 打开 Test App,在主菜单顶部确认 APK 版本/Git revision,再进入 `Custom Layer` → `Particle Core Layer Validation`。 +2. 过滤日志:`adb -s 192.168.1.146 logcat | grep -E 'ParticleValidation30_02|ParticleValidation'`。 +3. T1 初始创建:确认 State A/B、Trail A/B create,generation 初值和 `frame=1` commit;地图与蓝/黄顺序参照正常。 +4. T2 ACTIVE:保持至少 120 帧;确认 State 与 Trail 均 A/B 交替、frame 增长、轨迹 marker 移动/衰减且地图正常。 +5. T3 STATIC:点击 STATIC 并静置,确认 validation 不再主动推进;随后 pan/zoom/rotate/pitch,确认地图事件可触发 redraw,停止操作后不再持续推进。 +6. T4 resize:旋转屏幕或改变模拟器窗口尺寸;确认 `Trail A/B resize old=... new=... StatePreserved=true`,State A/B 没有重新 create,地图无异常。 +7. T5 disable/enable:点击 DISABLE,确认 layer/resource destroy 且停止;点击 ENABLE,确认新 generation 建立、slot/frame 从初始状态恢复,无 crash。 +8. T6 context/surface recreation:保持 App 进程存活,先 Home/Recent Apps 切出再返回;若没有出现 `context lost discard`,启用开发者选项“不保留活动”后离开并返回验证 Activity,或旋转触发 Activity/Surface 重建。仅界面重现不算成功,必须捕获旧 generation 的 `context lost discard`/destroy 和新 generation create。 +9. T7 recreation 后继续 ACTIVE 至少 120 帧;确认新 generation 的 ping-pong 恢复、Trail 从清空重新积累、无旧 generation 后续 commit、地图正常、无 crash/stale framebuffer。 +10. 保存完整关键日志和可见现象,回传给 Codex 进入阶段 B。 + +可选手工启动命令: + +```bash +adb -s 192.168.1.146 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity +``` + +## 8. 当前证据边界与未验证项 + +- 用户手工编译结果:未提供。 +- Android OpenGL 实际运行结果:未提供。 +- State/Trail 实机持续交替:未验证。 +- 当前 OpenGL driver 的 pass 同步/hazard:静态设计成立,尚无实机证据。 +- resize、disable/enable:尚无实机证据。 +- 真实 EGL context/surface recreation、旧资源失效与新 generation 恢复:尚无实机证据。 +- crash、framebuffer corruption、地图回归:尚无实机证据。 + +因此当前不能判定 PASS,也未发现可据以判定 FAIL 的运行证据;正式状态只能是 `WAITING_MANUAL_VALIDATION`。 + +## 9. 30-03 状态 + +不允许进入 30-03。必须先由用户完成上述 Android OpenGL 手工验证,并在阶段 B 将 30-02 正式更新为 `PASS`;若无法确认真实 context recreation,则按指令书判定 `BLOCKED`。 diff --git a/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v3.md b/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v3.md new file mode 100644 index 000000000..66f422afd --- /dev/null +++ b/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v3.md @@ -0,0 +1,100 @@ +# 30-02 GPU Ping-Pong 与 Context Recreation 最小技术验证报告 v3 + +日期:2026-08-30 + +当前状态:`WAITING_MANUAL_VALIDATION` + +## 1. 本轮实机发现 + +用户在 30-02 v2 对应 Android OpenGL APK 上完成 T3 检查并明确报告: + +- STATIC 静止时,粒子/Trail 正常停止; +- STATIC 下拖动地图时,MapLibre redraw 会使 State/Trail 继续推进; +- 停止拖动后,State/Trail 再次停止。 + +因此原实现的 T3 判定为 `FAIL`。这是明确的设备运行事实;本报告不补写用户未提供的 frame 数值或日志数值。 + +## 2. 原因 + +原实现只在 `needsRepaint()` 中用 ACTIVE/STATIC 控制验证层是否主动请求下一帧,但 `RenderParticleValidationLayer::update()` 没有按 ACTIVE/STATIC 区分 GPU 工作。 + +地图 pan/zoom/bearing/pitch 会合法触发 MapLibre redraw;每次 redraw 都会再次执行原 `update()`,继而: + +1. 注册 State write RenderTarget; +2. 注册 Trail write RenderTarget; +3. 设置 `pending=true`; +4. composite Tweaker 调用 `commit()`; +5. State/Trail slot 与 `committedFrame` 前进。 + +所以 `needsRepaint=false` 只能停止 validation 自己制造的连续帧,不能保证在其他合法地图帧中冻结 GPU simulation。 + +## 3. 最小修复 + +仅修改: + +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `src/mbgl/renderer/layers/render_particle_validation_layer.hpp`(说明性注释) + +修复内容: + +- `update()` 在已有资源上先撤销上一帧注册的 RenderTarget,然后读取当前 evaluated ACTIVE 状态。 +- STATIC 分支不注册任何 State/Trail RenderTarget,不创建 State/Trail update Drawable,并清除任何 `pending` swap。 +- STATIC 只为主 LayerGroup 建立 `FrozenComposite` Drawable,采样最后一次已经 commit 的 `trailRead` texture。 +- `FrozenComposite` Tweaker可以在当前 MapLibre translucent pass 更新 composite UBO/texture 并读取当前 viewport、zoom、bearing、pitch,但在调用 `commit()` 前明确返回。 +- STATIC 的 shader frame 值使用当前 `committedFrame`,不预先计算为下一 simulation frame。 +- STATIC 下即使 viewport 改变也不重建、清空或推进 Trail;冻结 texture 由主 pass 重新显示。恢复 ACTIVE 后,既有 ACTIVE resize 路径才重建 Trail。 +- 如果 layer 从未有过 ACTIVE/已 commit Trail 就直接处于 STATIC,不创建或清屏离屏资源,也不显示未初始化 texture。 +- ACTIVE 路径的 State A/B、Trail A/B、pass 顺序、hazard 约束和 slot commit 保持不变。 + +## 4. 静态控制流确认 + +以下事实已经从修复后源码确认: + +- `AddRenderTargetRequest` 只存在于 STATIC 提前返回之后的 ACTIVE 路径。 +- State/Trail `buildQuad()` 只存在于 ACTIVE 路径。 +- `resources->pending = true` 只存在于 ACTIVE 路径。 +- STATIC 进入时强制 `resources->pending = false`。 +- `FrozenComposite` 在 Tweaker 内先返回,不可能落入 `resources->commit()`。 +- STATIC composite 绑定 `trailTargets[trailRead]`,read/write slot 参数相同且没有 target attachment。 +- MapLibre redraw 没有被阻止;主 LayerGroup composite 仍会在当前地图 render pass 中执行。 + +## 5. 静态检查 + +- C++ `clang-format --dry-run --Werror`:PASS。 +- `git diff --check`:PASS。 +- ACTIVE/STATIC、RenderTarget 注册、pending、commit 与 `committedFrame` 控制流检索:PASS。 +- 编译、Android、Gradle、ADB、安装与运行:未执行,遵守用户要求。 + +静态检查只能证明代码路径,不替代 Android OpenGL 重测。 + +## 6. T3 重新验证 + +使用包含修复提交 `364b8f381e27eee36c4895084b2f0578a023fa61` 的新 APK: + +1. 进入 `Particle Core Layer Validation`,保持 ACTIVE,确认 State/Trail 正常交替并记下最近一次 `commit frame=N`、State read slot 与 Trail read slot。 +2. 点击 STATIC,确认日志出现:`STATIC frozen composite ... frame=N ... offscreenPasses=0 swap=false`。 +3. STATIC 静止至少 3 秒,确认粒子/Trail 不推进。 +4. STATIC 下连续执行 pan、zoom、bearing、pitch;地图必须正常移动和 redraw,冻结 Trail 可以随主 pass 重新显示。 +5. 操作期间确认画面中的 marker/fade 不推进;不得出现新的 State pass、Trail pass、slot swap 或 simulation `commit frame` 增长。 +6. 停止操作后仍保持冻结。 +7. 点击 ACTIVE,确认 GPU ping-pong 从冻结的 State/Trail slot 继续,而不是在 STATIC 操作期间跳过 frame/index;继续运行到下一次节流日志,确认 frame 恢复增长。 +8. 再执行一次 ACTIVE → STATIC → 地图手势 → ACTIVE,排除只在首次切换正确。 + +用户手工日志命令: + +```bash +adb -s 192.168.1.146 logcat | grep -E 'ParticleValidation30_02|ParticleValidation' +``` + +## 7. 仍需完成的 30-02 验证 + +本轮只修复 T3。以下仍沿用 v2 的待验证状态: + +- 修复后 T3 Android OpenGL 重测; +- T4 resize; +- T5 disable/enable; +- T6/T7 真实 context/surface recreation 与恢复; +- 无 stale resource/framebuffer corruption/crash; +- 最终完整 PASS 证据。 + +当前最终状态仍为 `WAITING_MANUAL_VALIDATION`,不得写 PASS,也不允许进入 30-03。 diff --git a/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v4.md b/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v4.md new file mode 100644 index 000000000..f734e7b75 --- /dev/null +++ b/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v4.md @@ -0,0 +1,69 @@ +# 30-02 GPU Ping-Pong 与 Context Recreation 最小技术验证报告 v4 + +日期:2026-08-30 + +当前状态:`WAITING_MANUAL_VALIDATION` + +## 1. 已确认实机状态与本轮问题 + +用户已确认此前 C++ STATIC freeze 修复有效:在 STATIC 下 pan、zoom、bearing、pitch 触发的普通 MapLibre redraw 不再推进 GPU simulation。DISABLE / ENABLE 也已由用户确认正常。 + +本轮 Android OpenGL 实机发现是另一层问题:屏幕旋转触发 Activity recreation 后,旋转前的 `STATIC + ENABLED` 被恢复成 `ACTIVE + ENABLED`。 + +该现象不改变已确认的 C++ STATIC freeze 结论;它是 Test App validation control mode 未在标准 Android Bundle 中恢复的问题。 + +## 2. 根因 + +`ParticleCoreLayerValidationActivity` 在 Activity 存活期间已经由 `loadValidationStyle(active, enabled)` 维护两个成员状态。但旧代码: + +- 没有在 `onSaveInstanceState()` 保存 `active`、`enabled`; +- 在 `onCreate()` 的 Map callback 中无条件调用 `loadValidationStyle(active = true, enabled = true)`; +- 同时无条件重置相机到 `(0, 0), zoom=2`。 + +因此标准旋转 recreation 无论此前为 STATIC、ACTIVE 或 DISABLED,都会退回 ACTIVE + ENABLED。 + +## 3. 定向修复 + +仅修改: + +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` + +具体调整: + +- 新增稳定 Bundle key:`particle_validation.active`、`particle_validation.enabled`; +- `onCreate()` 在 Map 初始化前恢复两个成员变量,首次进入默认 ACTIVE + ENABLED; +- `getMapAsync` 只在首次进入时设置默认 camera;recreation 时保持 MapView 的已保存状态; +- 恢复后的 `active/enabled` 仍只通过既有 `loadValidationStyle()` 应用 style 与页面状态; +- `onSaveInstanceState()` 写入两个 validation control 值,同时保留原有 MapView state 保存顺序; +- 新增低频日志:`Activity state restored active=<...> enabled=<...> recreated=<...>`。 + +未修改 AndroidManifest、layout、strings、C++ Core、gfx、RenderLayer、shader、GPU state、Gradle 或依赖。没有使用 `configChanges`、方向锁定、static/global、SharedPreferences、ViewModel 或 GPU 状态序列化。 + +## 4. 静态检查 + +- S1:Activity 中已不存在 recreation 后无条件 `loadValidationStyle(active = true, enabled = true)`:PASS。 +- S2:`savedInstanceState == null` 时默认 ACTIVE + ENABLED 且设置初始 camera:PASS。 +- S3:ACTIVE、STATIC、DISABLE、ENABLE 按钮仍调用唯一 `loadValidationStyle()`:PASS。 +- S4:`loadValidationStyle()` 继续统一更新成员状态、style 和状态文字:PASS。 +- S5:没有 Manifest workaround、持久化、全局状态或 GPU 资源状态:PASS。 +- `git diff --check`:PASS。 +- Kotlin/Android 编译、Gradle、ADB、安装与实机运行:未执行,遵守任务限制。 + +## 5. Rotation 重新验证 + +使用包含代码提交 `503f1c66958798ef7294165c3ddeaffbd4908ee9` 的新 APK。日志过滤: + +```bash +adb -s 192.168.1.146 logcat | grep -E 'ParticleValidation30_02|ParticleValidation' +``` + +1. R1 STATIC rotation:ACTIVE 运行几秒 → STATIC → 确认停止 → 竖屏/横屏互转两次。每次日志应为 `active=false enabled=true recreated=true`,页面保持 STATIC,不能自动 ACTIVE。 +2. R2 ACTIVE rotation:ACTIVE 下旋转,日志应为 `active=true enabled=true recreated=true`,页面保持 ACTIVE。 +3. R3 DISABLED rotation:DISABLE 后旋转,日志应为 `active=<切换前值> enabled=false recreated=true`,页面保持 DISABLED,不能自动 ENABLE。 +4. R4 STATIC gesture regression:STATIC 下 pan、zoom、bearing、pitch;地图正常 redraw,但 GPU simulation 仍不得推进,验证本轮没有破坏 v3 的 C++ freeze。 + +## 6. 判定边界 + +Rotation 通过只能证明 validation control mode 在 Activity recreation 后恢复正确;它不能单独判定 T4 viewport resize 或 T6/T7 context/resource recreation PASS。旋转路径可能同时重建 Activity、MapView、RenderLayer 和 GPU resources,旧 Trail 不要求跨 Activity 保存。 + +30-02 总体状态仍为 `WAITING_MANUAL_VALIDATION`,等待 R1–R4 人工证据;不得写 PASS,也不允许进入 30-03。 diff --git a/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v5.md b/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v5.md new file mode 100644 index 000000000..3c68d604b --- /dev/null +++ b/docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v5.md @@ -0,0 +1,50 @@ +# 30-02 GPU Ping-Pong 与 Context Recreation 最小技术验证报告 v5 + +日期:2026-08-30 + +正式状态:`PASS(EGL Context Recreation:Accepted Risk / 未验证;用户明确决定不阻塞)` + +## 1. 正式关闭结论 + +30-02 已正式关闭,允许进入 30-03。 + +用户已完成并确认 Android OpenGL 实机验证结果:ACTIVE / STATIC 控制、State A/B 与 Trail A/B 的 GPU ping-pong 验证、STATIC 下普通 MapLibre redraw 的冻结行为、DISABLE / ENABLE,以及 rotation 后 validation control mode 的恢复均已达到本验证项目的要求。 + +其中,STATIC 手势问题曾被用户实机判定为 T3 FAIL;v3 的最小修复后,用户已确认 pan、zoom、bearing、pitch 仍可使地图正常 redraw,但不会推进 State A/B update、Trail A/B fade/marker update、slot swap 或 simulation frame/index。该修复结果是本结论的一部分,并非通过禁止地图 redraw 达成。 + +## 2. 已完成实机结果的证据边界 + +以下结论来自用户完成的 Android OpenGL 人工运行验收;Codex 没有在本轮重新编译、安装、运行 Android 或执行 ADB: + +- ACTIVE 状态下 validation GPU ping-pong 与 Trail composite 可运行; +- STATIC 静止和 STATIC 下 pan、zoom、bearing、pitch 的地图 redraw 不再推进 simulation; +- DISABLE / ENABLE 行为正常; +- Activity rotation 后 validation control mode 可恢复,未再把 STATIC 强制改为 ACTIVE; +- 30-01 已正式 PASS,30-02 所依赖的 Core RenderLayer 接入与同帧 Transform 路径保持有效。 + +本报告不伪造 logcat 原文、帧号、GPU 数值或未由用户提供的上下文重建次数。 + +## 3. EGL Context Recreation:Accepted Risk / 未验证 + +真实 EGL context loss / recreation 没有获得独立、可复现的 Android OpenGL 实机证据。因此: + +- 不将 EGL Context recreation 写成已实机 PASS; +- 保留它为 `Accepted Risk / 未验证项`; +- 风险内容是:实际 EGL context 被系统销毁并重建时,validation-only RenderTarget generation/discard、重建和后续 composite 的完整运行时路径尚未单独验证; +- 用户已明确决定该未验证项不再阻塞当前阶段推进。 + +这是用户接受的验证残余风险,不是对 GPU ping-pong、STATIC freeze 或 rotation control-mode 修复失败的判定。 + +## 4. 历史状态与代码范围 + +- v1 的 `BLOCKED` 保留:它记录的是当时 30-01 尚未正式 PASS 的前置门槛,不能删除或改写; +- v2 记录 State/Trail A/B 与 generation 验证准备; +- v3 记录 T3 FAIL、根因及 STATIC frozen-composite 最小修复; +- v4 记录 Android rotation control-mode 恢复定向修复; +- 本 v5 只回填已完成实机结果与用户的风险接受决定,不新增 GPU 架构、RenderTarget、shader、Mask、Vulkan、iOS 或正式粒子实现。 + +## 5. 后续门槛 + +30-02 的正式状态为 PASS,且 EGL Context recreation 未验证风险已经由用户接受,不阻塞后续工作。允许进入 30-03。 + +若未来需要发布级 context-loss 覆盖,应将真实 EGL context recreation 单独列为新的验证项;它不追溯改变本报告所记录的用户决定。 diff --git a/docs/60_测试/30_03_Camera_WorldWrap与LandOceanMask最小技术验证报告_v1.md b/docs/60_测试/30_03_Camera_WorldWrap与LandOceanMask最小技术验证报告_v1.md new file mode 100644 index 000000000..347b5bba7 --- /dev/null +++ b/docs/60_测试/30_03_Camera_WorldWrap与LandOceanMask最小技术验证报告_v1.md @@ -0,0 +1,99 @@ +# 30-03 Camera / World Wrap 与 Land/Ocean Mask 最小技术验证报告 v1 + +日期:2026-08-30 + +当前状态:`PASS` + +## 1. 前置与结论边界 + +30-02 最新正式报告 v5 为 `PASS(EGL Context Recreation:Accepted Risk / 未验证;用户明确决定不阻塞)`,并明确允许进入 30-03。本轮是 30-03 阶段 A 的验证代码准备。 + +阶段 A 中 Codex 没有运行 Android 编译、Gradle、安装、ADB 或设备操作;这些限制保持不变。阶段 B 中,用户已完成 Android OpenGL 手工运行验收并明确确认 T1–T13 全部 PASS。以下最终结论如实回填用户验收,不补造未提供的逐帧日志数值。 + +## 2. 当前源码链 + +- Camera:`TransformState` 由 render frame 建立 `TransformParameters`;`PaintParameters::matrixForTile()` 调用当前 `TransformState::matrixFor()`,再乘当前 frame projection matrix。 +- World wrap:`UnwrappedTileID` 用 `wrap + CanonicalTileID` 表达绘制 world copy;`TransformState::matrixFor()` 将 `tileID.wrap * tileScale` 纳入 world translation。 +- 本验证没有独立 Camera、Overlay 或第二套 world 状态机。Composite Tweaker 直接从当前 `PaintParameters` 取得 `matrixForTile({wrap,{0,0,0}})`。 + +## 3. 本次 validation-only 实现 + +`ParticleGpuValidationShader` 的 State / Trail pass 仍是 30-02 的全屏 RenderTarget pass。主 composite 改为 canonical z0 world quad:vertex shader 以当前 frame world-copy matrix 投影,而不是固定 screen quad。 + +每帧按当前未 wrap Camera center 选择中心 wrap 和相邻 wrap (`centerWrap - 1..+1`) 绘制同一 canonical quad;只有中心 copy 可以提交 30-02 slot/frame,其他 copy 仅绘制,故不会把 world copy 数量变成 simulation 推进次数。 + +确定性地理锚点为:A `(0, 0)`、B `(0, -90)`、C `(0, -172.8)`、D `(0, 172.8)`。C/D 位于反经线两侧。它们是 shader 中的 canonical Mercator z0 world fraction,不由 Android UI 计算。 + +ACTIVE 继续运行 30-02 State/Trail A/B;STATIC 保留既有冻结语义:不注册离屏 target、不 update/fade/marker、不 slot swap、不增 simulation frame,但用当前 `PaintParameters` matrix 重画最后 Trail 的地理 composite 与 mask。 + +## 4. Antimeridian 表示 + +Mask fixture 使用正式设计中允许的全球 canonical bounds `west=-180, east=180, south=-85, north=85`,不把单一数据文件表示成跨反经线区域。C/D 两个 canonical anchor 跨越该边界;世界连续性由 `UnwrappedTileID.wrap` 与同一 canonical z0 quad 完成。 + +GPU sampling 采用 canonical world UV `[0,1]`,不在 shader 以 `if lon > 180` 进行局部纠正。每个 world copy 使用同一 canonical mask UV、各自 MapLibre matrix,因此 geometry 与 mask 不会分别选择不同 copy。 + +## 5. Shared Land/Ocean Mask + +当前 `particle-fixtures/coastal-mask/` 在开始时只有占位文件,仓库也尚未具备正式 fixture adapter/upload 链。本轮未伪造 production fixture pipeline;增加的是 validation-only deterministic CPU fixture generator:16×16 RGBA,alpha `0=LAND`、`255=OCEAN`,包含斜向 coast 和非轴对齐缺口。 + +它只创建一次 `Texture2D`,采用 nearest + clamp,`maskGeneration` 单调记录。Camera pan/zoom/bearing/pitch 和 `GLOBAL ↔ OCEAN_ONLY_A ↔ OCEAN_ONLY_B` 只改变 drawable uniform/style property,不重新上传 mask。A/B 都绑定同一 `maskTexture`;GLOBAL 不 discard,A 显示蓝色、B 显示绿色且均在 land discard。NoData 在本 synthetic fixture 中不存在;正式 OCEAN_ONLY contract 的 NoData fail-closed 仍未实现。 + +`markContextDestroyed()` 后下一次 ACTIVE update 丢弃整套 validation GPU resources(包括 mask texture)并以新 `maskGeneration` 从 CPU fixture 重建,不复用 stale handle。真实 EGL context recreation 仍不在本轮被声称验证。 + +## 6. Test App 与日志 + +复用 `Particle Core Layer Validation`,新增最小控制:`GLOBAL`、`OCEAN A`、`OCEAN B`、`DATELINE`、`WORLD WRAP`。A/B/GLOBAL 通过既有 Layer 的 paint properties 更新,预期资源复用,不重载 Style;ACTIVE/STATIC/DISABLE/ENABLE 保持原有入口。Activity Bundle 还保存 mask mode。 + +主要日志标签为 `ParticleValidation30_03`:fixture/mask create、maskGeneration、mode/consumer reuse、first camera-locked draw、每 30 frame 节流 camera 摘要、world wrap、antimeridian 与 30-02 STATIC frozen composite。没有每 frame 输出完整 matrix。 + +## 7. Codex 静态验证 + +- `CCACHE_DISABLE=1 cmake --build build-linux-opengl --target CMakeFiles/mbgl-core.dir/src/mbgl/renderer/layers/render_particle_validation_layer.cpp.o CMakeFiles/mbgl-core.dir/src/mbgl/style/layers/particle_validation_layer.cpp.o CMakeFiles/mbgl-core.dir/src/mbgl/shaders/gl/shader_info.cpp.o -- -j3`:PASS。 +- `clang-format --dry-run --Werror`(受影响 C++/header):PASS。 +- `git diff --check`:PASS。 +- Android 编译/运行、Gradle、ADB:Codex 未执行,遵守任务书;最终 Android OpenGL 视觉与日志判定由用户手工验收。 + +## 8. 用户手工 Android OpenGL 验证与最终结果 + +编译(由用户手工执行): + +```bash +cd /mnt/sda1/MapLibreParticleLab +source /root/.maplibre-particle-env +cd platform/android +RENDERER=opengl BUILDTYPE=Debug JOBS=3 make android-x86-64 +``` + +打开 `Custom Layer → Particle Core Layer Validation`,日志可使用: + +```bash +adb -s 192.168.1.146 logcat | grep -E 'ParticleValidation30_02|ParticleValidation30_03|ParticleValidation' +``` + +| 测试 | 最终结果 | 用户验收结论 | +| --- | --- | --- | +| T1 普通 Camera Pan | `PASS` | STATIC 下锚点与 mask 保持地理锁定,simulation 不推进。 | +| T2 Zoom | `PASS` | 缩放同步、maskGeneration 无无意义变化。 | +| T3 Bearing | `PASS` | geometry 与 mask 随地图旋转,无屏幕裁剪。 | +| T4 Pitch | `PASS` | 透视下保持地图地理对齐。 | +| T5 ACTIVE / STATIC Camera 分离 | `PASS` | simulation clock 与 camera redraw 分离,ACTIVE 恢复正常。 | +| T6 World Wrap | `PASS` | 相邻 world copy 连续,无额外 slot commit 或双重复制。 | +| T7 反经线东向西 | `PASS` | C/D、pattern 与 mask 连续。 | +| T8 反经线西向东 | `PASS` | C/D、pattern 与 mask 连续。 | +| T9 OCEAN_ONLY 基本裁剪 | `PASS` | land 抑制、ocean 显示,海岸边界稳定。 | +| T10 GLOBAL 不受 Mask 裁剪 | `PASS` | land 与 ocean 均可显示。 | +| T11 Shared Mask 复用 | `PASS` | A/B 判定一致,consumer 切换复用同一 maskGeneration。 | +| T12 Mask + World Wrap + Antimeridian | `PASS` | geometry 与 mask 使用同一 copy,无 ±180° seam。 | +| T13 Viewport / Rotation 后地理对齐回归 | `PASS` | 在验证层恢复后的 viewport/rotation 场景,锚点与 mask 保持地理对齐,mask 使用新 resource generation。 | + +T13 不替代 30-02 的 EGL context-loss 证据。另见第 9 节的已知延期项:rotation/viewport 后 validation 页面自动恢复仍有问题,但不改变用户已验收的 T13 地理对齐结论。 + +## 9. 最终判定、延期项与下一阶段 + +用户 Android OpenGL 手工验收已确认 T1–T13 全部 `PASS`。Camera 同帧 transform、world wrap、反经线连续性,以及 shared Land/Ocean Mask 的 GLOBAL / OCEAN_ONLY 语义均具备本最小技术验证所需的运行时证据。 + +已知延期项:`Rotation/Viewport 自动恢复问题`。该问题已知,用户接受延期,并明确安排在下一个任务修复;它不是对本轮已验收地理对齐、world wrap 或 mask contract 的否定。本轮未宣称该自动恢复问题已经修复。 + +仍不属于本轮 PASS 范围:真实 EGL context loss 的独立设备证据、正式 fixture adapter、正式 NoData fail-closed,以及生产 coastline 数据。30-02 的 EGL context recreation 仍维持既有 `Accepted Risk / 未验证` 记录。 + +最终结论:`PASS`。允许进入下一阶段的最小验证;下一个任务须优先处理上述 Rotation/Viewport 自动恢复延期项。 diff --git a/docs/60_测试/40_01_Android离线Fixture到正式粒子渲染最小闭环验证报告_v1.md b/docs/60_测试/40_01_Android离线Fixture到正式粒子渲染最小闭环验证报告_v1.md new file mode 100644 index 000000000..bfa6cb167 --- /dev/null +++ b/docs/60_测试/40_01_Android离线Fixture到正式粒子渲染最小闭环验证报告_v1.md @@ -0,0 +1,82 @@ +# 40-01 Android 离线 Fixture 到正式粒子渲染最小闭环验证报告 v1 + +## 状态 + +`PASS`。用户已完成 40-01 Stage B Android OpenGL x86_64 手工验收。本文保留 Stage A 静态检查及失败/修复历史,并以 Stage B 验收作为最终结论。 + +## 基线与范围 + +- 分支:`particle/main` +- 开始 HEAD:`1ec44701146925dec9a87618e3f90bee9a12073f` +- 本次 PASS 范围:Android OpenGL x86_64 离线 fixture 正式粒子闭环;不扩大为 iOS、Metal、Android Vulkan、真实在线数据或生产 coastline/mask 的 PASS。 + +## 最终正式链路 + +```text +APK asset://integration/particle-fixtures/{uniform-east,uniform-north}/ + -> RasterArraySource -> RenderRasterArraySource + -> ParticleFieldSnapshot -> GPU field texture + -> RasterParticleLayer (Source::Required) -> RenderRasterParticleLayer + -> State A/B -> Trail A/B -> formal Trail composite / shared coastal mask +``` + +- fixture 使用 `metadata.json + field.f32`,Core 仅接收 FileSource bytes;metadata、binary length 与 SHA-256 经过校验。 +- State pass 采样 GPU field,Trail 从 State write texture 读取;不使用 Kotlin/CPU 每帧粒子循环。NoData cell 以 field alpha=0 表示,确定性 respawn,绝不把 `-9999` 当速度。 +- STATIC 首帧可以 zero-step bootstrap,之后使用 frozen composite;Activity Bundle 恢复 active、enabled、mask mode 与 fixture mode。 + +## Codex 静态验证 + +| 检查 | 结果 | +|---|---| +| Linux OpenGL 受影响 Core 与 `source.test.cpp.o` 精确编译 | PASS(`-Werror`) | +| `clang-format --dry-run --Werror` | PASS | +| `git diff --check` | PASS | +| fixture metadata / SHA-256、adapter smoke、APK asset parity | PASS | +| shader source/header parity 与 generator 二次运行幂等 | PASS | +| RasterArray runtime generation / field identity 定向 C++ 测试对象 | PASS | + +完整 `mbgl-test` 不是本任务的精确验证范围,未将未执行测试伪造为 PASS。 + +## Android OpenGL x86_64 手工验收(Stage B) + +用户已确认下列项目全部 `PASS`: + +| 项目 | 结果 | +|---|---| +| GLOBAL + ACTIVE + UNIFORM EAST | PASS | +| ACTIVE → STATIC → ACTIVE | PASS | +| UNIFORM EAST → UNIFORM NORTH → UNIFORM EAST | PASS | +| STATIC + OCEAN A/B Rotation 自动恢复 | PASS | +| ACTIVE Rotation | PASS | +| Pan / Zoom / Bearing / Pitch | PASS | +| World Wrap | PASS | +| Antimeridian 双向跨越 | PASS | +| GLOBAL / OCEAN A / OCEAN B | PASS | +| Shared Mask reuse | PASS | +| DISABLE / ENABLE | PASS | +| 快速 Fixture 切换 | PASS | +| 最终重复 Rotation 回归 | PASS | + +Shared Mask 日志事实:同一 RenderThread / GPU resource generation 内,GLOBAL、OCEAN_ONLY_A、OCEAN_ONLY_B 均复用同一 `maskGeneration`,并记录 `reuse=true`。graphics resource 重建后 `maskGeneration` 可以递增;这是新 GPU resource generation 的重建,不是同 generation 内重复创建。 + +## 40-01 调试与修复历史 + +以下失败过程保留为调试与验证记录,不影响最终 PASS: + +- `SourceType::RasterArray` 首次 Android C++ 编译遗漏 OfflineDownload 两个 exhaustive switch;按 APK `asset://` fixture 的 no-op contract 补齐,未用 `default:` 掩盖遗漏。 +- enabled `RenderRasterArraySource` 曾返回空 RenderItem,RenderTree 无条件解引用导致首次启动 SIGSEGV;source 内补合法 no-op RenderItem,未弱化 orchestrator contract。 +- Android 出现旧 APK / `.cxx` native 缓存造成的观察偏差;以当前源码重新生成 native artifact 后继续验收。 +- source-scoped transient `markLayerRenderable(false)` 曾每帧 reset State/Trail;移除错误生命周期边界后 ACTIVE frame/slot 连续推进,STATIC 仅在必要时 zero-step bootstrap。 +- formal Composite 曾只让 Trail RGB 影响 alpha、主体仍为 legacy stripe;`FORMAL_PARTICLE` 改为显示 Trail RGB,legacy validation display 保留。 +- shader 逻辑曾与 generated OpenGL header 不同步;已回写 GLSL source、由项目 generator 生成 header,并完成 parity/幂等检查。 +- opaque RenderTarget clear 使 `previous.a` 不能表达未 seed;改用 `stateSeeded` 经 UBO 显式 State seed,初始 marker 分散于 world。 +- Style source recreation 时 instance-local field generation 碰撞,north 复用 east GPU texture;generation 改为 process-wide revision,renderer 同时比较 generation、fixtureID、dataVersion。 +- Rotation / Viewport 的 STATIC 自动恢复纳入本任务,已由 STATIC OCEAN A/B 与最终重复 Rotation 回归验证。 + +## 范围外事项 + +40-01 的 Android OpenGL x86_64 范围已 PASS。iOS/Metal、Android Vulkan、真实数据、生产 coastline/mask、性能和独立 EGL context-loss 设备证据不在本次 PASS 声明范围。NoData 有确定性 fixture/static smoke 覆盖,本轮未单独形成 NoData 视觉结论。 + +## 最终结论 + +40-01 Android 离线 Fixture 到正式粒子渲染最小闭环正式判定为 `PASS`。 diff --git a/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md new file mode 100644 index 000000000..f2bfa0386 --- /dev/null +++ b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md @@ -0,0 +1,76 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化人工验收 + +状态:等待用户手工 Android 验收。Codex 不执行本文件中的 Gradle、APK 或 ADB 操作。 + +## Windows 11 构建与安装 + +在 Windows 11 项目目录执行既有 Android OpenGL Debug 构建命令,并保留 plain 控制台输出,例如: + +```powershell +./gradlew :MapLibreAndroidTestApp:assembleOpenGLDebug --console=plain +``` + +安装、启动和日志均显式选择设备: + +```powershell +adb -s 192.168.1.146 install -r +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 logcat | findstr /I "REAL_WEATHER WEATHER_COLOR HOST_FIELD ParticleTimeline FIELD_REUSE FIELD_UPLOAD ParticleRender simulationRebuild trailCleared" +``` + +## 入口 + +进入 `Particle Core Layer Validation`: + +1. `MORE → REAL WEATHER`。 +2. 等待地图自动 fit 到 112.5–157.5E、0–55.776573N;应看到 OSM 底图、半透明风速色场及其上的运动粒子。 +3. `TIME → PLAY / RESTART`。真实 validTime 保持 6 小时步长;没有额外 Android 加速机制,可使用现有 `SPEED 2x`,或用 `STEP NEXT` 检查边界。 + +## V1–V8 视觉与行为矩阵 + +| 项 | 操作 | 期望 | +| --- | --- | --- | +| V1 | `REAL WEATHER` | 出现真实日本/西北太平洋覆盖范围、OSM 底图、风速色场和粒子;无黑屏/crash。 | +| V2 | 比较色场与粒子 | 色场强弱分布与粒子移动方向同源;U=eastward、V=northward,无明显横纵交换/符号反转。 | +| V3 | PLAY | Timeline READY 后 PLAY 成功;MIX 自动推进,颜色与粒子同时连续变化。 | +| V4 | 两次自动边界或 STEP NEXT | T0/T1→T1/T2→T2/T3;日志有 `OLD_T1_TO_NEW_T0` / `FIELD_REUSE`,只出现一个 `NEW_CONTENT` upload。 | +| V5 | 连续播放观察 | 正常 boundary 为 `simulationRebuild=false`、`trailCleared=false`;不得 style reload/layer rebuild。 | +| V6 | Pan/zoom/rotate/viewport resize | 底图、色场和粒子均保持地理对齐;旋转/恢复后自动可见,不按任何恢复按钮。 | +| V7 | `MORE → HOST DUPLICATE` | 对当前 Host window 得到 Duplicate NOOP,field upload/reset count 无增量。 | +| V8 | `MORE → HOST INVALID` | Host canonical invalid 被拒,最后有效 field、色场/Renderer 持续,无黑屏/crash。 | + +## 必要日志 + +正常加载必须可见: + +```text +REAL_WEATHER_DATASET_LOAD +HOST_FIELD_SUBMIT_REQUEST / HOST_FIELD_SUBMIT_ACCEPTED +HOST_FIELD_INITIAL_WINDOW_READY +ParticleTimeline: event=READY +REAL_WEATHER_INITIAL_WINDOW_READY +WEATHER_COLOR_FIELD_READY +REAL_WEATHER_COMPOSITE_READY +``` + +播放/边界检查: + +```text +PLAYBACK_PLAY +TIME_MIX_UPDATE or PLAYBACK_TICK +WINDOW_BOUNDARY +FIELD_REUSE from=OLD_T1 to=NEW_T0 +FIELD_UPLOAD reason=NEW_CONTENT +simulationRebuild=false +trailCleared=false +``` + +负向检查: + +```text +HOST_FIELD_SUBMIT_REJECTED +ADAPTER_REJECTED +result=NOOP +``` + +请保存上述日志、V1–V8 结果,以及任何色场与粒子错位、黑屏、旋转后不可恢复或 crash 的时间点。MIX 文本显示可能滞后属于既知 NON-BLOCKING observability,不应替代 Core log 判断。 diff --git a/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v2.md b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v2.md new file mode 100644 index 000000000..3c1f6487a --- /dev/null +++ b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v2.md @@ -0,0 +1,47 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化人工验收 v2 + +状态:等待用户手工 Android 验收。Codex 不执行本文件中的 Gradle、APK、ADB 或设备操作。 + +## Windows 11 构建、安装与日志 + +在 Windows 11 项目目录执行: + +```powershell +./gradlew :MapLibreAndroidTestApp:assembleOpenGLDebug --console=plain +adb -s 192.168.1.146 install -r +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 logcat | findstr /I "REAL_WEATHER GSI ParticleTuning ParticleMotion ParticleTimeline WINDOW_BOUNDARY FIELD_REUSE FIELD_UPLOAD DUPLICATE INVALID simulationRebuild trailCleared" +``` + +## 入口与默认画面 + +1. 进入 `Particle Core Layer Validation`。 +2. 底栏应为 `TIME | VIS | COUNT | MAP | REAL WEATHER | MORE`;MORE 中不应再有 REAL WEATHER。 +3. 点击 `REAL WEATHER`。确认 GSI 日本及周边地图、半透明 WeatherColorLayer 和细密粒子共同出现,且日志有 `REAL_WEATHER_COMPOSITE_READY ... basemap=GSI`。 +4. HUD 应持续显示:`P 2048 · W 0.75 · L 0.0040 · O 0.75 · T 0.55 · F 0.88`。粒子应明显持续运动,而不是粗大橙色块状轨迹。 + +GSI tile 若网络不可用,记录 `BASEMAP`/GSI 错误即可;WeatherColorLayer、粒子、Timeline 不得因此黑屏或 crash。 + +## 调参、RESET 与暂停 + +再次点击已激活的 `REAL WEATHER`,确认仅打开/关闭 `PARTICLE TUNING`,不出现 `STYLE_LOAD reason=REAL_WEATHER`、不重新加载 Dataset 或提交 Host fields。使用各 `-/+` 项分别改变 P/W/L/O/T/F:HUD 必须立刻显示实际生效值,且菜单关闭后仍显示。 + +- P 可在 256、512、1024、2048、4096、8192、16384 间调整;允许只见 particle State/draw resources 重建。 +- W/L/O/T/F 调整不得产生 STYLE_LOAD、Field upload、Timeline reset、Runtime Window reset 或 Weather dataset reload。 +- `RESET DEFAULT` 后 HUD 恢复 P=2048、W=0.75、L=0.0040、O=0.75、T=0.55、F=0.88。 +- 不点 PLAY 或在 `TIME → PAUSE` 后,Timeline cursor/MIX 可冻结,但粒子仍应在当前固定风场运动。 + +## Timeline、边界与地图交互 + +1. `TIME → PLAY / RESTART`,确认 READY、PLAY、自动 MIX 和 T0→T3。 +2. 观察至少两次 `production→production` boundary:T0/T1→T1/T2→T2/T3。每次应有 `FIELD_REUSE from=OLD_T1`、只上传 `NEW_T1`、`simulationRebuild=false`、`trailCleared=false`。 +3. 检查每约 1 秒的 `ParticleMotion`:sampleU/sampleV 非零时,有 `dt`、`visualSpeedScale=6000` 和非零 `estimatedStateDeltaWorld`;此日志为 CPU 估算,不做持续 GPU readback。 +4. Pan、Zoom、Rotate,并进行 Activity Rotation:GSI、色场和粒子保持地理对齐;旋转后应自动恢复,无需点击按钮。 + +## 负向与保护检查 + +- `MORE → HOST DUPLICATE`:当前 Host window 返回 NOOP,fieldUploadCount/simulationResetCount 无增量。 +- `MORE → HOST INVALID`:Adapter reject,保持最后有效 Field,Renderer/色场连续,无黑屏、无 crash。 +- 本次没有 Weather Service、HTTP、React Native 或 Jest;真实 WeatherV2 仍来自 Android asset。 + +保存日志中的 GSI、ParticleTuning、ParticleMotion、Timeline/Boundary、Duplicate/Invalid 片段及任何视觉问题。MIX 文字可能滞后、WAITING/INVALID 视觉不够明显仍是 NON-BLOCKING observability,应以 Core 日志判断。 diff --git a/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v3.md b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v3.md new file mode 100644 index 000000000..15b8c7fdf --- /dev/null +++ b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v3.md @@ -0,0 +1,35 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化人工验收 v3 + +状态:等待用户手工 Android 验收。Codex 不执行 Android Gradle、APK、ADB、App 启动或设备测试。 + +## Windows 11 构建与日志 + +在 Windows 11 项目目录手工执行: + +```powershell +./gradlew :MapLibreAndroidTestApp:assembleOpenGLDebug --console=plain +adb -s 192.168.1.146 install -r +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 logcat | findstr /I "ParticleStateTarget ParticleMotion REAL_WEATHER ParticleTimeline FIELD_REUSE FIELD_UPLOAD simulationRebuild trailCleared" +``` + +## 第一优先:State 精度 + +1. 进入 `Particle Core Layer Validation → REAL WEATHER`,确认 GSI、真实色场、HUD 默认 `P 2048 · W 0.75 · L 0.0040 · O 0.75 · T 0.55 · F 0.88`。 +2. **不要点击 PLAY,或在 `TIME → PAUSE` 后观察。** Particle position 必须在固定真实风场中连续、明显移动;Timeline cursor/MIX 冻结不应暂停 simulation。 +3. 日志必须有: + +```text +ParticleStateTarget: stateAType=Float stateBType=Float trailAType=UnsignedByte trailBType=UnsignedByte +ParticleStateTarget: event=CREATE_SUCCESS kind=STATE dataType=Float +ParticleMotion: ... sampleU=... sampleV=... dt=... visualSpeedScale=6000 estimatedStateDeltaWorld=... +``` + +4. 若有 `CREATE_FAILED kind=STATE dataType=Float`,记录完整错误;Particle State 应 fail-closed,不得静默以 UByte 继续。 +5. 若 Float 日志正确但粒子仍完全不动,**停止调节 visualSpeedScale=6000**,保存日志并进入后续 GPU State texel readback / State Update→Draw 定向任务。 + +## 继续综合验收 + +在第一优先项通过后,沿用 v2 的 REAL WEATHER tuning/RESET、PLAY T0→T3、两次 Window Boundary、Duplicate/Invalid、Pan/Zoom/Rotate/Activity Rotation 检查。正常边界仍应为 OLD_T1→NEW_T0 reuse、only NEW_T1 upload、`simulationRebuild=false`、`trailCleared=false`。 + +本轮没有 Weather Service、React Native、Jest 或 canonical 数据变化。 diff --git a/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v5.md b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v5.md new file mode 100644 index 000000000..39ab7eb67 --- /dev/null +++ b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v5.md @@ -0,0 +1,41 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化人工验收指令 v5 + +环境:Windows 11;设备 `192.168.1.146`。Android 编译、安装和设备操作均由用户手工执行。 + +```powershell +cd platform\android +$env:RENDERER='opengl'; $env:BUILDTYPE='Debug'; make apackage --console=plain +adb -s 192.168.1.146 install -r +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 logcat | Select-String 'Particle(StateTarget|Lifecycle|Motion|Trail|Timeline|GPU|Render)' +``` + +## A. 固定 T0 / Timeline Pause + +进入 `REAL WEATHER`,保持 Timeline Pause,连续观察 2–3 分钟: + +- ParticleMotion 持续;`ParticleLifecycle: activeSlots=2048` 持续存在。 +- 可见密度不随时间持续下降,中央区域不逐步清空,粒子不长期堆积在边缘。 +- 粒子平滑运动,无大面积同步闪烁。 + +## B. 放大地图 + +Zoom 到日本/台风附近,确认 segment 是细小连续的真实位移轨迹,不再是固定 world-length 的粗橙色短棍或放大的 Trail texel。 + +## C. 相机变化 + +执行 Pan、Zoom、Rotate、Resize/Activity Rotation。确认一次 `ParticleTrail: event=CAMERA_RESET reason=... statePreserved=true fieldResubmit=false timelineReset=false`;旧 Trail 可清除、新 Trail 立即累积,粒子 world position 连续。 + +## D. Trail Fade + +在 REAL WEATHER 参数菜单调节 `F`。F 增大应明显延长 Trail 保留;Pause 不应停止 wall-time 衰减。 + +## E. Timeline PLAY + +执行 `TIME → PLAY` 至 T3。继续确认 `OLD_T1 → NEW_T0 reuse`、only `NEW_T1` upload、正常 boundary 的 `simulationRebuild=false` 和 `trailCleared=false`。正常 forecast boundary 不应出现 `ParticleTrail: event=CAMERA_RESET`。 + +## F. Rotation + +Activity Rotation 后确认 Field/State 能恢复、粒子持续运行、Trail 按新 viewport 重新建立,且无需点击按钮恢复。 + +若出现 State/Trail 创建失败,保留 `ParticleStateTarget: event=CREATE_FAILED` 日志;不得以 UByte fallback 或调高 speed scale 规避。 diff --git a/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v6.md b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v6.md new file mode 100644 index 000000000..1ed68be2e --- /dev/null +++ b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v6.md @@ -0,0 +1,20 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化人工验收指令 v6 + +环境:Windows 11;设备 `192.168.1.146`。Android 构建、安装和设备操作由用户手工执行。 + +```powershell +cd platform\android +$env:RENDERER='opengl'; $env:BUILDTYPE='Debug'; make apackage --console=plain +adb -s 192.168.1.146 install -r +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 logcat | Select-String 'Particle(Motion|Lifecycle|Trail|Timeline|GPU|StateTarget)' +``` + +1. REAL WEATHER 自动 fit(z≈2–3):粒子应持续可见和运动,不应只剩 WeatherColorLayer,也不得回到固定粗橙色短棍。 +2. z≈5:粒子仍可见,`ParticleMotion` 检查 `referenceZoom=9`、有效 scale 和 simulation delta。 +3. z≈9:不应飞跃或高频越界 respawn;日志中的 simulation delta 不得超过 2400。 +4. Timeline Pause:forecast 不推进,Particle simulation 与 wall-time lifecycle 继续;`ParticleLifecycle` 应为 `lifecycleBaseRealSeconds=12`,不含 simulation lifecycle 字段。 +5. 连续 pinch zoom/pan/rotate:每段交互最多一次 `CAMERA_INVALIDATE`,停止约 150ms 后一条 `CAMERA_STABLE_RESUME`;State/Field/Timeline 不重置。只有实际 size 变化才出现 `VIEWPORT_RESIZE`。 +6. PLAY 至 T3:继续确认 OLD_T1→NEW_T0 reuse、only NEW_T1 upload、`simulationRebuild=false`、`trailCleared=false`。 + +所有 ADB 命令必须显式使用 `adb -s 192.168.1.146 ...`。若失败,保留完整 Particle 日志;不要通过改回 world-length streak 或提高固定 speed scale 规避。 diff --git a/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v7.md b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v7.md new file mode 100644 index 000000000..86c91dcb4 --- /dev/null +++ b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v7.md @@ -0,0 +1,22 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化人工验收指令 v7 + +环境:Windows 11;设备 `192.168.1.146`。Android 构建、安装与设备操作由用户手工执行;Codex 未执行这些步骤。 + +```powershell +cd platform\android +$env:RENDERER='opengl'; $env:BUILDTYPE='Debug'; make apackage --console=plain +adb -s 192.168.1.146 install -r +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 logcat | Select-String 'Particle(Density|Motion|Lifecycle|Trail|Timeline|GPU|StateTarget)' +``` + +1. `REAL WEATHER` 自动 fit(z≈2–3):粒子持续可见、运动且自然分布;不得回到固定 world-length 粗短棍,也不应呈规则均匀点阵。 +2. z≈5:停止相机操作至少 150ms。确认 `ParticleTrail: event=CAMERA_STABLE_RESUME` 后出现一条 `ParticleDensity mode=VIEWPORT_RESPAWN cameraStable=true margin=0.15`;粒子密度应比 v8 明显改善。 +3. z≈8–9:不提高 `particleCount` 仍有足够可见粒子;不得有整批 reseed、大面积闪烁或高频越界 respawn。`ParticleDensity` 的 `respawnBounds` 必须位于 `fieldBounds` 内。 +4. 连续 pinch zoom/pan/rotate:交互期间 State 继续,`ParticleTrail` 只一次 `CAMERA_INVALIDATE`,不反复重建 texture;稳定后 resume 并启用 viewport respawn。不得出现 `SIMULATION_RESET`、Field resubmit 或 `PARTICLE_COUNT_CHANGE`。 +5. Timeline Pause:forecast cursor/mix 冻结,但 Particle simulation 和 wall-time lifecycle 继续;生命周期仍为 12 real seconds、hash jitter 9–15 seconds。 +6. `TIME → PLAY` 至 T3:继续确认 `OLD_T1 → NEW_T0` reuse、only NEW_T1 upload、`simulationRebuild=false`、`trailCleared=false`。正常 forecast boundary 不得因 density 逻辑清 Trail。 + +若 `ParticleDensity` 显示 `FIELD_FALLBACK`,保留完整日志和相机状态;这是空/无效/过小交集的 fail-safe,不得通过提高 particleCount 或改回固定 streak 绕过。 + +所有 ADB 命令必须显式使用 `adb -s 192.168.1.146 ...`。 diff --git a/docs/60_测试/STEP-060_RNTestHost_Android人工编译与验收_v1.md b/docs/60_测试/STEP-060_RNTestHost_Android人工编译与验收_v1.md new file mode 100644 index 000000000..f0ef2e3fe --- /dev/null +++ b/docs/60_测试/STEP-060_RNTestHost_Android人工编译与验收_v1.md @@ -0,0 +1,16 @@ +# STEP-060 RN Test Host Android 人工编译与验收 + +本说明只列出人工步骤;Codex 未执行 Android Gradle、发布、安装或 ADB。 + +1. 确认当前为 `particle/main`,并记录 `git rev-parse HEAD`。Gate C 封存 Linux 环境是 custom MapLibre Native AAR 的构建环境;不要为 Test Host 生成工程的版本修改它。 +2. 移走或清理 `~/.m2/repository/org/maplibre/gl/android-sdk-opengl/13.4.1/` 的旧 artifact,避免同坐标 provenance 混淆。2026-08-29 旧 AAR 的 SHA-256 是 `5c67d2751fafe211a0df4b82deded09ab1c07cf6ca0f4a12a880283287f85e5c`,只能作为 Gate C 历史证据。 +3. 本轮唯一坐标是 `org.maplibre.gl:android-sdk-opengl:13.4.1-step060f`。必须先在 Linux 手工构建并 publish 这个新 AAR,记录 build output 与 Maven Local AAR SHA-256 且两者一致;不得复用 `step060e` 或先前 `13.4.1` artifact。 +4. 将该 AAR 按发布流程复制到 `/mnt/nas/pbf/`,Win11 用每次不同的 `?v=` 下载,核对 SHA-256 后安装 Maven Local。 +5. `cd integration/react-native/MapLibreParticleRNTestHost && npm ci && npx expo prebuild --platform android --clean`。`package-lock.json` 已在无 `--legacy-peer-deps` 的 npm 安装下通过;不要改用 Yarn 或生成 `yarn.lock`。本次 plugin 变更后必须 clean prebuild。 +6. 检查生成 root Gradle 有 `MapLibreParticleRNTestHost custom OpenGL SDK`、`nativeVersion=13.4.1-step060f`、`nativeVariant=opengl`;MLRN 与 Bridge 两条 `dependencyInsight` 都必须解析到 `13.4.1-step060f`。Maven Central 必须排除精确 custom module `org.maplibre.gl:android-sdk-opengl`,使该 AAR 缺失时失败而非 fallback。 +7. 首次 prebuild 后实际记录 Test Host 的 Gradle wrapper、AGP、Kotlin plugin、min/compile/targetSdk、NDK、JDK、Hermes 与 New Architecture。当前观察到的 `9.3.1/8.12.0/2.1.20/24/36/36/27.1.12297006` 属于 Test Host 生成工程,不等于 Gate C 环境。 +8. 再按实际生成工程编译。`app.json` 的 landscape 配置在本次 clean prebuild 后应使 `MainActivity` 生成 `android:screenOrientation="landscape"`;核对后以 `scripts/05-install-and-run.ps1` 安装启动。该脚本必须同时保持 `adb -s 192.168.1.146 reverse tcp:8091 tcp:8091` 与 `adb -s 192.168.1.146 reverse tcp:8081 tcp:8091`。 +9. 对同一 reactTag 依次验证 Phase A:getState、enable、WeatherColor visible、particle visible、FPS 30 均得到结构化 Native result。 +10. 验证 Phase B:stops、独立 particle/trail RGBA、count/line/length/opacity/fade 与 WeatherColor opacity。`weather opacity` 必须为 `ACCEPTED`,且不再出现 `ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_PROPERTY`。无 Binary Field 时 PLAY/PAUSE、HALF/NORMAL/DOUBLE 与 epoch-ms seek 仍可因 `TIMELINE_NOT_READY` 返回 `REJECTED`;不得改变这个 fail-closed 行为,也不得出现 JS `PLAYING/PAUSED` 第二状态机。 +11. 重载 style:旧 Runtime 必须 disposed;style ready attach 的新 Runtime 正常工作。remount Map 后,用旧 reactTag 调用必须返回 `MAP_VIEW_NOT_FOUND`,不得命中新 Map。 +12. 卸载 Host:确认 view detach dispose;无持续 Handler wake、无全局 current map。通过上述 Android 验收后才进入 STEP-061;此前不要尝试 number[]、JSON/base64 大 field。 diff --git a/docs/60_测试/STEP-072_V3-08_Android单次综合验收_v1.md b/docs/60_测试/STEP-072_V3-08_Android单次综合验收_v1.md new file mode 100644 index 000000000..3204620ae --- /dev/null +++ b/docs/60_测试/STEP-072_V3-08_Android单次综合验收_v1.md @@ -0,0 +1,125 @@ +# STEP-072 V3-08 Android 单次综合验收 + +> **历史 V3 验收文档。** 本文的 Native `.2`、Maven Local、旧 reverse/临时路径仅描述 STEP-072 当时事实;当前 QBIN V4 `.3` 验收请改读 `docs/00_项目总则/当前基线与开发路线_v2.md` 与 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。 + + +## 状态与边界 + +本文件只列出用户手工执行的单次 Android 验收流程。目标组合是 RN/TS `13.4.1-particle.3` 与 Native `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2`。`.2` 是 STEP-069 v2 multi-plane 所需的新坐标;不得复用、覆盖或误用旧 `.1`。 + +执行前记录当前 commit。所有 Gradle 调用均使用 `--console=plain`;所有设备命令均显式指定 `adb -s 192.168.1.146`。本流程完成前不得将 AAR、发布、Android 或 bridge 验收写为 PASS。 + +## 1. Native `.2` Release AAR 与 Maven Local + +在 `/mnt/sda1/MapLibreParticleLab/platform/android` 手工确认 `VERSION` 为 `13.4.1-particle.2`,然后执行 OpenGL Release AAR 构建与 Maven Local 发布: + +```bash +make RENDERER=opengl BUILDTYPE=Release JOBS=1 \ + 'MLN_ANDROID_GRADLE=./gradlew --console=plain --parallel --max-workers=$(JOBS) -Pmapbox.buildtype=$(buildtype) -Pmapbox.stl=$(MLN_ANDROID_STL) -Dorg.gradle.internal.http.socketTimeout=$(SOCKET_TIMEOUT) -Dorg.gradle.internal.http.connectionTimeout=$(CONNECTION_TIMEOUT)' \ + apackage +./gradlew :MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain +``` + +上述 `make` 写法已用 `make -n` 验证,最终 Gradle 命令包含 `--console=plain`、`--max-workers=1`、`-Pmapbox.buildtype=release` 和 `-Pmaplibre.abis=all`;不要把 `MLN_ANDROID_GRADLE` 只作为环境变量放在 `make` 前面,因为 Makefile 的普通赋值会覆盖环境变量。 + +记录构建产物 `MapLibreAndroid/build/outputs/aar/MapLibreAndroid-opengl-release.aar` 的 SHA-256,并与 Maven Local `.2` AAR 的 SHA-256 比对;二者必须完全一致。 + +AAR 不是可直接传给 `javap` 的 JAR;先提取 `classes.jar`: + +```bash +M2_AAR="$HOME/.m2/repository/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.2/android-sdk-particle-opengl-13.4.1-particle.2.aar" +unzip -p "$M2_AAR" classes.jar > /tmp/step072-particle2-classes.jar +javap -classpath /tmp/step072-particle2-classes.jar -public org.maplibre.android.maps.WeatherRuntime \ + | grep -E 'submitField|clearFields|setFieldValueSource|setParticleAdvectionScale' +``` + +若任一方法缺失、AAR 坐标不是 `.2` 或 SHA 不一致,停止后续 Test Host 验收。 + +## 2. Linux → Win11 Native `.2` handoff + +若 Test Host 继续按已验证的 CARBON7i/Win11 流程构建,则 Linux Maven Local 发布完成后,必须把同一 `.2` 的 AAR、POM、module 三个文件传到 Win11,并放入 Win11 当前用户的 Maven Local 对应目录;不能只复制 AAR,也不能让 Win11 fallback 到 `.1` 或官方 SDK。 + +目标目录结构: + +```text +%USERPROFILE%/.m2/repository/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.2/ + android-sdk-particle-opengl-13.4.1-particle.2.aar + android-sdk-particle-opengl-13.4.1-particle.2.pom + android-sdk-particle-opengl-13.4.1-particle.2.module +``` + +Win11 上再次计算 AAR SHA-256,必须与 Linux build output 及 Linux Maven Local AAR 三者一致。可用 `Get-FileHash -Algorithm SHA256`。完成后再进入 Test Host clean prebuild。若直接在 Linux 同机编译 Test Host,则可使用该机 Maven Local,不需要此 handoff;但不得因此省略 artifact SHA/API 检查。 + +## 3. Test Host 单次 Android 周期 + +沿用已验证的 CARBON7i/Win11 流程时,在 **Win11 当前 Test Host checkout 根目录**执行下面的 PowerShell 命令。先 clean prebuild,再用 dependencyInsight 确认 app 最终只解析到 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2`,然后只编译一次 Debug APK: + +```powershell +npm ci +npx expo prebuild --platform android --clean +Set-Location android +.\gradlew.bat :app:dependencyInsight --dependency android-sdk-particle-opengl --configuration debugRuntimeClasspath --console=plain +.\gradlew.bat :app:assembleDebug --console=plain +Set-Location .. +``` + +`dependencyInsight` 中若出现 `.1`、官方 `org.maplibre.gl:android-sdk-opengl` 最终胜出,或 `.2` 未解析,立即停止,不安装 APK。 + +在另一个 Win11 PowerShell 窗口启动 Metro,固定监听 8091: + +```powershell +npm run start +``` + +Metro ready 后执行固定设备安装/启动命令;每条 ADB 都必须显式带设备地址: + +```powershell +adb -s 192.168.1.146 reverse tcp:8091 tcp:8091 +adb -s 192.168.1.146 reverse tcp:8081 tcp:8091 +adb -s 192.168.1.146 install -r android/app/build/outputs/apk/debug/app-debug.apk +adb -s 192.168.1.146 shell am force-stop org.maplibre.particle.rntesthost +adb -s 192.168.1.146 shell am start -n org.maplibre.particle.rntesthost/.MainActivity +``` + +Metro binary asset extension 必须仍为 `.f32` 与 `.u8`。本轮 Test Host 只做这一轮 clean prebuild + Debug APK 编译/安装,不为单个数据组重复编译 APK。 + +## 4. 五组 field 数据逐组验收 + +对每一组分别先 clear,再按 T0、T1 顺序加载,且每次均记录页面结果和 logcat: + +| 数据组 | plane/粒子期望 | +| --- | --- | +| Wind real | vector;Wind/GLOBAL 粒子显示 | +| Rain scalar-only | scalar;不得显示粒子 | +| Wave scalar+DIRECTION | scalar + direction vector;色场与粒子均可用 | +| Tide VELOCITY | vector velocity;粒子可用 | +| Wave synthetic independence | scalar + direction vector;专用于独立性检查 | + +每一组都必须依序满足 T0=`ACCEPTED_PENDING_INITIAL_WINDOW`、T1=`INITIAL_WINDOW_READY`,并使最终 Runtime=`READY`。任一状态不同、出现拒绝或 Runtime 非 READY,记录失败组并停止将该组标记为通过。 + +对每个 Runtime READY 的数据组验证:PLAY/PAUSE、HALF/NORMAL/DOUBLE、seek T0/T1、AUTO/30/60 FPS,以及 Field/Particle 的独立显隐。额外检查 Wave synthetic independence:scalar 色场和 direction particle 的表现及开关互不绑定;Rain scalar-only 不得出现粒子。 + +## 5. Logcat 与结论 + +使用固定设备筛查 logcat,观察 v2 submit、initial window ready,以及 fatal bridge/JNI 错误: + +```powershell +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 logcat -v time | Select-String -Pattern 'submitField|INITIAL_WINDOW_READY|initial window ready|WeatherRuntime|FATAL EXCEPTION|JNI|bridge' +``` + +记录每组的 T0/T1/READY 证据、控制项结果、AAR SHA 比对、`javap` 输出摘要及 logcat。仅当五组均符合要求且无 fatal bridge/JNI 错误时,才可由用户决定更新 Android 验收状态;本文件本身不代表已执行或通过。 + +## 2026-09-11 V3-08 最终关闭 + +状态:**CLOSED / PASS**。 + +Win11/CARBON7i 使用 Gradle 8.14.3、AGP 8.12.0、Kotlin 2.1.20、JDK 17.0.18、NDK 27.1.12297006、compileSdk/targetSdk 36、minSdk 24 完成正式 npm 消费链与 Debug APK 构建。dependencyInsight 最终解析到 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2`。 + +实体机 `192.168.1.146` 上五组数据均满足 T0=`ACCEPTED_PENDING_INITIAL_WINDOW`、T1=`INITIAL_WINDOW_READY`、最终 Runtime=`READY`:Wind real、Rain scalar-only、Wave real、Tide/Current、Wave synthetic independence。 + +控制面 PLAY/PAUSE、HALF/NORMAL/DOUBLE、FPS AUTO/30/60、seek T0/T1、Weather/Particle 独立显隐、Field stops、Particle color、Trail color、Apply visual 均返回 `ACCEPTED`;最终 FATAL/JNI/UnsatisfiedLinkError 扫描为空。 + +测试期间发现 fixture 位于 Metro project root 外,临时 Win11 `D:\V308` 验收 worktree 通过 `watchFolders` 纳入 `integration/react-native/particle-v3-fields` 后 bundle 成功。该临时 Metro 修改不进入正式仓库,不属于 Native/RN Runtime 缺陷。 + +最终产物:Native `.2` AAR SHA-256 `f890f834692d3cacb8a281b2d0f82cf4a7f8ba7756132092be800505b961e9a2`;RN `.3` tgz SHA-256 `16e5dab86aac9832f7a48d8c35dd9821f80f52e7e53ff4e0e6c98724781eda3a`;APK SHA-256 `49824062CAB72771282294114B5A818D2067F9B786B8697D2D2A983BF0B743E2`。 diff --git a/docs/60_测试/STEP-097_V13修复后集中验证清单_v1.md b/docs/60_测试/STEP-097_V13修复后集中验证清单_v1.md new file mode 100644 index 000000000..4bd9e9776 --- /dev/null +++ b/docs/60_测试/STEP-097_V13修复后集中验证清单_v1.md @@ -0,0 +1,130 @@ +# STEP-097 V13 修复后集中验证清单 v1 + +状态:**STEP-097 整体 PARTIAL;A1 与 A2 的 Android/Core 检查已完成,B/C 未开始。** 下方较早段落按当时事实保留;后续 RDC 结果以本文末尾带日期的更新为准。Metal host Core 测试不等于 iOS runtime,iOS runtime 按用户豁免记 `USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`。 + +## 通用 Core 与现有测试入口(RDC) + +在本仓 Mac 当前 checkout,使用已配置的现有 build 目录;若目录不存在,先按当前构建规范配置。此处不自动运行: + +```sh +cmake --build build-step088-opengl --target mbgl-test-runner -j3 +build-step088-opengl/mbgl-test-runner --gtest_filter='RenderSegmentPlan.*:RenderSegmentDiagnostics.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:ParticleFastPath.*' +``` + +新入口 `RenderSegmentDiagnostics.OldCacheReleaseCannotChargeReplacementAtSameIndex` 预期同 index 的旧 revision release 不计入新 topology,当前 revision release 计 1。`RetainedUnderlayCache.UnsupportedAndResourceFailureFallBackSafely` 为旧入口,用于资源失败后的基础状态;尚不能替代真实 layer render 异常注入。失败保留完整日志,回传首个错误和日志路径。 + +## Android OpenGL(Particle-Android / RDC,NOT RUN) + +```sh +./tools/particle_android_validation/build_validation_apk.sh +``` + +RDC 应使用同一 commit 生成 APK,再由后续授权的 Win11/模拟器操作按标准流水线安装。现有 Test Host 仅有 C-R 控制,尚缺同一 vector source/same tiles 的 R-R、R-C-R、C-R-R fixture,故该 APK 即使构建成功也不能关闭 01/03。需比较 cache ON/OFF 的首帧、重建、reuse、主目标与离屏目标像素;失败后 diagnostics 不得 Ready,下帧可恢复。记录 Renderer、ABI、SHA-256、截图与日志。不得引用 STEP-094 历史 43/43 作为本轮结果。 + +## iOS Metal(Particle-iOS / RDC,NOT RUN) + +```sh +cmake --build build-ios --target ios-sdk-static -- -quiet +``` + +同 commit 的 iOS simulator 与真机需开启 Metal API Validation,捕获 retained R-R/R-C-R 的 color/depth/stencil texture、load/clear/store,特别确认 simulator 的 Depth32Float_Stencil8 同时绑定 depth 和 stencil。故障注入覆盖附件创建失败、encoder 创建失败及 layer render 异常;预期不发布 Ready cache、后续能 fallback/retry。专用自动测试入口尚缺,不能因静态修改或 `ios-sdk-static` 成功就宣称 Metal runtime PASS。 + +## 后续 GPU 性能(粒子负责人协调,NOT RUN) + +完成 A1/A2/B/C 代码与平台测试入口后,以相同设备、commit、Style、视口和场数据采样 cache ON/OFF、R×viewport×fieldKey 阶梯,记录 30/60 FPS、p50/p95/p99、GPU/CPU frame time、纹理创建/释放、峰值/长稳内存、方向/缩放/状态恢复。Metal capture 记录 `waitUntilCompleted` 的实际同步点及资源依赖。未经运行不填写数字;F06 multi-C 与 F08 精细失效另行设计。 + +## Test Host 依赖(B 阶段后,NOT RUN) + +目前默认 RN `.10` 与 Native `.13` 错配,阶段 B 未开始,不得运行安装并声称配对成功。B 修正 package/lock/SPM 后,RDC 再在 Test Host 目录执行: + +```sh +npm ci +npm run typecheck +``` + +必须同时检查实际已安装 RN tgz、Android Gradle substitution、iOS SPM exactVersion 与 lockfile;不得以 node_modules 临时软链代替默认安装结果。本清单不含 Jest 命令。 + +## 2026-09-25 FZG1 A1 部分执行结果(覆盖上方 Codex 交付时的 NOT RUN 状态) + +- 标准构建脚本已由 Mac RDC 实际执行;HEAD `91c2491b0`,x86_64 OpenGL Debug APK `BUILD SUCCESSFUL`,SHA-256 `8677197c5f83a22747cb51f0c261edffc5db4678fc25b3f366474f6ecb94dc99`。冻结于 `artifacts/maplibre-particle/step097-fzg1/`。 +- ADB `192.168.200.139:5555` 对 FZG1-2 实际执行覆盖安装、20 组截图/UI XML、既有 STEP-093 topology、CACHE OFF/ON、相机、非法范围恢复与 mutation/fail-closed,日志见 `docs/报告/STEP-097_A1_FZG1测试APK与实机验证报告_v1.md`。 +- 此处只是设备冒烟;A1 的同 tile stencil fixture、Metal 附件验证、真正 LayerGroup exception 注入与 `OldCacheReleaseCannotChargeReplacementAtSameIndex` GTest **仍 NOT RUN/缺专用入口**;A2/B/C 和软件发布亦未执行。 + +## 2026-09-26 第 19 项原生 GTest 已执行 + +- RDC 使用当前源码 HEAD `0d13552bed84f427dd43cae9856391bd26622179` 增量重编译 `mbgl-test-runner`:95/95 steps,exit 0;第 19 项 `OldCacheReleaseCannotChargeReplacementAtSameIndex` **1/1 PASS**;`RenderSegmentDiagnostics.*` **5/5 PASS**,两次退出码均为 0。原始日志和分析见 `docs/报告/STEP-097_A1_第19项GTest运行验证报告_v1.md`。 +- 上方第 19 项“NOT RUN”是 2026-09-25 FZG1 冒烟时点的历史事实,现已由本条更新。01 同 tile、02 Metal、03 render exception 测试仍未闭合;A1 不得整体标 PASS。 + +## 2026-09-26 第 19 项 FZG1 设备辅助测试结果 + +- 经 RDC 在 FZG1-2 重新覆盖安装与 hash 复核,执行两轮 topology 轮换和资源诊断场景,20 个截图检查点;18 个有效配置均 valid=true/fallback=false,缺失 START fail-closed 后恢复。B/T 的粗粒度 create-release 差额 0/1,实时日志中无应用崩溃记录。详见 `docs/报告/STEP-097_A1_第19项FZG1运行验证报告_v1.md` 与 Mac 忽略目录 `artifacts/maplibre-particle/step097-fzg1-gtest19-device/`。 +- 这次是 Android Runtime 冒烟,前述第 19 项精确计数归属 GTest 由 Mac 原生测试覆盖;01 同 source/tile stencil、02 Metal、03 绘制异常注入仍缺,A1 整体 PARTIAL,A2/B/C 未实施。 + +## 2026-09-26 A1 新入口执行清单(RDC,NOT RUN) + +通用 Core(当前 OpenGL 构建目录): + +```sh +cmake --build build-step088-opengl --target mbgl-test-runner -j3 +build-step088-opengl/mbgl-test-runner --gtest_filter='RenderOrchestratorError.*:RenderSegmentDiagnostics.OldCacheReleaseCannotChargeReplacementAtSameIndex' +``` + +预期异常 observer/failed 与 cache 不复用、重试后 Reuse;第 19 项保持 1/1 PASS。Metal GTest `MetalRetainedAttachments.*` 需由 RDC 使用可在 Mac 主机执行且配置 `MLN_WITH_METAL=ON` 的 `mbgl-test-runner` 构建目录执行;先检查所用目录的 CMakeCache、目标和 Metal device,再编译、执行并记录命令与日志。不能用 iOS 静态库构建替代主机 GTest。真机/simulator 的创建失败、encoder 失败与 API Validation 仍单独执行。 + +Android 同源 stencil:以本提交标准脚本构建 Validation APK,打开 Particle Core Layer Validation → STEP-093 → STEP-097 SAME SOURCE STENCIL;依次选择 RR、RCR、CRR。每种拓扑在 CACHE ON 首帧、稳定 reuse、REBUILD CURRENT 后,再切 CACHE OFF 与 ON;保存每个检查点截图、`STEP097_STENCIL_PLAN/READY/DIAGNOSTICS` 日志及 `READ SNAPSHOT`。预期 `accepted=true`、`valid=true`、`fallback=false`,多个 retained 段分别 build/reuse,四层同一 source 的轮廓与填充在缓存切换后无残影/漏裁剪。再以相机移动强制重建;记录主目标与离屏结果。此清单是待执行判据,不能据此宣称像素 PASS。真正 GPU 绘制异常及下一帧 full fallback/recovery 需 RDC 专项注入;GTest 状态机不能代替它。 + +## 2026-09-26 A1 实测结果(覆盖上方待运行记录) + +- Android/FZG1:新 APK 源码 `8c006ac83`,SHA-256 `48df19cf5a4c4250934efa2596423d3ad9571fe49a65b9b704a81cdb41f58671`;同 source/tile RR/RCR/CRR、重建、缓存 OFF/ON 18/18 状态与像素采样检查通过,无本次捕获的 FATAL。 +- Core:03 `RenderOrchestratorError.*` 2/2 PASS;19 `OldCacheReleaseCannotChargeReplacementAtSameIndex` 1/1 PASS,诊断组 5/5 PASS。 +- Metal:编译通过,2/2 SKIP(无 Metal device);用户豁免 iOS runtime,标 `USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`,不是 iOS 运行测试 PASS。详见 `docs/报告/STEP-097_A1_Android专项验收报告_v1.md`。后续 A2/B/C 未执行。 + +## 2026-09-26 A2 中途静态/Core 结果(不是 A2 关闭) + +- OpenGL `mbgl-test-runner` 编译 exit 0;定向 16/16 PASS;两次 Shader generator 均 exit 0 且输出一致。Metal host 可编译并执行同一跨 Core GTest 16/16 PASS,未运行 iOS 设备。 +- 上述结果仅覆盖现有原生测试入口。Codex A2 代码尚未审查/提交,Android 设备/APK 仍 NOT RUN,不能推进 B/C。证据与完整边界见 `docs/报告/STEP-097_A2_阶段验证记录_v1.md`。 + +## 2026-09-26 A2 最终验证结果(RDC 独立执行) + +### Core / Shader + +- 原先因旧测试断言与既有语义不一致而失败的 58 项完整集合,在修复断言后 **58/58 PASS**。`sameFieldGpuContent()` 仍表达忽略 producer generation/dataVersion 的解码 payload 等价;`sameFieldGpuEndpoint()` 单独负责严格 GPU endpoint identity。 +- 当前 broad filter 在 OpenGL 与 Metal host `mbgl-test-runner` 各 **121/121 PASS**。命令: + +```sh +build-step088-opengl/mbgl-test-runner --gtest_filter='Particle*.*:Source.RasterArray*:Source.HostField*:Source.ParticleField*:Source.CanonicalField*' +build-step097-metal-host/mbgl-test-runner --gtest_filter='Particle*.*:Source.RasterArray*:Source.HostField*:Source.ParticleField*:Source.CanonicalField*' +``` + +- 三项 A2 Source 定向回归在 OpenGL 与 Metal host 各 **3/3 PASS**:mix-only 50 ticks 不重物化、真 endpoint 与 revision 变化触发物化、同步通知重入不双推进、clear fail-closed。 +- `node shaders/generate_shader_code.mjs` 两次执行成功且输出幂等。Metal host 是 Mac 上 Core 测试,不是 iOS device runtime。 + +### Android / FZG1-2 + +- RDC 使用 `./tools/particle_android_validation/build_validation_apk.sh` 构建标准 OpenGL Debug x86_64 APK;61,753,803 bytes,SHA-256 `27bf4a6cc191bd86c4d1c10398478d81c2999edd61fe7242e40e5188516b0ca5`。FZG1-2 `192.168.200.139:5555` 安装成功,设备 APK SHA-256 相同。 +- Test-App-only V4 Golden Wind T0/T1:load、seek 25/75、speed 2x/1x、PLAY 30 秒、PLAY 时 UI/TIME 可响应、PAUSE/stable pause、replay、CLEAR、reload、再次 PLAY/PAUSE;脚本 exit 0、17 PNG、24 steps、ANR=0。`05_play_15s.png` 可见粒子。30 秒场景闭环不是长期稳定性或性能测试。 +- stable mix-only epoch 中 renderer 日志的 `fieldUploadCount=6/resetCount=2` 保持不变;explicit CLEAR/reload 后 upload 合法增至 10,reset 增至 4;reload 后 playback 稳定在该新计数。不能宣称跨 CLEAR/reload 零上传/零 reset。 +- APK 工作树源 diff 与 `mix-reuse-build-source.diff` SHA-256 均为 `18927a5f30a88f5c1c3a00e0017473dc33b04151c227e4baf52250dc4ad2e6a2`,源码快照一致。原始 build/device/test 日志、steps 与截图位于 `artifacts/maplibre-particle/step097-a2-final-validation/`。 + +### 历史失败与范围 + +- 12:43 首次 ANR 为 synchronous Source notification 重入 forecast tick;`forecastTickInProgress` guard 与重入 GTest 已修复并覆盖。 +- 后续无 ANR 但主线程仍高负载、PLAY/TIME 无响应的试跑由 debuggerd 定位到每 tick 重物化大场和深比较;mix-only immutable endpoint reuse 后通过本次 FZG1 闭环。旧失败日志和 debuggerd 保留,不从报告中删除。 +- 本次不测 iOS runtime;标 `USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`。没有完成 GPU 性能基准、FPS/pXX 或长期运行测试。STEP-097 整体仍 PARTIAL,B/C 未开始;不包含 Jest、STEP-098、发布 `.13`。 + +### 2026-09-26 A2 源码独立 scoped commit 核验 + +- A2 的 26 个生产源码、shader、TestApp-only V4 Golden fixture 和定向测试文件已固定为 `f49ffa68a17e0a505199bdbeccf07f8543f3a05f` 并推送,`origin/particle/main` 回读一致;没有 stage 用户未跟踪 STEP-098。完整代码/设备证据仍见 `docs/报告/STEP-097_A2_阶段验证记录_v3.md`,不因 Git 提交改变原 APK 的 `git_dirty=true` 来源事实。STEP-097 整体 PARTIAL,B/C 待用户指令。 + +## 2026-09-26 B 阶段 RDC 实测与待闭合 + +- Mac Core 增量构建 PASS;B 的 JSON 严格校验、非法配置清除与恢复、数值溢出拒绝和合法 validity=0 四项 GTest:**4/4 PASS**。 +- 扩大回归初次 132/134,旧 16-bit fixture 误用 4-byte UInt8 storage;修正为合法四样本 8-byte 小端之后重新编译,broad **134/134 PASS**,没有放宽生产校验。 +- Test Host package.json/package-lock.json 的 RN tarball 均为正式 `13.4.1-particle.13`,实包 SHA-256/SRI 一致。 +- Android 要分别验证本仓新源码的 MapLibreMap destroy listener JUnit 与 RN module 对正式 `.13` AAR 的 compile;前者不能替代后者。正式 .13 source commit 无 `addOnDestroyListener`,因此当前 bridge 存在 ABI 阻塞,不能标 Android consumer PASS。 +- C、STEP-098、Jest 与正式发布不执行;完整证据见 `docs/报告/STEP-097_B_独立验收与版本阻塞报告_v1.md`。 + +## 2026-09-26 B 阶段 RDC 验证更新 +- Mac 最新 GTest runner:定向 6/6、扩展 139/139 PASS,旧 broad 2 项失败经纠正测试 fixture 后消除;完整日志 `artifacts/maplibre-particle/step097-b-rdc/validation-*.log`。 +- Android Native OpenGL Debug:`MapLibreMapDestroyListenerTest` 2/2 PASS;`compileOpenglDebugJavaWithJavac` exit 0;owner Kotlin 独立 JVM JUnit 4/4 PASS(源码 SHA 与当前 B10 一致)。隔离单测不能替代 React Native 模块与发布 AAR 精确配对编译。 +- B10 当前默认 RN Gradle 对已发布 .13 Native AAR 依赖新缺失符号 `addOnDestroyListener`;RN consumer 校验标 `BLOCKED_NATIVE_AAR_COMPATIBILITY`,禁止记录 PASS 或覆盖 .13。Linux 独立 Gradle 状态另记,C 与 STEP-098 均不得越过此门槛。 diff --git a/docs/60_测试/固定测试场景_v1.md b/docs/60_测试/固定测试场景_v1.md new file mode 100644 index 000000000..2fd8d0bc5 --- /dev/null +++ b/docs/60_测试/固定测试场景_v1.md @@ -0,0 +1,9 @@ +# 固定测试场景 v1 + +- uniform-east:恒定向东 +- uniform-north:恒定向北 +- vortex:旋涡 +- shear:剪切 +- coastal-mask:Coverage/Mask + +全部离线固定可重复。 diff --git a/docs/70_发布/Android_RN_QBINV4_13.4.1-particle.3_发布基线_v1.md b/docs/70_发布/Android_RN_QBINV4_13.4.1-particle.3_发布基线_v1.md new file mode 100644 index 000000000..0a93de041 --- /dev/null +++ b/docs/70_发布/Android_RN_QBINV4_13.4.1-particle.3_发布基线_v1.md @@ -0,0 +1,35 @@ +# Android / RN QBIN V4 `13.4.1-particle.3` 发布基线 v1 + +日期:2026-09-14。状态:Android OpenGL / RN Test Host 人工验证通过。 + +## 版本 + +- Native:`cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3` +- RN:`@navsea/maplibre-particle-rn@13.4.1-particle.3` +- MLRN:`11.3.6` + +## 公网 Maven + +Repository:`https://pbf.jpcn.ccwu.cc/maven` + +AAR:`https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.3/android-sdk-particle-opengl-13.4.1-particle.3.aar` + +AAR SHA-256:`9ef03eb0d0c03457adc04847cfa69fb67d85191daecd64d3b76db2ad8cc688cd`。 + +## 公网 RN tgz + +`https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.3.tgz` + +2026-09-14 Kotlin guard 修正后的公网回读 SHA-256:`abdd5b5f313b6dc2bc2f2b00848d1901be72fed6a6e1fa15899efb10df9f923f`。 + +## 发布规则 + +`https://pbf.jpcn.ccwu.cc/` 对应 Linux `/mnt/nas/pbf`。Native AAR/POM/module 写入 `/mnt/nas/pbf/maven/...//`;RN tgz 写入 `/mnt/nas/pbf/npm/`。发布后必须从公网重新下载并比对 SHA,不能只验证 NAS 本地文件。 + +本次开发过程中曾为完成 Android 编译验证覆盖同一个 `.3` tgz;该做法从本基线起禁止。以后公开 artifact 内容变化必须递增版本,避免 npm/CDN/Gradle cache provenance 不清。 + +## 当前验证事实 + +Win11 Debug APK 已成功构建、安装并运行;Metro/App 端口固定为 Metro `8091`、App `8081`、ADB reverse `8081 → 8091`。Test Host V4 Wind/Wave/Current/Rain Golden QBIN 功能由用户人工确认正常。 + +完整发布/构建命令以 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md` 为准。 diff --git a/docs/70_发布/README.md b/docs/70_发布/README.md new file mode 100644 index 000000000..e9c0978cb --- /dev/null +++ b/docs/70_发布/README.md @@ -0,0 +1,9 @@ +# 发布目录 + +保存 Particle release、MapLibre upstream 升级、冻结 commit 和双平台回归结果。 + +## 当前 Android / RN 发布基线 + +当前有效基线:`Android_RN_QBINV4_13.4.1-particle.3_发布基线_v1.md`。 + +历史 release/Step 记录可用于追溯,但当前 Consumer 版本、URL、SHA 和发布规则以该文件、`docs/00_项目总则/当前基线与开发路线_v2.md`、`docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md` 为准。 diff --git a/docs/报告/10_架构调查_Codex完成报告_v1.md b/docs/报告/10_架构调查_Codex完成报告_v1.md new file mode 100644 index 000000000..23859fb5d --- /dev/null +++ b/docs/报告/10_架构调查_Codex完成报告_v1.md @@ -0,0 +1,68 @@ +# 10_架构调查 Codex 完成报告 v1 + +日期:2026-08-29 + +任务:`docs/40_任务/10_架构调查指令书_v1.md` + +分支:`particle/main` +结论:调查完成,建议直接进入 `20_架构设计` + +## 1. 实际调查范围 + +本次对当前仓库的目录和工程形态、Git 基线、项目总则与规范、JavaScript/TypeScript 配置、Android Gradle/JNI/renderer、iOS Pod/MapView/backend、MapLibre Core Map/Style/Source/Renderer/Layer/gfx、CustomLayer、Camera/Matrix、帧驱动、线程边界、粒子搜索结果、fixture 和潜在性能风险进行了只读调查。 + +## 2. 必做项状态 + +| 必做项 | 状态 | 说明 | +|---|---|---| +| 读取总则 v1 | 已完成 | 根 AGENTS、`docs/00_项目总则/`、`docs/00_项目规范/` 已完整读取 | +| 确认 Git 基线 | 已完成 | 分支、HEAD、远端、submodule 状态已查询 | +| 确认整体目录和工程形态 | 已完成 | 确认是 MapLibre Native monorepo,不是 RN app | +| 确认核心依赖版本 | 已完成 | package/lock、Android VERSION/Gradle、iOS VERSION/Podspec 已读取 | +| 追踪 MapLibre 实际调用链 | 已完成 | Android、iOS、Core 三条链已追踪 | +| 调查地图生命周期 | 已完成 | Core、Android、iOS 分别记录 | +| 调查自定义渲染扩展点 | 已完成 | A/B/C/D 分类已记录 | +| 分别调查 Android | 已完成 | MapView、JNI、renderer thread、OpenGL/Vulkan、CustomLayer | +| 分别调查 iOS | 已完成 | MLNMapView、CADisplayLink、OpenGL/Metal、CustomStyleLayer | +| 调查线程和执行边界 | 已完成 | Native、Worker、FileSource、Render、GPU 已区分 | +| 调查 Camera/坐标/Matrix | 已完成 | Core TransformState、CustomLayer 参数和高层差异已记录 | +| 调查帧驱动 | 已完成 | Android requestRender、iOS DisplayLink、Core render callback 已记录 | +| 搜索粒子/场数据/Shader 实验 | 已完成 | 无运行时粒子实现;fixture 仅占位,内置 shader 无粒子 shader | +| 调查可复用基础设施 | 已完成 | FileSource、immutable、thread pool、gfx resource、upload/shader registry | +| 整理性能风险 | 已完成 | 逐项标记高/中/低/尚无法判断 | +| 绘制必要架构图 | 已完成 | 总体、JS/Native、Android、iOS、数据边界共五图 | +| 区分事实/限制/候选/未知 | 已完成 | 报告第 17 节分类 | +| 判断是否追加调查 | 已完成 | 选择 A,不追加子任务 | +| 判断能否进入 20_架构设计 | 已完成 | 可以进入,未知项作为设计输入 | +| 未修改生产代码 | 已完成 | 无源码改动 | +| 未升级或安装依赖 | 已完成 | 无 lock/Gradle/Pod/npm 改动 | +| 生成正式调查报告 | 已完成 | `docs/20_调查/10_架构调查报告_v1.md` | +| 生成 Codex 完成报告 | 已完成 | 本文件 | + +## 3. 未完成或无法验证 + +- “工作区干净”未完成:调查开始时存在任务书自身的未跟踪文件,非本次产生。 +- RN 页面、RN binding、NativeModule/TurboModule/Fabric 和 JS 每帧回调无法验证:当前仓库没有 RN 宿主或对应配置。 +- 目标产品最终 iOS backend、目标宿主线程调度、Android 业务侧 raw CustomLayerHost 的稳定用法、设备 GPU 能力和正式粒子数据格式无法仅凭当前静态源码确认。 +- 本次未重新构建或测试:任务性质为只读调查,构建可能改变产物;已有历史构建结果仅按基线记录引用。 + +这些项目已在调查报告中使用“未确认/无法验证”明确标记,没有用外部项目或记忆补全。 + +## 4. 实际生成/修改文档 + +- 新增 `docs/20_调查/10_架构调查报告_v1.md`。 +- 新增 `docs/报告/10_架构调查_Codex完成报告_v1.md`。 +- 追加 `粒子开发记录.md` 的本次调查记录。 +- 未新增或修改 Step 文件:遵循用户“当前不要自行创建 Step”的明确限制。 +- 未修改 `docs/00_项目总则/` 或 `docs/00_项目规范/`。 + +## 5. 生产代码与依赖 + +- 生产代码:未修改。 +- 实验源码:未修改。 +- Android/iOS/Gradle/Pod/Metro/Babel/TypeScript 配置:未修改。 +- 依赖:未安装、未升级、未修改 lock 文件。 + +## 6. 阻塞项和下一步 + +没有阻塞 `20_架构设计` 的关键未知项。建议下一步进入 `20_架构设计`,将本报告中的 CustomLayer/Core gfx 候选、Android/iOS 差异、离线 fixture contract、Land/Ocean Mask 边界和 context-loss 生命周期作为设计约束;在需要选择最终 GPU/线程/数据格式前,再安排最小平台运行时验证。 diff --git a/docs/报告/20_架构设计_Codex完成报告_v1.md b/docs/报告/20_架构设计_Codex完成报告_v1.md new file mode 100644 index 000000000..d3d41347f --- /dev/null +++ b/docs/报告/20_架构设计_Codex完成报告_v1.md @@ -0,0 +1,106 @@ +# 20_架构设计 Codex 完成报告 v1 + +日期:2026-08-29 + +任务:`docs/40_任务/20_架构设计指令书_v1.md` + +分支:`particle/main` + +起始提交:`127de2271480000fcc8f390d81cf446e98757800` + +结束提交:`127de2271480000fcc8f390d81cf446e98757800`(本任务未提交) + +执行结论:PASS(文档设计任务) + +下一阶段:B,进入 `30_最小技术验证` + +## 1. 前置文档与 Git 基线 + +| 项目 | 状态 | 结果 | +|---|---|---| +| 根 AGENTS/基线 | 已完成 | 已读取 `AGENTS.md`、`PARTICLE_BASELINE.md` | +| 项目总则 | 已完成 | 已完整读取 `docs/00_项目总则/` 全部当前有效文件 | +| 项目规范 | 已完成 | 已完整读取 `docs/00_项目规范/` 全部当前有效文件 | +| 前置调查/报告 | 已完成 | 已读取调查报告和 Codex 完成报告 | +| 当前架构/记录/Step | 已完成 | 已读取现有架构文档、开发记录和 STEP-000 | +| 当前设计任务书 | 已完成 | 1092 行已完整读取 | +| Git baseline | 已完成 | `particle/main`,HEAD 为上述起始提交;submodule 已查询 | +| 工作区干净 | 未完成 | 开始时已有上一阶段文档变更及用户提供的两份未跟踪任务书;均予以保留 | + +## 2. 设计必做项状态 + +| 必做项 | 状态 | 结果 | +|---|---|---| +| 候选渲染路线比较 | 已完成 | 比较 CustomLayer、CustomDrawable、独立 Core layer、Overlay | +| 唯一主路线选择 | 已完成 | 独立 Core RasterParticleLayer/RenderRasterParticleLayer + gfx | +| Core 模块设计 | 已完成 | Field、Adapter、Simulation、Render、GPU、Mask、Controller | +| 与 Style/RenderTree/gfx 关系 | 已完成 | 正式进入 Style/RenderTree,offscreen pass + translucent composite | +| WeatherColor/Particle 边界 | 已完成 | 分离 Layer/RenderLayer/GPU 状态/动画生命周期 | +| Data/Field contract | 已完成 | RasterArraySource、immutable snapshot、revision、canonical U/V | +| Fixture contract | 已完成 | metadata.json、field.f32、mask.u8、字节序、校验和、五个预留场景 | +| GPU resource 设计 | 已完成 | velocity、State A/B、Trail A/B、Mask、uniform、shader set | +| GPU state/simulation | 已完成 | 逻辑状态、render-pass ping-pong、NoData/mask/respawn;compute 延后 | +| Trail 架构 | 已完成 | 第一阶段包含 screen-space ping-pong trail,Camera 变化清空 | +| Land/Ocean Mask | 已完成 | 独立 snapshot/cache/texture,OCEAN_ONLY 共享,缺失 fail closed | +| 数据流与频率分类 | 已完成 | 低频、每帧、Camera 变化三类已定义 | +| 线程模型 | 已完成 | Map/UI、Worker、Render、GPU 的所有权和交接已定义 | +| Camera/Matrix 同步 | 已完成 | 只读取当前 render frame Paint/Transform parameters | +| 帧驱动 | 已完成 | MapLibre render loop + needsRepaint,禁用独立粒子时钟 | +| Android 薄接入 | 已完成 | typed JNI;禁止 raw long;OpenGL 主线、Vulkan gate | +| iOS 薄接入 | 已完成 | Core Source/Layer peer;CustomStyleLayer 只作参考;后续 Metal/OpenGL gate | +| 生命周期状态机 | 已完成 | DISABLED 至 ERROR/CONTEXT_LOST 的状态、恢复和释放责任已定义 | +| 公共 API/配置分层 | 已完成 | backend-independent source/layer/options/status;内部参数不外露 | +| 错误/降级 | 已完成 | Field、Mask、GPU、backend、context、shader、memory pressure 均定义 | +| 性能预算 | 已完成 | 给出待验证目标和测量档位,未声称性能已达到 | +| 最小技术验证清单 | 已完成 | 压缩为四项,并标记阻塞性和最小方法 | +| 不采用方案 | 已完成 | 逐项拒绝任务书要求的八类方案及 CPU/compute-only 等 | +| 九类架构图 | 已完成 | 总体、Core、fixture 数据流、每帧、Camera、Android、iOS、Mask、状态机 | +| 架构冻结项 | 已完成 | 14 项正式冻结;同时列出不得冻结项 | +| 下一阶段判断 | 已完成 | B:进入 `30_最小技术验证` | + +## 3. 核心架构选择 + +正式主路线是新增独立 Core `RasterParticleLayer`、`RenderRasterParticleLayer` 和 `mbgl::particle` 模块,通过 `RasterArraySource` 接受 immutable Field snapshot,进入 MapLibre Style/RenderTree,并使用 gfx Texture/RenderTarget/Drawable/Shader/UploadPass 完成 GPU simulation、trail 与 translucent composite。 + +CustomLayer/CustomDrawable 不构成产品双轨;Android raw `CustomLayerHost` 不进入正式 API。平台只提供类型安全 Source/Layer peer、backend lifecycle 与 capability 薄接入。 + +## 4. 实际生成或修改文件 + +- 新增 `docs/10_架构/20_粒子系统总体架构设计_v1.md`。 +- 新增 `docs/报告/20_架构设计_Codex完成报告_v1.md`。 +- 追加 `粒子开发记录.md`。 +- 未修改现有 `docs/10_架构/MapLibre粒子总体架构_v1.md`;其简略 Layer 顺序与本设计一致。 +- 未新增或修改 `docs/50_Step记录/`;符合任务书第 31 节。 + +## 5. 无法验证与未完成 + +### 无法验证 + +- 独立 particle RenderLayer 在 Android OpenGL/Vulkan 的运行时 pass/barrier/context 行为。 +- State/Trail RenderTarget 的实际格式、性能和 context recreation 时间。 +- Camera/wrap/Mask 在真机/模拟器上的视觉正确性。 +- iOS Metal/OpenGL 的粒子 Core layer 薄接入。 +- 最终 FPS、最大粒子数、最佳 texture format、设备能力和生产 Mask 来源。 + +这些均已列入 `30_最小技术验证` 或明确标记为后续 Gate,不影响本次总体设计文档 PASS。 + +### 未完成 + +- 无。本任务要求的设计输出均已完成;生产实现、构建和运行时验证本来就是本任务禁止项。 + +## 6. 代码、依赖、构建与测试 + +- 生产/实验源码:未修改。 +- Android/iOS/Gradle/Pod/CMake/package/lock:未修改。 +- 依赖:未安装、未升级。 +- 构建:未执行;任务明确禁止会修改工程状态的构建。 +- 测试:未执行;本阶段仅做静态架构设计。 +- APK/AAR/其他产物:无。 +- 项目总则/项目规范:未修改。 + +## 7. 与指令书差异、阻塞和建议 + +- 与指令书差异:无。 +- 与长期总则冲突:无。 +- 阻塞:没有阻塞本次设计完成的事项;存在四项会阻塞后续正式实现或平台扩展的运行时验证。 +- 建议:下一份正式指令书进入 `30_最小技术验证`,只覆盖设计文档第 26 节四项验证,不创建完整粒子产品实现,也不重新讨论总体架构。 diff --git a/docs/报告/30_01_AndroidTestApp版本标识_Codex完成报告_v1.md b/docs/报告/30_01_AndroidTestApp版本标识_Codex完成报告_v1.md new file mode 100644 index 000000000..78250afb0 --- /dev/null +++ b/docs/报告/30_01_AndroidTestApp版本标识_Codex完成报告_v1.md @@ -0,0 +1,62 @@ +# 30-01 Android Test App 版本标识 Codex 完成报告 v1 + +日期:2026-08-29 + +执行结论:`WAITING_MANUAL_VALIDATION` + +## 1. 目标 + +在 Android Test App 主菜单列表顶部显示当前运行 APK 的版本与构建身份,使人工验证时能够直接确认设备上安装的是哪个 APK。 + +## 2. 基线与范围 + +- 分支:`particle/main`。 +- 开始 HEAD:`c9c7a8aac6b72461b4d1b4dc6985da9d8fcc6146`。 +- 开始时工作区无修改。 +- 本轮只修改 Android Test App 菜单 UI,不修改 Core renderer、正式粒子实现、GPU、Mask、Vulkan 或 iOS。 +- 按 30-01 指令书不创建或修改 Step。 + +## 3. 实现 + +`FeatureOverviewActivity` 在主菜单顶部显示: + +```text +APK () · / · Git +``` + +数据全部来自 APK 构建产物自身: + +- Test App `BuildConfig.VERSION_NAME`; +- Test App `BuildConfig.VERSION_CODE`; +- Test App `BuildConfig.FLAVOR`; +- Test App `BuildConfig.BUILD_TYPE`; +- MapLibre Android `BuildConfig.GIT_REVISION` 的前 10 位。 + +版本信息没有写死;不同 renderer、build type 或 Git 提交构建出的 APK 会显示对应身份。 + +## 4. 修改文件 + +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/FeatureOverviewActivity.kt` +- `platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_feature_overview.xml` +- `platform/android/MapLibreAndroidTestApp/src/main/res/values/strings.xml` +- `粒子开发记录.md` +- 本完成报告。 + +## 5. 检查 + +- `git diff --check`:PASS。 +- 两个修改 XML 使用 Python 标准库只读解析:PASS。 +- Android/Gradle/Native 编译:未执行;用户明确要求 Codex 不编译。 +- 设备显示结果:等待用户重新手工构建、安装后确认。 + +## 6. Git + +- commit message:`testapp: show APK build identity in menu`。 +- 本次 commit hash:因报告与实现同一提交,为避免自引用,在 Codex 最终回复中记录。 +- push:提交后在 Codex 最终回复中记录。 +- 目标远端:`origin/particle/main`。 + +## 7. 未完成 + +- 尚无用户手工 Android 构建与设备截图,因此不能宣称运行显示 PASS。 +- 30-01 Core RenderLayer 的修复后实机验证仍为 `WAITING_MANUAL_VALIDATION`。 diff --git a/docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v1.md b/docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v1.md new file mode 100644 index 000000000..cfa7c033d --- /dev/null +++ b/docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v1.md @@ -0,0 +1,141 @@ +# 30-01 Core RenderLayer 接入验证 Codex 完成报告 v1 + +日期:2026-08-29 + +任务:`docs/40_任务/30_01_CoreRenderLayer接入验证指令书_v1.md` + +执行阶段:A(验证代码准备) + +当前结论:`WAITING_MANUAL_VALIDATION` + +## 1. 前置文档读取 + +已读取根 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目总则/` 全部当前有效文件、`docs/00_项目规范/` 全部当前有效文件、架构调查报告、总体架构设计、两份前置 Codex 报告、开发记录、当前 STEP-000 和本任务书。已按任务要求调查 Style 注册、RenderLayer/RenderTree/RenderOrchestrator、Background/Raster/Custom 示例、Android renderer/frontend、repaint 和 Transform/Paint 链。 + +## 2. Git 基线 + +- 分支:`particle/main`。 +- 任务开始 HEAD:`74be2a5052428717d7c9e133c333da8bd783b958`。 +- 开始时远端同步:`particle/main...origin/particle/main = 0 0`。 +- 开始时无已跟踪修改;有用户提供的未跟踪当前任务书。 +- submodule:已查询,无本任务修改。 +- 主要变更 commit:`d046be5e522de9f8a02c5133d0ac1c8ff12ce949`。 +- commit message:`test: add core particle render layer validation`。 +- push:成功;`d046be5e5` 已推送到 `origin/particle/main`,并通过远端引用查询确认可见。 +- 公开远端:`ssh://git@192.168.200.230:2222/tei/MapLibreParticleLab.git`。 +- 远端分支:`particle/main`。 + +## 3. 实际修改文件 + +### Core 验证代码 + +- 新增 `ParticleValidationLayer`、Impl/properties、factory、`RenderParticleValidationLayer`。 +- 修改 Core/default 与 Android LayerManager 注册。 +- 修改 `RenderLayer` 增加默认 false 的 `needsRepaint()`。 +- 修改 `RenderOrchestrator` 汇总 Layer repaint 请求。 +- 修改 CMake/Bazel source lists。 + +### Android Test App + +- 新增 `ParticleCoreLayerValidationActivity` 和 layout。 +- 注册 Manifest。 +- 新增页面标题、说明和 ACTIVE/STATIC 文案。 + +### 文档 + +- 纳入当前正式任务书。 +- 新增技术验证报告和本完成报告。 +- 追加 `粒子开发记录.md`。 +- 未创建或修改 Step。 + +## 4. Style Layer 接入位置 + +JSON type `particle-validation` 经 Android `LayerManagerAndroid` 的 core-only factory 正式解析为 `style::ParticleValidationLayer`。其 `LayerTypeInfo` 为 source-less、无 layout、无 tile kind。它不是 RasterLayer 分支,也不是 MapView 外部 Overlay。 + +## 5. RenderLayer 创建链 + +```text +Style JSON → LayerManagerAndroid → ParticleValidationLayerFactory +→ ParticleValidationLayer immutable Impl +→ RenderOrchestrator layer diff +→ ParticleValidationLayerFactory::createRenderLayer +→ RenderParticleValidationLayer +``` + +## 6. RenderTree / pass + +`RenderParticleValidationLayer` 进入 source-less `LayerRenderItem`,按 style index 创建 source-less LayerGroup,并注册一个无像素输出的 validation Drawable。Drawable 属于 `RenderPass::Translucent`,随正常 LayerGroup order 进入 main translucent pass。Test App 将它固定放在蓝色参考圆与黄色参考圆之间。 + +## 7. Transform 获取链 + +```text +UpdateParameters::TransformState +→ RenderTreeParameters::TransformParameters +→ 当前 frame PaintParameters +→ validation DrawableTweaker::execute +``` + +读取 viewport、zoom、bearing、pitch、projection matrix 摘要和 MapLibre frame count。没有独立 Camera 状态。 + +## 8. Repaint 链 + +ACTIVE 的 `RenderParticleValidationLayer::needsRepaint()` 返回 true,由 RenderOrchestrator 写入 RenderTree `needsRepaint`,再经 RendererObserver、Map::Impl::onDidFinishRenderingFrame/onUpdate 和 AndroidRendererFrontend.requestRender 请求下一 MapLibre frame。STATIC 返回 false。没有 Timer、Handler loop、JS/RN frame loop。 + +## 9. Android OpenGL 验证入口 + +Test App → `Custom Layer` 分类 → `Particle Core Layer Validation`。虽然位于现有 UI 分类中,但实现不是 CustomLayer/CustomLayerHost;分类只用于减少 Test App 导航改动。 + +## 10. 用户手工编译命令 + +```bash +cd /mnt/sda1/MapLibreParticleLab +source /root/.maplibre-particle-env +cd platform/android +RENDERER=opengl BUILDTYPE=Debug JOBS=3 make android-x86-64 +``` + +APK 预期路径:`platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`。 + +## 11. 用户手工运行步骤 + +详见技术验证报告第 9 节。核心操作是:打开验证页面;保存 `ParticleValidation` 日志;验证 pan/zoom/rotate/pitch/resize;ACTIVE 静置观察连续 frame;STATIC 静置观察停止;STATIC 下操作地图确认正常重绘。 + +## 12. 构建与检查记录 + +- Android/Gradle/npm/ADB:未执行,遵循任务书禁止项。 +- Linux OpenGL CMake configure:PASS。 +- Linux OpenGL 全量增量:在 `41/321` 无错误时主动停止,原因是公共头变化导致完整 Core 重编译;未宣称全量 PASS。 +- 本任务新增/直接修改的 5 个 Linux Core 对象精确编译:PASS。 +- C++ clang-format、`git diff --check`:PASS。 +- Android XML 标准库解析:PASS。 +- APK/AAR:本轮未生成新产物。 + +## 13. 当前状态 + +`WAITING_MANUAL_VALIDATION`。 + +用户尚未提供 Android OpenGL 编译、运行、手势、resize、ACTIVE/STATIC 和日志证据,不能写 PASS。 + +## 14. 是否修改生产代码 + +是,但只包含验证所需薄接入:新增隔离的 VALIDATION ONLY Core Layer/RenderLayer、注册/build list,以及通用默认 false repaint hook。没有粒子生产功能;没有改 RasterLayer、Map、MapView 主循环或 backend 算法。 + +## 15. 是否扩大架构范围 + +否。未实现 GPU particle、Field、RasterArraySource、Trail、Mask、Vulkan、iOS、RN/JS bridge 或真实数据。 + +## 16. 依赖状态 + +未安装、未升级、未修改任何依赖、lock、SDK、NDK、Gradle、Java、Node 或 MapLibre baseline。 + +## 17. 已知问题与未完成 + +- Android OpenGL 编译和运行待用户。 +- 五项地图变化和 ACTIVE/STATIC 行为待实际证据。 +- 全量 Linux OpenGL 增量未完成;任务相关对象已编译通过。 +- 验证 Drawable故意不输出像素;以 pass/order/transform/repaint 日志为技术证据,蓝/黄参考圆用于确认地图与 style layer context。 +- commit/push 元数据已在实质提交后回填一次;本次元数据收尾提交的最终远端 HEAD 由最终回复另行记录,不循环 amend。 + +## 18. 是否可以进入下一验证 + +否。只有用户证据回填并将本任务最终结论更新为 PASS 后,才能进入 GPU State A/B + Trail A/B 验证。 diff --git a/docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v2.md b/docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v2.md new file mode 100644 index 000000000..3653fe228 --- /dev/null +++ b/docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v2.md @@ -0,0 +1,73 @@ +# 30-01 Core RenderLayer 接入验证 Codex 完成报告 v2 + +日期:2026-08-29 + +任务:根据 Android 实机日志修复 `RenderParticleValidationLayer::update()` 的 DrawableBuilder 生命周期错误 + +执行阶段:B(实机失败后的最小修复轮次) + +当前结论:`WAITING_MANUAL_VALIDATION` + +## 1. 前置读取 + +已复核 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目总则/`、`docs/00_项目规范/`、当前 STEP-000、30-01 指令书、架构调查/设计及其完成报告、30-01 v1 技术验证/完成报告和 `粒子开发记录.md`。 + +已直接读取当前 `DrawableBuilder::getCurrentDrawable()`、`flush(context)`、`clearDrawables()`、`LayerGroup::addDrawable()` 以及 Background、Raster、Fill、CustomLayer 等现有 Drawable 使用方式。 + +## 2. Git 基线 + +- 分支:`particle/main`。 +- 本轮开始 HEAD:`98f1a85c2dafa4478866a6bae8ec6aed5cf2b29e`。 +- 开始时工作区无修改。 +- origin:`ssh://git@192.168.200.230:2222/tei/MapLibreParticleLab.git`。 +- 远端分支:`particle/main`。 +- 开始时本地与 `origin/particle/main` 同步。 +- submodule:已查询,无本轮修改。 +- 主要变更 commit:`322be849680f7ad0dde632edb1cc0e309f389cce`。 +- commit message:`fix: finalize particle validation drawable`。 +- push:成功;主要变更 commit 已推送到 `origin/particle/main` 并确认远端可见。 + +## 3. 根因 + +旧代码通过 `getCurrentDrawable(true)` 创建 `currentDrawable`,直接配置 custom/pass/Tweaker 后调用 `clearDrawables()`。后者只转移 completed `drawables`,不会完成 `currentDrawable`,因此返回空容器;随后 `drawables.front()` 产生未定义行为,最终在 RenderThread 的 LayerGroup 路径触发 SIGSEGV。 + +## 4. 修复内容 + +在 `clearDrawables()` 前增加一次 `builder->flush(context)`。`flush` 把当前 Drawable 移入 completed 容器,随后其有效所有权才交给 `LayerGroup`。 + +没有添加“为空就跳过”的防御分支。验证 Drawable、Tweaker、translucent pass、同帧 Transform 和 ACTIVE/STATIC repaint 验证链全部保留。 + +## 5. 实际修改文件 + +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v2.md` +- `docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v2.md` +- `粒子开发记录.md` + +未创建或修改 Step:30-01 指令书第 21 节明确要求本任务不主动修改 `docs/50_Step记录/`。 + +## 6. 构建与检查 + +- Linux OpenGL 目标对象 `render_particle_validation_layer.cpp.o`:PASS。 +- `clang-format --dry-run --Werror`:PASS。 +- `git diff --check`:PASS。 +- 首次目标对象命令受沙箱只读 `ccache` 缓存阻塞;`CCACHE_DISABLE=1` 后通过。 +- Android/Gradle/npm/ADB:未执行,遵循 30-01 指令书的用户手工 Android 验证边界。 + +## 7. 范围核对 + +- 正式粒子功能:未实现。 +- GPU ping-pong/Trail/Mask:未涉及。 +- Vulkan/iOS:未涉及。 +- RasterLayer、Renderer 主循环或平台 timer:未修改。 +- 依赖、SDK、NDK、Gradle、MapLibre baseline:未修改。 + +## 8. 未完成与阻塞 + +- 修复后的 Android OpenGL 手工编译待用户。 +- 修复后的 Android 实机运行、Tweaker render-pass 日志、Transform、ACTIVE/STATIC、pan/zoom/bearing/pitch/resize 待用户。 +- 因缺少修复后运行证据,30-01 仍不能标记 PASS,也不能进入后续 GPU/Mask/Vulkan/iOS 验证。 + +## 9. 下一步 + +用户在本次 push 后重新手工编译、安装并运行验证页面,回传 `ParticleValidation` 日志。只有全部运行条件满足后,再更新最终技术结论。 diff --git a/docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v3.md b/docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v3.md new file mode 100644 index 000000000..40e5e26c7 --- /dev/null +++ b/docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v3.md @@ -0,0 +1,67 @@ +# 30-01 Core RenderLayer 接入验证 Codex 完成报告 v3 + +日期:2026-08-29 + +任务:30-01 用户人工验收 PASS 状态回填 + +当前结论:`PASS` + +## 1. 回填背景 + +30-01 v1/v2 原状态均为 `WAITING_MANUAL_VALIDATION`,原因是当时尚未取得 Android OpenGL 运行证据。 + +用户随后已完成 Android OpenGL 构建与实际运行验收,并明确确认结果为 OK/PASS。本次只将该已确认事实回填到正式项目文档和 Git 历史。 + +## 2. 用户实际确认与结论 + +用户已确认: + +- Android OpenGL 构建成功; +- Particle Core Layer Validation 实际运行; +- Core RenderLayer 接入验证 OK; +- ACTIVE/STATIC repaint 达到验证目的; +- Transform、pan、zoom、bearing、pitch、resize 的 30-01 范围已满足。 + +据此,`30-01 = PASS`,并允许进入 30-02。 + +未伪造用户未提供的逐帧日志、性能值、截图或额外设备细节。 + +## 3. Git 基线 + +- 分支:`particle/main`。 +- 任务开始 HEAD:`25072cff1b3a538af361b572cd7e79cd4a471b07`。 +- 该 30-02 阻塞提交已保留,未删除、reset、rebase 或修改。 +- 开始时工作区无修改;本地与 `origin/particle/main` 同步。 +- 公开远端:`ssh://git@192.168.200.230:2222/tei/MapLibreParticleLab.git`。 +- 远端分支:`particle/main`。 +- commit message:`docs: finalize 30-01 validation as pass`。 +- 本次 commit hash 与 push 后远端 HEAD:因本报告与状态回填同一提交,在最终回复记录,避免自引用。 + +## 4. 实际修改文件 + +- 新增 `docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v3.md`; +- 新增本完成报告; +- 追加 `粒子开发记录.md`。 + +未修改 30-01 Core/gfx/RenderLayer/Test App/Shader 代码,也未修改历史 30-02 BLOCKED 文档。未创建或修改 Step。 + +## 5. 未执行事项 + +- 未重新编译; +- 未重新运行 Android; +- 未执行 ADB; +- 未重新测试 30-01; +- 未开始 30-02 GPU 实现; +- 未开始 30-03。 + +## 6. 30-02 解锁 + +30-02 v1 阻塞的唯一前置原因是当时仓库没有 30-01 PASS 正式记录。本报告与技术报告 v3 将该前置状态更新为 PASS,因此 30-02 现在可以在用户后续明确要求时重新执行。 + +30-02 v1 BLOCKED 历史报告保持不变;后续重新执行 30-02 时必须新建 v2 报告反映“前置阻塞已经解除”。 + +## 7. 最终结论 + +30-01 正式完成,状态为 `PASS`。 + +允许进入 30-02;本任务在 PASS 回填后停止,不自动继续执行 30-02。 diff --git a/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v1.md b/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v1.md new file mode 100644 index 000000000..6a5e11f94 --- /dev/null +++ b/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v1.md @@ -0,0 +1,95 @@ +# 30-02 GPU Ping-Pong 与 Context Recreation 验证 Codex 完成报告 v1 + +日期:2026-08-29 + +执行结论:`BLOCKED` + +## 1. 30-01 前置状态 + +当前仓库不存在 `30-01 = PASS` 的正式记录: + +- 最新 30-01 技术验证报告 v2:`WAITING_MANUAL_VALIDATION`; +- 最新 30-01 Codex 完成报告 v2:`WAITING_MANUAL_VALIDATION`; +- 开发记录仍要求用户完成修复后的 Android OpenGL 手工编译和实机验证。 + +30-01 PASS commit:不存在。 + +因此触发 30-02 指令书第 2 节停止条件:不得开始 GPU 代码,只生成阻塞说明。 + +## 2. Git 基线 + +- 分支:`particle/main`。 +- 任务开始 HEAD:`a1bcea9bbcbac18b884e647a92775f568d96d0df`。 +- 开始时本地与 `origin/particle/main`:`0 0`,同步。 +- 开始时无已跟踪修改。 +- 开始时存在用户提供的未跟踪 30-02 指令书。 +- submodule:已查询,无本任务修改。 +- origin:`ssh://git@192.168.200.230:2222/tei/MapLibreParticleLab.git`。 +- 目标分支:`particle/main`。 +- 本次 commit hash:因本报告与阻塞文档同一提交,在最终回复记录。 +- commit message:`docs: record blocked gpu validation prerequisite`。 +- push 结果与最终远端 HEAD:提交后在最终回复记录。 + +## 3. 前置文档读取 + +已读取 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目总则/`、`docs/00_项目规范/`、当前 STEP-000、30-02 指令书、架构调查/设计、前置完成报告、全部当前 30-01 技术/完成报告和 `粒子开发记录.md`。 + +## 4. 实际修改文件 + +- 纳入用户提供的 `docs/40_任务/30_02_GPU_PingPong与ContextRecreation验证指令书_v1.md`; +- 新增 `docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v1.md`; +- 新增本 Codex 完成报告; +- 追加 `粒子开发记录.md`。 + +未创建或修改 Step,符合 30-02 指令书第 28 节。 + +## 5. State A/B + +未实现、未验证。阻塞发生在允许开始 GPU 代码之前。 + +## 6. Trail A/B 与 RenderTarget + +未实现、未验证。没有创建 texture、RenderTarget、validation shader 或 composite pass。 + +## 7. Ping-pong、pass 顺序与 hazard + +未实现、未验证。没有形成 State/Trail swap,也没有得出 Android OpenGL 同步语义结论。 + +## 8. resize、disable/enable 与 generation + +未实现、未验证。没有增加 resource generation 或生命周期代码。 + +## 9. context/surface lifecycle + +未进入源码实现或设备验证阶段;没有声称普通 Activity lifecycle 等同于真实 context recreation。 + +## 10. Test App 与用户命令 + +30-02 Test App 入口未创建。当前唯一动作是先完成 30-01 手工验证;本轮不提供 30-02 编译/运行步骤,以免用户误以为 30-02 已具备可运行代码。 + +## 11. 构建、测试与产物 + +- 编译:未执行。 +- 测试:只执行文档/Git 静态检查。 +- Android/Gradle/ADB:未执行。 +- APK/AAR:无。 +- 用户此前要求 Codex 不编译,本轮同时遵守该要求。 + +## 12. 修改边界 + +- 通用 gfx:未修改。 +- 生产代码:未修改。 +- shader:未新增。 +- 依赖/SDK/NDK/Gradle/MapLibre baseline:未修改。 +- GPU ping-pong、Mask、Vulkan、iOS:未实现。 +- 架构范围:未扩大。 + +## 13. BLOCKED 依据与未验证项 + +阻塞依据是 30-02 指令书第 2 节的硬前置门槛,而不是 GPU/gfx 技术失败。State、Trail、同步、resize、disable/enable、context recreation 等全部仍为未验证。 + +解除阻塞需要先用最新 30-01 修复提交完成 Android OpenGL 实机验证,并把 30-01 正式更新为 PASS。 + +## 14. 是否允许进入 30-03 + +否。30-02 当前为 `BLOCKED`,不得进入 30-03。 diff --git a/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v2.md b/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v2.md new file mode 100644 index 000000000..e0f32d023 --- /dev/null +++ b/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v2.md @@ -0,0 +1,126 @@ +# 30-02 GPU Ping-Pong 与 Context Recreation 验证 Codex 完成报告 v2 + +日期:2026-08-29 + +执行结论:`WAITING_MANUAL_VALIDATION` + +## 1. 前置状态与 Git 基线 + +- 30-01 正式状态:`PASS`。 +- 30-01 PASS 回填 commit:`b34b0a3e22f91a0101b0bbd02190e3384f6ef2ae`。 +- 本任务开始分支:`particle/main`。 +- 本任务开始 HEAD:`b34b0a3e22f91a0101b0bbd02190e3384f6ef2ae`。 +- 开始时本地与 `origin/particle/main` 同步,工作区干净。 +- 历史阻塞 commit `25072cff1b3a538af361b572cd7e79cd4a471b07` 与 30-02 v1 BLOCKED 报告均完整保留。 + +## 2. 前置材料读取 + +已读取 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目总则/`、`docs/00_项目规范/`、当前 STEP-000、30-02 指令书、架构调查/设计、30-01 最新 PASS 技术报告与完成报告、30-02 v1 历史阻塞报告及 `粒子开发记录.md`。按指令书未创建或修改 Step。 + +## 3. 实际修改文件 + +Core validation 与 shader: + +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp/.hpp` +- `shaders/particle_gpu_validation.vertex.glsl` +- `shaders/particle_gpu_validation.fragment.glsl` +- `shaders/manifest.json` +- `include/mbgl/shaders/gl/particle_gpu_validation.hpp` +- `include/mbgl/shaders/particle_gpu_validation_ubo.hpp` +- `include/mbgl/shaders/gl/shader_info.hpp` +- `include/mbgl/shaders/shader_defines.hpp` +- `include/mbgl/shaders/shader_manifest.hpp` +- `include/mbgl/shaders/shader_source.hpp` +- `src/mbgl/shaders/gl/shader_info.cpp` +- `src/mbgl/shaders/shader_source.cpp` +- `src/mbgl/gl/renderer_backend.cpp` + +Android Test App: + +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` +- `platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_particle_core_layer_validation.xml` +- `platform/android/MapLibreAndroidTestApp/src/main/res/values/strings.xml` + +文档:本 v2 完成报告、对应 v2 技术报告,并追加 `粒子开发记录.md`。 + +## 4. State A/B 实现与创建链 + +`ParticleGpuValidationResources` 持有两套固定 `16x16`、RGBA unsigned-byte `RenderTarget`。创建链为 `gfx::Context::createRenderTarget` → 现有 `RenderTarget`/`OffscreenTexture` → 独立 texture;每个 target 各有一个 validation-only `LayerGroup`。State shader 从 read slot 采样并将确定性演进结果写到相反 slot,没有 CPU full-state copy。 + +## 5. Trail A/B 与 composite + +Trail A/B 是两套 viewport 尺寸的真实 `RenderTarget`。Trail pass 采样 previous trail,执行 fade 加移动 marker,写 next trail。主 RenderLayer 的 translucent Drawable 随后采样本帧 next trail并以预乘 alpha 合成到 MapLibre 主 pass;没有 MapView 外部 Overlay。 + +## 6. ping-pong swap、pass 顺序与 hazard + +- 正常帧只注册 State write 和 Trail write;对应 read target 不进入当帧 render-target list。 +- 初次创建/resize 时,新的 read target 以空 LayerGroup 先清屏。 +- `RenderOrchestrator` 按添加顺序访问 RenderTarget;本层添加顺序保证 State 在 Trail 前,主 LayerGroup composite 在所有 offscreen target 后。 +- Drawable Tweaker 在主 translucent pass 到达时提交 A/B slot 交换,因此下一帧才把本帧输出作为 read。 +- Tweaker 同时比较 source/target texture identity,异常时输出 `HAZARD`;正常节流日志输出 `distinct=true`。 +- 该结论目前是源码/调度顺序确认;Android OpenGL driver 的实际同步语义仍待用户实机日志与画面确认。 + +## 7. Android OpenGL backend 事实 + +新增 shader 只加入 OpenGL `RendererBackend::initShaders`、GL `ShaderInfo` 与生成的 GL source。没有添加 Vulkan/Metal/WebGPU shader,也没有把 raw OpenGL 调用写入 Core validation 逻辑。 + +## 8. resize、disable/enable、context lifecycle 与 generation + +- resize:仅销毁并重建 Trail A/B;State A/B 不重建,日志标记 `StatePreserved=true`。 +- disable:renderability 关闭时移除 target、清空验证 Drawable、重置 frame/slot;当前 Test App 的 DISABLE 通过移除 validation layer 强制完整 resource destroy。 +- enable:Test App 重新加入 validation layer,新 resource generation 从初始 slot/frame 创建。 +- context/surface:Android OpenGL 现有 surface lifecycle 会通知 renderer context lost;本 RenderLayer 的 `markContextDestroyed()` 记录旧 generation discard,RenderLayer 重建时进程内 atomic generation 递增。 +- generation 机制只服务验证日志,不是正式粒子资源管理 API。 + +## 9. Test App 入口 + +继续复用 `Custom Layer` → `Particle Core Layer Validation`,保留 30-01 ACTIVE/STATIC,并增加 DISABLE/ENABLE。主菜单既有 APK version/renderer/build type/Git revision 标识保持不变。没有创建第二套 Test App 或产品 UI。 + +## 10. 手工编译与运行 + +手工编译命令: + +```bash +cd /mnt/sda1/MapLibreParticleLab +source /root/.maplibre-particle-env +cd platform/android +RENDERER=opengl BUILDTYPE=Debug JOBS=3 make android-x86-64 +``` + +APK 预期路径:`platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`。 + +完整 T1–T7 手工运行步骤、带 `adb -s 192.168.1.146` 的安装/启动/logcat 示例见技术报告 v2 第 6–7 节。 + +## 11. 检查与未执行项 + +- shader manifest JSON:PASS。 +- shader 生成/注册链静态一致性:PASS。 +- C++ clang-format:PASS。 +- `git diff --check`:PASS。 +- Android XML:人工结构检查;环境无 XML parser,未宣称工具 PASS。 +- Android/Gradle/ADB/APK 构建安装:未执行。 +- 任何 C++/Android 编译:未执行,遵守用户“不要编译”。 +- 用户实机日志、画面、resize、context recreation:尚未提供。 + +## 12. 修改边界 + +- 通用 gfx API:未修改;现有 API 足以表达本验证。 +- 正式生产 Particle/Field/Mask:未实现。 +- 依赖、SDK、NDK、Gradle、MapLibre baseline:未修改。 +- Vulkan/iOS/Metal、30-03:未开始。 +- Shader/OpenGL backend 的修改仅是注册 validation-only shader,不是通用 renderer 重构。 + +## 13. 当前判定与 30-03 + +Stage A 代码已准备并推送,但所有关键 GPU/context 门槛仍缺用户实际运行证据,因此状态必须为 `WAITING_MANUAL_VALIDATION`,不是 PASS/FAIL。30-03 不允许开始;若用户环境最终无法用日志确认真实 context recreation,则 30-02 应判定 `BLOCKED`。 + +## 14. Git 收尾 + +- 主要变更 commit:`2f1506747b1dad8e89e526058a2951e9734ba99b`。 +- commit message:`test: add particle gpu ping-pong validation`。 +- 主要变更 push:成功。 +- 公开远端:`origin`(`ssh://git@192.168.200.230:2222/tei/MapLibreParticleLab.git`)。 +- 远端分支:`particle/main`。 +- 主要变更远端可见 HEAD:`2f1506747b1dad8e89e526058a2951e9734ba99b`。 +- 本文档与开发记录的收尾 commit 及最终远端 HEAD:按指令书的自引用处理规则,在最终回复中记录。 +- 未提交的无关本地改动:无。 diff --git a/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v3.md b/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v3.md new file mode 100644 index 000000000..0431f9a8b --- /dev/null +++ b/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v3.md @@ -0,0 +1,80 @@ +# 30-02 GPU Ping-Pong 与 Context Recreation 验证 Codex 完成报告 v3 + +日期:2026-08-30 + +执行结论:`WAITING_MANUAL_VALIDATION` + +## 1. 输入与判定 + +用户提供了明确 Android OpenGL 运行现象:STATIC 静止时停止,STATIC 地图拖动时 State/Trail 随 redraw 继续推进,停止拖动后又停止。原 v2 实现的 T3 因此为 `FAIL`。 + +本轮授权范围仅是修复 30-02 STATIC freeze;未重新执行其他测试,也未扩大到正式粒子、Mask、Vulkan、iOS 或 30-03。 + +## 2. Git 基线 + +- 分支:`particle/main`。 +- 本轮开始 HEAD:`ba488f87027b3a53e7eb031e26c263c39a26bdc0`。 +- 开始时本地与 `origin/particle/main` 同步,工作区干净。 +- 30-01 仍为正式 `PASS`。 +- 30-02 v1 BLOCKED、v2 Stage A 文档与全部历史提交均保留。 + +## 3. 原因分析 + +`needsRepaint()` 正确地在 STATIC 返回 false,但它只决定 validation layer 是否主动请求 MapLibre 下一帧。地图手势产生的合法 redraw 仍会运行 RenderLayer `update()`。 + +原 `update()` 无条件建立 State/Trail write pass、注册 RenderTarget、设置 pending;主 composite Tweaker又无条件尝试 commit。因此每个由地图手势产生的帧都会推进 GPU ping-pong。缺陷不在 MapLibre redraw,而在 validation GPU scheduling 没有把 ACTIVE 状态作为执行 State/Trail pass 的硬门槛。 + +## 4. 修改文件 + +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `src/mbgl/renderer/layers/render_particle_validation_layer.hpp` +- `docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v3.md` +- 本完成报告 +- `粒子开发记录.md` + +没有修改 Test App、shader、通用 gfx、RenderTarget、Renderer 主循环或 style property。 + +## 5. 修复实现 + +- 新增 validation-only `FrozenComposite` pass mode。 +- STATIC 有历史 Trail 时,主 pass 只采样当前已 commit 的 `trailRead`。 +- STATIC 不注册 State/Trail RenderTarget,不运行 State update,不运行 Trail fade/marker update。 +- STATIC 清除 pending;FrozenComposite Tweaker 在 `commit()` 前返回,不交换 slot、不增加 `committedFrame`。 +- STATIC 仍在正常 MapLibre translucent render pass 内重绘 composite,并取得当前 viewport/zoom/bearing/pitch;没有阻止 pan/zoom/bearing/pitch redraw。 +- STATIC resize 不重建 Trail,恢复 ACTIVE 后才由原 ACTIVE resize 路径处理。 +- ACTIVE 恢复后沿用冻结时的 State/Trail read slot,继续既有 ping-pong。 + +## 6. 静态检查与未执行项 + +- `clang-format --dry-run --Werror`:PASS。 +- `git diff --check`:PASS。 +- GPU scheduling/commit 控制流检查:PASS。 +- Android 编译、Gradle、ADB、安装、运行:未执行。 +- C++ 编译:未执行。 + +严格遵守用户“不运行 Android 编译、Gradle 或 ADB”以及此前“不要编译”的要求。 + +## 7. 重新验证要求 + +技术报告 v3 第 6 节给出完整 T3 重测步骤。关键证据是: + +- STATIC 日志显示固定 `frame=N`、固定 State/Trail read slot、`offscreenPasses=0 swap=false`; +- STATIC 地图手势正常,但 marker/fade、State/Trail pass、slot 与 simulation frame 不推进; +- ACTIVE 恢复后从 N 继续增长。 + +其他 T4–T7 项仍按 v2 执行。 + +## 8. Git 收尾 + +- STATIC 修复 commit:`364b8f381e27eee36c4895084b2f0578a023fa61`。 +- commit message:`fix: freeze particle gpu state in static mode`。 +- 修复 commit push:成功。 +- 公开远端:`origin`(`ssh://git@192.168.200.230:2222/tei/MapLibreParticleLab.git`)。 +- 远端分支:`particle/main`。 +- 修复 push 后远端可见 HEAD:`364b8f381e27eee36c4895084b2f0578a023fa61`。 +- v3 报告/开发记录收尾 commit 与最终远端 HEAD:按自引用规则在最终回复记录。 +- 无未提交的无关修改。 + +## 9. 当前状态与范围 + +原 T3 实机结果已如实记为 FAIL;修复后的 T3 尚未由用户重测。因此 30-02 总体状态继续为 `WAITING_MANUAL_VALIDATION`,不得写 PASS,不允许进入 30-03。 diff --git a/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v4.md b/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v4.md new file mode 100644 index 000000000..f262a82ed --- /dev/null +++ b/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v4.md @@ -0,0 +1,58 @@ +# 30-02 GPU Ping-Pong 与 Context Recreation 验证 Codex 完成报告 v4 + +日期:2026-08-30 + +执行结论:`WAITING_MANUAL_VALIDATION` + +## 1. 任务基线 + +- 分支:`particle/main`。 +- 本轮开始 HEAD:`625836fcaf1474c51210f51b95dc9adeafdead9d`。 +- 30-01:正式 `PASS`。 +- 30-02:仍为 `WAITING_MANUAL_VALIDATION`。 +- 开始时存在用户提供、未跟踪的本定向修复指令书;它保留在工作区,未修改或提交。 + +## 2. 用户实机输入 + +用户确认 C++ STATIC freeze 已通过手势回归,DISABLE / ENABLE 正常;同时发现旋转后 STATIC 恢复为 ACTIVE。该问题发生在 Android Activity recreation 的 Test App control mode,不是 GPU ping-pong 或 MapLibre redraw 问题。 + +## 3. 根因与修复 + +根因是 Activity 未向 Bundle 写入 `active/enabled`,且 recreation 后 `onCreate()` 无条件把 validation style 设为 ACTIVE + ENABLED,并重置相机。 + +修复仅在 `ParticleCoreLayerValidationActivity.kt`: + +- 以稳定 Bundle key 保存/恢复 active 与 enabled; +- 只在首次 Activity 创建时初始化 camera; +- callback 使用恢复成员值调用唯一 `loadValidationStyle()`; +- 增加一次恢复日志; +- 保留既有 MapView lifecycle/state 保存与四个按钮状态机。 + +没有修改 Manifest、C++、gfx、RenderLayer、shader、GPU resource lifecycle、Gradle、依赖或 Test App UI。没有引入任何绕过 Activity recreation 的机制。 + +## 4. 检查与未执行项 + +- `git diff --check`:PASS。 +- Bundle key、初始默认值、统一 style 入口、旧硬编码删除和禁止项静态检索:PASS。 +- Kotlin formatter 工具在环境中不可用;未声称 Kotlin lint PASS。 +- Android/Gradle/ADB/安装/运行以及任何编译:未执行,遵守用户限制。 + +## 5. 重新验证与范围 + +技术报告 v4 给出 R1 STATIC、R2 ACTIVE、R3 DISABLED rotation 和 R4 STATIC gesture 回归。必须通过 Android OpenGL 实机日志确认恢复状态;Rotation control-mode 恢复不等价于 T4 resize 或 T6/T7 context recreation PASS。 + +当前不允许进入 30-03。 + +## 6. Git 收尾 + +- 代码 commit:`503f1c66958798ef7294165c3ddeaffbd4908ee9`。 +- 代码 message:`fix: preserve particle validation mode on rotation`。 +- 代码 push:成功,远端分支 `origin/particle/main`。 +- 文档实质 commit:`7ff37de89840f3c2434b939cfe018662d18bfcf3`。 +- 文档 message:`docs: record particle rotation state retest`。 +- 本报告的元数据收尾提交及最终远端 HEAD 按自引用规则在最终回复中记录。 +- 除用户提供的未跟踪指令书外,无本任务无关本地修改。 + +## 7. 当前状态 + +本轮仅完成 Rotation state restoration 代码准备。R1–R4 尚待用户手工验证,故最终状态保持 `WAITING_MANUAL_VALIDATION`。 diff --git a/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v5.md b/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v5.md new file mode 100644 index 000000000..ad7aaefa1 --- /dev/null +++ b/docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v5.md @@ -0,0 +1,43 @@ +# 30-02 GPU Ping-Pong 与 Context Recreation 验证 Codex 完成报告 v5 + +日期:2026-08-30 + +执行结论:`PASS(EGL Context Recreation:Accepted Risk / 未验证;用户明确决定不阻塞)` + +## 1. 本次工作 + +本次只将用户已经完成的 Android OpenGL 实机结果回填为 30-02 的正式结论,并正式关闭 30-02。没有重新修改 30-02 代码,没有重新编译,没有运行 Gradle、Android、安装、ADB 或任何设备命令。 + +30-02 因此允许进入 30-03。 + +## 2. 已回填的实机结论 + +用户已确认 30-02 的已完成运行结果,包括 ACTIVE / STATIC、State/Trail GPU ping-pong、STATIC 手势下冻结 simulation、DISABLE / ENABLE,以及 rotation 后 validation control mode 恢复。此前 STATIC 拖动地图会推进 simulation 的 T3 FAIL 已由 v3 最小修复解决;地图仍正常 redraw,只 composite 当前冻结 Trail,不进行 State/Trail update、slot swap 或 frame progression。 + +这些是用户实际运行确认;Codex 的职责是如实记录结论和既有修改范围,不补造日志或数值。 + +## 3. Accepted Risk + +真实 EGL Context recreation 没有独立 Android OpenGL 实机证据,故它不是已验证 PASS 项。它被正式标记为 `Accepted Risk / 未验证项`。 + +用户已明确决定该风险不阻塞 30-02 关闭或 30-03 开始。该决定只接受此残余验证风险,不表示宣称 EGL context-loss 路径已经实机通过。 + +## 4. 历史与范围 + +- v1 `BLOCKED`、v2 验证准备、v3 STATIC freeze 修复和 v4 rotation 修复报告均保留原样; +- 本次新建 v5,不篡改历史状态; +- 没有新增或修改 Core、gfx、RenderLayer、shader、RenderTarget、Mask、Vulkan、iOS 或正式粒子实现; +- 未跟踪的用户任务指令书继续保留在工作区,未纳入本次提交。 + +## 5. Git 收尾 + +- 任务开始 HEAD:`fb78f82777a5b2e0393b064b249b100ba86e80fd`。 +- 文档实质提交:`89a3852f7a6cb476188a423228ff91214bab0bb3`。 +- 文档实质提交消息:`docs: close 30-02 with accepted context risk`。 +- 本报告元数据收尾提交、push 结果与最终远端 HEAD 按自引用规则在最终交付中如实记录。 + +## 6. 最终状态 + +30-02 正式关闭:`PASS`。 + +唯一保留事项为 EGL Context Recreation 的 `Accepted Risk / 未验证`;用户决定不阻塞。因此允许进入 30-03。 diff --git a/docs/报告/30_03_Camera_WorldWrap与LandOceanMask验证_Codex完成报告_v1.md b/docs/报告/30_03_Camera_WorldWrap与LandOceanMask验证_Codex完成报告_v1.md new file mode 100644 index 000000000..e34bf4be4 --- /dev/null +++ b/docs/报告/30_03_Camera_WorldWrap与LandOceanMask验证_Codex完成报告_v1.md @@ -0,0 +1,67 @@ +# 30-03 Camera / World Wrap 与 Land/Ocean Mask 验证 Codex 完成报告 v1 + +日期:2026-08-30 + +执行结论:`PASS` + +## 1. 最终结论 + +用户已完成 Android OpenGL 手工验收并确认 T1–T13 全部 `PASS`。30-03 因此正式完成,允许进入下一阶段。 + +`Rotation/Viewport 自动恢复问题` 已知;用户接受延期,并安排在下一个任务修复。本报告不将该问题描述为已修复,也不将其混同为本轮 Camera / World Wrap / Land-Ocean Mask 的失败。 + +## 2. Git 与前置 + +- 分支:`particle/main`。 +- 开始 HEAD:`3b5a147bbaa3e3d967ee89b16745a3ab8f62c72d`。 +- 公开远端:`origin`,目标分支 `origin/particle/main`。 +- 30-02 前置:v5 正式 `PASS`,允许进入 30-03;EGL context recreation 是用户接受且不阻塞的未验证风险。 +- 开始时有两份用户提供的未跟踪任务指令书(30-02 rotation、30-03);均未修改、未提交。 + +已读取 AGENTS、项目总则、项目规范、基线、STEP-000、调查/架构设计/前序报告、开发记录、30-01/30-02 实际 validation Core 与 Android Test App,以及 Transform/Paint/world-wrap/gfx texture 相关源码。 + +## 3. 已完成 + +- Camera:Composite Tweaker 使用当前 `PaintParameters::matrixForTile()`;没有 UI Camera、独立 timer 或 Overlay。 +- World wrap:通过当前 `UnwrappedTileID{wrap, CanonicalTileID{0,0,0}}` 与 `TransformState::matrixFor()` 绘制中心及相邻 world copy;仅中心 copy commit simulation。 +- Antimeridian:使用全球 canonical bounds 与两侧 C/D anchor,未引入局部 longitude clamp 补丁或第二套 wrap 状态机。 +- Mask:创建一次 validation-only deterministic 16×16 CPU fixture 和一个 shared `Texture2D`;GLOBAL、OCEAN_ONLY_A、OCEAN_ONLY_B 复用同一 texture。 +- 生命周期:Camera 和 consumer 切换不会重传 mask;通知 context destroyed 后下一个 ACTIVE update 放弃旧资源并创建新 mask generation。 +- Test App:在已有页面增加 mask consumer、DATELINE 和 WORLD WRAP 控制;A/B/GLOBAL property update 不 reload Style。 +- 日志:新增 fixture/create/generation/reuse/camera/world-wrap/antimeridian/anchor 字段,camera 摘要节流至每 30 frame。 +- Linux Core 精确对象编译、clang-format、diff check:PASS。 +- 用户 Android OpenGL 手工验收:T1–T13 全部 PASS。 + +## 4. 实际修改 + +- `render_particle_validation_layer.cpp`:当前 frame matrix、world-copy composite、shared mask lifecycle 与日志。 +- `particle_validation_layer.*`:validation-only `particle-validation-mask-mode`(0/1/2)。 +- validation shader/UBO/shader info:world z0 projection、mask sampling与 anchor。 +- `ParticleCoreLayerValidationActivity.kt`、布局、strings:最小人工验证控制与 Bundle 保存。 + +未修改通用 Transform、Projection、RenderTree、gfx API、正式 Field/Mask pipeline、Vulkan、iOS、依赖或项目总则;没有新增第三方依赖。 + +## 5. 已知延期项、未完成与无法验证 + +- 已知延期:Rotation/Viewport 自动恢复问题。用户已接受延期,下一任务修复;本轮没有修复或伪称修复该问题。 +- 未完成:正式 fixture adapter、正式 NoData fail-closed 与生产 coastline 数据。 +- 无法验证:真实 EGL context recreation 的独立 Android OpenGL 证据;该项仍沿用 30-02 的 `Accepted Risk / 未验证` 边界。 +- `particle-fixtures/coastal-mask/` 尚无 production fixture adapter,因此使用最小 deterministic validation generator;它不是生产 coastline pipeline,正式 `mask.u8`/NoData contract 仍未实现。 + +## 6. Android 手工步骤与验收 + +技术报告 v1 第 8 节给出了当前 Makefile 的准确 OpenGL x86_64 command、带 `adb -s 192.168.1.146` 的日志命令和 T1–T13。Codex 未执行这些命令;用户手工执行并确认 T1–T13 全部 PASS。 + +## 7. Git 收尾 + +- 主提交:`8b4c5c6d71bef383076e0ac035e9b8c047f5afc2`。 +- 主提交消息:`test: validate particle camera wrap and ocean mask`。 +- 30-03 最终状态回填 commit:`88d2d6eb328b3571da955bbcf171f8ef99d1f3ad`。 +- 最终状态回填提交消息:`docs: close 30-03 camera mask validation`。 +- push 结果:成功,`particle/main` 从 `f42896ffd` 推进至 `88d2d6eb3`。 +- 该最终状态提交推送后的远端 HEAD:`origin/particle/main = 88d2d6eb328b3571da955bbcf171f8ef99d1f3ad`;本报告的 Git 元数据收尾提交采用 Git 自引用规则,在最终交付中记录其 hash 与最终远端 HEAD。 +- 本任务只会 stage 30-03 validation 代码、v1 报告和开发记录;两份用户未跟踪指令书保留。 + +## 8. 下一步 + +30-03 为 `PASS`,允许进入下一阶段。下一任务优先修复已知的 Rotation/Viewport 自动恢复问题;不得将该延期项误记为本轮已修复。正式 fixture adapter、NoData fail-closed、生产 coastline 数据及跨平台验证仍按后续任务独立推进。 diff --git a/docs/报告/40-02A_Android粒子Validation测试界面紧凑化_Codex完成报告_v1.md b/docs/报告/40-02A_Android粒子Validation测试界面紧凑化_Codex完成报告_v1.md new file mode 100644 index 000000000..b4d0a74c3 --- /dev/null +++ b/docs/报告/40-02A_Android粒子Validation测试界面紧凑化_Codex完成报告_v1.md @@ -0,0 +1,41 @@ +# 40-02A Android 粒子 Validation 测试界面紧凑化 Codex 完成报告 v1 + +## 结论 + +代码阶段完成,当前状态为 `IMPLEMENTED / WAITING FOR ANDROID UI VALIDATION`。 + +- 实际开始 HEAD:`00b3510ddb0ee6439f3a2392dcbd391cd5f2c7e0`(`docs: record 40-02 implementation`)。 +- 实际完成实现 HEAD:`f4d519ca789c0488d6b9d8259427ac070b15eb63`(`ui: compact particle validation controls`);本报告作为紧随其后的文档收尾提交。 + +## 修改 + +- `ParticleCoreLayerValidationActivity.kt`:只将长状态说明替换为单行紧凑状态,显示 mask、运行状态、EAST/NORTH 或 T0/T1 与 mix;所有 action/callback 未改。 +- `activity_particle_core_layer_validation.xml`:底部控制区由 7 行收为 4 行。状态行 36dp,控制行 34dp,padding 从 12dp 降为 4dp;MapView 继续全屏作为底层主观察区。 +- `styles_particle_validation.xml`:集中定义 32dp minimum-height、11sp、零 padding 的紧凑按钮样式。 + +原布局约为 7 个默认 48dp 按钮行加 12dp padding,容易超过 350dp;新控制区约为 36dp + 4×34dp + 8dp padding,即约 180dp。横屏中额外空间全部回到 MapView/粒子区域。 + +## 保留功能 + +UNIFORM EAST、UNIFORM NORTH、T0 EAST/T1 NORTH、ADVANCE B→C、MIX 0/50/100、ACTIVE、STATIC、DISABLE、ENABLE、GLOBAL、OCEAN A、OCEAN B、DATELINE、WORLD WRAP 均保留原 XML ID 和 Kotlin click binding。 + +未修改任何粒子 Core、shader、GPU Field、timeMix、Simulation、Trail、Composite、Mask、Camera/transform、ABI 或 Gradle 文件。 + +## 检查与验证 + +- 16 个现有 button ID 与 Kotlin `R.id` binding 的静态逐项核对:通过。 +- `git diff --check`:通过。 +- Java 17 标准 XML parser:通过。 +- 按任务限制未运行 Android Gradle、APK、ADB、VM 或实机。 + +## 用户手工验收 + +```bash +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug --console=plain +adb -s 192.168.1.146 install -r platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk +adb -s 192.168.1.146 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity +``` + +横屏检查 UI-1 至 UI-5:地图明显扩大;控制区缩小且所有 16 项按钮均可见、可点、不重叠、不裁切;逐项点击后确认原 40-02 行为不变。 + +已知限制:未做 Android 设备布局测量,最终安全区、系统导航栏和实际文字裁切需由用户验收。确认前不能判 `CLOSED / PASS`。 diff --git a/docs/报告/40-02_Android双时间片GPU时间插值_Codex完成报告_v1.md b/docs/报告/40-02_Android双时间片GPU时间插值_Codex完成报告_v1.md new file mode 100644 index 000000000..b00915b1c --- /dev/null +++ b/docs/报告/40-02_Android双时间片GPU时间插值_Codex完成报告_v1.md @@ -0,0 +1,60 @@ +# 40-02 Android 双时间片 GPU 时间插值 Codex 完成报告 v1 + +## 结论 + +代码阶段完成,状态为 `IMPLEMENTED / WAITING FOR ANDROID VALIDATION`,不是 PASS。 + +- 实际开始 HEAD:`b0f855eb094c21eda9471088537a79f493f12dca`(`docs: record 40-01 closeout push`) +- 实际完成实现 HEAD:`4675a8a29dd6fc7b684bfb64913a8c9075ad8583`(`feat: add GPU particle time interpolation`);本报告作为紧随其后的文档收尾提交。 + +## 实现内容 + +1. 双时间片数据结构:`ParticleFieldTimeWindow` 包含 immutable T0/T1 `ParticleFieldSnapshot`。兼容性检查比较 grid width/height、bounds、NoData、fieldType 和 displayDomain;fixture identity/dataVersion 不作为兼容性条件,但仍作为 GPU slot identity。 +2. Source:`RasterArraySource` 保持原 `fixture/metadata/field`,并可接收 `next-fixture/next-metadata/next-field`。T0/T1 均经 FileSource byte boundary 与 adapter 校验;不兼容窗口报 source error。 +3. GPU T0/T1:同一 `ParticleGpuResources` 现在持有 `fieldTexture` 与 `nextFieldTexture`;未创建第二套 Layer、State、Trail、Mask 或 simulation。 +4. Shader:唯一源 `shaders/particle_gpu_validation.fragment.glsl` 采样两个 field texture,分别 decode 后执行 `mix(velocity0, velocity1, u_marker.x)`。`u_marker.x` 来自 `raster-particle-time-mix`,不是 CPU 中间 Field。 +5. 生命周期:初次 resource create 上传 T0、T1。仅 timeMix 更新时只写 UBO;Camera/transform 不走 Field source-change 分支。A/B -> B/C 如能匹配 B,则移动/复用其 GPU texture,只上传 C,且保留 simulation/trail;没有重叠的真实新窗口保留 40-01 的安全 reset/reseed 行为。 +6. Mask/ACTIVE/STATIC:共享 mask 未复制且 timeMix 不参与 mask upload。ACTIVE 仍是既有连续 simulation;STATIC 不因双 Field 请求连续 repaint。World wrap、antimeridian、rotation/viewport 与既有 State/Trail ping-pong 路径未改。 + +## 修改文件及目的 + +- `src/mbgl/particle/field/offline_fixture_particle_adapter.*`:time-window 兼容性模型。 +- `include/mbgl/style/sources/raster_array_source.hpp`、`src/mbgl/style/sources/raster_array_source*`、`src/mbgl/style/conversion/source.cpp`:T0/T1 source contract 与加载。 +- `include/mbgl/style/layers/raster_particle_layer.hpp`、`src/mbgl/style/layers/raster_particle_layer*`、`particle_render_layer_properties.hpp`:timeMix 常量 Layer property。 +- `render_raster_particle_layer.hpp`、`render_particle_validation_layer.cpp`:双 Field texture slot、复用/advance 与低频资源日志。 +- `shaders/particle_gpu_validation.fragment.glsl`、生成的 OpenGL header、shader defines/info:实际 GPU dual-field sampling。 +- Android validation Activity/layout/strings:T0/T1、mix、advance 手工入口。 +- `test/style/source.test.cpp`:compatible/incompatible time-window snapshot 定向测试。 + +## 日志与资源证明 + +`ParticleGPU` 日志记录 T0/T1 fixture/dataVersion、upload count、timeMix、窗口 advance、reuse、simulationRebuild 和 trailCleared。正常 timeMix 更新输出 `simulationRebuild=0 trailRebuild=0`;不在每帧输出。资源销毁沿用 `ParticleGpuResources` 既有析构路径。 + +## 静态/本地检查 + +- `node shaders/generate_shader_code.mjs`:通过;随后再次生成应无语义改动。 +- Linux OpenGL 精确 object compile:`raster_array_source.cpp`、`raster_particle_layer.cpp`、`render_particle_validation_layer.cpp`、`shader_info.cpp`、adapter 及 `test/style/source.test.cpp`:通过。 +- `clang-format --dry-run --Werror`:通过。 +- `git diff --check`:通过。 +- 未运行完整 test binary、Android Gradle、ADB 或设备;遵守任务限制。 + +## 用户手工 Android 验证 + +在仓库根目录执行: + +```bash +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug --console=plain +adb -s 192.168.1.146 install -r platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk +adb -s 192.168.1.146 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity +adb -s 192.168.1.146 logcat -v time -s ParticleValidation mbgl +``` + +APK 输出路径为 `platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`。先点 **T0 EAST / T1 NORTH**,再依次点 **MIX 0% / MIX 50% / MIX 100%**;随后在 ACTIVE 下反复调 mix、点 **ADVANCE B→C**,并回归 Camera、GLOBAL/OCEAN、STATIC、Rotation/Viewport、World Wrap 与 Antimeridian。 + +## T1--T10 验收状态 + +T1 east、T2 50% northeast、T3 north、T4 连续 mix 无 upload、T5 B/C 复用、T6 Camera 无 upload、T7 ACTIVE/STATIC、T8 mask、T9 Rotation/Viewport、T10 World Wrap/Antimeridian:均为**用户手工执行**,尚未验证。 + +## 未完成、风险与下一步 + +本轮不包含 Android 设备证据,因此不能确认实际 OpenGL sampler binding、视觉方向或 APK UI 资源。时间窗口 advance 的 GPU slot 复用已由 identity 实现与日志覆盖,但必须以 T5 Android 日志确认。若 T1--T10 无阻塞失败,建议进入 40-03 Android OpenGL 性能与资源压力基线;否则先按失败日志做最小修复。 diff --git a/docs/报告/40-02_双时间片GPU时间插值_最终验收关闭_Codex完成报告_v1.md b/docs/报告/40-02_双时间片GPU时间插值_最终验收关闭_Codex完成报告_v1.md new file mode 100644 index 000000000..86c7e189b --- /dev/null +++ b/docs/报告/40-02_双时间片GPU时间插值_最终验收关闭_Codex完成报告_v1.md @@ -0,0 +1,23 @@ +# 40-02 双时间片 GPU 时间插值最终验收关闭报告 + +## 最终状态 + +`40-02`、`40-02C`、`STEP-043B`:`CLOSED / PASS`。 + +最终代码基线为 `4703d87e2305c2f7c13d605c296e900d7a05e408`,STEP-043B 使用 `RasterParticleLayer::setTimeMix(float)` 完成 Android→Core property bridge。 + +用户已手工完成 Android x86_64 OpenGL Debug 编译、APK 安装和 App 启动;Codex 本收尾任务未运行 Android Gradle、ADB 或设备操作,未修改 runtime,也未加入/执行 Jest。 + +## T1--T10 + +- T1 EAST、T2 NORTH:PASS;`U=east-positive`、`V=north-positive` 且 shader Mercator 边界 `dy=-v` 正确。 +- T3 TIME_WINDOW:PASS;Layer/resource 保持,simulation 未 reset。 +- T4 MIX 50%、T5 MIX 100%、T6 MIX 0:PASS;均为 property-only,零 Style reload、Field upload、simulation reset。 +- T7 DISABLE→MIX:PASS;`MIX_REJECTED reason=DISABLED`,旧 timeMix 保持。 +- T8 DISABLE→ENABLE:PASS;资源重建符合 RESOURCE_CREATE 生命周期。 +- T9 ADVANCE B→C:PASS;`OLD_T1→NEW_T0` reuse,只上传 C,reset delta 为零。 +- T10 综合回归:PASS;Layer/resource identity 连续,未出现异常 simulation reset。 + +ADVANCE 后 `uniform-north + vortex` 的 MIX 50% 局部低速或近零速是逐点向量插值的正常结果:局部 north 与 vortex 向量可相反,不代表 GPU 卡死、reuse 污染或 reset。 + +APK x86_64 单 ABI 约 696.5 MiB;其中 `libmaplibre.so` 约 338 MiB。该 packaging/ZIP 中间产物体积技术债不影响安装、启动或本阶段功能验收,独立后续处理,不阻塞本次关闭。 diff --git a/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v1.md b/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v1.md new file mode 100644 index 000000000..700883689 --- /dev/null +++ b/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v1.md @@ -0,0 +1,48 @@ +# 40-03 Android OpenGL 粒子数量容量基线 Codex 完成报告 + +## 状态 + +`40-03:IMPLEMENTED / WAITING FOR ANDROID VALIDATION` + +40-02 的已关闭结论未改变。本报告只交付正式 GPU 链的容量测试入口;Android x86_64 实机矩阵尚无结果,不能写 `PASS` 或声称存在稳定容量区间。 + +## 已完成 + +1. 当前正式粒子数的最终控制点是 `RenderRasterParticleLayer` 的 State A/B render target。原固定 `16x16` 即 `256` 个 State shader texel;每个 texel保存并推进一粒子的 RG position、B age、A validity。 +2. 新增 `RasterParticleLayer::setParticleCount(uint32_t)` 与 `raster-particle-particle-count`。默认仍为 256,Core 接受 256 到 1,048,576 的平方数,实际生效 count 为 State grid 面积。 +3. Android 验证页增加 `256 / 1K / 4K / 16K / 64K / 1M`。请求经既有 JNI float-property 薄桥进入强类型 Core setter,成功后才更新 host count;没有 CPU simulation、第二 Layer 或 Java Layer peer。 +4. 切换 count 会释放并替换 State A/B,记录一次有理由的 `SIMULATION_RESET reason=PARTICLE_COUNT_CHANGE`,随后重新 seed State/Trail。它不会执行 Style reload、Field upload、MIX 更新或时间窗推进;Field T0/T1、SHA-256 reuse、interpolation、mask 和 40-02 生命周期语义保持。 +5. 新增低频资源诊断:`PARTICLE_COUNT_REQUEST/APPLIED/REJECTED`、`PARTICLE_RESOURCES_CREATE/REBUILD/RELEASE`、`STYLE_PROPERTY_SET`。它们含 requested/effective count、State grid、resource generation 或失败原因;原有采样 schedule/pass/commit 日志继续用于确认 simulation、trail 与 composite。 + +Trail 仍将四个确定性 State texel投射为可视 marker/trail。这是正式 State simulation 的低开销可视健康指示,不改变实际 State texel 并行数,也没有复制任何 simulation。 + +## 本地检查 + +- `clang-format`:受影响 C++ 文件已格式化。 +- Linux/OpenGL 精确编译:`raster_particle_layer.cpp.o`、`render_particle_validation_layer.cpp.o`、`style_layer.test.cpp.o` 均通过。 +- 新增 `Layer.RasterParticleCountProperties` 测试对象已成功编译,覆盖默认 256、合法 65,536、非平方与越界拒绝;完整测试二进制在当前 build tree 不存在,因此未将其运行误报为通过。 +- `git diff --check`:通过。 +- Android Gradle:未执行。ADB / APK 安装 / 设备测试:未执行。Jest:未加入、未执行。 + +## 未完成/等待用户验证 + +- 六个档位的持续运行、帧表现、内存趋势、GL/GPU 错误与 crash 结果。 +- 稳定运行区间、性能明显下降区间和当前压力边界。 +- Android Kotlin/JNI 的最终 Android toolchain 编译和 x86_64 OpenGL 实测。 + +## Windows 11 手工验证 + +在 PowerShell 或 CMD 中依次执行: + +```powershell +cd /mnt/sda1/MapLibreParticleLab/platform/android +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug -Pandroid.injected.build.abi=x86_64 --console=plain +adb -s 192.168.1.146 install -r .\MapLibreAndroidTestApp\build\intermediates\apk\opengl\debug\MapLibreAndroidTestApp-opengl-debug.apk +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity +adb -s 192.168.1.146 logcat -v time ParticleValidation:I ParticleGPU:I "*:S" +``` + +页面默认 256;依次点击 `1K`、`4K`、`16K`、`64K`、`1M`,每档至少观察持续推进、四个代表性 trail、交互、内存和日志。每次切换应见 `PARTICLE_COUNT_REQUEST`、JNI `STYLE_PROPERTY_SET result=SUCCESS`、`PARTICLE_RESOURCES_RELEASE`、`SIMULATION_RESET reason=PARTICLE_COUNT_CHANGE` 与 `PARTICLE_RESOURCES_REBUILD`;不应见 `FIELD_UPLOAD`、Style reload 或无理由 reset。若高档创建失败,应记录 `PARTICLE_COUNT_REJECTED` 而保持旧档稳定运行。 + +APK 路径:`./MapLibreAndroidTestApp/build/intermediates/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`。 diff --git a/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v2.md b/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v2.md new file mode 100644 index 000000000..d2ff004ed --- /dev/null +++ b/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v2.md @@ -0,0 +1,42 @@ +# 40-03 Android OpenGL 粒子数量容量基线 Codex 完成报告 v2 + +## 状态 + +`40-03:IMPLEMENTED / WAITING FOR ANDROID VALIDATION` + +v1 的 Android 容量验证暂停是正确的:它的 State A/B 已支持 N 个状态,但 Trail/render 只消费四个固定 State sample。本次补齐后尚无 Android 实机证据,不能写 `CLOSED / PASS`。 + +## 已完成 + +1. 原固定 sample 位于 `shaders/particle_gpu_validation.fragment.glsl` 的 Trail 分支,固定 State UV 为 0.125、0.375、0.625、0.875;原 Trail 实际是一个全屏 quad,6 个 index,并非 N 个粒子 draw。 +2. 正式 full draw 采用静态 State-index vertex buffer,而不是 GPU instance ID:每个 index 一次性映射到 `(x=index%width, y=index/width)`,数目等于 `effectiveParticleCount`。它在初始资源创建和 count rebuild 创建/释放,稳定帧不做 CPU 粒子循环。 +3. formal Trail pass 先用既有全屏 quad 衰减 previous Trail,随后以 `gfx::Points` 提交所有 static State index。vertex shader 从当前 State write texture 读取该 index 的 RG position;每个 point 进入同一 Trail A/B target,Composite 无改动地读取 Trail write。 +4. 256 时 `trailDrawCount=256`;1K 时 `trailDrawCount=1024`。1M 使用相同机制,按最多 65,536 points 的 16-bit-index segment 切分为多个 draw segment,理论路径为 1,048,576 State texel / point draw。 +5. 固定四 sample 仅保留给 legacy `ParticleValidationLayer` 诊断。正式 RasterParticle Layer 不再依赖它们;没有 GPU→CPU readback、每帧 CPU position 生成、旁路 overlay 或第二 simulation。 +6. State seed 保持每个 State texel 的 `v_uv`,无需改变 reseed;每粒子初始位置已是 deterministic grid-distributed。没有修改 U/V、`dy=-v`、Field sampling、T0/T1、MIX、JNI 或 40-02 resource identity 语义。 +7. `PARTICLE_DRAW_RESOURCES_CREATE|REBUILD|RELEASE` 与 `PARTICLE_DRAW_REJECTED` 日志新增/补齐 `effectiveParticleCount`、`stateGrid`、`trailDrawCount`、`particleIndexCount`、`fullParticleDraw=true` 和 generation。 + +## 本地检查 + +- shader generator 连续两次运行,生成物稳定;源 GLSL 是唯一修改入口。 +- Linux/OpenGL 精确编译通过:`render_particle_validation_layer.cpp.o`、`raster_particle_layer.cpp.o`、`style_layer.test.cpp.o`。 +- 相关 Layer test object 已编译;完整测试二进制当前 build tree 不存在,未误报运行通过。 +- `clang-format --dry-run --Werror`、`git diff --check`:通过。 +- 未运行 Android Gradle、ADB、APK、设备测试或 Jest;没有 APK packaging 改动。 + +## 待用户 Android 验证 + +先只执行 T-FULL-01~03:256、1K、256→1K→256。确认 `trailDrawCount` 等于 effective count、视觉密度变化、持续运动,以及没有 Style reload / Field upload / MIX 改变后,再扩展到 4K/16K/64K/1M。 + +PowerShell 或 CMD: + +```powershell +cd /mnt/sda1/MapLibreParticleLab/platform/android +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug -Pandroid.injected.build.abi=x86_64 --console=plain +adb -s 192.168.1.146 install -r .\MapLibreAndroidTestApp\build\intermediates\apk\opengl\debug\MapLibreAndroidTestApp-opengl-debug.apk +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity +adb -s 192.168.1.146 logcat -v time | Select-String -Pattern 'ParticleGPU|PARTICLE_COUNT|STATE|DRAW|trailDrawCount|effectiveParticleCount|stateGrid|fullParticleDraw|STYLE|FIELD|MIX' +``` + +APK:`./MapLibreAndroidTestApp/build/intermediates/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`。 diff --git a/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v3.md b/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v3.md new file mode 100644 index 000000000..ed2d6b769 --- /dev/null +++ b/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v3.md @@ -0,0 +1,46 @@ +# 40-03 Android OpenGL 粒子数量容量基线 Codex 完成报告 v3 + +## 最终状态 + +`40-03:CLOSED / PASS` + +原目标是在正式 `RasterArraySource -> RasterParticleLayer -> GPU simulation -> Trail -> Composite` 链中建立有真实绘制意义的 Android OpenGL 粒子容量基线。 + +## 从 State-only 到 Full Particle Draw + +首次容量实现提交 `c0d1ad43c49ed6d526c3fd8b982c04840326d0d0` 仅将 State A/B 扩展为 256 至 1M。首次 Android 验证发现 Trail/render 只消费少量固定 State sample;所以它不代表 N 个模拟粒子同时被绘制,容量验收暂停。 + +Full Particle Draw 提交 `a0aae48ec8cb8a379909b88df1aeef45663f2985` 修正此缺口:每个 particle index 一一映射 State texel,静态 draw resources 仅在创建/count rebuild 时生成,vertex shader 直接从当前 State write texture 读取位置。正式 Trail 在同一 Trail A/B write target 先 fade、再 draw N 个 point,Composite 沿用既有 Trail write 输入。因此最终契约为:`State N -> draw N -> Trail -> Composite`。 + +没有每 frame CPU particle loop、GPU→CPU readback、CPU position upload、旁路 overlay 或第二 simulation。 + +## 用户 Android 实机验收 + +实机日志均包含 `fullParticleDraw=true`: + +| 档位 | State Grid | effectiveParticleCount | trailDrawCount | 结果 | +|---|---:|---:|---:|---| +| 256 | 16×16 | 256 | 256 | PASS | +| 1K | 32×32 | 1024 | 1024 | PASS | +| 4K | 64×64 | 4096 | 4096 | PASS | +| 16K | 128×128 | 16384 | 16384 | PASS | +| 64K | 256×256 | 65536 | 65536 | PASS | +| 1M | 1024×1024 | 1048576 | 1048576 | PASS;用户确认运行 OK | + +T-FULL-01(256→draw 256)、T-FULL-02(1K→draw 1024)和 T-FULL-03(256→1K→256)全部 PASS。T-FULL-03 中 State grid/draw count 变更、full draw resource 释放重建和 reset/reseed 均正确;无 stale resource、crash、由 count change 引起的 Field upload 或 Style reload,MIX 未改变。 + +40-02 的既有语义保持:MIX 仍为 property-only,不触发 Field upload 或 simulation reset;时间窗 Field reuse 仍记录 `simulationRebuild=false`、`trailCleared=false`。 + +## 容量结论与边界 + +Android OpenGL full particle draw 已通过,已实际验证 1,048,576 个粒子可正常运行,已验证容量 **≥ 1M**。项目当前没有继续压测 2M/4M/8M 的实际需求;40-03 不是理论 GPU 极限研究,故在满足需求后关闭。 + +1M 仅是本次验证环境的已验证容量,不是产品硬编码默认值、全部 Android 设备保证值或理论最大容量。 + +## 本收尾边界 + +- Android 设备验证由用户完成。 +- Codex 本收尾未执行 Android Gradle、ADB、APK 安装或设备操作。 +- 未修改功能代码、未加入/执行 Jest,未夹带 APK packaging 或其他阶段工作。 + +文档自检 `git diff --check` 通过后,本报告与 STEP-044 v3、开发记录一同提交。 diff --git a/docs/报告/40-04_Runtime_Field动态更新接口_Codex完成报告_v1.md b/docs/报告/40-04_Runtime_Field动态更新接口_Codex完成报告_v1.md new file mode 100644 index 000000000..705bfe231 --- /dev/null +++ b/docs/报告/40-04_Runtime_Field动态更新接口_Codex完成报告_v1.md @@ -0,0 +1,54 @@ +# 40-04 Runtime Field 动态更新接口 Codex 完成报告 + +## 状态 + +`40-04:IMPLEMENTED / WAITING FOR ANDROID VALIDATION` + +40-03 的 `CLOSED / PASS` 不变。本轮只交付运行中 Field 窗口推进接口;没有执行 Android 构建或实机验收。 + +## 完成结果 + +1. 正式 Runtime Field API 位于 `RasterArraySource`:`updateRuntimeFieldWindow(particle::ParticleFieldSnapshot newT1)`;验证辅助入口为 `advanceRuntimeFieldWindow()`。 +2. 参数是新的 immutable `ParticleFieldSnapshot`;它包含 fixture/content identity、dataVersion、runtime generation、grid、bounds、NoData、velocity 和 validity。 +3. 权威 T0/T1 是 `RasterArraySource::Impl::field/nextField`。 +4. Android/Kotlin 不持有 T0/T1;仅触发 source id 的 advance action,状态行也不复述具体 T0/T1。 +5. JNI 是薄桥:`MapLibreMap -> NativeMapView -> JNI -> Style::getSource() -> RasterArraySource`,不保存窗口、不做 Field 业务计算、不碰 GPU。 +6. A/B→B/C 时 Source 输出 old T1(B) 为 new T0;renderer 的 SHA-256/content identity 命中旧 T1,复用其 texture。 +7. B/C→C/D 同理,旧 T1(C) 成为 new T0。 +8. 正常推进只上传一个新 Field;旧 T1 的 GPU resource 被复用。 +9. 正常推进不 Layer recreate。 +10. 正常推进不 Style reload。 +11. 重叠窗口不 reset simulation,renderer 日志为 `simulationRebuild=false`。 +12. 重叠窗口不 clear trail,renderer 日志为 `trailCleared=false`。 +13. MIX 保持既有 property-only Core setter/UBO 路径,与 Runtime Field update 解耦。 +14. 重复当前 T1 内容为 `NOOP`;重复队列推进耗尽为明确 `NO_PRELOADED_FIELD` reject,均不产生 upload/reset。 +15. 无效、未加载、不兼容或 non-monotonic Snapshot fail closed,记录 `FIELD_WINDOW_UPDATE_REJECTED reason=...`,不替换当前窗口。 +16. 不存在每 frame Field copy/hash/upload;候选仅在 source asset 加载后缓存,runtime generation 仅在候选真正进入 T1 时分配。 +17. 未修改 full particle draw;仍是 40-03 已验收的 `State N -> draw N -> Trail -> Composite`。 +18. 本地检查:受影响 Linux/OpenGL Core、conversion 与 `source.test.cpp.o` 精确 `-Werror` 编译;新增定向 Source test 对象覆盖窗口两次推进、NOOP、invalid fail-closed。当前 build tree 缺少大量无关测试依赖,完整 `mbgl-test` 会触发全量重建,故未将其二进制执行误报为通过;`clang-format --dry-run --Werror` 与 `git diff --check` 通过。 +19. 等待 Android 手工验证:A/B、B/C、C/D 的 reuse/upload/no-reset/no-clear、C/D MIX、候选耗尽拒绝,以及 Android toolchain 编译。 +20. 实现 commit hash:`45502a53f4fc486bd24cb0008d1ab0ff221e7e63`。 +21. commit message:`feat: add runtime particle field updates`。 +22. push 结果:待文档收尾 commit 后推送至 `origin/particle/main`。 +23. `origin/particle/main` 最终 HEAD:待 push 后在交付记录中确认。 + +## Android 验证入口与日志 + +`TIME WINDOW` 加载 A=`uniform-east` / B=`uniform-north` 并由 Source 预加载 C=`vortex`、D=`nodata`。每次 `ADVANCE` 不调用 `setStyle()`:Core 记录 `FIELD_WINDOW_UPDATE_REQUEST` 和 `FIELD_WINDOW_UPDATE_APPLIED`,renderer 记录 `FIELD_REUSE from=OLD_T1 to=NEW_T0`、新内容的 `FIELD_UPLOAD reason=NEW_CONTENT` 以及 `simulationRebuild=false trailCleared=false`。第三次 advance 应只见 reject,不影响当前 C/D。 + +## Windows 11 手工验证 + +```powershell +cd /mnt/sda1/MapLibreParticleLab/platform/android +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug -Pandroid.injected.build.abi=x86_64 --console=plain +adb -s 192.168.1.146 install -r .\MapLibreAndroidTestApp\build\intermediates\apk\opengl\debug\MapLibreAndroidTestApp-opengl-debug.apk +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity +adb -s 192.168.1.146 logcat -v time ParticleValidation:I ParticleField:I ParticleGPU:I "*:S" +``` + +依次点击 `TIME WINDOW`、`MIX 0/50/100`、`ADVANCE`、`ADVANCE`,然后在 C/D 再点击 MIX 0/50/100。记录两次 `FIELD_REUSE from=OLD_T1 to=NEW_T0`、每次恰一条新内容 upload、无 Style reload/layer recreate/reset/trail clear;第三次 `ADVANCE` 应为 `NO_PRELOADED_FIELD` 拒绝且 C/D 继续运行。 + +APK 路径:`./MapLibreAndroidTestApp/build/intermediates/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`。 + +Android Gradle、ADB、实体机和 Jest 均未由 Codex 执行。 diff --git a/docs/报告/40-04_Runtime_Field动态更新接口_Codex完成报告_v2.md b/docs/报告/40-04_Runtime_Field动态更新接口_Codex完成报告_v2.md new file mode 100644 index 000000000..bf290e86f --- /dev/null +++ b/docs/报告/40-04_Runtime_Field动态更新接口_Codex完成报告_v2.md @@ -0,0 +1,52 @@ +# 40-04 Runtime Field 动态更新接口 Codex 最终完成报告 + +## 状态 + +`40-04:CLOSED / PASS` + +Android OpenGL 最终统一验证日期:2026-09-03。最终功能基线为 `4e31163516ad77473df672e162f554223cd2f09e`。 + +## 最终验收结论 + +- T-RUNTIME-01:PASS +- T-RUNTIME-02:PASS +- T-RUNTIME-03:PASS +- T-RUNTIME-04:PASS +- T-RUNTIME-05:PASS +- T-RUNTIME-06:PASS + +| 问题 | 结论 | +|---|---| +| A/B→B/C 是否通过 | 是 | +| B/C→C/D 是否通过 | 是 | +| OLD_T1→NEW_T0 是否连续复用 | 是 | +| 正常推进是否只上传新 T1 | 是 | +| 正常推进是否 reset simulation / clear trail | 否 / 否 | +| 正常推进是否 Style reload / Layer recreate | 否 / 否 | +| MIX 是否 property-only | 是 | +| Duplicate 是否返回 NOOP、重复 upload 或 reset | 是 / 否 / 否 | +| Fail-closed 是否通过且 reject 后窗口可用 | 是 | +| Android 最终验证是否通过 | 是 | + +## 实机证据 + +初始窗口为 uniform-east/uniform-north。MIX 0/50/100 仅记录 `TIME_MIX_UPDATE`,不增加 Field upload/reset。两次运行时推进依次得到 north/vortex、vortex/nodata;均记录 `FIELD_WINDOW_UPDATE_REQUEST`、`FIELD_WINDOW_UPDATE_APPLIED result=APPLIED`、`FIELD_REUSE from=OLD_T1 to=NEW_T0` 与单个 `FIELD_UPLOAD reason=NEW_CONTENT`,并确认 `simulationRebuild=false`、`trailCleared=false`。 + +在 vortex/nodata,MIX 50/100/0 后 `fieldUploadCount=5`、`resetCount=0` 保持。连续 Duplicate 记录 `FIELD_WINDOW_UPDATE_DUPLICATE_REQUEST`、`FIELD_WINDOW_UPDATE_APPLIED result=NOOP`、T0=vortex/T1=nodata 和 duplicate bridge `result=NOOP`,没有 `FIELD_UPLOAD`、`SIMULATION_RESET` 或 `STYLE_LOAD`。候选耗尽返回 `NO_PRELOADED_FIELD`;之后 MIX 50 仍维持 C/D 与相同计数,证明 fail-closed 不污染当前窗口。 + +用户主动建立时间窗的 `STYLE_LOAD reason=TIME_WINDOW` 是允许初始化;后续所有 Runtime 操作的 `styleEpoch=2` 未变化。MIX 的 `LAYER_CHANGED` 是 property change 通知,非 Layer recreate;`renderLayerInstanceId=1` 稳定。 + +## 接口与责任边界 + +`RasterArraySource::updateRuntimeFieldWindow(...)` 是正式 Runtime API;`repeatRuntimeFieldWindow()` 仅为 Duplicate Validation 重提当前 T1 并复用同一 API 的 NOOP 分支。Source 持有正式 T0/T1;JNI 为薄桥;Android Validation 触发动作;Renderer 负责 GPU reuse/upload;RasterParticleLayer 持续 simulation/render。Kotlin 不持有 T0/T1 权威状态。 + +40-04 未改变 U/V、`dy=-v`、GPU interpolation/MIX property-only 或 40-03 full particle draw 和 capacity ≥ 1,048,576。真实天气网络、GRIB、provider、NavSea/React Native、粒子视觉阶段、WeatherColorLayer、Vulkan、iOS/Metal、生产 mask 与 APK 技术债均未实现。 + +## 收尾检查与 Git + +- 本收尾修改功能代码:否。 +- 新的 Android Gradle / ADB / Jest:均未运行。 +- `git diff --check`:通过。 +- 收尾 commit:待本次文档提交后记录于 Git 交付。 +- 建议 message:`docs: close runtime particle field validation`。 +- remote branch:`origin/particle/main`。 diff --git a/docs/报告/40-04_T-RUNTIME-05_Duplicate_NOOP_Codex完成报告_v1.md b/docs/报告/40-04_T-RUNTIME-05_Duplicate_NOOP_Codex完成报告_v1.md new file mode 100644 index 000000000..581a319f3 --- /dev/null +++ b/docs/报告/40-04_T-RUNTIME-05_Duplicate_NOOP_Codex完成报告_v1.md @@ -0,0 +1,36 @@ +# 40-04 T-RUNTIME-05 Duplicate NOOP 补验收 Codex 完成报告 + +## 状态 + +`40-04:IMPLEMENTED / WAITING FOR ANDROID VALIDATION` + +用户已手工通过 T-RUNTIME-01~04、T-RUNTIME-06;本轮只补 T-RUNTIME-05 的 Android 验证入口,未执行 Android 构建或设备操作。 + +## 实现 + +- 新增 `RasterArraySource::repeatRuntimeFieldWindow()`。它读取 Source 权威 `Impl::nextField` 并调用既有正式 `updateRuntimeFieldWindow()`,不直接操作 renderer 或 GPU。 +- 当当前窗口为 `T0=vortex / T1=nodata`,这次重提交命中 `sameFieldGpuContent(currentT1, newT1)`,返回 `Noop`;不调用 `onSourceChanged()`,所以没有 Field upload、slot swap、simulation reset、trail clear、Style reload 或 Layer/Source recreate。 +- Android 增加 `DUPLICATE T1`。调用链为 `MapLibreMap -> NativeMapView -> JNI -> Style::getSource() -> RasterArraySource`;Kotlin/JNI 不持有 T0/T1,也不消费预加载候选。 +- JNI 只在返回 `Noop` 时报告 duplicate bridge 成功。日志应出现 `FIELD_WINDOW_UPDATE_DUPLICATE_REQUEST`、Core 的 `FIELD_WINDOW_UPDATE_APPLIED result=NOOP` 与 bridge `result=NOOP`。 + +## 本地检查 + +- Linux/OpenGL:`raster_array_source.cpp.o`、`source.test.cpp.o` 精确 `-Werror` 编译通过。 +- Source test 补充 C/D 后 duplicate assertion:结果 `Noop`、T0/T1 保持 vortex/nodata、Source changed count 不增加。 +- `clang-format --dry-run --Werror`、`git diff --check` 通过。 +- Android Gradle、ADB、APK、实体机和 Jest:均未执行。 + +## 用户手工验收 + +在已通过 A/B→B/C→C/D 后,保持 `T0=vortex / T1=nodata`,点击 `DUPLICATE T1` 一次。必须确认: + +```text +FIELD_WINDOW_UPDATE_APPLIED result=NOOP +FIELD_UPLOAD delta=0 +SIMULATION_RESET delta=0 +trail clear delta=0 +Style reload delta=0 +当前 T0/T1、MIX、frame 持续不变/继续运行 +``` + +随后仍可点击普通 `ADVANCE WINDOW`,它应继续保留既有 T-RUNTIME-06 的 `NO_PRELOADED_FIELD` fail-closed 语义。 diff --git a/docs/报告/40-05_Android_OpenGL正式粒子视觉与运行时渲染控制_Codex完成报告_v1.md b/docs/报告/40-05_Android_OpenGL正式粒子视觉与运行时渲染控制_Codex完成报告_v1.md new file mode 100644 index 000000000..fc94a1ef2 --- /dev/null +++ b/docs/报告/40-05_Android_OpenGL正式粒子视觉与运行时渲染控制_Codex完成报告_v1.md @@ -0,0 +1,75 @@ +# 40-05 Android OpenGL 正式粒子视觉与运行时渲染控制 — Codex 完成报告 + +日期:2026-09-03 +状态:CLOSED / PASS + +## 结果 + +40-05 / STEP-046 已完成代码、Android 验收入口与用户手工 Android OpenGL 最终验证,正式关闭为 `CLOSED / PASS`。 + +实施提交:`e48fa087e01e9710c5d0c18ab062726226bbb6bd`(`feat: add particle visual runtime controls`)。Android APK 编译、copy、安装、实体机/虚拟机操作及完整 logcat 抓取均由用户完成;本次文档收尾未重新执行 Android Gradle、ADB 或 Jest。 + +### A. 正式方向 geometry + +`RenderRasterParticleLayer` 的正式 full draw 不再画固定蓝色 point。每个 State texel 创建两个静态 line vertices;vertex shader 读取当前 State write、T0/T1 Field,并以 `timeMix` 取得最终 GPU velocity。方向由 `vec2(u, -v)` 在 Mercator 边界生成,所以继续遵守 `U=east-positive`、`V=north-positive`、`dy=-v`。没有 CPU 每粒子 direction/color、GPU readback、第二 simulation 或 Overlay。 + +### B/C. 视觉属性与速度颜色 + +Core `RasterParticleLayer` 具有 line width、streak length、particle/trail opacity、trail fade、speed-color enabled 的强类型 setter 和 style property 支持。速度色使用 shader 内蓝→绿→橙最小 ramp,输入是最终插值后的 magnitude;NoData/invalid velocity 在 vertex shader 直接不绘制。Legacy `ParticleValidationLayer` 的旧验证显示没有被改为正式视觉路径。 + +### D. Android runtime 控制 + +Android Test App 通过既有 `setStyleLayerFloatProperty()` boolean JNI bridge 调用 Core 强类型 setter,不建立 Java RasterParticleLayer peer。请求失败时 Kotlin 不写入本地 visual state;成功后才更新状态栏。新增控制:VISUAL DEFAULT、SIZE ±、LENGTH ±、OPACITY ±、TRAIL ±、FADE ±、COLOR,以及直接 VORTEX 与 256K capacity 入口。 + +### E. 观测和回归 + +资源日志现在明确区分 `particleDrawCount`(粒子数)与 `streakVertexCount`(两端点数)。visual property 变化输出一次结构化摘要,含 Field upload/reset counters 与 resource identity;没有每粒子或每帧日志,也没有虚构 GPU timer。 + +## 保持不变 + +- 40-02 T0/T1 GPU interpolation 和 MIX property-only。 +- 40-03 `N State texels -> N actual particle draw` 与已验证 capacity `>=1,048,576`。 +- 40-04 Runtime Field A/B→B/C→C/D、OLD_T1→NEW_T0 reuse、Duplicate NOOP、候选耗尽 fail-closed。 +- State/Trail ping-pong、STATIC frozen composite、mask reuse、camera/world wrap/antimeridian 和 rotation lifecycle。 + +视觉参数改变预期 `FIELD_UPLOAD delta=0`、`SIMULATION_RESET delta=0`、`STYLE_LOAD delta=0`;本实现没有 visual GPU resource rebuild。 + +## 已执行验证 + +- shader generator 连续两次运行:PASS,OpenGL generated header 已同步。 +- Linux OpenGL 精确对象编译: + - `CMakeFiles/mbgl-core.dir/src/mbgl/style/layers/raster_particle_layer.cpp.o` + - `CMakeFiles/mbgl-core.dir/src/mbgl/renderer/layers/render_particle_validation_layer.cpp.o` + - `test/CMakeFiles/mbgl-test.dir/style/style_layer.test.cpp.o` + - 结果:PASS。 +- C++ `RasterParticleVisualProperties` 最小测试已加入并成功编译。 +- `clang-format`、`git diff --check`:PASS。 + +未执行:Android Gradle、ADB、安装、真机、Jest。 + +## Android 最终验收结果 + +- T1:PASS。UNIFORM EAST、UNIFORM NORTH、VORTEX 正式 streak 显示和方向均正常。 +- T2:PASS。VIS DEFAULT、SIZE ±、LENGTH ±、OPACITY ±、TRAIL ±、FADE ±、COLOR 均经正式 Runtime Property 生效;未描述或实现为 Field reload / simulation rebuild。 +- T3:PASS。T0 EAST/T1 NORTH、MIX 0/50/100 和 speed color 正常;MIX 保持 property-only,没有 Field Window reload 或 simulation reset。 +- T4:PASS。40-04 冻结的 Runtime Field Window 语义无回归:A/B→B/C→C/D、`OLD_T1 -> NEW_T0` reuse、仅上传新 T1、Duplicate NOOP、fail-closed、Style 不重载、simulation 不无意义 reset、Trail 不无意义清空均通过。 +- T5:PASS。pan、zoom、bearing/camera、DATELINE、WORLD WRAP 未见正式粒子与地图组合异常。 +- T6:PASS。DISABLE/ENABLE 和屏幕/生命周期恢复后粒子自动恢复,无需额外按钮。 +- T7:PASS。GLOBAL、OCEAN A、OCEAN B 保持既有 mask 语义。 +- T8:PASS。256、1K、4K、16K、64K、256K、1M 均实际运行;1M 在正式 streak + speed color 路径上无 crash、黑屏、Renderer 中断、明显资源错误或结构性 full-particle-draw 失败。 + +当前 viewport 中 4K 以上会因高密度 streak + Trail 而接近铺满画面。这是 Validation 可读性现象:256/1K 用于视觉检查,4K 以上主要用于容量和稳定性,不构成功能失败,也不需要为此改 Renderer 或重新编译 APK。1M 仍仅代表当前验证设备上已验证的容量,不是理论最大值、默认产品数量或全设备保证。 + +logcat 中 `TimeStats: RenderEngineTimes are already at its maximum size[64]` 是 Android/RenderEngine/虚拟机侧噪声。用户同时观察到粒子 Renderer 连续执行 `State -> Trail -> Composite` 并保持 `hazard=SAFE`,未有对应 crash 或中断;该信息不构成 STEP-046 Renderer FAIL。 + +## Git 收尾 + +本报告随文档收尾提交并推送到 `origin/particle/main`。用户既有未跟踪任务书和审查 diff 不纳入提交。 + +## 最终结论 + +未发现阻塞性问题。40-05 / STEP-046:`CLOSED / PASS`。 + +## 范围外 + +未实现 WeatherColorLayer、真实天气 HTTP/GRIB/provider、React Native、Vulkan、iOS/Metal、生产 mask、粒子箭头/旋转或 APK ZIP 技术债。 diff --git a/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v1.md b/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v1.md new file mode 100644 index 000000000..256777d03 --- /dev/null +++ b/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v1.md @@ -0,0 +1,32 @@ +# 40-06 Runtime Timeline 与 Playback Codex 完成报告 + +状态:IMPLEMENTED / WAITING FOR ANDROID VALIDATION + +## 完成内容 + +STEP-047 已实现由 `RasterArraySource` 唯一持有的 Timeline/Playback runtime。它保存有界 immutable snapshots、严格递增 logical timestamps、cursor、T0/T1、mix、播放/暂停、速度以及 gap/invalid/recover/end 状态。Renderer 不保存 Android 状态,不了解 provider/files/network,只读取 Source Core 的当前窗口和 mix。 + +自动播放在边界生成新的窗口 revision,沿用既有 `OLD_T1 → NEW_T0` overlap GPU reuse;正常推进不要求 Style reload、Layer recreate、simulation reset 或 trail clear。MIX 是 Core clock 每帧产生的 scalar,不使用 Android Timer/JNI 每帧调用。 + +Android 侧新增薄 `controlRasterArraySourceTimeline(sourceId, command, value)` bridge;Activity 仅触发低频控制。Validation 控制台已收敛为 `TIME | VIS | COUNT | MAP | MORE`,所有既有动作可达,并增加 GAP/INVALID/RECOVER。 + +## Core 覆盖 + +Source unit test 覆盖:五个快照、play/pause、速度合法性、step、seek/end。源码还校验时间戳严格递增与最多八个验证 cache entries。 + +## 未做内容 + +未接入真实天气网络、GRIB、Server、forecast cache、供应商协议、React Native、iOS/Metal/Vulkan 或新的粒子视觉能力;未重写 40-02/03/04/05 已冻结路径。 + +## 用户 Android x86_64 验收命令 + +```bat +cd /d C:\path\to\MapLibreParticleLab\platform\android +.\gradlew.bat :MapLibreAndroidTestApp:assembleOpenglDebug -Pandroid.injected.build.abi=x86_64 --console=plain +``` + +APK:`MapLibreAndroidTestApp/build/intermediates/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`。 + +建议手工验收:TIME→LOAD TIMELINE,PLAY,0.5x/1x/2x、pause/resume、step/seek;观察自动窗口边界 reuse;GAP/INVALID 在边界 fail-closed,RECOVER 后继续;再回归 MIX、VIS、COUNT、MAP、DISABLE/ENABLE。 + +本轮未执行 Android Gradle、ADB、Jest 或实体机测试。 diff --git a/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v2.md b/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v2.md new file mode 100644 index 000000000..af6edcfb2 --- /dev/null +++ b/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v2.md @@ -0,0 +1,9 @@ +# 40-06 / STEP-047 集中修复完成报告 + +状态:IMPLEMENTED / WAITING FOR ANDROID RE-VALIDATION + +第一轮 Android 验收为 `PARTIAL / NEEDS FIX`,历史结论保留于 STEP-047 v2。此次修复将 Core logical window 拉长至约 200 秒;暂停/恢复、END restart、seek 状态保持、GAP/INVALID/RECOVER 与可见 speed/state 行为均已在 Core/Validation 层修正。 + +Source 仍是 Timeline 和 CPU preload cache 权威;Renderer 继续只消费当前 T0/T1/mix。CACHE/PRELOAD 日志来自真实 immutable Source entries,而非未来 GPU 上传。自动边界仍走既有 overlap GPU reuse。 + +未执行 Android Gradle、ADB、Jest 或设备测试;等待一次完整 Android Round 2。 diff --git a/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v3.md b/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v3.md new file mode 100644 index 000000000..bb942846f --- /dev/null +++ b/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v3.md @@ -0,0 +1,7 @@ +# 40-06 / STEP-047 Playback Tick 根因修复报告 + +状态:IMPLEMENTED / WAITING FOR ANDROID RE-VALIDATION + +旧 tick 位于 `RenderRasterParticleLayer::update()`,其不是连续 drawable-frame hook,故 PLAY 后只有短暂 cursor/mix 更新。新的 tick 位于 State pass `ParticleGpuValidationTweaker::execute()`;粒子已有 continuous repaint/render 就是其驱动来源。没有 Android timer、每帧 JNI、线程或 while-playing loop。 + +PLAY/PAUSE/RESUME/speed 的 monotonic anchor 继续由 `RasterArrayTimelineRuntime` 持有;每帧仅更新 mix,不上传 Field、不 reload Style、不 reset simulation 或清 Trail。窗口边界仍由既有 layer update/reuse 路径处理。 diff --git a/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v4.md b/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v4.md new file mode 100644 index 000000000..65305d1d0 --- /dev/null +++ b/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v4.md @@ -0,0 +1,21 @@ +# 40-06 / STEP-047 Runtime Timeline 与 Playback 最终完成报告 + +状态:CLOSED / PASS + +## 最终功能基线 + +Functional validated commit:`1343929474796b1313489ee9fecbf23b949580a7`(`fix: keep particle timeline playback ticking`)。本次仅作 docs-only closeout,另行提交的文档 commit 不代表新的功能实现。 + +## Android 最终验收 + +Android 在 Linux 编译、Windows 11 操作、Android 虚拟机 `192.168.1.146` 环境完成最终验证。持续 Playback Tick、Pause/Resume、Speed、Seek、Step、Gap Hold/Recover、Invalid Next fail-closed/Recover、Preload/Cache、Window GPU reuse、仅上传 NEW_T1、无正常 simulation reset/trail clear、VIS runtime、MAP/MORE 与紧凑 UI 全部 PASS。 + +Playback tick 通过 Core State drawable 的持续 frame lifecycle 驱动;没有 Android timer、每帧 JNI、额外线程或 busy loop。`OLD_T1 → NEW_T0` reuse、`simulationRebuild=false`、`trailCleared=false` 继续保持。 + +## 历史与技术债 + +Round 1 与 Round 2 的 `PARTIAL / NEEDS FIX` 记录保留于 v2/v3:前者为交互/可观测性缺口,后者定位到 tick 错接非持续 update。最终修复已由 Android 复验通过。 + +状态栏对自动 MIX、WAITING/INVALID 的展示仍是 `NON-BLOCKING` Validation UI observability 技术债。`TimeStats: RenderEngineTimes are already at its maximum size[64]` 属环境噪音,未见 Renderer failure。 + +本次未修改功能代码,未执行 Android Gradle、ADB、Jest 或新增测试。 diff --git a/docs/报告/40_01_Android正式粒子StateSeed修复_Codex完成报告_v1.md b/docs/报告/40_01_Android正式粒子StateSeed修复_Codex完成报告_v1.md new file mode 100644 index 000000000..34f50d91b --- /dev/null +++ b/docs/报告/40_01_Android正式粒子StateSeed修复_Codex完成报告_v1.md @@ -0,0 +1,37 @@ +# 40-01 Android 正式粒子 State Seed 修复 Codex 完成报告 v1 + +## 1. 结论 + +已修复正式粒子的 State 初始化契约。40-01 保持 `WAITING_MANUAL_VALIDATION`;未运行 Android Gradle、ADB 或实体机。 + +## 2. 根因 + +`RenderTarget::render()` 按 MapLibre 既有 contract 将每个 offscreen pass clear 为 `(0,0,0,1)`。State shader 原本把 `previous.a < 0.5` 当作“尚未 seed”,但新的 State A/B target 因此是 opaque black,条件恒为 false。所有 16×16 State texel 都从 `(0,0)` 开始;uniform-east 仅移动 x,四个 deterministic marker 聚在 canonical world 顶缘,当前视口通常不可见。 + +没有修改 `src/mbgl/renderer/render_target.cpp`。 + +## 3. Seed contract + +- `ParticleGpuResources::stateSeeded` 是唯一生命周期权威。 +- State pass 使用已有 UBO 的 `u_mask.w`:`1.0 = SEED_STATE`,`0.0 = CONTINUE_STATE`。 +- shader 在 `SEED_STATE` 时让每个 texel 从自身 `v_uv` 初始化,再采样 field;在 `CONTINUE_STATE` 时从 State read texture 的 `previous.rg` 继续。`field.a` 仍仅表示 Field/NoData validity。 +- Composite commit 后既有 `commit()` 将 `stateSeeded=true`。新 resource、context recreation 和 fixture/field reset 令其恢复 false;普通 ACTIVE、Camera、STATIC 与 transient `markLayerRenderable(false)` 不会重 seed。 +- STATIC zero-step bootstrap 使用相同 seed flag,但 `simulationStep=0`,因此建立分散的 deterministic state 而不推进 simulation。 + +Trail/Composite 的 State write → Trail write wiring、slot swap、hazard=SAFE、formal display、legacy validation display、fixture、mask、world wrap、antimeridian、rotation resource lifecycle 均未改变。 + +## 4. 诊断与静态验证 + +- 新增低频 `ParticleRender: stateSeed=INITIALIZE|CONTINUE`,含 generation、State A/B 和 fixture。 +- shader source:`shaders/particle_gpu_validation.fragment.glsl`;由 `node shaders/generate_shader_code.mjs` 生成 OpenGL header,未手改 generated header。 +- generator 二次运行幂等:PASS;GLSL source 与 generated fragment parity:PASS。 +- Linux OpenGL `render_particle_validation_layer.cpp.o`、相关 `test/style/source.test.cpp.o` 精确编译:PASS。 +- `clang-format --dry-run --Werror` 与 `git diff --check`:PASS。 + +## 5. 下一步 + +用户下一次只验收 GLOBAL + ACTIVE + `uniform-east`:四个 deterministic marker/trail 应分散在 world 内,且连续向 east 移动。通过前不声明 40-01 PASS。 + +## 6. Git 收尾 + +最终代码修复 commit:`087d80d0527c33743a6c59a5c7cce5eeb836dfb5`(`fix: seed particle state explicitly`)。push 成功后 remote branch 与 `origin/particle/main` HEAD 在交付结果中记录。 diff --git a/docs/报告/40_01_Android正式粒子Trail显示修复_Codex完成报告_v1.md b/docs/报告/40_01_Android正式粒子Trail显示修复_Codex完成报告_v1.md new file mode 100644 index 000000000..83f099eb3 --- /dev/null +++ b/docs/报告/40_01_Android正式粒子Trail显示修复_Codex完成报告_v1.md @@ -0,0 +1,35 @@ +# 40-01 Android 正式粒子 Trail 显示修复 Codex 完成报告 v1 + +## 1. 结论 + +已修复“simulation 正常推进但正式 RasterParticle 屏幕主体静止”的 Composite 显示路径。40-01 仍为 `WAITING_MANUAL_VALIDATION`;未运行 Android Gradle、ADB 或真机。 + +## 2. 实际根因 + +`RenderRasterParticleLayer` 的 State A/B 与 Trail A/B 已按日志正常推进,Trail pass 也读取当前 State write texture。但是 `particle_gpu_validation` 的 Composite 只把 `u_previous`(Trail)的 RGB 折算为 alpha intensity,最终 RGB 仍无条件输出 30-03 的蓝色 stripe 和黄色 anchors;并且 ACTIVE Composite 的 legacy stripe frame 参数为零。于是 GPU 粒子没有成为可见输出。 + +## 3. 修复 + +- `ParticleGpuResources::formalDisplay` 只在正式 `RasterParticleLayer` 建立资源时设为 true,并经现有 UBO 的 `u_mask.z` 传入 shader;不增加第二套 simulation。 +- `FORMAL_PARTICLE` Composite 以当前 Trail texture RGB 为最终 premultiplied color,并以 Trail intensity 决定 alpha。uniform-east 的 State 位置经 Trail 进入 Composite,因此屏幕应可见连续向 east 的 marker/trail 运动;STATIC/FrozenComposite 继续采样最后一个 Trail read slot。 +- `LEGACY VALIDATION DISPLAY` 保留原有 30-03 stripe、anchors、mask、world-wrap 与 antimeridian 分支,不受正式显示模式影响。 +- shader 源唯一为 `shaders/particle_gpu_validation.fragment.glsl`;使用 `node shaders/generate_shader_code.mjs` 生成 `include/mbgl/shaders/gl/particle_gpu_validation.hpp`。生成器会同步规范化 `src/mbgl/shaders/shader_source.cpp`,已保留该生成物以保证二次运行无差异。 +- 增加一次性 `displayMode=FORMAL_PARTICLE|VALIDATION` 日志,以及低频 `trailStateSource` / `compositeTrailSource` 日志,诊断 State→Trail→Composite 的正式数据流。 + +未改变 Offline fixture adapter、RasterArraySource、RasterParticleLayer、field/NoData、State/Trail ping-pong、mask、ACTIVE/STATIC lifecycle、zero-step bootstrap、FrozenComposite、rotation resource recreation、world wrap、antimeridian 或 RasterArray source RenderItem contract。 + +## 4. 静态验证 + +- Linux OpenGL:`render_particle_validation_layer.cpp.o` 精确编译 PASS。 +- 相关现有 C++:`test/style/source.test.cpp.o` 精确编译 PASS。 +- shader generator:连续运行两次后 `particle_gpu_validation.hpp` 与 `shader_source.cpp` 哈希一致,PASS。 +- `clang-format --dry-run --Werror`(受影响 C++)和 `git diff --check`:PASS。 +- 未运行完整 `mbgl-test`:该 target 在本 build tree 仍需 312 个对象,属于与本次 shader 显示修复无关的广泛构建,未把未执行测试描述为 PASS。 + +## 5. 后续人工验收 + +用户下一轮仅验收 GLOBAL + ACTIVE + `uniform-east`:正式 RasterParticle 必须肉眼清楚、连续地向 east 移动。通过后再测试 STATIC、uniform-north、Rotation 与 T1-T12。 + +## 6. Git 收尾 + +最终代码修复 commit:`c8e1f942ebd5c5447990bb4fe001fdaf234fd740`(`fix: display formal particle trails`)。push 成功后,remote branch 与最终 `origin/particle/main` HEAD 在交付结果中记录。 diff --git a/docs/报告/40_01_Android粒子Simulation生命周期修复_Codex完成报告_v1.md b/docs/报告/40_01_Android粒子Simulation生命周期修复_Codex完成报告_v1.md new file mode 100644 index 000000000..679b4b83b --- /dev/null +++ b/docs/报告/40_01_Android粒子Simulation生命周期修复_Codex完成报告_v1.md @@ -0,0 +1,51 @@ +# 40-01 Android 粒子 ACTIVE / STATIC Simulation 生命周期修复 Codex 完成报告 v1 + +## 1. 结论 + +已修复 Android 40-01 的跨 render simulation 生命周期错误。40-01 状态保持 `WAITING_MANUAL_VALIDATION`;本报告不声明 Android 实机验收 PASS。 + +## 2. 实际根因与调用链 + +- 日志 `ParticleValidation30_02: disabled reset generation=...` 的唯一打印方是 `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` 中 `RenderRasterParticleLayer::markLayerRenderable()`。 +- `RenderOrchestrator::createRenderTree()` 按 source 依次计算 layer 可渲染性。Test App style 同时有 `raster-array` fixture source 与其他 source;处理不匹配 source 时会临时向粒子 layer 传入 `willRender=false`,处理匹配的 RasterArray source 后同一 render tree 又传回 `true`。 +- 原 override 把这个 source-scoped 的临时 false 误认为持久 disabled:移除 target、清空注册并执行 `ParticleGpuResources::resetSequence()`。因此 `committedFrame`(simulationFrame)每个 render 都从 1 重新开始,State A/B 与 Trail A/B read slot 每帧都回到 A,seed 状态也被清除;STATIC 因而每帧重新 bootstrap。 + +## 3. 修复内容 + +- 仅修改 `src/mbgl/renderer/layers/render_particle_validation_layer.cpp`。 +- `markLayerRenderable()` 现在只调用基类实现;不在按 source 的 transient false 中重置粒子资源。没有修改通用 `RenderOrchestrator`,保持“最终 enabled RenderLayer 才 update”的既有契约。 +- `ParticleGpuResources` 仍由 `RenderRasterParticleLayer::gpuResources` 共享持有,故 `committedFrame`、State/Trail read slot、seed 标记、targets 与 `resourceGeneration` 均跨 `render()` 持久存在。 +- `resetSequence()` 补充清除未提交 bootstrap 标记与 static freeze sampled-log 标记,确保真实 reset 后的状态完整一致。 +- 新增一次性 ACTIVE/STATIC transition 日志(generation、fixture、simulationFrame、State/Trail read slot);schedule/pass/commit 改为记录前 3 帧和每 60 帧,便于确认 `1→2→3` 与 `A→B→A`,同时避免正常运行的 reset 日志洪水。 + +## 4. 修改后生命周期 + +- ACTIVE:新资源、fixture/field generation 切换、context loss 后资源重建等真实事件可重置;其余连续 render 保留状态并执行 `State → Trail → Composite`,commit 后 frame 与 A/B slot 正常推进。 +- STATIC:仅当 State/Trail 尚未 seed(首次创建或真实资源事件)时执行一次 zero-step bootstrap;已经有 committed state 时只生成 FrozenComposite,不注册 State/Trail target、不 commit、不增加 frame。 +- ACTIVE→STATIC:保留已有 simulation,进入 frozen composite;如资源尚未 seed,则进行一次必要 bootstrap。 +- STATIC→ACTIVE:沿用既有保存的 frame 和 ping-pong slot 继续 simulation,不再每帧回到 frame 1。 + +真实失效条件未扩大:fixture/field generation 改变会调用既有 `resetSequence()`;viewport 改变会按既有逻辑重建 Trail A/B;context loss 丢弃 resource 并生成新 generation;layer removal 清理已注册 targets。没有改变 State/Trail ping-pong 结构、fixture contract、resourceGeneration contract、public API、shader、mask、camera/world-wrap/antimeridian 或 GPU pass 顺序;30-02 的 `hazard=SAFE` 约束仍由原有 distinct read/write targets 与 `State,Trail,Composite` 排程维护。 + +## 5. 代码级自检 + +- [x] ACTIVE 连续 render 不再从 `markLayerRenderable(false)` 调用 disabled reset。 +- [x] `simulationFrame`/State A-B/Trail A-B 在 `ParticleGpuResources` 中,并由 layer 的 `gpuResources` 跨 render 持有。 +- [x] ACTIVE 日志采样可显示 `simulationFrame=1,2,3`。 +- [x] State 与 Trail 的读写 slot 在每次 commit 以 `1-read` 交替。 +- [x] STATIC bootstrap 受 `!stateSeeded || !trailSeeded` 限制,seed 后使用 FrozenComposite。 +- [x] STATIC→ACTIVE 不执行 transient disabled reset,保留现有 frame/slot。 +- [x] field generation、viewport、context、layer removal 的现有资源处理仍保留。 +- [x] `State,Trail,Composite` 的既有 pass 排程与 `hazard=SAFE` 检查未变。 + +## 6. 构建与交付 + +- Linux OpenGL 精确编译:`ninja -C build-linux-opengl CMakeFiles/mbgl-core.dir/src/mbgl/renderer/layers/render_particle_validation_layer.cpp.o`,PASS。 +- `clang-format`、`git diff --check`:PASS。 +- Android OpenGL Debug APK:`cd platform/android && ./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug -Pmaplibre.abis=x86_64 --console=plain`,PASS(`BUILD SUCCESSFUL`)。APK 为约 696 MB,归档内 native ABI 仅为 `x86_64`。 +- 最终代码修复 commit:`c222b9a3d26dfb548b2528b3088c7c6799832dc9`(`fix: preserve particle simulation lifecycle`)。 +- push:成功;remote branch:`origin/particle/main`。完成报告与 Step/开发记录作为同一任务的后续文档提交一并推送。 + +## 7. 用户手工验收 + +请使用本轮生成的 `platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk` 安装到 x86_64 测试设备,只验收:GLOBAL + ACTIVE 持续 10–15 秒可见运动;ACTIVE→STATIC 冻结;STATIC→ACTIVE 后再次持续运动。不要将本报告视作已完成 ADB/真机验收。 diff --git a/docs/报告/40_01_Android粒子最小正式闭环_Codex完成报告_v1.md b/docs/报告/40_01_Android粒子最小正式闭环_Codex完成报告_v1.md new file mode 100644 index 000000000..15a62a544 --- /dev/null +++ b/docs/报告/40_01_Android粒子最小正式闭环_Codex完成报告_v1.md @@ -0,0 +1,83 @@ +# 40-01 Android 粒子最小正式闭环 Codex 完成报告 v1 + +## 1. 执行结论 + +40-01 已完成 Stage A 实现、连续调试修复和 Stage B Android OpenGL x86_64 手工验收,正式状态为 `PASS`。 + +## 2. Git 基线 + +- branch:`particle/main` +- start commit:`1ec44701146925dec9a87618e3f90bee9a12073f`(`docs: record 30-03 final push metadata`) +- 工作树:保留三份既有未跟踪任务书,不纳入本任务提交。 +- submodule:已记录,未修改。 +- `git rerere.enabled`:`true`。 + +初始 Stage A 实现提交:`0b076c6d52fffb2ee4411cac64f06d43cea260c8`(`feat: establish android particle field rendering loop`)。 + +本轮修正实现提交:`39c7ae22ede51f4b755dc658e2e27b24b26b807e`(`feat: establish formal raster particle source chain`)。 + +首次 push:成功,`origin/particle/main` 从 `1ec447011` 更新为 `0b076c6d5`。本报告保留 Stage A 初始提交与中间修复的历史事实;Stage B 文档收尾 commit、push 结果与最终远端 HEAD 在交付时记录。 + +## 3. 实际修改 + +- 新增隔离的 `mbgl::particle::OfflineFixtureParticleAdapter` 与 backend-neutral `ParticleFieldSnapshot`,并新增正式 `RasterArraySource -> RenderRasterArraySource -> RasterParticleLayer -> RenderRasterParticleLayer`。正式 layer 为 `Source::Required`,不再由 source-less `particle-validation` 承担 40-01 闭环。 +- 新增确定性 `uniform-east`、`uniform-north`、`vortex` fixture 及生成脚本;metadata 遵循架构文档 v1 contract。 +- `RenderRasterParticleLayer` 复用已验证的 State A/B、Trail A/B、shared mask、camera transform、world wrap、antimeridian 与 canonical composite;没有新建 Android 侧或第二套 GPU 粒子引擎。历史 `ParticleValidationLayer` 仍可作 validation tool,但不是正式 source path。 +- Android Test App 将 fixture 打包进 `src/main/assets/integration/particle-fixtures/`,Style 使用 `asset://` URI;Core 只通过 FileSource bytes loader,Linux helper 保留仓库 fixture 读取。 +- NoData cell 不再使整个合法 field 无效。GPU field alpha 保存 validity,State sampling 命中 invalid cell 会确定性 respawn;NaN/Inf 仍 fail closed。 +- 修复 STATIC/context recreation 的资源 bootstrap:field、mask、trail、state 和首帧 composite 均可在 STATIC 中建立,bootstrap 为零 simulation step。 +- Test App 保存 fixture selection,并增加 east/north 最小切换控制。 + +## 4. 构建与测试记录 + +- Linux OpenGL 受影响对象编译:PASS。 +- C++ 格式、`git diff --check`、fixture JSON/生成/sha256:PASS。 +- adapter smoke test:PASS,三份普通 fixture 成功 decode/validate;`nodata` fixture 的单 cell invalid 标记被保留且 field 整体合法。Android asset 与仓库 fixture byte parity:PASS。 +- Stage A 阶段未运行 Gradle、APK 安装、ADB 或 Android 实机验证;Stage B 已由用户完成 Android OpenGL x86_64 手工验收,结果见第 5 节。 + +## 4.1 RasterArray 离线下载枚举接入修复 + +- Android OpenGL 实际编译发现 `OfflineDownload` 的“required offline resource count”与“offline resource queue/download”两个穷尽 `SourceType` switch 漏掉 `RasterArray`。 +- 当前 `RasterArraySource` 只以 `FileSource::request(Resource::source(...))` 读取 metadata 与 field;40-01 的 URL 是 APK `asset://integration/particle-fixtures/...`。其 Impl 未定义 tileset、zoom range 或 OfflineDownload 可管理的 URL/resource contract,因此这两个 switch 均增加显式 `SourceType::RasterArray` no-op,不增加资源计数或下载队列。 +- 未添加 `default:`;全仓非 vendor 的 `SourceType` switch 复核后,除本文件两个遗漏外均已处理 `RasterArray`。 +- Linux/Core 精确编译 `offline_download.cpp.o`(含 `-Werror`)、`clang-format --dry-run --Werror` 与 `git diff --check`:PASS。commit 和 push 结果在本轮交付中报告;状态继续为 `WAITING_MANUAL_VALIDATION`,未运行 Gradle 或 ADB。 + +## 4.2 RasterArray Source RenderItem 空指针修复 + +- Android OpenGL 首次启动日志确认 fixture 已成功从 APK asset 加载、粒子 layer 已 evaluate,随后 `Renderer::Impl::render()` 在 State A/B 创建前发生 fault addr `0x0` 的 SIGSEGV。 +- 根因是 loaded + needsRendering 使 `RenderRasterArraySource` enabled,但其 `createRenderItem()` 返回空指针;RenderOrchestrator 将该值放入 source item vector,RenderTree 随后无条件解引用。该全局契约不作容错放宽。 +- 修复在 RasterArray source 内按 ImageSource 生命周期准备并移交合法 no-op RenderItem:无 upload、无 render pass、无 particle draw。正式 GPU particle 绘制仍只属于 `RenderRasterParticleLayer`。 +- Linux/Core `render_source.cpp.o` 和含新增 `RasterArrayRenderItem` 的 `source.test.cpp.o` 均以 `-Werror` 精确编译;`clang-format --dry-run --Werror`、`git diff --check` 与全部 RenderSource `createRenderItem()` return audit 均通过。未运行完整 Linux 测试二进制、Android Gradle、ADB 或真机测试,状态仍为 `WAITING_MANUAL_VALIDATION`。 + +## 5. Stage B 最终 Android OpenGL x86_64 验收 + +用户已手工完成并确认以下全部 PASS:GLOBAL + ACTIVE + UNIFORM EAST;ACTIVE → STATIC → ACTIVE;UNIFORM EAST → UNIFORM NORTH → UNIFORM EAST;STATIC + OCEAN A/B Rotation 自动恢复;ACTIVE Rotation;Pan / Zoom / Bearing / Pitch;World Wrap;Antimeridian 双向跨越;GLOBAL / OCEAN A / OCEAN B;Shared Mask reuse;DISABLE / ENABLE;快速 Fixture 切换;最终重复 Rotation 回归。 + +同一 RenderThread/resource generation 内,GLOBAL、OCEAN_ONLY_A、OCEAN_ONLY_B 日志均复用相同 `maskGeneration` 并输出 `reuse=true`。graphics resource 重建后 generation 与 maskGeneration 可递增,这是新 GPU resource,不构成同一 generation 内重复创建。 + +## 6. 40-01 最终调试事实 + +- OfflineDownload 两处 exhaustive `SourceType` switch 补入 RasterArray 的显式 asset-fixture no-op。 +- enabled RasterArray source 空 RenderItem 导致的 RenderThread SIGSEGV 已修复为 source 内合法 no-op RenderItem。 +- 处理过 Android APK / `.cxx` 旧 native 缓存观察问题。 +- 修复 per-source transient renderability 导致的 ACTIVE/STATIC reset loop,使 simulationFrame 与 A/B ping-pong 跨 frame 持久。 +- 正式 Composite 现在显示 Trail RGB;legacy validation 的 stripe/anchors/world-wrap 诊断显示仍保留。 +- shader 的唯一 source 为 GLSL,generated OpenGL header 经 generator 同步且幂等。 +- opaque RenderTarget clear 下 State 通过 `stateSeeded` 显式 seed;不再错误依赖 sampled alpha。 +- RasterArray snapshot generation 为跨 source instance 的 runtime revision;renderer 用 generation、fixtureID、dataVersion 三元 identity 阻止 east/north GPU field 误复用。 +- Rotation/Viewport STATIC 自动恢复完成并通过回归。 + +## 7. 范围外事项 + +40-01 的 Android OpenGL x86_64 范围已 PASS。iOS/Metal、Android Vulkan、真实在线数据、生产 coastline/mask、性能和独立 EGL context-loss 设备证据不在本次 PASS 声明范围。 + +## 8. 与指令书差异 + +此前报告错误地写为“无”:当时正式 Source/Layer/RenderLayer 链尚未建立,且 Android runtime fixture 仍错误依赖源码仓库相对路径。本轮已修正为正式 Source-required 链和 APK asset 输入边界;后续修复和 Stage B 实机证据现已使 40-01 正式 PASS。 + +## 9. Stage B Git 收尾 + +- final Stage B closeout commit:`8eaed68c45a99eaed60d2b0534b9f10949af8311` +- commit message:`docs: close 40-01 android validation` +- push:成功,`origin/particle/main` 从 `34cc5be26` 更新到该 closeout commit。 +- remote branch:`origin/particle/main`;该次 closeout push 后 HEAD:`8eaed68c45a99eaed60d2b0534b9f10949af8311`。 diff --git a/docs/报告/40_01_RasterArrayFieldIdentity修复_Codex完成报告_v1.md b/docs/报告/40_01_RasterArrayFieldIdentity修复_Codex完成报告_v1.md new file mode 100644 index 000000000..e540c6daa --- /dev/null +++ b/docs/报告/40_01_RasterArrayFieldIdentity修复_Codex完成报告_v1.md @@ -0,0 +1,27 @@ +# 40-01 RasterArray Field Identity 修复 Codex 完成报告 v1 + +## 1. 结论 + +已修复 Android Style/source recreation 后 `uniform-north` 错误复用 `uniform-east` GPU field 的 identity collision。40-01 继续为 `WAITING_MANUAL_VALIDATION`;未运行 Android Gradle、ADB 或真机。 + +## 2. 根因 + +`RasterArraySource::tryLoad()` 将 `ParticleFieldSnapshot::generation` 由 source 实例成员递增。相同 source ID 的 Style 重建会新建实例,east 首次 load 与 north 首次 load 都可能得到 generation 1。`RenderRasterParticleLayer` 正式 sourceChanged 只比较 generation,故将不同 fixture 错认为同一 field,复用了旧 east GPU texture。 + +## 3. 新 contract + +- `ParticleFieldSnapshot::generation` 是 process-wide、单调递增的运行时 snapshot revision;由 `raster_array_source.cpp` 的 atomic 分配。它不表示 metadata `dataVersion`,不使用时间戳或随机数。 +- renderer 的 `fieldIdentityChanged(current, next)` 同时比较 `generation`、`fixtureID`、`dataVersion`。任一变化均触发既有 field upload、`resetSequence()`、State re-seed 与 Trail clear;三者都相同则不重复上传。 +- 未修改 shader U/V、State/Trail/Composite slot wiring、ACTIVE/STATIC lifecycle、Rotation bootstrap、mask、world wrap、antimeridian、RenderTarget 或 Android UI。 + +## 4. 诊断与测试 + +- source load 日志:`ParticleField: source loaded fixtureId=... generation=... dataVersion=...`。 +- renderer switch 日志:`ParticleRender: fixture switched from=... to=... oldGeneration=... newGeneration=... gpuFieldGeneration=... trail=cleared stateSeed=INITIALIZE`。 +- 定向 C++ 测试覆盖两个独立 east/north RasterArraySource 实例 generation 不同;generation 相同、fixture 不同仍为 changed;同 fixture/generation/dataVersion 为 unchanged,dataVersion 改变为 changed。 +- Linux OpenGL 精确编译:`raster_array_source.cpp.o`、`render_particle_validation_layer.cpp.o`、`source.test.cpp.o`,PASS。 +- `clang-format --dry-run --Werror` 与 `git diff --check`:PASS。 + +## 5. Git 收尾 + +最终代码修复 commit:`1cd3e771095922863b3717689c690fc8ab0ed8c6`(`fix: distinguish raster array field snapshots`)。push 成功后 remote branch 与 `origin/particle/main` HEAD 在交付结果中记录。 diff --git a/docs/报告/README.md b/docs/报告/README.md new file mode 100644 index 000000000..a22e8c955 --- /dev/null +++ b/docs/报告/README.md @@ -0,0 +1,3 @@ +# Codex 完成报告目录 + +统一 `STEP-xxx_<主题>_Codex完成报告_vN.md`,不得覆盖旧版本。 diff --git a/docs/报告/STEP-000_环境初始化_完成报告_v1.md b/docs/报告/STEP-000_环境初始化_完成报告_v1.md new file mode 100644 index 000000000..8eb3a27cc --- /dev/null +++ b/docs/报告/STEP-000_环境初始化_完成报告_v1.md @@ -0,0 +1,118 @@ +# STEP-000 环境初始化完成报告 v1 + +## 结论 + +MapLibreParticleLab Linux / Android 主开发环境已完成建立,可以进入粒子系统开发阶段。 + +## 主开发环境 + +- Ubuntu 24.04 LTS +- x86_64 +- 4 CPU 线程 +- 约 8 GB RAM +- 默认编译并行度:3 + +## Git + +- 正式开发分支:`particle/main` +- `upstream`:MapLibre 官方 GitHub +- `origin`:用户 Gitea +- `upstream` 只用于读取官方更新 +- `git rerere` 用于长期 Fork 冲突复用 + +## MapLibre 基线 + +- Android:`android-v13.4.1` +- iOS:`ios-v6.28.0` +- Android/iOS 使用共同 MapLibre Native core 基线 +- 实际 commit 以根目录 `PARTICLE_BASELINE.md` 为准 + +## Linux Native 验证 + +已通过: + +- CMake `linux-opengl` configure +- Linux OpenGL backend +- `mbgl-render` +- `mbgl-render-test-runner` + +## Android OpenGL + +已成功生成: + +`platform/android/MapLibreAndroid/build/outputs/aar/MapLibreAndroid-opengl-debug.aar` + +包含: + +- `arm64-v8a/libmaplibre.so` +- `armeabi-v7a/libmaplibre.so` +- `x86/libmaplibre.so` +- `x86_64/libmaplibre.so` + +结论:PASS。 + +## Android Vulkan + +已成功生成: + +`platform/android/MapLibreAndroid/build/outputs/aar/MapLibreAndroid-vulkan-debug.aar` + +包含: + +- arm64-v8a +- armeabi-v7a +- x86 +- x86_64 + +四种 ABI 的 `libmaplibre.so`。 + +Debug AAR 同时包含对应的 Khronos Vulkan Validation Layer。 + +结论:PASS。 + +## Android SDK + +已确认: + +- `ANDROID_HOME=/root/Android/Sdk` +- Android Platform Tools 可用 +- NDK `28.2.13676358` 可用 +- `platform/android/local.properties` 已配置 +- `render-test/android/local.properties` 已配置 + +## 已修正的问题 + +### Linux preset 构建目录 + +实际 CMake preset 输出: + +`build-linux-opengl` + +构建工具已统一使用该目录,不再错误引用 `build`。 + +### Android SDK 定位 + +Gradle 工程同时使用 `ANDROID_HOME` 和本机 `local.properties`,避免新 shell 或 Codex 环境找不到 SDK。 + +## Codex 开发规范 + +已建立: + +- `AGENTS.md` +- 中文构建规范 +- 中文 Codex 协作规范 +- Step 阶段记录规范 +- 文档目录与命名规范 +- Git/upstream 同步规范 +- 粒子总体架构原则 +- `docs/报告/` 固定完成报告目录 + +## STEP-000 状态 + +**完成并冻结。** + +下一阶段: + +**STEP-001:MapLibre Native 粒子接入点只读调查。** + +STEP-001 暂不实现正式 RasterParticleLayer,先调查 Style / Source / Tile / RenderLayer / gfx / Shader / OpenGL / Vulkan / Metal 的最小接入点。 diff --git a/docs/报告/STEP-043A_40-02C源码审查后最小修正_Codex完成报告_v1.md b/docs/报告/STEP-043A_40-02C源码审查后最小修正_Codex完成报告_v1.md new file mode 100644 index 000000000..1f7aadcd2 --- /dev/null +++ b/docs/报告/STEP-043A_40-02C源码审查后最小修正_Codex完成报告_v1.md @@ -0,0 +1,24 @@ +# STEP-043A 40-02C 源码审查后最小修正完成报告 + +开始基线为 `5dd4dbf4967c1238e1bde8fb66fe3dbd2c4fe52a`,状态为 `IMPLEMENTED / WAITING FOR ANDROID VALIDATION`。 + +- Reuse:旧 T0/T1 texture/image 可复用,但 `resources` snapshot 现在写入 wanted revision,避免下一帧重复 sourceChanged。 +- MIX:guard 完成后才赋值 `timeMix`;拒绝时没有 state/property/style mutation。 +- Diagnostics:新增 renderLayerInstanceId、LAYER_CREATE/DESTROY/CHANGED、FIELD_UPLOAD、FIELD_REUSE、SIMULATION_RESET/resetCount 与 TIME_MIX_UPDATE 关联字段。 +- Style:INITIAL、ENABLE、DISABLE、UNIFORM_EAST、UNIFORM_NORTH、TIME_WINDOW、ADVANCE 等调用点均传明确 reason;MIX 不会触发 Style load。 +- 检查:shader generator、Linux Core/source-test object、clang-format、diff check 通过。未运行 Android Gradle、ADB 或设备验证。 + +用户手工: + +```bash +cd /mnt/sda1/MapLibreParticleLab/platform/android +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug --console=plain +``` + +随后以 `adb -s 192.168.1.146` 安装并验证 T1--T10;在用户设备验收前不得判 PASS。 + +代码/文档提交:`de665212cac9e3bab771077f38e55604e4c28694`(`fix: correct particle time-window reuse state`)。本报告更新后的最终文档提交与 `origin/particle/main` HEAD 见本次收尾提交。 + +## Kotlin nullable Layer 编译修正 + +Android Kotlin 编译发现 `Layer?` 无法通过中间 `rejectReason` smart cast。`setTimeMix()` 现先直接检查 `layer == null`,记录 `MIX_REJECTED reason=LAYER_NOT_READY` 后 return;其余 STYLE_LOADING、DISABLED、TIME_WINDOW_OFF guards 同样在 `timeMix` 赋值前 return。未使用 `!!` 或安全调用静默跳过。修复提交:`7a1ed80b0f870ff8c01b27d56f7890e53992b79e`。 diff --git a/docs/报告/STEP-043B_40-02C_Android到Core_MIX属性桥接_Codex完成报告_v1.md b/docs/报告/STEP-043B_40-02C_Android到Core_MIX属性桥接_Codex完成报告_v1.md new file mode 100644 index 000000000..2340f2a28 --- /dev/null +++ b/docs/报告/STEP-043B_40-02C_Android到Core_MIX属性桥接_Codex完成报告_v1.md @@ -0,0 +1,32 @@ +# STEP-043B Android 到 Core MIX 属性桥接完成报告 + +开始 HEAD:`0ab94c4eabaeb72be9ec956df5486dfa951c8181`。Android 运行日志证明 Core/Renderer layer 存在但 `Style.getLayer()` 返回 null;其原因是 LayerManager 无 raster-particle Java peer。 + +新增最小 float property bridge:`MapLibreMap -> NativeMapView -> JNI NativeMapView::setStyleLayerFloatProperty -> Style::getLayer -> Layer::setProperty`。它返回 boolean;Host 仅在成功后更新 `timeMix`,失败记录 `CORE_LAYER_NOT_FOUND`。未做完整 layer peer、未 setStyle、未触及 renderer/shader/Field。 + +静态检查:MIX 路径不再使用 `getLayer(PARTICLE_LAYER_ID)` 或 `setStyle`,`git diff --check` 通过。未运行 Android Gradle、ADB、APK 安装或启动。 + +用户: + +```bash +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug --console=plain +``` + +APK:`platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`。T4--T10 仍待 Android 手工验收,状态不得判 PASS。 + +## Android 编译修正 + +第一次 Android 编译失败是因为 generic `Layer::setProperty()` 收到 `double`,而当前 conversion 系统没有 `Convertible` / `ConversionTraits` backing。最终使用已存在的强类型 `RasterParticleLayer::setTimeMix(float)`:JNI 先校验 property 名和 runtime Layer 类型,再调用 setter。未使用 generic Convertible,也没有新增 conversion trait。Kotlin 已完全删除 MIX 的 Java `Style.getLayer()` / `layer.setProperties()` 写入;JNI boolean 成功才写入 `timeMix`,失败保持原值。未修改 shader、Renderer、Field、ADVANCE;未运行 Android Gradle、ADB,未加入 Jest。 + +用户 x86_64 编译: + +```bash +cd /mnt/sda1/MapLibreParticleLab/platform/android +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug -Pandroid.injected.build.abi=x86_64 --console=plain +``` + +APK:`./MapLibreAndroidTestApp/build/intermediates/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`。 + +## 最终 Android 验收关闭 + +用户已完成 Android x86_64 OpenGL Debug 编译、安装、启动和 T4--T10:全部 PASS。MIX 0/50/100 均得到 bridge `SUCCESS` 与 `TIME_MIX_UPDATE`,没有 MIX 引起的 Style reload、Field upload 或 simulation reset;DISABLED reject 保持旧 timeMix;B→C 保持 OLD_T1→NEW_T0 reuse 且只上传 C。STEP-043B / 40-02C / 40-02 现为 `CLOSED / PASS`。Codex 未运行 Android Gradle、ADB 或设备操作,未加入/执行 Jest。 diff --git a/docs/报告/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_Codex完成报告_v1.md b/docs/报告/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_Codex完成报告_v1.md new file mode 100644 index 000000000..129787ac8 --- /dev/null +++ b/docs/报告/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_Codex完成报告_v1.md @@ -0,0 +1,19 @@ +# STEP-043 40-02C Android 双时间片 GPU 时间插值最终修正完成报告 + +状态:`IMPLEMENTED / WAITING FOR ANDROID VALIDATION`。开始 HEAD 为 `d4c17fc8b0d5005bb25e603bd0046fcb19602aa3`。 + +- NORTH:Field/decode/texture/timeMix 继续为 V north-positive;仅 State shader 最终 Mercator displacement 使用 `dy=-v`。 +- MIX:Activity 记录 style epoch/ready;未 ready、disabled、layer 缺失或 time-window off 时记录 `MIX_REJECTED`,绝不 fallback `setStyle()`;正常路径仅 `setProperties` 并记录 `MIX_PROPERTY_ONLY`。 +- Identity:generation/fixture/dataVersion 继续标识 revision;snapshot 保存已验证 metadata SHA-256,`sameFieldGpuContent` 用 fixture/data/hash/compatibility,令跨 reload 的 B 可复用。 +- ADVANCE:Renderer slot matching/overlap 改用 GPU content identity,因此 B generation 变化但内容不变时可复用旧 texture,仅新 C 需要 upload;无内容 overlap 仍保留安全 reset。 +- 检查:shader generator、Linux Core 与 source test object、clang-format、diff check 通过。未执行 Android Gradle/ADB。 + +用户手工: + +```bash +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug --console=plain +adb -s 192.168.1.146 install -r platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk +adb -s 192.168.1.146 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity +``` + +T1--T10(EAST/NORTH、MIX 0 upload/reset、MIX reject、layer lifecycle、B→C +1 upload、回归)均待用户 Android 日志/视觉验收。 diff --git a/docs/报告/STEP-048_下一阶段规划调查_Codex完成报告_v1.md b/docs/报告/STEP-048_下一阶段规划调查_Codex完成报告_v1.md new file mode 100644 index 000000000..6906831da --- /dev/null +++ b/docs/报告/STEP-048_下一阶段规划调查_Codex完成报告_v1.md @@ -0,0 +1,32 @@ +# STEP-048 下一阶段规划调查 Codex 完成报告 + +## 结论摘要 + +基于当前 `particle/main` 的真实实现,下一阶段推荐为「Provider-neutral C++ Field Data Adapter 与 Runtime 输入契约」。现有 Runtime Field Window、Timeline、GPU reuse 和 renderer 已可复用;真正阻塞正式数据链的是 `OfflineFixtureParticleAdapter` 的 fixture-only schema/`knownFixture()` 限制,以及 Android Validation Activity 仍负责 synthetic asset source 描述。 + +需要小型新 adapter contract/files,但不需要新的 Renderer、Timeline 或 WeatherColorLayer 架构。WeatherColorLayer 当前没有实际实现,因此不应提前引入。 + +## 实际检查文件 + +- `src/mbgl/style/sources/raster_array_source.cpp` +- `include/mbgl/style/sources/raster_array_source.hpp` +- `src/mbgl/particle/field/offline_fixture_particle_adapter.hpp/.cpp` +- `include/mbgl/style/layers/raster_particle_layer.hpp` +- `src/mbgl/style/layers/raster_particle_layer_impl.hpp` +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `src/mbgl/renderer/layers/render_raster_particle_layer.hpp` +- Android Validation Activity、NativeMap/NativeMapView JNI bridge + +Gitea API 与 Git remote 均确认调查基线为 `46dceb5cb6110c94393b7413465f843844dd6502`。 + +## 输出与收尾 + +生成:本调查报告及本完成报告。未修改功能代码、测试、Android、Renderer 或 build 文件;未执行 Android Gradle、ADB、Jest。`git diff --check` 通过后仅提交两份调查文档并 push `origin/particle/main`。 + +## Git 收尾补充 + +- commit hash:`f0954476783b60c6ad4fdde7229657abed2e70f6` +- commit message:`docs: investigate next particle runtime stage` +- push result:`SUCCESS` +- remote branch:`origin/particle/main` +- Final HEAD:`f0954476783b60c6ad4fdde7229657abed2e70f6` diff --git a/docs/报告/STEP-049_Android复验与最终收尾_Codex完成报告_v1.md b/docs/报告/STEP-049_Android复验与最终收尾_Codex完成报告_v1.md new file mode 100644 index 000000000..f136d7b33 --- /dev/null +++ b/docs/报告/STEP-049_Android复验与最终收尾_Codex完成报告_v1.md @@ -0,0 +1,59 @@ +# STEP-049 Android 复验与最终收尾 — Codex 完成报告 + +## 基线 + +- Starting HEAD:`e34f04ba7038c5cb8fba8dc72c0d253cc4f7c85b` +- Android 实际复验代码基线:`e34f04ba7038c5cb8fba8dc72c0d253cc4f7c85b` +- 本任务性质:`docs-only` +- branch:`particle/main` + +## Android 最终结论 + +用户已完成 Android 实机/虚拟机综合复验:`Android revalidation = PASS`。STEP-049 正式结论:`CLOSED / PASS`。 + +## 验收矩阵 + +| 验收项 | 结果 | +|---|---| +| Timeline READY / PLAY | PASS | +| auto MIX Core | PASS | +| Window Boundary | PASS | +| OLD_T1 → NEW_T0 reuse | PASS | +| only NEW_T1 upload | PASS | +| simulationRebuild=false | PASS | +| trailCleared=false | PASS | +| Pause / Resume | PASS | +| Step Prev / Next | PASS | +| Seek | PASS | +| Gap Hold / Recover | PASS | +| Invalid Next / Recover | PASS | +| Duplicate T1 NOOP | PASS | +| Renderer fail-closed | PASS | +| No black screen | PASS | + +Invalid Next 复验确认 `TIMELINE_REJECTED reason=INVALID_NEXT` 后保持最后有效 Field、Renderer 持续运行,Recover 后恢复 Window Boundary/reuse。Gap 同样保持 fail-closed。Seek 的非连续 Field 切换允许 `FIELD_NO_OVERLAP` 与对应 simulation reset,不影响正常连续 Playback 结论。 + +## 非阻塞技术债 + +Validation UI 的 MIX 百分比文本可能滞后;Core MIX 实际正常推进,Timeline 状态显示已清晰。分类:`NON-BLOCKING observability`。不重新触发 Android build/install。 + +## 本任务未执行内容 + +- 未执行 Android Gradle +- 未重新 build APK +- 未执行 ADB +- 未启动 App +- 未重新做 Android 测试 +- 未执行 Jest +- 未修改任何功能代码 + +Android 验收已经由用户完成,本次仅进行 docs-only closeout。 + +## Git 收尾 + +- commit hash:`c31d3449d4a4f45df1c8c68dcf084cd97f33e7b1` +- commit message:`docs: close STEP-049 field adapter validation` +- push result:`SUCCESS` +- remote branch:`origin/particle/main` +- Final HEAD(收尾提交推送时):`c31d3449d4a4f45df1c8c68dcf084cd97f33e7b1` +- 无关 untracked 文件:保留,未修改、未 stage、未提交。 diff --git a/docs/报告/STEP-049_Provider-neutral_Field_Data_Adapter与Runtime输入契约_Codex完成报告_v1.md b/docs/报告/STEP-049_Provider-neutral_Field_Data_Adapter与Runtime输入契约_Codex完成报告_v1.md new file mode 100644 index 000000000..1d8bad65f --- /dev/null +++ b/docs/报告/STEP-049_Provider-neutral_Field_Data_Adapter与Runtime输入契约_Codex完成报告_v1.md @@ -0,0 +1,41 @@ +# STEP-049 Provider-neutral Field Data Adapter 与 Runtime 输入契约 — Codex 完成报告 + +## 结果 + +已完成 C++ provider-neutral Field descriptor、payload adapter、structured error 和 immutable Snapshot 接线。状态:`IMPLEMENTED / WAITING FOR ANDROID VALIDATION`;不能在用户 Android 验收前宣称 `CLOSED / PASS`。 + +## Git 基线与修改 + +- 起始 HEAD:`f0954476783b60c6ad4fdde7229657abed2e70f6` +- branch:`particle/main` +- origin 起始 HEAD:`f0954476783b60c6ad4fdde7229657abed2e70f6` +- 实际修改:`CMakeLists.txt`、Field adapter 新增头/源文件、fixture adapter、`RasterArraySource`、validation renderer 日志、`test/style/source.test.cpp`、STEP-048 报告、STEP-049 Step 记录、粒子开发记录。 +- 未修改/未 stage:工作区已有的无关 untracked 任务文件与 `STEP-046-e48fa087-review.diff`。 + +## 设计结论 + +`ParticleFieldDescriptor` 是最小 canonical 输入契约,包含 field identity、类型/显示域、valid time、网格 bounds、dimensions、layout/encoding、stride、byte length、no-data、data version 和可选 checksum。`ParticleFieldDataAdapter::decode()` 只接收 descriptor、bytes、runtime generation,校验后生成自持有 vectors 的 Snapshot;不持有 Android、Renderer、Timeline、provider 或 network 对象。 + +`OfflineFixtureParticleAdapter` 继续负责 fixture-v1 metadata/whitelist/checksum,并调用正式 adapter。`RasterArraySource` 仍复用唯一的 `updateRuntimeFieldWindow()`;没有改变 T0/T1、Timeline、Playback、MIX、GPU reuse 或 Renderer 业务边界。 + +## 验证 + +- C++ / Core / OpenGL:`cmake --build build-linux-opengl --target mbgl-test-runner -j3`,PASS。 +- 定向已有测试:`./mbgl-test-runner --gtest_filter='Source.ParticleFieldDataAdapter*:Source.RasterArray*'`,11/11 PASS。 +- `git diff --check`:PASS。 +- Android Gradle、APK copy/install、ADB、设备:未执行(任务明确由用户承担)。 +- Jest:未新增、未执行。 + +## Git 收尾 + +- commit hash:`ce1a148f271d7383c081b39e24c9ebc14054f280` +- commit message:`feat: add provider-neutral particle field adapter contract` +- push result:`SUCCESS` +- remote branch:`origin/particle/main` +- Final HEAD(功能提交推送时):`ce1a148f271d7383c081b39e24c9ebc14054f280` + +## 未完成项与风险 + +- 用户尚未执行 Android 一次综合验收,因此 EAST/NORTH/VORTEX、Playback、neutral valid/invalid、fail-closed、reuse、MIX 与 no-black-screen 的设备结果待补。 +- 当前 adapter canonical format 仍只支持 `interleaved-uv` + `float32-le`,这是本阶段现有 Runtime 的明确边界,不是 provider schema。 +- `ParticleFieldSnapshot::isValid()` 继续按当前 Wind/Global renderer 语义校验;Wave/Tide/Current 的 Ocean-only 语义不在本阶段实现。 diff --git a/docs/报告/STEP-049_Timeline回归修复_Codex完成报告_v1.md b/docs/报告/STEP-049_Timeline回归修复_Codex完成报告_v1.md new file mode 100644 index 000000000..2a00ee758 --- /dev/null +++ b/docs/报告/STEP-049_Timeline回归修复_Codex完成报告_v1.md @@ -0,0 +1,45 @@ +# STEP-049 Timeline 回归修复 — Codex 完成报告 + +## 状态 + +`IMPLEMENTED / WAITING_ANDROID_REVALIDATION`。本报告不宣称 `CLOSED / PASS`。 + +## 基线与根因 + +- Starting HEAD:`dcedbe7394e74b80d91f00315a9956dfaea35229` +- 实际远端 `origin/particle/main` 起始 HEAD:`dcedbe7394e74b80d91f00315a9956dfaea35229` +- 根因:`tryLoadRuntimeField()` 以 generation `0` 调用正式 adapter;adapter 合法拒绝,Timeline candidate 在更晚的 `tryInitializeTimeline()` 才分配 generation。Runtime Window 使用已分配 generation,故出现 Timeline 失败而 Window 正常。 +- 原 STEP-049 11/11 未捕获:测试覆盖了直接 adapter、手工 Timeline 和 Runtime Window,但没有覆盖真实 Source 异步 candidate preload → adapter → Timeline READY 顺序。 + +## 实际修改 + +- `src/mbgl/style/sources/raster_array_source.cpp`:candidate decode 前分配全局 generation,移除 Timeline 阶段重复赋值;增强低频 adapter rejection 日志。 +- `src/mbgl/particle/field/field_data_adapter.cpp`、`.hpp` 与 fixture wrapper:保留正式 contract 和 generation 非零校验,提供 error code 名称用于日志。 +- `test/style/source.test.cpp`:新增真实 candidate → adapter → Timeline READY 回归测试。 +- `platform/android/MapLibreAndroidTestApp/.../ParticleCoreLayerValidationActivity.kt`:仅整理 Window/Timeline 状态可见性。 +- `docs/50_Step记录/STEP-049..._v2.md`、本报告、`粒子开发记录.md`。 + +未修改 Runtime Field Window、Duplicate、Renderer simulation、Timeline Core 状态机或 Android per-frame JNI;未新增兼容层。 + +## 验证 + +- C++/Core/OpenGL:`cmake --build build-linux-opengl --target mbgl-test-runner -j3`,PASS。 +- 定向测试:`Source.RasterArrayTimelineCandidatesReachReadyThroughFieldAdapter:Source.ParticleFieldDataAdapter*:Source.RasterArray*`,12/12 PASS。 +- 合法 candidate 通过正式 adapter 并进入 Timeline READY:PASS。 +- generation=0 / invalid descriptor 结构化拒绝:PASS。 +- `git diff --check`:PASS。 +- Android Gradle/APK/copy/install/ADB/设备:未执行。 +- Jest:未新增、未执行。 + +## Android 待复验 + +需验证 `ParticleTimeline: event=READY`、PLAY 不再 `TIMELINE_NOT_READY`、MIX/cursor 推进、跨至少两个 Window boundary、Pause/Resume、GAP WAIT、INVALID NEXT、fail-closed、Renderer 持续和 `simulationRebuild=false`/`trailCleared=false`。 + +## Git 收尾 + +- commit hash:`a418e50a0105a23e70112eb77cd813485eae6855` +- commit message:`fix: restore particle timeline readiness after field adapter` +- push result:`SUCCESS` +- remote branch:`origin/particle/main` +- Final HEAD(修复提交推送时):`a418e50a0105a23e70112eb77cd813485eae6855` +- 无关 untracked 文件:保留,未修改、未 stage、未提交。 diff --git a/docs/报告/STEP-050_正式数据链路下一阶段调查_Codex完成报告_v1.md b/docs/报告/STEP-050_正式数据链路下一阶段调查_Codex完成报告_v1.md new file mode 100644 index 000000000..170d32f8a --- /dev/null +++ b/docs/报告/STEP-050_正式数据链路下一阶段调查_Codex完成报告_v1.md @@ -0,0 +1,61 @@ +# STEP-050 正式数据链路下一阶段调查 — Codex 完成报告 + +## 基线 + +- Starting HEAD:`1572b6c3b1bcc2ab0007181bad0609e7d0134575` +- origin/particle/main 起始 HEAD:`1572b6c3b1bcc2ab0007181bad0609e7d0134575` +- 当前分支:`particle/main` +- 调查性质:代码只读、文档新增 + +## 调查结论 + +STEP-049 的 Adapter 已停在“canonical descriptor + bytes → immutable Snapshot”层。当前真实业务入口仍是 `RasterArraySource` 的 fixture style schema 和 `OfflineFixtureParticleAdapter`;没有 Particle Weather Service、provider abstraction、生产网络下载、GRIB/NetCDF decoder、production cache 或外部生产 field source。 + +推荐下一实现阶段:`STEP-051 Local Production-like Field Feed 与 Core Snapshot Submit Bridge`。先以离线 local/asset binary 验证正式 neutral producer → Adapter → Snapshot → Timeline/Runtime 链路,再决定是否进入真实 provider/network 阶段。 + +## 实际检查的核心文件 + +- `include/mbgl/particle/field/field_data_adapter.hpp` +- `src/mbgl/particle/field/field_data_adapter.cpp` +- `src/mbgl/particle/field/offline_fixture_particle_adapter.hpp/.cpp` +- `include/mbgl/style/sources/raster_array_source.hpp` +- `src/mbgl/style/sources/raster_array_source.cpp` +- `src/mbgl/style/sources/raster_array_source_impl.hpp` +- `src/mbgl/style/conversion/source.cpp` +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `src/mbgl/renderer/layers/render_raster_particle_layer.hpp` +- `test/style/source.test.cpp` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` +- `particle-fixtures/README.md` 与 `particle-fixtures/*` +- MapLibre 通用 `FileSource`/`Resource`/offline storage 与 `src/mbgl/util/io.cpp` + +## 设计判断 + +- 不需要新 Provider Framework 或复杂新架构;STEP-051 可新增一个小型 local feed/submit 模块。 +- 不需要 network;不需要 vendor-specific code。 +- 原则上不修改 Timeline state machine、Runtime Field Window 或 Renderer;只在 Source/producer 边界做最小接线。 +- Snapshot ownership 已足够承载当前生产-like 输入;generation 由 Core Source/Runtime 分配,identity/validTime 由 producer descriptor 提供。 +- 当前 Timeline 已能消费外部 Snapshot,但 Source builder、style schema 和 candidate preload 仍 fixture-specific;Timeline timestamp 还使用固定逻辑时间。 + +## 候选与推荐 + +已比较四个候选:Core Snapshot Feed API、Local Binary/File-backed Production-like Source、Weather Service/Network Provider 骨架、Timeline/Preload 通用化。推荐合并前两者形成 STEP-051;不推荐现在引入网络 provider,也不建议在缺少真实 producer 时单独抽象 Timeline/cache。 + +## Rotation/Viewport 与 MIX + +当前 Android Activity 有标准 MapView lifecycle 转发和控件状态保存,但没有独立 Particle viewport recovery state machine;历史 Rotation/Viewport 议题不应并入不触及 Android host 的 STEP-051,也不应单独触发 APK。MIX UI 百分比文本滞后继续记录为 `NON-BLOCKING observability`;只有 STEP-051 本来修改 UI 时才顺带处理。 + +## 输出与验证 + +- 调查报告:`docs/20_调查/STEP-050_正式数据链路下一阶段调查报告_v1.md` +- 完成报告:本文件 +- 未修改功能代码、Renderer、Android、Tests、Gradle、CMake、iOS 或既有 Step 文档。 +- 未执行 Android Gradle/APK/ADB/App/实机测试。 +- 未执行 Jest。 +- `git diff --check`:PASS。 +- commit hash:`0dacb72b5725e0f2e539630f1269e7fde62a1ab1` +- commit message:`docs: investigate production particle data pipeline` +- push result:`SUCCESS` +- remote branch:`origin/particle/main` +- Final HEAD(调查文档提交推送时):`0dacb72b5725e0f2e539630f1269e7fde62a1ab1` +- 阻塞项:无调查阻塞;STEP-051 需在实现前冻结 local feed metadata/timestamp/generation ownership。 diff --git a/docs/报告/STEP-051_Android_Validation_Host_Kotlin编译修复_Codex完成报告_v1.md b/docs/报告/STEP-051_Android_Validation_Host_Kotlin编译修复_Codex完成报告_v1.md new file mode 100644 index 000000000..bfb4ea9cf --- /dev/null +++ b/docs/报告/STEP-051_Android_Validation_Host_Kotlin编译修复_Codex完成报告_v1.md @@ -0,0 +1,38 @@ +# STEP-051 Android Validation Host Kotlin 编译修复 — Codex 完成报告 + +## 基线 + +- Starting HEAD:`c94c9c5bd4cce0322c9e7beb95e67385ae085720` +- branch:`particle/main` +- 任务范围:仅修复 `reason` 参数透传。 + +## 修复 + +在 `ParticleCoreLayerValidationActivity.kt` 中: + +- `loadValidationStyle()` 调用改为 `validationStyle(active, enabled, reason)`。 +- `validationStyle()` 签名增加 `reason: String`。 +- 保留 `INVALID_PRODUCTION_METADATA` 判断:正常入口使用 `field-00.json`,`INVALID PROD` 使用 `field-invalid.json`。 +- 未新增全局 reason、状态变量、Timer 或第二套 production state。 + +未修改 Core Adapter、Descriptor、Snapshot、Snapshot Submit Bridge、Timeline、Runtime Field Window、RasterParticleLayer、Renderer、JNI、Network 或 Provider Framework。 + +## 检查与边界 + +- 所有 `validationStyle(` 调用点:已检查,唯一调用点传入三个参数。 +- `PRODUCTION FEED` / `INVALID PROD` 菜单:仍存在。 +- `git diff --check`:PASS。 +- Android Gradle/APK/ADB/App:未执行。 +- Jest:未执行、未修改。 +- Rotation/Viewport:未修改。 +- 无关 untracked 文件:未修改、未 stage、未提交。 + +## Git + +- commit hash:`9760009b3`。 +- commit message:`fix: pass validation reason into STEP-051 style builder` +- push result:SUCCESS。 +- remote branch:`origin/particle/main` +- Final HEAD(功能修复提交):`9760009b3`。 + +当前 STEP-051 状态保持:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。用户后续手工执行 Android OpenGL Debug build,首要门槛为 `BUILD SUCCESSFUL`。 diff --git a/docs/报告/STEP-051_Android_Validation_Host入口补充_Codex完成报告_v1.md b/docs/报告/STEP-051_Android_Validation_Host入口补充_Codex完成报告_v1.md new file mode 100644 index 000000000..6abde44ec --- /dev/null +++ b/docs/报告/STEP-051_Android_Validation_Host入口补充_Codex完成报告_v1.md @@ -0,0 +1,52 @@ +# STEP-051 Android Validation Host 入口补充 — Codex 完成报告 + +## 基线 + +- Starting HEAD:`e3e7c7e219c490ab45ca40794db0bc59fc2c4817` +- branch:`particle/main` +- origin/particle/main 起点:`e3e7c7e219c490ab45ca40794db0bc59fc2c4817` +- 任务性质:STEP-051 内部 Android Validation Host 补充。 + +## 修改与接线 + +- `ParticleCoreLayerValidationActivity.kt`:MORE 菜单新增 `PRODUCTION FEED` 与 `INVALID PROD`,分别触发现有 production-like source 和非法 metadata source。 +- `activity_particle_core_layer_validation.xml`:增加不可见菜单 target `validationProductionInvalid`。 +- Android asset:增加 `field-invalid.json`。 +- `STEP-051_Local_Production-like_Field_Feed与Core_Snapshot_Submit_Bridge_v1.md`:追加入口说明。 + +`PRODUCTION FEED` 复用 STEP-051 已有 `production-fields`、`LocalParticleFieldFeed`、正式 Adapter、Snapshot、`submitInitialRuntimeFieldWindow` 和 Core Timeline。没有复制 feed、Timeline、Runtime Window、MIX 或 normalization。`INVALID PROD` 只提供明确非法 local metadata 触发,Adapter contract 未放宽。 + +日志区分 production-like 路径与 fixture 路径;Core 侧既有低频 `PRODUCTION_FEED_LOAD`、`SNAPSHOT_SUBMIT`、Timeline READY/Boundary 日志继续生效。未增加 JNI method、JNI command、每帧日志或 Android Timer。 + +## 边界判断 + +- Core / Timeline / Runtime Field Window / Renderer:未修改。 +- ParticleFieldDataAdapter / Descriptor / Snapshot:未修改。 +- Duplicate:继续复用当前 source 的既有 `DUPLICATE T1`,production source 激活后由同一 Runtime Window 处理。 +- MIX observability:未修改,仍为 `NON-BLOCKING` 技术债。 +- Rotation/Viewport:未修改。 + +## 验证 + +- Linux OpenGL `mbgl-test-runner` 精确构建:复用 STEP-051 已通过构建。 +- 相关定向测试:复用并确认 STEP-051 既有 `Source.LocalProductionFeed*`、`Source.ParticleFieldDataAdapter*`、`Source.RasterArray*` 为 14/14 PASS。 +- Android source static inspection:确认 MORE 菜单、resource ID、production asset 路径和旧 fixture 入口均存在。 +- JNI signature/registration:本次无 JNI 改动,无需新增注册。 +- `git diff --check`:PASS。 +- Android Gradle/APK/ADB/App/实机:未执行。 +- Jest:未执行、未修改。 + +## Git 与状态 + +- commit message:`test: expose STEP-051 production feed validation` +- implementation commit hash:`8a5a91acf` (`test: expose STEP-051 production feed validation`)。 +- push result:SUCCESS。 +- remote branch:`origin/particle/main` +- Final HEAD(本次 Host 补充提交):`8a5a91acf`。 +- 无关 untracked 文件未修改、未 stage、未提交。 + +当前 STEP-051 状态保持:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。 + +## 用户 Android 验收步骤 + +重新 build/install 一次后:MORE → `PRODUCTION FEED`,观察 `PRODUCTION` 与 `TL READY`;再进入 TIME → `PLAY / RESTART`,验证 MIX、至少两个 boundary、reuse/upload、`simulationRebuild=false`、`trailCleared=false`、Pause/Resume、Duplicate T1。随后可点 MORE → `INVALID PROD`,确认 Adapter reject、保持最后有效 Field、Renderer 持续且不黑屏。 diff --git a/docs/报告/STEP-051_Android复验与最终收尾_Codex完成报告_v1.md b/docs/报告/STEP-051_Android复验与最终收尾_Codex完成报告_v1.md new file mode 100644 index 000000000..227fc1722 --- /dev/null +++ b/docs/报告/STEP-051_Android复验与最终收尾_Codex完成报告_v1.md @@ -0,0 +1,64 @@ +# STEP-051 Android 复验与最终收尾 — Codex 完成报告 + +## 最终状态 + +`STEP-051 = CLOSED / PASS` + +## 基线与环境 + +- Starting HEAD:`abfb4f40abc3c6aed7d4c1c00cc5622097f15e0b` +- branch:`particle/main` +- Android 手工复验操作机:Windows 11 +- Android 设备/虚拟机:`192.168.1.146` +- ADB 规则:`adb -s 192.168.1.146 ...` +- Android Gradle、APK build/install、ADB、App 启动和设备操作均由用户手工执行;Codex 本任务未执行。 + +## Android 复验矩阵 + +| 验收项 | 结果 | +|---|---| +| Production-like T0~T3 load / `PRODUCTION_FEED_LOAD` | PASS | +| Initial T0/T1 submit | PASS | +| Timeline READY | PASS | +| PLAY / Playback | PASS | +| Auto MIX | PASS | +| Window Boundary #1 | PASS | +| Window Boundary #2 | PASS | +| `OLD_T1 → NEW_T0` reuse | PASS | +| Only NEW_T1 upload | PASS | +| `simulationRebuild=false` | PASS | +| `trailCleared=false` | PASS | +| Pause/Resume | PASS(沿用 STEP-049 已通过基线,本轮未独立重复执行) | +| Duplicate T1 NOOP | PASS | +| Invalid metadata fail-closed | PASS | +| Renderer continuity | PASS | +| Black screen | 未发生 | +| Crash | 未发生 | + +关键日志包括 `PRODUCTION_FEED_LOAD`(T0~T3)、`SNAPSHOT_SUBMIT kind=INITIAL_T0_T1`、`ParticleTimeline: event=READY snapshots=4`、至少两次 `WINDOW_BOUNDARY`,以及 Invalid metadata 的 `ADAPTER_REJECTED errorCode=INVALID_TIMESTAMP_OR_IDENTITY`。 + +## 技术债与架构保护 + +Validation UI 的 auto MIX 文本可能滞后,WAITING/INVALID 视觉提示仍不够明显,分类为 `NON-BLOCKING observability`;本任务未修复,也未因此重新 build/install。Rotation/Viewport 技术债未处理。 + +已确认未破坏:`OLD_T1 → NEW_T0` reuse、only NEW_T1 upload、正常播放 `simulationRebuild=false`/`trailCleared=false`、MIX property-only、Core-owned Timeline/Playback、Pause 语义、Invalid/Gap fail-closed、Renderer provider-neutral 边界,以及 WeatherColorLayer 与 RasterParticleLayer 分离。 + +## 本任务范围与 Git + +本任务仅修改: + +- `docs/50_Step记录/STEP-051_Local_Production-like_Field_Feed与Core_Snapshot_Submit_Bridge_v1.md` +- 本最终收尾报告 + +未修改任何功能代码、Kotlin、JNI、Core、Timeline、Runtime、Renderer、Adapter、生产数据或测试。未执行 Jest。无关 untracked 文件未修改、未 stage、未提交。 + +- `git diff --check`:PASS。 +- commit hash:`f77064876`。 +- commit message:`docs: close STEP-051 Android validation` +- push result:SUCCESS。 +- remote branch:`origin/particle/main` +- Final HEAD(STEP-051 收尾提交):`f77064876`。 + +## 结论 + +Android production-like feed、Snapshot submit、Timeline、Window boundary、Duplicate、Invalid fail-closed 和 Renderer continuity 已完成最终复验。STEP-051 正式关闭为 `CLOSED / PASS`;不进入 STEP-052。 diff --git a/docs/报告/STEP-051_Local_Production-like_Field_Feed与Core_Snapshot_Submit_Bridge_Codex完成报告_v1.md b/docs/报告/STEP-051_Local_Production-like_Field_Feed与Core_Snapshot_Submit_Bridge_Codex完成报告_v1.md new file mode 100644 index 000000000..ea5cf5485 --- /dev/null +++ b/docs/报告/STEP-051_Local_Production-like_Field_Feed与Core_Snapshot_Submit_Bridge_Codex完成报告_v1.md @@ -0,0 +1,70 @@ +# STEP-051 Local Production-like Field Feed 与 Core Snapshot Submit Bridge — Codex 完成报告 + +## 基线与范围 + +- Starting HEAD:`885e7aebef7fb5d02acaed57f9dcf98b8d9e9ba3` +- branch:`particle/main` +- origin/particle/main 起点:`885e7aebef7fb5d02acaed57f9dcf98b8d9e9ba3` +- 任务范围:local production-like field feed、Core Snapshot submit bridge、定向测试、Android validation style/resource、文档。 + +## 实际修改 + +- `include/mbgl/particle/field/field_data_adapter.hpp` +- `src/mbgl/particle/field/field_data_adapter.cpp` +- `include/mbgl/particle/field/local_particle_field_feed.hpp` +- `src/mbgl/particle/field/local_particle_field_feed.cpp` +- `src/mbgl/particle/field/offline_fixture_particle_adapter.cpp` +- `include/mbgl/style/sources/raster_array_source.hpp` +- `src/mbgl/style/sources/raster_array_source_impl.hpp` +- `src/mbgl/style/sources/raster_array_source.cpp` +- `src/mbgl/style/conversion/source.cpp` +- `test/style/source.test.cpp` +- `CMakeLists.txt` +- `particle-production/field-00.json` 至 `field-03.json` +- Android production metadata assets +- `ParticleCoreLayerValidationActivity.kt` +- 本 STEP 记录与本报告 + +未修改 Renderer、WeatherColorLayer、Timeline state machine、Runtime Field Window 核心语义、Gradle、iOS 或网络代码。未新增 Provider Framework、Weather Service、第二套 Adapter/Timeline/Window。 + +## Contract 与 architecture impact + +`ParticleFieldDescriptor.generation` 现在是 producer contract 的显式字段。正式 Adapter 要求 descriptor generation 非零且与 decode 参数一致;不会静默修正非法值。`LocalParticleFieldFeed` 解析 `maplibre-particle-field/v1`,生产者拥有 `fieldId/generation/validTime`,Snapshot 保持 provider-neutral。 + +`RasterArraySource` 的 `RasterArrayLocalFieldDescriptor` 只传输 metadata/payload resource;`submitInitialRuntimeFieldWindow(T0,T1)` 是薄 bridge,后续继续使用既有 `updateRuntimeFieldWindow()`。Timeline 使用 Snapshot `validTime`,fixture 的旧 null timestamp 保留历史 fallback。未修改 Playback ownership。 + +production-like Android 入口使用四个 local metadata asset 和现有 canonical binary asset;旧 fixture validation 保留。未将解析、normalization、network 或 Timeline 状态放入 Kotlin/Renderer。 + +## 测试与构建 + +- `cmake --build build-linux-opengl --target mbgl-test-runner -j3`:PASS。 +- `./build-linux-opengl/mbgl-test-runner --gtest_filter='Source.LocalProductionFeed*:Source.ParticleFieldDataAdapter*:Source.RasterArray*'`:14/14 PASS。 +- 覆盖:metadata→descriptor→Snapshot、identity/generation/validTime、严格时间递增、initial T0/T1 READY、PLAY、正式非法 generation 拒绝,以及既有 Window/Boundary/GAP/INVALID/Recover/Duplicate 回归。 +- `git diff --check`:PASS。 + +## Android / Jest 边界 + +- Android Gradle:未执行。 +- APK build/copy/install:未执行。 +- ADB:未执行。 +- App launch / Android 实机或虚拟机测试:未执行。 +- Jest:未执行、未修改。 +- MIX UI 文本滞后:未扩大处理,继续作为 `NON-BLOCKING observability`。 +- Rotation/Viewport:未修改,继续作为技术债。 + +## Unrelated 文件保护 + +开始时已有的 `STEP-046-e48fa087-review.diff`、用户任务书、Android log、临时文件及其他 untracked 文件未修改、未 stage、未提交。 + +## Git 收尾 + +- implementation commit hash:`7c679f09a` (`feat: add production-like particle field feed`). +- commit message:`feat: add production-like particle field feed` +- push result:SUCCESS;`7c679f09a` 与文档收尾提交均已推送。 +- remote branch:`origin/particle/main` +- docs handoff commit:`762fbdf0d` (`docs: record STEP-051 implementation handoff`)。 +- Final HEAD(远端已验证):`762fbdf0d`。 + +## 当前状态与 Android 验收 + +当前状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。用户应在一次 Android build/install 后验证 production feed load、Timeline READY/PLAY、MIX、至少两个 Window Boundary、reuse/upload 计数、`simulationRebuild=false`、`trailCleared=false`、Pause/Resume、Duplicate NOOP、非法 local metadata fail-closed、Renderer 持续且不黑屏。Android 验收前不得写 CLOSED/PASS。 diff --git a/docs/报告/STEP-052_正式数据源接入下一阶段调查_Codex完成报告_v1.md b/docs/报告/STEP-052_正式数据源接入下一阶段调查_Codex完成报告_v1.md new file mode 100644 index 000000000..c9a92b921 --- /dev/null +++ b/docs/报告/STEP-052_正式数据源接入下一阶段调查_Codex完成报告_v1.md @@ -0,0 +1,72 @@ +# STEP-052 正式数据源接入下一阶段调查 — Codex 完成报告 + +## 基线 + +- Starting HEAD:`e4ab1b9d421a4559fa3627df07ee91dcd9b680e4` +- branch:`particle/main` +- origin/particle/main 起点:`e4ab1b9d421a4559fa3627df07ee91dcd9b680e4` +- 调查性质:docs-only、代码只读。 + +## 调查范围 + +实际检查了: + +- `PARTICLE_BASELINE.md`、`docs/00_项目规范/` +- `include/mbgl/particle/field/field_data_adapter.hpp` +- `src/mbgl/particle/field/field_data_adapter.cpp` +- `include/mbgl/particle/field/local_particle_field_feed.hpp` +- `src/mbgl/particle/field/local_particle_field_feed.cpp` +- `include/mbgl/style/sources/raster_array_source.hpp` +- `src/mbgl/style/sources/raster_array_source.cpp` +- `src/mbgl/style/sources/raster_array_source_impl.hpp` +- `src/mbgl/style/conversion/source.cpp` +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `src/mbgl/renderer/layers/render_raster_particle_layer.hpp` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` +- `platform/android/MapLibreAndroid/src/cpp/asset_manager_file_source.cpp` +- `platform/android/MapLibreAndroid/src/cpp/http_file_source.cpp` +- `include/mbgl/storage/file_source.hpp` +- `include/mbgl/storage/resource.hpp` +- `include/mbgl/storage/response.hpp` +- `include/mbgl/storage/database_file_source.hpp` +- `src/mbgl/storage/file_source_manager.cpp` +- `include/mbgl/util/lru_cache.hpp` +- `particle-production/*`、`particle-fixtures/*`、`test/style/source.test.cpp` + +## 调查结论摘要 + +STEP-051 已建立 `metadata + binary → Descriptor → Adapter → Snapshot → RasterArraySource → Timeline/Runtime` 的 local production-like 路径,但 payload 仍是既有 canonical fixture binary,Source 仍直接编排资源请求和 candidates。当前没有 Particle Weather Service、provider contract、forecast manifest、feed coordinator、专用 retry/stale policy 或 Particle transport cache。 + +MapLibre 通用 `FileSource`/HTTP/ambient cache 可以复用为底层资源设施,但不能视为 Weather Service 已接入。Timeline cache、Runtime candidate cache、FileSource/Database cache 和 GPU T0/T1 slots 是不同层次,当前不能合并。 + +## Recommended Next Implementation + +**STEP-053 Provider-neutral Field Feed / Manifest 与 Arrival Coordinator**。 + +先以 Local/File/Mock transport 冻结 manifest、ordered arrival、metadata/payload pairing、错误分类、last-valid hold 和 initial/subsequent submit;不需要真实 network、服务器或 provider。完成 Linux/Core/OpenGL 验证后,如确需 Android 观测,再合并一次 build/install。 + +建议顺序:STEP-053 feed/manifest → STEP-054 HTTP Transport 与 Weather Service Adapter → STEP-055 Field Cache、Retry、Stale 与 Preload Hardening。 + +## 架构判断 + +- 不需要推翻 STEP-049/051 contract。 +- 不建议下一步直接接 Weather Service;中间缺少 feed lifecycle、manifest、错误/重试/stale 语义和 provider-neutral transport boundary。 +- STEP-053 不应修改 Timeline state machine、Runtime Field Window、Renderer、WeatherColorLayer 或 Android Timeline ownership。 +- Validation UI MIX 滞后继续为 `NON-BLOCKING observability`;Rotation/Viewport 继续为独立技术债。 + +## 输出与 Git + +- 调查报告:`docs/20_调查/STEP-052_正式数据源接入下一阶段调查报告_v1.md` +- 完成报告:本文件 +- 是否修改功能代码:否。 +- 是否执行 Android Gradle/APK/ADB:否。 +- 是否执行 Jest:否。 +- `git diff --check`:PASS。 +- commit hash:`979a1deeb4e6abe24c910c9f17248fd5c6c0aff8`。 +- commit message:`docs: investigate next production field source stage` +- push result:SUCCESS,已推送至 `origin/particle/main`。 +- remote branch:`origin/particle/main` +- Final HEAD(调查文档提交):`979a1deeb4e6abe24c910c9f17248fd5c6c0aff8`。 +- 无关 untracked 文件:保持未修改、未 stage、未提交。 + +阻塞项:无调查阻塞;STEP-053 实现前需冻结 manifest/arrival API 与错误生命周期,避免把 Source 扩展成 Weather Service 或 cache orchestrator。 diff --git a/docs/报告/STEP-053_Android_Validation_Host接线补充_Codex完成报告_v1.md b/docs/报告/STEP-053_Android_Validation_Host接线补充_Codex完成报告_v1.md new file mode 100644 index 000000000..c23258ddb --- /dev/null +++ b/docs/报告/STEP-053_Android_Validation_Host接线补充_Codex完成报告_v1.md @@ -0,0 +1,57 @@ +# STEP-053 Android Validation Host 接线补充 Codex 完成报告 + +## 基线与问题确认 + +- Starting HEAD:`b31f80c72d46a160f576b312f5d9aeebfa225a5e` +- 分支:`particle/main` +- 当前状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION` +- 本任务只补充 Android Validation Host 的可见性与低频观测,不重新实现 STEP-053 Core。 + +STEP-053 的 `RasterArraySource::submitHostField()`、Android `MapLibreMap.submitRasterArraySourceHostField()`、JNI forwarder 和三个隐藏 Validation controls 已在起点存在。MORE 菜单却把主入口显示为 `HOST FIELD SUBMIT`、不紧邻 `PRODUCTION FEED`,并且 Kotlin 在读取 local canonical dataset 后没有独立 dataset-load 日志。因此用户当前只能确认 STEP-051 `PRODUCTION FEED`,难以明确选择并观察 STEP-053 Host submission 路径。 + +## 实际修改 + +- `ParticleCoreLayerValidationActivity.kt` + - MORE 相关顺序调整为 `PRODUCTION FEED → HOST FIELD → INVALID PROD → HOST DUPLICATE → HOST INVALID`;STEP-051 的 `PRODUCTION FEED` 与 `INVALID PROD` 行为保持不变。 + - 主入口名称改为 `HOST FIELD`;duplicate/invalid 名称明确为 `HOST DUPLICATE`、`HOST INVALID`。 + - 每次读取 T0~T3 resource 后输出低频 `HOST_FIELD_DATASET_LOAD`,含 metadata resource、payload resource 与 payloadBytes。既有 Native `HOST_FIELD_SNAPSHOT_READY`、`HOST_FIELD_INITIAL_WINDOW_READY`、`HOST_FIELD_NEXT_SUBMIT` 继续提供 fieldId、generation、validTime 与 result。 +- `docs/50_Step记录/STEP-053_Windy-style_Canonical_Field与Host_MapLibre_Submission_Contract_v1.md` +- `粒子开发记录.md` + +实际调用路径未变: + +`HOST FIELD → host-field-submission raster-array source → Kotlin local metadata + binary bytes → MapLibreMap.submitRasterArraySourceHostField() → NativeMapView JNI → RasterArraySource::submitHostField() → ParticleFieldDataAdapter → ParticleFieldSnapshot → existing Timeline / Runtime Field Window / Renderer`。 + +没有新增 JNI forwarding,也没有修改 Core、Adapter、Timeline、Playback、Runtime Field Window、Renderer、GPU simulation、Trail、WeatherColorLayer、network 或 provider 逻辑。 + +`HOST DUPLICATE` 重交当前 Host dataset 的 T3,走同一 submit API,预期 `DUPLICATE_NOOP`。`HOST INVALID` 在有效 Host window 存在时提交既有 `field-invalid.json` 和 bytes,走同一 API 的 Adapter reject/fail-closed 分支,不通过 style reload 冒充 invalid Host submission。 + +## Linux 与静态验证 + +- `cmake --build build-linux-opengl --target mbgl-test-runner mbgl-render mbgl-render-test-runner -j3`:PASS(无 C++ 变更,targets 已最新)。 +- `./build-linux-opengl/mbgl-test-runner --gtest_filter='Source.*:Layer.RasterParticleCountProperties:Layer.RasterParticleVisualProperties'`:PASS,46/46。 +- Host API/JNI declaration、registration、Activity action/resource ID 与两个 Host submit 调用静态检查:PASS。 +- `git diff --check`:PASS。 +- Android Gradle、APK build/install、ADB、App 启动和设备测试:未执行。 +- Jest:未新增、未修改、未执行。 + +## 用户 Android 验收 + +用户在 Windows 11 构建并安装一次新 APK 后: + +1. 打开 `Particle Core Layer Validation → MORE → HOST FIELD`。 +2. 确认 `HOST_FIELD_DATASET_LOAD`、`HOST_FIELD_SUBMIT_REQUEST`、`HOST_FIELD_SNAPSHOT_READY`、`HOST_FIELD_INITIAL_WINDOW_READY`、`HOST_FIELD_NEXT_SUBMIT` 及 `ParticleTimeline: event=READY`。 +3. 执行 `TIME → PLAY / RESTART`,验证 MIX、至少两次 boundary、`OLD_T1 → NEW_T0` reuse、only NEW_T1 upload、`simulationRebuild=false` 和 `trailCleared=false`。 +4. 依次执行 `HOST DUPLICATE`、`HOST INVALID`,确认 NOOP 以及 Adapter reject/fail-closed、last valid Field/Renderer continuity、无黑屏、无 crash。 + +如采集日志,设备命令必须显式指定:`adb -s 192.168.1.146 logcat`。 + +## Git 与范围保护 + +- 功能与 Step 文档 commit:`4b453a109b6c861996b9970b03f6582a4c083b0e` +- commit message:`fix: expose STEP-053 host field validation path` +- 本报告提交及 push result 在 Git 收尾后记录于交付结果;remote branch:`origin/particle/main`。 +- Final HEAD(功能与 Step 文档提交):`4b453a109b6c861996b9970b03f6582a4c083b0e` +- 无关 untracked 文件未修改、未 stage、未提交;包括 `STEP-046-e48fa087-review.diff` 与既有任务文档。 + +阻塞项:无 Linux/代码阻塞。STEP-053 仍须用户完成 Android 综合验收,不能写为 `CLOSED / PASS`。 diff --git a/docs/报告/STEP-053_Windy-style_Field与Host_Submission_Codex完成报告_v1.md b/docs/报告/STEP-053_Windy-style_Field与Host_Submission_Codex完成报告_v1.md new file mode 100644 index 000000000..dbef43701 --- /dev/null +++ b/docs/报告/STEP-053_Windy-style_Field与Host_Submission_Codex完成报告_v1.md @@ -0,0 +1,137 @@ +# STEP-053 Windy-style Field 与 Host Submission Codex 完成报告 + +## 1. 基线与状态 + +- Starting HEAD:`285d23a58f6ecb16f360748df688d9d03dad6b3d` +- 分支:`particle/main` +- STEP-051:`CLOSED / PASS` +- STEP-052:调查完成;STEP-053 按任务书修正了 Weather/Manifest 所有权,MapLibre 仅从 Host 已规范化 Field 开始负责。 +- 当前状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION` + +## 2. 最终实际设计 + +新增唯一低频入口 `RasterArraySource::submitHostField(metadataText, payload)`。Host 每次提交一个 `maplibre-particle-field/v1` metadata 和一段 contiguous binary U/V payload。入口复用 `LocalParticleFieldFeed`、`ParticleFieldDataAdapter` 和 immutable `ParticleFieldSnapshot`,再路由到既有 initial T0/T1 与同一个 Core-owned Timeline。 + +没有新增 Descriptor 或 Snapshot,因为 STEP-049 的结构已经表达 identity、generation、validTime、grid geometry、bounds、layout、encoding、noData、byte length、dataVersion 和可选 checksum。也没有建立第二套 Adapter、Timeline、Window 或 cache。 + +Canonical layout 为 north-west origin、row-major、north-to-south rows、west-to-east columns,`float32-le interleaved-uv`:U=eastward-positive,V=northward-positive。大 Field 走二进制 bytes,不走巨大 JSON/JS number array。metadata 可以是小型 JSON;STEP-054 可把 ArrayBuffer/TypedArray/native buffer/file reference 映射到同一 Native bytes boundary。 + +## 3. API 与路径 + +### Core/C++ + +- `RasterArraySource(std::string id)`:创建空 Host ingress Source。 +- `RasterArraySource::submitHostField(metadata, payload)`:单 Field 正式提交。 +- `RasterArrayHostFieldSubmitResult`:区分 pending initial、initial ready、next accepted、duplicate,以及 source mode/adapter/incompatible/non-monotonic/capacity rejection。 +- `RasterArrayTimelineRuntime::append()`:仅追加低频、严格递增、capacity-bound Snapshot;不改变 Playback 状态机所有权。 +- style validation seam:`{"type":"raster-array","host-field-submission":true}`。 + +### Android/Validation + +- `MapLibreMap.submitRasterArraySourceHostField(String, String, byte[])`。 +- Java `NativeMap`/`NativeMapView` → JNI `NativeMapView` → `RasterArraySource::submitHostField()`。 +- API 返回与 C++ structured result 对齐的整数常量;JNI 只在一次 Field arrival 时复制 contiguous bytes,不做 per-frame JNI。 +- MORE 菜单新增 `HOST FIELD SUBMIT`、`HOST DUPLICATE`、`INVALID HOST`。 + +### Initial / Next / Duplicate / Invalid + +- Field #0:pending T0。 +- Field #1:复用 `submitInitialRuntimeFieldWindow()`,建立 T0/T1 和 Timeline READY。 +- Field #2/#3:append 到同一个 Timeline,继续由既有 renderer window revision 路径消费。 +- Duplicate:`DUPLICATE_NOOP`,Timeline size 与 active field 不变。 +- Invalid/unsupported/non-monotonic:结构化拒绝,最后有效 Snapshot/Timeline frame 不变,符合 fail-closed。 + +## 4. 架构影响 + +- Public/internal API:新增 Core Host submission API、Android MapLibreMap binary submission API、Host Source style validation flag;Timeline 增加内部有界 append/read helpers。 +- Adapter contract:未放宽、未复制;仍只接受合法 `interleaved-uv/float32-le`。 +- Runtime Field Window:未重写,`OLD_T1 → NEW_T0` 与 only NEW_T1 upload 不变。 +- Timeline/Playback:状态机、clock、cursor、speed、pause/gap/invalid ownership 未改变。 +- Renderer/GPU/Trail:未修改;不含 provider/network/vendor/manifest/cache 业务。 +- WeatherColorLayer:未修改,继续与 RasterParticleLayer 分离。 +- Network/Provider/Weather Service/React Native:未实现。 + +## 5. 测试与结果 + +### 构建 + +- `cmake --build build-linux-opengl --target mbgl-test-runner -j3`:PASS。 +- `cmake --build build-linux-opengl --target mbgl-render mbgl-render-test-runner -j3`:PASS。 + +### 定向测试 + +- `Source.CanonicalFieldUsesEastwardUAndNorthwardV` +- `Source.HostFieldSubmissionBuildsOneCoreOwnedTimeline` +- `Source.HostFieldSubmissionStyleContractCreatesEmptyIngress` +- `Source.HostFieldSubmissionRejectsInvalidWithoutChangingLastValidField` + +结果:4/4 PASS。覆盖 east U / north V、vortex corner orientation、initial T0/T1、T2/T3、strict validTime/generation、Duplicate、invalid identity/dimensions、wrong byte length、unsupported layout/encoding、non-monotonic 和 last-valid hold。 + +- `Source.*`:44/44 PASS,包含既有 Adapter、STEP-051 local feed、Timeline READY/Playback、Runtime Window/reuse/NOOP 回归。 +- RasterParticle property 回归:2/2 PASS。 +- JNI signature/registration、Host style property、Validation action resource ID:静态核对 PASS。 +- `xmllint`:环境未安装,未执行。 +- checksum mismatch:Host contract 当前未启用 checksum 校验,按任务书条件未伪造测试结果。 +- `git diff --check`:PASS。 + +最初新增 style conversion 测试使用 `dynamic_cast`,精确编译暴露项目 `-fno-rtti` 约束;已改为项目正式 `Source::as()`,后续构建/测试通过,无遗留阻塞。 + +## 6. 未执行项 + +- Android Gradle:未执行。 +- APK build/copy/install:未执行。 +- ADB/App 启动/设备测试:未执行。 +- Jest:未新增、未修改、未执行。 +- 用户正式 canonical dataset:尚未提供,未声称已验证。 + +## 7. 用户 Android 手工验收 + +1. 在 Windows 11 构建并安装新的 Android OpenGL Debug APK。 +2. 打开 `Particle Core Layer Validation → MORE → HOST FIELD SUBMIT`。 +3. 确认日志依次出现 `HOST_FIELD_SUBMIT_REQUEST`、`HOST_FIELD_SNAPSHOT_READY`、`HOST_FIELD_INITIAL_WINDOW_READY`、`HOST_FIELD_NEXT_SUBMIT` 与 `ParticleTimeline: event=READY`。 +4. 进入 TIME,执行 `PLAY / RESTART`,观察 east/north/vortex 变化、自动 MIX 和至少两个 Window Boundary。 +5. 确认 `reuse=OLD_T1_TO_NEW_T0`、only NEW_T1 upload、`simulationRebuild=false`、`trailCleared=false`。 +6. 执行 `MORE → HOST DUPLICATE`,确认 `DUPLICATE_NOOP` 且 upload/reset 不增长。 +7. 执行 `MORE → INVALID HOST`,确认 Adapter reject、最后有效 Field 保持、Renderer 连续、无黑屏、无 crash。 + +日志采集必须显式指定设备,例如: + +```bat +adb -s 192.168.1.146 logcat | findstr /I "HOST_FIELD ParticleTimeline WINDOW_BOUNDARY FIELD_REUSE FIELD_UPLOAD DUPLICATE INVALID simulationRebuild trailCleared" +``` + +## 8. 文件与 Git + +实际代码/测试修改文件: + +- `include/mbgl/particle/field/field_data_adapter.hpp` +- `include/mbgl/style/sources/raster_array_source.hpp` +- `src/mbgl/style/sources/raster_array_source.cpp` +- `src/mbgl/style/sources/raster_array_source_impl.hpp` +- `src/mbgl/style/conversion/source.cpp` +- `test/style/source.test.cpp` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java` +- `platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp` +- `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` +- `platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_particle_core_layer_validation.xml` + +文档修改:本 Step、`粒子开发记录.md` 和本报告。 + +- commit hash:`1e8eca79125e36284d8893e3134203d3f5289ce7`。 +- commit message:`feat: add host particle field submission contract` +- push result:SUCCESS,已推送至 `origin/particle/main`。 +- remote branch:`origin/particle/main` +- Final HEAD(功能实现与验收文档提交):`1e8eca79125e36284d8893e3134203d3f5289ce7`。 +- 无关 untracked 文件:保持未修改、未 stage、未提交;包括历史 diff、任务书、临时文档等。 + +## 9. Remaining risks + +- Android JNI/Kotlin/visual 行为尚待用户同一 APK 手工验证。 +- 当前 validation dataset 复用 STEP-051 T0~T3;用户指定正式数据尚未提供。 +- Host metadata checksum 尚未启用;若正式要求 integrity,需后续以同一 Descriptor 字段实现,不另建 contract。 +- Validation UI MIX/WAITING/INVALID observability 与 Rotation/Viewport 继续是 NON-BLOCKING 技术债。 + +阻塞项:无代码或 Linux 验证阻塞。当前不能写 `CLOSED / PASS`。 diff --git a/docs/报告/STEP-053_Windy-style_Field与Host_Submission_Codex完成报告_v2.md b/docs/报告/STEP-053_Windy-style_Field与Host_Submission_Codex完成报告_v2.md new file mode 100644 index 000000000..f7a0f59b4 --- /dev/null +++ b/docs/报告/STEP-053_Windy-style_Field与Host_Submission_Codex完成报告_v2.md @@ -0,0 +1,67 @@ +# STEP-053 Windy-style Field 与 Host Submission 最终完成报告 + +## 最终状态 + +`STEP-053 = CLOSED / PASS` + +- Starting HEAD:`c188d6b59006c3010bd80676c2f021795535fdda` +- 分支:`particle/main` +- 本任务性质:纯文档最终收尾。 +- 功能代码修改:否。 + +Android Host Submission 综合验收已由用户手工完成。本报告不将用户正式风场数据集未提供误写成失败:STEP-053 的目标是 canonical Host→MapLibre submission contract,该 contract 已由本地 canonical dataset 驱动实际 Core、Runtime 与 GPU Renderer 验证通过。 + +## Android 技术验收 + +| 验收项 | 结果 | +| --- | --- | +| `HOST FIELD` Host Submission Contract | PASS | +| 与 STEP-051 `PRODUCTION FEED` 路径分离 | PASS | +| `production-wind-t00` ~ `production-wind-t03` canonical dataset load | PASS | +| Host Submission → `ParticleFieldDataAdapter` → `ParticleFieldSnapshot` → Timeline → Runtime Field Window → GPU Renderer | PASS | +| T0/T1 Initial Window | PASS | +| T2/T3 Next Submission | PASS | +| `PLAYBACK_PLAY` | PASS | +| Automatic MIX | PASS | +| Window Boundary #1 | PASS | +| Window Boundary #2 | PASS | +| `OLD_T1 → NEW_T0` reuse | PASS | +| only NEW_T1 upload | PASS | +| normal `simulationRebuild=false` | PASS | +| normal `trailCleared=false` | PASS | +| `HOST DUPLICATE` / `DUPLICATE_NOOP` | PASS | +| `HOST INVALID` / Adapter fail-closed / last valid retained | PASS | +| Renderer continuity (`ParticleRender stateSeed=CONTINUE`) | PASS | +| black screen / crash | 未发生 | + +两次已确认的 production→production boundary 为: + +1. `production-wind-t00 / production-wind-t01 → production-wind-t01 / production-wind-t02` +2. `production-wind-t01 / production-wind-t02 → production-wind-t02 / production-wind-t03` + +每次均复用 OLD_T1 为 NEW_T0,并仅上传 NEW_T1。正常过程 `fieldUploadCount=4 → 5 → 6`。`simulationResetCount=1` 只对应旧 fixture `uniform-east / uniform-east` 首次切换至 Host `production-wind-t00 / production-wind-t01` 的允许初始化;该一次可解释的 `simulationRebuild=true` / `trailCleared=true` 不属于 production→production boundary 回归。 + +Duplicate 时 field upload 与 simulation reset 均无增量。Invalid 测试通过正式 Host submission 触发 Adapter reject,不依赖 style reload;last valid field 被保留,Renderer 持续运行。 + +## 数据集与技术债 + +- 用户正式风场数据集:`NOT PROVIDED / NOT VALIDATED`。 +- 该数据集可在后续使用相同 canonical contract 作替换/追加验证,不阻止 STEP-053 关闭。 +- Validation UI observability:`NON-BLOCKING`。MIX 文本可能滞后,WAITING/INVALID 视觉不够明显;这不是 Core、Timeline、Runtime 或 Renderer 缺陷,本任务未修复。 + +冻结边界保持:Core-owned Timeline/Playback、Runtime Field Window、`OLD_T1 → NEW_T0` reuse、only NEW_T1 upload、MIX property-only、Invalid/Gap fail-closed、Renderer 不含 provider/network/vendor 逻辑,以及 WeatherColorLayer 与 RasterParticleLayer 分离。 + +## 未执行项 + +Codex 本任务未执行 Android Gradle、APK build/install、ADB、App 启动、Android 设备/模拟器测试或 Jest;Android 验收由用户手工完成。未修改功能代码,未新增测试。 + +## Git 与范围保护 + +- closeout commit:`e98805524bfbef396deea4a6d41e70e900c794f3` +- commit message:`docs: close STEP-053 Android validation` +- remote branch:`origin/particle/main` +- push result、Final HEAD:由本报告提交后的 Git 收尾确认。 +- `git diff --check`:PASS。 +- 无关 untracked 文件未修改、未 stage、未提交,包括 `STEP-046-e48fa087-review.diff`、历史任务文档和其他用户文件。 + +阻塞项:无。STEP-053 已正式关闭;不进入 STEP-054。 diff --git a/docs/报告/STEP-054_真实WeatherV2风数据提取与Canonical测试数据生成_Codex完成报告_v1.md b/docs/报告/STEP-054_真实WeatherV2风数据提取与Canonical测试数据生成_Codex完成报告_v1.md new file mode 100644 index 000000000..b771bcc39 --- /dev/null +++ b/docs/报告/STEP-054_真实WeatherV2风数据提取与Canonical测试数据生成_Codex完成报告_v1.md @@ -0,0 +1,64 @@ +# STEP-054 真实 WeatherV2 风数据提取与 Canonical 测试数据生成 Codex 完成报告 + +## 完成状态 + +本任务已完成 WeatherV2 真实风场到 canonical Android 测试 asset 的离线转换。当前状态为:`IMPLEMENTED / WAITING_NEXT_ANDROID_INTEGRATION_VALIDATION`。 + +- Starting HEAD:`5fc922785d8ec007127d48cd3ab5dadce270b947` +- 分支:`particle/main` +- WeatherV2 源目录(只读):`/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-hres/wind/2026080600` +- 实际 validTime:`2026080700`、`2026080706`、`2026080712`、`2026080718` + +## 已查明的 WeatherV2 格式与语义 + +以用户提供的《NavSea 天气风数据取数与使用报告》作为 QBIN/NSFT 语义背景,并实际检查 WeatherV2 `src/dev/core/bin_export.py`、`src/dev/layers/wind.py`、`src/dev/core/qbin_export.py`、`src/dev/core/qbin.py`、`src/dev/core/field_pmtiles.py` 和 `src/dev/core/nsft.py`。 + +`wind_vectors.bin` 是 `weather-v2-particle-bin-v1` 的完整拼接 export,不是 QBIN/NSFT: + +`WVECBIN1` → `uint32-le header length` → JSON header → U float32-le plane → V float32-le plane → speed float32-le plane → valid uint8 plane。 + +实际 grid 为 `513×769`,row-major,bounds 为 west `112.5`、south `0.0`、east `157.5`、north `55.77657301866769`,单帧源文件为 `5,128,989` bytes。Web Mercator tile y 向南增加,故 row 是 north-to-south、column 是 west-to-east。 + +`decode_wind()` 将 decoded R 作为 U、decoded G 作为 V;writer metadata 说明 U=eastward-positive、V=northward-positive。QBIN `u_q/v_q` 和 NSFT 均从同一 decoded Field 生成;QBIN metadata 明确 `m/s`、`scale=0.01`。因此 WVECBIN1 的本次 U/V 单位确认是 m/s,没有猜测或进行 kt 换算。valid/noData 也同源:源 valid plane 为 uint8;canonical 将 invalid/非有限 U/V 转为现有 `noData=-9999.0` pair,绝不写作静风。 + +## 输出与一致性 + +新增独立开发工具 `tools/weatherv2_wind_canonical/convert_weatherv2_wind.py` 和中文 README;工具只读取输入,不依赖 Android Runtime、不引入网络/provider 逻辑。输出位于: + +`platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-weatherv2/ecmwf-hres-wind-20260807/` + +四帧均保留完整实际范围,不裁剪、不重采样。每个 `field.f32` 是现有 `float32-le interleaved-uv`,`3,155,976` bytes;metadata 复用 `maplibre-particle-field/v1`,validTime 为真实 UTC epoch milliseconds,generation/dataVersion 为 producer-owned、严格递增的同值。`dataset.json` 只是一份开发期 asset 索引,不是 Weather Manifest。 + +| validTime | SHA-256 | valid / invalid | source→canonical max U/V error | +| --- | --- | --- | --- | +| 2026080700 | `363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283` | 394497 / 0 | 0 / 0 | +| 2026080706 | `ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451` | 394497 / 0 | 0 / 0 | +| 2026080712 | `d86a6a51fd5216cf9368756962a5366fd32a627afd40cfde235ba7358c02c5a0` | 394497 / 0 | 0 / 0 | +| 2026080718 | `970785b0334a9d1511b8897874a81f681814a32b1e0b90e37cf64b77ca9fa39b` | 394497 / 0 | 0 / 0 | + +四个 payload hash 均不同,避免了误复制同一帧;相同输入重复转换 T0 的 SHA-256 相同,确认 deterministic。 + +## 验证结果 + +- 转换器 Python syntax、四时次成功转换、metadata JSON、binary length、validTime 严格递增、hash 一致性与四帧差异:PASS。 +- 8 个确定性抽样点(中心、角附近、内部)与全量有效数组的无损映射检查:PASS,最大 U/V 误差均为 0。 +- 无效 source directory:明确报错并非零退出:PASS。 +- `cmake --build build-linux-opengl --target mbgl-test-runner -j3`:PASS(无 C++ 改动,target 已最新)。 +- 既有 `ParticleFieldDataAdapter`、Local production feed、canonical U/V、Host submission/invalid 定向测试:5/5 PASS。 +- `git diff --check`:PASS。 + +## 范围与未执行项 + +未修改 WeatherV2 原始数据、MapLibre Core、Descriptor/Snapshot/Adapter、Timeline、Runtime Field Window、Renderer、Android Kotlin/JNI。没有把 Weather Service、HTTP、Provider、QBIN/NSFT download、缓存或目录扫描加入 MapLibre Runtime。 + +未执行 Android Gradle、APK build/install、ADB、App/设备/模拟器测试或 Jest。真实 asset 的 MapLibre map + WeatherColorLayer + RasterParticleLayer + playback Android 综合验收留给下一独立任务;本任务不进入 React Native Binding 或 UI 改造。 + +## Git 信息 + +- 实现 commit:`79d4aa6385275662bd892100bf282d6903f9fdc0` +- commit message:`feat: add WeatherV2 wind canonical converter` +- remote branch:`origin/particle/main` +- push result、Final HEAD:在本报告提交后的 Git 收尾确认。 +- 无关 untracked 文件未修改、未 stage、未提交;包括用户任务书、指定的天气报告与历史 `STEP-046-e48fa087-review.diff`。 + +阻塞项:无。下一步只应在单独阶段消费本 canonical asset 完成 Android 综合可视化验证。 diff --git a/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v1.md b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v1.md new file mode 100644 index 000000000..48f63a8fa --- /dev/null +++ b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v1.md @@ -0,0 +1,55 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 Codex 完成报告 + +## 基线与范围 + +- Starting HEAD:`da71c8784ec20ed830f5d23880718647d666cf2f` +- 分支:`particle/main` +- 状态:IMPLEMENTED / WAITING_ANDROID_VALIDATION +- 输入:STEP-054 已提交的真实 WeatherV2 ECMWF HRES wind canonical T0–T3(513×769,原始 validTime,U=eastward、V=northward、m/s)。本阶段没有重新生成、重采样或修改这些数据。 + +本任务实现 Android Validation 的真实场综合可视化,不引入 Weather Service、HTTP、Provider、下载、缓存、React Native 或 Jest。 + +## 实际修改 + +- 新增 provider-neutral、独立的 `weather-color` Style Layer、Factory 和 OpenGL RenderLayer。它从现有 `RasterArraySource` 的 canonical snapshot 读 T0/T1;不含 provider/network/business logic,也不与 `RasterParticleLayer` 合并。 +- `WeatherColorLayer` 颜色逻辑为:还原 canonical U/V → Core mix 插值 → `hypot(U,V)`;采用固定 0–30 m/s 色标和固定 opacity。 +- 更新粒子 GPU 字段采样:真实 metadata bounds 转为 Web Mercator UV;字段区域外 fail-closed,粒子 seed/恢复受相同 bounds 约束。为真实 m/s 引入 texture decode scale,避免旧 `[-1,1]` 编码饱和。 +- Android `MORE → REAL WEATHER` 一键加载 STEP-054 的 T0–T3,通过既有 Host Submission Contract submit;使用既有 OSM 无 Key raster basemap,自动 fit real bounds,并以 basemap → color → particle → HUD 排序。 +- 保存 REAL WEATHER / Host mode 状态;Activity 重建后自动以相同 metadata/payload/validTime 恢复可见内容。现有 RasterParticle viewport trail FBO resize 路径保持负责其 GPU target 重建。 +- 新增 `WeatherColorUsesIndependentFixedOpacity` Core style test;新增 Android V1–V8 人工验收指令。 + +## 架构保护 + +未修改 `ParticleFieldDescriptor`、`ParticleFieldSnapshot`、`ParticleFieldDataAdapter`、Host submission contract、Timeline state machine、Playback ownership、Runtime Field Window、Duplicate NOOP、Gap/Invalid fail-closed 或 Renderer provider/network 边界。 + +`RasterParticleLayer` 仍是唯一 tick Timeline 的 render consumer。WeatherColorLayer 只读取同一 Source-owned frame,故不建立第二时钟/Timeline。正常 Boundary 的 `OLD_T1 → NEW_T0 reuse` 和 `only NEW_T1 upload` 继续适用;颜色层也复用旧 T1 GPU texture。 + +## 验证 + +- CMake OpenGL 配置:PASS。 +- 受影响 C++ 精确编译:WeatherColor Layer、Factory、RenderLayer、RasterParticle shader-side renderer、默认 LayerManager:PASS(同一 CMake OpenGL 编译参数)。 +- 使用同一 CMake OpenGL 配置的 `mbgl-core` 全量目标已启动并确认既有依赖/前序 Core 可编译;按任务“不默认跑无关全量测试”的边界,在进入与 STEP-055 无关的大量 Core translation units 后停止。它不构成本阶段的验收条件。 +- 新增 `test/style/style_layer.test.cpp` 和受影响 `test/style/source.test.cpp`:以同一 CMake 参数精确编译 PASS;未构建/执行无关的完整 test runner。 +- `git diff --check`:待最终文档完成后执行。 +- Android Gradle / APK build / install / ADB / App launch / device test:未执行(用户手工)。 +- Jest:未执行,未修改。 + +## Android 待验收 + +按 `docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md` 执行一次构建/安装,重点确认:真实 T0–T3、色场/粒子地理对齐、PLAY/MIX、两次 Boundary、reuse/only NEW_T1、Duplicate/Invalid、旋转恢复以及无黑屏/crash。 + +真实 validTime 保持 6 小时间隔;当前没有另加 Android 时间压缩。可用既有 2x 或 STEP NEXT 观察边界。 + +## 非阻塞项 + +Validation UI MIX 文本滞后及 WAITING/INVALID 的视觉不够明显仍为 NON-BLOCKING observability,不被表述为 Core、Timeline 或 Renderer 故障。 + +## Git 收尾 + +- Implementation commit hash:`52600cd3bed8db85dd14b386270f1e617b9f8ba0` +- Commit message:`feat: add real WeatherV2 Android composite validation` +- Push result:成功,`origin/particle/main` 已从 `da71c878…` 更新至上述实现提交。 +- Remote branch:`origin/particle/main` +- Implementation Final HEAD:`52600cd3bed8db85dd14b386270f1e617b9f8ba0`(本报告的 Git 元数据更新将作为仅文档后续提交)。 + +无关 untracked 文件(历史 diff、任务书、日志等)将保持未触碰、未 stage。 diff --git a/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v2.md b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v2.md new file mode 100644 index 000000000..4c2de8e7f --- /dev/null +++ b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v2.md @@ -0,0 +1,51 @@ +# STEP-055 真实 WeatherV2 风场综合可视化修复 Codex 完成报告 v2 + +## 基线与结果 + +- Starting HEAD:`8a0873a5fb2c8867c7e2a126b4d4c185077dd6f9` +- 分支:`particle/main` +- 状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** +- 本轮针对首轮 Android 实机视觉问题修复;Android Gradle、APK、ADB、App/设备测试与 Jest 均未执行。 + +## 已完成 + +- REAL WEATHER basemap 改为 GSI 标准地图,并以 `basemap=GSI failSoft=true` 记录独立底图配置;不引入 key、商业 SDK 或天气网络。 +- REAL WEATHER 现在是 MAP 旁的一级菜单,MORE 不再重复。首次点击提交现有真实 T0–T3 Host dataset、fit bounds 并打开 tuning;再次点击只切换菜单。退出真实场景恢复进入前的 synthetic visual state。 +- 真实场默认 P=2048、W=0.75、L=0.0040、O=0.75、T=0.55、F=0.88;HUD 和 `PARTICLE TUNING` 使用同一状态。离散低频调参/RESET 不重载 Style、不重提 Field、不重置 Timeline 或 Runtime Window;只有 P 允许 State/draw resource recreate。 +- Core 粒子数支持明确的 validation presets 256/512/1024/2048/4096/8192/16384(并保留既有高档位);State grid 改为 `ceil(sqrt(count))`,剩余 texels不绘制。lineWidth/streakLength 下限分别为 0.25/0.0005。 + +## m/s 位移结论 + +此前 State shader 使用 `position += velocity * frame * 0.00004`:它没有 m/s、秒、地球尺度或纬度解释,真实数据虽能推进 A/B,但无法审计真实地理位移,粗宽 trail 也掩盖小位移。 + +现链路保持 canonical U=eastward、V=northward、单位 m/s:wall delta 限制为 1/240–0.05s,作为明确视觉仿真时钟乘 6000 simulation-seconds/real-second;shader 使用 `dx=U*dt/40075016.6856` 与 `dy=-V*dt/(40075016.6856*cos(latitude))` 写入 Web Mercator world state。6000 是 rendering-only time dilation,用于在日本 validation zoom 下将真实 m/s 的亚像素 real-time displacement 显示成可观察流线;它不修改 payload、Adapter、Snapshot、validTime、MIX 或 Timeline playback。低频 `ParticleMotion` 仅报告 CPU field sample 与同公式估算 delta,无 GPU→CPU readback。 + +Pause 仍只冻结 forecast Timeline;Particle State update 保持 active,因此固定风场下粒子继续运动。 + +## 架构保护与未做项 + +未修改 STEP-054 WeatherV2 assets、ParticleFieldDescriptor/Snapshot/DataAdapter、Host Submission contract、Timeline/Playback、Runtime Field Window 或 WeatherColorLayer 数据链。继续保持 A/B→B/C→C/D、OLD_T1→NEW_T0 reuse、only NEW_T1 upload、MIX property-only、正常 boundary `simulationRebuild=false`/`trailCleared=false`;未在 Renderer 引入 provider/network/vendor 逻辑,未接 Weather Service、React Native 或 Jest。 + +Rotation/Viewport 只纳入本轮人工验收,未扩大实现范围。MIX 文本滞后及 WAITING/INVALID 状态视觉仍为 NON-BLOCKING observability。 + +## Linux / 静态验证 + +- `node shaders/generate_shader_code.mjs`:PASS。 +- OpenGL CMake 受影响 Core TU 精确编译:`render_particle_validation_layer.cpp`、`raster_particle_layer.cpp`、`render_weather_color_layer.cpp`、`raster_array_source.cpp`:PASS。 +- `test/style/style_layer.test.cpp` 以同一 OpenGL CMake 参数精确编译:PASS(含 2048 preset 与 visual bounds 更新)。 +- `mbgl-test-runner` 构建已启动以复用现有 target,但在进入无关 Core translation units 前按本任务定向边界停止;不作为通过声明。 +- `git diff --check`:待文档完成后的最终检查。 + +## Android 后续一次验收 + +按 [人工验收指令 v2](../60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v2.md) 在 Windows 11 只做一次构建/install。重点检查 GSI、默认细密且明显运动的粒子、HUD/tuning/RESET、Pause 下 simulation、ParticleMotion、T0→T3 两次 boundary、Pan/Zoom/Rotate/Rotation、Duplicate、Invalid 和 Renderer continuity。 + +## Git 收尾 + +- 实施提交:`6c7886b0c372f9cb3c5b96c6fed385a676dd954a` +- 提交信息:`fix: tune real weather particle rendering` +- Push result:成功;`origin/particle/main` 从 `8a0873a5…` 前进到该实施提交。 +- Remote branch:`origin/particle/main` +- Final implementation HEAD:`6c7886b0c372f9cb3c5b96c6fed385a676dd954a`。本报告的 Git 元数据修订作为下一仅文档提交,不改变功能代码或本轮 Android 待验收状态。 + +无关未跟踪的历史 diff、任务书和日志保持未触碰、未 stage。 diff --git a/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v3.md b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v3.md new file mode 100644 index 000000000..b31e7345f --- /dev/null +++ b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v3.md @@ -0,0 +1,43 @@ +# STEP-055 真实 WeatherV2 风场 Particle State 纹理精度修复 Codex 完成报告 v3 + +## 基线、范围与根因 + +- Starting HEAD:`37c9aa31da1d2b3495c8802e6fd2f6b999562b06` +- 分支:`particle/main` +- 状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +真实 Weather State shader 已计算非零 m/s→Web Mercator 位移且 State A/B ping-pong 持续,但 State RG world coordinate 写回被共用的 RGBA8/UnsignedByte target 量化。`1/255≈0.00392 world` 大于 Android 已观测的 `0.000003–0.000008 world` 单步位移,故每步无法积累,是“simulation 在跑但粒子静止”的确定性根因。 + +## 修改 + +- `render_particle_validation_layer.cpp`:State A/B 初始创建与 particleCount replacement 均为 `TextureChannelDataType::Float`;Trail A/B 仍为 `UnsignedByte`。创建函数显式接收 data type/kind,并在 FBO 创建失败时 log error、拒绝 target,无 UByte fallback。 +- `render_target.hpp/.cpp`:新增最小 `RenderTarget::isRenderable()` forwarding,使 target 创建即可验证当前后端实际 FBO。 +- `particle_gpu_validation.fragment.glsl`:只修正 RG 为 Web Mercator world position 的注释;未改 m/s conversion 或 `visualSpeedScale=6000`。 +- 生成的 OpenGL shader header 同步更新。 + +Android OpenGL 既有实现已经支持 Float:`gl::Context::createRenderTarget → RenderTarget → gl::OffscreenTexture → Texture2D(RGBA, Float) → GL_RGBA32F/GL_FLOAT → glFramebufferTexture2D → checkFramebuffer()`。因此未修改 Android/OpenGL backend;若设备 FBO 不完整,现有 `isRenderable()` 返回失败,Particle target 记录具体 Float failure 并 fail-closed。 + +运行时日志包括每个成功 `CREATE_SUCCESS`,以及 State/Trail pair summary:`stateAType=Float stateBType=Float trailAType=UnsignedByte trailBType=UnsignedByte`。这基于已实际完成 backend FBO validation,而非仅打印请求值。 + +## 架构保护 + +未修改 canonical Field、U/V 语义、STEP-054 assets、Host submission、Timeline、Runtime Field Window、WeatherColorLayer 或 6000 visual speed scale。没有新增 GPU readback、网络、Provider、React Native、Jest 或 Android UI 变化。 + +## 验证 + +- `node shaders/generate_shader_code.mjs`:PASS;generated OpenGL header 与 fragment source 同步。 +- Linux OpenGL CMake 精确编译:`render_target.cpp`、`render_particle_validation_layer.cpp`、`gl/offscreen_texture.cpp`、`gl/texture2d.cpp`:PASS。 +- 现有非 Jest `test/style/style_layer.test.cpp`:以相同 OpenGL CMake 参数精确编译 PASS;本轮没有适合脱离实际 GL context 的 Float FBO 单元 runner,因此 FBO runtime 完整性保留 Android 实机检查。 +- `git diff --check`:PASS(最终提交前将再次执行)。 +- Android Gradle/APK/ADB/App/设备:未执行。 +- Jest:未执行,未修改。 + +人工验收说明:[v3](../60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v3.md)。第一门槛是 Timeline Pause 下 Float State 粒子连续运动;若仍不动,不调 6000,转入后续 GPU State 读回诊断。 + +## Git + +功能提交:`f0f0623e06a3237e3f667dccc1ee451b98e9be57`(`fix: use float particle state targets`)。 + +功能提交已成功 push 至 `origin/particle/main`。本报告的 Git 元数据补充将以独立文档提交完成;该文档提交不会改变功能实现。用户既有未跟踪文件未触碰、未 stage。 + +最终状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。 diff --git a/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v4.md b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v4.md new file mode 100644 index 000000000..543bef91b --- /dev/null +++ b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v4.md @@ -0,0 +1,41 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 Codex 完成报告 v4 + +Starting HEAD / remote:`97aa019f6ff07d1b91e17c46ad0e6e6c0cd339d5`,`origin/particle/main`。 + +## 根因与修复 + +当前代码确认:State RGBA Float 已工作,但 State 在越界/无效时回固定 seed,导致重复集中;B 是未充分使用的 age。particle vertex 又按 `position ± normalizedVelocity * streakLength` 生成固定 world 短线;Trail A/B 是 viewport-sized UByte texture,却由全球 world 坐标写入/按地图 world quad 采样。上述组合解释了密度衰减、边缘集中、缩放时粗短棍与闪烁。 + +State A/B 仍为 Float,Trail A/B 仍为 UnsignedByte。正式 State pass 现在以 B 保存 simulation age,以 A 保存“本帧 segment 是否可连接”标记;越界、invalid、near-zero velocity 或 age≥3600 simulation seconds 时,使用 particle UV 与旧位置的 GPU hash 在 Field bounds 内至多三次采样有效位置。重生帧 A=0,因此 draw 丢弃 `oldPosition → newPosition`;下一次正常 advection 后才画 segment。 + +Particle draw 现绑定 StateRead 与 StateWrite,直接画 `previousPosition → currentPosition`,并用当前 MapLibre `matrixForTile()` 投影到 Trail framebuffer。`streakLength` 保留公开 UI/API 兼容,但在正式真实天气路径不再主导 segment 长度。正式 Composite 改为单次 fullscreen view-space Trail 合成;相机 transform 改变时仅重建 Trail,记录 `CAMERA_RESET`,保留 State、Field、Timeline 与 Runtime Window。 + +旧 fade 是每 render frame `trail *= trailFade`。新公式是 `fadeFactor = pow(trailFade, wallDeltaSeconds * 60)`:UI 值保持“60 FPS 单帧保留率”,而 30/60/120 FPS 都按相同 wall-time 指数衰减。Timeline Pause 不影响该 wall-time fade。 + +visual clock 从 6000 降为 1200 simulation seconds/real second;典型 60 FPS 为 20 simulation seconds/frame。5 m/s 约 100 m/frame,在 z9 约 0.65 px;30 m/s 约 3.9 px,降低高风速跨帧跳跃与频繁越界风险,未改变 canonical m/s。 + +## 架构影响 + +未修改 canonical Field、WeatherV2 assets、Host submission、Timeline、Runtime Field Window 或 WeatherColorLayer。正常 forecast boundary 的 reuse、NEW_T1-only upload、`simulationRebuild=false`、`trailCleared=false` 保持;camera Trail reset 与 forecast boundary 的 Trail 语义明确分离。 + +修改文件: + +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `shaders/particle_gpu_validation.vertex.glsl` +- `shaders/particle_gpu_validation.fragment.glsl` +- `include/mbgl/shaders/gl/particle_gpu_validation.hpp`(generator 输出) + +## 验证 + +- `node shaders/generate_shader_code.mjs`:PASS。 +- Linux OpenGL CMake 精确编译 `render_particle_validation_layer.cpp`:PASS。 +- 既有非 Jest `style_layer.test.cpp` 精确编译:PASS。 +- `git diff --check`:PASS。 +- Android Gradle/APK/ADB/设备:未执行。 +- Jest:未执行。 + +## Git 与状态 + +功能提交:`4c84a791229aa9f11808460c5dbfcf96327fb780`(`fix: stabilize particle lifecycle and view-space trails`),已成功 push 至 `origin/particle/main`。本报告将以独立文档提交补充;用户已有未跟踪文件未修改、未 stage。 + +最终状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION**。 diff --git a/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v5.md b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v5.md new file mode 100644 index 000000000..5c44e137e --- /dev/null +++ b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v5.md @@ -0,0 +1,31 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 Codex 完成报告 v5 + +Starting HEAD / remote:`067bfdc38baa32d6e474a502d67bc0910d18e247`,`origin/particle/main`。 + +## 修改 + +zoom-aware 公式为 `effectiveVisualSpeedScale = clamp(1200 * 2^(9 - zoom), 300, 153600)`。最终 `simulationDeltaSeconds = min(clamp(wallDeltaSeconds, 1/240, 0.050) * effectiveScale, 2400)`。典型 60 FPS:z2 为 2400(由 cap 限制)、z5 为 320、z9 为 20 simulation seconds/frame。canonical m/s 和 Web Mercator displacement 公式未改。 + +State 生命周期重命名为 real-time 语义:base=12 real seconds,hash jitter 后 maxAge=9–15 seconds。State B 在正式路径累加 `wallDeltaSeconds`,而非 zoom-aware simulation delta;初始 hash stagger、普通 respawn age=0、无 GPU readback 均保持。 + +普通 pan/zoom/rotate/pitch 先触发一次 `ParticleTrail: event=CAMERA_INVALIDATE`,Trail accumulation 暂停但 texture 不销毁、State/Field/Timeline 不变;连续变化只更新时间戳。150ms 稳定后 `CAMERA_STABLE_RESUME`。实际 viewport size 变化才 `VIEWPORT_RESIZE` 并重分配 Trail texture。 + +修改文件: + +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `shaders/particle_gpu_validation.fragment.glsl` +- `include/mbgl/shaders/gl/particle_gpu_validation.hpp`(generator 输出) + +未修改 State Float、Trail UByte、previous→current segment、canonical、Timeline、Runtime Window、WeatherColorLayer、REAL WEATHER UI、WeatherV2 assets 或 converter。 + +## 验证 + +- shader generator:PASS。 +- Linux OpenGL 精确编译 `render_particle_validation_layer.cpp`:PASS。 +- 既有非 Jest `style_layer.test.cpp` 精确编译:PASS。 +- `git diff --check`:PASS。 +- Android Gradle/APK/ADB/设备及 Jest:未执行。 + +功能提交:`f926a3f00730bd298dcff964eb92da2ec7ef3bda`(`fix: stabilize particle visibility across zoom levels`),已成功 push 至 `origin/particle/main`。本报告随后以独立文档提交记录,不改变功能实现。 + +最终状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION**。 diff --git a/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v6.md b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v6.md new file mode 100644 index 000000000..572c7f6fb --- /dev/null +++ b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v6.md @@ -0,0 +1,35 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 Codex 完成报告 v6 + +Starting HEAD / remote:`d9a9c8ed57605abca86893ba7fb3b5265db3c0dc`,`origin/particle/main`。 + +本任务只调整 zoom 后的 viewport particle density;未修改功能范围外的数据链或 UI。 + +## 实现 + +`RenderRasterParticleLayer` 以当前 TransformState 的 viewport 四角取得 unwrapped Web Mercator AABB,按宽高各扩展 15%,然后与 canonical `fieldBounds` 求交。交集有效且 camera stable(沿用 150ms Trail debounce)时,该交集作为独立 `respawnBounds`。交集为空、无效或 extent 过小时,回退 full field bounds。 + +新增 UBO `respawnBounds`,并以 `fieldScale.z` 传递 `viewportDensityActive`。Field sampling、UV grid 映射和 U/V decode 始终使用 `fieldBounds`;viewport bounds 不参与 Field 采样。State shader 在 existing invalid/out-of-field/age-expired 条件外,仅于 `viewportDensityActive` 时将 expanded viewport 外 slot 标记为 respawn candidate。复生仍复用既有 hash、三次有效 Field sample、age=0 和首帧 alpha=0;没有 CPU readback、全体 reseed、均匀网格或 zoom-based particleCount。 + +camera moving 时,`viewportDensityActive=false` 且 respawn domain 固定为 full field,避免交互期间强制 viewport respawn。相机稳定后启用 viewport domain;`ParticleDensity` 低频日志输出 zoom、mode、margin、respawnBounds、fieldBounds、fallback 与 configuredSlots。resize 与普通 camera transform 的 Trail 语义保持 v8:只有 resize 重分配 Trail target。 + +## 修改文件 + +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `shaders/particle_gpu_validation.vertex.glsl` +- `shaders/particle_gpu_validation.fragment.glsl` +- `include/mbgl/shaders/particle_gpu_validation_ubo.hpp` +- `include/mbgl/shaders/gl/particle_gpu_validation.hpp`(generator 输出) + +未修改 State A/B Float、Trail A/B UnsignedByte、previous→current segment、zoom-aware speed、wall-time lifecycle、canonical Field、WeatherV2 assets、Host Submission、Timeline、Runtime Window、WeatherColorLayer 或 REAL WEATHER UI。 + +## 验证 + +- shader generator:PASS。 +- Linux OpenGL 精确编译 `render_particle_validation_layer.cpp`:PASS。 +- 既有非 Jest `style_layer.test.cpp` 精确编译:PASS。 +- `git diff --check`:PASS。 +- Android Gradle/APK/ADB/设备与 Jest:未执行。 + +功能提交:`f8e715d080d69db779fe37ea7a5aa37c0f7c42c4`(`fix: keep particle density stable across zoom`),已成功 push 至 `origin/particle/main`。本报告随后以独立文档提交记录,不改变功能实现。 + +最终状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION**。 diff --git a/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v7.md b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v7.md new file mode 100644 index 000000000..8a6bcb07d --- /dev/null +++ b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v7.md @@ -0,0 +1,34 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 Codex 完成报告 v7 + +Starting HEAD / remote:`4645280fc59ac8e356f4339bb7fad5d73b4a413a`,`origin/particle/main`。 + +本任务是 Android C++ 编译遗漏的小修复。v9 扩展 `ParticleGpuValidationUBO` 时,WeatherColorLayer 的 designated initializer 缺少 `respawnBounds`,在 Android 的 `-Wmissing-designated-field-initializers -Werror` 下失败。 + +## 修改 + +修改文件:`src/mbgl/renderer/layers/render_weather_color_layer.cpp`。 + +WeatherColorLayer initializer 现在按 struct 顺序写入 `fieldBounds`、`respawnBounds`、`fieldScale`、`matrix`。它不参与 viewport particle respawn,故 `respawnBounds = bounds(resources->t0)`,与其 `fieldBounds` 相同,保持中性范围和原色场行为。 + +完整 C++ initializer 搜索结果: + +1. `src/mbgl/renderer/layers/render_particle_validation_layer.cpp`:已包含 `respawnBounds`。 +2. `src/mbgl/renderer/layers/render_weather_color_layer.cpp`:本次补齐 `respawnBounds`。 + +没有其他 consumer 遗漏。 + +## 验证 + +- shader generator:PASS。 +- Linux OpenGL 精确编译 `render_particle_validation_layer.cpp`:PASS。 +- Linux OpenGL 精确编译 `render_weather_color_layer.cpp`:PASS。 +- 既有非 Jest `style_layer.test.cpp` 精确编译:PASS。 +- 全部 `ParticleGpuValidationUBO` initializer 搜索/字段检查:PASS。 +- `git diff --check`:PASS。 +- Android Gradle/APK/ADB/设备和 Jest:未执行。 + +未修改 viewport-aware respawn、zoom-aware visual speed、wall-time lifecycle、Trail camera invalidate/resume、canonical U/V、WeatherColorLayer 颜色场、Timeline、Runtime Field Window 或 particleCount。 + +功能提交:`f0658e0a222508bbb8bf2f627546e8628dcde6fa`(`fix: initialize particle respawn bounds in weather layer`),已成功 push 至 `origin/particle/main`。本报告随后以独立文档提交记录,不改变功能实现。 + +最终状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION**。 diff --git a/docs/报告/STEP-055_Float_State_fail-closed小修复_Codex完成报告_v1.md b/docs/报告/STEP-055_Float_State_fail-closed小修复_Codex完成报告_v1.md new file mode 100644 index 000000000..9b2a9b985 --- /dev/null +++ b/docs/报告/STEP-055_Float_State_fail-closed小修复_Codex完成报告_v1.md @@ -0,0 +1,35 @@ +# STEP-055 Float State fail-closed 小修复 Codex 完成报告 v1 + +Starting HEAD:`f8027312e6e06d539e9ad222b60ac9a496b1758d` + +## 修改 + +仅修改 `src/mbgl/renderer/layers/render_particle_validation_layer.cpp`。 + +`createValidationTarget()` 对整个 target 创建与接线路径捕获: + +- `catch (const std::exception&)`:记录 `stage=exception error=...`,返回空 target;覆盖 `std::runtime_error`、`std::bad_alloc` 等标准异常。 +- `catch (...)`:记录 `stage=unknown_exception`,返回空 target。 +- 可渲染性或 texture validation 不通过:记录 `stage=validation`,返回空 target。 + +因此异常不会从 Particle Validation target 创建路径继续传播,也不会静默降级为 `UnsignedByte`。 + +## 冻结语义 + +State A/B 仍为 `TextureChannelDataType::Float`;Trail A/B 仍为 `TextureChannelDataType::UnsignedByte`。未修改 canonical Field、Timeline、Runtime Window、WeatherColorLayer、REAL WEATHER UI 或 `visualSpeedScale=6000`。 + +## 验证 + +- Linux OpenGL CMake 精确编译 `render_particle_validation_layer.cpp`:PASS。 +- 既有非 Jest `style_layer.test.cpp` 精确编译:PASS。 +- `git diff --check`:PASS。 +- Android Gradle/APK/ADB/设备测试:未执行。 +- Jest:未执行。 + +## Git + +功能提交:`7f66b10013fd851fc819c1fea580e0e19f4c934d`(`fix: fail closed on particle target creation exceptions`)。 + +已成功 push 至 `origin/particle/main`;本报告为随后的文档元数据提交。用户既有未跟踪文件未修改、未 stage。 + +最终状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION**。 diff --git a/docs/报告/STEP-055_粒子生命周期错峰小修复_Codex完成报告_v1.md b/docs/报告/STEP-055_粒子生命周期错峰小修复_Codex完成报告_v1.md new file mode 100644 index 000000000..2ce6020b7 --- /dev/null +++ b/docs/报告/STEP-055_粒子生命周期错峰小修复_Codex完成报告_v1.md @@ -0,0 +1,21 @@ +# STEP-055 粒子生命周期错峰小修复 Codex 完成报告 v1 + +Starting HEAD:`c7acefcb21840bc1db736a5f061ba39b18ba2c36`。 + +修改文件: + +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `shaders/particle_gpu_validation.fragment.glsl` +- `include/mbgl/shaders/gl/particle_gpu_validation.hpp`(generator 输出) + +生命周期基础寿命为 `14400` simulation seconds;在 `1200` simulation seconds/real second 下为 12 real seconds。首次初始化 age 使用稳定 `particleHash(v_uv + vec2(71.19, 29.47)) * maxAge` 错峰;每 slot 的 `maxAge` 使用另一稳定 hash 在基础寿命的 `0.75–1.25` 倍之间抖动。普通运行时 respawn 保持 age=0。 + +日志将 `activeSlots` 更正为 `configuredSlots`,并明确 `readback=disabled`,不将配置数伪称为 GPU 实测 active 数。 + +未改动 `VisualSimulationSecondsPerRealSecond=1200`、State Float、Trail UByte、真实 previous→current segment、view-space Trail、time-based fade、canonical Field、Timeline、Runtime Window、WeatherColorLayer 或 REAL WEATHER UI;未新增 GPU readback。 + +验证:shader generator、Linux OpenGL 受影响 C++ 精确编译、既有非 Jest `style_layer.test.cpp` 精确编译、`git diff --check` 均通过。未执行 Android Gradle/APK/ADB/设备测试或 Jest。 + +功能提交:`7929052c5828ca86e97a830b75168d34ea516c19`(`fix: stagger particle lifecycle respawn`),已成功 push 至 `origin/particle/main`。本报告随后的文档提交不改变功能实现。 + +最终状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION**。 diff --git a/docs/报告/STEP-056_RN天气渲染桥接现状调查_Codex完成报告_v1.md b/docs/报告/STEP-056_RN天气渲染桥接现状调查_Codex完成报告_v1.md new file mode 100644 index 000000000..43f0f1a59 --- /dev/null +++ b/docs/报告/STEP-056_RN天气渲染桥接现状调查_Codex完成报告_v1.md @@ -0,0 +1,57 @@ +# STEP-056 React Native 天气渲染桥接现状调查 — Codex 完成报告 + +## 调查状态 + +`PASS`(只读调查完成)。本阶段没有实现 React Native bridge。 + +- Starting HEAD:`44a50e6e4a4e79fae7ddf36af007b5e35e969a92` +- 分支/远端:`particle/main` / `origin/particle/main` +- 调查文档提交:`f37f6eea1f7720c64b2cc949938f29539422b480` +- 提交信息:`docs: investigate React Native weather rendering bridge` +- Push:成功,`44a50e6e4..f37f6eea1 particle/main -> particle/main` +- 本报告生成时的已推送调查 Final HEAD:`f37f6eea1f7720c64b2cc949938f29539422b480` + +## 范围与实际检查 + +检查了 canonical field Adapter/Snapshot、RasterArraySource Host submission 与 Timeline、RasterParticle/WeatherColor style 与 renderer、shader 固定色表、Android `MapLibreMap`/`NativeMapView`/JNI、Android Test App REAL WEATHER 调用、Map render request/FPS 以及 layer/source resource destruction。 + +没有修改功能代码;没有执行 Android Gradle、APK build/install、ADB、App/设备测试或 Jest。 + +## RN bridge 结论 + +没有可直接复用的 RN binding:仓库无 tracked React Native、TurboModule、Fabric、JSI 或 NativeModule。已有并可复用的是 Android Host Field path: + +`MapLibreMap.submitRasterArraySourceHostField(String, String, byte[])` +→ `NativeMapView.nativeSubmitRasterArraySourceHostField` +→ `NativeMapView::submitRasterArraySourceHostField` +→ `RasterArraySource::submitHostField` +→ `ParticleFieldDataAdapter` +→ `ParticleFieldSnapshot`/Core Timeline/Runtime Window。 + +Test App 的 `ParticleCoreLayerValidationActivity::submitRealWeatherDataset()` 只是使用 Android asset 模拟未来外部 Host,不是 RN bridge。 + +## 推荐 binary 路径 + +513×769 canonical U/V 单帧为 3,155,976 bytes(约 3.01 MiB)。当前 payload 经 asset/`ByteArray`、JNI `GetArrayRegion` 到 `vector`、Adapter 到 Snapshot `vector`,存在多次 CPU materialization/copy。 + +RN v1 推荐 `ArrayBuffer`/`Uint8Array` → Android module `byte[]` → 现有 JNI,优先稳定正确的 canonical contract;不走巨大 JSON。zero-copy 不是现有能力,只有 profiling 证明 copy/GC 是瓶颈时才增加 direct `ByteBuffer` JNI overload 或 JSI native-buffer seam。 + +## Runtime unload、颜色与性能 + +- `visibility=none` 可隐藏 layer,粒子层也停止自身持续 repaint,但不会释放已持有 GPU/Native Snapshot。 +- 完整 `setWeatherEnabled(false)` 可行,但需新低频 lifecycle/clear bridge:停止 playback,remove Particle/WeatherColor layers 以触发 GPU release,clear source timeline/pending fields 或 remove RasterArraySource 以释放 Snapshot。当前没有 `clearWindFields()` 或一条正式 runtime unload API。 +- WeatherColor 仅有 opacity;0–30 m/s palette 硬编码在 `particle_gpu_validation.fragment.glsl::weatherSpeedColor`。dynamic color stops 应新增到 WeatherColorLayer style property/model,并以 uniform/texture 交给 shader 插值。 +- RasterParticle 当前仅有 `speed-color-enabled` 与多项数值属性;没有 `particleColor`/`trailColor`。应新增独立 typed properties,令 speedColor 仅为可选模式,不能由 WeatherColor ramp 隐式决定粒子色。 +- 当前约 60 FPS 是 `RenderRasterParticleLayer::needsRepaint()`、`RenderWeatherColorLayer::needsRepaint()` 经 Map `onUpdate()` 与 Android `requestRender()` 的连续链。`MapView#setMaximumFps()` 是全图 cap,不能作为天气专属 targetFps。weather-local gate 应独立性能批次,并以累计 wall elapsed 保持 lifecycle/motion/Timeline 语义。 + +## 推荐下一阶段 + +建议进入 **STEP-057 Android Host Weather Runtime Bridge Foundation**,先实现可由未来 RN module 薄包裹的 Android/Native typed facade:runtime lifecycle/clear、binary submit、timestamp playback controls、visibility/style、低频 structured result/error events、dynamic color stops 与独立 particle/trail color。仓库缺少 RN dependency/host app topology,故不应在此仓库凭空建立可编译 RN package。 + +weather-local targetFps/repaint gate 建议独立为下一性能批次,避免把 render scheduling 风险混入接口开发。 + +最大风险为:3 MiB payload 多拷贝带来的内存/GC 峰值;hide/suspend/unload 生命周期混同;以及错误 FPS gate 改变当前 State/Timeline 的 wall-time 行为。 + +完整证据、文件路径、API mapping 与实施拆分见 [STEP-056 调查报告](../20_调查/STEP-056_RN天气渲染桥接现状调查_v1.md)。 + +无关未跟踪文件(包括 `STEP-046-e48fa087-review.diff` 与既有 `docs/40_任务/` 文件)未修改、未 stage、未提交。 diff --git a/docs/报告/STEP-057C_Android_JNI数组读取编译修正_ChatGPT完成报告_v1.md b/docs/报告/STEP-057C_Android_JNI数组读取编译修正_ChatGPT完成报告_v1.md new file mode 100644 index 000000000..4752962d6 --- /dev/null +++ b/docs/报告/STEP-057C_Android_JNI数组读取编译修正_ChatGPT完成报告_v1.md @@ -0,0 +1,22 @@ +# STEP-057C Android JNI 数组读取编译修正完成报告 + +## 结论 + +- 状态:IMPLEMENTED / WAITING_ANDROID_REBUILD +- 基线:`369957e9640a56f7204032e5c30ab9ad169cea01` +- 功能提交:`4f76245533817751f4e24f76e47e856c35fb23c7` +- 提交说明:`fix: correct Android JNI float array reads` +- 推送:成功,`origin/particle/main` + +## 修正内容 + +Android x86_64 CMake 编译在 `native_map_view.cpp` 的四处 `jni::GetArrayRegion` 调用失败。参数类型为 `jni::Array` wrapper,当前 jni.hpp 的 5 参数 `GetArrayRegion` 只接受底层 `jarray&`。 + +已改为 wrapper 自带的 `GetRegion()`:particle/trail 使用 `std::array`,speeds/rgba 使用 `std::vector`。仅修改 `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp`,未触碰 STEP-057B 状态机、renderer、shader、WeatherColor、timestamp 或 Core 生命周期。 + +## 验证 + +- `git diff --check`:PASS +- 已确认写法与当前 jni.hpp `Array::GetRegion()` 契约一致 +- 未执行 Android Gradle、APK、ADB、设备测试或 Jest +- 下一步由用户手工重新执行 Android OpenGL Debug 构建 diff --git a/docs/报告/STEP-057D_WeatherRuntime接口收尾与RN桥接前冻结_Codex完成报告_v1.md b/docs/报告/STEP-057D_WeatherRuntime接口收尾与RN桥接前冻结_Codex完成报告_v1.md new file mode 100644 index 000000000..d776719df --- /dev/null +++ b/docs/报告/STEP-057D_WeatherRuntime接口收尾与RN桥接前冻结_Codex完成报告_v1.md @@ -0,0 +1,62 @@ +# STEP-057D WeatherRuntime 接口收尾与 RN 桥接前冻结 — Codex 完成报告 + +状态:`IMPLEMENTED / READY_ANDROID_COMPILE_CHECK` + +## 实际完成内容 + +本轮基于 `01295df4b814562f569375e7e7cf1f48ce35388f`,完成 Host facade 收尾,不进入 STEP-058,不实现 RN/JSI/TurboModule。 + +- 新增 `WeatherRuntime.setParticleColor(float,float,float,float)` 与 `setTrailColor(float,float,float,float)`,经独立 JNI setter 进入 Core。两者互不提交另一颜色,不影响 WeatherColor ramp、field、Timeline、Runtime Field Window 或粒子模拟。 +- 保留 `setParticleColors(...)` 兼容 helper,继续使用原子成对底层 setter;不在 facade 猜测默认值。Core 默认值仍由 `RasterParticleLayer::Impl` 所有。 +- REAL WEATHER submit 改用 `FieldSubmitResult` 与 `accepted()`/enum 日志,不再使用 `getCode()` 或整数结果做正常 Host 业务判断。 +- REAL WEATHER 的 playback 和 typed visual 属性不再回退到 `MapLibreMap` 低层接口;旧 fixture、HOST、time-mix、field-window 与 diagnostics 入口保留并明确属于 legacy/diagnostic。 +- 增加独立颜色 API、隔离性及 native rejection fail-closed 的既有 JUnit/Mockito 测试源码。 + +## 修改文件 + +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java` +- `platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp` +- `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` +- `platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java` +- `docs/50_Step记录/STEP-057D_WeatherRuntime接口收尾与RN桥接前冻结_v1.md` +- `粒子开发记录.md` + +## API 与旁路边界 + +正式 Host/RN API 使用独立 `setParticleColor`/`setTrailColor`、typed field result、typed playback 与 typed visual 方法。A 类 REAL WEATHER 正常控制均经 `WeatherRuntime`。B 类 legacy/diagnostic 仍可直接调用 `MapLibreMap`,仅服务 fixture、HOST INVALID/DUPLICATE、ADVANCE WINDOW、time-mix 和旧诊断,不构成 RN contract。 + +## 验证 + +- `git diff --check`:PASS(功能提交前及文档收尾前均执行)。 +- `rg` 源码审计:确认 REAL WEATHER 不再使用 `submitWindField(...).getCode()`,且 REAL WEATHER 不走 timeline/property 低层 fallback。 +- 未执行 Android Gradle、APK build、ADB、设备/虚拟机测试或 Jest。 +- 未执行 noisy compile/test;Android compile check 留待后续统一授权。 + +## 已知剩余风险 + +Android Java/JNI 编译、Mockito final-class 配置和设备上的独立颜色视觉效果尚未运行验证;需后续 Android compile check 与人工验收。未改变 Core/render 生命周期,未引入 RN bridge。 + +## 审计后小修正 + +独立代码审计发现 `controlTimeline()` 在 REAL WEATHER 模式下若 `WeatherRuntime` 返回 `REJECTED`,原条件表达式会跳过 reject 分支并误记为成功。已修正为显式 `commandAccepted` 判定: + +- `WeatherRuntime` 返回 `ACCEPTED` → 成功; +- REAL WEATHER 下返回 `REJECTED` / null → 明确拒绝并立即 return,绝不 fallback; +- 非 REAL WEATHER 的 legacy/diagnostic 路径仍允许调用底层 `MapLibreMap.controlRasterArraySourceTimeline(...)`。 + +该修正只修改 TestApp Host 模拟逻辑,不修改 `WeatherRuntime`、JNI、Core、Renderer、Timeline 或 GPU 语义。 + +## Git + +- 主功能 commit:`544e4aee3ad639ce5d64c054c3668e2ea2de3d72` +- 主功能 message:`fix: finalize WeatherRuntime host API boundary` +- 文档主体提交:`0e117a9255f708f3f74a39d022ff740976a28ba4` +- 审计小修正 commit:`74e3cc911b8cecd4f3d75613055c213a8bcebcdb` +- 修正 message:`fix: preserve WeatherRuntime timeline rejection` +- Remote branch:`origin/particle/main` +- 审计小修正已成功 push;功能代码 HEAD 与 Remote HEAD 均已推进到 `74e3cc911b8cecd4f3d75613055c213a8bcebcdb`。 +- 本报告后续文档提交只更新记录,不改变功能代码。 diff --git a/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v1.md b/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v1.md new file mode 100644 index 000000000..0a981dbf0 --- /dev/null +++ b/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v1.md @@ -0,0 +1,56 @@ +# STEP-057 Android Host 天气运行时接口基础 — Codex 完成报告 + +## 状态 + +`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。 + +- Starting HEAD:`faf7548220e9a1282275711c236987f7faedfa03` +- 功能提交:`a81b464d9b6381ae5b2fb953358d25c20d62bb78` — `feat: add Android host weather runtime facade` +- 文档提交:本报告所在提交 +- Final HEAD / origin/particle/main:在文档提交并 push 后记录于交付结果。 + +## 实际完成内容与 API + +新增 `org.maplibre.android.maps.WeatherRuntime`,以 `Config(sourceId, weatherColorLayerId, particleLayerId)` 作为唯一 Host 绑定。公开方法为 `submitWindField`、`clearWindFields`、`setWeatherEnabled`、两个 visibility setter、`setForecastTime`、play/pause/speed、WeatherColor opacity、particle visual、`setWeatherColorStops`、`setParticleColors` 和 `getState`。 + +`FieldSubmitResult` 逐一映射现有 Core result,`CommandResult` 和 `State(DISABLED/WAITING_FOR_FIELDS/READY/ERROR)` 供未来 RN 薄包装读取;无 per-frame callback。Host 不得到 T0/T1、MIX、State/Trail、纹理或 renderer resource。 + +Android → JNI → C++ 新接线为:visibility、particle/trail RGBA、WeatherColor stop arrays,以及既有 submit/clear/timestamp/timeline command。Test App 的 REAL WEATHER 提交和基础播放控制已走 facade。 + +`clearWindFields` 清除 pending T0、Timeline、authoritative window 与 Source snapshots。`setWeatherEnabled(false)` 暂停、隐藏两层、clear fields;渲染线程的空 Source path 清理 particle State/Trail/draw、field textures、WeatherColor resources,保留 Source/Layer 以支持 ON 后重新提交。日志包含 `TIMELINE_STOP`、`FIELD_WINDOW_CLEAR`、`SNAPSHOT_CLEAR` 与三类 resource release。 + +WeatherColor ramp 最多 8 stop,要求 2 个以上、speed finite 严格递增、RGBA 为有限 [0,1];无效更新 reject 且保留上一有效 ramp。UBO 实现低频更新,不上传 wind field。`particleColor` 和 `trailColor` 为独立 UBO 属性;不读取 WeatherColor ramp。legacy speed color 只在显式开启时影响当前粒子 segment。 + +timestamp 恰好 T0、T1 和中间值均由 Core 选择相邻快照和 MIX;中间插值。超范围、空 timeline、Gap/Invalid 返回 reject。window 未切换的 seek 不增加 revision,因而不造成 field upload、simulation rebuild 或 trail clear;跨窗口仍走 OLD_T1→NEW_T0 reuse。 + +## 修改文件 + +功能修改覆盖 Android Java facade/NativeMap/JNI、RasterArraySource Timeline、WeatherColor/RasterParticle style model、particle UBO/shader、两个 renderer、RenderOrchestrator、REAL WEATHER Test App 以及 `test/style/source.test.cpp`、`test/style/style_layer.test.cpp`。完整逐文件清单可由功能提交取得;接口契约见 `docs/30_设计/STEP-057_AndroidHost天气运行时接口契约_v1.md`。 + +## 本地验证 + +- `node shaders/generate_shader_code.mjs -o .`:PASS。 +- `ninja -C build-linux-opengl -j3 mbgl-core`:PASS。 +- `ninja -C build-linux-opengl -j3 test/CMakeFiles/mbgl-test.dir/style/style_layer.test.cpp.o test/CMakeFiles/mbgl-test.dir/style/source.test.cpp.o`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='Source.HostFieldClearAndTimestampSeekAreFailClosed:Layer.RasterParticleVisualProperties:Layer.WeatherColorUsesIndependentFixedOpacity'`:PASS。 +- `git diff --check`:PASS。 + +所有 build/codegen/test 命令均通过 `~/.codex/AGENTS.md` 要求的 output-throttle wrapper 执行;日志保存在本机 `/tmp/step057-logs/`,不纳入提交。 + +## 未执行与风险 + +Android Gradle:NOT RUN。APK build:NOT RUN。ADB:NOT RUN。Android device validation:NOT RUN。Jest:NOT RUN。 + +Linux OpenGL 不能证明 Android OpenGL/Vulkan JNI/GLSL runtime 行为;Android 手工验收需确认 OFF 后三个 release 日志、连续 ON/OFF 内存、动态 ramp/particle/trail color 无 field upload 或 reset、timestamp 中点、playback 和 T0/T1 window boundary 的 zero-reset 保护。 + +建议手工命令(设备固定为 `192.168.1.146`): + +```bash +cd platform/android && RENDERER=opengl BUILDTYPE=Debug make apackage +# APK: platform/android/MapLibreAndroidTestApp/build/outputs/apk/debug/MapLibreAndroidTestApp-debug.apk +adb -s 192.168.1.146 install -r MapLibreAndroidTestApp/build/outputs/apk/debug/MapLibreAndroidTestApp-debug.apk +adb -s 192.168.1.146 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity +adb -s 192.168.1.146 logcat -v time | grep -E 'WeatherRuntime|ParticleTimeline|ParticleGPU|WeatherColorLayer' +``` + +Windows 安装同样使用:`adb -s 192.168.1.146 install -r `;使用同一显式设备序列号。 diff --git a/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v2.md b/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v2.md new file mode 100644 index 000000000..86b346a2f --- /dev/null +++ b/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v2.md @@ -0,0 +1,34 @@ +# STEP-057A Android Host 天气运行时接口审计修正 — Codex 完成报告 + +状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。本轮仅修正 STEP-057,不进入 STEP-058、React Native bridge 或性能任务。 + +## 实际修正 + +1. `WeatherRuntime` 删除公开 `setParticleVisual(String, float)`。正式 API 改为 `setParticleCount`、`setParticleLineWidth`、`setParticleLength`、`setParticleOpacity`、`setTrailOpacity`、`setTrailFade`、`setParticleSpeedColorEnabled`;Core property 语义未改。Test App 的未映射 fixture diagnostic 才可使用低层 generic setter。 +2. 状态转换被收敛为可单测函数:READY enable 幂等;DISABLED 成功 enable → WAITING;ERROR 成功 pending field → WAITING;initial window ready/next snapshot → READY;成功 clear → WAITING;成功 disable → DISABLED。新增 `WeatherRuntimeStateTest`,使用现有 Android JUnit 体系;遵从任务限制未运行 Gradle。 +3. 取消 `RenderOrchestrator` 的所有 hidden RasterArray 层全局 prepare/update。新增默认关闭的 `RenderLayer::needsHiddenLayerSourceCleanup`;只有持有 GPU resource 且 Host-field source 已清空的 RasterParticle/WeatherColor layers opt-in,才在 hidden 时接收一轮 cleanup。普通 hidden MapLibre layer 生命周期不变。 +4. stop 上限继续由 C++ `MaxWeatherColorStops=8` 定义。Java 经 JNI 获取 capacity,facade 以 `ColorStop` list 验证,不暴露 UBO 数组。严格 speed/RGBA 校验和独立 particle/trail color 保持。 +5. Source timestamp tests 现在使用 epoch milliseconds:T0=`1704067200000`、T1=`1704070800000`,验证 exact T0、midpoint、exact T1、lower/upper out-of-range 与 clear 后 fail-closed。 + +既有 clear/unload、JNI、Core-owned T0/T1/MIX、dynamic ramp、独立颜色、field/window reuse、无 style reload 均保留。正常 window boundary 的 OLD_T1→NEW_T0 / only NEW_T1 upload 路径未改;本轮没有触及 simulation/trail reset 条件。 + +## 验证 + +所有编译、codegen 和测试均经 `build-output-throttle` wrapper,日志仅存 `/tmp/step057a-logs/`,未提交。 + +- shader generator:PASS。 +- Linux OpenGL `mbgl-core`:PASS。 +- Linux OpenGL `mbgl-render`:PASS。 +- Linux OpenGL `mbgl-test-runner`:PASS。 +- `style_layer.test.cpp` / `source.test.cpp` 精确编译:PASS。 +- 定向非 Jest tests(timestamp seek、clear、WeatherColor stop validation、particle/trail properties):PASS。 +- `git diff --check`:PASS。 + +未执行:Android Gradle、APK build、ADB、Android device validation、Jest。 + +## Git + +- 审计起点:`efaa3e61c94e056bf06a955eb250cdfdc1633ecd` +- 功能提交、文档提交、push 和 Final HEAD:在完成提交后以最终交付结果为准。 + +Android 后续人工验收仍需重点检查 OFF hidden cleanup logs、READY 重复 ON、ERROR→pending→WAITING、dynamic stop/colour 无 field upload/reset、epoch-ms seek 与正常 window boundary `simulationRebuild=false` / `trailCleared=false`。 diff --git a/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v3.md b/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v3.md new file mode 100644 index 000000000..f3f34f041 --- /dev/null +++ b/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v3.md @@ -0,0 +1,43 @@ +# STEP-057B WeatherRuntime 状态语义修正 — Codex 完成报告 + +状态:`IMPLEMENTED / WAITING_CODE_REVIEW`。 + +## 实际修改 + +本轮基于 `dad6876c72f29e2d5671677ee15e13cd25e653a1`,只修改: + +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java` +- `platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java` +- STEP-057 Step 记录、`粒子开发记录.md` 与本报告 + +没有修改 renderer、RenderOrchestrator、JNI、shader/UBO、WeatherColor ramp、timestamp Core、Runtime Field Window、Test App UI 或 React Native bridge。 + +## 最终状态语义 + +普通数据质量拒绝 `REJECTED_ADAPTER`、`REJECTED_INCOMPATIBLE`、`REJECTED_NON_MONOTONIC`、`REJECTED_CAPACITY` 以及空 metadata/payload 只表示本次提交失败,保留当前 `READY` 或 `WAITING_FOR_FIELDS`。`ACCEPTED_PENDING_INITIAL_WINDOW` → WAITING,`INITIAL_WINDOW_READY`/`ACCEPTED_NEXT_SNAPSHOT` → READY,`DUPLICATE` 保持 current。只有 `REJECTED_SOURCE_MODE` 和 `UNKNOWN` 进入 ERROR。 + +`READY` 或 `WAITING_FOR_FIELDS` 重复 enable 是 ACCEPTED no-op;`DISABLED` enable 成功恢复 visibility 并进入 WAITING,失败进入 ERROR;ERROR enable 返回 REJECTED 并保持 ERROR。clear 成功 → WAITING、失败保持 current;disable 成功 → DISABLED、失败 → ERROR;DISABLED 重复 disable 为 ACCEPTED no-op。 + +## 测试覆盖 + +`WeatherRuntimeStateTest` 使用现有 JUnit/Mockito,覆盖所有普通 rejection 在 READY/WAITING 下保持状态、SOURCE_MODE/UNKNOWN 错误、enable 分支、clear/disable,以及两条实际 public flow:READY 下 invalid submit 保持 READY,ERROR 下 enable 返回 REJECTED 且不调用 visibility recovery。该源码未单独引入测试框架。 + +## 验证 + +本轮执行 `git diff --check`:PASS。未执行编译或测试命令;因此 public flow 测试尚未运行。明确未执行:Android Gradle、APK build、ADB、设备/虚拟机测试、Jest。此前 STEP-057A 的 Linux/OpenGL 与非 Jest Core 验证不受本轮代码修改影响。 + +## 风险与审计状态 + +已知剩余风险是 Android Java/Kotlin 编译和 Mockito final-class 配置需在后续统一 Android 验收中确认;本轮没有改变 Core field/timeline 生命周期。当前提交具备再次独立代码审计条件,等待 code review 后再进入 Android T1–T13 综合验证。 + +## Git + +- 基线:`dad6876c72f29e2d5671677ee15e13cd25e653a1` +- 功能 commit:`486cc80b6ff66e455b776ca488856c0c2ee37ba0` +- commit message:`fix: correct STEP-057B weather runtime state semantics` +- Push:成功,`dad6876c7..486cc80b6 particle/main -> particle/main` +- Remote branch:`origin/particle/main` +- STEP-057B 功能 Final HEAD:`486cc80b6ff66e455b776ca488856c0c2ee37ba0` +- origin/particle/main HEAD(功能提交后):`486cc80b6ff66e455b776ca488856c0c2ee37ba0` + +本报告的文档收尾提交及其最终分支 HEAD 由交付消息一并列出;该收尾只更新本报告元数据。 diff --git a/docs/报告/STEP-058A_SharedCadence与WeatherRuntime生命周期修正_Codex完成报告_v1.md b/docs/报告/STEP-058A_SharedCadence与WeatherRuntime生命周期修正_Codex完成报告_v1.md new file mode 100644 index 000000000..50ce3b4da --- /dev/null +++ b/docs/报告/STEP-058A_SharedCadence与WeatherRuntime生命周期修正_Codex完成报告_v1.md @@ -0,0 +1,47 @@ +# STEP-058A Shared Cadence 与 WeatherRuntime 生命周期修正 — Codex 完成报告 + +状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION` + +## 实际修正 + +- shared cadence advance 收敛至 `RasterArrayTimelineRuntime::advanceIfDue(now)`:同一 mutex 内完成 due 判断、cadence revision、真实 monotonic elapsed 的 Timeline 推进和 frame 返回。 +- Particle 与 WeatherColor 都可消费 shared advance。WeatherColor-only + PLAY 首先到达 deadline 时可独立推进 MIX;Particle 在同一 cadence window 后到只读取已推进 revision,不会双 tick。 +- WeatherColor-only + PAUSE 的 `needsRepaint()` 不再主动持续刷新;Particle visible + PAUSE 仍以自身 simulation cadence 运作;两层 hidden 时 facade 不调度 wake-up。 +- 新增幂等 `WeatherRuntime.dispose()`。它取消 pending Handler callback;旧 Runnable 即使执行也不会 `triggerRepaint()` 或再次 schedule;dispose 后 Command API 拒绝、submit/playback/seek fail-fast。 +- Test App 在 style reload、离开 REAL WEATHER、Activity destroy 之前显式 dispose 旧 Runtime。 + +## 修改文件 + +- `include/mbgl/style/sources/raster_array_source.hpp` +- `src/mbgl/style/sources/raster_array_source.cpp` +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `src/mbgl/renderer/layers/render_raster_particle_layer.hpp` +- `src/mbgl/renderer/layers/render_weather_color_layer.cpp` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java` +- `platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` +- `test/style/source.test.cpp` +- STEP-058 设计/Step 记录、`粒子开发记录.md` 与本报告。 + +## 验证 + +- throttle 下 Linux/OpenGL `ninja -C build-linux-opengl mbgl-test-runner mbgl-render-test-runner`:PASS。 +- throttle 下 `mbgl-test-runner --gtest_filter='Source.RasterArray*'`:PASS,包含 shared cadence deterministic deadline、单 consumer advance、PAUSE、target switch 与 missed deadline no-busy-loop 覆盖。 +- `git diff --check`:PASS。 +- 未执行 Android Gradle、APK、ADB、设备/虚拟机测试或 Jest。 + +## 风险与人工验收 + +Android JNI/Java 编译、Handler callback 与真机 30/60 FPS、Particle OFF/WeatherColor ON PLAY/PAUSE、两层 OFF、style reload/remount 下旧 Runtime wake 停止仍需后续人工验收。Core 的 T0/T1/MIX、field reuse、normal boundary `simulationRebuild=false`/`trailCleared=false` 未重写。 + +## Git + +- Starting HEAD:`8d12a4b361fe0b59a03327ea4ee782f01c554533` +- 功能 commit:`ab86eb8f6780f2a77c3772e695650f46fc0b6608` +- 功能 commit message:`fix: share weather cadence across renderers` +- 文档 commit:`7188040e8790a8a462ac28ce93c9c995f759a84a` +- Push:成功,`8d12a4b36..c95d404c1 particle/main -> particle/main` +- Remote branch:`origin/particle/main` +- 功能/文档主体 Final HEAD:`7188040e8790a8a462ac28ce93c9c995f759a84a` + +本报告元数据收尾后的 Final HEAD 为 `c95d404c17f4bc56924cef8f1734492cd4c42e41`;交付消息列出最终文档收尾 commit,且不改变功能代码。 diff --git a/docs/报告/STEP-058_WeatherLocalTargetFps与RepaintGate_Codex完成报告_v1.md b/docs/报告/STEP-058_WeatherLocalTargetFps与RepaintGate_Codex完成报告_v1.md new file mode 100644 index 000000000..0b9febb10 --- /dev/null +++ b/docs/报告/STEP-058_WeatherLocalTargetFps与RepaintGate_Codex完成报告_v1.md @@ -0,0 +1,56 @@ +# STEP-058 Weather-local Target FPS 与 Repaint Gate — Codex 完成报告 + +状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION` + +## 实际完成 + +- `WeatherRuntime` 新增 typed `TargetFps.AUTO/FPS_30/FPS_60`、`setTargetFps()`、`getTargetFps()`。 +- 接通 `MapLibreMap → NativeMapView → JNI → RasterArraySource`;Core `RasterArrayTimelineRuntime` 作为 Particle/WeatherColor 共用 cadence authority,保存 target、deadline 与低频 tick 计数。 +- `needsRepaint()` 与 timeline due gate 统一;未到 deadline 时不推进 Timeline、State、Trail 或 field upload,camera frame 可重用已有 composite。 +- Android facade 仅使用已有 `triggerRepaint()` 做 delayed wake-up,不使用 `MapView#setMaximumFps()` 或 Android 高频 polling。 +- REAL WEATHER tuning popup 增加 FPS AUTO/30/60,HUD 显示当前 target;target 切换不 reload Style、resubmit field、reset simulation/Trail 或重建 field texture。 +- 增加 deterministic non-sleep cadence 测试与 source 定向覆盖。 + +## 修改文件 + +- `include/mbgl/style/sources/raster_array_source.hpp` +- `src/mbgl/style/sources/raster_array_source.cpp` +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `src/mbgl/renderer/layers/render_weather_color_layer.cpp` +- `platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp` +- `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java` +- `platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` +- `test/style/source.test.cpp` +- `docs/30_设计/STEP-058_WeatherLocalTargetFps与RepaintGate设计_v1.md` +- `docs/50_Step记录/STEP-058_WeatherLocalTargetFps与RepaintGate_v1.md` +- `粒子开发记录.md` + +## 验证 + +- `ninja -C build-linux-opengl mbgl-test-runner mbgl-render-test-runner`(经 build-output-throttle):PASS。 +- `mbgl-test-runner --gtest_filter='Source.RasterArrayTimelineCadenceUsesOneSharedDeterministicDeadline'`(经 throttle):PASS。 +- `mbgl-test-runner --gtest_filter='Source.RasterArray*'`(经 throttle):PASS。 +- `git diff --check`:PASS。 + +未执行 Android Gradle/APK、ADB、设备/虚拟机测试或 Jest。 + +## 设计边界与风险 + +天气 cadence 绑定 Source-owned timeline,未改变 canonical/T0/T1/MIX、Runtime Field Window、OLD_T1→NEW_T0 reuse、only NEW_T1 upload、simulation/trail reset 语义。Android delayed wake-up 与 JNI/Java compile、camera skipped-frame 的实际视觉连续性仍需人工验收;本报告不宣称 Android validated。 + +## Git + +- Starting HEAD:`6d514e3aae68f579af41b10f85efe0f3b28784b2` +- 功能 commit:`b9b3dd22530759129b8c80477409bb7e5df4f4cc` +- 功能 commit message:`feat: add weather-local render cadence` +- 文档 commit:`8a74a3c24e8ad59caee25bd9b5338d26d4b62039` +- Push:成功(功能与文档提交已推送) +- Remote branch:`origin/particle/main` +- 功能/文档主体 Final HEAD:`8a74a3c24e8ad59caee25bd9b5338d26d4b62039` + +本报告元数据收尾提交及其最终分支 HEAD 在交付消息中列出;收尾提交不改变功能代码。 diff --git a/docs/报告/STEP-060A_RNTestHost编译前修正_Codex完成报告_v1.md b/docs/报告/STEP-060A_RNTestHost编译前修正_Codex完成报告_v1.md new file mode 100644 index 000000000..0d5cff49d --- /dev/null +++ b/docs/报告/STEP-060A_RNTestHost编译前修正_Codex完成报告_v1.md @@ -0,0 +1,42 @@ +# STEP-060A RN Test Host 编译前修正 — Codex 完成报告 + +状态:`READY_ANDROID_VALIDATION` + +## 实际修正 + +1. RN `0.85.2` 当前安装的 `node_modules/@react-native/codegen/lib/generators/modules/GenerateModuleJavaSpec.js` 第 36 行生成 `public abstract class`,第 526 与 536 行为必选方法生成 `@ReactMethod` 与 `abstract`。Promise 返回在同一模板中追加 `Promise promise`。这证明 STEP-060 的 Kotlin implementation 必须 override Spec 方法。 +2. `WeatherRuntimeBridgeModule.kt` 已为 attach、detach、getState、enabled/visible/target FPS、forecast、playback/speed、stops、particle/trail colors 及全部 visual controls 共 20 个 JS Spec 方法补齐 `override`。逐项脚本比较 `NativeWeatherRuntimeBridge.ts` 的 20 个方法与 Kotlin `override fun`,没有缺失。View attach listener 的两个 override 是 Android 回调,不是 Spec 方法。 +3. 未发现除 override 外的 Codegen signature mismatch;number→`Double`、boolean→`Boolean`、string enum→`String`、ColorStop list→`ReadableArray` 和 Promise 末参均保持现有最小实现,没有重构 bridge。 +4. Test Host 统一使用 npm,`packageManager` 为 `npm@10.9.4`,锁文件是现有 `package-lock.json`(lockfile v3);没有 yarn.lock。 +5. 实际 `npm ci --ignore-scripts --no-audit --no-fund` 通过,未需要 `--legacy-peer-deps`。当前环境 npm 为 11.19.0;`packageManager` 声明保留任务指定的 npm 10.9.4 目标,人工验收以 `npm ci` 使用 lock。 +6. package/config 明确控制 package 版本、`app.json` 的 New Architecture 请求、MLRN native `13.4.1/opengl` root extras 与 Maven Local 防 fallback。AGP、Gradle、Kotlin、SDK、NDK、Hermes 和最终 New Architecture 生成值仍待首次 prebuild 后实际检查;文档不再把它们表述为已验证冻结。 +7. STEP-060 ownership/lifecycle 未改:每次 command 仍通过 Fabric UIManager 定位 concrete MLRNMapView,再到 MapLibreMap 的 map-owned WeatherRuntime。未新增 Adapter、Normalizer、global/current map 或第二 registry。 +8. 未实现 Binary Field,未加入 number[]/JSON/base64 field、JSI、NavSea、iOS、Core renderer/timeline 改动。 + +## 文件 + +- `integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/src/main/java/org/maplibre/particle/rntesthost/weather/WeatherRuntimeBridgeModule.kt` +- `integration/react-native/MapLibreParticleRNTestHost/package.json` +- `integration/react-native/MapLibreParticleRNTestHost/README.md` +- `docs/50_Step记录/STEP-060A_RNTestHost编译前修正_v1.md` +- `docs/60_测试/STEP-060_RNTestHost_Android人工编译与验收_v1.md` +- `docs/报告/STEP-060_RNTestHost与WeatherRuntime薄桥接_Codex完成报告_v1.md` +- `粒子开发记录.md` 与本报告。 + +## 实际验证 + +- `npm install --package-lock-only --ignore-scripts --no-audit --no-fund`:PASS。 +- `npm ci --ignore-scripts --no-audit --no-fund`:PASS(530 packages);不需 legacy peer deps。 +- throttle 下 `npx tsc --noEmit`:PASS。 +- RN Codegen generator 语义、20 个 JS Spec 与 Kotlin override 一一检查、`node --check`、package/lock 检查、yarn 残留搜索、`git diff --check`:PASS。 + +## 未执行与人工验收 + +未执行 Android Gradle、custom AAR publish、Expo prebuild、APK/AAB、ADB、安装、App launch、模拟器/真机、Jest。用户下一步按更新后的 STEP-060 人工验收说明:先记录 HEAD、清除旧 Maven Local 同版本 artifact、publish 当前 custom SDK、`npm ci`、Expo prebuild、核对实际工具链/Hermes/New Architecture 与 custom resolution,再进行 Phase A/B、style reload 和 remount 验收。 + +## Git + +- Starting HEAD:`6a99f1a1424ce7e30c26e825251df956a46bf4c4` +- 功能 commit:`51b58024323aeb47103ccc08e50a53ac0ab75d43`(`fix: correct RN WeatherRuntime codegen bridge`) +- 文档 commit:`7c771709e012d3525aa07b766d36fedde12aed7c`(`docs: record STEP-060A prebuild fixes`) +- Final HEAD / Remote HEAD / push:由本报告元数据收尾 commit 与 push 后交付消息给出。 diff --git a/docs/报告/STEP-060B_GateC封存环境与CustomAAR接入修正_Codex完成报告_v1.md b/docs/报告/STEP-060B_GateC封存环境与CustomAAR接入修正_Codex完成报告_v1.md new file mode 100644 index 000000000..b099e81c2 --- /dev/null +++ b/docs/报告/STEP-060B_GateC封存环境与CustomAAR接入修正_Codex完成报告_v1.md @@ -0,0 +1,39 @@ +# STEP-060B Gate C 封存环境与 Custom AAR 接入修正 — Codex 完成报告 + +状态:`IMPLEMENTED / WAITING_USER_GATEC_AAR_BUILD_AND_ANDROID_VALIDATION` + +## 实际完成 + +1. 以 `粒子开发记录.md` 的 STEP-000 为 Gate C 事实来源:Ubuntu 24.04 x86_64、Linux OpenGL/mbgl-render/mbgl-render-test-runner,以及 Android OpenGL/Vulkan Debug `apackage` 均为已封存的 MapLibre Native 编译环境历史。没有修改它。 +2. publication 源码核对:`MapLibreAndroid` apply `maplibre.gradle-publish`;`openglRelease` 被发布;`openglrelease` publication 的 artifactId 是 `android-sdk-opengl`;artifact settings group 是 `org.maplibre.gl`、VERSION 是 `13.4.1`。因此精确 Maven Local task 是 `:MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal`。 +3. 旧 `MapLibreAndroid-opengl-release.aar` 仍在约定路径,mtime 为 2026-08-29,SHA-256 为 `5c67d2751fafe211a0df4b82deded09ab1c07cf6ca0f4a12a880283287f85e5c`;classes.jar 有旧 MapLibreMap、没有 WeatherRuntime,不能用于当前验证。 +4. MLRN 11.3.6 实际 Gradle 模板用 root extras `nativeVariant/nativeVersion` 解析 `org.maplibre.gl:android-sdk-${variant}:${version}`。Test Host plugin 固定 `opengl`/`13.4.1`;Bridge 直接依赖同一 `org.maplibre.gl:android-sdk-opengl:13.4.1`。 +5. 修正 plugin repository rule:此前排除整个 `org.maplibre.gl` 会阻断 MLRN 所需 `android-sdk-turf` 与 plugins。现在 Maven Local 仍在首位,但 Maven Central 只排除唯一 custom module `android-sdk-opengl`。因此 custom SDK 没有 Maven Central fallback,且没有新增 flatDir/files/第二 Maven repository route。 +6. 文档明确 Gate C AAR build 与 Test Host generated Android toolchain 是不同层次。Test Host 首次 prebuild 观察到 `Gradle 9.3.1 / AGP 8.12.0 / Kotlin 2.1.20 / SDK 24/36/36 / NDK 27.1.12297006 / Hermes ON / New Architecture ON`,不反向修改 Gate C。package declaration 是 npm 10.9.4,而当前 shell 实际为 Node 24.20.0/npm 11.19.0;先前 `npm ci` 已通过。 + +## 修改文件 + +- `integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js` +- `integration/react-native/MapLibreParticleRNTestHost/README.md` +- `docs/60_测试/STEP-060_RNTestHost_Android人工编译与验收_v1.md` +- `docs/50_Step记录/STEP-060A_RNTestHost编译前修正_v1.md` +- `docs/50_Step记录/STEP-060B_GateC封存环境与CustomAAR接入修正_v1.md` +- `docs/报告/STEP-060_RNTestHost与WeatherRuntime薄桥接_Codex完成报告_v1.md` +- `粒子开发记录.md` 与本报告。 + +## 验证 + +- publication group/artifact/version/component/task、MLRN/Bridge exact coordinate、old AAR SHA/mtime/classes:静态核对 PASS。 +- `NativeWeatherRuntimeBridge.ts` 与 Kotlin 20 个 Codegen override:静态一致性 PASS。 +- throttle 下 `npx tsc --noEmit`、Node plugin syntax、package/lock、无受控 yarn.lock、无 flatDir/files/第二 dependency route、`git diff --check`:PASS。 + +## 未执行 / 风险 + +没有执行 Android Gradle build/assemble/publish、Expo prebuild、APK/AAB、ADB、设备/模拟器或 Jest。当前 HEAD 的新 AAR、Maven Local POM/module metadata、Test Host 实际 resolution、Android codegen/compile 与 Phase A/B 尚未验证,必须由用户按更新人工说明完成。用户手工生成的 `integration/react-native/MapLibreParticleRNTestHost/android/` 保持 untracked,未删除、修改或提交。 + +## Git + +- Starting HEAD:`80f360be7849be3e63557c4e074ee3a0fc02e15f` +- 功能 commit:`48b93b8609457b280de1034fe1dedc57cb3971e2`(`fix: align RN test host with Gate C Maven handoff`) +- 文档 commit:`e0da7f0c0c7d0c8153de5ce1fcc8e2af0bd0d780`(`docs: record STEP-060B Gate C Maven alignment`) +- Final HEAD / Remote HEAD / push:由本报告元数据收尾 commit 与 push 后交付消息给出。 diff --git a/docs/报告/STEP-060C_MavenLocal签名隔离修正_Codex完成报告_v1.md b/docs/报告/STEP-060C_MavenLocal签名隔离修正_Codex完成报告_v1.md new file mode 100644 index 000000000..52035650b --- /dev/null +++ b/docs/报告/STEP-060C_MavenLocal签名隔离修正_Codex完成报告_v1.md @@ -0,0 +1,70 @@ +# STEP-060C Maven Local 签名隔离修正 — Codex 完成报告 + +## 结论 + +STEP-060C 已为 `CLOSED / PASS`:用户于 2026-09-07 在 Gate C 封存 Linux 环境完成 Maven Local publication,Local 签名隔离按设计生效。本 PASS 仅覆盖 Maven Local signing isolation、发布与 artifact provenance;不代表 RN Test Host Android compile、STEP-060 Phase A/B、lifecycle、Binary Field 或 STEP-061 通过。 + +## 实际修改 + +- 保留 Vanniktech `signAllPublications()`,不移除 Maven Central 正式签名能力。 +- 将 Gradle `SigningExtension.required` 改为延迟 task-graph 条件:任一 `*PublicationToMavenCentralRepository` task 存在时为 true;否则为 false。 +- 因此 `:MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal` 的 Sign task 在无 signatory 时跳过,Central publication 仍要求签名。 +- 更新 Gate C/RN Test Host 人工步骤:当前已核对 AAR 可直接 publication,不因本次 publication 配置改动重建。 + +## 修改文件 + +- `platform/android/buildSrc/src/main/kotlin/maplibre.gradle-publish.gradle.kts` +- `docs/60_测试/STEP-060_RNTestHost_Android人工编译与验收_v1.md` +- `integration/react-native/MapLibreParticleRNTestHost/README.md` +- `docs/50_Step记录/STEP-060B_GateC封存环境与CustomAAR接入修正_v1.md` +- `docs/50_Step记录/STEP-060C_MavenLocal签名隔离修正_v1.md` +- `粒子开发记录.md` + +## API / publication 契约 + +| 项目 | 保持的契约 | +| --- | --- | +| Maven Local 坐标 | `org.maplibre.gl:android-sdk-opengl:13.4.1` | +| Local task | `:MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal` | +| Local 签名 | 无 signatory 时 Sign task 跳过;不使用 `-x sign` | +| Central 签名 | `*PublicationToMavenCentralRepository` task graph 中仍 required | +| AAR 来源 | Gate C 已验证的 custom OpenGL AAR;无 flatDir、files repo、第二仓库或 SNAPSHOT | + +## 人工 Maven Local 验收(2026-09-07) + +用户执行: + +```bash +cd /mnt/sda1/MapLibreParticleLab/platform/android +./gradlew :MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain +``` + +- 已完成:`:MapLibreAndroid:signOpenglreleasePublication SKIPPED`;`:MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal` 通过;`BUILD SUCCESSFUL in 3m 33s`(57 actionable tasks:49 executed、8 up-to-date);输出不含 `no configured signatory`。 +- 已完成:`~/.m2/repository/org/maplibre/gl/android-sdk-opengl/13.4.1/` 含 AAR、POM、module metadata;POM/module 坐标为 `org.maplibre.gl:android-sdk-opengl:13.4.1`。 +- 已完成:Maven Local AAR 与 `platform/android/MapLibreAndroid/build/outputs/aar/MapLibreAndroid-opengl-release.aar` SHA-256 同为 `2e73e25cfd31bb181f75ba2d84974afb949640f72de46bc0234d4579abe3ba38`。 +- 已完成:AAR `classes.jar` 有 `org/maplibre/android/maps/MapLibreMap.class`、`org/maplibre/android/maps/WeatherRuntime.class`;AAR 有 `arm64-v8a`、`armeabi-v7a`、`x86`、`x86_64` 四 ABI;Maven Local 不含 `.asc`,符合 Local publication 不签名的设计。 +- 历史说明:`47646fb708d096cf5ee0c3b8eeaeb7e6083081279c79f4a642db554efe88fad7` 为 publication 前中间 AAR SHA;Gradle 重新执行 `bundleOpenglReleaseAar` 后,它不是当前 published artifact。 + +## Codex 静态核验 + +- 静态读取 Vanniktech `gradle-maven-publish-plugin:0.34.0`:`signAllPublications()` 对 release 设置 `SigningExtension.required`;Gradle SigningExtension 的 `setRequired(Object)` 支持 deferred Provider,并在 Sign task 执行时解析。 +- 静态审计 publication 名称、artifactId、version 与 Central task 名称约束。 +- 只读核对 Local/build output AAR SHA、POM/module 坐标、classes、ABI 与 `.asc` 缺失;`git diff --check` 将在本次 docs-only commit 前再次执行。 + +## 未执行 / 无法验证 + +- 未执行:本轮 Codex 未运行 Android Gradle、AAR build、Maven publish、Expo prebuild、APK、ADB、Android 设备/虚拟机测试或 Jest;publication 结果为用户提供的人工实测证据。 +- 未完成:RN Test Host `npm ci`、clean Expo prebuild、generated Gradle resolution、Android compile/install、STEP-060 Phase A/B、style reload/map remount/stale reactTag/detach lifecycle;任务明确留给下一轮 STEP-060 Android 人工验收。 +- 无法验证:本轮没有设备/模拟器与 RN 生成工程执行授权,故不对上述 Android/RN 结果作 PASS 推断。 + +## 已知剩余风险与下一步 + +下一阶段仍是 STEP-060 RN Test Host Android 验收:`npm ci`、clean Expo prebuild、检查 generated Gradle 的 Maven Local/custom SDK resolution、用户手工 Android compile/install、Phase A/B 与 lifecycle 验收。通过后才可进入 STEP-061。 + +## 提交与推送 + +- 功能提交:`5f0804e8ba3a8a6fed1fff09ddd92a76a77bd458` — `fix: isolate Maven Local from release signing` +- 初始文档提交:`9e0666c7c284d5080ebd2e711f46928f788ddcec` — `docs: record STEP-060C local publish signing fix` +- 本次人工验收文档收尾 Starting HEAD:`9e0666c7c284d5080ebd2e711f46928f788ddcec`。 +- 本次 docs-only 内容提交:`9a38271ceecfc33d581d7604ddb16839d270961e` — `docs: close STEP-060C Maven Local validation`。 +- 该提交仅回填人工验收证据与后续验收边界;最终推送后的 Final HEAD、Remote HEAD 与 push 结果由本次任务交付时的 Git 实测结果确认。 diff --git a/docs/报告/STEP-060D_WeatherRuntimeBridge_attach精确诊断_Codex完成报告_v1.md b/docs/报告/STEP-060D_WeatherRuntimeBridge_attach精确诊断_Codex完成报告_v1.md new file mode 100644 index 000000000..fb8e80a64 --- /dev/null +++ b/docs/报告/STEP-060D_WeatherRuntimeBridge_attach精确诊断_Codex完成报告_v1.md @@ -0,0 +1,46 @@ +# STEP-060D WeatherRuntimeBridge attach 精确诊断 — Codex 完成报告 + +## 结论 + +已实现并交付 STEP-060 attach 精确诊断与唯一 Linux custom AAR,当前状态为 `IMPLEMENTED / WAITING_CARBON7I_ATTACH_DIAGNOSTICS`。本轮不是 STEP-060 CLOSED,也未进入 STEP-061。 + +## 正式源码修改 + +- `WeatherRuntimeBridgeModule.attach()` 现在分别检查 weather source、WeatherColor layer、particle layer。 +- 三对象不全时仍使用稳定错误码 `WEATHER_OBJECTS_NOT_READY`,但 message 固定包含 `reactTag`、`mapView`、`mapLibreMap`、`style`、三个固定 ID 的 present/missing 状态、source/layer 总数及有界 ID 摘要。 +- 摘要每类最多 24 个 ID,weather ID 优先;有剩余项时写 `…+N`,不使用反射、不打印无界 Style JSON。 +- `MAP_VIEW_NOT_FOUND`、`MAP_NOT_READY`、`STYLE_NOT_READY` 仍独立于 weather-object rejection;对象齐全时原 `replaceWeatherRuntime()`、Promise snapshot、控制语义和生命周期 listener 不变。 +- 为避免 Maven Local 缓存混淆,custom AAR 与 Test Host 已有坐标 wiring 改用 `13.4.1-step060d`;未改 Test Host 业务流程。 + +## Linux 验证 + +- `JOBS=3 RENDERER=opengl BUILDTYPE=Release make apackage`:PASS(正式 OpenGL Release 四 ABI AAR)。 +- `./gradlew :MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain`:PASS。 +- `./gradlew projects --console=plain`:PASS;确认 bridge project 为 `:maplibre-particle_weather-runtime-bridge`。 +- `./gradlew :maplibre-particle_weather-runtime-bridge:compileDebugKotlin --console=plain`:无法验证本轮源码。当前 untracked Expo Android 生成工程缺失 `NativeWeatherRuntimeBridgeSpec`;随后的标准 `:app:generateCodegenArtifactsFromSchema` 为 PASS 但子任务 `SKIPPED`,没有生成该 Spec。compiler 因此报告所有 TurboModule overrides 连带失败。此限制已记录,未通过修改生成工程或扩大 bridge/codegen 方案绕过。 +- `git diff --check`:PASS。 + +所有 Gradle/Make/codegen 命令均通过 build-output-throttle 执行;未执行 Jest。 + +## Custom AAR 交付 + +| 项目 | 值 | +| --- | --- | +| Maven group | `org.maplibre.gl` | +| artifact | `android-sdk-opengl` | +| version | `13.4.1-step060d` | +| build output | `platform/android/MapLibreAndroid/build/outputs/aar/MapLibreAndroid-opengl-release.aar` | +| Maven Local AAR | `/root/.m2/repository/org/maplibre/gl/android-sdk-opengl/13.4.1-step060d/android-sdk-opengl-13.4.1-step060d.aar` | +| SHA-256 | `7391440f02e976f295caa265bc15e895a7c0183883a0e2cb08deef4cf42b0096` | + +POM/module 坐标均为 `org.maplibre.gl:android-sdk-opengl:13.4.1-step060d`;发布 AAR 与 build output SHA 一致,含 `arm64-v8a`、`armeabi-v7a`、`x86`、`x86_64`。 + +## 未执行与下一步 + +未进入 CARBON7i、未更新 Win11 Maven Local、未执行 Win11 dependencyInsight、APK build/install、ADB 或 Android 运行验证;未实现 binary U/V field transport,未进入 STEP-061。下一步只能由 CARBON7i 用本报告的唯一坐标及 SHA 安装/解析 AAR 后,返回 attach 的精确诊断。 + +## Git + +- Linux source/AAR 提交:`c8030581b5c6285e2623e251e0eb261351321142` — `feat: diagnose WeatherRuntime bridge attach state`。 +- 文档主提交:`b96b531e0189d8d851f92ac3fdac0f95d59629da` — `docs: record STEP-060D attach diagnostics`。 +- 本报告中的 AAR SHA 由上述 source commit 构建;最终任务 HEAD、远端 HEAD 与 push 结果以本次任务交付的 Git 实测值为准。 diff --git a/docs/报告/STEP-060E_CoreOnlyLayer存在性检查修复_Codex完成报告_v1.md b/docs/报告/STEP-060E_CoreOnlyLayer存在性检查修复_Codex完成报告_v1.md new file mode 100644 index 000000000..9e630f3ea --- /dev/null +++ b/docs/报告/STEP-060E_CoreOnlyLayer存在性检查修复_Codex完成报告_v1.md @@ -0,0 +1,44 @@ +# STEP-060E CoreOnly Layer 存在性检查修复 — Codex 完成报告 + +## 结论 + +状态:IMPLEMENTED / WAITING_CODE_REVIEW。本轮完成 Android Host 到 JNI/Core 的 Style 存在性窄查询,修复 RN bridge 对 CoreOnly weather layer 的假阴性;STEP-060 未 CLOSED,未进入 STEP-061。 + +## 实际完成内容 + +- 根因:RasterParticleLayer 与 WeatherColorLayer 是 CoreOnly layer,Core 可正常渲染,但 Style.getLayer() 需要 Java peer,故会错误返回 null。 +- 新增公开 MapLibreMap.hasStyleSource(String)、hasStyleLayer(String);实现穿过 NativeMap/NativeMapView 和 JNI,C++ 直接以 map->getStyle().getSource(id) 与 getLayer(id) 判断。 +- WeatherRuntimeBridge.attach() 在 style fully loaded 后,使用三个独立 Core 查询检查 rn-weather-fields、rn-weather-color、rn-weather-particles;没有改变 attach 成功路径、replaceWeatherRuntime()、snapshot 或 View detach listener。 +- WEATHER_OBJECTS_NOT_READY 保留固定错误码和三个独立 present/missing 字段,新增 styleUri(JSON style 为空时 ),移除 style.getLayers()/getSources() 的 Java peer 枚举。 +- Maven 临时版本同步为 13.4.1-step060e。 + +## 修改文件 + +- platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/{NativeMap.java,NativeMapView.java,MapLibreMap.java} +- platform/android/MapLibreAndroid/src/cpp/{native_map_view.hpp,native_map_view.cpp} +- integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/src/main/java/org/maplibre/particle/rntesthost/weather/WeatherRuntimeBridgeModule.kt +- platform/android/VERSION、bridge android/build.gradle、plugins/withParticleNativeAndroid.js、Test Host README、本 Step 记录、本文档及 粒子开发记录.md。 + +## 验证 + +- ./gradlew :MapLibreAndroid:tasks --all --console=plain:PASS。 +- ./gradlew :MapLibreAndroid:compileOpenglReleaseJavaWithJavac --console=plain:PASS。 +- cmake --build platform/android/MapLibreAndroid/.cxx/RelWithDebInfo/243u115z/x86_64 --target maplibre -- -j2:PASS(JNI/OpenGL x86_64 等价精确 CMake/Ninja target,不打包 AAR)。 +- 静态检查:JNI Java 声明、C++ 声明/实现/注册名称一致;Bridge 不再调用 Style.getLayer()、Style.getLayers()、Style.getSource() 或 Style.getSources() 作为 weather object 真值。 +- git diff --check:PASS。 + +所有可能产生大量输出的 Gradle 编译均通过 build-output-throttle 执行;未执行 Jest。 + +## 未执行与风险 + +- AAR_BUILD_NOT_EXECUTED_BY_CODEX。 +- 未执行 Maven Local publication、Android APK build/install、ADB、设备或模拟器验证。 +- 本地 Expo 生成工程先前缺少 NativeWeatherRuntimeBridgeSpec;未为本轮新增大型 codegen 或 mocking 方案。后续人工构建新 AAR 并在 Win11 更新 Maven Local 后,仍需执行两条 dependencyInsight、APK build 与 attach/runtime 人工验收。 +- 运行期新 JNI 方法的真值行为需要上述 Android 人工验收;本轮未改变 CoreOnly 注册或 WeatherRuntime 控制状态机。 + +## Git + +- source baseline:2d2375cd532468821c6e3c158ee1a275f4522044 +- 功能 commit / Final HEAD / push 结果:将在本次受限验证、提交并推送后回填。 +- remote branch:origin/particle/main +- 预期 Maven 坐标:org.maplibre.gl:android-sdk-opengl:13.4.1-step060e diff --git a/docs/报告/STEP-060F_Android最终验收报告_v1.md b/docs/报告/STEP-060F_Android最终验收报告_v1.md new file mode 100644 index 000000000..73d5c16e4 --- /dev/null +++ b/docs/报告/STEP-060F_Android最终验收报告_v1.md @@ -0,0 +1,28 @@ +# STEP-060F Android 最终验收报告 v1 + +日期:2026-09-08 +结论:`PASS` + +## 构建与制品 + +- Linux AAR:`org.maplibre.gl:android-sdk-opengl:13.4.1-step060f` +- AAR 构建使用 `JOBS=1`,OpenGL Release 四 ABI 成功。 +- Linux build output 与 Maven Local AAR SHA-256:`53dfcd43f3479ae43a3c0fd91c10ccbfec7cf1c84ae22ae3435c94393806da6d`。 +- CARBON7i 通过带 cache-busting 参数的 `curl.exe -L` 下载,SHA-256 与 Linux 完全一致。 +- Win11 Maven Local 安装完成;Bridge 与 MLRN 两条 `dependencyInsight` 均解析到 `13.4.1-step060f`。 +- Test Host `assembleDebug`:`BUILD SUCCESSFUL in 1m 43s`。 + +## 设备运行结果 + +- 设备:`192.168.1.146`。 +- 安装脚本双 reverse 生效:`tcp:8091 -> tcp:8091`、`tcp:8081 -> tcp:8091`。 +- Activity 方向:`SCREEN_ORIENTATION_LANDSCAPE`。 +- UI:`WeatherRuntime attached`。 +- Runtime:`WAITING_FOR_FIELDS / PAUSE / AUTO`。 +- 完整验证 UI 的 `weather opacity`:`ACCEPTED`。 + +## Hard gate + +当前 App PID 日志未发现:`WEATHER_OBJECTS_NOT_READY`、`STYLE_PROPERTY_SET result=UNSUPPORTED_PROPERTY`、`UNSUPPORTED_PROPERTY`、`FATAL EXCEPTION`、`SIGSEGV`、`JNI DETECTED`、`RUNTIME_DISPOSED`、`MAP_VIEW_NOT_FOUND`、`STYLE_NOT_READY`。 + +`TIMELINE_NOT_READY` 在未提交 Binary Field 的 STEP-060 阶段仍属预期 fail-closed 行为。STEP-060 可正式标记 `CLOSED / PASS`,下一阶段允许进入 STEP-061 Binary U/V Field Transport。 diff --git a/docs/报告/STEP-060F_WeatherColorOpacity与Android收尾修复_Codex完成报告_v1.md b/docs/报告/STEP-060F_WeatherColorOpacity与Android收尾修复_Codex完成报告_v1.md new file mode 100644 index 000000000..6f96e275a --- /dev/null +++ b/docs/报告/STEP-060F_WeatherColorOpacity与Android收尾修复_Codex完成报告_v1.md @@ -0,0 +1,35 @@ +# STEP-060F WeatherColor Opacity 与 Android 收尾修复 — Codex完成报告 v1 + +状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION` + +## 完成内容 + +- 新增 `MapLibreMap.setWeatherColorOpacity(String layerId, float opacity): boolean`,通过 `NativeMap`、`NativeMapView`、JNI registration 与 `NativeMapView::setWeatherColorOpacity()` 到达 Core `WeatherColorLayer::setOpacity()`。 +- C++ 对不存在 layer、非 `WeatherColorLayer` 和非 finite opacity 返回 false;成功路径保持 WeatherColor CoreOnly,不创建 Java Layer peer。 +- `WeatherRuntime.setWeatherColorOpacity()` 已改为 typed WeatherColor API,保留 `ACCEPTED / REJECTED` 契约。 +- `setStyleLayerFloatProperty()` 仍只接受既有 raster-particle whitelist,仍只转换为 `RasterParticleLayer`;Timeline/Playback 语义未改。 +- 新增受控 Windows 启动脚本,保留 `tcp:8091 -> tcp:8091` 并增加 `tcp:8081 -> tcp:8091`;设备、package、activity 与 Metro 端口不变。 +- `app.json` 仍为 landscape;下次 Android prebuild/build 应生成 `MainActivity android:screenOrientation="landscape"`。 +- `platform/android/VERSION`、Bridge Gradle 与 Expo plugin 均更新到 `org.maplibre.gl:android-sdk-opengl:13.4.1-step060f`。 + +## 已知验证与边界 + +STEP-060E 的 Android attach 已 PASS,`WEATHER_OBJECTS_NOT_READY` 已解决。无 Binary Field 时 `TIMELINE_NOT_READY` 是预期 fail-closed 行为,本轮没有为此改变 Timeline。 + +已执行 JNI 声明/实现/registration 静态核对、WeatherColor setter 路径核对、particle whitelist 边界核对、版本/脚本/landscape 检查与 `git diff --check`。未执行 Android Gradle、AAR build、Maven Local publish、APK build、ADB、安装、设备测试或 Jest。 + +`AAR_BUILD_NOT_EXECUTED_BY_CODEX` + +## Git 交接 + +- Starting HEAD:`3f9f2456d636e7a013038ed69b8f392d6de4b197`。 +- Source commit SHA:`c91af1682574d1e7a24de1060b50f9a7e2e2e25d`。 +- Source commit message:`fix: route WeatherColor opacity through typed API`。 +- Source handoff local / remote HEAD:`c91af1682574d1e7a24de1060b50f9a7e2e2e25d` / `c91af1682574d1e7a24de1060b50f9a7e2e2e25d`。 +- Remote branch:`origin/particle/main`。 + +Codex CLI 运行在 `workspace-write` sandbox 中时,`.git` 被挂为只读:`git pull` 无法写 `FETCH_HEAD`,`git commit` 无法创建 `.git/index.lock`。因此源码与文档由 Codex 完成后,Git commit/push 由 ChatGPT 通过 RDC 在同一仓库审查后完成;不得把该限制误写成源码未提交。 + +## 后续人工链路 + +用户手工构建并发布 `13.4.1-step060f` OpenGL AAR,核对 Linux build/Maven Local 与 Win11 下载 SHA-256,确认 Bridge 与 MLRN 的 dependencyInsight 均解析该版本,随后 clean prebuild、APK 构建与双 reverse 安装。最终 Android gate 为横屏、attach、无 `WEATHER_OBJECTS_NOT_READY`、`weather opacity: ACCEPTED`、无 `UNSUPPORTED_PROPERTY`,同时无 field 下允许继续出现 `TIMELINE_NOT_READY`。在此之前不得标记 STEP-060 CLOSED / PASS,也不得进入 STEP-061。 diff --git a/docs/报告/STEP-060_RNTestHost与WeatherRuntime薄桥接_Codex完成报告_v1.md b/docs/报告/STEP-060_RNTestHost与WeatherRuntime薄桥接_Codex完成报告_v1.md new file mode 100644 index 000000000..a0d84920e --- /dev/null +++ b/docs/报告/STEP-060_RNTestHost与WeatherRuntime薄桥接_Codex完成报告_v1.md @@ -0,0 +1,58 @@ +# STEP-060 RN Test Host 与 WeatherRuntime 薄桥接 — Codex 完成报告 + +状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION` + +Starting HEAD:`b535d98263af58ac86845f15bb7c51bd85b59519`。 + +## 实际完成内容 + +1. RN Test Host 最终目录为 `integration/react-native/MapLibreParticleRNTestHost/`。固定 RN `0.85.2`、React `19.2.5`、Expo `56.0.4`、TypeScript `5.9.3`、MLRN `11.3.6`;New Architecture 已由 Expo config 请求,但 Android prebuild/生成工程尚未验证。 +2. MLRN `11.3.6` 实际 Android `build.gradle` 按 root extra `org.maplibre.reactnative.nativeVersion/nativeVariant` 解析 `android-sdk-`。Test Host config plugin 固定 `13.4.1`/`opengl`。 +3. STEP-060B 将 plugin 的 Maven Central 排除收敛为 custom module `org.maplibre.gl:android-sdk-opengl`,并将 Maven Local 排在首位。因此 custom `android-sdk-opengl:13.4.1` 缺失即依赖失败,不会从普通官方 SDK fallback;MLRN 的独立 turf/annotation/scalebar artifacts 不会因全 group 排除而失去正常来源。artifact 的字节来源仍须用户手工发布/检查。 +4. MLRN 的共享 `ReactTagResolver` 实例由 `MLRNPackage` 私有持有,不能安全注入外部模块。Bridge 没有复制其 `mViews`:使用 React Native/Fabric 的公开 `UIManagerHelper.getUIManager(...).resolveView(reactTag)`,验证结果是具体 `MLRNMapView`。 +5. 从该具体 View 读取 MLRN 公开只读 `mapLibreMap`;不存在 global/current map。 +6. `MapLibreMap` 新增唯一 map-owned `WeatherRuntime` attachment seam。Bridge 不维护 Map→Runtime 表:style replacement 通过 `MapLibreMap.setStyle()` dispose,View detach listener dispose,React `useEffect` unmount 也调用 detach;下一次 style ready attach 必定创建新 facade。 +7. style ready 前分别 fail-closed 为 `MAP_NOT_READY` 或 `STYLE_NOT_READY`;source/layers 缺失为 `WEATHER_OBJECTS_NOT_READY`。不存在/已 drop tag 为 `MAP_VIEW_NOT_FOUND`,已 dispose runtime 为 `RUNTIME_DISPOSED`;参数问题为 `INVALID_ARGUMENT`,facade throw 为 `NATIVE_REJECTED`。 +8. Style JSON 由 Test Host 预先创建 background、host-field `RasterArraySource`、WeatherColorLayer、RasterParticleLayer。Bridge 不创建 Weather 私有 Style registry;style reload 后由 MLRN style event 的 attach 重建。 +9. Phase A 五项均完成:`getState`、`setWeatherEnabled`、`setWeatherColorVisible`、`setParticleVisible`、`setTargetFps`。 +10. Phase B 完成:epoch-ms `setForecastTime`、playback state/speed、ColorStop list、独立 particle/trail RGBA、particle count/line width/length/opacity、trail opacity/fade、WeatherColor opacity 与 native snapshot。未完成:Binary Field、JSI/zero-copy、provider/network、NavSea 正式接入、iOS bridge。 +11. 为 snapshot 增加唯一最小 getter `WeatherRuntime.getPlaybackState()`;`state`、target FPS、disposed 均为既有 native 权威值,Bridge 不缓存播放/runtime state。没有 Adapter、Normalizer、第二 registry 或 renderer 改动。 + +## 修改/新增文件 + +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java` +- `integration/react-native/MapLibreParticleRNTestHost/`(Expo Test Host、Android TurboModule、style、README、lock/config plugin) +- `docs/50_Step记录/STEP-060_RNTestHost与WeatherRuntime薄桥接_v1.md` +- `docs/60_测试/STEP-060_RNTestHost_Android人工编译与验收_v1.md` +- `粒子开发记录.md` 与本报告。 + +## API 与生命周期 + +```text +JS typed TurboModule + reactTag + → Fabric UIManager resolveView(tag) + → concrete MLRNMapView.mapLibreMap + → MapLibreMap.replaceWeatherRuntime(config) + → WeatherRuntime typed command +``` + +Bridge 不接触 T0/T1/MIX/timeline integer command。`setForecastTime` 只接收 non-negative integral epoch milliseconds;ColorStop 限有限 RGBA `[0,1]` 与严格递增 speed;particle/trail color 独立于 WeatherColor ramp。无 `number[]`、JSON 数组、base64 或 Binary Field contract。 + +## 实际验证 + +- `npm install --package-lock-only --ignore-scripts --legacy-peer-deps --no-audit --no-fund`:PASS,生成/核对 lock。 +- 构建输出节流下 `npx tsc --noEmit`:PASS。 +- `node --check plugins/withParticleNativeAndroid.js`、版本/lock 一致性检查、禁止边界搜索:PASS。 +- `git diff --check`:PASS。 + +## 未执行与风险 + +没有执行 Android Gradle、custom artifact publish、APK/AAB、ADB、安装、App launch、模拟器/真机或 Jest。故 Android codegen/TurboModule 的实际 Java/Kotlin 编译、MLRN autolinking project name、Maven Local artifact provenance、Fabric event target、style reload/remount 的时序和 Handler/renderer 行为均仍待人工验收。详细步骤在 `docs/60_测试/STEP-060_RNTestHost_Android人工编译与验收_v1.md`;包括 custom SDK build/publish、Test Host build/install、地图、同 reactTag 的五个 Phase A 命令、Phase B、style reload、old tag fail-closed、remount/unmount。 + +## Git + +- 功能 commit:`4c906b21fbc9834699c5dcd62e551b05b2526c53`(`feat: add RN WeatherRuntime bridge test host`) +- 文档 commit:`5c6fa2c668a6fef76a87820672ae84e178aea403`(`docs: record STEP-060 RN bridge test host`) +- Push:待本报告元数据收尾 commit 后推送至 `origin/particle/main`。 +- Final HEAD / Remote HEAD:由本报告元数据收尾 commit 与 push 后的交付消息给出。 diff --git a/docs/报告/STEP-061_Android最终验收报告_v1.md b/docs/报告/STEP-061_Android最终验收报告_v1.md new file mode 100644 index 000000000..dc80d2354 --- /dev/null +++ b/docs/报告/STEP-061_Android最终验收报告_v1.md @@ -0,0 +1,79 @@ +# STEP-061 Android 最终验收报告 v1 + +日期:2026-09-09 +结论:`PASS` + +## 1. 验收对象 + +- 分支:`particle/main` +- Source commit:`4467adcce02db7f1bbbea1f4a829023c53085e39` +- AAR:`org.maplibre.gl:android-sdk-opengl:13.4.1-step061` +- Android Test Host:`D:\React\MapLibreParticleRNTestHost-Win11-STEP060-20260907` +- Android 设备:`192.168.1.146` + +## 2. AAR 与 Maven + +AAR 按固定命令构建: + +`JOBS=1 RENDERER=opengl BUILDTYPE=Release make apackage` + +构建输出确认 `--max-workers=1`,最终 `BUILD SUCCESSFUL in 7m 10s`。Maven Local publish 为 `BUILD SUCCESSFUL in 20s`。 + +Linux build output 与 Maven Local AAR SHA-256 均为: +`40ec19724cbe0807c09c6acf9b7e6e0dac2f2498f1269b6bacdd35740a6cd77a`。 +CARBON7i 使用 `curl.exe -L` 并为 AAR/POM/module 分别使用新的 `?v=` 下载;Win11 下载 AAR 与 Maven Local AAR SHA-256 均与 Linux 一致。 + +Bridge 与 MLRN 两条 `dependencyInsight` 均只解析到 `org.maplibre.gl:android-sdk-opengl:13.4.1-step061`,两次 Gradle 检查均 `BUILD SUCCESSFUL`。 + +## 3. APK 与启动环境 + +- `:app:assembleDebug`:`BUILD SUCCESSFUL in 1m 50s`,206 actionable tasks,22 executed、184 up-to-date。 +- Manifest 保持 `android:screenOrientation="landscape"`。 +- Metro `8091` 正常监听。 +- ADB reverse:`tcp:8091 -> tcp:8091`、`tcp:8081 -> tcp:8091`。 +- `adb install -r`:Success;`.MainActivity` 正常启动。 +- 验收 PID:`24476`。 + +## 4. Binary U/V runtime hard gate + +首次进入时为预期初态:`WAITING_FOR_FIELDS / PAUSE / AUTO`、Binary PENDING、`TIMELINE_NOT_READY`。 + +点击 `SUBMIT BINARY WIND T0/T1` 后,再执行 GETSTATE 重新读取 Native state,稳定结果为: + +- `runtime: READY / PAUSE / AUTO` +- `Binary Transport: READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV` +- `Invalid Probe: REJECTED_ADAPTER` +- `T0 Submit Result: ACCEPTED_PENDING_INITIAL_WINDOW` +- `T1 Submit Result: INITIAL_WINDOW_READY` +- `Runtime Field Window: Core evidence: INITIAL_WINDOW_READY + runtime READY` +- producer identity:`T0=1@1767225600000`、`T1=2@1767229200000` +- MIX:`Core-owned / not exposed` + +清空 logcat 后再次重复提交,结果仍一致。负向 probe 的 Native Warning 明确为:`HOST_FIELD_SUBMIT_REJECTED`、`REJECTED_ADAPTER`、`PAYLOAD_SIZE_MISMATCH`、`payloadBytes=95`,证明非法 payload 进入既有 Core Adapter 并 fail-closed。 + +## 5. Logcat hard gate + +当前 App PID 下以下计数均为 0: + +- `FATAL EXCEPTION` +- `SIGSEGV` +- `WEATHER_OBJECTS_NOT_READY` +- `JNI DETECTED ERROR` +- `UnsatisfiedLinkError` +- `NoSuchMethodError` + +Release AAR 中 `HOST_FIELD_SNAPSHOT_READY` 与 `HOST_FIELD_INITIAL_WINDOW_READY` 是 `Log::Info`;本轮设备 logcat 未观察到这两条 INFO 事件,因此未将其伪造为验收证据。正向证据以 bridge 直接返回的 Core result 与 GETSTATE 的 Native `READY` 为准。 +## 6. 边界与清理 + +- 未进入 NavSea。 +- 未把 provider/network/vendor/Weather Service/download/cache/prefetch 逻辑放入 MapLibre Native/Core。 +- 正式 transport 未使用 `number[]`、payload JSON、base64 或 hex。 +- 未加入或运行 Jest。 +- 本轮未启用临时 STEP-060 full-validation UI;legacy runtime `App.tsx` 与 Git 当前 `App.tsx` SHA-256 一致,baseline UI 保持不变。 +- `/mnt/nas/pbf` 中 `13.4.1-step061` 临时 AAR/POM/module 已全部删除。 + +## 7. 最终判定 + +Binary file-reference transport、非法 payload fail-closed、T0 pending、T1 initial-window-ready、Native runtime READY、依赖唯一性、AAR/APK 构建及 crash hard gate 全部通过。 + +STEP-061 可正式标记为 `CLOSED / PASS`。 diff --git a/docs/报告/STEP-061_Binary_UV_Field_Transport_Codex完成报告_v1.md b/docs/报告/STEP-061_Binary_UV_Field_Transport_Codex完成报告_v1.md new file mode 100644 index 000000000..8ac9654f0 --- /dev/null +++ b/docs/报告/STEP-061_Binary_UV_Field_Transport_Codex完成报告_v1.md @@ -0,0 +1,83 @@ +# STEP-061 Binary U/V Field Transport Codex完成报告 v1 + +日期:2026-09-09 +结论:`IMPLEMENTED / WAITING_RDC_REVIEW` + +## 调查结论与真实路径 + +开工前确认 `particle/main` 的 HEAD `e3c36880960ddb74abeafefe5c5662e1a61b46dc` 与 `origin/particle/main` 一致。STEP-053/057 已有唯一正式 ingress: + +`RN Test Host → WeatherRuntimeBridge → WeatherRuntime.submitWindField(metadataJson, byte[]) → MapLibreMap.submitRasterArraySourceHostField → JNI → RasterArraySource::submitHostField → LocalParticleFieldFeed → ParticleFieldDataAdapter → Core Timeline/Runtime Field Window`。 + +因此本次没有新建 RasterArray、Descriptor、Snapshot、Timeline 或 Adapter,也没有改动 Core。 + +## 修改清单 + +- `integration/react-native/MapLibreParticleRNTestHost/App.tsx`:新增固定 binary fixture 提交按钮与可见证据。 +- `integration/react-native/MapLibreParticleRNTestHost/src/canonicalWindFixture.ts`:新增确定性 4×3 T0/T1/invalid canonical WIND fixture。 +- `integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/src/NativeWeatherRuntimeBridge.ts`、`src/index.ts`:新增稳定 `clearWindFields` 与 `submitWindField(metadataJson, payloadFileUri)` contract/result type。 +- `integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/src/main/java/.../WeatherRuntimeBridgeModule.kt`:本地文件读取、大小保护与既有 WeatherRuntime 调用。 +- `integration/react-native/MapLibreParticleRNTestHost/package.json`、`package-lock.json`:将现有实际安装的 `expo-file-system@56.0.11` 提升为 Test Host 直接依赖。 +- `integration/react-native/MapLibreParticleRNTestHost/README.md`、本 Step 记录与开发记录:补充 transport contract/状态。 +- `platform/android/VERSION`、`integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js`、bridge `android/build.gradle`:只将待发布坐标统一为 `13.4.1-step061`,不改 Core 行为。 + +## RDC NEEDS FIX 修正 + +- App 初始状态和每次提交开始都为 PENDING;仅当 invalid/T0/T1/Native state 严格分别等于 `REJECTED_ADAPTER`、`ACCEPTED_PENDING_INITIAL_WINDOW`、`INITIAL_WINDOW_READY`、`READY` 时才显示 READY。任一不符立即显示 ERROR。 +- Runtime Field Window 文案改为 `Core evidence: INITIAL_WINDOW_READY + runtime READY`,另行标注 producer identity;MIX 为 `Core-owned / not exposed`。Android 最终 T0/T1 真值继续由既有 `HOST_FIELD_SNAPSHOT_READY` 与 `HOST_FIELD_INITIAL_WINDOW_READY` logcat 事件验证,不新增 Core getter。 +- Kotlin metadata 长度按 UTF-8 bytes 检查;无 URI scheme 的路径必须 absolute,并显式捕获 `IOException` 为 `PAYLOAD_FILE_UNREADABLE`。读取相关其他运行时异常同样 fail-closed。 +- 版本坐标统一为新的 `13.4.1-step061`;`13.4.1-step060f` 保持冻结。本轮依照明确限制未实际 build AAR 或 Maven publish。 + +## Binary transport + +JS 以 `ArrayBuffer + DataView.setFloat32(offset, value, true)` 明确写入 `float32-le`,每个 row-major north-west grid cell 连续写入 U(eastward)再 V(northward),layout 为 `interleaved-uv`。Expo `File.write(Uint8Array)` 将 raw bytes 写入 App cache。跨 TurboModule 的仅有 `metadataJson + file:// URI/path`,不传 `number[]`、`Array`、payload JSON、base64 或 hex。 + +Kotlin 仅接受 `file://` 或绝对本地路径,拒绝其他 scheme 和相对路径;metadata 限制按 UTF-8 bytes 检查。检查文件存在、可读、非空及不超过 64 MiB 后读取 raw `byte[]`,显式捕获 `IOException`,直接调用既有 `WeatherRuntime.submitWindField(metadataJson, payload)`。文件错误不会提交空数据或伪造成功。metadata/payload 的 schema、grid、bounds、layout、encoding、byteLength、generation、validTime 等最终校验继续完全由 `LocalParticleFieldFeed`/`ParticleFieldDataAdapter` fail-closed。 + +## RN 验收证据与预期 + +`Submit binary WIND T0/T1` 先调用既有 `clearWindFields()`,再提交: + +1. 声明正常 byteLength、实际少一个 byte 的 INVALID:预期 `REJECTED_ADAPTER`。 +2. generation=1、validTime=T0 的 frame:预期 `ACCEPTED_PENDING_INITIAL_WINDOW`。 +3. generation=2、validTime>T0 的兼容 frame:预期 `INITIAL_WINDOW_READY`。 + +UI 初始与每轮开始均显示 PENDING;只有四个严格断言全部通过才显示 `READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV`。`Core evidence: INITIAL_WINDOW_READY + runtime READY` 是已建立 initial window 的证据;generation/time 单列为 producer identity,不计算或持有 T0/T1/MIX runtime 状态;MIX 仍为 Core-owned/not exposed。最终 Android T0/T1 真值由既有 `HOST_FIELD_SNAPSHOT_READY` / `HOST_FIELD_INITIAL_WINDOW_READY` logcat 验证,本任务未新增 Core getter。 + +## 实际静态验证 + +- PASS(本轮修正后):RN Test Host `npm run typecheck`(build-output-throttle)。 +- PASS(本轮修正后):RN 0.85.2 Codegen 输出至 `/tmp/step061-weather-runtime-codegen`;Java abstract methods 仍为 `clearWindFields(double, Promise)` 与 `submitWindField(double, String, String, Promise)`,与 Kotlin override 一致;未提交生成物。 +- PASS(本轮修正后):`node --check plugins/withParticleNativeAndroid.js`。 +- PASS(本轮修正后):禁止 transport 审计未发现 base64、hex、number-array 或 raw-payload-JSON;`git diff --check` 通过。 + +## 未执行与范围 + +未执行 `make apackage`、AAR build/publish、Windows Maven Local、Windows APK、Android Gradle APK、ADB、设备/端到端验收,也未加入、运行 Jest。下一阶段需构建并 Maven Local publish 唯一 `13.4.1-step061` AAR,绝不覆盖冻结的 step060f;本轮 Codex 任务书限制在 RDC 源码审查前不得构建/发布 AAR。未修改任何 MapLibre Core、JNI、WeatherRuntime、NavSea、provider/network/vendor/download/cache 或粒子视觉代码。 + +## 工作区状态 + +本次未 commit、未 push。最终 `git status --short` 与本任务有关的范围为: + +```text + M integration/react-native/MapLibreParticleRNTestHost/App.tsx + M integration/react-native/MapLibreParticleRNTestHost/README.md + M integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/src/main/java/.../WeatherRuntimeBridgeModule.kt + M integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/src/NativeWeatherRuntimeBridge.ts + M integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/src/index.ts + M integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/build.gradle + M integration/react-native/MapLibreParticleRNTestHost/package-lock.json + M integration/react-native/MapLibreParticleRNTestHost/package.json + M integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js + M platform/android/VERSION + M 粒子开发记录.md +?? integration/react-native/MapLibreParticleRNTestHost/src/canonicalWindFixture.ts +?? docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v1.md +?? docs/报告/STEP-061_Binary_UV_Field_Transport_Codex完成报告_v1.md +``` + +开始前已存在大量历史 untracked 文档、`STEP-046-e48fa087-review.diff`、Test Host `android/` 生成目录及 bridge `android/build/`;均未触碰。`.codex-build-logs/` 为验证工具生成的未跟踪日志目录,未纳入提交。 + +## 停止条件 + +已停在工作区,等待 ChatGPT 通过 RDC 审查真实 diff;审查前不继续 AAR 或 Android 端到端流程。 diff --git a/docs/报告/STEP-062_RNTestHost真实WeatherV2底图粒子综合验证_Codex完成报告_v1.md b/docs/报告/STEP-062_RNTestHost真实WeatherV2底图粒子综合验证_Codex完成报告_v1.md new file mode 100644 index 000000000..01fdaccde --- /dev/null +++ b/docs/报告/STEP-062_RNTestHost真实WeatherV2底图粒子综合验证_Codex完成报告_v1.md @@ -0,0 +1,23 @@ +# STEP-062 RN Test Host 真实 WeatherV2 底图粒子综合验证 — Codex完成报告 v1 + +日期:2026-09-09 + +结论:**IMPLEMENTED / WAITING_ANDROID_VALIDATION**。 + +## 实际实现 + +Test Host `App.tsx` 已改为真实综合入口。style 固定为 background、`gsi-standard` raster、`rn-weather-color`、`rn-weather-particles`;GSI URL 为 Android Validation 同一标准地图 URL,tileSize=256、maxzoom=18、attribution 为国土地理院/GSI。MLRN 11.3.6 `Camera` 使用初始 center `[135,32]`、zoom `3.9`。RDC 复审后将 Basemap `READY` 从 style attach 成功改为由 `onDidFinishLoadingMap` 触发;`onDidFailLoadingMap` 独立置 `ERROR`,避免把 WeatherRuntime attach 误当成底图完成。 + +新增 `src/realWeatherDataset.ts` 以静态 `require()` 注册四个 `.f32` 与其 metadata。点击 `LOAD REAL WEATHER T0-T3` 后调用 existing `clearWindFields()`;对每一个 `Asset.fromModule(...).downloadAsync()` 的 `localUri` 强制要求 `file://`,再将小型 `metadataJson + payloadFileUri` 经 STEP-061 bridge 提交。raw payload 未转成 JS number[]、JSON、base64 或 hex。预期结果为 T0 `ACCEPTED_PENDING_INITIAL_WINDOW`、T1 `INITIAL_WINDOW_READY`、T2/T3 `ACCEPTED_NEXT_SNAPSHOT`,最后重新读取 Native `READY`;任意不符显示 ERROR。 + +冻结控制面已接入:PLAY/PAUSE、HALF/NORMAL/DOUBLE、按四个 producer epoch timestamp 的 seek、FPS AUTO/30/60、WeatherColor/Particle 各自 visibility、WeatherColor stops、particle/trail 色彩,以及 count、line width、length、particle/trail opacity、trail fade、WeatherColor opacity。没有新增 Native API,JS 不维护 T0/T1/MIX runtime window。 + +## 资产 SHA-256 对照 + +源为 `platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-weatherv2/ecmwf-hres-wind-20260807/`,目标为 Test Host `assets/weather/...`;dataset、四个 metadata 与四个 `3,155,976`-byte field.f32 全部逐文件一致。field hashes:T0 `363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283`;T1 `ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451`;T2 `d86a6a51fd5216cf9368756962a5366fd32a627afd40cfde235ba7358c02c5a0`;T3 `970785b0334a9d1511b8897874a81f681814a32b1e0b90e37cf64b77ca9fa39b`。 + +## 边界与验证 + +未修改 Native/Core、JNI、`WeatherRuntime.java` 或 `modules/weather-runtime-bridge`;未构建、发布 AAR,未进入 NavSea,未加入/运行 Jest。`npm run typecheck`(build-output-throttle)PASS;Metro Node syntax/config `.f32` 检查 PASS;资产 SHA 对照 PASS。RDC 追加执行 `npx expo export --platform android` 也 PASS,Metro 实际导出四个 `3,155,976`-byte `.f32` assets。Android Gradle、APK、ADB、安装和设备验收均未执行,仍需后续 APK-only 人工周期验证地图 tile、真实提交、视觉对齐、交互与稳定性。 + +`git diff --check` 已 PASS。Codex sandbox 的 `.git/index.lock` 为只读,因此 Git 收尾由 RDC 精确接管。功能实现提交为 `a91e3601f37f3adfe5d8b7b6a6c3f7f04fcadb8f`,message=`feat: add STEP-062 real weather test map`,已成功 push 到 `origin/particle/main`;未带入历史 untracked/generated 文件。 diff --git a/docs/报告/STEP-063_MapLibre粒子Android_v1发布工程化_Codex完成报告_v1.md b/docs/报告/STEP-063_MapLibre粒子Android_v1发布工程化_Codex完成报告_v1.md new file mode 100644 index 000000000..0c705799e --- /dev/null +++ b/docs/报告/STEP-063_MapLibre粒子Android_v1发布工程化_Codex完成报告_v1.md @@ -0,0 +1,42 @@ +# STEP-063 MapLibre 粒子 Android v1 发布工程化 — Codex完成报告 v1 + +结论:`IMPLEMENTED / WAITING_MANUAL_RELEASE_VALIDATION`。 + +## 真实基线与发布契约 + +实际确认 upstream Android 基础为 `13.4.1`,共同 core 为 `18545ca10d296704b2cba675b182ff2c8da24588`,当前发行 renderer 为 OpenGL。正式版本采用 `13.4.1-particle.1`;release manifest 位于 `release/maplibre-particle/release-manifest.json`。正式 npm 包路径为 `packages/maplibre-particle-rn/`,名称为 `@navsea/maplibre-particle-rn`;正式 Maven 坐标为 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`。 + +原开发 AAR `org.maplibre.gl:android-sdk-opengl:13.4.1-step061` 只保留在历史 Step/验收说明中。正式 Android publication、RN package Android bridge 与 package plugin 均指向正式 NavSea coordinate;未继续把 step061 用作当前发行依赖。 + +## RN package 与 Consumer + +正式 package 保留 Native WeatherRuntime bridge,re-export 当前 MLRN 公共 Map API。当前仓库不是完整 MLRN fork,故 package 将 `@maplibre/maplibre-react-native@11.3.6` 作为 peer dependency,并仅以 devDependency 支撑包自身开发;Consumer 只直接安装 NavSea package,npm 将唯一 MLRN 解析到 App 根依赖层,不应额外直接安装第二份官方 MLRN。package plugin 利用 Gradle dependency substitution,将 MLRN 的唯一 OpenGL SDK 请求替换为正式 NavSea AAR,避免两份 Native AAR。 + +Consumer Host 位于 `integration/react-native/MapLibreParticlePackageConsumerTestHost/`,只声明 `@navsea/maplibre-particle-rn@13.4.1-particle.1`,从其导入 Map、Camera、WeatherRuntime 和 style helper;没有 `file:`、workspace/source-path package dependency,也不复制 bridge 或引用 Native source tree。 + +## tarball 与静态验证 + +发布前 Consumer 验证修正 MLRN 为 peer dependency 后重新 `npm pack`,生成 `navsea-maplibre-particle-rn-13.4.1-particle.1.tgz`(9,100 bytes,SHA-1 `247e2db901f6588c9e48bc3156a448b97d203d0d`,SHA-256 `31335791a5a6e553a538f84816322e8cd119f16d7c2b15436ce785aa497da6a8`)。旧 tarball 哈希不再有效。tarball 含 package JSON、TS source/types、Android bridge/Gradle、autolinking config、Expo plugin、README、CHANGELOG、LICENSE;不含 node_modules、.git、build cache、APK、Test Host、WeatherV2 assets、logs、无关 docs 或凭证。 + +package JSON 和两个 plugin Node syntax 检查 PASS;正式 package TypeScript check PASS(经 build-output-throttle);release manifest/package/Consumer version contract、无 `file:` consumer dependency、正式 build config 无 step061 与 `git diff --check` 均 PASS。Consumer 预发布验证已实际安装 tarball。首次安装暴露 MLRN 被普通 dependency 嵌套到 `@navsea/.../node_modules/@maplibre/...`,Windows RN codegen 因路径超过 260 字符失败;该问题已通过 peer dependency 修正。修正后需要重新安装新 tarball、clean prebuild 并继续 APK 验证。没有运行 Jest。 + +## 未执行的人工验证与命令 + +Codex 未执行 Android Gradle、AAR build/publish、Maven Local publish、APK、ADB 或实体机测试。用户后续在具备 Android 环境时执行: + +```bash +cd platform/android +# 按当前正式 publication 配置手工构建并发布到 Maven Local 或临时 staging。 +./gradlew :MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain + +cd packages/maplibre-particle-rn +npm pack --pack-destination /tmp/maplibre-particle-release + +cd integration/react-native/MapLibreParticlePackageConsumerTestHost +npm install /tmp/maplibre-particle-release/navsea-maplibre-particle-rn-13.4.1-particle.1.tgz --save-exact +npm install --ignore-scripts +npx expo prebuild --platform android --clean +npx expo run:android +``` + +随后验证 GSI、WeatherColor、Particle、canonical fixture submit 与 Runtime READY。通过前不得创建正式 release tag,也不得写 `RELEASED` 或 `CLOSED / PASS`。 diff --git a/docs/报告/STEP-063_MapLibre粒子Android_v1发布工程化_Codex完成报告_v2.md b/docs/报告/STEP-063_MapLibre粒子Android_v1发布工程化_Codex完成报告_v2.md new file mode 100644 index 000000000..e56ed3d42 --- /dev/null +++ b/docs/报告/STEP-063_MapLibre粒子Android_v1发布工程化_Codex完成报告_v2.md @@ -0,0 +1,91 @@ +# STEP-063 MapLibre 粒子 Android v1 发布工程化 — 完成报告 v2 + +结论:`CLOSED / PASS`。 +日期:2026-09-09。 + +## 1. 最终发布契约 + +- MapLibre Native upstream:`13.4.1`。 +- Core commit:`18545ca10d296704b2cba675b182ff2c8da24588`。 +- renderer:OpenGL。 +- npm package:`@navsea/maplibre-particle-rn@13.4.1-particle.1`。 +- Maven:`cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`。 +- Maven repository:`https://pbf.jpcn.ccwu.cc/maven`。 +- release manifest:`release/maplibre-particle/release-manifest.json`。 +- Consumer:`integration/react-native/MapLibreParticlePackageConsumerTestHost/`。 + +正式 package 的职责是提供 React Native facade、WeatherRuntime bridge、Expo/Gradle 接线并复用唯一 MLRN peer dependency;正式 Android Native 来自 Maven AAR,不通过源码路径或 Maven Local 消费。 + +## 2. 正式制品与哈希 + +Android AAR:166,304,929 bytes,SHA-256: + +`63c976c5a73fe8f132f428afecca40b09fa2dbdedaa13721586c472ea7eb279d` + +npm tarball:`navsea-maplibre-particle-rn-13.4.1-particle.1.tgz`,9,100 bytes;SHA-1 `247e2db901f6588c9e48bc3156a448b97d203d0d`;SHA-256 `31335791a5a6e553a538f84816322e8cd119f16d7c2b15436ce785aa497da6a8`。 + +## 3. Consumer 依赖问题与最终修正 + +首次 Windows Consumer APK 编译失败原因不是 Particle AAR,而是正式 npm 包把 MLRN 作为普通 dependency,导致 native module 嵌套在 `@navsea/.../node_modules/@maplibre/...`,RN codegen/CMake 路径超过 Windows 260 字符限制。 + +修正提交:`fa431ba6fd1f182b8b77345b50a9d74e225f17d4`,将 `@maplibre/maplibre-react-native@11.3.6` 调整为 peer dependency,并仅保留 devDependency 支撑包自身开发。 + +修正后实际安装验证:根层 MLRN 存在,NavSea package 内嵌 MLRN 不存在;`npm ls` 只形成一份 MLRN native module。Gradle `dependencyInsight` 明确显示: + +```text +org.maplibre.gl:android-sdk-opengl:13.4.1 +→ cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1 +``` + +因此正式 Consumer APK 中使用的是正式 Particle AAR,而不是 step061、官方 OpenGL AAR、Maven Local 或 MapLibre Native 源码构建。 + +## 4. 真实 WeatherV2 验收 Host + +为了让 Package Consumer 真正模拟未来 NavSea 升级包后的使用方式,Consumer 从原 2×2 smoke fixture 升级为真实 WeatherV2 T0–T3。实现提交:`3ac678a3c8164082452bd77e61989ccc928ed8bd`,message:`test: validate release package with real WeatherV2`,已 push 到 `origin/particle/main`。 + +Consumer 仍只从 `@navsea/maplibre-particle-rn` 导入 `Map`、`Camera`、`WeatherRuntime`;没有重新引入旧 `weather-runtime-bridge` 或 direct MLRN import。 + +真实数据为 ECMWF HRES 2026-08-07 `00/06/12/18` 四帧。每帧 `513×769`、`3,155,976 bytes`,canonical contract 为 `maplibre-particle-field/v1`、`float32-le`、`interleaved-uv`,U eastward、V northward;React transport 为 `metadataJson + local file:// payload`。 + +Linux 已通过: + +- Consumer TypeScript typecheck; +- package peer/hoist 结构检查; +- 禁止 direct MLRN / 旧 bridge 依赖审计; +- Metro `.f32` asset 配置; +- `npx expo export --platform android`,实际导出四个 3.2MB `.f32`; +- `git diff --check`。 + +没有执行 Jest。 + +## 5. Windows / Android 最终人工验收 + +Win11 Consumer Android 环境按 NavSea 当前编译基线对齐:Gradle 8.14.3、buildTools 36.0.0、minSdk 24、compileSdk/targetSdk 36、NDK 27.1.12297006、Kotlin Plugin 2.1.20、JDK 17、RN 0.85.2、New Architecture 与 Hermes 开启。 + +用户手工完成正式 Package Consumer 的 Android 编译和安装,并在实体设备运行;2026-09-09 用户明确确认:`这个APK确认正常了`。该 APK 对应已经升级为真实 WeatherV2 T0–T3 的 Package Consumer,因此 STEP-063 要求的正式 package / Maven AAR / Runtime / WeatherColor / Particle 消费链人工门槛判定 PASS。 + +本报告不把未单独提供的 PID logcat 全量 hard-gate 扫描写成已执行;PASS 依据正式依赖解析、成功 APK 编译安装、真实 Consumer 运行以及用户最终确认。 + +## 6. 关闭边界 + +- 未修改 Particle GPU、WeatherColor 算法、Timeline、HostField canonical contract、JNI/Core 业务功能。 +- 未接入 NavSea 仓库。 +- 未执行 Jest。 +- npm registry 仍为 `null`。 +- release tag 仍为 `null`。 + +因此 STEP-063 的 `CLOSED / PASS` 只表示发布工程化、正式制品和 Package Consumer Android 验收完成;不等于 npm 已正式上传,也不等于 release tag 已创建。下一步应单独执行 Release Freeze / Tag。 + +## 7. React 使用说明 + +新增: + +`docs/30_设计/MapLibre粒子ReactNative使用说明_v1.md` + +该说明从 React Native 使用角度记录:正式包安装模型、Expo plugin、Map attach/detach、WeatherRuntime API、真实 T0–T3 提交方式、NavSea/Core 边界,以及最重要的 canonical 显示数据结构。 + +数据结构明确为规则二维 NW-origin row-major Grid;payload 是 contiguous `float32-le / interleaved-uv`,每 cell 8 bytes,`cell=y*width+x`,U eastward-positive、V northward-positive;metadata 包含 schema、field identity、generation、validTime、bounds、grid、noData 和 byteLength。文档同时区分当前 Native 强制校验字段与仅用于审计的额外 metadata。 + +## 8. Git 基线 + +本轮最终人工验收前的主线实现 HEAD:`3ac678a3c8164082452bd77e61989ccc928ed8bd`,已与 `origin/particle/main` 一致。本文档收尾只允许精确提交本轮新增/更新文档与 release manifest,不提交历史无关 untracked 文件。 diff --git a/docs/报告/STEP-064_RN纯FieldRenderer接口V2_Codex完成报告_v1.md b/docs/报告/STEP-064_RN纯FieldRenderer接口V2_Codex完成报告_v1.md new file mode 100644 index 000000000..3ad3569cc --- /dev/null +++ b/docs/报告/STEP-064_RN纯FieldRenderer接口V2_Codex完成报告_v1.md @@ -0,0 +1,58 @@ +# STEP-064 RN 纯 Field Renderer 接口 V2 完成报告 + +状态:RELEASED / CLOSED / PASS + +## 完成内容 + +`@navsea/maplibre-particle-rn` 已升为 `13.4.1-particle.2`。根入口仅公开 `FieldRuntime`、纯渲染类型、`particleFieldIds` 与 `createParticleFieldStyleFragment()`;旧 Weather/Wind 公共名称、MLRN facade 导出与兼容 wrapper 均已删除。 + +`FieldRuntime.submitField()` / `clearFields()` 继续薄映射既有 Native ingress。图场与粒子消费同一 canonical Field,分别由 `setFieldVisible()` 与 `setParticleVisible()` 控制。`FieldColorStop` 使用 `value`,RN 内部才转换既有 Native ABI 所需字段。 + +package 内 WeatherV2 HTTP、manifest、time index、WVECBIN1 decoder 与高层 loader 已移除。Consumer Test Host 改为直接使用官方 MLRN 的 Map/Camera,并自行保有 GSI、真实 fixture 与本地文件物化;最终仍只向 SDK 提交 metadata 与 file URI。 + +## 冻结边界 + +- Native/Core/JNI/shader/Android AAR 未修改。 +- Native artifact 为 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`;`.2` 仅为 RN/TS 公共 API revision。 +- 未构建/发布 AAR,未执行 Android Gradle、APK、ADB 或 Jest,未进入 NavSea。 + +## 静态验证 + +- package TypeScript typecheck:通过。 +- Consumer TypeScript typecheck:通过;因 Consumer 尚未安装 `.2` 发布包,检查采用临时 TypeScript module-resolution 配置,不改变 Consumer 产品依赖。 +- `npm pack`:通过,`.2` tarball 仅生成在 `/tmp/step064-pack/`,未触碰仓库既有未跟踪 `.1` tarball。 +- tarball 内容审计:通过;不含 `src/weatherV2/`、WeatherV2 HTTP/decoder/loader、Test Host、node_modules 或构建缓存。 +- package 根导出、业务语义残留审计和 `git diff --check`:通过。旧 Weather/Wind 名称仅留在内部 Native ABI/codegen 接缝及其薄映射实现,不构成公开 API。 +- RN 0.85.2 Codegen parser 已核对支持 `TurboModuleRegistry.get()` 与 `getEnforcing()`;当前 import-safe `get()` 方案保留。 +- 收尾复审修正:`.2` Consumer validation 明确为 pending,`.1` PASS 仅作为 previous baseline;FieldColorStop 文档示例改为满足至少 2 个 stop 的既有 Native 契约。 + +## 未完成与后续最小验证 + +Consumer 尚未安装最终 `.2` tarball 并在既有 `.1` Native artifact 的 APK 中运行。后续最小步骤是:安装 `.2` tarball,启动 Metro,确认 Map/Camera 来自官方 MLRN,加载宿主 canonical T0-T3 fixture,确认 `FieldRuntime` attach、提交、独立开关与控制命令正常。该人工 Consumer 验证完成前,本 Step 保持等待状态。 + +## Git 收尾 + +- 实现提交:`59a48608fb140d04f9008cb8273119999a87b6ef` +- 提交信息:`refactor: make RN particle API field-renderer only` +- Push:成功,`particle/main -> origin/particle/main`。 +- 实现提交 push 后:`HEAD == origin/particle/main == 59a48608fb140d04f9008cb8273119999a87b6ef`。 +- Codex sandbox 曾因 `.git/index.lock` 只读失败未能自行提交;最终由 ChatGPT 通过同一 Linux 仓库精确 stage、commit、push,本任务无关 modified/untracked 文件未带入。 +- 本段为 docs-only 收尾更新;最终仓库 HEAD 以任务最终回复记录为准。 + +## 2026-09-10 Consumer 验证与 Release Freeze 前结论 + +- 固定 Win11 Consumer 已安装 `.2` tgz,依赖树为一份 `@maplibre/maplibre-react-native@11.3.6`。 +- 首轮发现 `createParticleFieldStyleFragment()` 使用无效 `raster-particle-opacity`,Native 报 particle layer missing;已在 RN-only 范围改为 `raster-particle-active`。 +- 修正后 attach、T0–T3、Runtime READY、图场/粒子独立开关及恢复均通过。 +- 未重新构建 Android APK/AAR;复用既有 Native `.1` APK + Metro 验证 RN `.2`。 +- hard gate 无上述 Native/崩溃错误。当前允许进入 release tag 与固定 HTTPS tgz 发布。 + +## 正式发布结果 + +- Release source:`e43b58707cfd0f87965424477ed3f95625b3f724`。 +- Tag:`maplibre-particle/rn/13.4.1-particle.2`,已推送远端。 +- 正式 tgz URL:`https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.2.tgz`。 +- size:10,660 bytes;SHA256:`ace9e0401824839d18a93cc4201c1f5d4cccbff0636570cfde82fd80b60779b5`。 +- NAS 文件、tag pack 与公网重新下载内容一致;公网返回 HTTP 200。 +- Win11 固定 Consumer 从正式 HTTPS URL 重装成功,`@navsea/maplibre-particle-rn=13.4.1-particle.2`、MLRN=11.3.6 且 dedupe,TypeScript PASS。 +- Native AAR 仍为 `13.4.1-particle.1`,本次没有重新构建、修改或发布 AAR。 diff --git a/docs/报告/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_Codex完成报告_v1.md b/docs/报告/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_Codex完成报告_v1.md new file mode 100644 index 000000000..6374a4686 --- /dev/null +++ b/docs/报告/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_Codex完成报告_v1.md @@ -0,0 +1,33 @@ +# STEP-065 MapLibreParticleLab V3 通用 Scalar / Vector Field 架构冻结 Codex 完成报告 + +状态:CLOSED / PASS(设计冻结;非实现/构建验收) + +## 已完成 + +已依据文件/符号级真实代码完成 Contract、GPU、粒子单位、binary ingress、timeline/reuse、RN ABI 与 WeatherV2 指定源文件的只读调查;正式冻结 `maplibre-particle-field/v2`、0/1 scalar + 0/1 vector + shared validity、`SCALAR|VECTOR_MAGNITUDE`、`VELOCITY|DIRECTION`、独立 plane file references、v1 thin normalize 和 V3-02 至 V3-08 实施/验收路线。四类 WeatherV2 映射、Wave synthetic independence fixture、风险和受影响文件矩阵均已记录。 + +## 逐项状态 + +|项目|结果| +|---|---| +|生产代码修改|未完成(按任务边界:否)| +|WeatherV2/ NavSea 修改|未完成(按任务边界:否)| +|Android Gradle / APK / ADB|未执行(按任务边界)| +|Jest|未加入、未执行(按任务边界)| +|只读架构调查与 V3 Contract|已完成| +|文档/记录|已完成| +|构建/运行验证|无法验证(任务禁止且本任务不实现)| + +## 未完成项与风险 + +没有本 Step 的文档交付阻塞。后续实现未解决项:Kotlin/JNI 采用三 nullable `byte[]` 或带 offsets 的 v2 container 的 ABI 决策、64 MiB 单文件限制向总 plane 上限的调整、GPU texture format/内存实测、DIRECTION 的产品 advection scale 默认值、Android GL/Vulkan 与未来 Metal 验证。它们均不应由本设计任务臆断为已通过。 + +建议下一任务为 `STEP-066 V3-02 Core Contract + Scalar Snapshot/Adapter`:只改 Core Contract/adapter 和离线 fixture,首先锁定 v1 normalize、三组合 validation、timeline compatibility;不得提前提交 Android build。 + +## Git 收尾 + +- Scoped content commit:`44999edb256ae73c3abc94ef3f0d208730c47897`。 +- Commit message:`docs: freeze V3 scalar vector field contract`。 +- Push:成功,`particle/main -> origin/particle/main`(`2e3ab2627..44999edb2`)。 +- 该内容提交 push 后复核:`HEAD == origin/particle/main == 44999edb256ae73c3abc94ef3f0d208730c47897`。 +- 本报告的此 Git handoff 回填将以单独 docs-only scoped commit 推送;最终远端 HEAD 以任务最终回复所列值为准。除任务书与五个文档/记录外未暂存任何文件。 diff --git a/docs/报告/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_Codex完成报告_v1.md b/docs/报告/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_Codex完成报告_v1.md new file mode 100644 index 000000000..b045dddf2 --- /dev/null +++ b/docs/报告/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_Codex完成报告_v1.md @@ -0,0 +1,39 @@ +# STEP-066 MapLibreParticleLab V3-02 Core Contract 与 Scalar Snapshot Adapter Codex 完成报告 + +状态:CLOSED / PASS + +## 结果 + +V3 Core 已从 Wind-only U/V payload 统一为 generic snapshot:0/1 scalar、0/1 vector、1 shared validity。v1 仍可读取,并在 adapter ingress 转为 `VELOCITY` vector snapshot;v2 vector-only、scalar-only、scalar+vector 均由 Core 通过。无 channel、尺寸、validity、semantic、finite 或 valid-cell noData 错误均拒绝。 + +`fieldType` 与 `displayDomain` 保留为可选 legacy metadata,但不再决定 Core capability、validity 或 T0/T1 compatibility。content identity 直接比较 decoded scalar/vector/validity plane,任一 plane 改变不会误判 same-content。 + +## 逐项状态 + +|任务项|结果| +|---|---| +|规范、STEP-065、实际源码与工作区核对|已完成| +|V3 generic snapshot/descriptor/payload bundle|已完成| +|WIND/GLOBAL validity hardcode 移除|已完成| +|v2 vector-only/scalar-only/scalar+vector|已完成| +|shared validity、VectorSemantics、v1 thin normalize|已完成| +|interpolation compatibility/content identity|已完成| +|Timeline/Runtime Window ownership 保护|已完成| +|最小 Core fixtures/tests|已完成(内存 plane fixture)| +|精确 Linux/Core 编译和过滤测试|已完成| +|Android/JNI/RN/WeatherV2/NavSea 不修改|已确认| +|Step、报告、开发记录|已完成| + +## 验证与未完成项 + +Linux OpenGL `mbgl-test`、受影响 `source.test.cpp.o`、`mbgl-test-runner` 均已通过;`Source.ParticleFieldV3*` 过滤测试退出码为 0;`git diff --check` 通过。首次受限沙箱编译因 ccache 临时目录只读失败,获准使用现有构建缓存后完成,非源码错误。 + +未完成且不属于本 Step:V3-03 scalar GPU texture/`FieldValueSource`、V3-04 DIRECTION normalizing/advection、V3-05 v2 JNI/RN 三 plane ingress、真实业务 fixture/Android 验收。既有 renderer 对无 vector snapshot 明确 fail-closed,未伪造 scalar 为 U/V。 + +## Git 收尾 + +- Source/function commits:`379c9ed0e`(`core: add scalar vector field snapshots`)与 `6d1f3e051`(`core: preserve fixture times in generic snapshots`)。 +- Docs commits:`b366a2d16`(`docs: record STEP-066 core contract`)与 `9a09278a2`(`docs: record STEP-066 push handoff`)。 +- 已完成 handoff commit:`9a09278a28170446fd3e180ae5f7c2d8d558972d`。 +- 已完成 push:`particle/main -> origin/particle/main`,先后为 `d23ae2079..b366a2d16`、`b366a2d16..9a09278a2`;当时复核 `HEAD == origin/particle/main == 9a09278a28170446fd3e180ae5f7c2d8d558972d`。 +- 本次仅补纳遗留任务书与本报告 Git 收尾回填;其 scoped commit/push 后最终 HEAD 以本次收尾回执为准。 diff --git a/docs/报告/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_Codex完成报告_v1.md b/docs/报告/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_Codex完成报告_v1.md new file mode 100644 index 000000000..f07a8dd76 --- /dev/null +++ b/docs/报告/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_Codex完成报告_v1.md @@ -0,0 +1,22 @@ +# STEP-067 MapLibreParticleLab V3-03 Scalar GPU 与 Generic Field Renderer Codex 完成报告 + +状态:CLOSED / PASS + +## 生产代码修改 + +- `weather_color_layer.*` 与 `weather_color_stop.hpp`:增加明确 `FieldValueSource`(默认 `VectorMagnitude`),将 color stop 的 C++ 内部字段改为 generic `value`。 +- `render_weather_color_layer.cpp`:同一 renderer 按 source 显式上传 scalar 或 vector bundle,A 写 shared validity;source 不匹配时 unavailable。切换 source 不复用旧的不同编码,正常同 source rollover 继续重用 immutable decoded bundle。 +- `particle_gpu_validation.fragment.glsl` 与生成的 OpenGL header:Weather pass 用 UBO flag 选择 scalar 或 vector magnitude,分别对 T0/T1 做数值插值后进入通用 ramp;默认 ramp 保留旧 Wind 的 0..30 数值外观,但不再声明固定 m/s 单位。 +- `render_particle_validation_layer.cpp`:Particle 仅接受 vector + `Velocity`;scalar 或 `Direction` 清理粒子资源、不可绘制。粒子 shader 的物理 U/V 计算未改变。 + +## 验证结果 + +shader generator、三个受影响 C++ 编译对象、`Source.ParticleFieldV3*` 与 WeatherColor layer 过滤测试、`git diff --check` 均 PASS,命令输出均经 build-output-throttle 限流。此前受限终端未完成的全量重链,已由用户在当前 `HEAD`(`06fcbef9ccb820e595f06acbd356950de7e3a73f`)手工执行 `ninja -C build-linux-opengl -j3 mbgl-test mbgl-test-runner` 收尾,输出为 `ninja: no work to do.`;因此确认 `mbgl-test`、`mbgl-test-runner` 的完整目标和依赖已最新,没有待处理的构建或重链接工作。 + +该手工 Ninja 结果不等于重新执行全量测试用例:本次未重跑完整测试套件,只确认构建/重链接目标无待处理工作。 + +未执行 Android Gradle、AAR、APK、ADB 或 Jest;未触碰 STEP-062 用户修改、历史 untracked、build/cache/tgz。 + +## Git 收尾 + +实现基线提交为 `06fcbef9ccb820e595f06acbd356950de7e3a73f`(`docs: record STEP-067 scalar GPU renderer`)。本次关闭仅提交 STEP-067 任务归档、Step 记录、完成报告和开发记录;不会暂存 STEP-062 用户修改、历史未跟踪文件、build/cache 或 tgz。关闭提交 hash、push 结果与最终 `HEAD == origin/particle/main` 复核见本次交付终端回执。 diff --git a/docs/报告/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_Codex完成报告_v1.md b/docs/报告/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_Codex完成报告_v1.md new file mode 100644 index 000000000..72be052f5 --- /dev/null +++ b/docs/报告/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_Codex完成报告_v1.md @@ -0,0 +1,50 @@ +# STEP-068 MapLibreParticleLab V3-04 DIRECTION 粒子推进 Codex 完成报告 + +结论:CLOSED / PASS。 + +## 已完成 + +生产代码限定在 RasterParticleLayer Core 属性、Particle renderer、Particle shader/UBO 和 generated OpenGL shader header。`DIRECTION` 采用 T0/T1 U/V 分量插值后再归一化;零或近零、NaN/Inf 混合向量 fail-closed。`advectionScale` 默认 1.0,必须 finite 且大于零,仅作用于 DIRECTION 的推进长度。VELOCITY 没有归一化,也不读取该 scale,保持既有 U east/V north 与 Web Mercator 位移数学。 + +State 与 line vertex 都使用相同的 resolved advection 语义。renderer 仍拒绝 scalar-only、缺 semantics 和 T0/T1 semantics mismatch;字段内容 reuse、T0/T1/MIX、rollover、lifecycle 与 trail 没有重写。变更 scale 不会重传 field texture 或重建 simulation/trail。 + +增加 C++ Layer 属性测试,以及 provider-neutral CPU oracle 测试:东、北、斜向、T0 east/T1 north/MIX 0.5 的 northeast、反向归零 invalid、scale 仅改 Direction magnitude,和 VELOCITY 保持未归一化。 + +## 验证结果 + +- shader generator:PASS。 +- 最终受影响精确对象(Particle/WeatherColor renderer、RasterParticle style、source/style tests):PASS。 +- `ninja -C build-linux-opengl -j3 mbgl-test mbgl-test-runner`:PASS,完整目标重编译/重链完成。 +- STEP-068 相关过滤测试 `Source.ParticleFieldV3*:Layer.RasterParticle*`:8/8 PASS。 +- `git diff --check`:PASS。 +- Android Gradle/AAR/APK/ADB、Jest:未执行,任务明确禁止。 + +## 2026-09-11 全量 Linux 重编译遗漏修复 + +外部全量 Linux 重编译报告 `render_weather_color_layer.cpp:108` 的 +`-Wmissing-field-initializers`:共享 `ParticleGpuValidationUBO` 增加 `advection` 后, +WeatherColor 的聚合初始化未同步。本次只显式初始化该字段为 `{0, 1, 1e-5, 0}`: +Direction flag 关闭、scale 为单位值,且 WeatherColor 不消费 advection,因此不影响 +色场。审计所有 C++ 聚合初始化器后,Particle renderer 已完整初始化;WeatherColor +是唯一遗漏。 + +本次未改 shader 输入,不需为该 C++ 初始化修复重新运行 shader generator。修复后的 +WeatherColor 精确对象编译和 `git diff --check` 均 PASS(编译输出已限流保存);随后 +外部终端完成 `mbgl-test` / `mbgl-test-runner` 完整目标重编译与重链并 PASS。 + +## 独立既有测试问题、风险与下一步 + +扩大过滤范围时,当前基线自带的 `Layer.WeatherColorUsesIndependentFixedOpacity` 暴露一个与 STEP-068 无关的测试断言错误:测试已成功把 stops 改为 `{-2,3}`,最后却断言仍等于旧的 `{0,10,20}`。通过 `git show HEAD:test/style/style_layer.test.cpp` 已确认该逻辑在 STEP-068 前就存在;本 Step 不修改该旧测试,避免把无关修复混入 DIRECTION 实现。 + +STEP-068 自身相关 8 个测试全部 PASS,完整 Linux 目标也已 PASS,因此可以关闭。剩余平台风险保留到后续综合阶段:Android/Vulkan 与最终设备 shader driver 仍未在本 Step 验证。下一步为 V3-05 Android/JNI/RN generic v2 multi-plane ingress;仍不在此处引入 WeatherV2/NavSea 业务逻辑。 + + +## Git 交付 + +- 主实现 commit:`f3c33c67275b24f61b80924abbb9d10d68b2cb7c` +- commit message:`feat: add direction particle advection` +- push result:成功 +- remote branch:`origin/particle/main` +- 主实现提交推送后确认:`HEAD == origin/particle/main == f3c33c67275b24f61b80924abbb9d10d68b2cb7c` +- 本报告随后仅追加 Git 交付记录并进行独立文档收尾提交;主实现 commit 保持不变。 +- 未纳入 STEP-062 用户修改及历史无关 untracked/build/cache/tgz。 diff --git a/docs/报告/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_Codex完成报告_v1.md b/docs/报告/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_Codex完成报告_v1.md new file mode 100644 index 000000000..50b69fe3f --- /dev/null +++ b/docs/报告/STEP-069_MapLibreParticleLab_V3-05_Android_JNI_RN_v2多Plane入口_Codex完成报告_v1.md @@ -0,0 +1,27 @@ +# STEP-069 MapLibreParticleLab V3-05 Android/JNI/RN v2 多 Plane 入口 Codex 完成报告 + +结论:IMPLEMENTED / WAITING_ANDROID_VALIDATION。 + +## 已完成 + +`@navsea/maplibre-particle-rn` 已升至 `13.4.1-particle.3`。v2 `FieldRuntime.submitField` 仍接收必填 validity、可选 scalar/vector 的 `FieldPayloadRefs`;仅 Native TurboModule ABI 改为 `validityFileUri, scalarFileUri, vectorFileUri` 三个明确 string 参数,空 scalar/vector string 只表示 ABI optional marker。Kotlin 在读入前检查 local-file、非空及 aggregate 64 MiB。Java 的 explicit packed v2 container 只存在于 Java/JNI,使用 plane presence flags 和 lengths;JNI 恢复 `ParticleFieldPayloadBundle`,JS 不构造大容器。Java 对 nullable scalar/vector 有 `@Nullable` 注释,并在 Java facade 额外拒绝 total 大于 64 MiB,防止绕过 Kotlin 的 oversized packed allocation。v1 ABI 保留,`.3` 用 `submitLegacyVectorField` 明确标识。 + +Core v1/v2 ingress decode 后共用 Timeline 提交 helper,未改 duplicate、T0/T1、MIX、rollover 或生命周期。`setFieldValueSource` 仅接受 0/1 对应 `SCALAR|VECTOR_MAGNITUDE`,`setParticleAdvectionScale` 仅接受 finite 且大于零,均只改 renderer property。 + +## 验证与未完成 + +- `git diff --check`:PASS。 +- 外部复核指定的实际 codegen 命令 `node integration/react-native/MapLibreParticleRNTestHost/node_modules/@react-native/codegen/lib/cli/combine/combine-js-to-schema-cli.js /tmp/step069-schema.json packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts`:PASS。 +- `./node_modules/.bin/tsc --noEmit -p packages/maplibre-particle-rn/tsconfig.json`:PASS。 +- `source.test.cpp.o` 精确编译与 `ninja -C build-linux-opengl -j3 mbgl-test` 重链:PASS;`Source.HostFieldV2BundleUsesTheSameTimelineIngress:Source.HostFieldLegacyAndCompatibleV2VectorOnlyShareOneTimeline`:2/2 PASS,明确覆盖 legacy v1 与 compatible v2 vector-only 的同一 Timeline ingress。 +- 上述 build/codegen/test 命令均经 build-output-throttle;Android Kotlin/Java/JNI 签名只做源码一致性核对,未运行 Android 编译。 +- Android Gradle、AAR、APK、ADB 与 Jest 均按任务禁止未执行。Android Kotlin/Java/JNI compile 和设备验证必须在 V3-08 完成,故不标记 Android CLOSED/PASS。 + +## Git 收尾 + +- implementation commit:`148bf80fe41ba566de6c1d4b439cfe372a8a453c`,message:`feat: add v2 multi-plane field ingress`。 +- handoff commit:`12827489f4c377175554562632129df125c0a556`,message:`docs: record STEP-069 v2 field ingress`。 +- 首轮 push:成功,`9e9ea07bc..12827489f particle/main -> particle/main`。 +- remote branch:`origin/particle/main`;首轮 push 后 `HEAD == origin/particle/main == 12827489f4c377175554562632129df125c0a556`。 +- 本报告的 Git 结果回填会形成一个最小 metadata 收尾提交;该提交完成后再次 push,并在任务最终验收中以实际 `git rev-parse HEAD` / `origin/particle/main` 核对最终 HEAD。Git commit 无法在自身正文中稳定自包含自身最终 hash,因此不伪造自引用 hash。 +- 未纳入:STEP-062 两处用户 dirty、历史 untracked、`.codex-build-logs/`、旧 tgz/build/cache。 diff --git a/docs/报告/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex完成报告_v1.md b/docs/报告/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex完成报告_v1.md new file mode 100644 index 000000000..662f31990 --- /dev/null +++ b/docs/报告/STEP-070_MapLibreParticleLab_V3-06_四类场Fixture与RN通用回归_Codex完成报告_v1.md @@ -0,0 +1,11 @@ +# STEP-070 MapLibreParticleLab V3-06 四类场 Fixture 与 RN 通用回归 Codex 完成报告 + +结论:IMPLEMENTED / WAITING_ANDROID_VALIDATION。 + +Wind、Wave、Tide/Current 已形成真实本地离线 canonical fixture。Wind 从既有 v1 field 转换时,shared validity 现在严格要求 U/V 都 finite 且都不等于 `-9999`。Wave 直接读取 WeatherV2 `2026090912` run 的正式 `waves_particles.bin` 与 sidecar:height 进入 scalar,规范化后的 `unit_to_u/v` 进入 `DIRECTION` vector,period 被明确排除。Tide 从 CMEMS `2026090900` run 的正式 `current_vectors.bin` 读取 U/V 形成 `VELOCITY` vector。每帧摘要记录源 BIN 与 sidecar SHA-256、target_time、bounds、grid、projection 与 provenance。 + +Rain 仍是明确的 `rain-decoder-fixture`:本任务未实现 PMTiles reader,其数值不是真实场值。所有 fallback/synthetic generation 均为小且严格递增的 `1/2`,不再使用 epoch 毫秒。 + +RN Test Host 的 Wave/Tide 已改用新 real 目录;所有提交继续只使用 `@navsea/maplibre-particle-rn` 的 `FieldRuntime` 公共 API 和本地 file URI。fixture validator、package TypeScript、Host typecheck、TurboModule schema 与 `/tmp/step070-ccache` 下的 `source.test.cpp.o` 精确重编译均 PASS。随后恢复 Linux OpenGL 完整依赖闭包构建,459/459 完成并成功链接 `mbgl-test-runner`;`Source.ParticleFieldV3*` 过滤测试 6/6 PASS,整条验证链退出码 0,`git diff --check` PASS。 + +未运行 Android Gradle/AAR/APK/ADB/Jest,故没有 Android PASS 声明。implementation commit 为 `8588e6fcb4f3f5d874159c2e42619c5227cb7921`(`feat: add V3 field fixtures and RN regression host`),已成功 push 到 `origin/particle/main`。最终 handoff 只提交 STEP-070 的 Metro `.u8` asset 配置与验收文档,不纳入 STEP-062 两份用户 dirty 文档或历史 untracked/build/cache/tgz。未修改 WeatherV2、NavSea 或 Core Runtime 算法;Rain 真实 reader 与 Android/设备验收仍是后续工作,Linux 完整重链/过滤测试已经完成。 diff --git a/docs/报告/STEP-071_MapLibreParticleLab_V3-07_Linux综合冻结_Codex完成报告_v1.md b/docs/报告/STEP-071_MapLibreParticleLab_V3-07_Linux综合冻结_Codex完成报告_v1.md new file mode 100644 index 000000000..784fc8fad --- /dev/null +++ b/docs/报告/STEP-071_MapLibreParticleLab_V3-07_Linux综合冻结_Codex完成报告_v1.md @@ -0,0 +1,37 @@ +# STEP-071 — MapLibreParticleLab V3-07 Linux 综合冻结完成报告 + +结论:CLOSED / PASS(Linux)。 +日期:2026-09-11 + +## 实际完成 +V3-07 在 `particle/main` 上完成 Linux 综合冻结。最终验证链从 canonical fixture、RN/TS/codegen 到 Linux OpenGL test target 和 V3 相关 Source/Layer 回归全部通过。 + +最终结果: +- fixture validator:PASS; +- package TypeScript:PASS; +- RN Test Host typecheck:PASS; +- TurboModule codegen:PASS; +- `mbgl-test` / `mbgl-test-runner`:PASS / up-to-date; +- V3 综合过滤:21/21 PASS; +- `git diff --check`:PASS; +- 最终验证链退出码:0。 + +## 冻结中修正 +综合回归暴露两个旧测试 fixture 问题:Host T0/T1 使用重复 payload,以及 Timeline preload 三帧 `timeIndex` 全为 0。两处都通过测试侧局部修正解决;生产 Runtime 行为未改变。 + +## 未执行与边界 +- 未运行 Android Gradle/AAR/APK/ADB/Jest,因此没有 Android PASS 声明。 +- 未修改 `src/`、`include/`、shader、JNI、Android、RN runtime、WeatherV2 或 NavSea。 +- 已知 `Layer.WeatherColorUsesIndependentFixedOpacity` 既有断言债未纳入本 Step gate,也未修复。 +- STEP-062 两份用户 dirty 与历史无关 untracked/build/cache/tgz 未纳入。 + +## 下一步 +进入 V3-08:按既定策略仅安排一次 Android 编译、安装和实体机综合验收,覆盖 V3-05/V3-06 尚未完成的 Android/JNI/RN v2 multi-plane 与四类场路径。 + +## Git handoff +- 冻结提交:`a4bf6b7aea447254e2b01a95215dc67b2ba8c9a4` +- 提交信息:`test: close V3 Linux freeze` +- Push:成功 +- Remote branch:`origin/particle/main` +- Push 后已验证:`HEAD == origin/particle/main == a4bf6b7aea447254e2b01a95215dc67b2ba8c9a4` +- 本报告的 Git 元数据回填将作为单独 docs-only closeout 提交;不改变上述冻结代码基线。 diff --git a/docs/报告/STEP-072B_V3-08_RN_Kotlin_ReadableMap编译修复_Codex完成报告_v1.md b/docs/报告/STEP-072B_V3-08_RN_Kotlin_ReadableMap编译修复_Codex完成报告_v1.md new file mode 100644 index 000000000..ff252d1ef --- /dev/null +++ b/docs/报告/STEP-072B_V3-08_RN_Kotlin_ReadableMap编译修复_Codex完成报告_v1.md @@ -0,0 +1,17 @@ +# STEP-072B V3-08 RN Kotlin ReadableMap 编译修复 Codex 完成报告 v1 + +## 结论 + +**PASS / CLOSED**。 + +Codex 严格按任务边界,仅在 `WeatherRuntimeBridgeModule.kt` 增加 `com.facebook.react.bridge.ReadableMap` import,未修改 API、ABI、TurboModule spec、Maven 坐标或业务逻辑。 + +## 验证结果 + +Linux hard gate 全部通过:fixture validator、package TypeScript、RN Test Host typecheck、TurboModule codegen、`git diff --check`。 + +重新发布的 `@navsea/maplibre-particle-rn@13.4.1-particle.3` 公网 tarball SHA-256 为 `16e5dab86aac9832f7a48d8c35dd9821f80f52e7e53ff4e0e6c98724781eda3a`。 + +Win11 正式 npm 消费链编译通过:`:navsea_maplibre-particle-rn:compileDebugKotlin` PASS,`:app:assembleDebug` PASS,`BUILD SUCCESSFUL in 6m 20s`。APK SHA-256 为 `49824062CAB72771282294114B5A818D2067F9B786B8697D2D2A983BF0B743E2`。 + +实体机五组 V3 数据与 Runtime/控制面最终验收全部通过,STEP-072/V3-08 可关闭。 diff --git a/docs/报告/STEP-072_V3-08_Android_Native_v2与单次综合验收准备_Codex完成报告_v1.md b/docs/报告/STEP-072_V3-08_Android_Native_v2与单次综合验收准备_Codex完成报告_v1.md new file mode 100644 index 000000000..6fae3a1c8 --- /dev/null +++ b/docs/报告/STEP-072_V3-08_Android_Native_v2与单次综合验收准备_Codex完成报告_v1.md @@ -0,0 +1,55 @@ +# STEP-072 — V3-08 Android Native v2 与单次综合验收准备完成报告 + +结论:IMPLEMENTED / WAITING_ANDROID_VALIDATION +日期:2026-09-11 + +## 实际完成 +V3-08 前置准备已完成:Native Android 版本目标升为 `13.4.1-particle.2`,RN package `13.4.1-particle.3`、Expo plugin、RN Test Host 与 release manifest 均统一指向该新 Native 坐标;旧 `.1` 不再作为当前 active dependency。 + +Test Host 同时存在两套 `WeatherRuntimeBridge` 的问题已消除。Expo autolinking 复核后仅保留正式 `@navsea/maplibre-particle-rn` bridge,legacy local bridge 不再参与 autolink/codegen。 + +## 静态验证结果 +- Expo autolinking:PASS;legacy bridge 不在 dependency graph。 +- RN Test Host TypeScript:PASS。 +- package TypeScript:PASS。 +- RN TurboModule codegen:PASS;仅 `NativeWeatherRuntimeBridge`。 +- V3 fixture validator:PASS。 +- Expo plugin JavaScript syntax:PASS。 +- release manifest JSON:PASS。 +- `git diff --check`:PASS。 +## 验收流程冻结 +新增 `docs/60_测试/STEP-072_V3-08_Android单次综合验收_v1.md`。流程按既有成功路径明确分为:Linux 构建/发布 Native `.2` → SHA/API 检查 → Linux→Win11 Maven Local handoff → Win11 clean prebuild/dependencyInsight/单次 APK build → Metro 8091 → 固定设备 `192.168.1.146` 安装/启动 → 五组 field 数据综合验收。 + +文档已修正两个容易导致误操作的问题:AAR 需先提取 `classes.jar` 再 `javap`;Makefile 的 `MLN_ANDROID_GRADLE` 必须作为 make 命令行变量传入,不能仅作为环境变量。 + +## 未执行与边界 +- 未执行 Android Gradle、AAR build、Maven publish、APK build、ADB 或实体机测试。 +- 未运行或加入 Jest。 +- 未修改 MapLibre Core/JNI/WeatherRuntime/renderer、WeatherV2、NavSea 或 V3 field runtime 语义。 +- STEP-062 两份用户 dirty 文档、STEP-062 Android 最终报告及历史 untracked/build/cache/tgz 均未纳入。 +- legacy bridge 的 untracked `android/build/` 按任务要求原样保留,未清理。 + +## Git handoff +- 实现/准备提交:`11cf58d344e64e6aff77deceb2c34c8eda045f1b`。 +- 提交信息:`chore: prepare V3-08 android validation`。 +- Push:成功,`particle/main -> particle/main`。 +- Remote branch:`origin/particle/main`。 +- 实现提交 push 后验证:`HEAD == origin/particle/main == 11cf58d344e64e6aff77deceb2c34c8eda045f1b`。 +- 本报告 Git 元数据回填将作为 docs-only closeout 提交;该提交后的仓库 Final HEAD 由最终验证结果记录,不伪造自引用 hash。 + +## 下一步 +仅由用户执行 V3-08 单次 Android 综合验收。验收通过前 release manifest 保持 `awaiting_android_manual_build_publish_acceptance` / Android `not_run`,不得把 `.3` 标记 released 或创建 release tag。 + +## 2026-09-11 V3-08 最终关闭 + +状态:**CLOSED / PASS**。 + +Win11/CARBON7i 使用 Gradle 8.14.3、AGP 8.12.0、Kotlin 2.1.20、JDK 17.0.18、NDK 27.1.12297006、compileSdk/targetSdk 36、minSdk 24 完成正式 npm 消费链与 Debug APK 构建。dependencyInsight 最终解析到 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2`。 + +实体机 `192.168.1.146` 上五组数据均满足 T0=`ACCEPTED_PENDING_INITIAL_WINDOW`、T1=`INITIAL_WINDOW_READY`、最终 Runtime=`READY`:Wind real、Rain scalar-only、Wave real、Tide/Current、Wave synthetic independence。 + +控制面 PLAY/PAUSE、HALF/NORMAL/DOUBLE、FPS AUTO/30/60、seek T0/T1、Weather/Particle 独立显隐、Field stops、Particle color、Trail color、Apply visual 均返回 `ACCEPTED`;最终 FATAL/JNI/UnsatisfiedLinkError 扫描为空。 + +测试期间发现 fixture 位于 Metro project root 外,临时 Win11 `D:\V308` 验收 worktree 通过 `watchFolders` 纳入 `integration/react-native/particle-v3-fields` 后 bundle 成功。该临时 Metro 修改不进入正式仓库,不属于 Native/RN Runtime 缺陷。 + +最终产物:Native `.2` AAR SHA-256 `f890f834692d3cacb8a281b2d0f82cf4a7f8ba7756132092be800505b961e9a2`;RN `.3` tgz SHA-256 `16e5dab86aac9832f7a48d8c35dd9821f80f52e7e53ff4e0e6c98724781eda3a`;APK SHA-256 `49824062CAB72771282294114B5A818D2067F9B786B8697D2D2A983BF0B743E2`。 diff --git a/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v1.md b/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v1.md new file mode 100644 index 000000000..426f88036 --- /dev/null +++ b/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v1.md @@ -0,0 +1,80 @@ +# STEP-073 QBIN2 V4 Core 与通用 Field Profile Validator 完成报告 + +## 结论 + +**已完成 / PASS。** 本 STEP 只建立 QBIN2 V4.4 的 provider-neutral C++ Core primitive 与 Contract Schema v1 的通用 Validator。没有实现 QBIN 二进制 Decoder,未接入 Android/JNI/RN,未修改 Renderer,也未切换现有 V3 `submitField(...)` 生产路径。 + +## Git 记录 + +- 任务开始 HEAD:`a0aa367f302ad0e3518931c311ae8764efd2d566`。 +- 初始实现提交:`0a190adff3a24af59647af4977f1c1e7e107ec28`,`feat: add QBIN2 V4 generic field profile validation`。 +- 最终实现 HEAD:`3526e324ce6c623cfacd4a658605549ccd32d4da`。 +- Final follow-up:`3526e324ce6c623cfacd4a658605549ccd32d4da`,`fix: enforce validity descriptor class`;强制独立 validity descriptor 自身为 `Validity`,避免被伪装为 Scalar。 +- 远端分支:`origin/particle/main`。 +- push 结果:已成功从 `a0aa367f3` 推送至 `0a190adff`,随后已成功推送 follow-up 至 `3526e324c`。 +- 任务开始前的既有 modified/untracked 文件未暂存、未提交、未删除或格式化。 + +## 实际修改文件 + +- `CMakeLists.txt`、`test/CMakeLists.txt`:注册 V4 Core、Validator 与测试。 +- `include/mbgl/particle/field/field_profile_contract.hpp`:Contract Schema v1、value class/component/time relation 与 Core 单位常量。 +- `include/mbgl/particle/field/field_runtime.hpp`、`src/mbgl/particle/field/field_runtime.cpp`:provider-neutral frame/grid/plane、Series/store、ForecastClock 与 revision 处理。 +- `include/mbgl/particle/field/field_profile_validator.hpp`、`src/mbgl/particle/field/field_profile_validator.cpp`:独立通用 Validator。 +- `test/particle/field_runtime.test.cpp`:Validator、revision、独立 cadence 与 clock 自动化测试。 +- `docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v1.md`、`粒子开发记录.md`:正式阶段与开发记录。 + +## V4 Core 草稿审查与修正 + +已保留草稿的 `FrameContentIdentity`、`FieldSeries`、每 fieldKey 独立 store/window 与 `ForecastClock` 边界。修正点是增加 `DecodedFieldPlane::requiredFlag`,并在 `DecodedFieldFrame` 中加入独立 `validityPlane` descriptor。Runtime 仍没有 QBIN header、byte offset、CRC、网络或 provider 字段。 + +`ForecastClock` 仅拥有 requested time、播放状态和速度;未接管 `RasterArrayTimelineRuntime`。旧 `ParticleFieldDescriptor`、`ParticleFieldSnapshot`、`ParticleFieldDataAdapter`、`LocalParticleFieldFeed` 和 `RasterArrayTimelineRuntime` 均未删除或迁移。 + +## Validator API 与结果映射 + +最终入口为 `FieldProfileValidator::validate(const ResolvedFieldProfileContract&, const DecodedFieldFrame&)`,返回 `FieldProfileValidationResult { code, detail }`。 + +| 结果枚举 | 含义 | +| --- | --- | +| `Accepted` | 已接受 | +| `UnsupportedContractSchema` | 不支持的 schema version | +| `InvalidContract` | Contract 结构非法 | +| `InvalidFrame` | Frame/dtype/结构非法 | +| `MissingRequiredPlane` | 缺少 Contract required plane | +| `PlaneDescriptorMismatch` | required descriptor 或 REQUIRED flag 不一致 | +| `UndeclaredRequiredPlane` | 实际 REQUIRED plane 未被 Contract 声明 | +| `AdditionalPlaneNotAllowed` | 禁止额外的非 REQUIRED plane | +| `InvalidVelocityGroup` | 速度 group 缺分量、重复分量或 group=0 | +| `InvalidCoreUnit` | Velocity/Direction/Validity 的 Core 单位或 dtype 违规 | +| `UnsupportedSharedValidityMode` | Schema v1 当前 frame 能力无法表达 `sharedValidityRequired=false` | +| `TimeRelationMismatch` | Contract 要求的 valid/reference 时间关系不成立 | + +验证仅使用 Contract descriptor,不基于 `profileID` 做产品分支。普通 Scalar semanticID 与 unitID 保持 opaque;测试中的未知 semantic `777` 和 unit `65432` 在 Contract/Frame 一致时已接受。 + +## Validity、REQUIRED 与 Core 规则 + +Validity 采用“独立 descriptor + `validityBits` payload”:`validityPlane.storage` 与 `validityBits` 是同一共享只读引用,因此不复制 payload,且后续 projection 可复用同一 validity domain。Validator 强制该 descriptor 自身为 `Validity`,并检查 semantic/valueClass/unit/group/component/dtype/REQUIRED/count;要求 `BITSET`、`NONE`、group `0`、component `None`。 + +REQUIRED 为双向 fail-closed:Contract 每项必须在 Frame 中存在、完整 descriptor 匹配且实际 REQUIRED=true;任一实际 REQUIRED=true plane 必须被 Contract 完整声明。`allowAdditionalPlanes=true` 只允许额外的非 REQUIRED plane。 + +Velocity 的通用检查要求非零 group、唯一 U_EAST/V_NORTH、两者 count/unit 一致且单位为 MPS;Direction Angle 要求 DEGREE。上述规则均没有 profile 名称逻辑。 + +## Frame identity、cadence 与时钟 + +自动化测试确认:同 fieldKey + validTime + identity + content 返回 `Duplicate`;同 identity 不同 content 返回 `RejectedIdentityConflict`;generation/dataVersion 更新返回 `ReplacedRevision`,且 replacement 继续检查相邻 frame interpolation compatibility。不同 fieldKey 在同一 requested time 各自计算不同 cadence 的 T0/T1/mix。ForecastClock 的 pause/play/速度与 Runtime window 独立。 + +## 实际 Linux 验证 + +所有构建/测试通过输出节流脚本执行: + +- `cmake --build build-linux-opengl --target mbgl-test -j3`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='ParticleFieldRuntime.*:ParticleFieldProfileValidator.*'`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*'`:PASS。 +- `git diff --check`:PASS。 + +未执行 Android Gradle、APK 编译、ADB 或真机操作;这符合本 STEP 禁止范围。未执行 iOS/Metal 验证,也不声明其通过。 + +## 未完成 / 无法验证 + +- 未完成:QBIN2 Encoded Ingress / `QbinV2Decoder`,按任务边界留给后续审计批准后的 STEP。 +- 未完成:Android/JNI/RN、Renderer、GPU 与 iOS/Metal 验证;本 STEP 明确禁止或不要求这些工作。 +- 无法验证项:无阻塞的本 STEP 必做 C++ 项。 diff --git a/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v2.md b/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v2.md new file mode 100644 index 000000000..e5fcd601d --- /dev/null +++ b/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v2.md @@ -0,0 +1,34 @@ +# STEP-073 QBIN2 V4 Core 与通用 Field Profile Validator 审计修正完成报告 v2 + +## 结论 + +**已完成 / PASS。** 本次仅修正 STEP-073 v1 的三项确定审计问题,并补充 C++ 覆盖;没有实现 QBIN Decoder,未接入 Renderer、JNI、Android 或 RN,现有 V3 `submitField(...)` 与 timeline 生产路径保持不变。 + +## 审计修复结果 + +1. **DEGREE Core Unit ID**:`FieldUnitDegrees` 已由错误的 `2` 改为 QBIN V2 registry 冻结值 `4`。测试同时确认 `NONE=0`、`MPS=1` 保持不变;Direction Angle 的 `unitID=4` 被接受,`unitID=2` 得到 `InvalidCoreUnit`。 +2. **Plane 唯一键**:Frame plane 与 Contract required plane 均按 `(semanticID, groupID)` 唯一。component 不再参与定位键,作为 descriptor 的 valueClass/unitID/component/requiredFlag 比较项;同 semantic/group、不同 component 的 Frame 或 Contract 均结构非法;已定位 plane 的 component 错误返回 `PlaneDescriptorMismatch`。既有 U/V 速度组仍以各自 semanticID 配合 U_EAST/V_NORTH 规则通过。 +3. **ForecastClock HALF 漂移**:时钟在内部保存 half-millisecond remainder,避免每次 HALF tick 独立四舍五入。测试确认连续两次 `advance(1)` 只前进 1ms、多 tick 不发生 2x 漂移、seek 清除旧 remainder、NORMAL/DOUBLE 行为不变、pause 不前进并在 overflow 时 fail-closed。 + +## Linux 验证 + +构建和测试全部经 `build-output-throttle` 节流执行: + +- `cmake --build build-linux-opengl --target mbgl-test -j3`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='ParticleFieldRuntime.*:ParticleFieldProfileValidator.*'`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*'`:PASS。 +- `git diff --check`:PASS。 + +首次 build 因沙箱禁止 ccache 写入 `/root/.cache` 而未开始编译;经授权以相同节流命令重试后通过,不影响上述结论。 + +## Git 收尾 + +- 任务开始 HEAD:`c3033eb5a4560e5778dcae7f341965caca4f570c`。 +- 本次实现提交 / Final 功能 HEAD:`d6a784a791601a0829529b1a5d3b184bcd23dd3b`,`fix: correct STEP-073 QBIN2 V4 core contract`。 +- push:成功,`origin/particle/main` 从 `c3033eb5a` 前进至 `d6a784a79`。 +- 任务前已有 modified/untracked 文件(包括 STEP-062 文档、历史任务文档、构建日志与 RN 目录)未暂存、未删除、未提交。 + +## 未执行 / 未完成 + +- 未执行 Android Gradle、APK、ADB、真机操作或 Jest;未执行 iOS/Metal 验证。 +- 未完成 QBIN2 Encoded Ingress/Decoder、Binding/Projection、JNI、Renderer 或 RN 接入,均在本任务禁止范围内。 diff --git a/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v3.md b/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v3.md new file mode 100644 index 000000000..53ce3e533 --- /dev/null +++ b/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v3.md @@ -0,0 +1,46 @@ +# STEP-073 QBIN2 V4 Core 与通用 Field Profile Validator 审计修正完成报告 v3 + +## 结论 + +**已完成 / PASS。** 本轮只修复 QBIN2 V4 provider-neutral Core contract 与 Generic Field Profile Validator 的协议一致性遗漏;未进入 Decoder、Binding/Projection、Renderer、JNI、Android、RN 或 V3 生产入口。 + +## 逐项结果 + +| 项目 | 结果 | 说明 | +| --- | --- | --- | +| Known Core semantic 固定物理含义 | 已完成 | 静态协议表覆盖 semantic `1`、`2`、`3`、`10`、`11`,同时校验 Contract 和 Frame,错误为 `SemanticContractMismatch`。 | +| Unknown SCALAR 可扩展性 | 已完成 | 未增加 Scalar unit 白名单;opaque semantic/unit 的 Contract/Frame 一致组合仍通过。 | +| Direction Circular Quantization | 已完成 | 通用 value-class 校验固定 DEGREE=4、NONE、group0、UINT16、offset=0、scale=360/65536,并容忍极小 float32 表示误差。 | +| v2 plane identity / DEGREE / HALF | 已完成 | 原有 `(semanticID, groupID)` 唯一性、DEGREE=4 及 ForecastClock HALF 测试继续通过。 | +| V3 定向回归 | 已完成 | FieldDataAdapter、RasterArrayTimeline、HostField 过滤回归通过。 | +| QBIN2 Decoder、Binding/Projection、Renderer、JNI、Android、RN | 未完成(禁止范围) | 本任务未实现或改动。 | +| Android/ADB/Jest/iOS-Metal 验证 | 无法验证(未执行) | 任务明确禁止 Android Gradle、APK、ADB、真机与 Jest;未主张 iOS/Metal 结果。 | + +## 自动化测试 + +新增/修正的 C++ 覆盖确认: + +- semantic `1` 的 Contract+Frame 同时伪装为 Scalar 被拒绝;semantic `2` 的错误 component、semantic `3` 的错误 validity descriptor 被拒绝;规则来自 protocol table 而非 profile 名称。 +- semantic `10` 的合法 Direction 接受;MPS/unit 错误、U_EAST component、非零 group、非 UINT16 dtype、非 canonical scale 及非零 offset 都拒绝。 +- Unknown Scalar 的 opaque unit 继续接受;既有 semantic/group 唯一性、DEGREE=4、ForecastClock HALF 和 V3 回归持续通过。 + +全部 noisy 命令均通过 `build-output-throttle` 执行: + +- `cmake --build build-linux-opengl --target mbgl-test -j3`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='ParticleFieldRuntime.*:ParticleFieldProfileValidator.*'`:PASS。 +- `build-linux-opengl/mbgl-test-runner --gtest_filter='Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*'`:PASS。 +- `git diff --check`:PASS。 + +首次受沙箱构建因既有 ccache 需要写入 `/root/.cache/ccache` 而失败;经授权用完全相同的节流命令重试后 PASS。 + +## Git 收尾 + +- 开始 HEAD:`1a453a6623b4fae63b67b604d2bd690b1ebc77ef`。 +- Final 实现 HEAD:`4b8d200831ce464b6fbb141785ecf0245e8c26ca`。 +- 实现提交:`4b8d200831ce464b6fbb141785ecf0245e8c26ca`,`fix: close STEP-073 QBIN2 V4 validator audit`。本报告、v3 Step 与开发记录以独立 closing docs commit 收尾;两笔提交都只包含本任务允许的文件。 +- push:在 closing docs commit 后尝试推送 `origin/particle/main`;最终结果以该次 Git 命令输出为准。 +- 任务开始前已有 STEP-062 文档 modified 与历史 untracked 文件保持原样,未暂存、未删除、未提交。 + +## 停止条件 + +范围内修正和 Linux 验证已完成。本报告后立即停止,等待 ChatGPT 独立审计,不进入 STEP-074。 diff --git a/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v4.md b/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v4.md new file mode 100644 index 000000000..517512557 --- /dev/null +++ b/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v4.md @@ -0,0 +1,28 @@ +# STEP-073 QBIN2 V4 Core 与通用 Field Profile Validator 完成报告 v4 + +日期:2026-09-13 + +## 结果 + +STEP-073 最终关闭修正完成。Frame Core 现在明确维护 single shared validity invariant;空 numeric plane list 不再被结构层误判。Validator 在 required descriptor 匹配前拒绝实际保留 Core semantic 的物理含义冲突,unknown Scalar opaque 行为与 Direction v3 量化规则保持。 + +## 实际修改 + +- `DecodedFieldFrame::isStructurallyValid()` 对 designated validity 强制 `Validity`、`Bitset`、`NONE`、group 0、`None` component、`REQUIRED`、正确 cells/bit payload,并要求 descriptor storage 与 `validityBits` 是同一共享引用;常规 `planes[]` 不允许任何 Validity plane。 +- 删除空 `planes[]` 的结构拒绝。于是完整 validity-only Frame 保持结构有效,Generic Validator 对缺失 Profile numeric required plane 返回 `MissingRequiredPlane`。 +- 以既有静态协议表先核验 actual planes,故 semantic 10 的 component/group 错误,以及 semantic 1 伪装成 Scalar,都优先返回 `SemanticContractMismatch`,而不是 `MissingRequiredPlane` 或 `PlaneDescriptorMismatch`。 +- 测试新增 second validity、designated validity descriptor 各字段、`requiredFlag=false` 与 storage identity 的 fail-closed 覆盖;修正 ForecastClock pause 后的测试恢复播放。 + +## 独立 runner 验证 + +`mbgl-test-runner` 本体已重新构建和链接,并通过 `--gtest_list_tests` 确认加载 `ParticleFieldProfileValidator.`、`ParticleFieldRuntime.`。 + +- V4 filter:实际 `Running 11 tests`,`PASSED 11 tests`,failed=0。 +- V3 定向回归:实际 `Running 11 tests`,`PASSED 11 tests`,failed=0。 +- `git diff --check`:PASS。 + +构建/测试输出按节流要求写入 `/tmp` 日志;仓库当前不存在 skill 所指的 `scripts/run_limited.py` helper。首次 sandbox 构建因只读 ccache 失败,获准后以同一受限日志模式重跑并成功,不涉及源代码失败。 + +## 范围与未完成项 + +未触及 QBIN2 Decoder、Binding/Projection、Renderer、JNI、Android、RN、Style schema 或 V3 `submitField(...)`。未执行 Android Gradle、APK、ADB、真机、Jest、iOS/Metal。没有范围内未完成项;按任务要求停止,等待独立复验,不进入 STEP-074。 diff --git a/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v1.md b/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v1.md new file mode 100644 index 000000000..70e4d6b03 --- /dev/null +++ b/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v1.md @@ -0,0 +1,13 @@ +# STEP-074 QBIN2 V4 Encoded Ingress 与 Decoder Codex 完成报告 v1 + +## 已完成 + +- 开始 HEAD:`b8a629b7b65067f7f561db473312484bd8f0451d`;最终 HEAD:待提交记录。任务开始前 dirty/untracked 文件未纳入本任务。 +- 独立 ingress decoder、CRC、canonical packing、shared validity、invalid raw-zero 和 decode→contract facade。 +- 新增 CMake production/test sources;新增 suites `ParticleQbinV2Decoder` 与 `ParticleEncodedFieldIngress`,共 6 tests。 +- 实际重链 `mbgl-test-runner` PASS;新增 filter 实际 Running 6,failed=0;STEP-073 filter 实际 Running 11,failed=0;V3 filter 实际 Running 11,failed=0;`git diff --check` PASS。 + +## 未完成 / 无法验证 + +- 受限暂存提交与 push 尚待执行;最终 HEAD/push 结果届时记录。 +- 未执行 Android/ADB/Jest/iOS/Metal;范围明确排除 URL/HTTP/gzip/Host/JNI/RN/Renderer/Binding。 diff --git a/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v2.md b/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v2.md new file mode 100644 index 000000000..842310f6e --- /dev/null +++ b/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v2.md @@ -0,0 +1,27 @@ +# STEP-074 QBIN2 V4 Encoded Ingress 与 Decoder Codex 完成报告 v2 + +## 已完成 + +- 开始 HEAD:`83a84452f23d019cbb143630eac04631552fef89`;任务前已有 dirty/untracked 未纳入。 +- production 问题 A:identity 按 raw payload 与 descriptor 的固定 Little Endian canonical bytes 作 FNV;float 以 IEEE754 bits 编码,不依赖 host endian/object representation,也不含 packing offset。 +- production 问题 B:限制语义改正为 QBIN 整体 `<= 64 MiB`、Plane raw bytes 总和 `<= 32 MiB`;后者返回 `LimitExceeded`,文件长度错误返回 `LengthMismatch`。 +- production 问题 C:BITSET 仅可作为 VALIDITY,VALIDITY 必为 BITSET;accepted 前执行 frame structural self-check。 +- fixture builder 明确写 uint16/32/64、float32、float64 的 Little Endian,未成为 production writer。 +- corruption matrix、CRC、padding、枚举、时间/边界、capacity、validity 与五种 numeric dtype invalid-cell 场景均进入实际 decoder runner;identity 有固定 golden `75930fed16c969a7`,四 Plane 独立性已验证。 +- 513×769 fixture 校验 U/V/validity raw byte-for-byte、canonical payload、共享 validity storage。 +- Ingress 覆盖 valid、missing/invalid Contract、missing required plane、known Core conflict、unknown opaque Scalar Contract;未写 `EncodedFieldStore`。 + +## 验证结果 + +- Linux OpenGL `mbgl-test-runner` 重新构建及重链:PASS。 +- `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`:实际 Running 18 tests,18/18 PASS(v1 为 6)。 +- STEP-073:11/11 PASS。 +- 既有 V3:11/11 PASS。 +- `git diff --check`:PASS。 +- 禁止范围:未执行 Android Gradle、APK、ADB、真机、Jest、iOS/Metal;未修改 Renderer、Binding、Projection、Store submission、Host、网络或平台目录。 + +## 未完成 / 无法验证 + +- 无协议 Core 范围内未完成项。 +- Android/iOS 与网络链路明确不在本 Step 范围,未验证且不据此作任何声明。 +- 最终 commit HEAD 与 push 结果在 Git 收尾后补充。 diff --git a/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v3.md b/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v3.md new file mode 100644 index 000000000..5c629ef26 --- /dev/null +++ b/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v3.md @@ -0,0 +1,22 @@ +# STEP-074 QBIN2 V4 Encoded Ingress 与 Decoder Codex 完成报告 v3 + +## 已完成 + +- 基线为 `56b8dab90f914438e6fcaec6278146c96c8df638`;任务开始前已有 dirty/untracked 内容原样保留,未纳入本任务。 +- 新增 `RejectsTooSmallInputBeforeMagic`、`RejectsBadMagicAfterCompleteHeader` 与 `RejectsInvalidRuntimeMetadataButAllowsZeroDataVersion`,所有断言直接调用 production `QbinV2Decoder::decode` 并验证具体 error code;`dataVersion=0` 被明确验证为 Accepted。 +- 新增 `RejectsBigEndianHeaderWidthWithValidCrc`:将 width=2 的四字节改为 `00 00 00 02`、重算 CRC 后返回 `LimitExceeded`,没有把它当作 Little Endian width=2。 +- 新增 `RejectsMixedEndianDescriptorCountWithValidCrc`:将 descriptor count 的四字节改为 `00 00 00 04`、重算 CRC 后返回 `InvalidDescriptor`。 +- 新增 `AcceptsRawLimitWithCanonicalPaddingExcluded`:1447×1449、7 Plane,raw sum=31,712,633,canonical payloadLength=31,712,648;测试显式断言 raw sum `<= 32 MiB`、payload 大于 raw sum 且 decoder Accepted。所有上限仍满足:cells=2,096,703、planes=7、QBIN 小于 64 MiB。 + +## 验证结果 + +- Linux OpenGL `mbgl-test-runner` 已实际重建及重链:PASS。 +- `mbgl-test-runner --gtest_list_tests`:PASS。 +- `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`:实际 Running 24 tests,24/24 PASS。 +- STEP-073 `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`:11/11 PASS。 +- 既有 V3 `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*`:11/11 PASS。 +- `git diff --check`:PASS。 + +## 范围与未完成项 + +只修改 C++ Decoder tests 与本任务文档;未发现生产缺陷,因此 production Decoder 未改。未触及 Host、网络、JNI、Android/RN、Binding/Projection、Renderer、Store submission、iOS/Metal 或 Jest。上述平台与链路不在本 Step 范围,未验证且不作 PASS 声明。 diff --git a/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v4.md b/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v4.md new file mode 100644 index 000000000..515ba6a48 --- /dev/null +++ b/docs/报告/STEP-074_QBIN2_V4_EncodedIngress与Decoder_Codex完成报告_v4.md @@ -0,0 +1,34 @@ +# STEP-074 — QBIN2 V4 Encoded Ingress 与 Decoder Codex 完成报告 v4 + +日期:2026-09-13 + +## 结果 + +已完成:只更新 `test/particle/qbin_v2_decoder.test.cpp` 中的 `AcceptsRawLimitWithCanonicalPaddingExcluded` fixture。新的 1101×1890、17-plane canonical QBIN 的数值为: + +- rawSum:33,554,352 bytes。 +- 32 MiB limit:33,554,432 bytes。 +- payloadLength:33,554,448 bytes。 +- totalQbinBytes:33,555,104 bytes。 + +测试直接断言 `rawSum <= limit`、`payloadLength > limit`、`payloadLength > rawSum` 与 `totalQbinBytes <= 64 MiB`,并调用 production `QbinV2Decoder::decode(...)`,结果 Accepted。 + +## 逐项状态 + +- 已完成:边界构造覆盖 canonical padding 将 payload 推过 32 MiB,而 decoded raw sum 未越界。 +- 已完成:cells=2,080,890(不超过 2,097,152),planes=17(不超过 32),single shared validity、descriptor 合法与 canonical packing 均成立。 +- 已完成:真实重建 `mbgl-test-runner`;Decoder/Ingress Running 24、Runtime/Profile Running 11、Source 回归 Running 11,均 PASS、零失败。 +- 已完成:`git diff --check` PASS。 +- 未完成:无。 +- 无法验证:Android Gradle/APK/ADB/Jest、iOS/Metal;本任务明确禁止执行,且本轮未涉及这些路径。 + +## 范围审计 + +未修改 production Decoder,未触及 Host、网络、JNI、RN、Binding/Projection、Renderer 或 Store submission。已有 STEP-062 修改及历史 untracked/build 文件原样保留。 + +## Git + +- 首个 scoped commit:`3274c11a6618a071792fd056680276b9f9868622`(`test: finalize STEP-074 QBIN2 padding boundary`)。 +- 首次 push:成功,`origin/particle/main` 已从 `f4a495ff3` 前进至 `3274c11a6`。 +- Final HEAD(完成报告记录提交):`bba99f8acfecdfc44e8484785c7a543da173e5b1`(`docs: record STEP-074 delivery`)。 +- 最终 push:成功,`origin/particle/main` 与上述 HEAD 一致;本报告之外未暂存既有 STEP-062 修改、任务书或其他 untracked 文件。 diff --git a/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v1.md b/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v1.md new file mode 100644 index 000000000..3a70a86a2 --- /dev/null +++ b/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v1.md @@ -0,0 +1,27 @@ +# STEP-075 QBIN2 V4 Binding 与 Projection Identity Codex 完成报告 v1 + +日期:2026-09-13 + +## 已完成 + +- 完成纯 Core 的 Color/Particle binding 与 projection resolver。Host 仅表达 binding intent;Core 仍拥有 requested forecast time 和 per-field window。 +- Identity 由 binding selector、实际 T0/T1 window frame coordinate、所选 plane 与 shared validity 的 content identity 组成;SampleState 单独承载 requested time/mix。U/V/validity 未变化时,Scalar-only revision 不改变 Particle identity。 +- U/V 强制 VELOCITY_COMPONENT、MPS equality、U_EAST/V_NORTH 与同组;Direction 强制 DIRECTION_ANGLE/NONE/unit equality。所有 lookup/assertion 都 fail-closed,绝不 fallback 到其他 fieldKey/plane。 +- 增加 4 个独立 Binding tests:不同 cadence、MIX identity/storage、Scalar-only revision isolation、错误矩阵和 DirectionAngle。 + +## 验证结果 + +- `cmake --build build-linux-opengl --target mbgl-test-runner -j3`:PASS(真实重建,输出节流)。 +- 规定 GTest filter:50 tests / 0 failures / 0 errors;Binding 4、Runtime/Profile 11、QBIN Decoder/Ingress 24、V3 Source 11。 +- `git diff --check`:PASS。 + +## 未完成或无法验证 + +- Android Gradle/APK/ADB/Jest、iOS/Metal、Renderer/GPU、Host/JNI/RN 均按任务禁止或范围限制未执行,不能标记 PASS。 +- 不可插值相邻帧由既有 FieldSeries submit 拒绝,因此合法 Store 不会产生该 resolve window;Resolver 仍保留 `InterpolationIncompatible` 防御返回。 + +## Git + +- 首个 scoped commit:`c465b804c`,`feat: add V4 field binding projection identities`。 +- 首次 push:成功,`31783f25a..c465b804c particle/main -> particle/main`。 +- 实现提交后的 `origin/particle/main` HEAD:`c465b804c`。本报告 Git 字段回填将作为独立 docs-only commit 再次推送。 diff --git a/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v2.md b/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v2.md new file mode 100644 index 000000000..3b2f9edf8 --- /dev/null +++ b/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v2.md @@ -0,0 +1,26 @@ +# STEP-075 QBIN2 V4 Binding 与 Projection Identity Codex 完成报告 v2 + +日期:2026-09-13 + +## 已完成 + +- Projection content identity 改为显式 `ProjectionFrameCoordinate(fieldID, validTimeMs)` 加上 binding 实际消费的 T0/T1 selected plane 和 shared validity content identity。完整 generation/dataVersion 不再污染任一 projection identity。 +- 对称 revision 隔离已由 Store 的 `ReplacedRevision` 公共路径验证:Scalar-only 仅改变 Color,U/V-only 仅改变 Particle,validity-only 同时改变 Color 与 Particle。 +- `InterpolationIncompatible` 已从 Resolver 公共错误语义移除;FieldSeries submit 已 fail-closed 拒绝不兼容相邻 frame,合法公开 resolve 路径不可形成该状态。 +- 独立 fieldKey/cadence、MIX identity/storage stability、DirectionAngle success/failure 与既有 V4/V3 回归均保持。 + +## 验证结果 + +- Linux OpenGL `mbgl-test-runner` 真实重建 PASS(输出节流)。 +- 规定 GTest XML:50 tests、0 failures、0 errors;Binding 4、Runtime/Profile 11、QBIN Decoder/Ingress 24、V3 Source 11。 +- `git diff --check` PASS。 + +## 未完成或无法验证 + +- Android Gradle/APK/ADB/Jest、iOS/Metal、Renderer/GPU、Host/JNI/RN、网络和真实 QBIN Golden 数据均按任务禁止或不在范围内,未执行且不能标记 PASS。 +- 无已知实现阻塞;本任务不进入 STEP-076。 + +## Git + +- scoped commit:`90d05f596a42c157502f9a2a5f20425c999cebab`,`fix: close STEP-075 projection identity audit`。 +- push 成功:`3616daec3..90d05f596 particle/main -> particle/main`。 diff --git a/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v3.md b/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v3.md new file mode 100644 index 000000000..21bd0f123 --- /dev/null +++ b/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v3.md @@ -0,0 +1,29 @@ +# STEP-075 QBIN2 V4 Binding 与 Projection Identity Codex 完成报告 v3 + +日期:2026-09-13 + +## 已完成 + +- `ParticleBinding` equality 现严格按 mode 比较实际消费的 selector,inactive selector 不再污染 Projection content identity。 +- VectorComponents 的 inactive `directionPlane` 与 DirectionAngle 的 inactive U/V selector 都已由离线单元测试证明不会改变 binding equality 或 resolved `ParticleProjectionContentIdentity`。 +- 两种 mode 的 active selector 变更仍使 binding 不同,并经缺失 selector 的 `PlaneMissing` 路径 fail-closed。 +- selector identity 仍限定为 `(semanticID, groupID)`,component 仍只参与 selected-plane descriptor assertion。 + +## 验证结果 + +- Linux OpenGL `mbgl-test-runner` 真实重建 PASS,且所有命令经全局 build-output-throttle 执行。 +- Binding 6/6、Runtime/Profile 11/11、QBIN/Ingress 24/24、Source 11/11;每组均为 0 failure。 +- `git diff --check` PASS。 + +## 未完成或未验证 + +- Android Gradle/APK/ADB/Jest、iOS/Metal、Renderer/GPU、Host/JNI/RN、网络及真实 QBIN 均按任务禁止或不在范围内,未执行,不能标记 PASS。 +- 无实现阻塞;本任务不进入 STEP-076。 + +## Git + +- scoped commit:`2645bd725f63c4da12ab48a0cc11ff034c9d9878`。 +- message:`fix: close STEP-075 binding selector identity audit`。 +- push result:`653e6f2f1..2645bd725 particle/main -> particle/main`(成功)。 +- remote branch:`origin/particle/main`。 +- Final HEAD(scoped implementation delivery):`2645bd725f63c4da12ab48a0cc11ff034c9d9878`。 diff --git a/docs/报告/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_Codex完成报告_v1.md b/docs/报告/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_Codex完成报告_v1.md new file mode 100644 index 000000000..e42abf523 --- /dev/null +++ b/docs/报告/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_Codex完成报告_v1.md @@ -0,0 +1,28 @@ +# STEP-076 QBIN2 V4 Core Host API 与 RasterArraySource 接线 Codex 完成报告 v1 + +## 结果 + +已完成 V4 QBIN Host API 到 `RasterArraySource` 的隔离接线。raw QBIN bytes 经 metadata/key 检查、decode、contract validate 后才进入按 `fieldKey` 独立维护的 `EncodedFieldStore`。Color 与 Particle binding 保持独立 intent,并通过同一 `ForecastClock` 在 resolve 时产生各自 window/mix;本 Step 没有让 renderer 消费 V4 projection,也没有改变 V3 host/timeline 行为。 + +## 开始基线与修改文件 + +- 开始 HEAD:`b148fb6a0c6711182194f6778d80c8f8a60eb037`。 +- 修改:`include/mbgl/style/sources/raster_array_source.hpp`、`src/mbgl/style/sources/raster_array_source.cpp`、`test/CMakeLists.txt`、`test/source/raster_array_v4_host_api.test.cpp`、本 Step、本文报告和 `粒子开发记录.md`。 +- 历史 STEP-062 dirty/untracked/build 输出未暂存或提交。 + +## 验证结果 + +| Filter | Running | Failure | +| --- | ---: | ---: | +| `Source.RasterArrayV4HostApi*` | 3 | 0 | +| `ParticleFieldBinding.*` | 6 | 0 | +| `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*` | 11 | 0 | +| `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*` | 24 | 0 | +| `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*` | 11 | 0 | + +真实 Linux OpenGL `mbgl-test-runner` 已重建;所有命令经全局 build-output-throttle,`git diff --check` PASS。未执行 Android Gradle/APK/ADB/Jest、iOS/Metal。 + +## Git 收尾 + +- scoped commit、push result、`origin/particle/main` 远端和 Final HEAD 以本次提交/推送的实际输出为准,未混入任务前内容。 +- 未完成/阻塞:无;按任务边界停止,不进入 Android/JNI/RN bridge。 diff --git a/docs/报告/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_Codex完成报告_v2.md b/docs/报告/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_Codex完成报告_v2.md new file mode 100644 index 000000000..eb2297e02 --- /dev/null +++ b/docs/报告/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_Codex完成报告_v2.md @@ -0,0 +1,35 @@ +# STEP-076 QBIN2 V4 Core Host API 与 RasterArraySource 接线 Codex 完成报告 v2 + +## 结果 + +已完成 v1 独立审计指出的测试证明缺口,未改变任何 production API 或架构。四种指定 V4 reject 都在公共 `RasterArraySource` resolve 路径上证明无 partial mutation;V4 clear 已在真实可观察的 V3 初始窗口存在时,证明不影响 V3 `field/nextField`,且 V4 binding intent 可在重新提交后继续 resolve。 + +## 基线与修改 + +- 开始 HEAD:`ae03d8bfd2813eb5e65074db16018e37c7cff1c4`。 +- 实际 production 修改:无。 +- 实际修改文件:`test/source/raster_array_v4_host_api.test.cpp`、`docs/50_Step记录/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_v2.md`、`docs/报告/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_Codex完成报告_v2.md`、`粒子开发记录.md`。 +- 开始时已有 STEP-062 dirty、历史 untracked 与 build 输出均保持原样;本次 scoped commit 不包含它们。 + +## 测试证明 + +- `RejectedDecode`、`RejectedContract`、`RejectedInvalidFieldKey`、`RejectedStaleRevision`:每次 reject 前后 public Particle projection 均可 resolve;比较了 projection content identity、T0/T1 coordinate、两条 selected plane storage 和 validity storage identity。 +- `clearEncodedField(...)` 与 `clearAllEncodedFields()`:先用公开 V3 `submitInitialRuntimeFieldWindow(...)` 建立窗口,clear 后 V3 field/nextField 均存在,identity/generation/validTime 不变;V4 resolve 均为 fail-closed;重新提交 V4 后 Color/Particle binding 都可 resolve。 +- V4 suite 保留原 3 个并新增第 4 个测试;未使用旧 `clearHostFields()`,未读取 V4 Store 私有成员。 + +| Filter | 实际 Running | Failure | +| --- | ---: | ---: | +| `Source.RasterArrayV4HostApi*` | 4 | 0 | +| `ParticleFieldBinding.*` | 6 | 0 | +| `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*` | 11 | 0 | +| `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*` | 24 | 0 | +| `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*` | 11 | 0 | + +真实执行 `cmake --build build-linux-opengl --target mbgl-test-runner -j3` 并 PASS;所有 noisy build/test 均经全局 `build-output-throttle`。`git diff --check` PASS。 + +## 范围与 Git 收尾 + +- 未执行 Android 编译/Gradle/APK/ADB/实体机,也未执行 Jest 或 iOS/Metal。 +- 未修改 Renderer、shader、JNI、Android、RN、Test Host、NavSea、QBIN decoder 或 production Field Runtime/Binding。 +- scoped commit message:`test: close STEP-076 V4 host API audit`;push target:`origin/particle/main`。实际 commit hash、push result、remote branch 和 Final HEAD 以紧随本报告的 scoped Git 收尾输出为准。 +- 未完成/阻塞:无;完成后停止,不进入下一 Step。 diff --git a/docs/报告/STEP-077_QBIN2_V4_Android_JNI_RN桥接调查_Codex完成报告_v1.md b/docs/报告/STEP-077_QBIN2_V4_Android_JNI_RN桥接调查_Codex完成报告_v1.md new file mode 100644 index 000000000..e7c6e0df8 --- /dev/null +++ b/docs/报告/STEP-077_QBIN2_V4_Android_JNI_RN桥接调查_Codex完成报告_v1.md @@ -0,0 +1,35 @@ +# STEP-077 QBIN2 V4 Android / JNI / RN 桥接调查 Codex 完成报告 v1 + +## 结果 + +已完成只读架构调查并形成 V4 桥接实施边界。推荐 V4 新建与 V3 平行的 encoded-field bridge:RN 传本地 `qbinFileUri`,Kotlin 有界读取为 `byte[]`,JNI copy 后调用 `RasterArraySource` V4 Host API;小型 contract 用受 schema 限制的 control-plane JSON,其余 binding/time/playback 使用 typed primitive。Host 不控制 T0/T1/MIX。 + +现有 V3 `submitField` 保留。V3 的 `clearFields`、`setForecastTime`、`setPlaybackState`、`setPlaybackSpeed` 直接走旧 Host-field/timeline,不能承载 V4;V4 必须有 `clearEncoded*` 与独立 forecast 命名空间,避免误操作 V3 timeline。 + +## 实际调查范围 + +- 逐层审计 `FieldRuntime.ts`、`NativeWeatherRuntimeBridge.ts`、`types.ts`、`WeatherRuntimeBridgeModule.kt`、`WeatherRuntime.java`、`MapLibreMap.java`、`NativeMapView.java`、`native_map_view.hpp/.cpp` 与 `RasterArraySource` V4 API。 +- 确认 Fabric tag 每次在 UI queue 解析到当前 `MLRNMapView -> MapLibreMap`;不新增 registry,不缓存 map/source pointer。style reload、detach 或 dispose 后应以明确 lifecycle error 拒绝,Host 重 attach 后重新提交。 +- 列出十项 `RasterArrayEncodedFieldSubmitResult` 到 RN resolved result 的一一映射,并与文件、参数、runtime、style/source 错误分层。 +- 调查 Linux 静态入口:package/Host TypeScript、TurboModule codegen config、C++/JNI 精确编译、以及现有 Mockito `WeatherRuntimeStateTest` 的 JVM facade 边界;未执行 Android Gradle/APK/ADB/实体机。 + +完整结论、API 表、参数 ABI、风险与后续三段实施建议见 [调查报告](../20_调查/STEP-077_QBIN2_V4_Android_JNI_RN桥接调查报告_v1.md)。 + +## 修改与验证 + +- production code、package version、Test Host、Renderer/GPU:均未修改。 +- 新增调查报告与 STEP 记录;将任务状态更新为完成;追加 `粒子开发记录.md`。 +- `git diff --check`:PASS(本任务文档)。 +- 没有执行 noisy build/test,因此没有 Android/Gradle、APK、ADB、真机、真实 QBIN、Jest 或 iOS/Metal 结果可报告。 + +## Git 收尾 + +- 文档调查提交:`e0088a5f0873ed95a902a9402683ffdd0dcc155c` +- 提交信息:`docs: complete STEP-077 V4 bridge investigation` +- 首次 push 结果:`0759a0610..e0088a5f0 particle/main -> particle/main` +- remote branch:`origin/particle/main` +- 本完成报告作为 Git 元数据收尾另行 scoped commit 并紧随推送;其最终 HEAD 以交付时 `git rev-parse HEAD` 为准。 + +## 未完成/阻塞 + +无调查阻塞。桥接实现、Android 编译与实体机验证都刻意不属于本 Step;按任务书在本报告提交推送后立即停止,不进入 STEP-077 实现。 diff --git a/docs/报告/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_Codex完成报告_v2.md b/docs/报告/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_Codex完成报告_v2.md new file mode 100644 index 000000000..cd96a3dde --- /dev/null +++ b/docs/报告/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_Codex完成报告_v2.md @@ -0,0 +1,35 @@ +# STEP-078 QBIN2 V4 Android / JNI / RN 桥接实现 Codex 完成报告 v2 + +## 结果 + +V4 bridge 已完整闭环并与 V3 隔离。v1 保留了 encoded QBIN submit、local-file guard、V4 clear/time 骨架及 Java/JNI transport;本次修正了 contract 字段、Color unit assertion、mode-aware Particle selector,并拆分 forecast state/speed。binding 不再是占位 rejection。 + +## 设计与错误边界 + +- `contractJson` 有唯一的 provider-neutral C++ parser;其只把 frozen control-plane JSON 转为 `ResolvedFieldProfileContract`,随后仍由 Core structural validation 和 `FieldProfileValidator` 裁决。 +- Color 固定传递 `fieldKey/(semanticID,groupID)/expectedValueClass/expectedUnitID`。 +- Particle 固定 primitive ABI slots 为 U、V、direction;active selector 要求 uint16 范围与非零 semantic,inactive selector 只能为零对。 +- V4 Core result 保持十种稳定字符串。JNI 的 `0` 仅为 source missing/type mismatch 私有 sentinel,最终成为 `SOURCE_MISSING` rejection;不占用 Core enum。 +- state 只调 Core playback state,speed 只调 Core playback speed。 + +## 实际修改文件 + +`CMakeLists.txt`、`test/CMakeLists.txt`、新增 parser header/source/test;`packages/maplibre-particle-rn/src/{FieldRuntime.ts,NativeWeatherRuntimeBridge.ts,types.ts}`;Kotlin `WeatherRuntimeBridgeModule.kt`;Android Java `WeatherRuntime.java`、`MapLibreMap.java`、`NativeMap.java`、`NativeMapView.java`;以及 `native_map_view.hpp/.cpp`;本 Step 记录、本报告和`粒子开发记录.md`。 + +本次没有修改 Renderer/GPU/shader、Test Host、NavSea 或 STEP-073~076 已冻结 Core 业务语义。任务开始前 STEP-062 dirty、历史 untracked、`.codex-build-logs/`、generated/android/.expo 和 tgz 均没有纳入暂存或提交。 + +## Linux 验证 + +- `particle-codex` 中限流构建 `mbgl-test-runner`:PASS。 +- `ParticleFieldProfileContractJson.*`、Source V4、Binding、QBIN/Ingress、Runtime/Profile、V3 Source 定向过滤:PASS。 +- package TypeScript:PASS;TurboModule schema combine/codegen:PASS。 +- JNI Java declaration ↔ C++ registration 静态 name-set:PASS。 +- `git diff --check`:PASS。 + +所有 noisy build/test 都使用全局 `build-output-throttle`,日志放在 `/tmp/step078-build-logs`,没有触碰受保护的既有 build 输出。 + +## Android 未验证与 Git 收尾 + +Android Kotlin/Java/JNI 的真实编译、Gradle、APK、ADB 和实体机均按任务要求未执行,须在后续一次性 Android 验收中完成;iOS/Metal 与 Jest 亦未执行。无实现阻塞。 + +scoped commit 的 hash、message、push result、remote branch 与 Final HEAD 在本 Step Git 收尾命令完成后以该命令输出为准;目标为 `origin/particle/main`。完成后立即停止,不进入 STEP-079。 diff --git a/docs/报告/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_Codex完成报告_v3.md b/docs/报告/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_Codex完成报告_v3.md new file mode 100644 index 000000000..ce80c9836 --- /dev/null +++ b/docs/报告/STEP-078_QBIN2_V4_Android_JNI_RN桥接实现_Codex完成报告_v3.md @@ -0,0 +1,30 @@ +# STEP-078 QBIN2 V4 Android / JNI / RN 桥接实现 Codex 完成报告 v3 + +## 结果 + +已按 `73779b89bcce1094eb344f4727d6d23904f667f3` 基线关闭审计发现的两个 production guard 缺口,未重构已通过的 V4 bridge,未进入 STEP-079。 + +QBIN transport 现在在 Kotlin local-file、Java facade、JNI copy 三处均为 1..64 MiB;JNI 的检查在 `std::vector` 构造之前,直接 JNI caller 的空/超限数组 fail-closed。该 transport guard 是独立 helper,未混入 QBIN decoder:decoder 自有的 64 MiB 文件与 32 MiB decoded raw limits 不变。 + +RN/TS 以 `TextEncoder`、Kotlin 以 `toByteArray(Charsets.UTF_8)`、Java 以 `StandardCharsets.UTF_8` 执行相同的 1..64 UTF-8 bytes guard,覆盖 fieldKey、runtime fieldID、profileID。profileID 的 lowercase ASCII structural contract 仍由 Core parser/validator 最终裁决;未统一或放宽其它 ID 字符集。contractJson 仍为既有 256 KiB UTF-8 上限。 + +Java facade 亦在解析 profileID 前保留 `contractJson` 的 256 KiB UTF-8 transport/control-plane 上限,避免直接 Java caller 绕开 Kotlin seam 扩大既有边界。 + +## 验证结果 + +- `particle-codex` tmux session 内所有 noisy build/test 均经 `build-output-throttle`:package TypeScript、TS 多字节 fail-fast directed test、TurboModule combine/codegen、Linux `mbgl-test-runner` rebuild 与规定 V4/V3 filter 均 PASS。 +- 新 C++ `ParticleEncodedFieldTransport` test 证明 0/64 MiB+1 拒绝、64 MiB 接受,且 helper 在 JNI vector copy 之前调用。 +- 新 JDK-only Java guard test 证明 Java facade 采用的 helper 接受 64 MiB、拒绝 0 和 64 MiB+1,并拒绝 33 CJK 字符(99 UTF-8 bytes)。Kotlin 与 Java source 的 UTF-8 guard 已定向静态核对。 +- Java JNI declaration 与 C++ registration 的 V4 name-set 静态核对 PASS;`git diff --check` PASS。 + +## 未完成与边界 + +Android Kotlin/Java/JNI 的真实编译、Gradle、APK、安装、ADB 和实体机验证均按任务要求未执行,留待后续一次性 Android 验收;iOS/Metal 与 Jest 也未执行。没有读取真实服务器 QBIN,未改 Renderer/GPU/shader、Test Host、NavSea 或 STEP-073~076 冻结 Core 语义。 + +任务开始前的 STEP-062 dirty、历史 untracked、`.codex-build-logs/`、Test Host generated/android/.expo 和 tgz 未纳入本次提交。scoped commit、push result、remote branch 与 Final HEAD 记录于本报告的 Git 收尾更新及 Step 记录;完成后停止,不进入 STEP-079。 + +## Git 收尾 + +- 实现 commit:`75a974e77503c56ec5bd2823ac1b689225ed80a7`,message:`fix: close STEP-078 V4 bridge guard audit`。 +- push result:PASS,`73779b89b..75a974e77 particle/main -> particle/main`;remote branch:`origin/particle/main`。 +- 本报告与 Step 的 Git 收尾补充会以独立、仅文档的 STEP-078 scoped commit 推送;该 commit 的 Final HEAD 在提交完成后报告,并立即停止。 diff --git a/docs/报告/STEP-078_QBIN2_V4_ColorBinding_VelocityComponent接口修订_Codex完成报告_v4.md b/docs/报告/STEP-078_QBIN2_V4_ColorBinding_VelocityComponent接口修订_Codex完成报告_v4.md new file mode 100644 index 000000000..d81578ba6 --- /dev/null +++ b/docs/报告/STEP-078_QBIN2_V4_ColorBinding_VelocityComponent接口修订_Codex完成报告_v4.md @@ -0,0 +1,34 @@ +# STEP-078 QBIN2 V4 ColorBinding VELOCITY_COMPONENT 接口修订 Codex 完成报告 v4 + +## 结果 + +已完成最小、向后兼容的 public API 修订:RN `FieldRuntime.setEncodedColorBinding(...)` 现在可传递 Core 已支持的 `VELOCITY_COMPONENT`。传输路径为 TS string → Kotlin `1` → JNI/Core `FieldValueClass::VelocityComponent`,不增加 TurboModule 参数,也不改变 Java 透传方法形状。 + +`VALIDITY` 没有被开放为 ColorBinding:TS/Kotlin 均拒绝该字符串,JNI 仅接受 `0..2`,Core 回归继续证明它返回 `InvalidBinding`。未实现 vector magnitude、单位换算或任何 provider/weather 特例。 + +## 修改与证明 + +- TS 类型与 public validation 加入 `VELOCITY_COMPONENT`。 +- Kotlin bridge 映射 `SCALAR=0`、`VELOCITY_COMPONENT=1`、`DIRECTION_ANGLE=2`。 +- JNI guard 从仅 `0/2` 收敛为合法连续值域 `0..2`,故 `3` 及其它非法值仍拒绝。 +- `ParticleFieldBinding.FailsClosedAndSupportsDirectionAngle` 增加真实 velocity component plane 的 accepted projection/storage identity 断言,另加 Validity fail-closed 断言。 + +## 验证结果 + +在 `particle-codex` tmux session 内,所有 typecheck、codegen、build 与 test 命令均经 `build-output-throttle`,8 个限流日志完整产生于 `/tmp/step078v4-build-logs`。package TypeScript typecheck、TurboModule schema combine/codegen、Linux OpenGL `mbgl-test-runner` rebuild,及 `ParticleFieldBinding.*`、Source V4 Host API、QBIN/Ingress、Runtime/Profile、V3 Source 指定回归均 PASS。 + +此外,Java `NativeMapView` 的 6 参数 native declaration、C++ `NativeMapView` member signature 与 `METHOD` registration 已静态核对一致;task-scope `git diff --check` PASS。 + +## 未完成与边界 + +按任务要求未运行 Android Gradle、APK、install、ADB、实体机或模拟器;Android Kotlin/Java/JNI 的真实编译和 Windows 11 Test Host 验收仍待用户执行。Jest、iOS/Metal 也未验证。既有 STEP-062 dirty、历史 untracked、workspace `.codex-build-logs/` 与其它无关内容均未纳入本任务。 + +未进入 STEP-079;它必须由 ChatGPT 独立复验本修订后再启动。 + +## Git 收尾 + +- implementation commit hash:`e02353820aafa1834ce3a2ab3afe9261fadd83bc`。 +- commit message:`fix: expose V4 velocity component color binding`。 +- push result:PASS,`dd1d70147..e02353820 particle/main -> particle/main`。 +- remote branch:`origin/particle/main`。 +- Final HEAD(implementation scoped commit):`e02353820aafa1834ce3a2ab3afe9261fadd83bc`。本报告、Step 记录和开发记录随后使用独立、仅文档 scoped commit 收尾并 push;该提交完成即停止。 diff --git a/docs/报告/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_Codex完成报告_v2.md b/docs/报告/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_Codex完成报告_v2.md new file mode 100644 index 000000000..2bda9b4a3 --- /dev/null +++ b/docs/报告/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_Codex完成报告_v2.md @@ -0,0 +1,50 @@ +# STEP-079 真实 QBIN Golden Fixture 接入 RN Test Host Codex 完成报告 v2 + +## 结果 + +状态:**IMPLEMENTED / WAITING FOR WINDOWS 11 ANDROID VALIDATION**。 + +RN Test Host 现保留 V3 功能并新增独立 V4 QBIN Golden 区。8 个冻结 raw QBIN 已随 Test Host 打包,JS 不接收 payload bytes、数组、Base64 或 URL;只由 `expo-asset` materialize 为本地 `file://` URI 并调用冻结的 `FieldRuntime.submitEncodedField(...)`。 + +Wind/Current 的真实 U/V descriptor 以 `VELOCITY_COMPONENT` 传入 ColorBinding,Wave/Rain 以各自真实 scalar semantic 传入;Wave direction 才作为 ParticleBinding,Rain 没有虚构粒子数据。load 按 clear → binding → T0 → T1 → midpoint 运行,T0/T1 必须分别返回 `ACCEPTED_INSERTED`,否则界面显示实际结果并失败。midpoint 只证明 V4 seek command accepted,不能证明或显示 mix=0.5。 + +## 已完成 + +- 冻结来源 `/root/sourceserver/weatherV2/_artifacts/MapLibre_QBIN_V2_单元测试数据_v1/` 的 8 个 raw fixture 已复制至 Test Host 自有 assets;开发 verifier 对 source 与 copy 的 size/SHA-256 全部 PASS。 +- V4 manifest 保留 profile contract、runtime identity、validTime、midpoint、source path、size/SHA-256 和真实 binding;运行时没有 source-server 依赖。 +- Metro 最小追加 `.qbin`,V3 UI/transport 没有移除或改成 V4-only。 +- Test Host typecheck、package typecheck、TurboModule schema combine/codegen、Linux OpenGL runner target、Contract JSON / Transport / Source V4 / Binding / QBIN-Ingress / Runtime-Profile / V3 Source 回归和 `git diff --check` 均 PASS。 + +## 未完成与无法验证 + +- 未执行 Android Gradle、APK build、install、ADB、模拟器、实体机或 Jest;也未验证 iOS/Metal。 +- 因任务明确禁止 Linux Android 操作,无法声明 Android PASS,也无法在此环境观察 V4 画面、实际 decode/submit、binding、midpoint、clear 或 PLAY/PAUSE 的设备结果。 +- 任务开始前既有 STEP-062 dirty、历史 untracked、`.codex-build-logs/`、generated Android/Expo/modules、tgz 与旧任务书均未纳入提交。 + +## Windows 11 Android 手工验收 + +在 Windows checkout 的 `particle/main`,由用户执行(路径与 APK 产物名如与本地不同,以实际生成工程为准): + +```powershell +git fetch origin +git checkout particle/main +git pull --ff-only origin particle/main +cd integration/react-native/MapLibreParticleRNTestHost +npm ci +npx expo prebuild --platform android --clean +npx expo run:android +adb -s 192.168.1.146 reverse tcp:8091 tcp:8091 +adb -s 192.168.1.146 reverse tcp:8081 tcp:8091 +adb -s 192.168.1.146 shell am start -n org.maplibre.particle.rntesthost/.MainActivity +``` + +如选择手工 APK 安装,须使用实际 APK 路径且显式指定同一设备:`adb -s 192.168.1.146 install -r <实际-debug-apk路径>`。 + +观察项:依次选择 Wind/Wave/Current/Rain 并 load;每类均确认 T0/T1 为 `ACCEPTED_INSERTED`、Color/Particle binding 为 `ACCEPTED`(Rain Particle=N/A)、midpoint 仅显示 command accepted、T0/T1 seek、V4 PLAY/PAUSE/speed 和 clear selected/all 均返回 `ACCEPTED`。检查 Metro/Android logcat 无 QBIN 读取、decode、contract 或 binding rejection;不得以画面存在替代以上文字结果。Android build/install/ADB 结果由用户记录后才可更新此状态。 + +## Git 收尾 + +- implementation commit hash:`f1ea88b0ce9c6995f05a8d2ae2eb2696d72e5f28`。 +- commit message:`feat: add V4 QBIN golden fixture test host`。 +- remote branch:`origin/particle/main`。 +- implementation Final HEAD:`f1ea88b0ce9c6995f05a8d2ae2eb2696d72e5f28`。本报告、Step 记录和开发记录随后使用仅文档 scoped commit 推送;push result 和收尾 Final HEAD 以该次 Git 输出为准,完成后立即停止。 diff --git a/docs/报告/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_接口阻塞报告_v1.md b/docs/报告/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_接口阻塞报告_v1.md new file mode 100644 index 000000000..0637de0ae --- /dev/null +++ b/docs/报告/STEP-079_真实QBIN_GoldenFixture接入RNTestHost_接口阻塞报告_v1.md @@ -0,0 +1,35 @@ +# STEP-079 真实 QBIN Golden Fixture 接入 RN Test Host 接口阻塞报告 v1 + +状态:阻塞(未实现,未执行 Android 验收) + +## 结论 + +STEP-078 的冻结 V4 RN public API 无法表达真实 Wind/Current U 或 V plane 的正确 ColorBinding。因此本 Step 依照任务的停止条款退出,没有以 Test Host wrapper、错误 enum 或 Core/API 修改绕过。 + +## 可复核证据 + +冻结 WeatherV2 fixture manifest/profile registry 规定: + +| 数据集 | 要提交的 Color plane | fixture descriptor | +| --- | --- | --- | +| Wind `wind/1` | U 或 V | semantic `1`/`2`,group `1`,unit `1`,`VELOCITY_COMPONENT` | +| Current `current/1` | U 或 V | semantic `1`/`2`,group `1`,unit `1`,`VELOCITY_COMPONENT` | + +冻结 RN API `packages/maplibre-particle-rn/src/FieldRuntime.ts` 的 `setEncodedColorBinding` 只接受 `SCALAR`、`DIRECTION_ANGLE`;Android `WeatherRuntimeBridgeModule.kt` 也只将这两个字符串映射为 native value class。它没有 `VELOCITY_COMPONENT` 的 public enum 通路。 + +Core 并非可以安全忽略该差异:`src/mbgl/particle/field/field_binding.cpp` 会对 ColorBinding 的 selected T0/T1 plane 验证 value class。以 `SCALAR` 提交真实 U/V 必返回 `PlaneClassMismatch`。把 value class 伪造为 scalar 既不是 fixture-derived binding,也不会形成有效 V4 bridge 验证。 + +Wave(scalar Height)与 Rain(scalar precipitation)没有此问题;Wave 的 DirectionAngle particle binding 与 Wind/Current 的 VectorComponents particle binding 也均有冻结 public API 表达。但任务要求同一 Test Host V4 dataset contract 至少包含 ColorBinding,且固定规则特别允许 Wind/Current U/V 的 ColorBinding;缺少该 enum 使四组完整合同无法成立。 + +## 未执行的内容 + +- 未保留或提交 8 个 copied raw QBIN fixture,避免仓库出现无法被完整 Test Host contract 使用的半成品。 +- 未创建 V4 dataset/materialize/UI;未修改 V3 UI 或 Test Host Metro。 +- 未运行 TypeScript、TurboModule codegen、C++ rebuild/回归、Android Gradle/APK/ADB,因没有可验证的合规实现。 +- 未创建实现提交;没有 fixture、Test Host 或 package/Core 改动。阻塞记录已以 `afcc74066f1395678621fa2b1a70d10bc722a623`(`docs: record STEP-079 V4 fixture API blocker`)推送到 `origin/particle/main`。 + +## 最小解阻条件 + +需要新的、显式授权的 API 修订 Step:令 `EncodedColorBindingInput.expectedValueClass`、RN validation、TurboModule spec、Kotlin enum mapping 以同一冻结 ABI 支持 `VELOCITY_COMPONENT`,并增加相应的 Core/bridge contract tests。该 Step 必须单独决定 Renderer 是否仅 bridge-only;本报告不建议把 vector magnitude 伪造为 color input,也不建议在 Test Host 绕过 public API。 + +在该修订完成且稳定后,应重新从本任务指定 fixture source materialize 8 个 `.qbin`,进行完整 source/size/SHA-256 审计,并执行本任务要求的 Linux static validation。Windows 11 Android 验收目前没有可交付的 V4 Test Host,因此不能提供安装/ADB 验收步骤或宣称 Android PASS。 diff --git a/docs/报告/STEP-080_Android_AAR与RNTestHost标准构建流水线_Codex完成报告_v1.md b/docs/报告/STEP-080_Android_AAR与RNTestHost标准构建流水线_Codex完成报告_v1.md new file mode 100644 index 000000000..6a720bc22 --- /dev/null +++ b/docs/报告/STEP-080_Android_AAR与RNTestHost标准构建流水线_Codex完成报告_v1.md @@ -0,0 +1,45 @@ +# STEP-080 Android AAR 与 RN Test Host 标准构建流水线 Codex 完成报告 v1 + +## 结果 + +已完成 Android AAR → RN package → build-manifest → Windows RN Test Host 的固定交付流水线。正式默认 Linux 命令是: + +```bash +tools/particle_android_pipeline/linux_build_publish.sh +``` + +它固定 OpenGL Release、四 ABI、`publishOpenglreleasePublicationToMavenLocal`、RN package TypeScript typecheck 与 npm tarball,并以完全版本相等为 hard gate。成功时输出 Git ignore 的版本隔离 bundle: + +```text +artifacts/maplibre-particle//build-manifest.json +``` + +manifest 是 Windows 唯一的 artifact/version 输入,包含 HEAD、branch、dirty 状态、RN/Native version、Maven coordinate、AAR/POM/module/tgz 的绝对路径/bytes/SHA-256、时间、四 ABI、OpenGL 和 Linux validation result。 + +Windows consumer 支持: + +```powershell +tools\particle_android_pipeline\windows_build_testhost.ps1 -ManifestPath -DryRun +tools\particle_android_pipeline\windows_build_testhost.ps1 -ManifestPath +``` + +它在写入前验证 manifest/bundle SHA,拒绝不同 SHA 的 Maven Local 覆盖,拒绝 dirty Test Host tracked source,固定 Test Host RN dependency 到 manifest tgz 和 native substitution 到 manifest coordinate。默认只构建 Debug APK 并输出其 path/bytes/SHA-256;默认不执行 ADB。`-Install` 与 `-Install -Run` 是唯一设备入口,固定使用 `192.168.1.146`。 + +## Linux 验证 + +所有 Linux 验证均在 `particle-codex` tmux 内完成。 + +- Bash syntax 与无写入 `--preflight`:PASS。 +- 使用现存完整 Maven Local `13.4.1-particle.3` 的 `--reuse-existing`:PASS。该模式没有重新编译 AAR,也没有执行 Gradle publish;它验证 AAR/POM/module,确认历史 `.2` AAR/POM/module 逐字节未变,经 build-output-throttle 完成 package TypeScript typecheck 与 npm pack。 +- 生成 manifest 的 JSON、AAR/POM/module/tgz 路径、bytes、SHA-256、版本与 Maven coordinate:PASS。 +- tgz 内 `package/package.json` version 与 Native version:PASS。 +- `git diff --check -- .gitignore`:PASS。 + +此次实际 handoff 的 AAR SHA-256 是 `9ef03eb0d0c03457adc04847cfa69fb67d85191daecd64d3b76db2ad8cc688cd`;tgz SHA-256 是 `6c8af6ff19adb4e25785b54bea3edd1dc7d0502d9158cf1c519d84b69d08ee55`。 + +## 未执行与限制 + +- 为遵守任务边界,未再次运行完整四 ABI AAR build/Maven publish;默认脚本已保留该完整流程,复验只使用明确标记的 existing Maven Local mode。 +- Linux 没有 `pwsh`,Windows PowerShell parser/static validation 记录为 unavailable,而非 PASS。 +- 未运行 Windows APK/ADB/安装/设备、Jest、iOS/Metal。 +- Native `VERSION`、既有 JNI 修正、STEP-062 dirty、历史 untracked/build/cache/tgz 均未纳入本 Step 的提交;仅新增流水线、忽略规则与本 Step 文档。 diff --git a/docs/报告/STEP-081_iOS_Metal_Renderer与真机Smoke入口_Codex完成报告_v1.md b/docs/报告/STEP-081_iOS_Metal_Renderer与真机Smoke入口_Codex完成报告_v1.md new file mode 100644 index 000000000..51935d379 --- /dev/null +++ b/docs/报告/STEP-081_iOS_Metal_Renderer与真机Smoke入口_Codex完成报告_v1.md @@ -0,0 +1,46 @@ +# STEP-081 iOS Metal Renderer 与真机 Smoke 入口 Codex 完成报告 v1 + +## 1. 执行结论 + +PARTIAL。Codex 代码实现与静态检查完成,但当前受控执行环境拒绝创建 `.git/index.lock`,所以无法暂存、提交或 push。iPhoneOS build/Metal runtime/真机验收未运行,Step 保持 `WAITING_RDC_BUILD_REVIEW`。 + +## 2. Git 基线 + +- branch:`particle/main` +- Start HEAD:`f23f4143b9642de0e8d6661dd567e791494a0a0b` +- End/Final HEAD:`f23f4143b9642de0e8d6661dd567e791494a0a0b`(未创建 commit)。 +- 初始工作树有未跟踪任务书 `docs/40_任务/STEP-081_iOS_Metal_Renderer与真机Smoke入口_Codex任务_v1.md`;它不属于本提交。 + +## 3. 实现摘要 + +新增 Metal `ParticleGpuValidationShader`,覆盖 State A/B、trail fade、line draw、composite/frozen composite、WeatherColor、timeMix、field/respawn bounds、VELOCITY 与 DIRECTION、独立 particle/trail/speed color、weather ramp、invalid discard 与 matrix projection。Metal source 明确复用 31×16-byte UBO。 + +Metal registry 在 `RendererBackend::initShaders` 注册,属性和 texture table 由 stable shader IDs 映射;CMake Metal list 显式包含新增 source。 + +iOS Test App 的 Runtime Styling 菜单提供 `Particle / QBIN V4 Smoke (T0)` action sheet。Smoke 使用 bundle 中从 RN Wind Golden fixture source copied 的 T0/T1,private `MLNMapView::mbglMap` seam 创建现有 C++ source/layers,并允许 T0/midpoint/T1 和 clear/reload。 + +## 4. 与任务书差异 + +无架构差异。CMake fixture copy 采用 POST_BUILD app-bundle copy,而非在仓库复制 fixture。未建立 iOS SDK public API。 + +## 5. 构建与静态检查 + +- 构建:`NOT_RUN_BY_DESIGN — Codex prohibited from building`。 +- 未运行 Xcode/CMake/Ninja/Make/Bazel/clang/metal/simctl/真机命令,未运行 Jest。 +- 静态检查:`git diff --check`;Metal source/registry/CMake/fixture path 的 `rg` 结构检查;RenderRasterParticleLayer 与 RenderWeatherColorLayer gfx API read-only review。 + +## 6. 风险和未完成项 + +MSL 的实际 compiler diagnostics、Metal float render target/line pipeline compatibility、runtime shader compilation、device visual output、clear/recreate release 均待 RDC 真机验证。没有 simulator 验收计划。 + +## 7. 给 GPT/RDC 的验收清单 + +1. 用 `platform/ios/app/CMakeLists.txt` 的 `app` target 和 Metal-enabled iPhoneOS CMake/Xcode generation 路径执行 device build(勿使用 Simulator)。 +2. 安装到真实 iPhone/iPad,进入 Settings → Runtime Styling → `Particle / QBIN V4 Smoke (T0)`。 +3. 依次触发 T0、midpoint、T1;确认日志 `ParticleQbinV4Smoke: event=FIELD_READY`、`WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY`、particle state/trail resource creation,且 WeatherColor 与 particles 均可见。 +4. 触发 Clear / release,再次 Load T0,确认 `CLEAR_RELEASED` 及资源重建。 +5. 若 runtime MSL compile/pipeline 失败,按此顺序检查 `particle_gpu_validation.hpp` pass source、`.cpp` attribute/texture table、`renderer_backend.cpp` registry、`cmake/metal.cmake` list;特别核对 UBO buffer index 与 `short2` attribute binding。 + +## 8. Git 收尾 + +未完成:执行 `git add` 时 Git 返回 `Unable to create .../.git/index.lock: Operation not permitted`。因此 commit hash、push result、remote branch 与新的 Final HEAD 均不存在;不得宣称已完成 Git 收尾。具备 `.git` 写入权限的 RDC/GPT 应复核 diff 后仅暂存本报告列出的 STEP-081 文件,提交 `feat: bring up particle renderer on ios metal` 并 push `origin/particle/main`。 diff --git a/docs/报告/STEP-081_iOS_Metal_Renderer与真机Smoke入口_RDC真机验收报告_v2.md b/docs/报告/STEP-081_iOS_Metal_Renderer与真机Smoke入口_RDC真机验收报告_v2.md new file mode 100644 index 000000000..a8e30c462 --- /dev/null +++ b/docs/报告/STEP-081_iOS_Metal_Renderer与真机Smoke入口_RDC真机验收报告_v2.md @@ -0,0 +1,61 @@ +# STEP-081 iOS Metal Renderer 与真机 Smoke 入口 RDC 真机验收报告 v2 + +日期:2026-09-18 +状态:CLOSED / PASS + +## 验收环境 + +- 仓库:`MapLibreParticleLab`,分支 `particle/main`。 +- 设备:Pleiades,iPad mini (5th generation) / iPad11,2,iPadOS 26.5.2。 +- Xcode 26.3,iPhoneOS SDK 26.2,arm64 device build。 +- Test App Bundle ID:`jp.eyaku.maplibreparticlelab.step081`。 +- 未使用 Simulator。 + +## 最终通过项 + +- 签名 device build、安装、启动通过。 +- Deterministic Uniform East:粒子整体向右/东,PASS。 +- Deterministic Uniform North:粒子整体向上/北,PASS。 +- Deterministic Vortex:粒子可见、持续移动并形成连续轨迹,PASS。 +- QBIN Wind Golden:T0、MID、T1 均正常显示,PASS。 +- QBIN Clear:正常释放;Clear 后重新 T0 可恢复,PASS。 + +## 真机定位出的 Metal 问题与修复 + +### 1. Render pipeline cache attachment format 缺失 + +`ShaderProgram::getRenderPipelineState()` 原 cache key 未包含 color/depth/stencil attachment pixel format。粒子同一 shader 跨 State RGBA32Float、Trail RGBA8Unorm 与主 renderable 使用时,可能错误复用不兼容 Metal pipeline。修复为在 reuse hash 中合并 color/depth/stencil format。 + +物理 iPad 诊断中,修复前 Trail target 即使强制纯绿色 fragment 仍为全零;修复后同一路径 RGBA8 target 出现有效写入,因此该根因得到直接设备证据。 + +### 2. 亚像素 previous→current line 无稳定覆盖 + +State/Field/vertex texture sampling、矩阵与坐标范围均被分别证明正确。实际 previous→current 屏幕线段平均约 0.065 px、最大约 0.24 px,全部小于 0.5 px。Metal line primitive 对该长度没有稳定可见 coverage。 + +最终保留真实 previous→current line,并新增 Metal-only current-endpoint 1 px quad coverage。该 fallback 只补 raster coverage,不修改 State、U/V、advection、时间步、速度或物理线长。中间 Point fallback 会导致整屏绿色,已撤销且不进入最终代码。 + +### 3. QBIN smoke 相机不在 fixture 范围 + +Wind Golden QBIN 解码出的 grid 为 513×769,bounds 为 112.5E..157.5E、0..55.776573N。原 Smoke 入口 FIELD_READY 后不调整相机,世界中心视图看不到字段。Test App 现定位约 27.8883N / 135E、zoom 3;此修改只影响 private smoke 可观察性。 + +## 其他正式修复 + +- Darwin layer manager 注册 ParticleValidation / RasterParticle / WeatherColor Core-only factories。 +- Metal OffscreenTexture 的 size 初始化与 `isRenderable()` 实现修正。 +- Metal RenderPass 增加 vertex texture / sampler binding;当前 `Texture2D::bind()` 为粒子正式路径同时绑定 vertex+fragment。 +- Metal MSL 恢复 raw `short2 a_pos` 语义,并修正 fullscreen State/Trail/Composite Y 方向。 +- Metal render pipeline cache key 现在包含 attachment formats。 + +## 清理与边界 + +- 所有 `STEP081_*` readback、固定绿线、matrix-only、State-range 等临时诊断均从最终源码删除。 +- Test App deterministic fixture 保留高对比显示参数,仅用于人工 smoke;不改变 Core 物理语义。 +- 未修改 QBIN V4 decoder/profile/binding 协议、Android/RN 公共 API、NavSea 或 provider/network 责任边界。 +- `MBXAppDelegate.m` 的既有日志级别 dirty 与两个 vendor/submodule dirty 不属于 STEP-081,不纳入提交。 + +## Git 收尾 + +- 功能与真机验收提交:`f1c0f92b2b40bf0a8ea8f74d5390996a9da242bc`。 +- commit message:`fix: close STEP-081 iOS Metal particle rendering`。 +- 首轮 push:成功,`origin/particle/main` 已到达 `f1c0f92b2b40bf0a8ea8f74d5390996a9da242bc`。 +- 无关 `MBXAppDelegate.m`、`docs/doxygen/doxygen-awesome-css`、`platform/windows/vendor/vcpkg` 保持未提交。 diff --git a/docs/报告/STEP-081_iOS_Metal_Renderer与真机Smoke入口_RDC编译验证报告_v1.md b/docs/报告/STEP-081_iOS_Metal_Renderer与真机Smoke入口_RDC编译验证报告_v1.md new file mode 100644 index 000000000..785975d99 --- /dev/null +++ b/docs/报告/STEP-081_iOS_Metal_Renderer与真机Smoke入口_RDC编译验证报告_v1.md @@ -0,0 +1,25 @@ +# STEP-081 iOS Metal Renderer 与真机 Smoke 入口 RDC 编译验证报告 v1 + +日期:2026-09-17 +状态:DEVICE_BUILD_PASS / WAITING_DEVICE_SMOKE + +## 结果 + +- CMake `ios-metal` 配置成功。 +- iPhoneOS 26.2 / arm64 / Debug / `app` target 最终 `BUILD SUCCEEDED`。 +- 未使用 Simulator;因 provisioning 与当前真机 unavailable,最终构建使用 `CODE_SIGNING_ALLOWED=NO`。 +- `particle_gpu_validation.cpp` 已进入 `mbgl-core` arm64 编译并通过。 +- `MBXViewController.mm` private Smoke 入口已通过 Objective-C++ arm64 编译。 +- 产物:`build-ios/platform/ios/app/Debug-iphoneos/app.app`,约 61 MB;主程序为 arm64 Mach-O。 +- Bundle 内 `ParticleSmokeFixtures/T0.qbin` 与 `T1.qbin` SHA-256 与 RN Test Host Golden fixture 完全一致。 + +## RDC 编译期修正 + +1. Test App private include root 改用 `${CMAKE_SOURCE_DIR}/src`,避免子项目 `PROJECT_SOURCE_DIR` 指向 `platform/ios/app`。 +2. `weakSelf` 使用显式 `MBXViewController *` 类型。 +3. fixture path 使用 `NSString *`,移除错误的 `const NSString *`。 +4. 安装/启用仓库锁定的 Bazel 8.6.0 以完成 Darwin generated code。 + +## 尚未完成 + +当前登记 iPhone/iPad 均为 `unavailable`。因此尚未执行签名安装、设备启动、Metal runtime shader compile,以及 T0 / midpoint / T1 / Clear-release Smoke。STEP-081 不能关闭,下一状态为 `WAITING_DEVICE_SMOKE`。 diff --git a/docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v1.md b/docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v1.md new file mode 100644 index 000000000..5833b87e1 --- /dev/null +++ b/docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v1.md @@ -0,0 +1,40 @@ +# STEP-082 iOS Native FieldRuntime Facade 与 XCFramework 发行基线 Codex 完成报告 v1 + +## 1. 执行结论 + +IMPLEMENTED / WAITING_MAC_XCFRAMEWORK_VALIDATION。公开 facade、生命周期、V4 转发、Bazel public-header 接线和 distribution 模板均已实现;不能写 CLOSED/PASS,因为正式 `MapLibre.dynamic.xcframework` 被任务基线既有 style-codegen 阻断,未生成可检查的 XCFramework 或 zip。 + +## 2. Git 基线 + +- branch:`particle/main` +- start commit:`3e03949d3411c830cfff4bf320cd350f5b6a7527` +- implementation commit:`d94f2bffd68fa01f941b8de051d435a2cde074c2` +- implementation message:`feat: add ios particle field runtime facade` +- submodule:开始时已记录,未修改。 + +## 3. 实际实现 + +- 新增 public `MLNParticleFieldRuntimeConfiguration`、`MLNParticleFieldRuntime` 与 `MLNMapView (ParticleFieldRuntime)`;MapView associated object 强持有,runtime 弱持有 MapView,replace 先 dispose 旧对象,dispose 幂等。 +- attach 与每条命令都重新验证 current style 内 source/layer 的真实 Core 类型和 source 关联;不缓存 `RasterArraySource*`。map 不可用、style reload、source/layer missing/type mismatch 均以独立 NSError fail-closed。 +- `NSData` QBIN V4 submit 复用 Core parser 和 `RasterArraySource::submitEncodedField`;ObjC enum 无损映射十个 Core submit result。color/particle binding、forecast time/play/speed 和两种 clear 均接通现有 Core。 +- `MLNParticleFieldRuntime.h` 进入 `MLN_IOS_SDK_HEADERS` 和 `Mapbox.h`;`MLNMapView_Private.h` 未加入 public list,只由 `.mm` 实现使用。 +- 新增 `release/maplibre-particle/ios/` 的 README、SwiftPM binary target 模板和 `NOT_BUILT` manifest;module/product 固定为 `MapLibre`。 + +## 4. 验证记录 + +- PASS:`git diff --cached --check`(implementation commit 前)。 +- PASS:iPhoneOS arm64 public-header standalone import syntax check。 +- PASS:iPhoneOS arm64 Objective-C++ facade syntax check;最终仅出现既有 private header 的 EAGL deprecation warning。CMake 生成的 Xcode project 已含 `MLNParticleFieldRuntime.mm`,并实际产生 `MLNParticleFieldRuntime.o/.d/.dia`。 +- PASS:`cmake --build build-ios --target ios-sdk-static -- -quiet` 返回 0,完成 iPhoneOS arm64 `ios-sdk-static` build;唯一输出仍是上述既有 EAGL deprecation warning。 +- BLOCKED:`bazel build //platform/ios:MapLibre.dynamic` 在 facade 编译前失败。`//:generated_style_code` 声明 `src/mbgl/style/layers/particle_validation_layer.cpp` 为输出,但 `scripts/generate-style-code.mjs` 不生成它;错误为 `output ... was not created`。这是当前基线 Core/Bazel codegen 声明问题,不在本 Step 允许修改范围。 +- 未生成 XCFramework:因此无法核验 Headers、binary public symbol、private header 排除、zip size/SHA-256;manifest 诚实标为 `NOT_BUILT`。 + +## 5. 范围与未完成事项 + +未修改 Android、RN、NavSea、QBIN V2、FieldProfile/BindingProjection、renderer/Metal physics,未重做 STEP-081,也未提前进入 STEP-083。必须先在独立授权范围修复/确认 Bazel style-codegen 基线,再从此实现 commit 重跑 XCFramework、检查 public header/symbol 并记录真实 zip SHA-256。 + +## 6. 工作树、提交与推送 + +任务书列出的 `docs/doxygen/doxygen-awesome-css`、`platform/ios/app/MBXAppDelegate.m`、`platform/windows/vendor/vcpkg` 均未触碰、未暂存。用户提供的未跟踪 STEP-082 任务书也未加入 commit:其原始行尾空格会使 `git diff --check` 失败,故未改写。 + +本报告的初始 docs commit `1af5aab6f83f0a744ac2b881682254d013d17ecb` 已成功推送至 `origin/particle/main`;本次 iPhoneOS build 结果随后以 scoped docs update 补充。最终远端 HEAD 以本轮结束时 `origin/particle/main` 为准。 diff --git a/docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v2.md b/docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v2.md new file mode 100644 index 000000000..775842a7a --- /dev/null +++ b/docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v2.md @@ -0,0 +1,40 @@ +# STEP-082 iOS Native FieldRuntime Facade 与 XCFramework 发行基线 Codex 完成报告 v2 + +## 1. 执行结论 + +`CLOSED / PASS`。iOS public `MLNParticleFieldRuntime` facade、MapView-owned 生命周期、V4 forwarding、public header 导出和 XCFramework distribution 基线均已完成;宿主已在真实 implementation commit 上修复 packaging blocker,并完成实际 Bazel XCFramework 验证。 + +## 2. Git 与范围 + +- branch:`particle/main` +- 本轮确认的 implementation commit:`136ed726cf749c24b3673c046581a38ef48be390` +- implementation message:`fix: close ios xcframework packaging blockers` +- 本轮只修改 release manifest、STEP-082 Step 记录、`粒子开发记录.md` 和本报告。 +- 用户既有 dirty 未触碰:`docs/doxygen/doxygen-awesome-css`、`platform/ios/app/MBXAppDelegate.m`、`platform/windows/vendor/vcpkg`。 +- 两份未跟踪 STEP-082 任务书未修改、未纳入收尾范围。 + +Git closeout commit/push 将由宿主在本报告生成后完成,最终 hash 以宿主回填/最终回复为准;本报告不伪造未知 hash、push 或远端状态。 + +## 3. blocker 修复后的真实构建验证 + +- `bazel build //:generated_style_code`:PASS。 +- `bazel build //platform/ios:MapLibre.dynamic`:PASS。日志:`/tmp/step082-host-maplibre-dynamic-final2.log`;最终行为 `Build completed successfully`。 +- artifact:`bazel-bin/platform/ios/MapLibre.dynamic.xcframework.zip`。 +- size:`24639200` bytes。 +- SHA-256:`64faa174c5c0dc0a2da3e289f0beaf2dedf7298665e3345f39836729b668e239`。 + +## 4. XCFramework 导出审计 + +- `ios-arm64` 和 `ios-arm64_x86_64-simulator` 均有 `Headers/MLNParticleFieldRuntime.h`。 +- 两个 slice 的 `Mapbox.h` 都 import `MLNParticleFieldRuntime.h`。 +- `MLNMapView_Private.h` public export count 为 `0`。 +- 两个 framework binary 均包含 `_OBJC_CLASS_$_MLNParticleFieldRuntime` 和 `_OBJC_CLASS_$_MLNParticleFieldRuntimeConfiguration`。 +- device slice 架构为 `arm64`;simulator slice 架构为 `x86_64+arm64`。 + +## 5. Release 元数据 + +`release/maplibre-particle/ios/release-manifest.json` 已更新为 `VALIDATED`。`distributionVersion` 保持 `6.28.0-particle.1`,module 保持 `MapLibre`,artifact 文件名不变;真实 size/SHA-256 与 source commit 已记录。artifact 尚未发布远端,URL 明确为 `LOCAL_BUILD`,不伪造可下载地址。 + +## 6. 未完成项与边界 + +本 Step 的本地 XCFramework build/export 验证已完成。远端 artifact 上传、远端 URL 和消费端下载回读不在本次范围;必须在真实发布后另行记录。未修改 Android、RN、NavSea、QBIN/FieldProfile/BindingProjection、renderer 或 Metal particle physics,也未重做 STEP-081。 diff --git a/docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v3.md b/docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v3.md new file mode 100644 index 000000000..18458ae55 --- /dev/null +++ b/docs/报告/STEP-082_iOS_Native_FieldRuntime_Facade与XCFramework发行基线_Codex完成报告_v3.md @@ -0,0 +1,24 @@ +# STEP-082 iOS Native FieldRuntime Facade 与 XCFramework 发行基线 Codex 完成报告 v3 + +## 执行结论 + +`IMPLEMENTED / IOS_CORE_PATH_PASS / WAITING_FINAL_CONTROL_REGRESSION`。 + +本报告是 STEP-083 发现 renderer 选择错误后的 post-close correction;保留 v1/v2 的历史证据,但旧默认 OpenGL XCFramework 不再具有 iOS Particle 发行资格。 + +## Renderer 纠正与发行 artifact + +- 不带 renderer flag 的 `bazel build //platform/ios:MapLibre.dynamic` 默认选择 OpenGL。旧 zip:`24639200` bytes,SHA-256=`64faa174c5c0dc0a2da3e289f0beaf2dedf7298665e3345f39836729b668e239`,source commit=`136ed726cf749c24b3673c046581a38ef48be390`。它在 RN Test Host 中可显示色场但无粒子,因此作废,不能发布为 Particle iOS baseline。 +- 固定发行命令:`bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic`。 +- 新 Metal zip:`MapLibre.dynamic.xcframework.zip`,`24943196` bytes,SHA-256=`ae88975fc20f4d9961b95079638d0e9c0d8e61d4394d5c0a8ffcc48d173a827f`,source commit=`bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd`。 +- device binary 审计含 `MLNMapView`、Metal/`MTLDevice`/`ParticleGPU`,无 OpenGL marker。release manifest 保持 `VALIDATED`、URL=`LOCAL_BUILD`;未伪造远端发布。 + +## 真实消费证据与边界 + +Pleiades iPad mini 5 上,RN Test Host 的 single-MapLibre SPM signed Release 已构建、MapLibre.framework 已签名并安装。一次 V4 Wind load 实际执行 `clearAllEncodedFields`、color/particle binding、T0/T1 `ACCEPTED_INSERTED` 和 midpoint seek,用户确认“粒子恢复了”。 + +本次未逐按钮完成 PLAY/PAUSE、clear selected/all、detach/re-attach 控制回归;这部分保留给 STEP-083 final control regression,不能据此扩展为完整交互验收。 + +## 范围与 Git + +新 Metal binary 的可追溯 source commit 是已提交 facade baseline `bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd`;RN/Test Host 的未提交文件不影响该 MapLibre binary 内容。本报告不提交、不 push,且未修改 Android、Shared Core、renderer 算法或 `NativeWeatherRuntimeBridge.ts`。 diff --git a/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v1.md b/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v1.md new file mode 100644 index 000000000..af4c773a0 --- /dev/null +++ b/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v1.md @@ -0,0 +1,47 @@ +# STEP-083 iOS RN 统一 FieldRuntime / TurboModule / Fabric 与 XCFramework 消费 Codex 完成报告 v1 + +## 1. 执行结论 + +`IMPLEMENTED / WAITING_IOS_VALIDATION`。未提交、未 push,最终环境验收交给 RDC。 + +## 2. 调查结论 + +- MLRN `11.3.6`:Fabric class 是 `MLRNMapViewComponentView`;它在 `prepareView` 设 `contentView = [[MLRNMapView alloc] ...]`;`MLRNMapView : MLNMapView`。 +- MLRN 当前 module 的真实 lookup 是注入 `viewRegistry_DEPRECATED` 的 `addUIBlock`,在 block 内按 tag 查 component,再验证 component/content classes。bridge 同样使用该当前 RN UIManager registry;没有复制它或保留 view pointer。 +- RN `0.85.2` generated ObjC++ spec 的 module pattern 为 protocol + `getTurboModule:` 返回 `std::make_shared<...SpecJSI>(params)`,Promise selector 使用 `resolve:(RCTPromiseResolveBlock) reject:(RCTPromiseRejectBlock)`;`codegenConfig.ios.modulesProvider` 生成 provider registration。 +- MLRN podspec 的 `$MLRN_SPM_SPEC` 确实存在,默认 product 为 `MapLibre`;post-install 将其加入 MLRN pod target 与 user target。 + +## 3. Public API parity matrix + +| Spec methods | iOS public seam | status | +| --- | --- | --- | +| attach, detach, getState | `MLNMapView` category + runtime snapshot | implemented | +| clearWindFields, clearFields, submitWindField, submitField | V3 runtime facade | implemented | +| submitEncodedField, encoded binding/time/playback/clear (8) | V4 runtime facade | implemented | +| value source, advection, enabled/visibility, FPS/time/playback/speed (9) | V3 control facade | implemented | +| color stops, particle/trail color, count, line width, length, opacities, fade (8) | visual facade | implemented | + +The 34 `NativeWeatherRuntimeBridge.ts` methods are present as 34 ObjC++ selectors. No TS public type, union, method name, argument ordering, result string, or Android implementation changed; only the Android-specific missing-module text became platform-neutral. + +## 4. Package and binary identity + +- Added `ios/WeatherRuntimeBridge.h/.mm`, `MapLibreParticleRN.podspec`, iOS autolinking and codegen provider configuration; npm files include all of them. +- The Particle pod depends only on `MapLibreReactNative` `11.3.6`; it has no MapLibre dependency, vendored framework, or SPM reference. Thus it cannot introduce a second MapLibre product. +- The Test Host gained only an iOS bundle identifier. Its `App.tsx` retains one shared JS path and no `Platform.OS` branch. + +## 5. Checks actually run + +- Podspec Ruby syntax: PASS. +- package dry-run tarball, with temporary cache: PASS; includes iOS native files, podspec, Android, and src. +- codegen provider configuration static assertion: PASS. +- Spec/implementation method-count parity: 34/34 PASS. +- `git diff --check`: PASS. +- `npm ci --offline --ignore-scripts`: BLOCKED by absent locally cached `update-browserslist-db-1.3.2.tgz`; no network was used. Therefore TypeScript/codegen/Xcode/Pod/Test Host runtime have not run. + +## 6. Required RDC validation + +The STEP-082 XCFramework artifact is a local zip and its manifest URL is `LOCAL_BUILD`; it is not yet an MLRN-consumable Swift Package distribution. Configure a real custom `$MLRN_SPM_SPEC` coordinate first, then run codegen/typecheck/Pods/Xcode and Test Host smoke. Check a single MapLibre product/class set and the V4 lifecycle sequence before closing STEP-083. + +## 7. Working tree protection + +The user dirty entries `docs/doxygen/doxygen-awesome-css`, `platform/ios/app/MBXAppDelegate.m`, and `platform/windows/vendor/vcpkg`, plus the two pre-existing STEP-082 task files, were not touched. No Git commit or push was performed. diff --git a/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v2.md b/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v2.md new file mode 100644 index 000000000..d9f564593 --- /dev/null +++ b/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v2.md @@ -0,0 +1,43 @@ +# STEP-083 iOS RN 统一 FieldRuntime TurboModule Fabric 与 XCFramework 消费 Codex 完成报告 v2 + +## 执行结论 + +`IMPLEMENTED / IOS_CORE_PATH_PASS / WAITING_FINAL_CONTROL_REGRESSION`。 + +未提交、未 push。本报告只记录 RDC 已发生的 iOS consumer 验证和 Metal artifact 纠正;它不把未逐按钮测试的控制面写成完成。 + +## Git 基线与范围 + +- branch:`particle/main`;MapLibre Metal binary source commit:`bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd`。 +- RN/Test Host 未提交文件不影响该已提交 MapLibre binary 内容。 +- 未修改 `NativeWeatherRuntimeBridge.ts`、Android、Shared Core 或 renderer 算法。 +- 用户既有 dirty `docs/doxygen/doxygen-awesome-css`、`platform/ios/app/MBXAppDelegate.m`、`platform/windows/vendor/vcpkg` 与三份未跟踪 `docs/40_任务` 任务书均不纳入本次修改或提交范围。 + +## Renderer 与 artifact + +旧 `bazel build //platform/ios:MapLibre.dynamic` 默认使用 OpenGL。其 artifact 在 RN Test Host 中呈现色场而无粒子,不能用于 Particle iOS 发行。 + +固定命令为: + +```sh +bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic +``` + +新 Metal `MapLibre.dynamic.xcframework.zip`:`24943196` bytes,SHA-256=`ae88975fc20f4d9961b95079638d0e9c0d8e61d4394d5c0a8ffcc48d173a827f`。device binary 含 `MLNMapView`、Metal/`MTLDevice`/`ParticleGPU`,无 OpenGL marker。manifest 为 `VALIDATED`,URL 仍为 `LOCAL_BUILD`,未声明远端发布。 + +## RDC 已发生验证 + +- npm dependency install(`npm ci`)、typecheck、RN codegen、Expo prebuild 和 Pod integration 均已实际执行。 +- device arm64 build 与 signed Release 均出现 `BUILD SUCCEEDED`;MapLibre.framework 签名成功,Pleiades iPad mini 5 安装运行。 +- MLRN、App 与 Particle pod 共享 single MapLibre SPM product,没有第二份 MapLibre binary。 +- 一次 V4 Wind load 实际完成 `clearAllEncodedFields`、color binding、particle binding、T0 `ACCEPTED_INSERTED`、T1 `ACCEPTED_INSERTED` 和 midpoint seek;用户确认“粒子恢复了”。 + +## 未完成的 final control regression + +PLAY/PAUSE、clear selected、clear all、detach/re-attach 尚未逐按钮实测。它们是本 Step 的剩余验收项;在完成前不得将状态改写为 `CLOSED` 或 `PASS`。 + +## Final control regression 入口追加(待真机执行) + +RN Test Host 的 `App.tsx` 已新增单一 `RUN V4 FINAL CONTROL REGRESSION` 按钮。有效 `reactTag` 下,它复用现有 V4 `prepareV4` 公共 API 加载路径,并串行执行 PLAY → 约 500ms → PAUSE → clear selected → prepare → clear all → detach → attach → prepare → refresh;所有 command result 必须为 `ACCEPTED`,三次 prepare 的 T0/T1 均必须为 `ACCEPTED_INSERTED`。运行中按钮禁用,结果区域保留每一步摘要;任何失败明确显示失败步骤,不显示伪 PASS。detach/attach 后最后一次 prepare 会重新 binding/submit 并恢复粒子,供 Pleiades iPad 真机目视确认。 + +本追加只提供最终人工回归入口,不代表已执行真机验收。状态仍为 `IMPLEMENTED / IOS_CORE_PATH_PASS / WAITING_FINAL_CONTROL_REGRESSION`,直到用户确认该序列和最终粒子画面后才可关闭。 diff --git a/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v3.md b/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v3.md new file mode 100644 index 000000000..d050c5767 --- /dev/null +++ b/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v3.md @@ -0,0 +1,35 @@ +# STEP-083 iOS RN 统一 FieldRuntime TurboModule Fabric 与 XCFramework 消费 Codex 完成报告 v3 + +## 执行结论 + +`CLOSED / PASS`。 + +Pleiades iPad mini 5 真机已完成最终控制面回归;用户先确认 Metal 修正版 V4 Wind“粒子恢复了”,随后确认单按钮回归“FINAL CONTROL REGRESSION PASS”。本报告为同一 STEP-083 的正式关闭记录,不新建功能 Step,不提交、不 push。 + +## 最终真机验收 + +- 设备:Pleiades iPad mini 5。 +- Test Host signed Release:`BUILD SUCCEEDED`,已安装并启动。 +- MLRN、App 与 Particle bridge 使用 single MapLibre SPM product;没有第二份 MapLibre framework。 +- `RUN V4 FINAL CONTROL REGRESSION` 单按钮已在真机完成:`prepare#1 → PLAY → 约500ms → PAUSE → clear selected → prepare#2 → clear all → detach → attach → prepare#3 → refresh`。 +- 全部命令返回预期成功结果;最终 prepare/refresh 后 V4 Wind 粒子恢复并保持可见。用户两次真机确认分别为“粒子恢复了”与“FINAL CONTROL REGRESSION PASS”。 + +## Metal XCFramework 验证坐标 + +- renderer:Metal;构建命令:`bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic`。 +- artifact:`MapLibre.dynamic.xcframework.zip`。 +- size:`24943196` bytes。 +- SHA-256:`ae88975fc20f4d9961b95079638d0e9c0d8e61d4394d5c0a8ffcc48d173a827f`。 +- release manifest 仍为本地验证坐标 `LOCAL_BUILD`;本次未修改 release manifest,也未进行发布。 + +## 范围与保留项 + +- App.tsx 的 final regression 按钮保留为回归入口,未改动其逻辑。 +- 未修改 RN public API、iOS native/Core、renderer 或 release manifest。 +- 未覆盖 v2 报告。 +- 三处用户 dirty `docs/doxygen/doxygen-awesome-css`、`platform/ios/app/MBXAppDelegate.m`、`platform/windows/vendor/vcpkg` 保持不动;三份 `docs/40_任务` 未跟踪任务书也保持不动。 +- 当前 `particle/main` HEAD:`d8bb9f547bf0db2f222fb49f1ec5df77373de310`;本次仅做收尾文档且不提交。 + +## 下一步 + +STEP-083 已正式关闭。下一步进入 NavSea iOS 首批大集成,不再拆分微小的 build/install Step。 diff --git a/docs/报告/STEP-085_V4_Renderer性能修复_Codex完成报告_v1.md b/docs/报告/STEP-085_V4_Renderer性能修复_Codex完成报告_v1.md new file mode 100644 index 000000000..d268f8030 --- /dev/null +++ b/docs/报告/STEP-085_V4_Renderer性能修复_Codex完成报告_v1.md @@ -0,0 +1,52 @@ +# STEP-085 V4 Renderer 性能修复 Codex 完成报告 v1 + +## 1. 执行结论 + +`IMPLEMENTED / STATIC_CHECKS_PASS / BUILD_NOT_RUN_BY_CODEX — WAITING_FOR_RDC` + +Codex model:gpt-5.6-terra。推理强度:high。机器:Mac-mini-tei.lan。仓库:`/Users/tei/dev/MapLibreParticleLab-step085`。 + +## 2. Git 基线 + +- branch:`step/085-v4-render-efficiency` +- STEP-084 Final / STEP-085 Start HEAD:`bc6b8dd84b4c10e5263d61832661508c02d84d88` +- End HEAD、commit、push 与 remote HEAD:本报告随最终 scoped commit 回填,以最终 Git 事实为准。 +- STEP-084 已由 `fix: disable particle speed color by default` 推入 `origin/particle/main`,本 Step 从该更新后的 remote HEAD 创建独立 worktree。 + +## 3. 实际实现 + +1. `RasterArrayRendererCadence` 是 Source-owned shared internal state。它以 monotonic deadline gate V4 Particle simulation,不推进 forecast。Particle active/visible/field-ready 时才请求 repaint;未到 deadline 的 camera frame 只重合成已有 trail。 +2. WeatherColor 不再把 `sourceTimeline == nullptr` 解释为动画。V4 静态 PAUSE 返回 false;legacy playback 仍沿用 timeline cadence。 +3. V4 materialized projection 与 RenderLayer source endpoint 改为 immutable shared pointer。materialization 只在 binding/window refresh 时产生,prepare 不复制完整 `scalar`、`vector`、`valid`。 +4. WeatherColor 只在 wrap 集合变化时重建 drawable;texture/UBO 参数由 persistent Resource/Tweaker 读取,正常 MIX、opacity、stops 和 field endpoint 更新不重建 LayerGroup drawable。 +5. Android `WeatherRuntime` 和 iOS `MLNParticleFieldRuntime` 仅补下一帧 wake:已绑定 particle fieldKey 且该 key 已收到 accepted V4 submit、可见且 target 为 30/60 时才请求 repaint;hidden/clear/disable/dispose 停止,disable 后 re-enable 可恢复既有 V4 field 的 wake。公共 RN/ObjC/Java API 未新增。 +6. 新 debug counters 为 non-spam atomic 累计值;release no-op。覆盖 repaint/update/simulation/drawable/texture/GPU/trail/materialize/window revision。 + +## 4. 根因与语义 + +根因是 V4 具有独立 projection,却被 renderer 的 `sourceTimeline` sentinel 当作“持续动画”;同时 prepare 直接给 value snapshot 赋值。修复后 Forecast PAUSE 只暂停自动 forecast time,Particle visual simulation 仍在 active cadence 下运行。V4 PLAY 自动推进没有 production owner:`advanceForecastClock` 只定义未被生产 Runtime/bridge 调用,本 Step 明确保留为后续设计项。 + +## 5. 静态检查与测试 + +- 已执行:`git diff --check`,通过。 +- 已执行:对 V4 projection/cadence/repaint/materialization/drawable code path 的 `rg` 定向静态审计,确认 no-timeline WeatherColor 不再 permanent repaint,V4 Particle 调用 `consumeIfDue()`。 +- 已执行:平台 wake 静态审计;wake 只使用 Host command bookkeeping,Core `RasterArrayRendererCadence` 仍是 deadline 和 simulation advance 的唯一裁决者。 +- 新增:`RasterArrayV4RendererCadenceGatesVisualSimulationWithoutForecastPlayback` C++ test;V4 materializer tests 调整为 shared snapshot lifetime 断言。 +- 未运行测试二进制:任务书不允许以未编译的 native test 冒充验证,且 Native 编译/链接由 RDC 负责。 + +## 6. 未执行的编译与运行 + +未执行 Bazel、CMake/Ninja/Make、clang/clang++ Native compile、Xcode、Metal/iOS build、Gradle、AAR/APK、artifact、ADB、设备安装、真机或 CPU/GPU/heap 采样。状态为 `BUILD_NOT_RUN_BY_CODEX — WAITING_FOR_RDC`,不代表 Android/iOS/Metal/AAR/性能 PASS。 + +## 7. RDC 后续入口 + +1. 在本 Final HEAD 上执行受影响 C++ renderer/source/test 精确编译和相关 test filter;确认 shader/codegen/build graph。 +2. 执行 `bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic`,并使用当前 iOS Test Host 的 Metal XCFramework 消费链完成 device build。 +3. 按 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md` 构建新的 Android OpenGL AAR、发布新版本坐标、从公网 Maven/npm 消费并构建 Test Host;不得覆盖现有版本。 +4. 统一执行任务书 22/23:A/B/C/D、30/60/AUTO、PAUSE、speed-color OFF/ON/OFF,以及 pan/zoom/bearing/pitch/resize;采集 RenderThread/GPU/jank 与 debug counters。确认静态 WeatherColor idle、camera frame 不误推进、field upload/GPU/trail/drawable 不异常增长。 + +## 8. 已知问题、未完成项 + +- V4 ForecastClock PLAY 自动推进的 production owner 缺失,未在本 Step 引入第二时钟。 +- Native Heap/detach/destroy 未归因或修改。 +- 所有 Native 编译、artifact 和设备验收待 RDC;本次 Android Java / iOS Objective-C++ wake 收紧也待目标平台编译。如发现跨 backend 架构问题,应停止 RDC 扩改并重新进入 Codex 任务。 diff --git a/docs/报告/STEP-087_QBIN_V3解码与13.4.1-particle.9升级_Codex完成报告_v1.md b/docs/报告/STEP-087_QBIN_V3解码与13.4.1-particle.9升级_Codex完成报告_v1.md new file mode 100644 index 000000000..6f42e81c3 --- /dev/null +++ b/docs/报告/STEP-087_QBIN_V3解码与13.4.1-particle.9升级_Codex完成报告_v1.md @@ -0,0 +1,57 @@ +# STEP-087 QBIN V3 解码与 13.4.1-particle.9 升级完成报告 v1 + +## 1. 执行方式 + +本任务最初尝试在 Mac 上交给 Codex 执行,但 Codex CLI 返回 usage limit,未实际执行修改。后续由 ChatGPT 通过 RDC 直接在 Mac 专用工作树 `/Users/tei/dev/MapLibreParticleLab-qbv3-p9` 完成代码、测试、版本升级、文档与 Git 收尾。 + +## 2. 实施内容 + +- 基线包含 `13.4.1-particle.8` 的 Android / iOS 统一发行改动。 +- 新增 `QbinV3Decoder`,按 WeatherV2 QBIN V3 v2.2 冻结稿实现固定 Header / Descriptor、CRC32、canonical packing、VALIDITY、Predictor + ZigZag + Rice、Direction 环形残差与资源上限校验。 +- 生产 Block Geometry 固定为 16×16;不做双块尺寸猜测。 +- `EncodedFieldIngress` 显式识别 `QBV2` / `QBV3` magic;V2 兼容链保留,V3 使用新 decoder;解码后仍统一进入 `FieldProfileValidator`。 +- `RasterArraySource::submitEncodedField()` 改为统一使用 `EncodedFieldIngress::decodeAndValidate()`。 +- CMake / Bazel Core 注册 V3 decoder;CMake test 注册 V3 tests。 +- 新增 Current / Rain / Wave / Wind 共 8 个 QBIN V3 Golden Fixtures 及 manifest / 校验脚本。 +- Native / RN 当前版本声明统一升级到 `13.4.1-particle.9`。 + +## 3. Block Geometry 结果 + +C++ benchmark 使用四类真实 Golden 数据共 8 帧: + +- 16×16:2,470,419 bytes;encode 218.713 ms;decode 104.121 ms。 +- 32×32:2,514,288 bytes;encode 200.101 ms;decode 101.451 ms。 +- 32×32 payload 增加 43,869 bytes(约 +1.7758%),而 8 帧总 decode 仅减少约 2.67 ms。 +- 因网络/缓存/跨桥传输尺寸是 QBIN V3 的核心目标,最终冻结 16×16。 + +## 4. 验证结果 + +- QBIN V3 Golden Fixtures 校验:PASS,8 files,block=16×16。 +- C++ 定向测试:PASS 8 / FAIL 0。 +- Current / Rain / Wave / Wind V3 解码结果与对应 V2 canonical raw 做逐 plane / validity byte-for-byte 对比:PASS 8 / FAIL 0。 +- `git diff --check`:PASS。 +- iOS Metal `MapLibre.dynamic` 正式 Bazel 构建:PASS;生成 device-arm64 `MapLibre.dynamic.xcframework.zip`。 +- Android 统一 Release 构建:PASS,包含 armeabi-v7a、arm64-v8a、x86、x86_64 四 ABI。 +- Android APK / ADB:按项目规则未执行,由用户手工完成。 + +## 5. 文档与数据 + +- 原始设计:`docs/30_设计/WeatherV2_QBIN_V3通用天气场二进制格式与React独立Render_Binding设计_v2.2_最终冻结稿.md`。 +- Block Geometry 决策:`docs/30_设计/MapLibreParticle_QBIN_V3_Decoder与BlockGeometry实施决策_v1.md`。 +- Step 记录:`docs/50_Step记录/STEP-087_QBIN_V3解码与13.4.1-particle.9升级_v1.md`。 +- Golden Fixtures:`integration/react-native/MapLibreParticleRNTestHost/assets/qbin-v3-golden/`。 + +## 6. Git 收尾 + +- 实现提交:`0e8c87fe49fb45c23a88d8f6cbf79dbc70df393c` +- 实现提交信息:`feat: add QBIN V3 decoder for particle.9` +- 发布 finalize 提交:`63047540cba703c8efeae0df71e759e165315f85` +- 发布 finalize 提交信息:`release: finalize unified particle.9` +- Push:PASS +- 远端分支:`origin/particle/main` +- Release Final HEAD / Tag HEAD:`63047540cba703c8efeae0df71e759e165315f85` +- Release Tag:`maplibre-particle/rn/13.4.1-particle.9` +- Android AAR:`6114067c45685e2d20b3dd5115c480a39341177ac8608f86ec431562f649e395` +- RN tgz:`98dc94e88dc69c06c9d99cc8ece3547f0489ba650c4a3d71814eab45e8720dcb` +- iOS XCFramework zip:`fbda91e09d53e3cd1362590bfc0410b6fb5f9fef4cf99796d4ab7b4b84c93562` +- SwiftPM:`6.28.0-particle.9`,public readback PASS。 diff --git a/docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v1.md b/docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v1.md new file mode 100644 index 000000000..80c4d095b --- /dev/null +++ b/docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v1.md @@ -0,0 +1,155 @@ +# STEP-088 静态底图缓存 PoC 与 Renderer 观测 Codex 完成报告 v1 + +## 1. 执行结论 + +- 状态:`IMPLEMENTED / WAITING FOR ANDROID VALIDATION` +- 结论:Retained Static Underlay PoC、OpenGL/Metal compositor、Renderer counters 与 Native 定向测试代码已实现;RDC 已完成 Mac OpenGL/Metal 编译和 4/4 定向测试。Android/NavSea 尚未验证,因此不关闭 Step,也不进入 STEP-089/090。 + +## 2. Git 基线 + +- 机器:Mac mini,Darwin x86_64 +- 仓库:`/Users/tei/dev/MapLibreParticleLab` +- 分支:`particle/main` +- 起始 HEAD:`f47469d2ab77cdb959ecfe4081dc571fa1ab78f9` +- STEP-088 实现 Final HEAD:`8b9ab303228c307612c47f7f3379ad06206ea14e` +- 实现提交信息:`feat: add retained underlay render cache poc` +- 当前版本坐标:`13.4.1-particle.10` +- 实现 push:成功,`f47469d2a..8b9ab3032 particle/main -> particle/main` +- 远端分支:`origin/particle/main` +- 既有 dirty/untracked:保留且未暂存,包括 `AGENTS.md`、两个 modified submodule、`platform/ios/app/MBXAppDelegate.m` 与用户既有任务文档。 + +## 3. 开工前只读确认 + +已按当前代码确认: + +- RasterParticle 与 WeatherColor 的 `needsRepaint()` 经 `RenderOrchestrator::hasTransitions()` 驱动完整 MapLibre frame。 +- `Renderer::render()` 每帧仍调用 `createRenderTree()`,随后进入 `Renderer::Impl::render()`。 +- `updateLayers/processChanges`、LayerGroup tweaker/upload、`visitRenderTargets` 均位于 draw 分段之前,STEP-088 未跳过。 +- main clear、opaque reverse、translucent forward、legacy items、debug、postRender、present 的原始顺序已核对。 +- `RenderTarget` / `OffscreenTexture` 以及 Heatmap/Hillshade 的 offscreen + texture composite 模式已作为实现样板,未修改 Heatmap/Hillshade 自身语义。 +- background layer optimized-as-color 继续通过 main framebuffer `backgroundColor` clear;underlay offscreen 使用透明 clear。 + +## 4. 实际实现 + +### Boundary + +- `src/mbgl/renderer/render_layer.hpp`:增加 Renderer-internal boundary 与 legacy-rendering virtual hooks。 +- `render_weather_color_layer.hpp`、`render_raster_particle_layer.hpp`:声明为 boundary candidate。 +- `render_location_indicator_layer.hpp`:标识 legacy rendering,供 boundary 以下 ordering fallback。 +- `render_orchestrator.cpp`:在实际 ordered layers 上取最小 candidate index。 + +### LayerGroup 分段 + +- `render_orchestrator.hpp`:增加 before/from、forward/reverse 四种 visitor,callback 获得完整 LayerGroup 序列 original ordinal。 +- `renderer_impl.cpp`:`currentLayer = totalGroups - 1 - originalOrdinal`,`depthRangeSize` 仍按完整 group 数计算,避免子集从 0 编号改变 depth/transparency。 + +### Cache 与 Renderer Flow + +- 新增 `retained_underlay_cache.hpp/.cpp`:管理 key、valid、RGBA offscreen、compositor group、build/reuse/fallback counts 与 last invalidation reason。 +- key 包含 viewport、boundary 与 projection/camera matrix。 +- retained path 只改真正 draw 阶段;createRenderTree、source/layer update/prepare、tweaker/upload 全部保留。 +- cache build 顺序为 independent targets → static opaque/translucent to offscreen → main clear → cached composite → dynamic opaque/translucent → legacy/debug/postRender/present。 + +### Compositor 与 Backend + +- 新增 `RetainedUnderlayCompositeShader` 的 OpenGL GLSL/generated header 与 Metal shader source/metadata。 +- shader manifest、BuiltIn enum、shader info/defines、GL/Metal backend registration 与 CMake source lists 已接线。 +- gfx Context 增加四参数 offscreen virtual;OpenGL 补齐 depth/stencil renderbuffer attachment,Metal/Vulkan/WebGPU 明确 override 已有实现。 + +### Debug Counters + +在 `include/mbgl/particle/renderer_debug_counters.hpp` 新增: + +- `FullRendererFrame` +- `RetainedUnderlayBuild` +- `RetainedUnderlayReuse` +- `RetainedUnderlayFallback` +- `RetainedUnderlayInvalidation` +- `RetainedUnderlayStaticGroupDraw` +- `RetainedDynamicGroupDraw` + +Release/NDEBUG 仍 no-op,不增加 Host API,不逐帧刷日志。 + +## 5. 修改文件 + +实现提交共 39 个文件: + +- 构建:`CMakeLists.txt`、`cmake/opengl.cmake`、`cmake/metal.cmake`、`test/CMakeLists.txt`。 +- Context/Offscreen:`include/mbgl/gfx/context.hpp`、`include/mbgl/{mtl,vulkan,webgpu}/context.hpp`、`src/mbgl/gl/context.cpp/.hpp`、`src/mbgl/gl/offscreen_texture.cpp/.hpp`。 +- Renderer:`src/mbgl/renderer/render_layer.hpp`、`render_orchestrator.cpp/.hpp`、`renderer_impl.cpp/.hpp`、三个相关 RenderLayer headers、新增 `retained_underlay_cache.cpp/.hpp`。 +- Shader:`shaders/manifest.json`、两个新增 GLSL、GL/Metal generated/handwritten headers、Metal source、shader source/manifest/defines/info 与 GL/Metal backend registration。 +- 观测:`include/mbgl/particle/renderer_debug_counters.hpp`。 +- 测试:新增 `test/renderer/retained_underlay_cache.test.cpp`;RDC 小修 `test/gl/gl_functions.test.cpp` 的 Apple/macOS 符号条件。 + +## 6. Fallback 与安全边界 + +- 无 boundary:原 Renderer。 +- static prefix 为空、style 未 loaded、has3D、debug options:full render。 +- static prefix 存在 transition/repaint、symbol placement/fade 或 source fading tile:full render。 +- boundary 以下存在 legacy renderer item:full render。 +- viewport、camera fingerprint、boundary 改变:cache invalid 后 build。 +- offscreen texture/compositor 创建或 renderable 检查失败:计数并 full-render fallback。 + +STEP-088 的成立前提是固定 Camera 且 Style/Tile 稳定。production 级 style/tile/drawable/context invalidation 明确留给 STEP-089,未在本次越界实现。 + +## 7. 新增文件 + +- `include/mbgl/shaders/gl/retained_underlay_composite.hpp` +- `include/mbgl/shaders/mtl/retained_underlay_composite.hpp` +- `shaders/retained_underlay_composite.vertex.glsl` +- `shaders/retained_underlay_composite.fragment.glsl` +- `src/mbgl/renderer/retained_underlay_cache.hpp` +- `src/mbgl/renderer/retained_underlay_cache.cpp` +- `src/mbgl/shaders/mtl/retained_underlay_composite.cpp` +- `test/renderer/retained_underlay_cache.test.cpp` + +## 8. 与任务书差异 + +- 功能范围无扩大:未进入 STEP-089/090。 +- 按新 `AGENTS.md`,长时间 Mac 编译/测试由 RDC 执行,Codex 只负责实现、静态审计、文档与 Git 收尾。 +- RDC 为完成 Mac OpenGL test-runner 链接,增加了任务范围内必要的 `test/gl/gl_functions.test.cpp` Apple/macOS 平台条件小修。 +- 没有提交用户对 `AGENTS.md` 的全局流程修改,也没有提交既有无关 dirty/untracked。 + +## 9. RDC 构建与测试记录 + +- OpenGL:`cmake --build build-step088-opengl --target mbgl-test-runner -j3`,结果成功。 +- 定向测试:4/4 成功: + - `RetainedDynamicBoundary.SelectsFirstCandidate` + - `RetainedLayerGroupRange.PreservesForwardReverseOriginalOrdinals` + - `RetainedUnderlayCache.BuildReuseAndInvalidation` + - `RetainedUnderlayCache.UnsupportedAndResourceFailureFallBackSafely` +- iOS/Metal device arm64:`cmake --build build-ios --config Debug --target mbgl-core -j3`,结果 `BUILD SUCCEEDED`。 +- `git diff --check`:成功。 +- RDC 小修说明:Darwin 的 gl functions 实现仅在 iPhone/Simulator 定义 `glInvalidateFramebuffer` / `glInvalidateSubFramebuffer`;macOS test-runner 不应无条件引用,故增加 Apple target 条件。该修复仅影响测试平台链接,不改变 Renderer 业务语义。 + +按最新 `AGENTS.md` 分工,以上长时间编译/测试由 RDC / 普通 Terminal 执行并回传;Codex 最终收尾没有再次启动 build/test。 + +## 10. Android 未执行与无法验证 + +以下均未执行:Android Gradle、AAR/APK、ADB、Android 真机/模拟器、NavSea 实机、Jest。因此无法验证复杂 NavSea Style 下的实际视觉、缓存 counters、draw-call 降幅、RenderThread/main/JS CPU 收益以及所有交互 invalidation。 + +## 11. PoC 限制、已知问题与未完成事项 + +- 未实现 STEP-089 的 production 全量 invalidation。 +- 未实现 STEP-090 的 Map/update/createRenderTree/source/prepare CPU fast path。 +- 未修改 NavSea、React、Host API 或 C-001。 +- Android/NavSea 手工验证待用户执行;结果回传前不得把本 Step 标记为关闭。 + +## 12. 生成产物 + +本 Step 不生成 AAR/APK/XCFramework/npm artifact;没有发布物、ABI、大小或 SHA-256。Mac 输出仅用于编译/测试验证,不作为发行 artifact。 + +## 13. 是否满足进入 STEP-089 条件与下一步建议 + +当前不满足。进入 STEP-089 的前置条件是用户/RDC 在 Android/NavSea 复杂真实 Style 上确认:画面与 ordering 正确、fixed Camera 下 build 约一次且 reuse/Particle advance 持续、static group draw 不随 Particle FPS 增长、camera/resize/fallback 不出现 stale cache。STEP-088 未取得这些证据前不得进入 STEP-089;STEP-090 同样未执行。 + +下一步只建议执行 STEP-088 Android/NavSea 手工验证,不创建或执行新的 Step。 + +## 14. Git 收尾 + +- Final implementation commit hash:`8b9ab303228c307612c47f7f3379ad06206ea14e` +- Commit message:`feat: add retained underlay render cache poc` +- Push result:成功 +- Remote branch:`origin/particle/main` +- STEP-088 implementation Final HEAD:`8b9ab303228c307612c47f7f3379ad06206ea14e` +- 本报告与 Step/开发记录由后续独立 docs closeout commit 承载;该提交的精确 hash 因 Git commit 不能自引用,记录在最终响应和远端 HEAD 核验中。 diff --git a/docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v2.md b/docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v2.md new file mode 100644 index 000000000..95da36451 --- /dev/null +++ b/docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v2.md @@ -0,0 +1,172 @@ +# STEP-088 静态底图缓存 PoC 与 Renderer 观测 Codex 完成报告 v2 + +## 1. 执行结论 + +- 状态:`ANDROID TEST APP PASS / WAITING FOR NAVSEA VALIDATION` +- Android MapLibreAndroidTestApp 运行态验证:PASS。 +- STEP-088 overall:未关闭,不得写作 CLOSED/PASS。 +- NavSea 复杂真实 Style 尚未验证,因此不得进入 STEP-089/090。 + +## 2. Git 基线与提交 + +- 机器:Mac mini,Darwin x86_64 +- 仓库:`/Users/tei/dev/MapLibreParticleLab` +- 分支:`particle/main` +- 本轮起始 HEAD:`e7d109246a225eb1c1b2f177aa30e3c7fcdf658e` +- v1 实现提交:`8b9ab303228c307612c47f7f3379ad06206ea14e` +- 本轮代码修正提交:`1d1a7915e0886c46be40f4fc9a2eff13192f804b` +- 本轮代码提交信息:`fix: scope retained underlay placement fallback` +- 代码 push:成功,`e7d109246..1d1a7915e particle/main -> particle/main` +- 远端分支:`origin/particle/main` +- v2 文档 closeout commit 与最终 Final HEAD 记录在最终响应中,避免 Git commit 自引用。 + +## 3. Android 首轮失败事实 + +较早 STEP-088 APK 在 fixed camera / only Particle 场景的代表数据为: + +```text +frame=1200 build=0 reuse=0 fallback=1199 invalidation=1199 +staticDraw=0 dynamicDraw=0 particleAdvance=1199 +``` + +cache 从未 build。审查确认 retained path 无条件受全局 placement transition、`placementChanged` 和 `symbolFadeChange` 约束,导致 static prefix 不含 placement 时仍每帧 fallback。 + +## 4. 实际修改文件 + +代码提交只包含: + +- `src/mbgl/renderer/render_orchestrator.cpp` +- `src/mbgl/renderer/render_orchestrator.hpp` +- `src/mbgl/renderer/renderer_impl.cpp` +- `platform/android/MapLibreAndroidTestApp/build.gradle.kts` + +文档提交只包含: + +- `docs/50_Step记录/STEP-088_静态底图缓存PoC与Renderer观测_v2.md` +- `docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v2.md` + +未修改或提交 `AGENTS.md`、submodule、iOS dirty 文件、STEP-082/083 untracked、既有 untracked STEP-088 task/规划文档或其他用户文件。 + +## 5. Renderer placement-scoping 修正 + +- `RenderOrchestrator::retainedUnderlayUsesPlacement(boundary)` 只遍历 boundary 之前的 ordered layers,判断 retained static prefix 是否真正依赖 placement。 +- `retainedUnderlayIsStable(...)` 继续保护 light/static-layer transition;只有 static prefix 使用 placement 时才检查 `placementController.hasTransitions(...)`。 +- `Renderer::Impl::render(...)` 只有在 static prefix 使用 placement 时才要求 `placementChanged == false` 且 `symbolFadeChange == 0`。 +- fallback reason 拆分为 `static-transition` 与 `static-placement`,便于区分原因。 +- 无 boundary、empty prefix、style-not-loaded、3D、debug、legacy ordering、resource/compositor failure 等 STEP-088 fallback 未改变。 + +该修正只调整 retained static prefix 的 eligibility,没有实现 production 全量 invalidation,也没有改变动态 boundary、LayerGroup ordinal/depth、draw ordering、Host API 或 C-001。 + +## 6. Debug reason 日志 + +保留 debug-only `ParticleRetained` 周期日志中的: + +```text +reason= +``` + +它复用 `RetainedUnderlayCache::getLastInvalidationReason()`,用于 Android 验证 `first-frame`、`camera-or-boundary`、`style-not-loaded`、`static-transition` 和 `static-placement`。日志仍位于 `#ifndef NDEBUG`,不改变 Release 行为,也不增加 Host API。 + +## 7. Android Validation strip property + +MapLibreAndroidTestApp 增加: + +```text +maplibre.validation.stripNativeDebugSymbols=true +``` + +默认 Debug 构建继续 `keepDebugSymbols += "**/*.so"`。只有显式启用 Validation property 时才允许 strip native symbols,使 x86_64 Debug APK 从约 375 MB 降至约 57 MB。Debug counters 与 `NDEBUG` 语义不变。 + +## 8. RDC Mac 最终复验 + +- `cmake --build build-step088-opengl --target mbgl-test-runner -j3`:PASS / no work to do;源码/object 时间戳已确认当前 placement-scoping 修改已编入 object。 +- STEP-088 定向测试 4/4 PASS: + - `RetainedDynamicBoundary.SelectsFirstCandidate` + - `RetainedLayerGroupRange.PreservesForwardReverseOriginalOrdinals` + - `RetainedUnderlayCache.BuildReuseAndInvalidation` + - `RetainedUnderlayCache.UnsupportedAndResourceFailureFallBackSafely` +- `cmake --build build-ios --config Debug --target mbgl-core -j3`:`BUILD SUCCEEDED`。 +- `git diff --check`:PASS。 + +以上均由 RDC 执行。Codex 本次收尾未执行任何新 build/test、Gradle、ADB、Xcode 或设备操作。 + +## 9. Android Test App 环境 + +- 设备:`192.168.200.139:5555`,FZG1-2 +- Android:13 +- ABI:x86_64 +- 构建:MapLibreAndroidTestApp OpenGL Debug x86_64 +- Validation strip property:启用 +- build/install/ADB/Activity:成功 + +## 10. Android Test App 运行态结果 + +### Fixed camera / only Particle + +```text +frame=840 build=1 reuse=836 fallback=2 invalidation=3 +reason=first-frame staticDraw=2 dynamicDraw=5022 particleAdvance=839 +``` + +后续 `reuse` 持续增长,`staticDraw` 保持不变。 + +### Camera DATELINE + +`build` 从 1 增至 2,`staticDraw` 从 2 增至 4;随后 reuse 恢复持续增长,原因为 `camera-or-boundary`。符合 camera transition 后安全 invalidation/fallback、稳定后单次 rebuild/reuse。 + +### REAL WEATHER + +`REAL_WEATHER_INITIAL_WINDOW_READY` 和 `WEATHER_COLOR_FIELD_READY` 正常;GSI raster + WeatherColor + Particle 正常进入 retained path。代表数据: + +```text +frame=6960 build=3 reuse=6907 fallback=49 invalidation=52 +staticDraw=6 particleAdvance=6959 +``` + +fixed camera 后 reuse 持续增长、`staticDraw` 保持 6。`TARGET_FPS_CHANGED target=FPS_30` 成功,缓存继续 reuse。 + +### Resize / rotation + +viewport `1280x680 → 800x1160 → 1280x680`;过渡期间安全 `style-not-loaded` fallback,稳定后 rebuild/reuse。设备已恢复 `accelerometer_rotation=1`、`user_rotation=0`。 + +### STATIC / ACTIVE + +STATIC 时 RenderThread 约 0%;ACTIVE 后恢复 continuous render,retained reuse 继续增长且 `staticDraw` 不再增长。旧 Test App 设备上的 ACTIVE RenderThread 约 96%~107% 仅作 Test App 对照,不代表 NavSea 最终性能。 + +### 异常扫描 + +未发现 `FATAL EXCEPTION`、`SIGSEGV`、`SIGABRT`、ANR、`GL_INVALID`、`EGL_BAD` 或 OOM。 + +## 11. 验收结论与边界 + +Android MapLibreAndroidTestApp 已证明: + +- static prefix 不使用 placement 时,全局 placement 状态不再错误阻塞 retained cache; +- first build 后能够持续 reuse,static draw 不再随 Particle frame 增长; +- camera 和 viewport 变化会安全 invalidation/rebuild; +- GSI raster + WeatherColor + Particle、FPS 30、STATIC/ACTIVE 均可运行; +- 未发现 crash、ANR、GL/EGL 或 OOM 异常。 + +但 Test App 不能替代 NavSea 复杂真实 Style。以下仍无法验证: + +- 真实产品 layer stack 的视觉与 ordering; +- 大量 PBF/fill/line/symbol static prefix 的 `staticDraw` 行为; +- Particle OFF/ON RenderThread、main、JS CPU 收益; +- pan/zoom/resize/fallback 后是否始终无 stale underlay。 + +因此 STEP-088 overall 仍为 `ANDROID TEST APP PASS / WAITING FOR NAVSEA VALIDATION`,不是 CLOSED/PASS。 + +## 12. 范围与未完成事项 + +- 未进入 STEP-089 production invalidation。 +- 未进入 STEP-090 animation-only fast path。 +- 未修改 NavSea、React、Host API 或 C-001。 +- NavSea 复杂真实 Style 验证完成前,不得进入 STEP-089/090。 + +## 13. Git 收尾 + +- Renderer/Validation fix commit:`1d1a7915e0886c46be40f4fc9a2eff13192f804b` +- Commit message:`fix: scope retained underlay placement fallback` +- Push result:成功,`e7d109246..1d1a7915e particle/main -> particle/main` +- Remote branch:`origin/particle/main` +- v2 文档 closeout commit hash、message、push result 与最终 Final HEAD:由最终响应记录。 diff --git a/docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v3.md b/docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v3.md new file mode 100644 index 000000000..2eb91ad6e --- /dev/null +++ b/docs/报告/STEP-088_静态底图缓存PoC与Renderer观测_Codex完成报告_v3.md @@ -0,0 +1,46 @@ +# STEP-088 静态底图缓存 PoC 与 Renderer 观测 Codex 完成报告 v3 + +## 最终结论 + +本轮完成 STEP-088 的运行时 Cache 控制、Release 最小诊断、Android/iOS Native API、RN RendererRuntime、NavSea 正式 PBF 测试场景及性能基线。 + +Android 实测已经确认: +1. retained underlay 可以停止静态 LayerGroup 的重复 draw; +2. Cache ON/OFF 可以在同一进程、同一 Map 中运行时切换; +3. 静态 draw 不是当前主要 CPU 成本; +4. 真正的大头是 Particle continuous repaint 触发后,PBF/vector/static renderer pipeline 被反复处理; +5. 因此后续应按规划进入 STEP-089 invalidation/revision,再进入 STEP-090 Static CPU Fast Path。 + +## 主要验证 + +- PURE PARTICLE:RenderThread ≈ 10.2% +- PURE PBF idle:RenderThread ≈ 0% +- production Style + PBF layers hidden + Particle:RenderThread ≈ 25.5% +- production PBF + Particle:RenderThread ≈ 96~100% +- BEFORE_SYMBOL_CLUSTER Cache ON/OFF: + - RenderThread 100.2% → 96.5% + - Process 114.2% → 102.8% + +## 构建/测试 + +- Android x86_64 Validation APK:PASS +- OpenGL target:PASS +- Directed C++ tests:5/5 PASS +- Metal mbgl-core:PASS +- ios-sdk-static:PASS +- RN TypeScript:PASS +- RN Test Host TypeScript:PASS +- RN iOS bridge:旧 STEP-083 SwiftPM 测试包缺少新增 MLNMapView API,因此失败;不是当前源码 platform SDK 编译失败,不作为 STEP-089 阻塞项。 +- git diff --check:PASS + +## Git 收尾 + +提交与 push 信息在完成 scoped commit 后补入本文件。 + +## Git 收尾记录 + +- Code commit: `8384c8bd241bcc70c974449475a9a8ca64a1e626` +- Code commit message: `feat: add retained underlay runtime controls` +- Remote branch: `origin/particle/main` +- Push result: SUCCESS +- Final HEAD: `a32e4e1d5b3bc5a5ddc2ff618b964e116265561c` diff --git a/docs/报告/STEP-089_静态渲染Revision与Invalidation基础层_Codex完成报告_v1.md b/docs/报告/STEP-089_静态渲染Revision与Invalidation基础层_Codex完成报告_v1.md new file mode 100644 index 000000000..78bbbbca2 --- /dev/null +++ b/docs/报告/STEP-089_静态渲染Revision与Invalidation基础层_Codex完成报告_v1.md @@ -0,0 +1,63 @@ +# STEP-089 静态渲染 Revision 与 Invalidation 基础层 Codex 完成报告 v1 + +## 最终状态 + +**CLOSED / PASS** + +## 完成项 + +- 已完成 static revision / buildRevision 门控。 +- 已完成 topology snapshot / topologyRevision。 +- 已覆盖 camera、viewport、pixel ratio、style、layer、source、tile、image、glyph、placement/fade、context、debug、3D/custom fail-closed。 +- 已移除 transient drawable churn 对 static revision 的误触发。 +- 已修复 equivalent stencil tile set 因 shared_ptr identity 导致的误失效。 +- 已保留真正 LayerGroup topology 变化的失效能力。 +- 已保持 STEP-088 Cache ON/OFF 运行时能力。 +- 未实现 STEP-090 fast path。 + +## 实机回归中发现并修复的问题 + +1. 首版把 LayerGroup drawable maintenance 当成 semantic mutation,导致 staticRevision 每帧递增。 +2. stencil tile shared_ptr identity 在正常帧中变化,导致 false positive。 +3. LayerGroup transient churn 继续导致 topology false positive。 +4. 最终改为 semantic content revision + stable topology snapshot,稳态复用恢复。 + +## 验证 + +- OpenGL:PASS。 +- Retained directed tests:12/12 PASS。 +- Metal/iOS mbgl-core:BUILD SUCCEEDED。 +- Android APK:BUILD SUCCESSFUL。 +- Android 稳态: + - build 固定; + - reuse 持续增长; + - staticRevision == buildRevision 且保持稳定。 +- Camera 变化: + - revision 前进; + - cache rebuild 一次; + - 随后恢复稳定 reuse。 +- git diff --check:PASS。 + +## Android 代表日志 + +```text +frame=480 build=2 reuse=302 staticRevision=21 buildRevision=21 +frame=600 build=2 reuse=422 staticRevision=21 buildRevision=21 +frame=720 build=2 reuse=542 staticRevision=21 buildRevision=21 +``` + +相机变化后: + +```text +build=19 reuse=3519 staticRevision=44 buildRevision=44 +build=19 reuse=3639 staticRevision=44 buildRevision=44 +build=19 reuse=3759 staticRevision=44 buildRevision=44 +build=19 reuse=3879 staticRevision=44 buildRevision=44 +``` + +## Git + +- Code commit: `bcec6ce5e0f4d1cfc35fd0b4dddadfaf6e37e0fb` +- Code commit message: `feat: add retained underlay revision invalidation` +- Remote branch: `origin/particle/main` +- Push result / Final HEAD:见最终收尾提交。 diff --git a/docs/报告/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex完成报告_v1.md b/docs/报告/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex完成报告_v1.md new file mode 100644 index 000000000..dcbe6c167 --- /dev/null +++ b/docs/报告/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex完成报告_v1.md @@ -0,0 +1,133 @@ +# STEP-090 Particle-only Static CPU Fast Path Codex 完成报告 v1 + +## 最终结论 + +**CLOSED / PASS** + +本阶段实现并验证了 MapLibre Native 内部自动 Particle-only CPU fast path。 + +## 完成项 + +- 已将 fast-path gate 放在 createRenderTree 之前。 +- 已实现 prepared RenderTree 跨帧复用。 +- 已实现 Particle-only 动态判定。 +- 已跳过 full static createRenderTree / prepare / static layer update。 +- 已保持 retained underlay GPU cache。 +- 已增加 Debug counters: + - ParticleFastFrameHit + - ParticleFastFrameMiss + - ParticleFullFrame +- 已增加周期性 fastReason 诊断。 +- 已处理 annotation pending mutation。 +- 已处理 non-particle repaint / transition。 +- 已处理 placement/fade / fading tile。 +- 已处理 camera / viewport / style / source / tile / topology / revision gate。 +- 已处理 context lost / clearData / reduceMemoryUse / style reload prepared tree 清理。 +- 未新增 Host/RN 手工 fast-frame API。 +- 未实现多动态层通用调度。 +- 未加入 Jest。 + +## 首轮实机问题与修复 + +首轮 Android 实机: +- prepared tree 实际正常跨帧保存; +- 但 fastHit=0; +- 诊断 initially 显示 no-prepared-tree。 + +调查发现: +1. clearPreparedRenderTree 覆盖了真实 miss reason,导致诊断误导; +2. 实际阻塞为 placement-or-fade; +3. 上一 full frame 的 placementChanged / symbolFadeChange 被当成永久 blocker。 + +最终修正: +- clear 不再覆盖上一真实 miss reason; +- placement gate 改为当前 `placementController.hasTransitions(timePoint)`; +- 上一 full frame 的 placementChanged / symbolFadeChange 不再永久阻塞; +- current active placement/fade 仍 fail closed。 + +## Android A/B 性能 + +### AFTER_CLIP + +FastPath ON: +- Process ≈ 27.7% +- Main ≈ 2.9% +- RenderThread ≈ 21.7% + +FastPath OFF: +- Process ≈ 111.4% +- Main ≈ 2.8% +- RenderThread ≈ 97.0% + +RenderThread 下降约 75 个百分点。 + +### BEFORE_SYMBOL_CLUSTER + +FastPath ON: +- Process ≈ 26.3% +- Main ≈ 2.7% +- RenderThread ≈ 20.3% + +## 运行时行为 + +稳定 fast path: +- fastHit 持续增长; +- particleFull 固定; +- retained build 固定; +- retained reuse 持续增长; +- staticRevision == buildRevision; +- fastReason=none。 + +Cache OFF: +- fastReason=retained-disabled; +- fastHit 停止; +- particleFull 持续增长; +- CPU 回到 full-path 基线。 + +Camera: +- 变化时 full; +- revision/build 更新; +- 稳定后 fastHit 自动恢复。 + +TOP: +- static-transition/static-placement; +- fast path 不命中; +- 保守 full path。 + +Viewport/orientation: +- resize/context变化时 full/rebuild; +- 稳定后自动恢复 fast path; +- 设备旋转设置最终恢复原值。 + +## 长时间运行异常复测 + +2026-09-23 08:23:27,长时间运行后的 Test App 曾出现一次 `app died, no saved state`。当时 frame counter 已约 1,868,000,fast path 长时间稳定;日志中未检出 FATAL、signal、ANR、GL/EGL 或 OOM crash signature,原因未能确定。 + +随后按最终收尾要求重新启动 Test App,进入 `NAVSEA PBF / AFTER_CLIP / CACHE ON` 连续复测约 3 分钟: + +- PID 始终为 29295,Activity 保持 top resumed; +- `particleFull=218` 固定; +- `fastHit` 约从 20662 增长到 33622; +- `buildRevision == staticRevision == 18`; +- `fastReason=none`; +- 6 个连续 30 秒 RenderThread CPU 样本:21.2% / 21.3% / 21.5% / 21.7% / 21.8% / 22.0%; +- 结束后扫描 FATAL EXCEPTION / Fatal signal / SIGSEGV / SIGABRT / ANR / GL_INVALID / EGL_BAD / OOM / app died,无命中。 + +结论:该 `app died` 作为**长时间单次异常、短时复测未复现**记录,不据此归因于 STEP-090 fast path;后续长时间 soak 继续观察。 + +## 测试与构建 + +- OpenGL:PASS。 +- Directed tests:21/21 PASS。 +- Android APK:BUILD SUCCESSFUL。 +- Android 安装/实机:PASS。 +- Metal/iOS mbgl-core:BUILD SUCCEEDED。 +- git diff --check:PASS。 +- 异常日志扫描:PASS。 + +## Git + +- Final implementation commit: `4e3d27e1f3fb0b3ee6eea78cfec7db7fa78d2b3e` +- Commit message: `feat: add particle static CPU fast path` +- Push result: SUCCESS +- Remote branch: `origin/particle/main` diff --git a/docs/报告/STEP-091_ZoomOut_Viewport_Particle_Reseed_Codex完成报告_v1.md b/docs/报告/STEP-091_ZoomOut_Viewport_Particle_Reseed_Codex完成报告_v1.md new file mode 100644 index 000000000..0ca2d84a2 --- /dev/null +++ b/docs/报告/STEP-091_ZoomOut_Viewport_Particle_Reseed_Codex完成报告_v1.md @@ -0,0 +1,96 @@ +# STEP-091 Zoom Out Viewport Particle Reseed Codex 完成报告 v1 + +## 1. 执行结论 + +**PASS(Codex 阶段)/ Android 实机验收待 RDC 执行** + +根因已按当前正式 Renderer 修复:Zoom Out 的 camera gesture 稳定后,在新的有效 viewport respawn domain 已计算完成的同一帧,一次性把 State/Trail seed 状态置为未播种;下一次既有 State/Trail pass 完成全粒子新 viewport 播种与旧 Trail 清理。没有执行完整 simulation reset。 + +## 2. Git 基线 + +- 已完成:branch `step091-zoom-out-reseed`。 +- 已完成:Starting HEAD `297b9126200cea3da3bdef2faacedcd9845f6d08`,对应任务指定 `origin/particle/main` 基线。 +- 已完成:implementation commit `4286c16a53d4f49001f3b41ed4638d3c6edc95b3`。 +- 已完成:implementation commit message `fix: reseed particles after zoom out`。 +- 已完成:implementation push 至 `origin/particle/main` 成功,远端由 `297b9126200cea3da3bdef2faacedcd9845f6d08` 前进到 `4286c16a53d4f49001f3b41ed4638d3c6edc95b3`。 +- final documentation closeout push / Final HEAD / `origin/particle/main`:见 Git 收尾章节与最终交付回读。 + +## 3. 实际修改文件 + +- 已完成:`src/mbgl/renderer/layers/render_particle_validation_layer.cpp`。 +- 已完成:`test/CMakeLists.txt`。 +- 已完成:`粒子开发记录.md`。 +- 已完成:用户提供的 `docs/40_任务/STEP-091_ZoomOut_Viewport_Particle_Reseed_Codex任务_v1.md` 保留并纳入本 Step 文档提交。 + +## 4. 新增文件 + +- 已完成:`src/mbgl/renderer/particle_viewport_reseed.hpp`。 +- 已完成:`test/renderer/particle_viewport_reseed.test.cpp`。 +- 已完成:`docs/50_Step记录/STEP-091_ZoomOut_Viewport_Particle_Reseed_v1.md`。 +- 已完成:本报告。 + +## 5. 实际实现 + +- 已完成:`ParticleViewportReseedGesture` 只在一轮 invalidation 开始时保存 `startZoom`,连续帧不覆盖。 +- 已完成:沿用既有 150 ms debounce;stable 后先计算新 domain/bounds,再消费 gesture 判定。 +- 已完成:采用 `0.05` epsilon。只有 `endZoom < startZoom - 0.05` 才属于明确 Zoom Out。 +- 已完成:有效 viewport domain 下仅设置 `stateSeeded=false` 与 `trailSeeded=false`,复用现有 shader initial-seed、State/Trail target clear 与 pass 顺序。 +- 已完成:fallback/无效 domain 返回 `SkipInvalidDomain` 并清除 active gesture,不留延迟 pending。 +- 已完成:一次 gesture 只消费一次;下一轮重新捕获新的 startZoom;真实 `resetSequence()` 会取消内部 gesture。 +- 已完成:新增成功与 skip 单次诊断日志,包含任务要求字段。 +- 已完成:Zoom In、Pan、Rotate、Pitch 和 epsilon 抖动不触发 reseed。 + +## 6. 禁止项与接口边界 + +- 已完成:未为 Zoom Out 调用 `resetSequence()`。 +- 已完成:未重建 State/Trail GPU resource。 +- 已完成:未重新上传 Field;未 reset Timeline/T0/T1/MIX;未 reload style/source。 +- 已完成:未修改 OpenGL/Metal shader。 +- 已完成:未修改 UBO layout。 +- 已完成:未修改公共 Layer、Host、RN API。 +- 已完成:未引入 partial ratio、CPU readback、第二套粒子系统或 Host camera callback。 + +## 7. STEP-089 / STEP-090 保护 + +- 已完成:未修改 retained underlay、staticRevision/buildRevision、topology/invalidation 或 ParticleFastPath 文件。 +- 已完成:camera invalidation 与 stable debounce 主路径保持;本次只在动态粒子层 stable domain 判定后改变一次 seed flags。 +- 已完成:continuous simulation cadence、WeatherColor 分层、Runtime Field Window/T0/T1/MIX 保持。 +- 无法验证:Android 实机上的 `fastHit` 增长恢复由 RDC 后续串行确认;Codex 未运行 APK/ADB。 + +## 8. 构建记录 + +- 已完成:Mac OpenGL Debug 配置:`cmake -S . -B build-step091-opengl -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DMLN_WITH_OPENGL=ON -DMLN_WITH_GLFW=OFF -DMLN_WITH_WERROR=OFF`,PASS。sandbox module/ccache temp 指向 `/private/tmp`。 +- 已完成:`ninja -C build-step091-opengl -j3 CMakeFiles/mbgl-core.dir/src/mbgl/renderer/layers/render_particle_validation_layer.cpp.o test/CMakeFiles/mbgl-test.dir/renderer/particle_viewport_reseed.test.cpp.o`,2/2 PASS。 +- 未完成且不作为本 Step PASS 声明:新 build tree 的全量 `mbgl-test-runner` 冷链接;在受影响 objects 已独立 PASS 后,于 182/659 主动停止,避免把精确验证扩大成无关全仓冷构建。 + +## 9. 测试记录 + +- 已完成:仓库内 gtest source/object 编译 PASS。 +- 已完成:`clang++ -std=c++17 -Wall -Wextra -Werror -Isrc /private/tmp/step091_particle_viewport_reseed_directed.cpp ...` 后运行原生 directed executable,8/8 PASS。 +- 已完成:覆盖明确 Zoom Out、Zoom In、Pan、Rotate/Pitch、epsilon 边界、fallback 消费、单 gesture 单次、下一 gesture startZoom 刷新。 +- 已完成:`git diff --check` PASS。 +- 未运行:Jest。 +- 未运行:Android Gradle/APK/ADB/设备。 + +## 10. 生成产物 + +- 无:本任务不生成 AAR/APK/发布 artifact。 + +## 11. 已知问题与未完成事项 + +- 无已知 Codex 实现阻塞。 +- 无法验证:Android 正式 RasterParticleLayer 视觉效果、单次 `VIEWPORT_RESEED` 日志、GL/EGL、crash/ANR 与 ParticleFastPath stable 恢复,按任务边界交由 RDC。 + +## 12. 与任务书差异 + +- 无生产实现范围差异。 +- 验证方式说明:仓库内定向 gtest 已编译;因本 worktree 是全新 build tree,未完成非必要的 659-object 全 runner 冷链接,改以同一 production helper 的独立原生 C++ executable 实际运行 8/8,满足定向 native test 要求,未宣称全量 runner PASS。 + +## 13. Git 收尾 + +- implementation commit hash:`4286c16a53d4f49001f3b41ed4638d3c6edc95b3`。 +- commit message:`fix: reseed particles after zoom out`。 +- implementation push result:SUCCESS,`297b91262..4286c16a5 HEAD -> particle/main`。 +- remote branch:`origin/particle/main`。 +- documentation closeout push result:在本报告纳入 closeout commit 后执行,结果由最终交付回读记录。 +- Final HEAD / `origin/particle/main`:由最终 push 后的 `git rev-parse HEAD`、`git ls-remote` 回读记录在最终交付;Git commit 无法在其自身内容中自指尚未生成的 closeout hash。 diff --git a/docs/报告/STEP-092_ContinuousRange可退化分段RetainedCache_Codex完成报告_v1.md b/docs/报告/STEP-092_ContinuousRange可退化分段RetainedCache_Codex完成报告_v1.md new file mode 100644 index 000000000..2d017761a --- /dev/null +++ b/docs/报告/STEP-092_ContinuousRange可退化分段RetainedCache_Codex完成报告_v1.md @@ -0,0 +1,157 @@ +# STEP-092 Continuous Range 可退化分段 Retained Cache Codex 完成报告 v1 + +## 1. 执行结论 + +**PARTIAL:实现完成,等待 RDC 独立编译、Native directed test、OpenGL/Metal 验证。** + +未把静态审计写成编译通过,也未把 Android 项写成已执行;因此当前不满足任务的最终 PASS Gate。 + +## 2. Git 基线 + +- 机器:`Mac-mini-tei.lan`,Darwin x86_64。 +- 仓库:`/Users/tei/dev/MapLibreParticleLab`。 +- branch:`particle/main`。 +- Starting HEAD:`b29e564944197072a0effaec36ca95c48fe0649b`。 +- 起始 `origin/particle/main`:`b29e564944197072a0effaec36ca95c48fe0649b`。 +- 起始 dirty:仅用户提供的 untracked STEP-092 任务书;已保留并纳入本任务文档范围。 +- Implementation commit:`a9eaec6b65cc26aa70e03002bdc1326c46cad245`。 +- Implementation message:`feat: add continuous retained render ranges`。 +- Implementation push:SUCCESS,`b29e56494..a9eaec6b6 HEAD -> particle/main`。 +- 文档提交无法在自身内容中自指其最终 SHA;closeout SHA、最终本地/远端 HEAD 与最终 status 由提交后回读写入最终交付响应。 + +## 3. 修改文件 + +### C++ Renderer / Map + +- `include/mbgl/map/map.hpp` +- `include/mbgl/renderer/retained_underlay.hpp` +- `src/mbgl/map/map.cpp` +- `src/mbgl/renderer/continuous_layer_range.hpp`(新增) +- `src/mbgl/renderer/render_orchestrator.cpp/.hpp` +- `src/mbgl/renderer/renderer_impl.cpp/.hpp` +- `src/mbgl/renderer/retained_underlay_cache.cpp/.hpp` +- `src/mbgl/renderer/retained_underlay_runtime_state.hpp` + +### Android API / bridge + +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RetainedUnderlayDiagnostics.java` +- `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp/.hpp` + +### iOS API / bridge + +- `platform/ios/src/MLNMapView.h/.mm` +- `packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.mm` + +### RN API + +- `packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts` +- `packages/maplibre-particle-rn/src/RendererRuntime.ts` +- `packages/maplibre-particle-rn/src/types.ts` +- `packages/maplibre-particle-rn/android/src/main/java/org/navsea/maplibre/particle/weather/WeatherRuntimeBridgeModule.kt` + +### Tests / docs + +- `test/renderer/retained_underlay_cache.test.cpp` +- `docs/40_任务/STEP-092_ContinuousRange可退化分段RetainedCache_Codex任务_v1.md` +- `docs/50_Step记录/STEP-092_ContinuousRange可退化分段RetainedCache_v1.md` +- 本报告 +- `粒子开发记录.md` + +## 4. Range config 所有者与解析 + +- map-scoped owner:`Map::Impl::retainedUnderlayState`。 +- thread safety:atomic control/counters + mutex-protected ID/config/resolution。 +- revision:每次实际 ID pair 变化单调递增;重复相同配置不前进。 +- ID→index:`RenderOrchestrator::createRenderTree()` 在完整 `orderedLayers` 上解析,visibility 不参与。 +- explicit valid 时只使用 `[startIndex, endIndex + 1)`;不读取 legacy boundary。 +- explicit invalid 时 index 置 `-1`、资源/prepared gate 失效并 full fallback;不按 Particle/Weather 类型纠正。 + +## 5. 1/2/3 Render Range 与 0/1/2 cache + +- single-layer Continuous 正式支持。 +- Bottom+Continuous+Top / Bottom+Continuous / Continuous+Top / Continuous-only 全部由同一 plan 表达。 +- 两个 cache holder 分别代表 Bottom/Top capability;只有 range present 才建立 key 并 lazy 创建 RGBA texture/render target/compositor。 +- empty drawable content 不会把 Style 中实际存在的 Bottom/Top topology 偷偷消掉;可构建透明 retained texture。 + +## 6. Renderer、Z-order 与 Top 正确性 + +- Bottom build/reuse/composite → Continuous render → Top build/reuse/composite。 +- Opaque reverse、Translucent forward 保持;original ordinal、`currentLayer`、`depthRangeSize` 按全 Style 计算。 +- Top 使用透明 clear、RGBA offscreen、direct sample shader 与 alpha-blended premultiplied composite,可覆盖 Continuous Particle。 +- 未增加 LandMask RT、业务 Layer ID、Anchor/Marker 或第二套 metadata Z-order。 + +## 7. Invalidation / legacy / Fast Path + +- STEP-089 content/topology/static revision 继续作为 conservative invalidation 基础;range config/end index/Top topology 已纳入。 +- 未配置 explicit range:保持 legacy retained-underlay。 +- explicit valid:legacy `startsRetainedDynamicRange()` 只可用于 fast-safety 类型证明,不参与 topology/boundary。 +- fast gate 对不存在 cache 不要求 valid;实际存在 Bottom/Top 都必须 valid/buildRevision 匹配。 +- unknown Continuous layer 不能证明 particle-only 安全时 full CPU fallback;正常 full range render/cache 不受影响。 + +## 8. Diagnostics + +已增加并跨 C++/Android/iOS/RN 暴露: + +- `rangeConfigured`, `rangeValid` +- `startLayerId`, `endLayerId`, `startIndex`, `endIndex` +- `bottomPresent`, `bottomBuild`, `bottomReuse`, `bottomInvalidation` +- `topPresent`, `topBuild`, `topReuse`, `topInvalidation` +- 保留 aggregate `fallback`, `lastReason` 与原有 enabled/build/reuse/invalidation。 + +Continuous-only 的 Bottom/Top present 均为 false,仍可为 rangeValid=true。 + +## 9. Native tests + +测试源码新增覆盖: + +- `start == end`、`start < end`、`start > end` +- start/end missing +- hidden endpoint ID 仍参与完整 ordered list +- reorder、endpoint 删除后重新解析 +- 四种 topology 与 0/1/2 cache decision +- Bottom/Top 不存在不造成 fast cache miss +- config empty-ID/revision/same-ID +- explicit valid/invalid 均不混用 legacy boundary + +实际 test runner 尚未由 Codex 执行,等待 RDC。 + +## 10. OpenGL / Metal / Android + +- OpenGL:源码实现完成;未执行 build/runtime,等待 RDC。 +- Metal:共用 range/cache topology 与既有 Metal compositor;源码实现完成;未执行 `ios-sdk-static`,等待 RDC。 +- Native directed tests:未执行,等待 RDC。 +- Android 项:**未执行 Android Gradle、AAR/APK、ADB、安装、设备视觉或生命周期验证**。 +- Jest:按任务要求未新增、未执行。 + +## 11. 静态检查与验证命令 + +Codex 已执行 `git diff --check`;最终提交前再次执行并回读。 + +RDC 精确命令: + +```bash +cmake --build build-step088-opengl --target mbgl-test-runner -j3 +build-step088-opengl/mbgl-test-runner \ + --gtest_filter='ContinuousLayerRange.*:RetainedDynamicBoundary.*:RetainedLayerGroupRange.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:ParticleFastPath.*' +cmake --build build-ios --target ios-sdk-static -- -quiet +git diff --check +``` + +## 12. Known limitations / 未完成 + +- 尚无 RDC 编译结果,桥接/codegen/ObjC++/C++ 编译结论不得写 PASS。 +- 尚无 OpenGL/Metal frame capture 或资源计数运行证据。 +- 尚无 Android lifecycle/style mutation/视觉验收。 +- 本任务只支持一个 explicit Continuous Range;无 clear API、多个 Range 或 N Segment Planner。 + +## 13. Git 收尾 + +- scoped staging:只纳入本任务上列源码、测试与文档。 +- 禁止项:未使用 `git add -A`、`git add .`、reset、clean 或 force push。 +- Implementation commit SHA/message:`a9eaec6b65cc26aa70e03002bdc1326c46cad245` / `feat: add continuous retained render ranges`。 +- Implementation push result:SUCCESS。 +- Remote branch:`origin/particle/main`。 +- Final HEAD / `origin/particle/main` / `git status --short`:待提交与 push 后外部回读;在两者一致前不声明 Git 完成。 diff --git a/docs/报告/STEP-092_ContinuousRange可退化分段RetainedCache_Codex完成报告_v2.md b/docs/报告/STEP-092_ContinuousRange可退化分段RetainedCache_Codex完成报告_v2.md new file mode 100644 index 000000000..9afe914d3 --- /dev/null +++ b/docs/报告/STEP-092_ContinuousRange可退化分段RetainedCache_Codex完成报告_v2.md @@ -0,0 +1,165 @@ +# STEP-092 Continuous Range 可退化分段 Retained Cache Codex 完成报告 v2 + +## 1. 执行结论 + +**PASS:Codex 实现 + RDC 独立 Mac Native 验证闭环完成。** + +本报告是在 v1 PARTIAL 基础上的验证收尾版。RDC 已完成 OpenGL test-runner 编译、定向 Native tests、Metal-only iOS 静态 SDK 编译,并修复两处真实验证问题后重新通过。Android Gradle/APK/ADB/实体机与 Metal GPU frame capture 未执行,均明确保留为未执行项。 + +## 2. Git 基线与提交 + +- 仓库:`/Users/tei/dev/MapLibreParticleLab` +- branch:`particle/main` +- Starting HEAD:`b29e564944197072a0effaec36ca95c48fe0649b` +- Codex implementation:`a9eaec6b65cc26aa70e03002bdc1326c46cad245` +- Codex v1 docs:`b89c8c8cb4d941abdcd704b278284e209ba7399b` +- RDC validation fix:`4908244d2`,message:`fix: close STEP-092 native validation issues` +- 文档收尾提交在本报告写入后产生,最终 HEAD / remote HEAD 由收尾回读确认。 + +## 3. Codex 实现范围 + +C++ Renderer: +- explicit Continuous Range config / resolution +- 0/1/2 retained range cache +- Bottom / Continuous / Top render slicing +- Fast Path gate +- diagnostics 与 invalidation + +跨端 API: +- C++ Map API +- Android Java/JNI +- iOS ObjC++ +- RN TypeScript / Android / iOS bridge +核心语义: +- `[startLayerId,endLayerId]` 为闭区间。 +- single-layer Continuous 合法。 +- hidden endpoint 仍决定 topology。 +- Style Layer 顺序是唯一 Z-order。 +- explicit valid 与 legacy boundary 排他。 +- explicit invalid fail closed。 +- Opaque reverse / Translucent forward 与 global ordinal 保持。 + +## 4. RDC 第一次 OpenGL 编译 + +命令: +`cmake --build build-step088-opengl --target mbgl-test-runner -j3` + +结果:FAIL。 + +首个真实错误: +- `renderer_impl.cpp:873-874` +- `RenderItem` 无 `index` 成员。 + +原因: +- `layerRenderItems` 实际类型是 `std::set`; +- 代码把当前元素绑定为 `const RenderItem&` 后又读取派生类 `index`。 + +## 5. RDC 第二次 OpenGL 编译 + +第一次局部修复改为 `LayerRenderItem&` 后继续编译,新的真实错误为: +- `LayerRenderItem::hasRenderPass` private +- `LayerRenderItem::render` private + +最终修复: +- `LayerRenderItem& layerItem` 专门读取 `index`; +- 再绑定 `const RenderItem& item = layerItem` 调用公开虚接口。 + +第三轮 OpenGL build: +- exit code:0 +- target:`mbgl-test-runner` +- 结果:PASS。 +## 6. Native directed tests + +过滤器: +`ContinuousLayerRange.*:RetainedDynamicBoundary.*:RetainedLayerGroupRange.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:ParticleFastPath.*` + +第一轮: +- 29 tests / 6 suites +- PASS 28 +- FAIL 1 + +唯一失败: +`RetainedUnderlayCache.RuntimeControlDisablesAndReenablesMapScopedCache` + +实际值: +- disabled.invalidation = 3 +- expected = 2 + +根因: +- baseline 中 `setEnabled(false)` 已记录一次 aggregate `runtime-disabled` invalidation; +- STEP-092 新增 range cache 同步路径再次调用 aggregate `invalidate()`; +- 因而同一 disable 事件被重复计入 aggregate diagnostics。 + +RDC 修复: +- disable 时不再重复调用 aggregate `recordInvalidation`; +- 保持 legacy aggregate 计数语义; +- 仍为实际 Bottom/Top cache 记录独立 `recordRangeInvalidation`。 + +第二轮: +- 29/29 PASS +- exit code:0 +- 全部 6 suites PASS。 +## 7. Metal / iOS 验证 + +后端真实性: +- `MLN_WITH_METAL=ON` +- `MLN_WITH_OPENGL=OFF` +- Xcode project 定义 `MLN_RENDER_BACKEND_METAL=1` + +命令: +`cmake --build build-ios --target ios-sdk-static -- -quiet` + +结果: +- exit code:0 +- PASS +- 仅有既有 EAGL deprecated、bundle identifier / script phase warning。 + +因此本轮不是“iOS target 名称推断”,而是实际 Metal-only 配置下完成原生编译与链接。 + +## 8. GPU resource lifecycle / composite 核验 + +RDC 审计的实际代码路径: +- RendererImpl 常驻 Bottom/Top cache holder,但 GPU 资源不在构造时创建。 +- `prepareResources()` 只在对应 decision 为 Build/Reuse 时进入。 +- Range 不存在时 key/decision 为 Disabled,不执行 `prepareResources()`。 +- `createOffscreenTexture(..., UnsignedByte, true, true)` 仅在 texture 不存在或 viewport 变化时执行。 +- 同 viewport 复用已有 texture。 +- `compositeLayerGroup` 存在时不重建 compositor。 +- build render target clear color:透明 `0,0,0,0`。 +- composite:`gfx::ColorMode::alphaBlended()`。 +- `alphaBlended()` 使用 `src=One`、`dst=OneMinusSrcAlpha`,符合 premultiplied-alpha。 +- OpenGL 与 Metal Composite Shader 均直接采样 retained RGBA texture。 +- Renderer final 顺序:Bottom composite → Continuous render → Top composite。 +- Native test 已验证四种 topology、0/1/2 cache decision、reuse/invalidation 与 absent cache fast gate。 + +这些结果共同证明当前源码中不存在固定分配两个 retained GPU target,也不存在 stable frame 每帧重建同尺寸 texture/compositor 的路径。 + +## 9. RDC 实际修改 + +1. `src/mbgl/renderer/renderer_impl.cpp` + - 修复 legacy `LayerRenderItem` index 访问与基类虚接口调用。 + +2. `src/mbgl/renderer/retained_underlay_cache.cpp` + - 修复 runtime disable aggregate invalidation 重复计数; + - 保留 Bottom/Top range invalidation diagnostics。 + +RDC 修复提交: +- `4908244d2` +- `fix: close STEP-092 native validation issues` +- push:SUCCESS。 + +## 10. 未执行项目 + +- Android Gradle / AAR / APK:未执行。 +- ADB / 安装 / Android 实体机视觉:未执行。 +- iOS 实机 GPU runtime:未执行。 +- Metal frame capture / GPU resource counter:未执行。 +- Jest:未新增、未执行。 + +当前仓库未提供一个可直接运行的 STEP-092 Metal GPU runtime test target,因此未将 Metal compile PASS 描述成 frame-capture PASS。 + +## 11. 最终验收判断 + +STEP-092 的规定架构、四种 Range、0/1/2 retained cache、global layerIndex、Opaque/Translucent、Fast Path、diagnostics、RN/Android/iOS API、Native directed tests、OpenGL Native build、Metal-only iOS build、资源生命周期路径、文档和 Git 闭环均已完成。 + +结论:**CLOSED / PASS(Mac Native 范围)**。 diff --git a/docs/报告/STEP-093_ContinuousRange运行态综合验收_Codex完成报告_v1.md b/docs/报告/STEP-093_ContinuousRange运行态综合验收_Codex完成报告_v1.md new file mode 100644 index 000000000..23e5a241a --- /dev/null +++ b/docs/报告/STEP-093_ContinuousRange运行态综合验收_Codex完成报告_v1.md @@ -0,0 +1,127 @@ +# STEP-093 Continuous Range 运行态综合验收 Codex 完成报告 v1 + +## 1. 交付结论 + +**IMPLEMENTED / WAITING_RUNTIME_VALIDATION** + +Codex 已完成 STEP-093 所需的 Android 可重复运行态测试入口、最小 diagnostics 可观察性、Fast Path fail-closed 原因、跨平台字段透传与定向测试源码,并完成静态一致性审查。Android/iOS runtime 均未实际执行,所以没有任何运行态场景被标记为 PASS。 + +## 2. Git 信息 + +- Repository:`/Users/tei/dev/MapLibreParticleLab` +- Branch:`particle/main` +- Starting HEAD / starting remote:`8a5d88008c142e2a80e0ff602d228e0e67de2d83` +- Implementation commit:`b3ec8edc87ece404b5f7b3706f5acf2a70de6862`(`feat: add STEP-093 runtime validation tooling`) +- Documentation closeout:本报告所在 scoped commit,在报告写入后生成;准确 hash、push 和最终 remote 回读以最终交付消息为准。 + +## 3. 修改文件 + +Core/Renderer: + +- `include/mbgl/renderer/retained_underlay.hpp` +- `src/mbgl/renderer/particle_fast_path.hpp` +- `src/mbgl/renderer/renderer_impl.cpp/.hpp` +- `src/mbgl/renderer/retained_underlay_cache.cpp/.hpp` +- `src/mbgl/renderer/retained_underlay_runtime_state.hpp` +- `test/renderer/retained_underlay_cache.test.cpp` + +Android: + +- `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RetainedUnderlayDiagnostics.java` +- Test App Manifest、validation Activity 与 layout。 + +iOS / RN: + +- `platform/ios/src/MLNMapView.h/.mm` +- RN TypeScript types/spec、Android/iOS bridge、RN Test Host diagnostics display。 + +文档: + +- STEP-093 任务书、Step 记录、Codex 完成报告; +- `粒子开发记录.md` 追加记录。 + +## 4. 测试入口 + +Android:启动 `ParticleCoreLayerValidationActivity`,点击底部 `STEP 093`。菜单提供 A/B/C/D 四种 topology、E single-layer、mutation/cross-range、endpoint/visibility、invalid range、camera 与 Fast Path fail-closed;屏幕显示完整 pull diagnostics。 + +iOS:正式 diagnostics API 与 RN bridge 字段已补齐,但本轮没有建立/运行 iOS 专用 STEP-093 validation UI。状态为 `WAITING_IOS_RUNTIME_VALIDATION`。 + +完整 RDC 命令、逐项操作、判定标准和验收矩阵见同版本 Step 记录。 + +## 5. Diagnostics + +新增并端到端暴露: + +- Range configured/valid/IDs/index/continuous bounds/fallback/last reason; +- Bottom/Top present/cache state/build/reuse/invalidation/last reason; +- Bottom/Top texture create/composite create/resource release; +- Fast Path eligible/active/fallback reason/hit/miss/full CPU frame; +- unsupported Continuous layer 的 `unsupported-continuous-layer` 原因。 + +所有数据复用 STEP-092 map-scoped runtime state;没有增加 topology metadata 或 Z-order 真相源。counter 只代表事件累计,不代表 GPU memory bytes。 + +## 6. 静态审查与实际测试结果 + +实际执行并通过: + +- Native → Android/iOS → RN diagnostics 字段与 Android 39 项 ABI 索引静态核对; +- 场景 layer 顺序、endpoint 与菜单接线静态审查; +- `xmllint --noout` 检查 Test App Manifest/layout:PASS; +- `git diff --check`:PASS。 + +明确未执行: + +- CMake/Ninja/Xcode/Gradle 编译:NOT RUN; +- Native directed tests:NOT RUN; +- TypeScript/codegen/Android JNI 实际编译:NOT RUN; +- APK build/install、ADB、模拟器/真机:NOT RUN; +- Android runtime/视觉/长时间运行:`WAITING_ANDROID_RUNTIME_VALIDATION`; +- iOS/Metal runtime/视觉/长时间运行:`WAITING_IOS_RUNTIME_VALIDATION`; +- Jest:未新增、未执行。 + +STEP-092 的 OpenGL/Metal build 与 29/29 tests 是起始历史证据,不是本轮新增代码的验证结果。 + +## 7. Android runtime 结果 + +**WAITING_ANDROID_RUNTIME_VALIDATION** + +没有执行 Android Gradle、APK、ADB、安装、启动或画面操作。A–E、Z-order、stable reuse、invalidation、mutation、endpoint、visibility、invalid、Fast Path 与 alpha 全部为 NOT RUN。 + +## 8. iOS/Metal runtime 结果 + +**WAITING_IOS_RUNTIME_VALIDATION** + +没有执行 iOS build/install/simulator/device/Metal runtime。Android 后续即使 PASS,也不能自动推导 iOS PASS;Metal Native build 历史结果也不能替代 runtime rendering。 + +## 9. 架构边界 + +本轮没有: + +- 修改 `[startLayerId,endLayerId]` 闭区间或内部半开区间数学; +- 增加第二套 Bottom/Continuous/Top metadata; +- 增加 anchor/marker、特殊 layer type、多 range 或频率分类; +- 在 Renderer 硬编码业务 Layer ID; +- 重构 RenderGraph/PBF/Particle 算法; +- 把 visibility 变成 topology 规则; +- 为 Fast Path 移动未知 Continuous layer。 + +Style layer 顺序仍是唯一 Z-order 真相;App 只提供 endpoint ID,Native 解析 index。 + +## 10. 风险与已知限制 + +- 尚无任何运行态证据,不能确认真实 GPU 资源、画面合成或平台差异。 +- Android diagnostics 每秒 pull 一次但不逐秒 log;证据需手动 `READ DIAGNOSTICS`、截图或外部采样。 +- counter 在同一 Map 生命周期累计,RDC 应对比增量。 +- iOS 缺专用 STEP-093 validation 页面,需后续补齐实际可操作入口。 +- 本轮新增定向测试源码尚未编译/运行,任何预期 test 数都不能写成 PASS。 + +## 11. 最终状态 + +- 实现:完成; +- 静态一致性审查:完成; +- Android runtime:`WAITING_ANDROID_RUNTIME_VALIDATION`; +- iOS/Metal runtime:`WAITING_IOS_RUNTIME_VALIDATION`; +- 综合:**IMPLEMENTED / WAITING_RUNTIME_VALIDATION**。 + +Git push、`HEAD == origin/particle/main` 与 clean worktree 由本报告之后的 scoped 文档提交和最终回读确认;这些 Git 结果不改变 runtime 尚未执行的事实。 diff --git a/docs/报告/STEP-094_通用RenderSegment分段渲染与Style分类诊断_Codex完成报告_v1.md b/docs/报告/STEP-094_通用RenderSegment分段渲染与Style分类诊断_Codex完成报告_v1.md new file mode 100644 index 000000000..2b4a12b1b --- /dev/null +++ b/docs/报告/STEP-094_通用RenderSegment分段渲染与Style分类诊断_Codex完成报告_v1.md @@ -0,0 +1,126 @@ +# STEP-094 通用 RenderSegment 分段渲染与 Style 分类诊断 Codex 完成报告 v1 + +## 1. 执行结论 + +**PASS / CLOSED(RDC 独立验证完成)** + +任务书范围的 Core、Renderer、Android/iOS/RN bridge、定向测试、diagnostics pull 入口和中文文档均已完成。Codex 负责实现,RDC 按 AGENTS.md 独立完成 OpenGL、Metal、RN codegen/typecheck 与 Android Validation APK 验证。规定的代码与构建 Gate 均通过;Android/iOS 实体机 runtime/视觉未执行,未将其虚报为 PASS。 + +## 2. Git 基线 + +- Branch:`particle/main` +- Starting HEAD:`8fbb892a1236a55329ed9ef5a612640c9363d3f1` +- Starting remote:`origin/particle/main` 同上 +- Implementation commit:`016fd1fff`(完整 SHA `016fd1fff6a33cf795c10d4f5c5413d308383677`,如最终 Git 回读一致则以 Git 为准) +- Commit message:`feat: add generic render segment model` +- Implementation push:PASS,已推送 `origin/particle/main` +- 文档收尾:本报告、Step 记录、设计文档与开发记录在实现 commit 后独立提交;包含本报告的 commit 无法在文件自身内自引用 SHA,最终远端 HEAD 由 RDC push 后回读确认。 + +## 3. 实际实现 + +- `RenderSegmentStrategy/Definition/Plan/Diagnostics` typed Core model。 +- schemaVersion=1 JSON parser/serializer,非法输入使 active plan invalid,不复用 stale topology。 +- boundary Layer ID → ordered Style layerIndex resolver,完整 coverage、严格递增、非空 Segment。 +- N Segment Renderer 遍历与 topology-sized retained cache ownership。 +- 每个 retained cache 独立 revision/build/reuse/invalidation/resource counters;Continuous 无 cache。 +- topology change 释放旧 cache;稳定 viewport lazy reuse。 +- LayerMutation 通过 immutable Impl fingerprint segment-local invalidation;无法可靠归属的 mutation 全 retained invalidation。 +- V12 `setContinuousLayerRange()` compatibility mapping 与旧 diagnostics API 保留。 +- 单 Continuous Particle Fast Path 保留;多 Continuous 明确 fallback。 +- diagnostics 返回 ordered styleLayers、ordered segments、每段 layers、sourceId/sourceLayer(可取得时)和 cache stats。 +- Android/iOS platform API、既有 RN TurboModule、typed TS API 和 RN Test Host pull 入口完成。 + +## 4. 修改文件类别 + +- Core public/private:Map API、RenderSegment model/resolver/JSON、runtime state、orchestrator、renderer、retained cache。 +- Build:CMake 与 Bazel core header/source lists。 +- Android:MapLibreMap/NativeMap/NativeMapView/JNI。 +- iOS:MLNMapView public API 与 ObjC++ bridge。 +- RN:types、Native Spec、RendererRuntime、Android/iOS bridge、Test Host。 +- Tests:`test/renderer/retained_underlay_cache.test.cpp`。 +- Docs:STEP-094 设计、Step 记录、本报告、开发记录。 + +准确逐文件列表以最终 `git status --short` 为准。 + +## 5. Diagnostics 与一致性 + +- valid 时 `segmentCount == segments.length`、`styleLayerCount == styleLayers.length`、Segment 连续覆盖 `[0,layerCount)`。 +- 每段 `layers` 从同一 ordered styleLayers slice 生成;flatten 后顺序与 styleLayers 完全一致。 +- 全局 layerIndex 不按 Segment 重编号。 +- Continuous `cache=null`;Retained cache object 包含 content revision 与生命周期 counters。 +- invalid plan 保留当前 ordered styleLayers,segments 为空且 `valid=false/fallback=true`。 +- snapshot 只在显式 pull 时 JSON stringify;Style 字符串 metadata 仅在 config/style layer metadata 变化时重建。 + +## 6. OpenGL / Metal / Tests + +- OpenGL build:PASS。命令 `cmake --build build-step088-opengl --target mbgl-test-runner -j3`,exit 0;仅既有 Boost/macOS deployment warnings。 +- Native directed tests:PASS。过滤集共 8 suites / 43 tests,43/43 PASS;其中 `RenderSegmentPlan` 9、`RenderSegmentDiagnostics` 4,并包含 V12 Continuous/Retained/Fast Path 回归。 +- Metal build:PASS。预先确认 `MLN_WITH_METAL=ON`、`MLN_WITH_OPENGL=OFF`;`cmake --build build-ios --target ios-sdk-static -- -quiet` exit 0。存在既有 EAGL/statusBar deprecation 与 bundle-id warning,无编译/链接错误。 +- RN package TypeScript:PASS。 +- RN Test Host TypeScript:PASS,验证时临时将 node_modules package 指向本工作树,结束后恢复旧依赖。 +- RN codegen:PASS。工程的 `npx react-native codegen` 因缺少 `@react-native-community/cli` 入口返回 1;未升级/修改依赖,改用同一已安装 RN 0.85 `@react-native/codegen` 的 combine-js-to-schema + generate-all,exit 0,并在 Android Java/JNI/C++ JSI 与 iOS ObjC++ generated Spec 中确认两个新方法。 +- Android Validation APK:PASS。标准脚本 `build_validation_apk.sh`,Gradle `BUILD SUCCESSFUL in 4m 45s`;x86_64-only、strip=true;APK 60,294,298 bytes,`libmaplibre.so` 39,421,736 bytes,SHA-256 `848c3196c64cee6dfb1e9627f4af32b6e235fdd82afd1cfb8474c7fc5d591c71`。 +- Android/iOS runtime/视觉:NOT RUN。 +- Jest:未加入、未执行。 + +## 7. RDC 精确验证命令 + +### OpenGL Native runner + +```bash +cd /Users/tei/dev/MapLibreParticleLab +cmake --build build-step088-opengl --target mbgl-test-runner -j3 +build-step088-opengl/mbgl-test-runner --gtest_filter='RenderSegmentPlan.*:RenderSegmentDiagnostics.*:ContinuousLayerRange.*:RetainedDynamicBoundary.*:RetainedLayerGroupRange.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:ParticleFastPath.*' +``` + +### Metal / iPhoneOS arm64 compile + +```bash +cd /Users/tei/dev/MapLibreParticleLab +cmake --build build-ios --target ios-sdk-static -- -quiet +``` + +RDC 必须先确认 `build-ios/CMakeCache.txt` 的 `MLN_WITH_METAL=ON`、`MLN_WITH_OPENGL=OFF`,不能用 target 名称推断后端。 + +### RN TypeScript / Codegen + +在现有 RN Test Host 已安装依赖环境中执行: + +```bash +cd /Users/tei/dev/MapLibreParticleLab/integration/react-native/MapLibreParticleRNTestHost +npm run typecheck +npx react-native codegen +``` + +若当前工程的 RN 0.85 codegen 正式入口由 Expo prebuild/Pods 驱动,RDC 应使用现行 STEP-083 流程,并核对 generated Android/iOS Spec 确实含 `setRenderSegmentPlanJson`、`getRenderSegmentDiagnosticsJson`,不要只看 TS。 + +### Android compile / Validation APK + +```bash +cd /Users/tei/dev/MapLibreParticleLab +./tools/particle_android_validation/build_validation_apk.sh +``` + +若还需 RN Test Host Android compile,使用当前 STEP-080 v2 公网/本地 package handoff 流程生成包含本工作树 package 的测试依赖后再编译;不得误用远端 `.10` tgz 代替本轮源码。 + +## 8. 需要验证的 targets / tests + +- `mbgl-test-runner` +- `ios-sdk-static`(Metal-only iPhoneOS arm64) +- Android `MapLibreAndroid` JNI/Java 与 `MapLibreAndroidTestApp` OpenGL Debug x86_64 +- RN `WeatherRuntimeBridgeSpec` Android/iOS codegen +- RN Test Host TypeScript +- Native suites:`RenderSegmentPlan`、`RenderSegmentDiagnostics` 及全部 V12 Continuous/Retained/Fast Path 回归 +- 运行态重点:R-C-R、C-R-R、R-R-C、R-C-R-C-R、相邻 R-R 独立 counters、invalid/recovery、hidden boundary、Cache OFF、单 Continuous Fast Path、多 Continuous fallback、Style/segment layer flatten 一致性 + +## 9. Known limitations / 未完成 + +- 多 Continuous Fast Path 未优化,按任务边界明确 fail closed。 +- Source/Tile mutation 仍为保守全 retained invalidation。 +- RN Test Host package.json 仍指向历史远端 `.10` tgz;本次通过临时工作树 package + 直接 RN 0.85 codegen 验证新接口,没有把旧包结果冒充新实现。 +- Android/iOS 实体机 runtime、视觉与性能观察 NOT RUN;本报告的 PASS 仅表示 STEP-094 规定的代码、编译、测试、ABI/codegen Gate 已通过。 +- Android Validation build-info 中 commit 字段为构建时尚未提交的起始 HEAD,且 `git_dirty=true`;这是预提交验证事实,产物 SHA 与验证结果有效,但不是发布 artifact。 + +## 10. 最终状态 + +代码/构建验收:`PASS / CLOSED`。实现 commit `016fd1fff` 已 push;文档收尾 commit 完成后由 RDC 再次确认 `HEAD == origin/particle/main` 与工作树 clean。 diff --git a/docs/报告/STEP-095_V13独立严格代码与性能审查_Codex完成报告_v1.md b/docs/报告/STEP-095_V13独立严格代码与性能审查_Codex完成报告_v1.md new file mode 100644 index 000000000..ff48aeca0 --- /dev/null +++ b/docs/报告/STEP-095_V13独立严格代码与性能审查_Codex完成报告_v1.md @@ -0,0 +1,90 @@ +# STEP-095 V13 独立严格代码与性能审查 — Codex 完成报告 v1 + +日期:2026-09-25(Asia/Tokyo)。本轮为独立静态调查与中文文档交付,不是修复或产品验收。 + +## 1. 执行摘要与状态 + +已完成 V13 冻结源码审查、旧 F01~F10 独立复核、V12/V13 成本比较和 RDC 验证方案。新增最高优先问题为 target 切换后 stencil 记录未失效、Metal depth/stencil 契约未完整下传、绘制抛错后 cache 仍可能标成功、V4 PLAY 未接入生产时钟推进。另有 JSON、数值入库、listener、物化/UI 成本等明确源码链。 + +没有运行构建、测试、设备或 profiler;不宣称已发生黑屏、OOM、崩溃或达到某个性能收益。调查内容完成,Git 状态以[阶段记录第 4 节](../50_Step记录/STEP-095_V13独立严格代码与性能审查_v1.md)追加结果为准。**调查交付 PASS 与 V13 产品运行态 PASS 分离;后者本轮不成立,运行验证均 NOT RUN/NEEDS_RUNTIME。** + +## 2. 基线与审查范围 + +环境为 `Mac-mini-tei.lan`、`/Users/tei/dev/MapLibreParticleLab`、`particle/main`。起始 HEAD/实际 origin 为 `05878d662308b9a92c20c433c3cd2943d74ae718`,工作树 clean。冻结源码 `ee0b89e711bf01335b3aa71d36def785cd97fec3`,功能提交 `016fd1fff6a33cf795c10d4f5c5413d308383677`。RN/Android `13.4.1-particle.13`、iOS `6.28.0-particle.13`;manifest/tag 已核对,manifest sourceCommit `ddf20dec…` 至冻结点无生产源码变化。 + +本次恢复指令由用户在同一会话明确指定 `gpt-6-astra high`,符合任务要求。该记录不冒充底层启动参数的独立核验。保留已有调查正文,仅做模型来源澄清及引用校正。 + +已读取 AGENTS、当前规范/总则/基线,先源码后旧报告,交叉检查 STEP-088~094;范围包括 Segment JSON/resolver、全局排序、runtime/cache、GL/Metal offscreen、State/Trail/时间场/QBIN、Android/iOS/RN、Host 版本锁及 Integration 只读状态。Vulkan、全部驱动/并发交错、业务 provider/HTTP、产物独立重建下载、真实 NavSea 接入未验证。 + +完整 manifest 坐标/散列、源码路径图及范围限制见[调查第 2 节](../20_调查/STEP-095_V13独立严格代码与性能调查_v1.md)。发布回读仅 HISTORY,文档提交不发布新软件。 + +## 3. Finding 总表与详细证据入口 + +以下为交付索引;每项的精确文件/行范围、最短可复核调用链、反例、修复方向和验收方案完整保存在[调查第 3~6 节](../20_调查/STEP-095_V13独立严格代码与性能调查_v1.md),作为本报告证据明细,不另复制一套可能漂移的行号。D=缺陷路径,C=确定成本,R=后果待运行,E=环境偏差;源码链置信度均高。 + +| 编号/级别/类别 | 问题及最短触发/影响 | 最小修复方向 | 验收 | +| --- | --- | --- | --- | +| N01 P1 D+R | 同 tiles 的 R-R/R-C 跨 target 复用 CPU stencil 映射 | target generation 隔离/重置 | OFF基线、stencil capture | +| N02 P1 D+R | Metal clear/attachment 契约缺口 | 完整翻译 depth/stencil/combined attachment | 真机/模拟器 validation/capture | +| N03 P1 D+R | group 抛错被 catch,残图仍 markBuildSucceeded | build 成功状态传递,失败不可发布 | 故障注入/恢复/no reuse | +| N04 P1 D | 一次 set time 后 PLAY 无生产 advance | owner-thread 唯一时钟接线 | PLAY/PAUSE/速度/跨窗口 | +| N05 P2 D | NUL 截断/重复 strategy 绕过严格语义 | 长度感知与唯一键校验 | JSON/两桥接错误回归 | +| N06 P2 D+R | 坏数值先入库,物化失败丢可用投影 | 入库前约束/事务发布 | 好旧帧→坏高revision | +| N07 P2 D | 同 view attach/detach listener递增 | 单listener/幂等/移除 | 100次/换view/销毁 | +| C01 P1 C | full diagnostics 锁内深复制进热路 | 轻量一致快照,完整pull隔离 | alloc/锁/复制次数 | +| C02 P1 C+R | retained及普通粒子Metal wait | 先profile,再有序提交 | wait/GPU/State-Trail顺序 | +| C03 P1 C | dirty销毁同尺寸allocation | content/resource失效分离 | mutation/resize/context分别计数 | +| C04 P1 C+R | GPU多R和CPU多field无总预算 | map预算/安全fallback | 阶梯压力/peak/长稳 | +| C05 P2 C | ordinal线性与fast全items扫描 | generation索引/粒子列表 | S/G扩展和reorder回归 | +| C06 P1 C+R | 同时间/mix也整网格物化、UI提交 | 早退/端点复用/安全异步发布 | 大网格/UI/alloc/rebuild | +| C07 P2 C | tick drawable重建、端点复制 | 保留对象/不可变共享 | tick与窗口边界分测 | +| O01 P2 D | 旧cache析构计数落新topology index | identity/generation计数 | 真cache切换/析构 | +| E01 P1 E | Host `.10`/native `.13` | 对齐依赖和lock,独立后续任务 | 干净包/typecheck/codegen/设备 | +| E02 P3 E | Integration旧协调状态 | 负责人按接入事实更新 | commit/包/设备交叉核对 | + +## 4. 旧 F01~F10 独立复核 + +| 旧项 | 意见 | 修正依据 | +| --- | --- | --- | +| F01 | CONFIRMED | 深复制链成立,成功fast可两次而非所有帧必两次 | +| F02 | CONFIRMED | retained仅build等待,补普通State/Trail等待 | +| F03 | CONFIRMED | dirty释放成立,稳定reuse不分配 | +| F04 | CONFIRMED | 无总预算成立,OOM/泄漏未测 | +| F05 | QUALIFIED | 错配成立,旧TS2307为历史环境失败,不能仅归`.10` | +| F06 | QUALIFIED | 显式设计限制建议P2;full CPU不等于关闭GPU reuse | +| F07 | CONFIRMED | ordinal线性,但全局编号对正确性必要 | +| F08 | QUALIFIED | 保守全局失效,裁窄前须证明依赖完整 | +| F09 | CONFIRMED | 可具体复核先换entries后析构;不证明stale纹理 | +| F10 | QUALIFIED | 文档偏差降P3;不能推定NavSea实际接入 | + +不为凑数标 DISPROVED;没有证据的运行后果均留 NEEDS_RUNTIME。旧报告未作出的断言不反向归咎于旧作者。 + +## 5. V12/V13 与缺失验证 + +V12 固定最多两个 R,V13 任意独立 R/C,增加管理/资源上界;单 C fast 保留,多 C 回 full CPU。成本模型为完整快照 O(L+S)、多段 ordinal 最坏前缀 O(S×G)、retained 资源 O(R×W×H)、场物化 O(P)。普通粒子 wait、drawable/物化多为继承成本,不全归 V13。无 profiler 不给收益百分比,不外推 STEP-090 21.7%/97.0%历史值。 + +| 验证 | 本轮状态 | 不能替代它的历史证据 | +| --- | --- | --- | +| GL/Metal编译、Native/TS/codegen | NOT RUN | STEP-094构建/43 tests、旧审查69 tests/TS仅HISTORY | +| 任意R/C像素、depth/stencil/alpha | NOT RUN/NEEDS_RUNTIME | resolver/diagnostics单测不覆盖GPU | +| CPU/GPU时间、30/60FPS、单C/多C | NOT RUN/NEEDS_RUNTIME | 历史特定Style CPU不可外推 | +| allocation/peak、context loss、长稳 | NOT RUN/NEEDS_RUNTIME | 逻辑release计数不等于物理显存 | +| PLAY/数值/attach/destroy端到端 | NOT RUN | 手动clock advance不等于生产接线 | +| 默认Host版本/ABI与设备调用 | NOT RUN | 临时工作树TS/codegen不等于默认安装 | +| Android GL / iOS Metal / Vulkan运行 | 全部NOT RUN | 编译成功不证明运行支持 | + +完整 topology/异常/生命周期矩阵、精确既有验证命令及日志要求见调查第 9 节。新增缺陷尚需增加对应测试;仅复跑现有过滤集不能关闭。所有运行由 RDC 后续独立执行。 + +## 6. 修复建议、交付边界与 Git + +建议分批:A 正确性 gate N01~N04;B 输入/生命周期/观测/Host N05~N07/O01/E01;C 轻快照/ordinal/相同时间早退;D GPU预算、allocation/content分离和同步;E multi-C fast、source精确依赖、异步大场与正式mask独立设计。不得为性能绕过保守gate、合并相邻R或引入provider/HTTP/业务调度。只建议,不创建修复任务。 + +仅新增本报告、[调查](../20_调查/STEP-095_V13独立严格代码与性能调查_v1.md)、[阶段记录](../50_Step记录/STEP-095_V13独立严格代码与性能审查_v1.md),追加根[粒子开发记录](../../粒子开发记录.md)。没有生产源码、shader、API、package/lock、build、manifest、tag、产物或Integration变更。 + +起始SHA见第2节;实际主提交SHA/message、push、remote HEAD/status和最终收尾提交的自引用说明统一记录在阶段第4节。推送前交付暂记PARTIAL;完成并核对后以追加结果为准。最终由RDC独立审计本文证据和任务边界,Codex不宣布正式产品验收。 + +### 已完成的交付核对 + +主提交 `ad5f92c48ea353c18548c79d223e9a06888330d3`(`docs: audit V13 source and performance for STEP-095`)已push origin particle/main,exit 0;实际远端HEAD回读与本地一致,核对时工作树clean。四份文档、9处相对链接、63处带行号源码引用范围及cached diff空白检查通过,开发记录仅追加。调查主稿仍285行;无生产源码变化。 + +据此调查主交付为 **PASS**,产品运行态仍 **NOT RUN/未验收**。本追加结果由独立docs-only收尾提交保存,其最终SHA及二次push/clean核对按阶段记录自引用规则在最终回复提供;不虚构自身哈希或未执行测试。 diff --git a/docs/报告/STEP-096_V13隔离盲审_Codex完成报告_v1.md b/docs/报告/STEP-096_V13隔离盲审_Codex完成报告_v1.md new file mode 100644 index 000000000..617596dfc --- /dev/null +++ b/docs/报告/STEP-096_V13隔离盲审_Codex完成报告_v1.md @@ -0,0 +1,55 @@ +# STEP-096 V13 隔离盲审 Codex 完成报告 v1 + +执行结果:**PASS(仅盲审文档交付)**。日期:2026-09-25。 + +本轮只使用隔离快照及其当前有效规范和[任务书](../40_任务/STEP-096_V13隔离盲审_Codex任务_v1.md),独立读取源码形成结论,未导入任何既有审查发现。PASS 不表示 V13 Android/iOS GPU 运行态、性能、构建或发布通过。 + +## 交付 + +1. [独立调查报告](../20_调查/STEP-096_V13隔离盲审调查_v1.md):架构、完整发现证据、反例、成本模型、覆盖缺口、后续验证矩阵及分批建议。 +2. [Step 记录](../50_Step记录/STEP-096_V13隔离盲审_v1.md):输入、只读过程、范围、结果和未运行事项。 +3. [本完成报告](STEP-096_V13隔离盲审_Codex完成报告_v1.md):交付结果、索引、证据边界与移交责任。 + +三份正文与相对链接、引用文件及行号、行尾空格已作轻量静态自检;这不属于构建或测试执行。 + +## Finding 索引 + +完整路径、行号、触发条件、反例和验证动作集中在调查报告对应 B 编号中。编号来自本轮源码证实顺序。 + +| 编号 | 优先级 / 类型 | 独立发现 | +|---|---|---| +| B01 | P1 / 确定缺陷 | 跨目标 tile stencil 覆盖记录未失效 | +| B02 | P1 / 确定缺陷 | Metal 未落实 gfx depth/stencil 清屏请求 | +| B03 | P2 / 源码确定成本 | Metal 离屏 present 同步等待 GPU | +| B04 | P2 / 源码确定成本 | retained 内容失效释放同尺寸 GPU 资源 | +| B05 | P2 / 源码确定成本 | 时间更新先全量物化后比较 | +| B06 | P1 / 确定缺陷 | encoded PLAY 无生产时钟推进调用方 | +| B07 | P2 / 确定缺陷 | 8-bit 场纹理零值编码偏差 | +| B08 | P2 / 确定缺陷 | Mercator 东向速度未作纬度修正 | +| B09 | P2 / 设计限制 | 缺正式共享地理 Ocean Mask | +| B10 | P2 / 源码确定成本 | UI 派发链同步解码/物化 | +| B11 | P2 / 设计限制 | store 无全局 key/字节预算 | +| B12 | P2 / 确定缺陷 | retained 绘制异常后仍可提交成功缓存 | +| B13 | P1 / 确定缺陷 | 粒子更新门槛遗漏合法窗口 V/有效性变化 | + +共 13 条,P1 四条、P2 九条,无证据成立的 P0。源码确定缺陷七条、确定成本四条、设计限制两条。颜色投影候选因实际数组比较而排除,粒子强引用环、纯重排遗漏等候选也未成案;没有为凑数量将其写成问题。 + +## 证据质量和输入限制 + +- 每条保留隔离树内真实源码定位及可构造条件,区分源代码事实与运行后果。同步点、分配/遍历、缺失调用方、身份门槛、数值公式可静态分析;设备画面、CPU/GPU 时长、OOM、崩溃和收益全部 **NEEDS_RUNTIME**。 +- 目标 SHA `ee0b89e711bf01335b3aa71d36def785cd97fec3` 为任务书中的导出来源;隔离树不提供 Git 对象核验。本轮未运行 Git,未声称 branch、HEAD、commit、push、remote 或 clean。 +- 快照 manifest 声明 RN/Android `13.4.1-particle.13`、iOS `6.28.0-particle.13` 和产物 sourceCommit `ddf20dec9fe9cd05b77274827b6825829500dd5a`;未外部回读,不能当作安装环境或产物字节核验证据。 +- 实际模型/TUI/screen/effort 启动记录、历史构建日志、主仓库内容和其他审查材料均 **NOT AVAILABLE**;未向目录外寻找。当前规范中旧坐标仅作为设计/流程背景,不覆盖快照实现事实。 +- 这是选择性静态追踪,未穷尽所有源码、后端和竞态。测试源码被阅读,不等于通过测试。Vulkan 全链、ABI/codegen、透明/深度全组合、平台销毁竞态和设备表现仍待后续验证。 + +## 验证与改动边界 + +构建、链接、codegen、打包、C++/GL/Metal/Jest 测试、设备安装/运行、profiler 和 GPU capture:**NOT RUN**。发布、公网回读、正式仓库操作和 Git:**NOT RUN**。 + +本轮只写任务规定的三份中文 Markdown 报告;未改生产源码、测试、shader、manifest、package/lock 或工具链,未追加开发日志。这里的“源码未变”指本轮没有源码写操作,不代表已借助 Git diff 或导出对象对比核验整个快照。 + +## 停止与移交 + +Codex 文档交付后停止。RDC 负责随后核对隔离输出清单、计算三份原稿 SHA-256 并记录冻结时间,再独立移交正式仓库、追加开发记录、做四文件 scoped commit/push 及远端核对。本轮没有执行散列冻结或移交,没有最终提交 SHA。 + +冻结后不由本会话依据外部资料改写发现。后续修复、测试和解盲比较应有独立任务与记录;所需最小验证矩阵已在调查报告给出。 diff --git a/docs/报告/STEP-097_A1_Android专项验收报告_v1.md b/docs/报告/STEP-097_A1_Android专项验收报告_v1.md new file mode 100644 index 000000000..6180b7827 --- /dev/null +++ b/docs/报告/STEP-097_A1_Android专项验收报告_v1.md @@ -0,0 +1,38 @@ +# STEP-097 A1 Android 专项验收报告 v1 + +## 结论及范围 + +**A1 Android/Core 已完成本轮可执行检查点;iOS/Metal 运行测试按用户决定豁免。** 不将豁免写成实际 Metal PASS。STEP-097 总体仍进行中,A2/B/C 尚待实现和验证;无新软件发布。 + +- 仓库:`/Users/tei/dev/MapLibreParticleLab`,`particle/main`,源码提交 `8c006ac83d16c2683037f90484fd86fb917a9d78`。Integration `main` 核对 `318f8ccf911ac3136c3086a1d14e61f83e2d4bce`。 +- Android/FZG1-2:直接 ADB `192.168.200.139:5555`,非 Win11 虚拟机。APK OpenGL Debug/x86_64,60,311,571 bytes,SHA-256 `48df19cf5a4c4250934efa2596423d3ad9571fe49a65b9b704a81cdb41f58671`。标准脚本编译成功、ZIP/ABI 校验通过;`install -r` 返回 Success,设备安装文件哈希一致。 +- 构建记录 `git_dirty=true` 的原因是用户另行放在 `docs/40_任务/` 的未跟踪 STEP-098 MD;本次构建起始及源码提交无 tracked 修改,未将 STEP-098 纳入编译或提交。冻结 `.13` 未修改。 + +## 01:同源同 tile Stencil — Android 实测 + +新入口为 `Step097StencilFixture`,单一 `step097-shared-polygon` GeoJSON tiled source 的四层同一 Polygon,运行中仅改变 R/C 连续 Segment 排列,不改来源、几何及图层视觉参数。 + +- 实测 RR、RCR、CRR 各两轮,稳定帧、REBUILD CURRENT、CACHE OFF/ON 与再次拓扑切换,共 **18 个检查点**,18 张 1920×1200 PNG 与 18 份 UI XML。 +- 所有检查点均 `valid=true`、`fallback=false` 且实际策略序列匹配当前选项;缓存 OFF 时各 R 报告 fallback,ON 后重新构建,拓扑有效不变。代码回归脚本退出码 0;实时日志含 10 条 `STEP097_STENCIL_READY`,没有本次采集范围内的 FATAL/SIGSEGV/SIGABRT。 +- 使用 AppKit 对原始 PNG 采样:18 张中心像素全部为 RGB `[193,176,119]`,区域外像素全部为 `[27,49,75]`,中心与区域外有明显差异。两种采样均一致,不以截图文字代替像素核验。 +- 以上证明这组固定 same-source scene 在指定采样点和可见完整画面未出现异常;它不是对所有 tile/camera/pitch/zoom 组合的数学证明。 + +证据在忽略目录 `artifacts/maplibre-particle/step097-a1-fzg1/`:`run-stencil.log`、`pixel-check.log`、`after/*.png`、`after/*.xml`、`after/live-logcat.txt`、`after/step097-events.txt`、`after/fatal-events.txt`、`after/observations.json`、`install.log`、`installed.sha256`。 + +## 02:Metal 附件 — 源码与编译,不执行 iOS runtime + +Metal 宿主测试二进制中包含 `MetalRetainedAttachments.*` 两个 GTest,Metal host CMake 增量编译退出码 0。当前 Mac 没有可用 `MTL::CreateSystemDefaultDevice()`,两个测试均按守卫 SKIP;没有将 0/2 误记为 PASS。针对 `metal-cpp` 对象指针解引用的最小编译修正已提交 `8c006ac83`。 + +用户于 2026-09-26 明确决定:本轮不执行 iOS 测试,Android 验收通过后 iOS 按其决定视作接受。记录为 `USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`,不宣称 iOS GPU/Metal 实际验证。 + +## 03:绘制异常恢复 — 原生测试 + +`RenderOrchestratorError.*` 两个新增原生测试实际执行 **2/2 PASS,exit 0**,覆盖 opaque/translucent 绘制异常、错误上报、失败 cache 不发布/不复用及干净重试恢复。日志:`artifacts/maplibre-particle/step097-a1-fzg1/render-exception.log`。本次不将 GTest 扩写为 Android GPU 真实异常注入 PASS。 + +## 19:资源释放归属 — 已有结果 + +`OldCacheReleaseCannotChargeReplacementAtSameIndex` 已在此前重新编译的 Mac Core 原生 runner 实测 **1/1 PASS**;诊断组 **5/5 PASS**。FZG1 资源辅助冒烟 20 个检查点已经单独报告。精确 revision 归属由原生 GTest 负责,不据设备摘要断言不存在 GPU 内存泄漏。 + +## A1 交接与限制 + +A1 在 Android/Core 验证和用户 iOS 豁免口径下可进入 A2;保留 B/D `topPresent=true` 的旧诊断疑点,不将其冒充已修复。下一阶段按原任务书 A2→B→C 严格串行;每阶段 Codex 实现/测试入口,RDC 负责实际构建、执行与独立审查;不加入 Jest、不覆盖 `.13`。 diff --git a/docs/报告/STEP-097_A1_FZG1测试APK与实机验证报告_v1.md b/docs/报告/STEP-097_A1_FZG1测试APK与实机验证报告_v1.md new file mode 100644 index 000000000..fd3a288b6 --- /dev/null +++ b/docs/报告/STEP-097_A1_FZG1测试APK与实机验证报告_v1.md @@ -0,0 +1,53 @@ +# STEP-097 A1 FZG1 测试 APK 与实机验证报告 v1 + +**结论:Android FZG1 既有场景冒烟完成;STEP-097 A1 仍为 PARTIAL / BLOCKED。** 本次确实重新构建、安装并通过 ADB 操作 FZG1-2,获得 20 张截图与设备诊断。它不替代 01 stencil 同瓦片双目标注入、02 iOS Metal 附件验证、03 LayerGroup 绘制异常注入及 19 C++ GTest 的正式检查点;A2/B/C 未执行。 + +## 1. 源码、设备与边界 + +- 验证日期:2026-09-25;开发/构建/ADB 控制:Mac-mini-tei.lan;仓库:`/Users/tei/dev/MapLibreParticleLab`,`particle/main`,代码 HEAD `91c2491b0ef73fb9e01d7861b9c4ed7b55498197`,构建开始 `git_dirty=false`。 +- Integration 已通过 RDC 同步 `main` 至 `318f8ccf911ac3136c3086a1d14e61f83e2d4bce`;本轮未改公共协议、Integration 或 NavSea。 +- 设备:Panasonic `FZG1-2`,ADB `192.168.200.139:5555`,Android-x86/Bliss x86_64,1920×1200,ABI 列表以 `device-abi.txt` 为准。ADB 直接从 Mac 连接设备;**没有使用 Win11 虚拟机或模拟器**。 +- 包名:`org.maplibre.android.testapp`;Activity:`.activity.particle.ParticleCoreLayerValidationActivity`;测试时始终使用 `adb -s 192.168.200.139:5555`。 + +## 2. APK 构建和安装的实际结果 + +- 命令:`./tools/particle_android_validation/build_validation_apk.sh`;OpenGL Debug、x86_64、strip native debug symbols;Gradle `BUILD SUCCESSFUL in 15m 59s`,86 tasks(29 executed / 57 up-to-date)。 +- APK:`artifacts/maplibre-particle/step097-fzg1/MapLibreAndroidTestApp-STEP097-A1-91c2491b0-FZG1-x86_64.apk`,**60,295,184 bytes**;内含 `lib/x86_64/libmaplibre.so` 39,422,632 bytes;ZIP/ABI/标准 Pipeline 检查通过。 +- APK SHA-256:`8677197c5f83a22747cb51f0c261edffc5db4678fc25b3f366474f6ecb94dc99`;`build-info.txt` 记录同 commit、ABI、时间和 hash。冻结副本与标准 Pipeline 产物字节一致。 +- 已备份旧安装包至 `artifacts/maplibre-particle/step097-fzg1/before/original-installed-base.apk`。新包使用 `adb -s 192.168.200.139:5555 install -r ` 覆盖安装,输出 `Success`;设备内 `base.apk` 的 SHA-256 与新包完全相同。未清理设备应用数据、未改 NavSea App。 +## 3. FZG1 ADB 操作与截图证据 + +脚本:`artifacts/maplibre-particle/step097-fzg1/run_smoke.py`(本地验证资料,不属于产品源码)。`smoke-run.log` 与 `mutation-run.log` 两次执行 RC=0;另有针对事件的 `live-capture-run.log` RC=0。原始截图、UI XML、日志统一保存在同目录的 `after/`。以下只表述真实场景和屏幕/诊断结果: + +| 截图 | 场景 | 设备观察 | +| --- | --- | --- | +| 00–01 | 普通启动,A:R-C-R | Activity 可启动;A 范围 valid=true、fallback=false,B/T 各一缓存并有 build/reuse;蓝底、粉/灰图形及白线可见。 | +| 02–05 | B:R-C、C:C-R、D:C only、E:single continuous | 场景可切换,截图显示对应图形可见/隐藏;状态均 valid=true、fallback=false。 | +| 06–08 | 恢复 A,CACHE OFF→ON | OFF:`runtime-disabled`、fallback、Fast inactive;ON:`first-frame` 重新构建并恢复 reuse。 | +| 09 | CAMERA NEXT | 相机变化后仍有有效范围与重建/复用,未观察到崩溃或整幅永久清空。 | +| 10–11 | 缺失 START→RESTORE VALID | `valid=false/fallback=true/caches=0`,恢复后 `valid=true/fallback=false`、重建。 | +| 12–15 | TOP REORDER、CROSS CONTINUOUS、FAST FAIL-CLOSED、RESET A | 层序变体可切换;带额外 Continuous 层时 Fast 关闭并保留全路径绘制,重置后恢复 Fast。 | +| 16–19 | 实时抓日志时重置 A、相机、缺失 END、恢复 | 日志有 `STEP093_SCENE_READY` / `STEP093_RANGE`,缺失 END 后设备诊断为 `end-layer-missing`,恢复后有效。 | + +- 设备全局 Logcat 缓冲区较小,单次事后 dump 可能覆盖早期 STEP093 事件,故补充**实时过滤采集** `after/live-particle-logcat.txt`。该文件记录 A 场景 READY、缺失 END 和恢复的实际事件;`after/logcat-final.txt` 保留事后日志。 +- 在本次保存的设备日志与实时采集内未发现 `FATAL EXCEPTION`、`SIGSEGV` 或 `SIGABRT`;最后确认该测试 Activity 仍为 topResumedActivity。这不是长稳/全设备无故障的证明。 +## 4. 异常观察及未关闭的检查点 + +**待复核诊断现象(不直接判定绘制缺陷):** B「BOTTOM+CONTINUOUS」与 D「CONTINUOUS ONLY」截图中没有上段粉色/白线对象,但诊断仍分别报告 `topPresent=true`、`caches=2/1`,且 T reuse 计数继续增长。可能涉及空段、旧 topology 统计或诊断刷新/对象身份;需用独立布局期望与源码追踪证实,不能仅凭截图认定旧上段污染像素。截图:`02_B_R-C.png`、`04_D_C.png`。 + +| 原 A1 问题 | 本次验证覆盖 | 状态 | +| --- | --- | --- | +| 01 N01/B01 stencil CPU/GPU 切换 | 只做 R-C-R 等既有不同 GeoJSON source 的视觉冒烟、相机/缓存切换;**没有同 vector source/same tiles 在 R-R/R-C-R 切换的像素 fixture** | 未闭合 | +| 02 N02/B02 Metal depth/stencil | 本次 APK 是 Android OpenGL;不可能代替 Metal 附件、clear/load/store 和 combined attachment 验证 | 未执行 | +| 03 N03/B12 render 抛异常后错误缓存成功 | 做了配置无效 fallback→恢复及 Fast fail-closed;**没有真正 LayerGroup render 抛异常注入与下帧恢复** | 未闭合 | +| 19 F09/O01 旧 cache release 归属 | 设备保留了 topology/资源计数截图;新增 `OldCacheReleaseCannotChargeReplacementAtSameIndex` GTest **没有执行** | 未闭合 | + +本次不是完整 STEP-097,也不意味着 24 项均修复;A2、B、C、正式 FZG1 同瓦片遮罩回归、iOS Metal、C++ GTest、性能 benchmark、发布与 NavSea 产品 Gate 均未完成。不存在新正式软件发布,原 `.13` tag/manifest/产物未动。 + +## 5. 后续准确步骤 + +1. 补齐 01 同 tile 的 R-R/R-C-R/C-R-R 设备 fixture,02 Metal 附件测试入口,03 render exception 注入及 recovery;解决/解释 B、D 的 topPresent 诊断现象。 +2. RDC 在同 commit 执行 19 定向 GTest,补齐上述源码测试;Android 再集中构建/安装一次而不是逐按钮重装,iOS Metal 由 Mac 单独做对应设备验收。 +3. 满足 A1 代码/测试入口检查点后,才进入 STEP-097 A2/B/C;本次不得修改旧三轮调查或将部分冒烟标成 A1 PASS。 + +证据目录与 APK 目前位于 Mac 仓库忽略的 `artifacts/maplibre-particle/step097-fzg1/`。已安装 APK 和冻存 APK SHA 相同,相关文本记录、截图、日志可复核。 diff --git a/docs/报告/STEP-097_A1_第19项FZG1运行验证报告_v1.md b/docs/报告/STEP-097_A1_第19项FZG1运行验证报告_v1.md new file mode 100644 index 000000000..bc3c60ba6 --- /dev/null +++ b/docs/报告/STEP-097_A1_第19项FZG1运行验证报告_v1.md @@ -0,0 +1,34 @@ +# STEP-097 A1 第 19 项 FZG1 运行验证报告 v1 + +## 1. 结论及验证边界 + +**FZG1-2 Android OpenGL 运行态冒烟 PASS;第 19 项原生 GTest 已于此前单独 1/1 PASS;A1 整体仍 PARTIAL。** 本次通过 Mac RDC 直接连接 Panasonic FZG1-2,不使用 Win11 虚拟机。20 个设备截图检查点、拓扑切换、缓存开关、无效范围回退及恢复完成;设备粗粒度资源计数符合被检查的不变量,未观察到本次采样范围内的应用崩溃。 + +本次 APK 未内嵌 Mac 原生 GTest,也不能从设备截图直接验证旧 topology revision 的内部归属。旧 revision 不计入新段 index 的精确断言由此前 `OldCacheReleaseCannotChargeReplacementAtSameIndex` C++ GTest(1/1 PASS)承担。设备测试仅对实际 Renderer 生命周期、画面与诊断提供辅助证据。 + +## 2. 基线、APK 和设备安装 + +- 时间:2026-09-26 08:10–08:19 JST;Mac `Mac-mini-tei.lan`;源码分支 `particle/main`,起始 HEAD `237a3e66083ede03f56b5e719086d3a1719b4790`,工作树 clean;Integration `main` HEAD `318f8ccf911ac3136c3086a1d14e61f83e2d4bce` 已核对同步。 +- APK 源码提交 `91c2491b0ef73fb9e01d7861b9c4ed7b55498197`。与当前 HEAD 比较只有文档、Step、测试记录变化,生产代码和现有测试源码没有变化,因此直接复用经标准构建的 `OpenGL Debug / x86_64` APK,不为文档提交重复 Gradle 编译。 +- 文件:`artifacts/maplibre-particle/step097-fzg1/MapLibreAndroidTestApp-STEP097-A1-91c2491b0-FZG1-x86_64.apk`,60,295,184 bytes;SHA-256 `8677197c5f83a22747cb51f0c261edffc5db4678fc25b3f366474f6ecb94dc99`。 +- 目标:`FZG1-2`,ADB 序列号 `192.168.200.139:5555`,1920×1200,x86_64;包名 `org.maplibre.android.testapp`。 +- 已执行 `adb -s 192.168.200.139:5555 install -r `,返回 `Success`;设备上 `base.apk` 的 SHA-256 与原始 APK 一致。覆盖安装未清 App 数据。 +## 3. ADB 场景、诊断和截图 + +- 从 `ParticleCoreLayerValidationActivity` 开始,连续执行两轮 A(R-C-R) → B(R-C) → C(C-R) → D(C only) → E(single continuous) → A 恢复;然后 TOP_REORDER、CROSS_CONTINUOUS、恢复默认、CACHE OFF/ON、MISSING START 无效配置和 RESTORE VALID。全程按界面真实控件通过 ADB 点击,不直接伪造内部计数。 +- 输出 20 张 1920×1200 PNG、20 份 UI XML,以及每次 UI STATUS/DIAG 的 `observations.json` 和实时 Logcat。除了启动首页和预期无效场景,另外 **18 个检查点均报告 `valid=true/fallback=false`**。 +- 缺失 START 后 `valid=false/fallback=true`、`reason=start-layer-missing`、`caches=0`;恢复后 `valid=true/fallback=false`、`reason=first-frame`、`caches=2`。 +- 对带 STEP-093 诊断的 19 个检查点,解析 B/T 两组 `res=textureCreate/compositeCreate/resourceRelease`;两组均满足 `textureCreate-resourceRelease ∈ {0,1}`。无效配置 B/T 差额均为 0,恢复后均为 1。**这是设备现有摘要计数检查,不能推论底层 GPU 没有泄漏,也不等同逐 revision 归属断言。** +- 实时日志 `live-logcat.txt` 记录 34 条 STEP093 日志事件,筛选 `step093-events.txt` 共 38 行;`fatal-events.txt` 为空。本次脚本正常结束 `FZG1_DEVICE_SMOKE_OK`,退出码 0;最后确认测试 Activity 仍在前台。 +- 已查看 A、无效 START、恢复有效三张截图:主要图形、粒子与诊断区域仍可见,未观察到整幅永久清空。B/D 不显示上段对象但诊断仍为 `topPresent=true` 的历史待核现象仍存在;不据此直接判定图形错误,另需源码/拓扑语义核查。 + +## 4. 初次脚本重试与真实状态 + +第一次本地 ADB 测试脚本在普通首页截图后,因校验器错误地要求首页已有 STEP-093 专项诊断而触发 `Missing range diagnostic: 00_boot`;该失败属于测试脚本的启动页断言,不是 Renderer 测试失败。保留 `device-test-run-attempt1.log`;限定修正为启动首页允许 `n/a` 后完整重新执行,最终脚本退出码 0。没有跳过后续场景或修改 App。 +## 5. 证据、未完成项与下一步 + +证据根目录(Mac 本地忽略目录):`artifacts/maplibre-particle/step097-fzg1-gtest19-device/`。其中 `install.log`、`before/` 和 `after/installed_sha256.txt` 对应安装;`after/*.png` 与 `after/*.xml` 对应 20 个检查点;`after/observations.json`、`after/status-summary.txt`、`after/live-logcat.txt`、`after/step093-events.txt`、`after/fatal-events.txt` 保留原始诊断。单独压缩包:`STEP097_A1_第19项_FZG1设备证据.zip`,APK 不包含在证据 zip 内,仍使用上述冻结 APK。 + +第 19 项可以记录为**原生 GTest 已通过 + Android 设备辅助冒烟通过**。本次没有验证逐次 GPU 纹理实际释放/峰值内存,没有 iOS Metal 验证,也未补齐 A1 中 01 同源同 tile stencil、02 Metal depth/stencil 附件、03 LayerGroup 真正 render 异常注入专用入口;因此 STEP-097 A1 仍为 PARTIAL,不能进入 A2/B/C 或宣称 V13 发布/产品 Gate PASS。 + +本次只新增报告和追加阶段记录,未修改生产源码、旧调查、正式发布产物、公共契约或 NavSea。 diff --git a/docs/报告/STEP-097_A1_第19项GTest运行验证报告_v1.md b/docs/报告/STEP-097_A1_第19项GTest运行验证报告_v1.md new file mode 100644 index 000000000..44db5ef87 --- /dev/null +++ b/docs/报告/STEP-097_A1_第19项GTest运行验证报告_v1.md @@ -0,0 +1,48 @@ +# STEP-097 A1 第 19 项 GTest 运行验证报告 v1 + +## 1. 执行结论 + +**第 19 项 F09/O01 的现有原生单元测试:PASS。** 专项 `RenderSegmentDiagnostics.OldCacheReleaseCannotChargeReplacementAtSameIndex` 实测 **1/1 通过、退出码 0**;相关 `RenderSegmentDiagnostics.*` **5/5 通过、退出码 0**。这是 Core 诊断计数验证,并非 GPU 内存泄漏测试或 A1 整体验收。 + +- 执行机器:`Mac-mini-tei.lan`,日期 2026-09-26(日本时间);仓库 `/Users/tei/dev/MapLibreParticleLab`;分支 `particle/main`。 +- 实测源码 HEAD:`0d13552bed84f427dd43cae9856391bd26622179`;其中第 19 项生产/测试源码提交 `e67e4961b5b1a1ff8fa7c1e3263799d63f89cadc`;开始时工作树 clean。 +- Integration `main` 已由 RDC 同步核实至 `318f8ccf911ac3136c3086a1d14e61f83e2d4bce`。本次无公共契约或生产源码修改。 + +## 2. 构建和运行 + +在已有、可追踪源码的 `build-step088-opengl` CMake/Ninja 配置中,针对 `mbgl-test-runner` 增量重编译: + +```sh +cmake --build build-step088-opengl --target mbgl-test-runner --parallel 3 +build-step088-opengl/mbgl-test-runner --gtest_filter=RenderSegmentDiagnostics.OldCacheReleaseCannotChargeReplacementAtSameIndex --gtest_color=no +build-step088-opengl/mbgl-test-runner '--gtest_filter=RenderSegmentDiagnostics.*' --gtest_color=no +``` + +- 构建:**exit 0**,95/95 Ninja steps,实际耗时 **261.74 秒**;测试程序确实重新链接且新增测试列在 `--gtest_list_tests` 输出中,未复用旧二进制代替新测试。 +- 编译有 Boost header 未使用形参与 vendor 静态库 macOS deployment target 链接警告;无构建失败,本次未更改工具链/警告规则。 +- 产物:`build-step088-opengl/mbgl-test-runner`;SHA-256 `9fbbc08c44d8445114040ef3a127f5efdad29d0e6d0d1437617421c8a392de52`。 +## 3. 测试和断言证据 + +专项用例源码:`test/renderer/retained_underlay_cache.test.cpp`,`RenderSegmentDiagnostics.OldCacheReleaseCannotChargeReplacementAtSameIndex`。 + +测试先配置旧 Segment 拓扑并取得 `oldRevision`,再将 Continuous 边界由 `weather` 改为 `land`,要求 `newRevision != oldRevision`。对相同 index=0 分别发送两种资源释放: + +1. `recordSegmentResourceRelease(0, oldRevision)`:新 topology 第 0 段的 `resourceRelease` 必须仍为 **0**。 +2. `recordSegmentResourceRelease(0, newRevision)`:同一当前段的 `resourceRelease` 必须变为 **1**。 + +本轮真实输出: + +```text +[ RUN ] RenderSegmentDiagnostics.OldCacheReleaseCannotChargeReplacementAtSameIndex +[ OK ] RenderSegmentDiagnostics.OldCacheReleaseCannotChargeReplacementAtSameIndex (0 ms) +[==========] 1 test from 1 test suite ran. +[ PASSED ] 1 test. +``` + +相邻诊断组的 5 个用例亦全部通过:可见性元数据、旧缓存释放、缓存关闭保持拓扑、非法计划的 Style 顺序、快照序列化。全部无失败和跳过。 + +## 4. 事实边界、证据与后续 + +- 该测试仅验证**当前 topology 的按段释放计数不被旧 revision 污染**;不证明底层 GPU 纹理的实际释放、总分配平衡、没有显存泄漏或 FZG1/iOS 运行态表现。Aggregate release 仍由源码语义保证,本用例未单独断言 aggregate 数值。 +- 第 19 项的**已有 GTest 执行检查点可标为 PASS**;STEP-097 A1 **仍 PARTIAL**:01 同 vector source/same tiles stencil、02 Metal 附件、03 真正 LayerGroup 绘制异常注入均没有被此测试覆盖。 +- 保存日志:忽略目录 `artifacts/maplibre-particle/step097-gtest19/build.log`、`gtest19.log`、`diagnostics-suite.log`、`build-metadata.txt`。本轮没有执行 Jest、APK 构建/重装、性能或新版本发布。 diff --git a/docs/报告/STEP-097_A2_阶段验证记录_v1.md b/docs/报告/STEP-097_A2_阶段验证记录_v1.md new file mode 100644 index 000000000..c0ad41cf4 --- /dev/null +++ b/docs/报告/STEP-097_A2_阶段验证记录_v1.md @@ -0,0 +1,38 @@ +# STEP-097 A2 阶段验证记录 v1 + +状态:**PARTIAL / 代码待完成与审查**。本记录是 2026-09-26 Mac RDC 当前检查点,不是 A2 交付完成报告,不允许据此进入 B/C。 + +## 基线与执行事实 + +- 仓库:`/Users/tei/dev/MapLibreParticleLab`;分支 `particle/main`;起点 HEAD `72d93693842d0f98b1f26b4ec38d5b7389656184`。Integration `main` 同步 HEAD `318f8ccf911ac3136c3086a1d14e61f83e2d4bce`。 +- A1 的 Android/FZG1 检查点已在 `docs/报告/STEP-097_A1_Android专项验收报告_v1.md` 记载;iOS runtime 按用户决定豁免,不能把未运行写为 PASS。 +- GPT-6-Sol medium 可见 Codex 会话 `6215.7445.codex` 在 A2 修改 17 个已跟踪文件,并新增内部 GPU 数学头和定向测试;触及当期用量上限后停下,**源码和测试仍未形成 A2 完整提交**。未启动 B/C;不切换低能力模型冒充完成。 +- 用户另有未跟踪 `docs/40_任务/STEP-098_QBIN_V4单帧首显与八帧滚动缓存_Codex任务_v1.md`,原样保留;与本轮 A2 无关。 + +## 已进行的独立检查 + +| 检查 | 真实结果 | +| --- | --- | +| `git diff --check` | exit 0 | +| OpenGL `cmake --build build-step088-opengl --target mbgl-test-runner --parallel 3` | exit 0,28 构建步骤,含新增测试目标 | +| OpenGL 定向 `ParticleGpuField.*:Source.RasterArrayV4*:ParticleFieldRuntime.ForecastClock*` | **16/16 PASS,exit 0** | +| `node shaders/generate_shader_code.mjs` 连续两次 | 两次 exit 0、输出一致,未产生额外工作树路径 | +| Metal host `cmake --build build-step097-metal-host --target mbgl-test-runner --parallel 3` | exit 0,224 步,耗时约 519.65 秒;没有运行 iOS 设备 | +| Metal host 同样的跨 Core 定向测试 | **16/16 PASS,exit 0** | + +上述编译器提示 Boost vendor 未使用参数、macOS deployment target 链接警告,实际构建 exit 0。测试仅证明已有入口的行为,不等于整项 A2 运行态 PASS。 + +## 改动内容尚需复核 + +- 04 单一 map/source owner 基于 monotonic tick 推进 ForecastClock,Android/iOS Bridge 只作为唤醒调度; +- 05 GPU 双端点身份包含 U/V、validity、空间网格、解释/版本信息;只变 mix 不应触发新上传; +- 06 内部 RGBA8 signed zero 从 byte 128 精确重建,零/量化误差有数学测试;未改变公开 QBIN 格式; +- 07 Mercator 东向 U 与北向 V 采用相同纬度度量及极区夹紧,GL/Metal 代码准备中。 + +这些是**在工作树内可见的实现方向,不是已批准的最终行为**。待 Codex 恢复后完成剩余审查、针对性修正和阶段文档,再由 RDC 独立编译/测试/设备验证,明确代码完成后才能进入 B。 + +## 原始证据及后续 + +忽略目录:`artifacts/maplibre-particle/step097-a2-rdc/`。包含未提交差异备份、新增测试文件备份、`build-opengl.log`、`a2-focused-tests.log`、`shader-codegen1.log`、`shader-codegen2.log`、`build-metal-host.log`、`a2-metalhost-core-tests.log`。本次没有 Android APK/ADB 验收、iOS runtime、Jest 或发布。 + +当前 Codex 模型用量预计 2026-09-26 13:54(日本时间)恢复;已安排 13:55 从现有会话与工作树核实后继续执行。继续时不得重复启动原有进程、不得先进入 B/C、不得覆盖未跟踪 STEP-098 或旧 `.13` 发布资产。 diff --git a/docs/报告/STEP-097_A2_阶段验证记录_v2.md b/docs/报告/STEP-097_A2_阶段验证记录_v2.md new file mode 100644 index 000000000..0c181fe38 --- /dev/null +++ b/docs/报告/STEP-097_A2_阶段验证记录_v2.md @@ -0,0 +1,44 @@ +# STEP-097 A2 阶段验证记录 v2 + +**状态:PARTIAL / 独立扩展回归发现既有断言与既有函数语义冲突。** 本报告接续 v1,不覆盖其原始验证事实;A2 生产代码仍为未提交的工作树改动,不可据此进入 B/C。 + +## 1. 本次实际动作 + +- 2026-09-26(JST),在 Mac `/Users/tei/dev/MapLibreParticleLab`,`particle/main` 当前远端与本地同为 `bd1cfe95b7595a95314b9083754823ae170c5c8e`;Integration `main` 已核对 `318f8ccf911ac3136c3086a1d14e61f83e2d4bce`。 +- 现有 A2 工作树修改仍保留,未修改生产文件、未提交 A2 源码;用户的未跟踪 STEP-098 任务 MD 原样保留。本次只扩大原生回归并核对失败的旧源码/测试。 +- v1 已记录:OpenGL 和 Metal host 原生测试程序各自编译 exit 0;A2 聚焦 16/16 PASS,两次 Shader 生成一致;Metal 仅主机源码/原生测试,不是 iOS 实机测试。 + +## 2. 扩大回归 + +使用 OpenGL `build-step088-opengl/mbgl-test-runner`,运行: + +```sh +--gtest_filter='ParticleFieldRuntime.*:ParticleFieldProfileValidator.*:ParticleGpuField.*:ParticleFieldBinding.*:Source.RasterArray*:Source.HostField*:Source.ParticleField*:Source.CanonicalField*' +``` + +结果 **58 项中 57 PASS、1 FAIL,退出码 1**。失败项 `Source.RasterArrayGpuContentIdentityExcludesRuntimeGeneration` 位于 `test/style/source.test.cpp:288`,预期 `sameFieldGpuContent(east, changed)` 为 false,实测 true。 + +核查本次未修改的 HEAD 定义:`sameFieldGpuContent()` 明确只比较兼容性、解码 scalar/vector/valid,不比较 `generation`、`dataVersion` 等生产者身份;旧测试却在仅增加 `dataVersion` 时要求 false。两者存在**原有测试期望与既有函数契约不一致**。本轮未另建旧 HEAD 二进制作基线重演,因此只称静态可证的原有矛盾,不声称已做旧版运行复现。 + +A2 新增 `sameFieldGpuEndpoint()` 负责严格 GPU 端点身份,明确比较 generation/dataVersion/validTime、U/V、validity、grid、encoding 等;对应新 `ParticleGpuField.*` 四项已通过。旧内容等价与新端点等价不应被混为一谈。 + +排除上述旧测试后,同一组其余 **57/57 PASS,退出码 0**;这只是隔离验证,不是宣布全部回归 PASS。 + +## 3. 后续修复门槛 + +- Codex 恢复 GPT-6-Sol medium 后,先复核旧内容比较与新端点比较各自用途;修正旧测试断言,使其与实际函数语义一致,或发现更深的契约问题时写阻塞报告。禁止仅为让旧断言通过而更改新端点身份约束或放宽安全机制。 +- 修改后重新编译原生 runner,原 58 项完整回归必须退出 0,并复跑 A2 聚焦用例。随后核对 Android 端 A2 APK 与当前最终代码一致。 +- **Android Gradle 编译由用户手工执行。** 标准 Mac 仓库命令为 `cd /Users/tei/dev/MapLibreParticleLab && ./tools/particle_android_validation/build_validation_apk.sh`。在 A2 代码完成前执行只能算中途 smoke,不是最终 Android 验收。用户提供编译结果后,再通过 RDC 对 FZG1-2 `192.168.200.139:5555` 安装、Logcat、截图验证。 +- iOS runtime 按用户明确决定豁免,记 `USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`;不虚报 iOS 实测。 +- 未进入 B/C,未运行 STEP-098,未改冻结的 `.13` 正式发布。 + +## 4. 原始证据 + +Mac 忽略目录 `artifacts/maplibre-particle/step097-a2-rdc/`: + +- `a2-broader-core.log`:58 项、1 FAIL 原始记录; +- `a2-broader-excluding-legacy-test.log`:其余 57/57 PASS; +- `a2-broader-run-metadata.txt` / `a2-broader-exclude-metadata.txt`:过滤器; +- v1 中已有 `build-opengl.log`、`build-metal-host.log`、`a2-focused-tests.log`、`a2-metalhost-core-tests.log`、Shader 生成日志。 + +本报告不覆盖测试失败,也不将尚未提交的代码描述为正式完成。 diff --git a/docs/报告/STEP-097_A2_阶段验证记录_v3.md b/docs/报告/STEP-097_A2_阶段验证记录_v3.md new file mode 100644 index 000000000..205a07fb2 --- /dev/null +++ b/docs/报告/STEP-097_A2_阶段验证记录_v3.md @@ -0,0 +1,96 @@ +# STEP-097 A2 阶段验证记录 v3 + +## 1. 结论 + +**A2 Android/Core 验证完成;STEP-097 整体仍 PARTIAL。** A2 的 04–07 在 Core、OpenGL/Metal host 回归和 FZG1-2 设备闭环达到本阶段检查点。没有开始 B/C,没有执行 Jest、STEP-098 或 `.13` 发布。iOS runtime 依用户决定豁免,记为 `USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`,不是 iOS PASS。 + +Codex 会话配置:**GPT-6 Luna High**,用户可见会话。RDC 独立执行了 CMake/GTest、shader generator、标准 Android APK 构建、FZG1-2 安装和设备场景。Codex 本轮未执行这些命令。 + +## 2. Git 与版本基线 + +- 仓库:Mac `Mac-mini-tei.lan`,`/Users/tei/dev/MapLibreParticleLab`;分支 `particle/main`。 +- 验证源码基线 HEAD:`5bb1a0e6353c5dfe8db8a61bbeba9cef447280ef`;A2 源码工作树 dirty,尚未提交。结束 HEAD 未变化。 +- 本次记录前的 A2 source diff SHA-256:`18927a5f30a88f5c1c3a00e0017473dc33b04151c227e4baf52250dc4ad2e6a2`;与 `artifacts/maplibre-particle/step097-a2-final-validation/mix-reuse-build-source.diff` SHA-256 完全一致。标准 APK 的 build-info 也记录相同 commit/branch 和 `git_dirty=true`,证明设备验证所用 source snapshot 与本轮代码一致。 +- Integration `main`:`318f8ccf911ac3136c3086a1d14e61f83e2d4bce`;C-001 未修改。 +- 冻结的 `.13` 发布坐标、tag、manifest 与产物未修改;本轮不发布。 +- 用户 untracked `docs/40_任务/STEP-098_QBIN_V4单帧首显与八帧滚动缓存_Codex任务_v1.md` 保持原样、未 stage。 + +## 3. A2 实施内容 + +| 项 | 实施与不变量 | +| --- | --- | +| 04 N04/B06 Forecast PLAY | Map Continuous update 驱动 Source-owned monotonic forecast clock;Android/iOS timer 只负责唤醒。同步 SourceObserver reentry 由 `forecastTickInProgress` 防止重复推进。显式 setTime、PLAY/PAUSE、rate、clear 保持 Core clock 所有权。 | +| 05 B13 GPU endpoint / mix-only reuse | `sameFieldGpuContent()` 继续只表示 version-agnostic decoded payload 等价;新的 `sameFieldGpuEndpoint()` 严格比较 revision/time、U/V、validity、grid、encoding 等 renderer endpoint 身份。Source 对相同 content identity、binding、immutable decoded frame 与 plane/validity storage pointer 复用已有 materialized T0/T1,只更新 `sampleState`。frame/revision 或任一输入 storage 改变则重新物化;invalid/clear fail-closed。移除每 tick 深比较完整 snapshot vectors。 | +| 06 B07 RGBA8 signed zero | 增加内部 `particle_gpu_math.hpp` signed RGBA8 编码,byte 128 明确表示 0;shader 以 high precision 还原 byte。未改公开 QBIN/FieldProfile encoding。数学测试覆盖正负取消、零、NaN/invalid 与量化下限。 | +| 07 B08 Mercator U/V | helper 与 GL/Metal shader 的东向 U、北向 V 都使用相同纬度度量,极区分母夹紧;保持 canonical m/s 和现有视觉时间倍率。测试覆盖 0°/45°/60° 四方向及极区有限值。 | + +三项 A2 Source 测试还覆盖 50 次 mix-only clock tick 不重物化、真实 endpoint 和相同 valid-time revision 替换触发重物化、clear 后 projection 清空,以及 source notification 嵌套 tick 不双推进。Host-facing 新 API、C-001、QBIN 格式未改变。 + +## 4. 修改文件 + +### 源码、shader 与 A2 测试 + +- `include/mbgl/particle/field/field_data_adapter.hpp` +- `include/mbgl/particle/field/particle_gpu_math.hpp`(新增) +- `include/mbgl/shaders/gl/particle_gpu_validation.hpp` +- `include/mbgl/shaders/mtl/particle_gpu_validation.hpp` +- `include/mbgl/style/sources/raster_array_source.hpp` +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step097V4GoldenFixture.kt`(新增) +- `platform/android/MapLibreAndroidTestApp/src/main/assets/integration/step097-v4/wind/T0.qbin`、`T1.qbin`、`contract.json`(新增 test-only fixture) +- `platform/ios/src/MLNParticleFieldRuntime.mm` +- `shaders/particle_gpu_validation.fragment.glsl`、`shaders/particle_gpu_validation.vertex.glsl` +- `src/mbgl/map/map_impl.cpp` +- `src/mbgl/particle/field/field_data_adapter.cpp` +- `src/mbgl/particle/field/field_projection_materialization.cpp` +- `src/mbgl/particle/field/field_runtime.cpp` +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `src/mbgl/style/sources/raster_array_source.cpp`、`src/mbgl/style/sources/raster_array_source_impl.hpp` +- `test/CMakeLists.txt` +- `test/particle/field_runtime.test.cpp` +- `test/particle/particle_gpu_math.test.cpp`(新增) +- `test/source/raster_array_v4_host_api.test.cpp` +- `test/style/source.test.cpp`(仅修正旧断言对 `sameFieldGpuContent()` 的预期;严格 endpoint identity 由新断言覆盖) + +本报告之后的文档变更为 `docs/30_设计/STEP-097_V13三轮问题去重与修复矩阵_v1.md`、本报告、`docs/报告/STEP-097_V13三轮审查问题合并修复_Codex完成报告_v2.md`、`docs/50_Step记录/STEP-097_V13三轮审查问题合并修复_v1.md`、`docs/60_测试/STEP-097_V13修复后集中验证清单_v1.md` 及根目录 `粒子开发记录.md`。无其他步骤范围代码。 + +## 5. RDC 构建与测试 + +| 验证 | RDC 结果 | +| --- | --- | +| 原有完整 58 项回归(修正旧断言后) | OpenGL **58/58 PASS**。先前 57/58 的失败根因是旧断言把 `sameFieldGpuContent()` 当成 revision-sensitive;函数原有契约明确忽略 generation/dataVersion。测试现在分别验证 payload 等价与 strict endpoint identity,没有放宽 endpoint 比较。 | +| 当前 broad Core filter `Particle*.*:Source.RasterArray*:Source.HostField*:Source.ParticleField*:Source.CanonicalField*` | OpenGL **121/121 PASS**;Metal host **121/121 PASS**。 | +| A2 定向 Source 三项 | OpenGL **3/3 PASS**;Metal host **3/3 PASS**,包括 mix-only 50 ticks、endpoint/revision invalidation 与同步通知重入。 | +| shader generator | `node shaders/generate_shader_code.mjs` 连续两次成功,输出幂等。 | + +构建/测试日志:`artifacts/maplibre-particle/step097-a2-final-validation/mix-reuse-broad-opengl.log`、`mix-reuse-broad-metal-host.log`、`mix-reuse-focused-opengl.log`、`mix-reuse-focused-metal-host.log`、`build-mix-reuse-opengl.log`、`build-mix-reuse-metal-host.log`。Metal host 测试是 Mac Core runner,不是 iOS runtime 或设备 Metal 验证。 + +## 6. Android APK 与 FZG1-2 + +- 标准命令由 RDC 执行:`./tools/particle_android_validation/build_validation_apk.sh`。 +- 产物:`artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk`;OpenGL Debug、x86_64、61,753,803 bytes;SHA-256 `27bf4a6cc191bd86c4d1c10398478d81c2999edd61fe7242e40e5188516b0ca5`。步骤归档副本:`artifacts/maplibre-particle/step097-a2-final-validation/MapLibreAndroidTestApp-A2-mix-reuse-x86_64.apk`。 +- RDC 安装至 Panasonic FZG1-2(ADB `192.168.200.139:5555`)成功;设备内 APK SHA-256 与构建产物相同。 +- Test-App-only Golden Wind T0/T1 场景脚本 exit 0,17 PNG、24 steps、ANR=0:load、seek 25/75、speed 2x/1x、PLAY 30 秒与 PLAY 时 UI/TIME 响应、PAUSE/stable pause、replay、CLEAR、reload、再次 PLAY/PAUSE。截图 `after-v4-mix-reuse/05_play_15s.png` 可见粒子。 +- 上传/reset 计数按资源 epoch 记录:clear/reload 前 mix-only playback 稳定于 `fieldUploadCount=6/resetCount=2`;CLEAR/reload 后计数合法增加到 `fieldUploadCount=10/resetCount=4`,reload 后 playback 稳定。没有宣称跨 CLEAR/reload 零上传/零 reset。 +- APK 输入源码 diff 与保存快照 `mix-reuse-build-source.diff` 的 SHA-256 都为 `18927a5f30a88f5c1c3a00e0017473dc33b04151c227e4baf52250dc4ad2e6a2`。 + +## 7. 历史失败、性能与平台边界 + +- 2026-09-26 12:43 首次 FZG1 ANR 的 main stack 是 Forecast clock tick 的同步 SourceObserver reentry;`forecastTickInProgress` guard 与回归测试针对该缺陷。后续有一轮没有 ANR 但 UI/TIME 仍不可用、主线程高负载;debuggerd 定位到每 tick 对大 QBIN projection 重复物化 validity/vector 并深比较 snapshot。mix-only immutable snapshot reuse 后,最终 FZG1 闭环通过。 +- 历史 debuggerd、首次 ANR、第二次无响应试跑及最终验证证据均保存在 `artifacts/maplibre-particle/step097-a2-final-validation/`;报告保留失败经过,不把中间失败覆盖成 PASS。 +- PLAY 30 秒只是交互闭环,不是 long-run 稳定性、FPS、frame time、内存或 GPU 性能基准;本轮没有这类数字。 +- 用户豁免 iOS runtime:`USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`。Android 与 Metal host Core 结果不能替代 iOS device PASS。 +- Integration C-001 未改,Host 公开 API/QBIN schema 未改;没有进入 B/C、没有运行 STEP-098/Jest、没有更改 `.13` 发布物。 + +## 8. 阶段与 Git 收尾 + +- A2 状态:Android/Core 代码、原生回归和 FZG1 验证完成。 +- STEP-097 整体:**PARTIAL**;B/C 未开始。其余 24 项逐项状态见更新后的去重矩阵。 +- 本报告无 commit/push。RDC 后续做独立 QA 与 scoped Git 操作;用户未跟踪 STEP-098 不得纳入。 + +## 9. RDC Git 源码提交核验(报告编写后更新) + +- A2 源码、标准 TestApp 内部 fixture 与定向测试已以 **`f49ffa68a17e0a505199bdbeccf07f8543f3a05f`** 独立 scoped commit 提交并推送 `origin/particle/main`;26 个代码/fixture/test 路径,未包含任何文档、STEP-098、Integration、NavSea 或发布资产。远端 HEAD 回读相同。 +- 该提交只是把已经通过 RDC 验证的源码固定进 Git;构建时仍如实为 `5bb1a0e` + dirty source diff。源码部分保存快照的 SHA-256 对应提交内容,不把后续文档更新误算进构建输入。 +- 文档与 Step/矩阵/测试清单将作为独立 scoped commit 收尾。 diff --git a/docs/报告/STEP-097_B_独立验收与版本阻塞报告_v1.md b/docs/报告/STEP-097_B_独立验收与版本阻塞报告_v1.md new file mode 100644 index 000000000..26edcbf3d --- /dev/null +++ b/docs/报告/STEP-097_B_独立验收与版本阻塞报告_v1.md @@ -0,0 +1,36 @@ +# STEP-097 B 阶段独立验收与版本阻塞报告 v1 + +状态:**PARTIAL / BLOCKED_NATIVE_AAR_COMPATIBILITY**。B 不判 PASS,C 不启动。本报告按实际验证结果更新,不以 Codex 终态代替验收。 + +## 基线与执行 +- 日期:2026-09-26;Mac 仓库 `particle/main` HEAD `b1239ac3b1554943200e6fbf14321459ccc86527`;实施工作树 dirty,尚未 commit/push。 +- Integration `main` 已由 RDC fetch 核验,本地及远端均为 `318f8ccf911ac3136c3086a1d14e61f83e2d4bce`;Contract C-001 未改。 +- Codex 两次均使用 GPT-6 Luna High,`codex exec --json`;分别收到 `turn.completed`、退出码 0;完整 JSONL/status.json 位于 `artifacts/maplibre-particle/step097-b10-codex/` 和 `step097-b-fixture-codex/`。 +- 工作范围:08 JSON、09 数值 ingress、10 Android owner/map destroy、20 Test Host 版本配对;用户未跟踪 STEP-098 及正式 .13 发布资产未触碰。 + +## 代码和独立证据 +- 08:按真实字节长度解析,拒绝 NUL、重复键、非法 strategy/schema,失败清旧计划;已存在定向 GTest。 +- 09:有效 cell 数值及 scale/offset 有限性验证在提交 Store 前执行;合法 validity=0 保留,坏高 revision 不覆盖旧场。 +- 10:新 View attachment owner、generation guard 与 map destroy observer;附 Android owner/JUnit 和 MapLibreMap 真正 onDestroy 测试。 +- 20:Test Host package.json 与 lock 均精确为 RN `13.4.1-particle.13`;正式 tarball SHA-256 `3a726db06cb967fe56de77ca4a72feed98cd642a2d1b73fd51b1531d15ef7539` 与 release manifest 一致,lock integrity 的 SHA-512 与实包一致。 +- Mac Core 增量构建 PASS;B 四项定向 GTest **4/4 PASS**;原 broad 134 项中 2 项旧 UInt8→Int16/UInt16 4-byte fixture 不合法,修正为合法 8-byte 小端四样本后重新构建,broad **134/134 PASS**。未放宽生产数值校验或既有断言。 +- 原始日志:`artifacts/maplibre-particle/step097-b-rdc/core-*.log`。 + +## 阻塞门槛 +- B10 新增 `MapLibreMap.addOnDestroyListener()` 为 Android SDK 新方法;正式 .13 release source commit `ddf20dec9fe9cd05b77274827b6825829500dd5a` **没有此方法**。 +- Test Host 默认及 RN 模块 Gradle 仍消费已发布原生 AAR `13.4.1-particle.13`。所以新 RN Kotlin bridge 直接调用该方法时,现有默认正式消费组合不具备此符号;不能用本工作树 Native 源码 JUnit 冒充 .13 RN consumer 编译通过。 +- 需要选择不增加已发布 ABI 依赖的完整生命周期方案,或者另走正式新 Native/RN 同版发行与配对验证。未经上游决定不得改写 .13 tag、manifest/AAR,也不得以反射或私有绕道掩盖版本问题。 + +## 2026-09-26 RDC 补充独立实测(Mac,覆盖上方旧测试数字) +- B10 Codex 的持久化 `status.json`:`gpt-6-luna/high`,75 条 JSONL 事件,`turn.completed`,exit 0;旧 fixture 修正任务同样 `turn.completed` / exit 0。两次交付本身不代表验收。 +- 本轮 Mac 原生测试 runner 增量编译 exit 0;六项 B 定向 GTest **6/6 PASS**,完整扩展 filter **139/139 PASS**;旧 fixture 仅修正 16-bit 四样本合法长度,未放宽生产校验或原断言。证据:`artifacts/maplibre-particle/step097-b-rdc/validation-{build-core,focused-gtest,broad-gtest}.log` 与 `validation-status.json`。 +- Mac 使用现有 OpenJDK 17 和 Android SDK:`:MapLibreAndroid:testOpenglDebugUnitTest --tests org.maplibre.android.maps.MapLibreMapDestroyListenerTest` **2/2 PASS**;`:MapLibreAndroid:compileOpenglDebugJavaWithJavac` exit 0。JUnit XML 位于 `platform/android/MapLibreAndroid/build/test-results/testOpenglDebugUnitTest/`;证据在 `android-validation-status.json`、`validation-native-{destroy-junit,compile}.log`。 +- Owner Kotlin 单测使用独立临时 JVM 项目,源码及测试副本 SHA-256 与本工作树对应文件分别一致:生产 `36b437a0bc1573bafcf1a99ee37d4c438b72ea9ad5a9847fb0021f8093a3296a`,测试 `4c27cc3e82c0570b78b27aec46d565359eff11e18c2ff292e4188c158e8a4407`;JUnit **4/4 PASS**,0 fail/skip。第一次 offline 插件 marker 缺失,第二次正常解析并通过;详见 `owner-jvm-status.json` 和 `validation-owner-jvm-retry.log`。这是 owner helper 的 JVM 回归,不冒充 RN Host 集成。 +- `git diff --check` exit 0,正式 .13 release commit `ddf20dec9` 的 `MapLibreMap.java` 无 `addOnDestroyListener()`,而当前 RN Bridge 调用该方法且 Gradle 精确锁定旧 .13;因此 **默认 RN consumer 编译仍未通过/不可宣称 PASS**,此阻塞不因上述单元测试通过而解除。 +- Linux 独立工作树的 Gradle 仍有原进程,状态未确认完成,不能以 Mac 测试代替 Linux 结果;须等待并单独记录最终退出状态。 +- 当前结论维持 `PARTIAL / BLOCKED_NATIVE_AAR_COMPATIBILITY`。STEP-097 整体未关闭;C、STEP-098、正式发布均 NOT RUN。未授权改写 .13 tag/manifest/产物,不把当前不兼容代码推入正式消费基线。 + +## Git 阻塞检查点(2026-09-26,后续事实) +- 为避免将当前无法和正式 .13 AAR 配对编译的新 RN bridge 提交进 `particle/main`,RDC 以 `b1239ac3b1554943200e6fbf14321459ccc86527` 创建独立 worktree,逐文件 SHA-256 比对 21 个 B 源码/测试/文档,并 scoped commit/push 到 `step097-b-blocked-20260926`。 +- 首个 checkpoint commit:`e463ba04ce956a54eed02b77f981eeeedd9a5534`;独立远端回读相符。主线 `origin/particle/main=b1239ac3b1554943200e6fbf14321459ccc86527` 不变。原 Mac dirty B 工作树及用户 STEP-098 不变;发布资产无差异。 +- Git 保存成果不等于 B 验收通过;B 仍 `PARTIAL / BLOCKED_NATIVE_AAR_COMPATIBILITY`,待 Integration / 发行流程明确新 Native/RN 同版配对或实现不依赖新 ABI 的可靠替代方案。不得启动 C、STEP-098 或覆盖 .13。 diff --git a/docs/报告/STEP-097_V13三轮审查问题合并修复_Codex完成报告_v1.md b/docs/报告/STEP-097_V13三轮审查问题合并修复_Codex完成报告_v1.md new file mode 100644 index 000000000..9b97415d2 --- /dev/null +++ b/docs/报告/STEP-097_V13三轮审查问题合并修复_Codex完成报告_v1.md @@ -0,0 +1,57 @@ +# STEP-097 V13 三轮审查问题合并修复 — Codex 完成报告 v1 + +## 1. 执行结论 + +**PARTIAL,停在 A1。** 阶段 0 已静态复核并建 24 项矩阵;A1 的 01/02/03/19 已做部分源码修改。01/02/03 缺少任务要求的专用可执行像素、Metal 附件及异常注入测试入口,不能标 `CODE_IMPLEMENTED_STATIC_CHECKED`。19 有 GTest 源码且静态核对完成,测试 **NOT RUN**。按照任务书“前一阶段没有明确代码完成和静态检查结果,不推进下一阶段”,A2/B/C 未开始。此次提交不是 V13 运行态验收,也不是软件发布。 + +## 2. Git、机器与版本基线 + +- 机器:`Mac-mini-tei.lan`;目录:`/Users/tei/dev/MapLibreParticleLab`;分支:`particle/main`。 +- 起始 HEAD 与远端查询值:`720d309364628f85049cf9969d71c71e8dfacce9`;起始工作树 clean,子模块未见偏离。 +- A1 部分代码检查点:`e67e4961b5b1a1ff8fa7c1e3263799d63f89cadc`。本报告成稿时的代码 HEAD 为该提交;本报告及最终文档另作 scoped 交付提交,最终 HEAD 以交付后的 Git 记录为准。 +- `.13` tag 解引用:`ee0b89e711bf01335b3aa71d36def785cd97fec3`;manifest 记录 RN/Android `13.4.1-particle.13`、iOS `6.28.0-particle.13`。没有更改 tag、manifest 或已发布产物。 +- Integration 本地只读 HEAD `39ad8415fbdb76c1822460bb09f3a3f35fe141e0`,C-001/RFC-001 为既有合同;没有由 RDC 独立确认其远端同步。 + +## 3. 实际修改与新增文件 + +| 范围 | 文件与内容 | +| --- | --- | +| A1 代码 | `src/mbgl/renderer/paint_parameters.{hpp,cpp}`、`render_target.cpp`、`renderer_impl.cpp`、`render_orchestrator.hpp`、`retained_underlay_cache.{hpp,cpp}`、`retained_underlay_runtime_state.hpp`;`src/mbgl/mtl/offscreen_texture.cpp`、`render_pass.cpp`。 | +| A1 测试源码 | `test/renderer/retained_underlay_cache.test.cpp` 增加旧 topology release 不计入新同 index 的 GTest。 | +| 新增文档 | `docs/30_设计/STEP-097_V13三轮问题去重与修复矩阵_v1.md`、`docs/50_Step记录/STEP-097_V13三轮审查问题合并修复_v1.md`、`docs/60_测试/STEP-097_V13修复后集中验证清单_v1.md`、本报告。 | +| 仅追加 | 根目录 `粒子开发记录.md`。 | + +没有修改 Integration、NavSea、项目总则、旧调查、release manifest、公开 Field/QBIN 协议或已发布坐标。 + +## 4. 各问题状态 + +具体旧编号、源码定位、反例、测试入口和平台待测项见[24 项矩阵](../30_设计/STEP-097_V13三轮问题去重与修复矩阵_v1.md)。编号 01/02/03:源码已局部修改,仍为 `WAITING_RDC_BUILD_AND_RUNTIME`,且专用测试源码缺口阻止 A1 完成。19:`CODE_IMPLEMENTED_STATIC_CHECKED`,GTest **NOT RUN**。04/05/07/08/09/10/11/13/16/17/20:`WAITING_RDC_BUILD_AND_RUNTIME` 在矩阵中表示待实施,不表示已有代码。06/12/14/15/18/21/22/23/24:`DEFERRED_DESIGN`;其中 23/24 是 Integration 协调项。无项目被判产品级 PASS,无项因三轮重复编号被删除。 + +## 5. 已实施事实、差异与停止原因 + +1. 01:新 render target 绑定后重置 `nextStencilID` 与 tile clipping map;同一 target 内未清 stencil 时仍可复用。接入 retained、普通 offscreen 与主目标三处目标切换。 +2. 02:Metal 离屏 `isRenderable()` 检查 color/depth/stencil,simulator/x86 combined depth-stencil 同时绑定两个附件;RenderPass 转换 color/depth/stencil clear/load/store,创建失败显式抛出供 retained fallback。实际 Metal validation **NOT RUN**。 +3. 03:范围 visitor 可回报绘制异常;retained build 收到错误后标失败并转 full fallback,不发布成功缓存。真实 layer 故障注入 **NOT RUN**。 +4. 19:旧 cache 的 release 仅在同 topology revision 更新当前 segment 计数,aggregate release 仍记录。新增同 index 换边界的定向 GTest 源码。 + +与任务书差异:未完成 A1 关键测试入口,故没有进入 A2/B/C,未形成各阶段成功提交。停止依据是任务书第 2、4、7 节的不可跨越检查点,而非将未运行的构建当作失败。当前代码检查点仍需 RDC 独立审查,特别核对 Metal API/资源所有权和异常后的 GL/Metal fallback。 + +## 6. 静态检查、构建与测试记录 + +- 已执行:`git diff --check` 与 `git diff --cached --check`,均无输出、exit 0;`rg -n '[[:blank:]]+$'` 核对新文档,无匹配;`rg` 核对 `beginRenderTarget` 三处调用和 `recordSegmentResourceRelease` 的新签名及测试调用。已只读核对 metal-cpp 的 `StoreActionStore`、`setClearStencil` 声明。 +- CMake/Ninja/Xcode/Gradle、codegen、链接、APK/AAR/XCFramework、npm install/typecheck、GTest、Jest、GPU capture、设备与 NavSea:**NOT RUN**。未有本轮 build log、测试数、耗时或性能数。 +- 生成产物:无。大小/SHA-256/Renderer/Build Type/ABI:不适用。软件发布:**NOT RUN**。 +- 后续精确命令、fixtures 缺口、预期与责任见[集中验证清单](../60_测试/STEP-097_V13修复后集中验证清单_v1.md)。 + +## 7. 已知问题、未完成事项与下一步 + +- 先为 01 增加同 vector source/same tiles 的 R-R/R-C-R/C-R-R 可执行像素 fixture;为 02 增加 Metal descriptor/combined attachment 与失败注入入口;为 03 增加 layer render 异常注入及下一帧恢复入口。之后静态审查 A1,再由 RDC 在授权下集中编译和运行。 +- A2 的 PLAY/endpoint/零量化/Mercator,B 的 JSON/坏帧/Android listener/Host 版本,C 的 diagnostics/ordinal/物化/资源优化均未实施。不得将本报告用于关闭这些问题。 +- Integration C-001/RFC-001 未更新;23 的 Compatibility/Status 及 24 的共享地理 Ocean Mask 需粒子负责人协调。此次未触发需要改变公共合同的实现,因此没有伪造“公共接口阻塞报告”或声称决定已被接受。 +- 建议继续 STEP-097 A1 的测试入口与独立审查,在完成当前检查点后再按原任务书进入 A2;不自行创建下一 Step。 + +## 8. 2026-09-26 A1 测试入口续作(补充,历史结论保留) + +从 `be5b9080ed926a2632c5126c3522e242a2a85967` 的 clean `particle/main` 继续,只处理 A1 01/02/03 测试入口。新增 `Step097StencilFixture.kt` 与 Validation Activity 菜单,提供一个 GeoJSON source 上的 R-R/R-C-R/C-R-R;新增 `render_orchestrator_error.test.cpp` 正反范围异常/observer/cache 重试 GTest;新增 `mtl_retained_attachments.test.cpp` 离屏附件和 clear/load/store GTest。`render_pass.{hpp,cpp}` 将生产 clear 逻辑抽为内部辅助函数供测试共用;`test/CMakeLists.txt` 登记两个测试源。更新矩阵、Step、集中验证清单和开发记录。 + +代码 scoped commit:`d0ced7235`。此续作的 `git diff --check` 静态检查无输出;构建、GTest、Jest、APK、Metal/iOS、设备截图及软件发布均 **NOT RUN**。RDC 已于本续作之前执行第 19 项 GTest 1/1 PASS 和 FZG1 设备辅助冒烟;不能把那些旧结果归到本次新测试源码。01/02/03 仍需 RDC 编译与 runtime 验证,特别是 Metal 失败注入和 GPU 层异常 fallback,A1 仍 PARTIAL;A2/B/C 未开始。文档 scoped commit 与远端状态以最终 Git 记录为准。 diff --git a/docs/报告/STEP-097_V13三轮审查问题合并修复_Codex完成报告_v2.md b/docs/报告/STEP-097_V13三轮审查问题合并修复_Codex完成报告_v2.md new file mode 100644 index 000000000..8404cf3d5 --- /dev/null +++ b/docs/报告/STEP-097_V13三轮审查问题合并修复_Codex完成报告_v2.md @@ -0,0 +1,96 @@ +# STEP-097 V13 三轮审查问题合并修复 — Codex 完成报告 v2 + +## 1. 执行结论 + +**PARTIAL:A1 与 A2 的 Android/Core 检查完成;B/C 未开始。** A2 04–07 已由 Codex GPT-6 Luna High 在用户可见会话实现,由 RDC 独立构建、测试和执行 FZG1-2 运行闭环。iOS runtime 依据用户决定豁免,状态为 `USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`,不是 iOS PASS。没有运行 Jest、STEP-098 或发布 `.13`。 + +## 2. Git、环境与发布基线 + +- 机器:`Mac-mini-tei.lan`;仓库:`/Users/tei/dev/MapLibreParticleLab`;branch:`particle/main`。 +- 源码验证基线 HEAD:`5bb1a0e6353c5dfe8db8a61bbeba9cef447280ef`;验证时 worktree dirty,未提交。报告编写只增加/更新本报告所列 A2 文档。 +- A2 source diff SHA-256 `18927a5f30a88f5c1c3a00e0017473dc33b04151c227e4baf52250dc4ad2e6a2`,与 APK build source snapshot 完全相同。 +- Integration `main`=`318f8ccf911ac3136c3086a1d14e61f83e2d4bce`;C-001 未修改。 +- 冻结 `.13` 坐标/tag/manifest/发布产物未改。没有提交或 push。 +- 用户 untracked STEP-098 任务文件保持未跟踪、未修改、未 stage。 + +## 3. 修改文件 + +源码、shader、测试及 Test-App-only V4 Golden fixture 的完整路径列于 [A2 阶段验证记录 v3](STEP-097_A2_阶段验证记录_v3.md) 第 4 节。核心范围包括 `RasterArraySource` 与 `Map::Impl` Forecast clock、严格 GPU endpoint identity 和 mix-only snapshot reuse、signed RGBA8 helper、GL/Metal shader 与 Mercator 数学、Android/iOS timer wake bridge、A2 Core 回归和 test-only Wind T0/T1 fixture。 + +本轮交付文档: + +- `docs/报告/STEP-097_A2_阶段验证记录_v3.md` +- 本文件 `docs/报告/STEP-097_V13三轮审查问题合并修复_Codex完成报告_v2.md` +- 更新 `docs/30_设计/STEP-097_V13三轮问题去重与修复矩阵_v1.md`、`docs/50_Step记录/STEP-097_V13三轮审查问题合并修复_v1.md`、`docs/60_测试/STEP-097_V13修复后集中验证清单_v1.md` +- 仅追加根目录 `粒子开发记录.md` + +未修改 Integration、NavSea、项目总则、C-001、QBIN schema、`.13` 发布物、用户 STEP-098 或 Host public API。 + +## 4. 24 项状态 + +| # | 状态与实际结论 | +| --- | --- | +| 01 N01/B01 | A1 Android/Core 检查完成;同源 stencil fixture FZG1 检查已在 A1 报告记录。A1 iOS runtime 按用户决定豁免。 | +| 02 N02/B02 | A1 Android/Core 阶段收口;Metal host 不是设备验证。无 iOS runtime,按用户豁免记录。 | +| 03 N03/B12 | A1 Android/Core 阶段收口;异常恢复 GTest 已在 A1 报告记录。 | +| 04 N04/B06 | A2 完成;唯一 Source forecast clock、wake-only timer 与同步回调重入保护;OpenGL/Metal host 定向测试和 FZG1 PLAY/PAUSE 实测通过。 | +| 05 B13 | A2 完成;`sameFieldGpuContent` 仍为 payload 等价,strict endpoint identity 独立比较;mix-only snapshot reuse 与 revision invalidation 通过 Core/设备验证。 | +| 06 B07 | A2 完成;内部 signed zero encoding/shader 路径和数学边界测试通过 OpenGL 与 Metal host broad 回归。 | +| 07 B08 | A2 完成;U/V 纬度 metric 和极区夹紧,数学回归与 Android 场景通过;不等于 iOS runtime。 | +| 08 N05 | `WAITING_RDC_BUILD_AND_RUNTIME`;B 未开始。 | +| 09 N06 | `WAITING_RDC_BUILD_AND_RUNTIME`;B 未开始。 | +| 10 N07 | `WAITING_RDC_BUILD_AND_RUNTIME`;B 未开始。 | +| 11 F01/C01 | `WAITING_RDC_BUILD_AND_RUNTIME`;C 未开始。 | +| 12 F02/C02/B03 | `DEFERRED_DESIGN`;同步等待依赖未在本阶段处理。 | +| 13 F03/C03/B04 | `WAITING_RDC_BUILD_AND_RUNTIME`;C 未开始。 | +| 14 F04/C04 | `DEFERRED_DESIGN`;无资源预算改动。 | +| 15 C04/B11 | `DEFERRED_DESIGN`;无 store 总容量语义改动。 | +| 16 F07/C05 | `WAITING_RDC_BUILD_AND_RUNTIME`;C 未开始。 | +| 17 C06/B05/B10 | mix-only immutable materialization 子项随 A2 完成;其他 diagnostics、ordinal、UI owner 工作仍待后续,整体不得解读为 C 完成。 | +| 18 C07 | `DEFERRED_DESIGN`;drawable/tweaker 生命周期未改。 | +| 19 F09/O01 | A1 已有 GTest 与 FZG1 证据保留;不把资源诊断夸大为 GPU 无泄漏证明。 | +| 20 F05/E01 | `WAITING_RDC_BUILD_AND_RUNTIME`;B/Test Host package lock 未改、未验证。 | +| 21 F06 | `DEFERRED_DESIGN`;保留 multi-C 安全 fallback。 | +| 22 F08 | `DEFERRED_DESIGN`;保留保守失效策略。 | +| 23 F10/E02 | `DEFERRED_DESIGN`;需要跨项目协调,Integration status 未改。 | +| 24 STEP-095 §7/B09 | `DEFERRED_DESIGN`;共享地理 Ocean Mask 需要后续跨项目契约。 | + +矩阵保留全部 24 项、旧编号及平台边界。 + +## 5. 实现摘要与偏差 + +1. Forecast clock 每次由 Map Continuous update 调用一次;同步 SourceObserver 重入不再触发第二次 tick。Android/iOS timers 负责 triggerRepaint 唤醒,不独立推进时间。 +2. Strict endpoint identity 涵盖 frame/revision、grid、U/V、validity 与 encoding。mix-only 时复用当前 immutable materialized snapshots;新 frame/revision 或 storage identity 改变时重新 materialize;clear/invalid 仍 fail-closed。没有使用 generation/dataVersion 偷换 `sameFieldGpuContent()` 的已有语义。 +3. RGBA8 byte 128 表示 signed zero,GL/Metal shader 使用高精度 byte reconstruction。东/北向 Mercator 位移应用一致纬度 metric。 +4. 新增 A2 定向 source 测试、GPU math 测试及 test-only Golden Wind 两帧 fixture;没有新建 Host API、QBIN 格式或 C-001 语义。 +5. 首次 FZG1 ANR 是同步 clock notification 重入;guard 修复后仍有一轮因每 tick 重物化大场、深比较而 UI 不响应。最终 mix reuse 版本完成设备闭环。原始失败日志与 debuggerd 均保留。 + +## 6. RDC 构建、测试、设备及产物 + +- 原 58 项回归:旧断言修复后 **58/58 PASS**。失败断言与旧函数定义不符,修复测试语义;没有放宽新 strict endpoint 条件。 +- OpenGL broad Core filter:**121/121 PASS**。 +- Metal host 相同 broad Core filter:**121/121 PASS**。 +- 三项 A2 Source 定向测试:OpenGL 和 Metal host 各 **3/3 PASS**。 +- Shader generator 连续两次成功、输出幂等。 +- 标准 APK `./tools/particle_android_validation/build_validation_apk.sh`:OpenGL Debug、x86_64、61,753,803 bytes;SHA-256 `27bf4a6cc191bd86c4d1c10398478d81c2999edd61fe7242e40e5188516b0ca5`。FZG1-2 安装成功、设备 hash 相同。 +- Golden 闭环:17 PNG、24 steps、exit 0、ANR=0;PLAY 30 秒并确认 TIME/UI 操作响应,随后 PAUSE/stable pause/replay/CLEAR/reload/replay/PAUSE。15 秒截图有可见粒子。 +- 日志资源 epoch:首次 stable mix-only `upload=6/reset=2` 保持不变;CLEAR/reload 后合法到 `upload=10/reset=4`,随后 playback 稳定。没有宣称跨 CLEAR/reload 零上传或 reset。 +- Android 产物、测试日志、设备日志、截图、步骤清单、debuggerd 与历史失败证据在 `artifacts/maplibre-particle/step097-a2-final-validation/`。 +- 本轮无 FPS、p50/p95/p99、frame time、内存或 long-run 性能结论。iOS runtime 未运行,用户豁免。 + +## 7. 未完成与限制 + +- STEP-097 总体仍 `PARTIAL`:B/C 未进入。04–07 完成不代表 08–24 全完成;第 17 项只有 mix-only 子项完成。 +- iOS runtime:`USER_ACCEPTED_WITHOUT_IOS_RUNTIME_TEST`。Metal host GTest 只证明共享 Core host 路径,不能替代 iOS Metal device PASS。 +- FZG1 Golden 是 Test-App-only fixture,不是 NavSea、正式产品数据链或全量天气验收。 +- 30 秒 PLAY 交互不构成长时稳定性或正式 GPU 性能验收。 +- 无提交/推送;用户 STEP-098 未触碰;`.13` 未发布。 + +## 8. 后续交接 + +RDC 对当前 dirty A2 代码与本报告做独立 QA,按用户后续授权执行 scoped Git 操作。若后续继续 STEP-097,只能由用户明确允许进入下一个阶段后启动 B;本报告不自动开启 B/C。 + +## 9. RDC 源码 Git 后续事实(本报告编写后) + +- 源码 scoped commit **`f49ffa68a17e0a505199bdbeccf07f8543f3a05f`** 已推送 `origin/particle/main`,远端核对一致。此事实覆盖前文“未提交/未推送”的报告编写时状态,保留原始验证 HEAD 以便追溯 dirty APK 的实际构建来源。 +- 文档另行提交;不将源代码 Git commit 当作额外编译/设备测试。用户 STEP-098 仍不提交,B/C 不启动。 diff --git a/docs/报告/STEP-098_QBIN_V4单帧与八帧滚动缓存_Codex完成报告_v1.md b/docs/报告/STEP-098_QBIN_V4单帧与八帧滚动缓存_Codex完成报告_v1.md new file mode 100644 index 000000000..381e9802c --- /dev/null +++ b/docs/报告/STEP-098_QBIN_V4单帧与八帧滚动缓存_Codex完成报告_v1.md @@ -0,0 +1,129 @@ +# STEP-098 QBIN V4 单帧与八帧滚动缓存 Codex 完成报告 v1 + +日期:2026-09-26(JST) +结论:**IMPLEMENTED / WAITING_RDC_VALIDATION_AND_PLATFORM_RUNTIME**。Codex 源码阶段已交付;未构建、未运行 T01–T17,不能标记功能 PASS。 + +## 1. 执行身份与契约 + +- 执行器指定实际模型:**GPT-6 Luna High**。 +- 仓库:Mac 原始正式源码目录 `/Users/tei/dev/MapLibreParticleLab`;branch `particle/main`。 +- 起始/结束 HEAD:`b1239ac3b1554943200e6fbf14321459ccc86527`(本轮无 commit)。 +- 起始时 `origin/particle/main` 同为 `b1239ac3b1554943200e6fbf14321459ccc86527`。 +- Integration `main`/`origin/main`:`1dd576d3304e267e53ed64d31f9711d2350ce10a`,工作树干净。 +- 本轮依据 RFC-004、ADR-001、C-002、COORD-005。新契约状态为已接受的设计决定,非代码/设备/V15 发布验收。旧 RFC-003/COORD-004 B 已按用户指示作废删除;此事实解除 `STEP-098_公共契约阻塞报告_v1.md` 的历史阻塞,本轮未修改旧报告或原任务 v1。 +- 本机 Android Native/RN 版本文件仍为 `13.4.1-particle.13`,iOS VERSION 为 `6.28.0`。未更改版本锁、`.13` release/tag/manifest/AAR、独立 checkpoint worktree 或远端。 +- `git submodule status` 与记录 SHA 一致。开工时工作树已有 STEP-097 B tracked/untracked 改动及 STEP-098 原任务等,共 29 条 status 项;均保留。没有执行 reset、clean、stage、commit 或 push。 + +## 2. 实施结果 + +### Core、Ingress、Resolver + +- Store 按每个 `fieldKey` 最多 8 个帧升序存储。精确 validTime 返回真实单帧 T0=T1、mix=0;只有非精确请求且存在合法相邻 pair 时才做插值,未覆盖时间 fail-closed。 +- 分离可共存 schema 与插值身份:相同 grid/plane schema 的新 reference/profile/fieldID 批次可进入窗口并按精确时刻单帧首显;双帧插值必须匹配 `fieldID`、QBIN `referenceTimeMs`、resolved `profileID/version` 和 grid/plane schema。Ingress 验证后保留 profile 身份,避免同 schema 不同 profile 被混插。 +- 满额新时间插入使用临时候选有序集合:以新帧 validTime 为中心比较旧最早/最晚端点距离,远端先在候选集删除;等距按 C-002 删除原最早端。候选邻接 schema 验证成功后才 `swap`;失败不改 Store。满额正常有效插入仍返回既有 `AcceptedInserted`,`RejectedCapacity` 名称保留兼容。 +- Duplicate、同时间 revision/stale/identity-conflict 仍走既有分支,不触发其他时间淘汰;submit 不触碰 forecast clock。每 key 可独立达到 8 帧。 +- 增加原始 decoded 字节计数:每帧为有效性 bitset + 非 validity plane payload,Store/Series 汇总;不计 profile 元数据、Float32 物化快照、图像、GPU texture、particle/trail。 + +### Projection、Renderer、Bridge + +- Binding 和 materialization 支持精确单帧,T0/T1 共享同一个物化 Snapshot。标量-only 帧仍不能成为向量 Particle projection。 +- Particle 与 WeatherColor 首次安装同一 endpoint 时复用 image/texture/scale;当前 bracket 不变时 Source projection 继续沿用 existing equality/materialization reuse;被淘汰的活动端点重新 resolve 后失效清除。 +- 实现位于 Shared Core/common Renderer,GL/Metal 共用语义;没有声明 Android 或 iOS runtime 通过。 +- `FieldRuntime.ts`、`NativeWeatherRuntimeBridge.ts`、Android Kotlin/JNI 与 iOS Objective-C++/Facade 静态核对:旧 `submitEncodedField(...)`、`setEncodedForecastTime(...)` 名称、签名、返回名称和调用顺序未修改;bridge 没有本轮改动。 +- **ABI 说明**:RN/平台 bridge 方法 ABI 未变;`DecodedFieldFrame` 新增内部 profile 字段,C++ 数据结构布局发生变化,需同源码重新构建 Native 消费端。未使用 `.13` AAR 冒充该 ABI 验证。 + +## 3. 本轮修改文件 + +### STEP-098 源码与测试 + +- `include/mbgl/particle/field/field_runtime.hpp` +- `src/mbgl/particle/field/field_runtime.cpp` +- `src/mbgl/particle/field/field_projection_materialization.cpp` +- `src/mbgl/particle/ingress/qbin_v2_decoder.cpp` +- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp` +- `src/mbgl/renderer/layers/render_weather_color_layer.cpp` +- `test/particle/field_binding.test.cpp` +- `test/particle/field_runtime.test.cpp` +- `test/particle/qbin_v2_decoder.test.cpp` +- `test/source/raster_array_v4_host_api.test.cpp` + +### 新增/追加文档 + +- 新增 `docs/30_设计/STEP-098_QBIN_V4单帧与八帧滚动缓存设计_v1.md` +- 新增 `docs/50_Step记录/STEP-098_QBIN_V4单帧与八帧滚动缓存_v1.md` +- 新增本报告 `docs/报告/STEP-098_QBIN_V4单帧与八帧滚动缓存_Codex完成报告_v1.md` +- 仅追加根目录 `粒子开发记录.md` + +现存 dirty B 文件、STEP-098 原任务、旧阻塞报告与 B untracked helper/test 均保留。`test/particle/field_runtime.test.cpp` 和 `test/source/raster_array_v4_host_api.test.cpp` 开工前已有 B dirty 内容;其既有 fixture 修正未覆盖,本轮在其上增加 STEP-098 测试,并给 Source QBIN helper 加入 reference-time 参数。本轮没有新增 RN、Android/iOS bridge、Host/NavSea/Server 文件。 + +## 4. T01–T17 测试代码覆盖与真实运行状态 + +“代码覆盖”指对应源码测试场景已新增或由既有定向测试覆盖;所有行的**实际运行状态均为 NOT RUN**。没有将未运行测试写成 PASS。 + +| 项 | 代码覆盖 | 实际运行 | +| --- | --- | --- | +| T01 | 单帧精确 projection/materialization、标量-only 不产生向量、Source 色场/粒子单帧 | NOT RUN | +| T02 | 第二帧加入后中点进入双帧插值 | NOT RUN | +| T03 | 乱序升序;wind/wave 独立 key;批次边界不混插 | NOT RUN | +| T04 | 满 8 后未来帧保留、最早端淘汰 | NOT RUN | +| T05 | 满 8 后过去帧保留、最晚端淘汰 | NOT RUN | +| T06 | 内部时间等距 tie 删除原最早端并维持 8 帧有序 | NOT RUN | +| T07 | 新 reference/profile 批次精确首显、补帧升级、回到旧批次精确单帧 | NOT RUN | +| T08 | Duplicate、identity conflict、revision、stale 有既有 Core/Source 用例;无单个覆盖所有分支的新满额综合用例 | NOT RUN | +| T09 | 损坏/合同/数值/Grid schema 拒绝及旧投影/字节计数保持;既有 Binding 覆盖错误向量 | NOT RUN | +| T10 | pair mix 0.25/0.5/0.99 与真实 validTime;精确端点使用单帧表示 | NOT RUN | +| T11 | 非覆盖时间返回 WindowUnavailable | NOT RUN | +| T12 | 外围滚动时活动 pair 与 requestedTime 保持 | NOT RUN | +| T13 | 活动 endpoint 被淘汰后清除物化 projection | NOT RUN | +| T14 | wind 与 wave 各持 8 帧、各自字节计数 | NOT RUN | +| T15 | 同帧共用 Snapshot/首次 GPU endpoint 资源的源码路径;context loss/真实 GPU 未覆盖 | NOT RUN | +| T16 | Core 120 次双向滚动、8 帧上限、clear/clearAll 与 decoded byte 回落;Runtime/GPU 长期泄漏未覆盖 | NOT RUN | +| T17 | 现有 API/类型/方法顺序静态核对;自动化两步成功/失败 consumer 测试未运行 | NOT RUN | + +## 5. 已执行与未执行验证 + +| 项目 | 实际结果 | +| --- | --- | +| `git diff --check` | **通过**,仅为空白与 patch 格式检查 | +| 新增 C++ 编译 | **NOT RUN** | +| Core/OpenGL GTest | **NOT RUN** | +| Metal host GTest / iOS 编译 | **NOT RUN** | +| Android Gradle、RN consumer、Test Host | **NOT RUN** | +| iOS/Xcode、Metal 设备 | **NOT RUN** | +| Jest/npm CI/ADB/模拟器/实体机 | **NOT RUN** | +| release pipeline、artifact、commit/push | **NOT RUN** | + +RDC 按顺序执行以下命令,阶段 B 通过后才进入 C: + +### 阶段 B:Store / Ingress / Binding + +```bash +ninja -C build-step088-opengl mbgl-test-runner +build-step088-opengl/mbgl-test-runner --gtest_filter='ParticleFieldRuntime.*:ParticleFieldBinding.*:ParticleEncodedFieldIngress.*' +``` + +### 阶段 C:Source / Projection / Renderer(OpenGL 与 Metal host) + +```bash +ninja -C build-step088-opengl mbgl-test-runner +build-step088-opengl/mbgl-test-runner --gtest_filter='Particle*.*:Source.RasterArray*' +ninja -C build-step097-metal-host mbgl-test-runner +build-step097-metal-host/mbgl-test-runner --gtest_filter='Particle*.*:Source.RasterArray*' +``` + +OpenGL/Metal host 命令均未在本轮执行。Metal host 结果也不能替代 iOS device runtime。 + +## 6. 手工验收、内存与剩余事项 + +Windows 11 Android 手工命令和 T01–T17 一轮场景清单见 [STEP-098 记录 §D](../50_Step记录/STEP-098_QBIN_V4单帧与八帧滚动缓存_v1.md)。只可使用后续相同源码生成且 Native/RN 版本匹配的 V15 Host APK、RN 包和 Native AAR;命令显式使用 `adb -s 192.168.200.139`。本轮未启动 V15 pipeline。iOS 需由 Mac 用户在同一冻结源码手工构建并用 Metal 设备检查单帧、双帧、批次边界、双向滚动、clear/context 恢复和长时内存。 + +CPU decoded 原始存储字节测量 API 已加入,测试源码含 120 次循环和 clear 后归零断言,但断言未运行。Float32 物化占用、GL/Metal 纹理、trail/GPU 峰值、32 帧风险和长时间内存趋势均未测量;不能据此报告内存安全 PASS。当前没有证据显示必须增加全局 MiB 上限,后续由 RDC/设备数据判断是否另行设计预算。 + +## 7. 工作树、阻塞与停止点 + +- 旧 `BLOCKED_CONTRACT` 不再适用,RFC-004 等新契约已正式接受。 +- 本轮剩余门槛是 RDC 尚未构建/测试。测试失败、编译失败或状态未知时,停留该阶段并保留现场;不得跳过到平台/V15。 +- STEP-097 B 的既有源码仍在本地工作树,原 Mac 工作树与 checkpoint worktree 均未重置或清理。本轮没有将 B 改动归入 STEP-098 报告修改集。 +- 当前版本锁仍是 Native/RN `.13`;新增 `DecodedFieldFrame` C++ layout 不兼容证据尚未通过同版本新构建验证。不能用旧 `.13` AAR 替代。 +- 未完成:RDC Core/Renderer 构建与 GTest、Android/iOS 同源码配套编译与真实 runtime、T01–T17 逐项实际执行、长时间内存/资源验证、V15 pipeline、版本制品与 Git 收尾。 +- 本次停止点:**Codex 源码交付**。无 commit、push、release,也没有宣称 STEP-098 全部验收完成。 diff --git a/docs/报告/STEP-098_RDC定向验收报告_v1.md b/docs/报告/STEP-098_RDC定向验收报告_v1.md new file mode 100644 index 000000000..50085562c --- /dev/null +++ b/docs/报告/STEP-098_RDC定向验收报告_v1.md @@ -0,0 +1,33 @@ +# STEP-098 RDC 定向验收报告 v1 + +日期:2026-09-27(JST) +状态:**CORE / OPENGL_HOST / METAL_HOST_SOURCE_VALIDATION_PASS;PLATFORM_AND_RELEASE_PENDING** +仓库:Mac `/Users/tei/dev/MapLibreParticleLab`,执行前 `particle/main` HEAD `b1239ac3b1554943200e6fbf14321459ccc86527`,以此工作树叠加 STEP-097 B 及 STEP-098 实际源代码测试。 + +## 实际顺序和证据 + +1. Codex `GPT-6 Luna High` 源码运行 `step098-codex-20260926-224510` 的 JSONL 已有 `turn.completed`、退出码 0。设计/Step/完成报告存在;它自身没有编译,也不作为独立功能 PASS。 +2. RDC B 初次运行 `step098-b-core-20260927-0833` 因 launchd PATH 不含 ninja 退出 127;核实原进程已结束才补 PATH 重新运行。 +3. B 第一次真实编译 `step098-b-core-retry1-20260927-0834` 在 Source 测试引用不存在的 `Impl.encodedFieldStore` 处失败。按 public Source projection/seek 改测试断言,不改生产 API。下一次 `step098-b-core-retry2-20260927-0836` 编译 PASS,但 19 个定向测试中 2 个失败:测试 fixture 把 grid east=2 设成了小于 west=130 的非法 Grid,实际正确返回 `RejectedInvalidFrame`,而用例期待 `RejectedIncompatibleFrame`。将两处 east 改成 132(有效但与原 east=131 不兼容),没有降低生产校验。 +4. B 再次执行 `step098-b-core-retry3-20260927-0838`:OpenGL Host Native C++ 编译退出 0,Core/Binding/Ingress 19/19 PASS;原始日志 `artifacts/maplibre-particle/step098-rdc/b-core-build.log`、`b-core-gtest.log`。 +5. C1 `step098-c-opengl-20260927-0840`:Source/Projection/Renderer 扩展 OpenGL Host 119/119 PASS,日志 `c-opengl-gtest.log`。 +6. C2 `step098-c-metal-host-20260927-0841`:Metal Host C++ 编译退出 0,同 filter 119/119 PASS,日志 `c-metal-host-build.log`、`c-metal-host-gtest.log`。现有 maplibre-tile-spec 个别对象的 macOS deployment target 警告非失败。 +7. 全部新旧文件 `git diff --check` PASS;本记录时尚未执行正式 Android AAR、RN Test Host、iOS XCFramework、V15 Pipeline 或任何实体机验证。 + +## 未满足的门槛与边界 + +- `T01~T17` 已有对应源码/测试覆盖;上列 19/119/119 是实际运行的 Host GTest,不可将 T15 GPU/context loss、T16 长时 CPU/GPU 内存或 T17 RN 两步调用/真实 bridge 集成全部标 PASS。 +- `DecodedFieldFrame` 新增 profile 身份,需用**同一冻结源提交**构建 Android/RN/iOS;现有正式 `.13` AAR 与旧 `.14-dev` 不能替代。 +- V15 正式构建/发布需要先取得干净且确定的 release Git commit、检验本地空间、版本锁、脚本 prepare/build/publish/finalize 各自证据;公开版本不可覆盖。 +- STEP-097 B 用户原工作内容必须保留,禁止 reset/clean、混入无关 untracked。按已授权的统一 V15 发行目标进行 scoped Git 收尾,不能将 Host 源级 PASS 冒称设备验收或正式 RELEASE_PASS。 + +## 真实统计 + +| 阶段 | 构建 | GTest | 结论 | +| --- | --- | --- | --- | +| B OpenGL Core/Binding/Ingress | PASS | 19/19 | PASS | +| C1 OpenGL Host | 已由 B 更新编译 | 119/119 | PASS | +| C2 Metal Host | PASS | 119/119 | PASS | +| Android/RN 同源配套 | NOT RUN | NOT RUN | PENDING | +| iOS XCFramework/设备 | NOT RUN | NOT RUN | PENDING | +| V15 Pipeline | NOT RUN | NOT RUN | PENDING | diff --git a/docs/报告/STEP-098_公共契约阻塞报告_v1.md b/docs/报告/STEP-098_公共契约阻塞报告_v1.md new file mode 100644 index 000000000..123559e9c --- /dev/null +++ b/docs/报告/STEP-098_公共契约阻塞报告_v1.md @@ -0,0 +1,47 @@ +# STEP-098:公共契约闸门阻塞报告 v1 + +日期:2026-09-26(日本时间) +状态:**BLOCKED_CONTRACT / 未进入功能实施和发行**。 + +## 1. 当前真实基线 + +- 用户本轮要求:RDC 严格串行执行 STEP-098,并使用统一 Pipeline 发布 V15;未要求将历史 .14-dev AAR 当作 V15 产物。 +- 源码仓库:Mac `/Users/tei/dev/MapLibreParticleLab`,分支 `particle/main`,本轮开工 HEAD 与公开 `origin/particle/main` 均为 `b1239ac3b1554943200e6fbf14321459ccc86527`。 +- 该正式工作树已有 22 项修改/未跟踪内容(含 STEP-097 B 和本任务 MD);这些既有内容全部受保护,不得 reset、clean、覆盖或整体暂存。 +- 本任务原始文件:`docs/40_任务/STEP-098_QBIN_V4单帧首显与八帧滚动缓存_Codex任务_v1.md`;其状态明确为“待契约闸门通过后执行”。 +- Integration:Mac `/Users/tei/dev/navsea-particle-integration` 已执行 `git fetch origin main`;本地 `main` 和 `origin/main` 一致,均为 `318f8ccf911ac3136c3086a1d14e61f83e2d4bce`,工作树干净。 +- 当前已安装正式版本:Android/RN `13.4.1-particle.13`,iOS 基础 VERSION `6.28.0`。先前 Native `13.4.1-particle.14-dev-d00d2ddc-bf4539890c6f` 仅为本地开发 AAR,不能替代 RN 联编、双平台测试或 V15 正式发行证据。 + +## 2. 契约冲突与停止原因 + +- `rfc/RFC-003_编码场两帧首显与增量补帧语义.md` 仍标为 `DRAFT/Shared Core 与 Android/iOS 评审待完成`,其目标语义第 1 项是至少两帧覆盖后才合法显示,第 4 项是超容量明确拒绝。 +- STEP-098 的目标不同:一个有效时间的单帧可按真实 validTime 首显并驱动合法向量粒子;后续每 fieldKey 维持按时间升序的最多 8 帧,合法第 9 帧按新帧时间选择远端淘汰,正常满额提交不返回容量拒绝。 +- `coordination/active/COORD-004/状态.md` 将 B 列为 `B_RFC_DRAFT`;同目录 `决议.md` 仅明确 A 的 React Style 方向,B 仍待调查。未发现正式接受 STEP-098 新语义的 RFC/ADR/COORD 决定或合同更新。现有 C-001 不可被当成这项新行为已经批准。 +- 按 STEP-098 任务 §1 第 3~4 项,必须先由粒子负责人/Integration 正式接受且消除上述冲突,记录准确文件、版本、commit;本执行窗口不能把聊天目标、自身判断或 DRAFT 冒充正式跨平台决定。 + +## 3. 需要的正式协调决定 + +请由粒子负责人在 Integration 的 COORD-004/RFC-003 或新 ADR/Contract 中明确并正式接受: +1. 单帧真实时刻的首显和向量粒子行为;合法相邻两帧插值;不覆盖目标的旧帧必须 fail-closed,不能伪造数据或时间。 +2. 每 fieldKey 独立的 8 帧排序/容量;正常已满时先验证、再原子淘汰并保留新帧;无效帧零副作用、不同 key 不相互挤占。 +3. 未来/过去/内部插入的确定性端点淘汰,距离相同时的 tie-break,以及当前显示端点能否参与稳定优先级;禁止依赖尚未下发的新目标时间决定淘汰。 +4. 同 validTime duplicate/revision/stale/conflict、dataset/profile/grid 身份、请求时间不被 submit 静默修改、现有 RN 两步调用、Renderer/GPU 资源复用与释放、内存边界及 Android/iOS 同一 Shared Core 契约。 +5. 核对任务书中“13:00 ... 21:00 共 8 帧”的计数错误(若逐小时含两端实际为 9 个时间点),在正式合同及实施示例中写为确实 8 帧的区间。 +6. 指明是否因生命周期、错误/数据所有权语义变化需要升级 C-001,以及适用的正式版本与跨平台责任。 + +以上正式结果若与 STEP-098 MD 的行为不一致,应先修订任务书,不得由 Android/iOS 各自选择不同语义。 + +## 4. Git 和 V15 Pipeline 独立门槛 + +- 用户提出 V15:统一脚本 `tools/particle_release_pipeline/release.sh 15` 的目标将是 Android/RN `13.4.1-particle.15` 与 iOS `6.28.0-particle.15`。这是待发行目标,不是已经构建/发布的事实。 +- Pipeline 的 `prepare`、`build` 均要求工作树干净且版本锁正确;当前主工作树 dirty,不能直接运行。STEP-097 B 的未提交工作及正式版本锁需先按范围审计、受控收尾,不能 `git add -A` 或把 .14-dev 当正式 .15。 +- Pipeline 的 public target 不得覆盖现有版本;正式发布必须在同一冻结 source commit 上完成 Android Native 四 ABI、RN、iOS Metal/XCFramework,分别验证,再检查 SHA、manifest、SwiftPM tag 与公网实际回读。 +- Mac 只余约 11 GiB 可用空间,是完整构建前需要核实的容量风险;不能据此虚报构建失败或 PASS。 + +## 5. 本轮实际执行与未执行 + +- 已执行:RDC 只读 Git/任务/发布脚本审计、Integration fetch 和 SHA 对齐、RFC-003/COORD-004/Contract/职责核查。 +- 未执行:Codex 功能实现、Core/Bridge/Renderer 修改、T01~T17 测试、Android/iOS 编译或设备验收、V15 Pipeline 的 prepare/build/publish/finalize、Git commit/push。 +- 本报告是独立新增文件,不修改 STEP-098 MD、STEP-097 既有改动、Integration 文档、VERSION、历史制品或任何公网发行目录。 + +**当前结论:BLOCKED_CONTRACT。第一闸门未正式通过,不启动第二阶段,也不进行 V15 发行。** diff --git a/docs/报告/STEP-099_RenderSource就绪与RenderSegment失效诊断_Codex完成报告_v1.md b/docs/报告/STEP-099_RenderSource就绪与RenderSegment失效诊断_Codex完成报告_v1.md new file mode 100644 index 000000000..b39175f15 --- /dev/null +++ b/docs/报告/STEP-099_RenderSource就绪与RenderSegment失效诊断_Codex完成报告_v1.md @@ -0,0 +1,536 @@ +# STEP-099 RenderSource 就绪与 RenderSegment 失效诊断 — Codex 完成报告 v1 + +## 1. 执行结论 + +**CLOSED / PASS(诊断阶段完成)**。 + +STEP-099 按任务书保持“纯诊断、fail-closed、不优化正式语义”的范围完成。Renderer/Source/RenderSegment diagnostics、Android Test App T1~T4 场景、定向 native 测试和 Android 实机运行证据均已取得,Q1~Q7 可以据实回答。 + +本 Step **没有**修改 retained cache 算法、fast path gate、`singleContinuousSegment`、multi-continuous 行为、segment-local invalidation、RenderSegment 执行语义、NavSea、Source `isLoaded()` 定义或 RN 业务 API 语义。 + +## 2. 基线与 Git + +- 仓库:`/Users/tei/dev/MapLibreParticleLab-step099` +- branch:`step099-render-source-diagnostics` +- frozen release tag:`maplibre-particle/rn/13.4.1-particle.17` +- frozen release commit / start base:`e8b451fe70054107773e82484c80e90df3889147` +- implementation commit:`8a5214d92dd2f8899db76d2194da42999fa0c87d` +- implementation message:`diagnostics: add STEP-099 render source readiness audit` +- Integration/NavSea:未修改。 +- 文档 closeout commit、push result、`origin/particle/main` Final HEAD:Git 提交无法在自身内容中自指其最终 SHA,按项目既有规则由最终交付回读记录。 + +## 3. 实际修改范围 + +生产源码 / diagnostics: + +- `include/mbgl/renderer/render_segment.hpp` +- `src/mbgl/renderer/render_orchestrator.cpp` +- `src/mbgl/renderer/render_orchestrator.hpp` +- `src/mbgl/renderer/render_segment_json.hpp` +- `src/mbgl/renderer/render_source.hpp` +- `src/mbgl/renderer/retained_underlay_runtime_state.hpp` +- `src/mbgl/renderer/sources/render_tile_source.cpp` +- `src/mbgl/renderer/sources/render_tile_source.hpp` +- `packages/maplibre-particle-rn/src/index.ts` +- `packages/maplibre-particle-rn/src/types.ts` + +测试 / Test App: + +- `test/renderer/retained_underlay_cache.test.cpp` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step099ReadinessFixture.kt` + +项目记录: + +- `docs/50_Step记录/STEP-099_RenderSource就绪与RenderSegment失效诊断_v1.md` +- `docs/报告/STEP-099_RenderSource就绪与RenderSegment失效诊断_Codex完成报告_v1.md` +- `粒子开发记录.md` + +## 4. 诊断能力 + +### 4.1 全局 readiness + +新增只读 snapshot: + +- `styleLoaded` +- `imageManagerLoaded` +- `renderOrchestratorLoaded` +- `spriteLoaded` +- `hasFadingTiles` +- global retained `contentRevision` +- RenderSegment plan/config/topology 信息 +- 最近 mutation sequence / reason / source / affected segment diagnostics + +正式 loaded 路径保持原式: + +```cpp +renderTreeParameters->loaded = updateParameters->styleLoaded && isLoaded(); +``` + +diagnostics snapshot 在旁路采样,失败被 best-effort 隔离,不决定正式 loaded。 + +### 4.2 per-source readiness + +每个 RenderSource additive 暴露: + +- sourceId / sourceType +- enabled / isLoaded +- tileBacked +- tileCount / incompleteTileCount / loadedTileCount +- hasFadingTiles +- last source/tile mutation sequence +- last tile action / reason + +默认非 tile source 返回完整显式初始化的空统计,Android NDK `-Werror` 下已验证通过;未改变任何 Source `isLoaded()` 实现。 + +### 4.3 RenderSegment 只读依赖 + +每个 segment additive 暴露: + +- segmentIndex / strategy / beginIndex / endExclusive +- `dependentSourceIds` +- `unknownDependencies` + +规则: + +- 从实际 ordered layers 的 sourceId 推导; +- shared source 可出现在多个 segment; +- source-less background 不制造假依赖; +- symbol/custom 等无法仅靠 sourceId 完整表达的依赖标记为 unknown/global; +- 依赖图只用于诊断,不参与 cache/loaded/fast path/execution。 + +### 4.4 mutation 定位 + +Source/tile mutation 复用已有回调记录 sequence/source/reason/action。原 `retainedUnderlayContentRevision` 的增长次数与正式调用路径不变;diagnostics 不增加额外 revision。 + +query 路径不清理或修改 event history,重复 snapshot/JSON 查询保持无副作用。 + +## 5. Native 定向测试 + +### 5.1 Metal host 定向 GTest + +实际执行: + +```bash +cmake --preset macos-metal +cmake --build build-macos-metal --target mbgl-test-runner --parallel 3 +build-macos-metal/mbgl-test-runner --gtest_filter='RenderSegmentDiagnostics.*' +``` + +结果: + +- `RenderSegmentDiagnostics.*`:**6/6 PASS** +- 新增 `ReadinessSnapshotMapsSourcesToSegmentsWithoutSideEffects`:PASS +- 覆盖 source loaded/unloaded、tile stats、source→segment、shared source、source-less、重复 query、topology/cache/content revision 无副作用。 + +证据: +`artifacts/maplibre-particle/android-validation/step099-metal-gtest.log` + +SHA-256: +`f0618750ead03eae1b251126dd49488a9269a120373a66e3453fa9aaa1dc0f10` + +说明:该 6/6 发生在 Android NDK 首轮暴露“默认 readiness struct designated initializer 未显式列完 optional 字段”之前;随后仅补全默认空字段初始化。最终源码由 Android 全量 native build 成功覆盖。为了在最终源码上重跑 Metal,重新 configure 时当前 Mac 缺 `libuv` pkg-config,configure fail;没有安装系统依赖来制造 PASS。 + +### 5.2 Vulkan + +`macos-vulkan` configure:**BLOCKED / NOT RUN TESTS**。 + +原因: + +```text +The following required packages were not found: + - libuv +``` + +这是当前 Mac host 环境依赖,不是 STEP-099 源码编译错误。 + +### 5.3 Jest + +**NOT RUN**。用户未要求 Jest,任务未加入 Jest。 + +## 6. Android 构建与设备 + +### 6.1 首轮 Android 编译问题 + +首轮 x86_64 OpenGL build 在 Android NDK `-Werror` 下发现: + +```text +render_source.hpp:84:51: +error: missing field 'tileCount' initializer +[-Werror,-Wmissing-designated-field-initializers] +``` + +修复仅补全 `RenderSourceReadinessStats` 默认空字段初始化,不改变运行语义。 + +随后 native C++ 已越过原失败点并完整编译。 + +### 6.2 磁盘阻塞与恢复 + +第一次完整 native build 后,在: + +`MapLibreAndroidTestApp:stripOpenglDebugDebugSymbols` + +因 Mac 仅剩约 2 GiB 出现: + +```text +No space left on device +``` + +RDC 只清理可再生 Gradle 9.5.1 cache / 本轮失败中间产物,保留 Android native object;可用空间恢复到约 12 GiB 后增量重跑。 + +### 6.3 最终 APK + +命令: + +```bash +./tools/particle_android_validation/build_validation_apk.sh +``` + +最终结果: + +- Gradle:**BUILD SUCCESSFUL in 4m 54s** +- actionable tasks:86(7 executed / 79 up-to-date) +- variant:OpenGL Debug +- ABI:x86_64 +- APK:61,923,024 bytes +- `libmaplibre.so`:39,602,856 bytes +- APK SHA-256: + `5de73f0358b835a78d3ae7e2ac22f4efdb2a66775b8c63654a8bb7313957d90a` + +构建时 metadata: + +- base commit:`e8b451fe70054107773e82484c80e90df3889147` +- `git_dirty=true` +- 构建内容与随后形成的 implementation commit `8a5214d92` 源码一致;当前 source tree 对 implementation commit clean,剩余仅 docs closeout。 + +### 6.4 Android 设备 + +Mac ADB 直接连接: + +- serial:`192.168.200.139:5555` +- model:`FZG1-2` +- Android:13 +- API:33 +- ABI:x86_64 + +APK 覆盖安装成功,`ParticleCoreLayerValidationActivity` 冷启动成功。 + +运行 log: +`artifacts/maplibre-particle/android-validation/step099-logcat.txt` + +log SHA-256: +`0f15d75dde77e7dd2f4d98c46a4c1501a628205e8aae62130b0cb96db6b8482f` + +## 7. T1~T4 实测 + +### T1 STATIC + +场景: + +- R0:静态 GeoJSON retained +- C:本地静态 RasterArray field 驱动 WeatherColor + Particle +- R1:静态 retained +- camera fixed + +结果: + +- 启动阶段 1 个 false window:204 ms; +- 随后 `renderOrchestratorLoaded=true` 稳定; +- `particle-fixture` 与 `step099-static` 均 loaded; +- retained cache 建立后持续 reuse; +- fast path 进入 `active=true / eligible=true / reason=none`; +- 没有持续 style-not-loaded。 + +aggregate: + +```text +falseWindowCount=1 +falseWindowAvgMs=204 +falseWindowMaxMs=204 +openFalseWindowMs=0 +``` + +结论:静态基线可稳定进入 loaded/cache reuse/fast path。 + +### T2 GEOJSON_1HZ + +两个 GeoJSON source 每 500 ms 交错更新,因此每个约 1 Hz。 + +实际: + +- 总 source update:122 次,约 61 次/源; +- 60 秒量级观察中,出现 10 个 loaded=false window; +- 平均 203 ms; +- 最大 205 ms; +- 最终 open window = 0; +- 总 false 时间约 2.03 s,约占 60 s 的 3.4%,不是“几乎始终 false”。 + +aggregate: + +```text +falseWindowCount=10 +falseWindowAvgMs=203 +falseWindowMaxMs=205 +openFalseWindowMs=0 +``` + +同时 final sample: + +- global contentRevision 已到 1679; +- R0 仅依赖 `step099-static`,但 cache build=241、invalidation=362; +- R1 依赖动态 GeoJSON,也同样 build=241; +- mutation diagnostics 能正确指出动态 source 只 `affected=[2]`,但正式 cache invalidation 仍非局部。 + +### T3 PERMANENT_UNREADY + +额外加入: + +`step099-unready-raster-array` + +它: + +- 没有 layer; +- 因此 `enabled=false`; +- host-submission 无 field; +- `isLoaded=false`。 + +其他主要 source 均 loaded。 + +实测: + +- `renderOrchestratorLoaded=false` 持续; +- retained segment cache 持续 fallback; +- `lastInvalidationReason=style-not-loaded`; +- 观察结束仍没有恢复。 + +aggregate: + +```text +falseWindowCount=0 +openFalseWindowMs=21459 +``` + +结论:即使 source 与当前 layer/segment 无关且 disabled,只要仍在 `renderSources` 且 `isLoaded=false`,V17 全局 `RenderOrchestrator::isLoaded()` 就保持 false。 + +### T4 R-C-R + +结构: + +- R0:`deps=[step099-static]` +- C:`deps=[particle-fixture]`,WeatherColor + Particle +- R1:`deps=[org.maplibre.annotations, step099-dynamic, step099-static]` +- `step099-dynamic` 约 1 Hz 更新 + +实际: + +- dynamic update:61 次; +- loaded false window:4; +- 平均 203 ms; +- 最大 207 ms; +- global contentRevision:1760 → 2540; +- mutation diagnostics:`step099-dynamic` 对应 `affected=[2]`; +- R0 并不依赖 `step099-dynamic`,但: + - build:1 → 121 + - invalidation:20 → 200 +- R1 同步得到相同级别 build/invalidation 增长; +- fast path 在稳定窗口可恢复 active,但 global revision / transient readiness 会反复制造 full CPU work。 + +aggregate: + +```text +falseWindowCount=4 +falseWindowAvgMs=203 +falseWindowMaxMs=207 +openFalseWindowMs=0 +``` + +结论:依赖诊断已经能准确表达“R1 source-local”,但 V17 正式 content revision/cache invalidation 仍是 global。 + +## 8. Q1~Q7 + +### Q1:单个 permanently unloaded source 会造成 V17 style-not-loaded 吗? + +**YES / CONFIRMED。** + +T3 中 `step099-unready-raster-array` 无 layer、`enabled=false`、`isLoaded=false`;其他主要 source loaded,但整体 `renderOrchestratorLoaded=false` 持续 21.459 s 未恢复,retained cache 持续 `style-not-loaded` fallback。 + +源码事实与运行事实一致:`RenderOrchestrator::isLoaded()` 遍历全部 `renderSources`,不按 enabled/segment dependency 排除 source。 + +### Q2:1 Hz GeoJSON 会周期性让 isLoaded=false 吗?平均/最大窗口? + +**YES / CONFIRMED。** + +T2 双 source 交错各约 1 Hz: + +- 10 次 false window; +- avg 203 ms; +- max 205 ms。 + +T4 单动态 source 约 1 Hz: + +- 4 次 false window; +- avg 203 ms; +- max 207 ms。 + +### Q3:错开的多个 1 Hz GeoJSON 会导致 global loaded 几乎无法稳定为 true 吗? + +**NO / 本次观测不支持。** + +T2 约 60 s 中总 false 时间约 2.03 s,约 3.4%;绝大多数时间 global loaded=true。它确实产生 readiness thrash,但不是“几乎永远 false”。 + +### Q4:GeoJSON tile mutation 会推进 global retainedUnderlayContentRevision 并影响 retained cache 吗? + +**YES / CONFIRMED。** + +T4 global contentRevision 1760 → 2540。仅依赖静态 source 的 R0 也从 build 1 → 121、invalidation 20 → 200。 + +### Q5:不引用 GeoJSON 的 retained segment 仍会受它的 loaded/fading/tile mutation 影响吗? + +**YES。** + +运行证据: + +- T4 diagnostics 正确报告动态 source `affected=[2]`; +- 但 R0 `deps=[step099-static]` 仍随动态 source mutation 重建/失效。 + +源码证据: + +- `retainedRangeIsStable(begin,end)` 虽为 range API,但最后遍历 **全部 `renderSources`**,任一 source `hasFadingTiles()` 都让任意 retained range unstable。 + +本次 T4 稳态 `fade=false`,因此 R0 的实际反复失效主要直接证明 global content revision/tile mutation;GLOBAL_FADE 属源码确认、未在本轮场景主动触发的全局 gate。 + +### Q6:`tryRenderParticleFastFrame` 仍有哪些 global gate 会阻挡未来 localized cache CPU fast path? + +**确认仍存在:** + +1. **GLOBAL_UPDATE_SIGNATURE** + - `particleFastPathNonCameraInputsMatch` 全局比较: + - styleLoaded / spriteLoaded + - light + - images + - **sources** + - **layers** + - fontFaces / fileSource / glyphURL + - transition / placement / tile LOD 等全局输入。 + +2. **GLOBAL_CONTENT_REVISION** + - `particlePreparedContentRevision == orchestrator.getRetainedUnderlayContentRevision()`。 + +3. **GLOBAL_FADE** + - `particleFastPathDynamicState` 遍历全部 renderSources 汇总 fading; + - `retainedRangeIsStable` 也遍历全部 renderSources。 + +4. **GLOBAL_PENDING / TRANSITION / REPAINT** + - pendingChanges + - renderLight transition + - 任意 layer transition + - 任意非粒子 layer repaint + - placement transition。 + +5. **GLOBAL_ANNOTATION_MUTATION** + - AnnotationManager `hasPendingChanges()`。 + +6. **RETAINED REVISION / CACHE / TOPOLOGY / BOUNDARY** + - retained cache validity/static revision + - global retained revision tracker + - topology revision + - boundary/config revision。 + +7. **singleContinuousSegment** + - 仍要求 plan 恰好能由一个 Continuous segment 表达; + - multi-continuous 继续 fail closed,本 Step 未修改。 + +因此即使未来 cache invalidation 变成 source/segment-local,fast path 仍有多组 global gate 需要后续 Step 单独设计,不能在 STEP-099 顺手放宽。 + +### Q7:blocker 分类 + +**运行态 CONFIRMED:** + +- `PERMANENT_SOURCE_UNLOADED` +- `DYNAMIC_SOURCE_READINESS_THRASH` +- `GLOBAL_CONTENT_REVISION` + +**源码 CONFIRMED,本轮未主动触发独立运行场景:** + +- `GLOBAL_FADE` +- `GLOBAL_UPDATE_SIGNATURE` + +**OTHER:** + +- global pendingChanges +- placement/light/non-particle transition/repaint +- annotation mutation +- topology/boundary/revision/cache validity +- singleContinuous fail-closed + +## 9. 任务边界审计 + +确认未改: + +- retained cache 核心算法; +- fast path gate 逻辑; +- `singleContinuousSegment`; +- multi-continuous 策略; +- segment-local invalidation 正式语义; +- RenderSegment execution; +- NavSea; +- private NavSea Native API; +- RN 业务 API; +- Source `isLoaded()` 定义。 + +新增 dependency / affectedSegmentIndices 只是观察信息;它没有参与正式 invalidation。 + +## 10. 证据文件 + +忽略目录: + +`artifacts/maplibre-particle/android-validation/` + +主要文件: + +- `build-info.txt` +- `build.log` +- `MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk` +- `step099-logcat.txt` +- `step099-device-info.txt` +- `step099-metal-gtest.log` +- `step099-cmake-metal.log` + +APK SHA-256: + +`5de73f0358b835a78d3ae7e2ac22f4efdb2a66775b8c63654a8bb7313957d90a` + +STEP-099 log SHA-256: + +`0f15d75dde77e7dd2f4d98c46a4c1501a628205e8aae62130b0cb96db6b8482f` + +## 11. 下一阶段建议 + +STEP-099 只负责定位,不在本 Step 修复。 + +下一阶段应基于本轮证据拆开处理: + +1. 先决定 global source readiness 是否要引入“影响当前 style/render segment 的 readiness 范围”,解决 permanently unready / transient unrelated source 对全局 loaded 的影响; +2. 再设计 source/segment-local content revision 与 invalidation,避免 R1 GeoJSON mutation 重建 R0; +3. 最后单独审查 fast-path global signature/fade/pending/placement 等 gate,不能把 cache locality 与 fast-path locality 混成一个修改; +4. 保持 Style Z-order 唯一事实来源和当前通用 Render Segment 模型。 + +## 12. 最终状态 + +**STEP-099 CLOSED / PASS。** + +Pass 含义仅为: + +- diagnostics 能回答任务问题; +- Native 定向测试已有 PASS 证据; +- 最终 Android x86_64 OpenGL APK BUILD SUCCESS; +- FZG1-2 T1~T4 实跑完成; +- Q1~Q7 有源码/运行证据; +- 未改变正式渲染语义。 + +不代表: + +- Vulkan host 已通过; +- iOS runtime 已测试; +- 下一阶段 localized readiness/invalidation 已实现; +- fast path global gates 已优化。 diff --git a/docs/报告/STEP-100_SourceScoped_RenderSegment局部失效_Codex完成报告_v1.md b/docs/报告/STEP-100_SourceScoped_RenderSegment局部失效_Codex完成报告_v1.md new file mode 100644 index 000000000..d9b958500 --- /dev/null +++ b/docs/报告/STEP-100_SourceScoped_RenderSegment局部失效_Codex完成报告_v1.md @@ -0,0 +1,454 @@ +# STEP-100 SourceScoped RenderSegment 局部失效 — 完成报告 v1 + +## 1. 最终结论 + +**CLOSED / PASS(功能与 Android 运行态目标确认)**。 + +Codex 实现使用: +- model:`gpt-6-sol` +- reasoning effort:`high` + +RDC 独立完成: +- 源码 diff 审计; +- Android x86_64 OpenGL 全量 native build; +- Validation APK; +- FZG1-2 自动安装与 T1/T2/T3/T4; +- STEP-100 60 秒 locality window; +- validation build script 小型工程修复; +- Git diff/status 审计。 + +本 Step 核心目标已确认: + +> 动态 GeoJSON Source 更新只影响依赖它的 RETAINED Segment;无关 R0 cache 不再跟着 invalid/rebuild,并且在 global loaded=false 时仍可安全 reuse。 + +Native host GTest 因当前 Intel Mac 缺 `libuv` 未执行;曾尝试 Homebrew 安装,但 Homebrew 要连带升级 CMake/OpenSSL/Python 且 Intel 已无 bottle 支持,因此 RDC 主动终止,未把环境变化作为任务依赖。Android 最终 C++ native build 与设备运行态用于本轮功能验收。 + +Jest:**NOT RUN**,用户未要求。 + +## 2. Git / 基线 + +- worktree:`/Users/tei/dev/MapLibreParticleLab-step100` +- branch:`step100-segment-local-source-invalidation` +- start HEAD:`c4b481fd2c5b5da20d31f6ae54011f0552d7dbd0` +- start remote:`origin/particle/main = c4b481fd2c5b5da20d31f6ae54011f0552d7dbd0` +- frozen V17 tag:`maplibre-particle/rn/13.4.1-particle.17` +- frozen V17 commit:`e8b451fe70054107773e82484c80e90df3889147` +- V17 tag/release worktree:未修改。 + +最终 STEP-100 commit / push / remote HEAD 在 Git 收尾后由最终交付回读记录,避免文档自引用。 + +## 3. 实际修改 + +生产源码: + +- `include/mbgl/renderer/render_segment.hpp` +- `src/mbgl/renderer/render_orchestrator.hpp` +- `src/mbgl/renderer/render_orchestrator.cpp` +- `src/mbgl/renderer/renderer_impl.cpp` +- `src/mbgl/renderer/retained_underlay_cache.cpp` +- `src/mbgl/renderer/retained_underlay_runtime_state.hpp` +- `src/mbgl/renderer/render_segment_json.hpp` +- `packages/maplibre-particle-rn/src/types.ts` + +Test App / tests: + +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step099ReadinessFixture.kt` +- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` +- `test/renderer/retained_underlay_cache.test.cpp` + +工具: + +- `tools/particle_android_validation/build_validation_apk.sh` + - RDC 修复 `unzip | awk ... exit` 在 `pipefail` 下偶发 SIGPIPE/141; + - 仅移除 awk 的提前 exit,不改变 APK 验证语义。 + +文档: + +- `docs/40_任务/STEP-100_SourceScoped_RenderSegment局部失效_Codex任务_v2.md` +- `docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md` +- 本报告; +- `粒子开发记录.md` + +## 4. 生产设计 + +### 4.1 Source-scoped revision + +新增正式 source-scoped revision bookkeeping。 + +明确的 Source/Tile mutation: +- source diff; +- feature state; +- tile changed; + +推进对应 sourceId revision。 + +现有 global `retainedUnderlayContentRevision` 继续增长,供: +- compatibility; +- diagnostics; +- Fast Path fail-closed。 + +RETAINED Segment cache 不再直接以 global Source/Tile revision 作为内容 revision。 + +### 4.2 Segment dependency + +正式 renderer 与 diagnostics 共用 source→segment 依赖推导。 + +规则: +- layer sourceId 决定 source dependency; +- source-less background 不制造假 dependency; +- shared source 可影响多个 segment; +- symbol 的 glyph/image/placement 作为明确 global prerequisite; +- 真正无法证明的 dependency fail closed。 + +### 4.3 Per-segment readiness + +未修改: +- `Source::isLoaded()`; +- `renderTreeParameters->loaded = styleLoaded && isLoaded()`。 + +full CPU retained path 增加 per-segment source readiness/stability: + +- dependent source unloaded → 仅该 Segment fallback; +- dependent source fading → 仅该 Segment unstable; +- unrelated source unloaded/fading → 不影响该 Segment; +- style/image/sprite/glyph/placement/context 等 global prerequisite 仍 fail closed。 + +### 4.4 Mixed Build / Reuse / Fallback + +移除 retained full-CPU path 的 all-or-none `useRetainedRanges` 依赖。 + +每个 RETAINED Segment独立决定: + +- Build +- Reuse +- Fallback + +主 buffer 按 Style Segment 顺序执行: + +- RETAINED Build/Reuse → composite cache +- RETAINED Fallback → live render only that range +- CONTINUOUS → live render + +因此允许: + +```text +R0 = Reuse +C = Live +R1 = Fallback +``` + +不再因为 R1 fallback 把 R0 一起变成 live redraw。 + +Fast Path gate / `singleContinuousSegment` / multi-continuous Fast Path:**未修改**。 + +## 5. Android build + +首次标准 validation script 的 Gradle 本体实际结果: + +```text +BUILD SUCCESSFUL in 12s +86 actionable tasks: 6 executed, 80 up-to-date +``` + +但脚本在 Gradle 成功后以 141 退出。RDC 用 `bash -x` 精确定位到: + +```bash +LIB_BYTES="$(unzip ... | awk '/.../ {print $1; exit}')" +``` + +在 `set -o pipefail` 下触发 SIGPIPE。 + +RDC 仅去掉 awk 的 `exit`,重跑标准脚本: + +```text +RC=0 +[validation-apk] BUILD SUCCESS +``` + +最终 APK: + +- variant:OpenglDebug +- ABI:x86_64 +- apk bytes:61,956,640 +- libmaplibre.so bytes:39,636,472 +- SHA-256: + `86e22bf081bf7d3196f0bf0e7c56af51bde01e9f0b01e0bab238cb794273315a` + +证据: +`artifacts/maplibre-particle/android-validation/` + +## 6. 设备 + +- serial:`192.168.200.139:5555` +- model:FZG1-2 +- Android:13 +- API:33 +- ABI:x86_64 + +APK 覆盖安装成功,`ParticleCoreLayerValidationActivity` 冷启动成功。 + +STEP-100 log: +`artifacts/maplibre-particle/android-validation/step100-logcat.txt` + +log SHA-256: +`e9d18ea987d12cb30e321d0d9c669b2588972aac89e5277920bf1fa9fee8146b` + +## 7. T1 STATIC + +运行约 20 秒。 + +aggregate: + +```text +falseWindowCount=1 +falseWindowAvgMs=203 +falseWindowMaxMs=203 +openFalseWindowMs=0 +``` + +最终: + +- R0 build=1 +- R0 reuse=1217 +- R0 decision=reuse +- R1 build=1 +- R1 reuse=1187 +- R1 decision=reuse + +静态 R-C-R 正常。 + +## 8. T4 R-C-R 主验收 + +结构: + +```text +R0 -> step099-static +C -> particle-fixture +R1 -> step099-dynamic (+ framework annotation source if present) +``` + +window marker: + +```text +baseline tick=8 17:05:21.642 +final tick=130 17:06:23.240 +``` + +窗口约 61.6 秒。 + +dynamic update 总计:68 次。 + +global revision: + +```text +128 -> 973 +``` + +说明 global compatibility revision **仍正常增长**,没有通过停掉 global revision“作弊”。 + +### R0 + +baseline: + +```text +build=1 +reuse=132 +invalidation=20 +srcRev=5127273714767523329 +decision=reuse +``` + +final: + +```text +build=1 +reuse=4042 +invalidation=20 +srcRev=5127273714767523329 +decision=reuse +``` + +Delta: + +```text +buildDelta = 0 +invalidationDelta = 0 +reuseDelta = +3910 +sourceRevision = unchanged +``` + +**硬门槛 PASS。** + +### R1 + +baseline: + +```text +build=5 +invalidation=26 +``` + +final: + +```text +build=135 +invalidation=211 +``` + +动态 Source mutation 正常驱动 R1。 + +mutation diagnostics 持续: + +```text +sourceId=step099-dynamic +affected=[2] +scope=source-local-diagnostic +``` + +### loaded false windows + +T4: + +```text +falseWindowCount=5 +falseWindowAvgMs=202 +falseWindowMaxMs=203 +openFalseWindowMs=0 +``` + +即 transient global loaded=false 仍存在,但不再让 R0 build/invalidation 增长。 + +## 9. T2 双 GeoJSON readiness 回归 + +约 35 秒。 + +aggregate: + +```text +falseWindowCount=6 +falseWindowAvgMs=203 +falseWindowMaxMs=205 +openFalseWindowMs=0 +``` + +R0 在动态窗口保持: + +- build=1 +- invalidation=18 +- revision unchanged +- reuse 持续增长。 + +R1 随两个动态 GeoJSON 持续 build/invalidate。 + +结论: +**DYNAMIC_SOURCE_READINESS_THRASH 仍是 global loaded 事实,但不再误伤无关 retained cache。** + +## 10. T3 permanently unready unrelated source + +`step099-unready-raster-array`: + +- no layer; +- enabled=false; +- isLoaded=false。 + +global: + +```text +renderOrchestratorLoaded=false +openFalseWindowMs=21468 +``` + +约 21.468 秒持续 false。 + +与此同时: + +R0: +- build=1 +- invalidation=19 +- reuse:11 → 1200 +- decision=reuse + +R1: +- build=1 +- invalidation=19 +- reuse:11 → 1200 +- decision=reuse + +因此: + +> global loaded 仍保持旧兼容语义,但 unrelated permanently-unready source 已不能把无关 RETAINED Segment 拖成 style-not-loaded fallback。 + +**硬门槛 PASS。** + +## 11. Mixed fallback + +T4 中 R1 `fallbackCount` 从约 21 增长到 76,而 R0 fallback 计数保持不变,同时 R0 reuse 持续增长。 + +这证明运行期间真实发生了: + +```text +R0 = Reuse +R1 = transient Fallback / rebuild +``` + +而不是只有“两个段都 ready”的理想场景。 + +未观察到 crash / ANR。 + +## 12. Fast Path 边界 + +RDC diff 审计确认未修改: + +- `tryRenderParticleFastFrame` +- `particleFastPathNonCameraInputsMatch` +- `singleContinuousSegment` +- multi-continuous fail-closed +- global Fast Path signature/fade/contentRevision/pending/placement 等 gate。 + +STEP-100 只修 full CPU retained locality。 + +## 13. Native host tests + +Codex 已新增 `RenderSegmentLocality.*` C++ 测试源码。 + +RDC 尝试恢复 `macos-metal` test runner,但 host configure 需要 `libuv`,当前未安装。 + +尝试 `brew install libuv` 时发现 Intel Homebrew 会连带升级: +- CMake +- OpenSSL +- Python +- ca-certificates 等 + +且当前 Intel macOS 已无官方 bottle 支持。为避免污染开发环境,RDC 主动停止。 + +因此: + +- Native host GTest:**ENV BLOCKED / NOT RUN** +- Android C++ full native build:**PASS** +- Android device runtime:**PASS** + +## 14. Jest / iOS + +- Jest:NOT RUN。 +- iOS runtime:本 Step 未运行。 +- V18 unified release:在 STEP-100 Git 收尾后单独执行。 + +## 15. 最终判定 + +STEP-100 的核心实现目标全部成立: + +1. global revision 继续增长; +2. R1 GeoJSON mutation 不再 invalid/rebuild R0; +3. R0 buildDelta=0; +4. R0 invalidationDelta=0; +5. R0 reuseDelta>0; +6. transient unrelated unloaded 不影响 R0 reuse; +7. permanent unrelated unloaded 不影响 R0 reuse; +8. mixed Reuse/Fallback 实际发生; +9. Fast Path 语义未放宽。 + +因此本 Step 功能与 Android runtime 验收: + +**PASS / CLOSED**。 + +下一步允许进入 V18 unified release。 diff --git a/docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md b/docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md new file mode 100644 index 000000000..d0e5d6995 --- /dev/null +++ b/docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md @@ -0,0 +1,184 @@ +# STEP-101 RenderOrchestrator 销毁生命周期修复 Codex 完成报告 v1 + +## 1. 执行结论 + +**PASS(RDC Native / Android runtime):V18 teardown destroyed-mutex 回归已修复并通过设备闭环。** Codex 阶段仅交付源码;随后 RDC 独立完成 Android full Native build、focused C++ TestRunner、STEP-101 T1~T4、20 次真实 renderer reset/recreate、STEP-100 locality 回归与 crash buffer 检查。V19 尚未在本报告阶段公开发布。 + +## 2. Git 基线 + +- 工作区:`/Users/tei/dev/MapLibreParticleLab-step101` +- 分支:`step101-render-orchestrator-teardown` +- start HEAD:`d29e84c1a90daeea104b389d28b000c4f017d08d` +- implementation commit:`64cf8f44350eb8ec4b45360db4d3f9505ac4ac0b` +- implementation message:`fix: make render orchestrator teardown safe` +- push result:**PASS**,`origin/particle/main` 独立回读与 implementation commit 一致。 +- 文档 closeout commit / 最终远端 HEAD:Git 提交无法在自身内容中自指其最终 SHA,按项目既有规则由最终交付回读记录。 +- 子模块:`git submodule status` 列出的子模块均以 `-` 开头,表示当前工作区未初始化;本轮未修改或初始化子模块。 +- V18 Android/RN `13.4.1-particle.18`、iOS `6.28.0-particle.18` 保持不变;V19 **未发布**。 + +## 3. 根因与实现 + +V18 `readinessDiagnosticsMutex` 等成员声明在 `renderSources` 之后,按 C++ 反序析构会先于 sources 消失。RenderSource/Tile 销毁存在同步 `Cancelled` callback 到 Orchestrator 的可达路径,随后可能锁已销毁 mutex。这是已证实的生命周期漏洞,与 Android FORTIFY 报错高度吻合。虽未取得精确到最终 callback 的完整符号化现场栈,但同一设备上 V18 条件稳定复现,而 STEP-101 修复后 T1~T4、20 次 reset、动态 GeoJSON 与 diagnostics 条件均不再崩溃,因此将其作为本次 Android teardown 回归的已确认工程根因。 + +当前源码的析构顺序为:处理 contextLost layers → 第一次 `threadPool.waitForEmpty()` → detach 各 RenderSource、GlyphManager、ImageManager observer → readiness state 仍活着时 `renderSources.clear()` → 第二次 `threadPool.waitForEmpty()` → 常规成员析构。`RenderSource::setObserver(nullptr)` 恢复 NullObserver,tile 回调无需新增空指针分支。显式 detach 与清理建立 owner teardown 合同,不依赖今后成员顺序碰巧安全;没有增加 teardown flag。 + +render-job 队列独立于 tagged scheduler work。静态检查显示 tile/worker 销毁可投递捕获 render data/bucket 的 render jobs;`waitForEmpty()` 不处理它们。OpenGL 同步 Android reset 路径先 `renderer.reset()`、再 `backend.reset()`,其 Context 析构调用 `runRenderJobs(true)`;若启用 async renderer cleanup,backend 的清理时点另由后续生命周期决定。Metal Context 与 Vulkan backend 也有各自的 `runRenderJobs(true)` 清理入口,均未在本轮运行验证。测试显式 `runRenderJobs()`,但没有构建或设备证据证明所有 backend 模式均安全。这是 RDC 必须核对的剩余风险。 + +正常 source remove 使用 `renderSources.erase()`,`clearData()` 使用 `renderSources.clear()`;两者均在 Orchestrator 实体及 readiness state 存活时执行。`TileCache::~TileCache()` 会清理 pending releases 并等待先前 deferred deletion 结束。这个审计没有证成任意外部并发调用都安全,本轮不修改正常 erase/clearData 行为,也不声称所有 source replacement 异步问题已解决。 + +## 4. 实际修改与审计结论 + +源码/测试差异共七个文件: + +1. `src/mbgl/renderer/render_orchestrator.cpp`:显式 detach、source clear 与双 drain。 +2. `src/mbgl/renderer/render_orchestrator.hpp`:仅为 focused test 开放 friend helper。 +3. `src/mbgl/renderer/render_source.cpp`:nullptr observer 回退 NullObserver。 +4. `test/renderer/render_orchestrator_error.test.cpp`:新增两个非 Jest teardown tests;helper 注册 Orchestrator observer,测试断言 source 销毁前 observer 已 detach、Cancelled 不再到旧 observer、第二次 drain 和独立 render-job 队列。 +5. `platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp`:`nativeReset` 与 `resetRenderer` 入口/完成日志,仅用于配对观测,未改变 reset 调用顺序。 +6. `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/renderer/surfaceview/SurfaceViewMapRenderer.java`:SurfaceView detach 与 Java `nativeReset()` 进入/返回日志。 +7. `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt`:T1~T4,tile ready/diagnostics、动态 GeoJSON、Activity recreate、重建与完成日志;保存状态里的空场景不会在配置变更后重新从 Intent 启动。 + +本轮新增/更新文档:本报告、`docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md`、`粒子开发记录.md`。既有未跟踪的 STEP-101 任务书 v1/v2 没有修改。 + +与 v2 合同静态核对:observer、双 drain、render-job 独立审计、四个 Android 场景均有对应代码;STEP-100 locality/fast path/source loaded 语义无本次差异。T2 代码计的是 reset **请求**,20 次 Native reset 必须由 log 中每轮 Java `nativeReset`、JNI `nativeReset`、`MapRenderer::resetRenderer entered/completed` 和新 renderer ready 独立确认,不能凭 UI DONE 计数认定。T3 使用已有 T4 R-C-R fixture 并持续更新动态 GeoJSON;T4 在 reset 前额外取 diagnostics。现有 C++ test 使用最小 tile-backed Source,并非实际 Android source;设备等价验收仍不可省略。 + +## 5. 实际检查与未执行项目 + +- `git diff --check`:**PASS**(已跟踪 diff 的 whitespace/error 静态检查;未跟踪的新文档不在此命令检查范围)。 +- 源码 diff/合同静态审计:完成;补了 observer detach 断言、Java/JNI reset 边界日志、T3 mutation 证据及场景恢复边界。 +- CMake/Ninja/Native compile、C++ GTest:**NOT RUN**。 +- Android Gradle、Validation APK、AAR/RN candidate:**NOT RUN**。 +- ADB、T1~T4、crash buffer、STEP-100 locality 运行态、NavSea 冷启动:**NOT RUN**。 +- Jest、iOS/Metal build 与设备、publish/tag/commit/push:**NOT RUN**。 +- 产物路径、大小、SHA-256、运行日志:本轮没有新产物,均无结果。 + +## 6. RDC 精确验证命令与验收证据 + +在本工作区执行 Mac Native 定向验证(若既有 `libuv` 环境问题仍在,保留首个错误并标为环境阻塞): + +```bash +cmake --preset macos-metal +cmake --build build-macos-metal --target mbgl-test-runner --parallel 3 +build-macos-metal/mbgl-test-runner --gtest_filter='RenderOrchestratorTeardown.*:RenderSegmentLocality.*' +``` + +Android Validation APK 使用固定入口,不手拼 Gradle: + +```bash +./tools/particle_android_validation/build_validation_apk.sh +``` + +RDC 使用显式设备 serial。每个场景先清日志、停止旧 Activity,再单独启动;`T2` 等待 20 轮结束。安装前核对 APK 文件、SHA-256、ABI 与运行设备: + +```bash +adb -s 192.168.200.139:5555 install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk +adb -s 192.168.200.139:5555 logcat -c +adb -s 192.168.200.139:5555 shell am force-stop org.maplibre.android.testapp +adb -s 192.168.200.139:5555 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity --es step101_scenario T2_RESET_LOOP_20 +adb -s 192.168.200.139:5555 logcat -v threadtime -d > /tmp/step101-t2-logcat.txt +rg -n 'STEP101_|SurfaceView detached; nativeReset|nativeReset returned|MapRenderer::nativeReset|MapRenderer::resetRenderer|pthread_mutex_lock called on a destroyed mutex|Fatal signal|ANR' /tmp/step101-t2-logcat.txt +``` + +对 T1/T3/T4 分别重复相同 `logcat -c`、`force-stop`、`am start`、日志保存步骤,将 `step101_scenario` 依次替换为 `T1_RESET_ONCE`、`T3_RESET_WITH_DYNAMIC_GEOJSON`、`T4_RESET_WITH_DIAGNOSTICS`。逐轮配对 `RESET_REQUEST`、`MAPVIEW_DESTROY_ENTER/RETURN`、`SurfaceView detached; nativeReset entered`、JNI `MapRenderer::nativeReset entered/completed`、Native `resetRenderer entered/completed`、新 `STYLE_READY` 与 `NEW_RENDERER_READY`;T2 需至少 20 个完整 reset/recreate 配对,且无 SIGABRT/ANR。T3 核对 `STEP101_DYNAMIC_SOURCE_MUTATION` 在每轮 reset 前持续出现,T4 核对 `PRE_RESET_DIAGNOSTICS` 与 reset 后 diagnostics。STEP-100 T4 R-C-R 另按既有 Validation 菜单复跑,核对无关 R0 `buildDelta=0`、`invalidationDelta=0`、`reuseDelta>0`,动态 R1 正常 build/invalidation。 + +上述只是交接命令,**本轮均未执行**。V19 candidate 必须先于 NavSea 临时锁包和五次超过 30 秒的冷启动;crash buffer 与 locality 均通过后,才可进入不可变发布。NavSea 外部工作区/候选制品命令由 RDC 按其实际环境另行确定,本轮不伪造路径或结果。 + +## 7. 已知问题、未完成事项与下一步 + +静态审计无法证明 Android 每次 Activity recreate 都实际完成 native reset,也无法证明异步 backend 清理和真实 Source 并发场景无 late callback。Native 编译与定向测试、四个 Validation 场景、STEP-100 locality、NavSea 等价五次冷启动、crash buffer 均待 RDC;任何失败均应保留现场并按首个真实错误回传。无理由在本阶段修改 NavSea、发布 V19 或重写 STEP-100 语义。 + + +## 8. RDC 独立验收(2026-10-07) + +### 8.1 Android full Native / Validation APK + +标准入口: + +```text +./tools/particle_android_validation/build_validation_apk.sh +``` + +结果:**PASS**。 + +```text +variant=OpenglDebug +abi=x86_64 +apk_bytes=61957678 +libmaplibre_bytes=39637512 +sha256=14cbd35ed0bc1366389ddea69925901013e3b209ed3575462421a7920c73c721 +``` + +该构建实际编译了本 Step 修改后的 C++ Core、`libmaplibre.so`、JNI/Java reset 日志和 Android Validation T1~T4。 + +### 8.2 STEP-101 Android teardown runtime + +设备:FZG1-2 / Android 13 / API 33 / x86_64 / `192.168.200.139:5555`。 + +- T1 `RESET_ONCE`:真实 `MapRenderer::resetRenderer entered/completed`、`nativeReset entered/completed`、新 renderer `STYLE_READY / NEW_RENDERER_READY`;PASS。 +- T2 `RESET_LOOP_20`:完整运行至 iteration 20,最终 `STEP101_SCENARIO_DONE ... resetsRequested=20`;逐轮出现 destroy/reset/new renderer ready;PASS。 +- T3 `RESET_WITH_DYNAMIC_GEOJSON`:reset 前动态 GeoJSON mutation 持续产生,reset 完成后新 renderer 继续 ready;PASS。 +- T4 `RESET_WITH_DIAGNOSTICS`:reset 前 `getRenderSegmentDiagnostics()` 可读,随后真实 reset 完成并重建;PASS。 +- 四组验收 crash buffer 均为空;未出现 `pthread_mutex_lock called on a destroyed mutex`、SIGABRT 或 ANR。 + +### 8.3 STEP-100 locality 回归 + +R-C-R T4 重新运行 60+ 秒,稳定窗口: + +```text +global revision: 62 -> 894 + +R0 retained: +build 1 -> 1 delta 0 +invalidation 19 -> 19 delta 0 +reuse 12 -> 3890 delta +3878 + +R1 retained: +build / invalidation 持续增长 +``` + +动态 source mutation 仍只影响对应 segment;R0 source revision 保持不变。STEP-101 teardown 修复没有破坏 STEP-100 Source→Segment locality。 + +### 8.4 Focused Android C++ TestRunner + +Mac host `mbgl-test-runner` 仍因本机缺失可用 `libuv` pkg-config 包而 ENV BLOCKED;没有安装或升级系统依赖来掩盖该环境事实。 + +改用仓库已有 Android Native C++ TestRunner,x86_64 build **PASS**。RDC 为 TestRunner 临时打包真实 `test/fixtures/` 到 `data.zip`,仅满足 Android runner 既有解压契约;该临时 asset 已删除,不进入 Git。 + +设备过滤执行: + +```text +RenderSegmentLocality.SourceRevisionsAreScopedAndRecreationUsesNewIdentity PASS +RenderSegmentLocality.ReadinessAndFadeFollowOnlyDependencies PASS +RenderSegmentLocality.MixedExecutionDrawsEachLiveRangeOnceInStyleOrder PASS +RenderSegmentLocality.FailedBuildCannotReuseOldTexture PASS +RenderOrchestratorTeardown.NullObserverAndDetachCoverEveryTileCallback PASS +RenderOrchestratorTeardown.PendingTileCancellationAndSecondDrainRepeatSafely PASS +TestRunner finished with status: '0' +``` + +最终 TestRunner APK(含临时 fixtures,仅作验收证据)SHA-256: + +```text +e1b39b3eb035175b79dd60dbe39496ea982c2dd6e2f1593fd60dd9630f393712 +``` + +crash buffer 为空。 + +首次 TestRunner 编译暴露 6 个测试源码级 C++20 designated-initializer / protected-base conversion 问题;RDC 仅修正测试源码字段补齐和显式转换,未改变生产语义。修复后 Android C++ TestRunner build 与上述 filtered tests 均 PASS。 + +### 8.5 结论 + +STEP-101 的 Native 生命周期修复达到关闭条件: + +1. RenderSource observer 在 owner teardown 前显式 detach; +2. readiness state 仍存活时显式销毁 renderSources; +3. source clear 前后 tagged work 双 drain; +4. glyph/image observer 同步 detach; +5. 20 次真实 Android renderer reset/recreate 无 destroyed-mutex; +6. 动态 GeoJSON、diagnostics 条件下仍安全; +7. STEP-100 locality 保持 PASS; +8. focused C++ tests PASS。 + +Jest:**NOT RUN**。 + +V19 candidate、NavSea 五次冷启动与正式 publish/finalize 属于后续发行 Gate,不改变本 Step Native 修复的 PASS 结论。 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..2799298fb --- /dev/null +++ b/docs/报告/STEP-102_RenderSegment无Placement段局部复用_Codex完成报告_v1.md @@ -0,0 +1,129 @@ +# STEP-102 RenderSegment 无 Placement 段局部复用 Codex 完成报告 v1 + +## 1. 执行结论 + +**代码完成,等待独立构建与运行验证;STEP-102 尚非 PASS。** 已按任务书实现 full CPU placement-free Retained Segment 局部复用、逐段 fallback reason 和实际 main-buffer 执行诊断。C++ 与 Android 测试均只交付源码,未运行。没有编译、设备或视觉结果,不能据此宣称无 stale、Z-order 错乱或 crash。 + +## 2. Git 基线与工作树 + +- 工作区:`/Users/tei/dev/MapLibreParticleLab-step102` +- 当前机器:macOS 15.7.7 / Darwin x86_64;未连接或操作设备。 +- 分支:`step102-placement-free-retained-locality` +- start HEAD:`6df21dac38f072c5ad35b8dfc7ec5217509602bc` +- end HEAD:仍为 `6df21dac38f072c5ad35b8dfc7ec5217509602bc`;本阶段未 commit、push、merge。 +- 子模块:`git submodule status` 均以 `-` 开头,本 worktree 未初始化;未更改。 +- 工作树:仅下列任务文件有本阶段差异;任务开始时已经未跟踪的 v2/v3 审查说明及 STEP-102 任务书未修改、未暂存。 +- V19/V20 release worktree、`particle/main`、tag 与发布坐标未修改;无新 artifact。 + +## 3. 实际修改与新增文件 + +生产代码: + +1. `include/mbgl/renderer/render_segment.hpp`:结构查询、placement gate、全局 live reason 辅助函数及 diagnostics 字段。 +2. `src/mbgl/renderer/render_orchestrator.hpp`、`src/mbgl/renderer/render_orchestrator.cpp`:对 full CPU 提供结构查询和 placement transition 查询;旧动态查询保持原样。 +3. `src/mbgl/renderer/renderer_impl.cpp`:逐段 gate/fallback、全局 live reason、实际执行记录。 +4. `src/mbgl/renderer/retained_underlay_runtime_state.hpp`、`src/mbgl/renderer/render_segment_json.hpp`:只读快照与 JSON 字段。 +5. `packages/maplibre-particle-rn/src/types.ts`:现有 diagnostics 返回类型 additive 扩展。 + +测试/Validation: + +6. `test/renderer/retained_underlay_cache.test.cpp`:新增 `RenderSegmentPlacementFree.*` C++ 测试源码。 +7. `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt`:STEP-102 场景菜单、Intent 入口、轮询及审计日志。 +8. 新增 `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step102PlacementFixture.kt`:离线 R-C-R symbol/placement-free 两组 fixture。 + +文档:新增 `docs/50_Step记录/STEP-102_RenderSegment无Placement段局部复用_v1.md`、本报告;追加 `粒子开发记录.md`。 + +## 4. 实现与任务书逐项对照 + +- **结构能力**:扫描同一 style-layer 元数据中的 `layerType == "symbol"`;越界 range 返回 placement-capable 以 fail-closed。`retainedRangeUsesPlacement()` 的 `needsPlacement()` 动态语义未变。custom/custom-drawable 仍由原 unsupported gate 处理。 +- **逐段 gate**:placement-free 返回通过;placement-bearing 要求 `!placementChanged && symbolFadeChange == 0 && !placementTransitionActive`。`symbolFadeChange==0` 只是沿用保守 gate 的条件,不命名为严格 stable。transition disabled 且 fade=1 仍不能让含 symbol 段复用。 +- **逐段 fallback**:优先明确 source、global resource、placement、layer transition、legacy/unsupported 等本段理由。`static-placement` 只会由本段 gate 失败产生;不再拷贝 aggregate placement/transition 理由给邻段。 +- **full CPU main-buffer**:全局 live 条件只删掉 aggregate `!retainedPlacementStable`,其他既有 3D、debug、非 Continuous MapMode、sprite、global resources、light transition 条件保留。原 `visitRenderSegmentExecution()` 和 Style 顺序不变。camera/viewport/pixelRatio、topology、content/global revision、resource/compositor failure 的原有失效路径未放宽。 +- **兼容/安全边界**:aggregate `retainedStaticUsesPlacement`、`retainedPlacementStable`、prepared-tree/Fast Path eligibility 与 revision 观察继续原用;含 symbol 的 retained 仍 fail-closed;C-001、RN method surface、`setRenderSegmentPlan()` 输入未变。 + +与 v3 审查设计无架构差异。逐段 gate 额外显式查询当前 placement transition,属于对 placement-bearing 的更保守判定;原 `retainedSegmentIsStable()` 内仍保留既有 transition 检查。测试和 Android 场景均不修改 Placement 核心算法。 + +## 5. diagnostics 字段与真实执行语义 + +每段新增 `placementCapable`、`placementGatePassed`、`actualExecution`。前者来自 style-layer 结构,第二项表示本帧 full CPU placement cache gate;Continuous 段的 gate 为非阻塞值。`actualExecution` 在 main-buffer 全局 live 分支或 mixed executor 的 composite 回调实际发生后记录。即使 `cache.finalDecision=reuse`,全局门槛触发时仍记录 `actualExecution=live`。 + +全局新增 `placementChanged`、`symbolFadeChange`、`placementTransitionActive`、`crossSourceCollisions`、`mainBufferExecutionMode`、`globalLiveFallbackReason`。`mixed` 表示进入 Style 顺序的 mixed executor,可以包含某些 Retained live;`global-live` 表示整帧 live 分支。尚未渲染时 `mainBufferExecutionMode/actualExecution=not-rendered`。查询只拷贝已记录快照,不推进 revision;Android/iOS Shared Core 使用同一 JSON writer,RN TS 只补类型。 + +## 6. 测试源码覆盖矩阵 + +| 合同场景 | 新增/现有测试源码 | 尚待运行证明 | +| --- | --- | --- | +| A:R0 free、C live、R1 symbol live;Style 顺序 | `RenderSegmentPlacementFree.MixedExecutionKeepsFreeRangesInStyleOrder` | Android T1 真实 GPU composite/live 与视觉 Z-order | +| B:symbol 当前 placementData 为空仍 capable | `StructuralCapabilityAndConservativeGate` 直接输入 symbol style 结构,不依赖 placementData | Android 空 bucket/placementData 条件仍需设备审计 | +| C:两个 free Retained 复用 | `MixedExecutionKeepsFreeRangesInStyleOrder` 中 R0/R2;Android T2 | 60 秒 build/reuse/invalidation 窗口 | +| D:source-local unloaded/fading/mutation | 既有 `RenderSegmentLocality.ReadinessAndFadeFollowOnlyDependencies` 与本轮两 free cache 断言;Android T1 动态 symbol source | 设备 R0 计数不误增 | +| E:bearing 保守 gate | `StructuralCapabilityAndConservativeGate` changed/fade=1/transition 活跃组合 | transition disabled 场景的 GPU 行为 | +| F:真实全局门槛 | `RealGlobalGatesStillForceGlobalLive`;既有 revision/cache tests | style/context/image/glyph/light 等设备失效 | +| G:拓扑与 key | `TopologyAndCameraChangesCannotReuseOldRetainedKey`;既有 `OldCacheReleaseCannotChargeReplacementAtSameIndex` | 真正 GPU resource 释放及无 stale texture | +| H:reuse 但 global-live | `DiagnosticsRecordActualMainBufferAndDoNotMutateOnRead` | 设备日志中 cache/actual 分离 | + +测试源码只验证可隔离的策略、cache 决策、diagnostics 和 mixed traversal;无法单凭这些单元测试替代真实 renderer/GPU 场景。 + +## 7. Android Validation 场景 + +Test App 的 STEP-093 菜单新增 `STEP-102 PLACEMENT LOCALITY`,也可用 Intent `step102_scenario` 启动: + +- `T1_FREE_SYMBOL`:R0 background/fill(无 symbol)→ C0 持续粒子 → R1 symbol;每 500ms 更新 R1 GeoJSON point,让 placement/source 活跃。 +- `T2_TWO_FREE`:相同 R0/C0,R1 改为 fill;两个 Retained 均结构上 placement-free。 + +每个 `STEP102_SAMPLE` 记录固定 camera、placement 四个全局字段、main-buffer mode/reason、每段 capability/gate/actual、cache decision、build/reuse/invalidation/fallback。tick 8/130 打窗口标记。日志字段用于验收,不在 App 中自行宣告 PASS。 + +## 8. 实际检查与 NOT RUN + +- 已执行:`git status --short`、`git diff`/`rg` 只读审计;`git diff --check` **PASS**。三个新增未跟踪文件另以 `git diff --no-index --check /dev/null <文件>` 检查,无空白错误;该命令因有差异正常退出 1。 +- **NOT RUN**:CMake/Ninja/Bazel configure/build/link、C++ GTest、Android Native TestRunner、Android Gradle、APK/AAR、codegen、Jest、ADB、设备/截图/logcat、iOS/Metal、NavSea、发布、commit、push。 +- 无 APK/AAR、大小、SHA-256、运行日志或最终性能结果。没有编译失败或测试失败的事实;缺少编译/运行证据。 + +## 9. 后续精确验证命令与硬门槛 + +先由 RDC 在本工作区运行 host 定向测试;若当前 Mac `libuv` 环境仍阻塞,应记录首个真实 configure 错误,而非改动依赖: + +```bash +cmake --preset macos-metal +cmake --build build-macos-metal --target mbgl-test-runner --parallel 3 +build-macos-metal/mbgl-test-runner --gtest_filter='RenderSegmentPlacementFree.*:RenderSegmentLocality.*:RenderSegmentDiagnostics.*' +``` + +**Android Validation APK 只由用户手工构建**,使用项目固定入口: + +```bash +cd /Users/tei/dev/MapLibreParticleLab-step102 +./tools/particle_android_validation/build_validation_apk.sh +``` + +构建成功后由 RDC/用户核对 APK ZIP、ABI、`libmaplibre.so`、SHA-256、branch/commit/dirty 状态,再在选定设备上独立运行。下列 `` 需替换为实际 `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 与发行流程。 + +## 11. V21 发行风险豁免记录(2026-10-08) + +- 用户明确要求:V21 必须包含 STEP-102;本次发行不以正式 GTest、Android T1/T2 实机回归、iOS/Metal 运行态验收为前置条件,改由代码审查后执行完整发布流水线。 +- 源码审查基线:`0ce551ad3791a21dbd8507e6cf8c607c270b6eee`。已检查结构性 symbol/Placement 分类、逐段 gate、真实全局 live 门槛、main-buffer 顺序、诊断数据和 Android Validation 场景;`git diff --check` 通过,未发现明确阻断发布的源码问题。 +- 已有构建证据仅限 STEP-102 Metal Core 编译成功(Bazel 642/642),以及临时 helper smoke 10/10。正式 C++ GTest 受 `libuv` 依赖缺失阻塞,Android 与 iOS 运行态验收没有执行。 +- **验收豁免不等于测试 PASS**:STEP-102 不标记 `CLOSED / PASS`,而按“源码审查后接受未验收风险发行 V21”记录。不能声称已经证明运行态无陈旧纹理、Z-order 错乱或崩溃。 +- V21 Pipeline 的 prepare、build、publish、finalize 仍必须全部执行并以真实返回值、artifact 校验和公开回读为准;构建或发行阶段失败不可豁免。 diff --git a/docs/报告/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex完成报告_v1.md b/docs/报告/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex完成报告_v1.md new file mode 100644 index 000000000..4c09b4668 --- /dev/null +++ b/docs/报告/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex完成报告_v1.md @@ -0,0 +1,623 @@ +# UPSTREAM-001 MapLibre 官方升级兼容性架构审计 Codex 完成报告 v1 + +日期:2026-10-09(JST)。任务基线:`630ec657687bad4200c059bbccbb950d9c6f7ce9`。本报告只交付架构审计、三方干跑与后续建议,没有实施升级。 + +## 1. 结论摘要 + +**结论:CONDITIONAL GO。** 可以立项开展隔离迁移;不允许据本报告直接发布升级版本。必须先完成命名空间/接口迁移、Renderer 适配边界与两端单 SDK 收敛,再由 RDC/用户独立构建和运行验收。本次没有编译、运行、设备或发行 PASS。 + +- Particle 的 Field/Ingress/Binding 具备可移植基础,WeatherColor 与 Particle 为独立 RenderLayer;不需要重写引擎。但 GPU 实现在新增的 `render_particle_validation_layer.cpp` 内,仍依赖 MapLibre 内部 RenderTarget、LayerGroup、Heatmap 顶点结构、PaintParameters 和生命周期,不能把“新增文件”解释成“独立 SDK”。 +- 长期维护的主要负担是 `Renderer::Impl` 与 `RenderOrchestrator` 中的缓存调度、Fast Path 输入签名、source/placement 失效及资源所有权。相对初始基线,两个 `.cpp` 分别增加/删除 **632/39**、**505/5** 行,已超过 thin-hook 的含义。 +- 官方目标均发生 `mbgl` → `mln` 目录和 C++ 命名空间迁移。标准 dry-run:Android **76** 个冲突路径(31 content + 44 file location + 1 modify/delete);iOS **78** 个(33 + 44 + 1)。两者退出码均 **1:模拟完成且存在冲突**,不是执行失败,也不是升级成功。 +- `.21` 相对初始基线共 **765** 个变化路径:680 新增、85 修改。两条官方升级线原路径交叉分别 **72/76**,其中可自动合并的交叉路径为 **40/42**。不能把 85、72、76 或旧说法中的“62 个修改文件”当作文本冲突数。 +- 已读官方目标函数定义,确认 `RenderLayer::update()` 新增 `const PaintParameters&`、`PaintParameters` 增加 `renderableSize`、`RenderStaticData::backendSize` 被移除。新增粒子层、Fast Path 和 retained key 的调用必须适配;这是确定的源码接口差异,未执行编译,不能伪称已观察到编译报错。 +- RN 11.5.0 必须与 Native 升级分开验收。当前 RN peer/dev/Podspec 锁 11.3.6,Android 插件强制 13.4.1/`.21`,iOS 借用 MLRN 私有 SPM helper。只升 npm 版本会留下版本约束与 API/二进制风险。 + +## 2. 核对的版本与 SHA + +| 项目 | 本次核实结果 | 证据边界 | +| --- | --- | --- | +| 机器 | `Darwin Mac.lan 24.6.0`,x86_64 | `uname -a`,无设备操作 | +| 工作区 | `/Users/tei/dev/MapLibreParticleLab-upstream-audit-20261009` | 独立审计 worktree | +| 分支 / HEAD | `audit/upstream-native-20261009` / `630ec657687bad4200c059bbccbb950d9c6f7ce9` | 起点与收尾一致 | +| 本地远端跟踪引用 | `origin/particle/main` = 同上 SHA | `git ls-remote origin refs/heads/particle/main` 退出 128,DNS 失败;不能宣称实时远端 HEAD 已回读 | +| 初始 Core | `18545ca10d296704b2cba675b182ff2c8da24588` | HEAD 与两条目标各自的 merge-base 均为此 SHA,退出 0 | +| Android 目标 | `c7506d6aff3800e946f89e1a51432fc22d1a6534` | 本地 commit subject 为 Release MapLibre Android 13.6.1;任务书给定 tag 对应关系 | +| iOS 目标 | `01791ad02c1bfe3b22c42a97619b972aba962f82` | 本地 commit subject 为 Release MapLibre iOS 6.31.0;任务书给定 tag 对应关系 | +| 官方目标相互关系 | 两者 merge-base 为 Android 目标 SHA | Android 目标是 iOS 目标祖先;不能把它们当成两个无关 Core | +| 本地 tags | `android-v13.6.1^{commit}`、`ios-v6.31.0^{commit}` 均退出 128 | tag 引用未安装;以完整 commit SHA 复现,不伪称完成 tag 公网认证 | +| 当前 Android / RN | `13.4.1-particle.21` | `platform/android/VERSION`、RN package.json、release manifest | +| 当前 iOS | `6.28.0-particle.21`,Metal,模块 MapLibre | `release/maplibre-particle/ios/release-manifest.json` | +| `.21` 制品源码 | manifest 的 `sourceCommit=16a5d15f164dc93d220e3fac62de66a9c9527ccc` | 与审计 HEAD/发行记录提交是不同概念;未重新下载或核验制品 | +| 官方 RN | v11.5.0,release 页面短 SHA `059f46b`,2026-10-04 | 没有取得完整 RN tag checkout,不能声称审查了全部 RN 11.5.0 源码 | +| Integration | Mac 现存 `/Users/tei/dev/navsea-particle-integration` HEAD=`80667d6c343411d03a760425438e3d292f452118` | 只读 README/规则/COORD-005/C-001/C-002/状态;未访问 Linux 文件系统或修改合同 | +| Git / submodule | Git 2.54.0;非 shallow;39 个 submodule 均未初始化(`-`) | submodule status 退出 0;不构成构建环境就绪 | + +根目录总则当前路线 v2 仍描述 `.3`,Integration 部分状态仍是旧开发状态,STEP-094 设计仍描述 source 全局失效;以 `.21` 代码与后续 STEP-100/102 实现事实为准,不回写历史文档。已阅读 STEP-098~102 记录及 STEP-094 设计;**本审计基线的 docs 中没有 STEP-103 文件**,因此不把其他 dirty worktree 或临时执行日志中的 STEP-103 当成 `.21` 已有能力。 + +## 3. 方法和数据可信度 + +### 3.1 执行范围 + +读取根规则、项目规范、四份有效总则、架构设计、任务书、相关 Step、实际源码与 base→HEAD diff。未启动子代理、第二个 Codex、后台构建、Jest、Gradle/APK/AAR、Xcode、ADB 或设备步骤。未真实 merge/rebase、未变更分支/index、未修改源码/依赖/构建脚本/Integration。任务书允许的两份文档中,本轮仅新增本报告,任务书保持原样。 + +证据临时目录:`/private/tmp/upstream-001-audit-20261009/`。Git object 写入也隔离于其 `objects/`,原共享对象库仅作只读 alternate。临时目录不是项目交付文件,RDC 应连同 JSONL/stdout/stderr 归档;本报告已嵌入冲突全表、命令与关键哈希,不依赖临时目录才能理解结论。 + +### 3.2 失败与恢复过程 + +| 操作 | 实际退出码 | 事实 | +| --- | --- | --- | +| 三个 `git merge-base HEAD ` | 0 | 均为初始 Core SHA | +| 初次 `git diff --name-status base ` | 各 128 | promisor 补取 `3354d3daaf97803cc42e43758177cb126a7f1671` 时 github.com DNS 失败 | +| 初次标准 `git merge-tree --write-tree --messages HEAD <目标>` | 各 128 | promisor 补取 `0e98150849597096c3fe2f1128b6e89884dcabdb` 时 DNS 失败;stdout 为空,不能计作零冲突 | +| 原对象目录、关闭 rename 的诊断干跑 | 各 128 | `unable to create temporary file: Operation not permitted` / `failure to merge` | +| `git diff --no-renames --name-status base <目标>` | 各 0 | 可得原路径交叉统计,但把移动表示为 D+A | +| 临时可写对象目录、标准 rename 干跑 | 各 1 | 完成,产生 tree 与冲突清单;未要求越过权限或修改原共享对象目录 | +| 同一临时对象目录标准干跑复跑 | 各 1 | 两次 stdout 字节完全一致,tree SHA 相同,stderr 为空 | +| 临时对象目录普通 `git diff --name-status` | 各 0 | rename-aware 清单取得成功 | +| 临时对象目录关闭 rename 的对照干跑 | 各 1 | Android 63、iOS 64 个路径;人为禁用目录迁移识别,**不是主结论** | +| 远端 HEAD / 本地 tag 验证 | 128 | 分别为 DNS 失败 / 本地引用不存在,尚待 RDC | + +失败原始日志保留,没有把后一次完成倒写成首轮成功。成功使用的环境覆盖没有改 Git 配置或合并算法;没有使用 `-Xours`、`-Xtheirs` 或关闭冲突检测。没有证据证明网络后来恢复,不能从本地干跑成功推导网络已通。另有探索性旧路径读取、缺失测试文件名及 glob 查询失败,随后按实际新路径/文件定位;这些不是编译或测试失败。 + +### 3.3 统计定义 + +1. footprint:base→HEAD 的 Git 路径与 numstat,文档/fixture/测试/构建均分开说明。 +2. 原路径交叉:关闭 rename 时两边变更路径集合的交集。Android 23 M+49 D=72;iOS 27 M+49 D=76。49 D 中 48 被 Git 识别为 rename,shader_manifest 为 modify/delete;不是 49 个独立算法冲突。 +3. 冲突路径:标准 merge-tree stdout 第一段 stage 记录的路径去重;不把 stage 1/2/3 三条算三文件,也不靠 `Auto-merging` 文本计数。冲突类型另由 `CONFLICT (...)` 消息统计,恰与本轮路径数一致。 +4. 自动合并交叉路径:交叉集合中,旧路径及其 `mbgl→mln` 对应路径均不在 unmerged 清单者。它仅说明文本可合并;不代表符号、ABI、shader、资源或视觉正确。 +5. 行号默认是审计 HEAD。标注“目标”的 `src/mln/...` 行号来自指定官方 commit blob;审查用 namespace 归一 diff 只写临时文件,**未用归一文本作为正式干跑输入**。 + +## 4. 两条 Native 升级线冲突统计与具体文件 + +| 指标 | Android 13.6.1 | iOS 6.31.0 | +| --- | ---: | ---: | +| 官方 diff(rename-aware)路径数 | 2196 | 2250 | +| A / M / D / R | 91 / 709 / 27 / 1369 | 117 / 719 / 69 / 1345 | +| 官方 diff(no-renames)A / M / D | 1460 / 709 / 1396 | 1462 / 719 / 1414 | +| 与我方原路径交叉 | 72 | 76 | +| 交叉中冲突的官方既有文件 | 32 | 34 | +| 交叉中自动合并的官方既有文件 | 40 | 42 | +| 新增扩展的 file-location 冲突 | 44 | 44 | +| 全部 unmerged 路径 | **76** | **78** | +| content / file location / modify-delete | **31 / 44 / 1** | **33 / 44 / 1** | +| stage 条目数(不可当文件数) | 139 | 145 | +| 标准 dry-run 退出码 | **1** | **1** | +| 结果 tree | `9509597174b70c9eede21d6900b6f54fb092fefa` | `ae31097f803a49be28c27eca6291252baebfa4c0` | + +iOS 比 Android 多出的文本冲突是 `platform/darwin/src/MLNStyleLayerManager.mm` 和 `src/mln/shaders/shader_source.cpp`。虽然目标名标注某个平台,比较对象是整个 Native 仓库,Android 干跑同样可能报告 iOS 文件冲突。 + +完整冲突路径见附录 A;全部 85 个官方修改文件及交叉标记见附录 B。自动合并不能豁免审查,尤其包括 `render_layer.hpp`、`paint_parameters.*`、`render_target.*`、`gfx/context.hpp`、`mtl/offscreen_texture.cpp`、`mtl/texture2d.cpp`。 + +### 4.1 目标源码证据与合并含义 + +| 位置 / 函数 | 已核实差异 | 迁移处理 | +| --- | --- | --- | +| 目标 `src/mln/renderer/render_layer.hpp:159`,`RenderLayer::update` | 新增 `const PaintParameters&` 参数;该官方文件本次文本自动合并 | 同步 Particle/WeatherColor override、声明、普通/hidden cleanup/Fast Path 调用;不能只修改 factory | +| 目标 `src/mln/renderer/renderer_impl.cpp:184`、`:224`、`:302` | 使用 `renderableSize` 构造 PaintParameters,并以其作为 UBO world size | retained viewport/key、scissor、target size 与主画布统一取当前 frame 数据 | +| 目标 `src/mln/renderer/render_static_data.hpp` | 删除 `backendSize`,增加 symbol geometry 方法 | `.21` renderer_impl.cpp:745、:766 等旧引用需适配;不能恢复旧字段掩盖所有权变化 | +| 目标 `src/mln/renderer/render_orchestrator.cpp`,`updateLayers` / `onSymbolError` | 传递新 PaintParameters,并增加 symbol error 转发 | 外提适配器须保留官方 observer 行为,包括 `.21` 自有的 source revision 和 teardown | +| 目标 `src/mln/renderer/render_source.cpp` / `include/mln/style/types.hpp` | 新增 CustomMVTVector 与 source factory 分支 | 合并保留官方新增 source 与自定义 RasterArray,source dependency 不认识的新类型需 fail closed | +| 目标 `src/mln/renderer/render_target.cpp`、`include/mln/gfx/context.hpp` | 审查的归一 diff 无其它差异;本地追加 isRenderable / depth-stencil 接口 | 可保留薄能力接口,仍做 GL/Metal clear、bind、恢复验证 | +| 目标 `src/mln/mtl/render_pass.cpp` | 保存 commandBuffer,并修改 encoder 构造附近代码 | 合并自有 clear/store 与 vertex texture 逻辑时同时保留官方 commandBuffer 生命周期 | +| 目标 `src/mln/mtl/offscreen_texture.cpp` | 官方也已把基类构造参数 `size` 修正为 `size_` | 删除重复维护的修复补丁;保留并单独审查自有 stencil/renderability 扩展 | +| 两条目标 shader_manifest | Git 不识别为 rename,旧头删除与我方追加冲突 | 按新 shader manifest/生成机制注册扩展,不能把旧头直接并排保留 | + +44 个位置冲突解决为“搬到 mln”后,文件内容仍可能保留 `namespace mbgl` / ``。标准结果 tree 中还保留 **25 个 `/mbgl/` 路径**:24 个完全新增的 particle 子树文件及旧 shader_manifest。已直接读取模拟结果,确认搬到 `src/mln/renderer/layers/render_raster_particle_layer.hpp` 的内容仍是旧 namespace/include 和旧 update 签名。因此“解决冲突标记”不是完整升级;新增纯模块也需要机械命名迁移。 + +## 5. Particle 耦合清单 + +### 5.1 可保留的独立模块与实际边界 + +| 代码证据 | 独立性判断 | 适配重点 | +| --- | --- | --- | +| `src/mbgl/particle/field/field_runtime.cpp:173`,FieldSeries::submit;`:223`,resolve | Shared Core 内按 validTime 排序、候选集合原子校验后 swap;exact 时 T0/T1 指向同帧;插值检查批次身份;不依赖 renderer | 迁移 namespace/include,保留 C-002 八帧、拒绝零淘汰与单帧真实时刻语义 | +| `src/mbgl/particle/ingress/qbin_v2_decoder.cpp:75`,EncodedFieldIngress::decodeAndValidate | 字节解码→profile 校验→decoded frame,在 Core;未把 HTTP/provider 放进 renderer | 二进制/数值/错误枚举一致性;不得用升级机会改变 QBIN 公共契约 | +| `src/mbgl/style/sources/raster_array_source.cpp:431`,submitEncodedField | host metadata/contract/raw bytes→Ingress→Store→refreshV4RendererState;不在提交中偷偷改目标时间 | Source::Impl/observer 桥是版本适配层;建议把 Store/Projection 协调留在专用 runtime,不继续扩充官方 Map | +| `src/mbgl/renderer/layers/render_particle_validation_layer.cpp:1175`,prepare | 只消费 RenderRasterArraySource projection,绑定 T0/T1/mix;没有平台专属引擎 | 此 renderer 源文件实际实现正式 RenderRasterParticleLayer,不能按文件名把它当可删除测试 | +| 同文件 `ParticleGpuResources:320`、update:1211、GPU tweaker:524 | state/trail A/B、shader、texture、timeline 与 context 生命周期集中在新增文件;使用 gfx 资源 | 2136 行集成了 fixture/正式路径、GPU pass、时钟和诊断;P1 外提资源与 pass executor,保持 engine 一套 | +| `render_weather_color_layer.cpp:163`、prepare:197、update:229 | 与 Particle 有独立 class/resources,消费同一 projection;scalar/vector magnitude 分离 | 复用 `HeatmapTextureLayoutVertex`/RenderStaticData geometry(:274)是内部布局耦合,宜专用 quad adapter | +| `render_particle_validation_layer.cpp:1141`、:1157、:1223 | layerRemoved 移除注册 target;contextDestroyed 标志;清空 field 时清 drawable、解除 target/tweaker/resource 环 | 跨版本必须保持 render-thread 销毁;不能用任意 UI 线程 delete 或通用 shared_ptr 推断无环 | +| `src/mbgl/layermanager/raster_particle_layer_factory.cpp:19` | 创建专用 RenderRasterParticleLayer;default/Android/Darwin 仅注册类型 | factory 注册、SourceType、source switch 是真正应保留的薄 hook | + +本次口径仅取 `src/` 与 `include/` 下所有 base→HEAD 变化:68 个新增文件、11438 新增行;47 个官方既有文件、2035 新增行/133 删除行。新增文件承载新增行比例 **84.90%**,不是目标“约 95%”;它是可复现的近似工程指标,不是全部平台/测试代码比例,更不能衡量语义独立性。附录 C 列出 68 个新增 Core 文件;另外 612 个新增路径主要为文档、平台/测试、RN、fixture、集成与工具,不可拿它们稀释核心耦合。 + +### 5.2 真正 thin hook、可外提和深耦合 + +- **不可避免的官方接入 hook**:LayerManager 三平台注册、SourceType/RenderSource factory、shader registry/enum/清单、repaint/hidden-source-cleanup 扩展点、Map runtime state 传递、PaintParameters target 边界重置。目标是窄调用和能力描述,不在 hook 中展开业务策略。 +- **可外提的官方修改**:`style/conversion/source.cpp:206` 的 103 行 RasterArray parser 应移到专用 converter;`map/map_impl.cpp:115` 的 RasterArray 类型扫描/forecast tick 应由 source frame participant 适配器承担;JNI `native_map_view.cpp:1192` 的 field/layer 控制实现、MapLibreMap/MLNMapView diagnostics 可委托独立 facade。公开方法与生命周期所有者仍留在正式 SDK,不能以 global map registry 替代。 +- **深耦合必须分步重构**:Renderer::Impl 直接组织段 key/revision/cache 与主 buffer;RenderOrchestrator 同时管理 topology、dependency、source readiness、placement 与快路径;MTL Texture2D::bind 对所有纹理镜像 vertex binding,影响所有官方层,宜改为 shader-stage-aware backend 能力。 +- **通用 backend 修复应独立维护/争取上游化**:offscreen depth/stencil、clear/store、attachment-aware pipeline key。这些不能全塞入 Particle 私有 adapter,否则普通地图 target 仍可能不一致;保留在 gfx 的窄语义接口,并隔离成单独补丁与回归项。 +- **Vulkan 尚不能宣称等价**:现有新增 shader 源及注册明确覆盖 GL/Metal;在 `src/mbgl/vulkan`、`include/mbgl/shaders/vulkan`、`cmake/vulkan.cmake` 未找到 ParticleGpuValidation/RetainedUnderlayComposite 注册。context 的 override 补齐不等于 Vulkan 粒子功能已实现。后续必须验证能力返回/fail-closed 与专用实现,不能继承早期普通 Vulkan 构建 PASS。 + +## 6. RenderSegment 耦合清单 + +| 主题 | `.21` 已实现事实 | 未来兼容约束 / 验收 | +| --- | --- | --- | +| 单一 topology | `render_segment_plan.hpp:32` 解析 explicit/legacy/continuous-range;`:160` 检查完整覆盖;全局 Style index 不重新编号 | boundary 删除、重排、hidden、新插层、非法 JSON 均重解;不能复用旧合法 cache 掩盖非法 plan | +| source→segment | `include/mbgl/renderer/render_segment.hpp:50` 从 Style metadata 统一推导依赖;`:114` RevisionBook;orchestrator:1172 查询 source loaded/fading | shared source 使多个段失效;无 source 的 background 不造依赖;custom/unknown/missing fail closed;remove/recreate 同 id 保留 revision 身份 | +| 分层失效 | renderer_impl:631–676 合并 source revision、layer fingerprint、pixelRatio,逐段 Build/Reuse/Fallback;orchestrator:260 保留全局 revision | source/tile/feature-state 走 source epoch;image/glyph/style/context 等共享事件走 global epoch;不能漏掉新的官方 mutation 入口 | +| loaded 与局部复用 | orchestrator:698 仍是 `styleLoaded && isLoaded()`;full CPU segment gate 用自己的 dependency/readiness | unrelated unloaded source 可阻塞 global loaded/快路径,不能据此禁用无关段的局部复用;也不能改 loaded 语义造 PASS | +| placement | `render_segment.hpp:75` 结构性 symbol 能力;renderer_impl:645–656 按段 gate | 无 symbol 段可在别段 placement 活跃时复用;含 symbol 段仍受 changed/fade/transition 约束;不是 segment-local placement 算法 | +| Fast Path | renderer.cpp:47 尝试快路径;:51 在全帧前释放 prepared tree;renderer_impl:199 起匹配签名/全局内容/拓扑/cache/稳定性;particle_fast_path.hpp:97 逐项 gate | singleContinuous 限制保持;sources/layers/images/light/font 等身份或参数变化必须回全帧;新增上游输入必须纳入签名,不能只检查 camera | +| 全局限制 | orchestrator:1155 的 retainedRangeIsStable 检查所有 source fading;快路径 dynamic state 含 pending/light/placement/非粒子 repaint/annotation | STEP-100/102 的 full CPU 局部复用不等于快路径局部化;多 Continuous 正常执行仍需 full CPU | +| Style / pass 顺序 | renderer_impl:805 构建 retained 段;:880/:904 opaque reverse/translucent forward;保留 layerGroup 原 ordinal 与全局 depthRange;:1005 顺序执行 composite/live | 半透明 fill/line/symbol、跨段遮挡与自定义层必须视觉对照;3D、debug、custom、legacy 等保守退回 live,不可简单按段局部重编号 | +| Top / Bottom | renderer_impl:506–524 按 segmentIndex 创建独立 cache;遇到首个 Continuous 后用 Top 兼容 role;cache:310 透明 clear,:286 禁 depth composite | Top/Bottom 是旧诊断兼容角色,不是只有两张 texture;多段以 segment diagnostics 为准,不能复用 Bottom texture 充当 Top | +| Context / target | paint_parameters.cpp:111 beginRenderTarget 清 stencil ID 和 tile mask;cache:246 depth+stencil offscreen、:310 clear、:320 present | 新目标 renderableSize、Metal attachment 格式/commandBuffer 与 GL FBO/stencil 恢复都要测;context generation 变化不得复用旧 texture | +| 所有权 / teardown | orchestrator:180–209 两次 tagged drain、中间 detach observer 并销毁 sources;render_source.cpp:74 nullObserver;renderer prepared tree 在替换前丢弃引用 | tagged drain 不等于排空独立 render jobs;reset 循环、source cancel 回调、style reload、后台恢复必须独立验证 | + +建议内部抽出 `SegmentPlanner/DependencyTracker`(不持 GPU)、`RetainedSegmentExecutor`(持 cache、执行顺序与 target scope)、`FrameReuseAdapter`(官方 Update/Paint/Placement 输入映射)、`ParticleGpuResources/PassExecutor`(动画资源)。这些是建议职责,不是本轮新建 API。官方文件只传入明确 snapshot/事件并调用;资源生命周期和 Style 顺序必须保留在 renderer 所有权范围,不能外提成第二个 renderer。 + +## 7. RN 11.5.0 适配风险 + +### 7.1 官方信息与未取得材料 + +[RN v11.4.0 官方 release](https://github.com/maplibre/maplibre-react-native/releases/tag/v11.4.0) 明确升级 Android Native 13.6.1、iOS Native 6.31.0;[官方 release 列表](https://github.com/maplibre/maplibre-react-native/releases) 记载 v11.4.1 的 iOS SPM target 挂接修复;[v11.5.0 release](https://github.com/maplibre/maplibre-react-native/releases/tag/v11.5.0) 增加 feature state API。这是 release 证据,不是对 11.5.0 每个构建文件的逐行认证。 + +尝试 raw/GitHub 的 v11.5.0 android/build.gradle、Podspec 及 packages 子路径,返回 404 或 Cache miss,**未取得完整 RN 目标源码**。因此其私有 helper 签名、组件内部布局、Gradle property 名称是否保持均列为待核实,不伪造兼容结论。 + +### 7.2 当前包已核对的问题 + +| 文件与证据 | 风险与建议 | +| --- | --- | +| `packages/maplibre-particle-rn/package.json:34` / devDependencies | MLRN peer/dev 均精确 11.3.6;迁移时与 consumer lockfile 一起更新,经兼容 Gate 后锁定新版本,不能用宽泛 range 掩盖不支持 | +| `app.plugin.js:22–23` | 已有 marker 就提前返回;项目升级若保留旧生成块,可能继续锁旧 `.21`。建议版本化且幂等替换受管块,验证重复 prebuild;本轮没有运行 prebuild | +| `app.plugin.js:23` / `android/build.gradle:17` | substitution 只覆盖 `org.maplibre.gl:android-sdk-opengl`;另直接 implementation 自有 `.21`;nativeVersion=13.4.1、variant=opengl | 必须确认 MLRN 11.5 请求仍被替换,debug/release 每 configuration 仅一个 SDK、每 ABI 一个 libmaplibre.so;若官方改坐标/variant,旧替换可能漏掉。强制旧 SDK 也可能使 RN 新 API 缺失 | +| `MapLibreParticleRN.podspec:10–34` / `:47` | 依赖 MLRN 11.3.6,借 `$MLRN._add_spm_to_target` 与 `$MLRN_SPM_SPEC`,属于私有 helper 耦合 | 检查 v11.4.1 SPM 修复后 helper 的参数/调用顺序与 targets;只复用一套 MapLibre product,不另外加官方 Pod/XCFramework | +| `ios/WeatherRuntimeBridge.mm:103–118` | fresh Fabric registry→`MLRNMapViewComponentView`→contentView→`MLRNMapView`/MLNMapView;按类名识别内部组件 | 新 RN 组件名/包装结构变化即失效;用单独 MLRN adapter 集中适配,保留 stale tag/wrong tag 拒绝 | +| `android/.../WeatherRuntimeBridgeModule.kt:363–369` | Fabric resolveView、强类型 MLRNMapView、view.mapLibreMap | 核实 11.5 Kotlin 类/属性/可见性;attach owner 仍跟随 concrete MapLibreMap,不能用进程全局表绕过销毁 | +| `src/NativeWeatherRuntimeBridge.ts` / `RendererRuntime.ts` / 两端 bridge | typed TS→schema JSON→platform→Core;V4 submit/time、attach/detach、diagnostics | 维持公开签名/错误、JS safe integer 与平台 uint64 边界;codegen 构建由 RDC 后续执行 | +| `.21` Core orchestrator:1015–1046 | set/removeFeatureState 已推进 source revision | RN 11.5 新 feature-state 入口需证明实际命中这一链;不能仅验证方法调用成功,必须观察依赖段刷新与无关段 reuse | + +功能验证必须覆盖:1)单 SDK 依赖树和制品身份;2)Fabric 新旧 map attach/detach、style reload、销毁后调用、20 次 reset;3)V4 单帧向量持续运动、标量无粒子、双帧 mix、八帧滚动、跨批次 unavailable、坏帧零副作用;4)RenderSegment R-C-R/C-R/R-C/多段、连续与 retained 中 feature-state/GeoJSON 更新;5)相机/pitch/wrap/resize、mask、透明度/颜色定量;6)GL 与 iOS Metal context loss/后台恢复和内存趋势。Android/iOS 各自提交日志/截图/计数,不互相替代。 + +## 8. 改造优先级矩阵 + +工作量为相对估计:S=局部少量文件,M=跨数个接口/模块,L=跨执行链和平台 Gate;不是承诺工期。P0 指迁移正确性前置,P1 指长期维护治理,P2 指后续优化。 + +| 级别 / 项目 | 路径与当前耦合证据 | 方案 | 风险 / 工作量 | 验收标准 | Android / iOS | +| --- | --- | --- | --- | --- | --- | +| P0 命名与构建移植 | 68 新增 Core 文件、44 location 冲突、24 particle 文件未随目录移走;附录 A/B/C | 独立机械迁移补丁处理路径/namespace/include/清单;删旧 shader_manifest 接法 | 漏引用/并存 namespace,M | 全部自定义符号单 namespace,生成清单一致;无旧 include、缺失 source 或重复 shader | 共用 Core,双端 | +| P0 帧接口适配 | render_layer.hpp 新 PaintParameters;renderer_impl 旧 backendSize;新增粒子 override | 建立 FrameReuseAdapter,先接全帧,再接 Fast Path;不恢复被官方移除状态 | 错 viewport/override/失效输入,M | 同帧 size/matrix 一致;普通/hidden/Fast 三入口完整;编译与 resize Gate | GL/Vulkan/Metal | +| P0 执行边界 | renderer_impl.cpp:501–1020 +632/-39,orchestrator.cpp +505/-5 | 先外提 segment 决策/执行与官方输入映射,行为保持;策略不继续嵌入官方主函数 | Z-order、placement、stale cache,L | retained on/off 图像对照;global index/pass 顺序;source/placement/资源失败逐项验证 | 双端必测 | +| P0 offscreen / MTL 生命周期 | gfx context overload、paint target reset、mtl render_pass clear 与新 commandBuffer | 窄 offscreen descriptor/target scope,保留官方所有权并独立迁移通用修复 | stencil 污染/格式错配/释放次序,M | GL stencil;Metal clear depth/stencil、pipeline format、device/simulator、context reset | Android GL;iOS/Metal | +| P0 单 SDK 收敛 | RN package/plugin/podspec 与 `.21` implementation | 同时更新支持锁、替换规则、SPM helper adapter 与发行 manifest | 双二进制/旧 API/生成块残留,M | Gradle/Pods/SPM 仅一个 Native;Symbols/headers 来自同一源码 | 两端分别验收 | +| P0 回归证据 | STEP-102 未完整运行验收即纳入 V21;STEP-103 本基线无文件 | 冻结旧版作对照;建立升级候选完整 Gate | 继承未知行为,M | 独立日志、同源制品 SHA、真实设备结果;无测试豁免冒充 PASS | 各平台明确状态 | +| P1 source dependency | render_segment.hpp:50 / RevisionBook;orchestrator:242/1172 | 从 diagnostics DTO 分离 typed dependency snapshot/event adapter,保持 shared/global/unknown | 新 source/feature-state 漏失效,M | 同 source 多段刷新,无关段 reuse,remove/recreate、unknown fail closed | 共用 | +| P1 GPU 模块外提 | render_particle_validation_layer.cpp 2136 行,Heatmap geometry 与资源环 | 专用 quad/资源/pass/lifecycle 模块,RenderLayer 只转发 | 跨 pass commit 与清理退化,M | 同样 state/trail generation、同帧推进一次、clear/reset 无泄漏 | GL/Metal;Vulkan另 Gate | +| P1 backend stage 抽象 | mtl/texture2d.cpp:262 全纹理镜像 vertex bind | 用 shader-stage/binding metadata 描述需要的 stage,通用修复尽量上游化 | 影响普通层 binding/cache,M | 既有地图 shader 与 Particle 都正确;无无效 stage 绑定 | iOS/Metal 主,Core接口共用 | +| P1 公共桥减重 | native_map_view.cpp:1192、MapLibreMap.java、MLNMapView.mm | 专用 Native facade/adapter 持实现,官方类保留方法与 map 生命周期 hook | JNI/ObjC API/销毁顺序,M | 方法签名不变,错误一致,stale tag/destroy callback 验证 | 两端 | +| P1 parser / 时钟薄接入 | style/conversion/source.cpp:206;map_impl.cpp:115 | RasterArray converter 外提;source frame participant tick | clock 改变错误/数据所有权,S–M | submit 不改目标时间;cadence/pause/background 正确 | 共用 | +| P1 Vulkan 能力闭环 | Vulkan 缺本任务 shader 注册证据 | 确认能力与 fail-closed;专用 backend 实现另立项 | 不支持被误报支持,L | Android Vulkan 粒子与 retained 实测;不能只编译普通地图 | Android Vulkan | +| P2 多 Continuous Fast Path | singleContinuousSegment / 全局 signature | 先保留 full CPU;待依赖模型和预算稳定再单独设计 | 跨段 placement/顺序/上传遗漏,L | 多段对照、source mutation/placement 全组合,必要时 RFC | 共用 | +| P2 footprint 预算与上游补丁 | 85 官方修改、47 Core 原文件 | 建 ledger、自动审计、补丁主题拆分、删除已上游化修复 | 日常维护成本,S | 每版本 diff/冲突/功能 gate 有独立可复现证据 | 共用 | + +不是所有 P1 都应阻塞首个候选。Field/Ingress/Binding、专用 layer/factory 和窄注册点可以在机械迁移后直接移植;Renderer 调度与新帧接口、offscreen 生命周期、单 SDK 必须先界定并验证。不得先做多段高级优化再补基础升级验收。 + +## 9. 阶段性迁移与验收 + +### Gate 0:冻结与可回滚准备 + +RDC 回读 origin HEAD,认证两个官方 tag→SHA,归档 `.21` 的源码/manifest/制品;取得 RN 11.5.0 完整源码、11.3.6→11.5.0 diff。统一选择一个 Shared Core 候选:iOS 目标包含 Android 目标,但能否由该 commit 同时发行两端须构建验证,不能据祖先关系直接定版。本轮只给建议,不创建升级分支。 + +回滚点:保留 `.21` 不可变制品与 consumer 锁。任何 Gate 失败,停止推进;候选分支可丢弃,生产分支/旧制品不覆盖。需运行回退时可用既有 map-scoped retained enable 开关关闭优化,另保留完整旧版本回退;不把关闭功能当作新版本通过。 + +### Gate 1:行为保持的边界整理 + +在旧基线单独重构 segment executor / frame adapter / target scope;保持 C-001/C-002、Style 顺序与既有 fail-closed,不顺带提高 Fast Path 支持范围。RDC 完成旧基线等价回归后冻结此检查点。若无法保持行为,停下修复,不混进 namespace 迁移使定位困难。 + +### Gate 2:机械迁移与 Native 三方整合 + +从选定官方目标在隔离候选中按主题移植:Core/字段→类型注册→shader/backend→Renderer adapter→平台 bridge→测试/发行清单。先统一 mln namespace/include,适配 PaintParameters/renderableSize、source enum、symbol observer;处理 shader_manifest 删除和官方已有修复。保留官方新功能,禁止整文件选 ours/theirs。 + +RDC 记录每主题冲突解决、构建日志和 patch footprint;无冲突标记、无未注册新 shader/重复 namespace 后才进入功能 Gate。Android 和 iOS release tag 是同仓库不同发布点,不建立两套 Core fork。 + +### Gate 3:分 backend / 平台验证 + +由 RDC 在目标版本实际支持的 CMake/Bazel target 上配置/编译;本次没有替它运行。可复用旧仓库文档中的 host 路径仅作候选,目标改名后应以配置输出确定 runner,不能硬称 `mbgl-test-runner` 必然保留。已有 runner 的定向过滤器如下(由 RDC 设置真实路径): + +```bash +"$UPSTREAM_AUDIT_TEST_RUNNER" --gtest_filter='Particle*.*:Source.RasterArray*:Retained*.*:ContinuousLayerRange.*:RenderSegment*.*:RenderOrchestratorTeardown.*' +``` + +该变量必须指向同候选源码构建的测试程序。额外执行官方基础地图回归。Android Gradle/APK/安装/ADB/实体机仍按用户手工规则;iOS/Metal host 编译不能替代 iOS 设备。GL、Vulkan、Metal 分别记录结果,不以某 backend 通过覆盖其它 backend。 + +硬门槛:R-C-R 动态 source 只刷依赖段;无关 source 永久 unloaded 时局部 retained 仍可复用;含 symbol / 无 symbol 的 placement 场景正确;global live fallback 不错过图层;feature-state set/remove 使依赖段更新;camera/pitch/wrap/resize/context reset 无旧纹理;20 次 teardown、clear/reload 无回调到已销毁对象;单帧/八帧 C-002 同两端语义;定量 WeatherColor 和统一 mask 不退化。记录实际 build/reuse/invalidation、actualExecution、sourceRevision、fastPath miss reason、日志与图像对照。 + +### Gate 4:RN 与发行 + +确认 Android 每个 debug/release dependency graph 中仅自有一个 Native SDK;由用户手工 Gradle 验证 dependencyInsight 和 APK so 唯一性。iOS 检查 Podfile.lock、Package.resolved、Pods/宿主 target 的同一个 MapLibre SPM product 与私有 helper 兼容。两端 Fabric bridge 功能通过后,再由授权发行任务升版本、build/publish/readback/finalize;不覆盖 `.21`,manifest 绑定同一源码。 + +必须回 Integration RFC/Contract 的情形:公共 API/返回值/错误、attach/detach 与资源所有权、QBIN/profile/time/window/mask 语义、Host/Native 责任边界、Android/iOS 公共行为产生分叉;或希望把多 Continuous/placement 新语义公开为能力。纯 namespace、内部模块外提和行为保持适配不自动要求新 Contract。 + +### 长期跟踪与 patch footprint budget(建议,未修改总则) + +每个官方 release 单独审计 SHA/merge-base、rename-aware diff、三方冲突和自动合并后的行为清单。维护四组补丁:独立扩展、注册 hook、通用 backend 修复、平台/发行;每组有 owner、原因、上游替代情况和回归场景。不能只重放历史 Step 提交而不核对最终 diff。 + +建议预算:新增功能中 ≥95% 实现新增行进入专用模块(排除文档/fixture/generated/tests 的分母);官方函数内 hook 原则上一个调用、每处目标 ≤10 行;超过 30 行策略逻辑须评审外提;一次功能新增官方接点 >3 个须解释;本轮先让 47 个 Core 既有文件的修改规模不增长,重点把 renderer_impl/orchestrator 的新增策略行量减少至少一半,最终数字由行为保持 Gate 核定。预算是预警,不可为达数字拆碎代码或把复杂性藏进宏。 + +长期收益:把下一次官方 renderer 签名变化限制到 adapter/窄 hook;Field/GPU 逻辑继续复用。仍需承担 backend 与 Fabric 私有接口回归费用;“新增文件比例高”无法消除 API/ABI 和行为升级成本。 + +## 10. 阻塞和未知 + +- 本次没有实际编译/测试/设备/发布,所有升级运行验收为 **NOT RUN**;静态审计与 merge-tree 不等于功能 PASS。 +- RN 11.5.0 目标源码未完整取得,私有 SPM helper/Fabric 细节待 RDC 审核;本地 Native tags 与实时 origin HEAD 未认证。 +- `.21` STEP-102 记录明确以源码审查接受部分未跑 Gate 后发行;本轮不将其补写为运行 PASS。STEP-103 不在冻结基线,不能用未合入实现支持结论。 +- Vulkan 粒子/retained 注册缺口、Metal vertex texture 全局绑定、资源环与 target clear 的跨设备行为需专项验证;没有测量内存/FPS,不给性能承诺。 +- 完整 target source 不是全部逐文件审完;重点读取了本报告所列函数与自动合并热点,未下载到的 RN 文件已单列。单次三方模拟只能描述指定 SHA,不能预测所有未来上游版本。 +- Git 收尾待 RDC 验收。未 commit/push,未收到执行器的最终 Git 回执。 + +## 11. 实际模型、退出状态与文件修改清单 + +实际配置来自当前会话 JSONL,不由默认 config.toml 或自称推断: + +```text +文件:/Users/tei/.codex/sessions/2026/10/09/rollout-2026-10-09T08-48-37-01a11deb-3653-73b2-8b2d-43f0e59ccc3a.jsonl +第 1 行:session_meta + id=01a11deb-3653-73b2-8b2d-43f0e59ccc3a + cwd=/Users/tei/dev/MapLibreParticleLab-upstream-audit-20261009 + originator=codex_exec; cli_version=0.161.0; source=exec; model_provider=openai +第 8 行:turn_context,时间 2026-10-08T23:48:46.536Z + model=gpt-6-astra + effort=high + collaboration_mode.settings.model=gpt-6-astra + collaboration_mode.settings.reasoning_effort=high +环境 CODEX_THREAD_ID 与上述 id 一致。 +``` + +模型 Gate 符合 GPT-6 Astra High;未切换模型或推理档位。**本 Codex 执行器进程最终退出码在本轮结束前无法由本进程自证,待 RDC 从外层执行器回执填写。** 不把任意 shell 的 exit 0 或本报告写入成功当成 Codex 全任务退出 0;已观测的 dry-run exit 1、失败 exit 128 和检查退出状态见本报告及末尾收尾记录。 + +实际项目修改:只新增 `docs/报告/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex完成报告_v1.md`。预先存在的未跟踪 `docs/40_任务/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex任务_v1.md` 未修改。没有生成其它项目文档,因此没有更新 Step 或开发记录;本任务明确只允许这两份文档,优先于常规收尾规则。 + +独立验收需要:本报告、下列全路径表/命令/哈希、原始干跑 stdout/stderr、当前 JSONL 开始配置与外层结束回执、工作树 scoped diff、远端 HEAD 回读,以及后续单独授权的构建/设备证据。本轮可交付审计结论,不可交付新 SDK 功能 PASS。 + +## 附录 A:标准干跑完整冲突路径 + +路径为 merge-tree 的 unmerged 路径;`mln` 是模拟目标路径,并未创建到源码工作树。`—` 表示该升级线无此冲突。位置冲突不等于文本冲突。 + +| 路径 | Android | iOS | +| --- | --- | --- | +| `CMakeLists.txt` | 文本 | 文本 | +| `bazel/core.bzl` | 文本 | 文本 | +| `cmake/metal.cmake` | 文本 | 文本 | +| `cmake/opengl.cmake` | 文本 | 文本 | +| `include/mbgl/shaders/shader_manifest.hpp` | 修改/删除 | 修改/删除 | +| `include/mln/layermanager/particle_validation_layer_factory.hpp` | 位置 | 位置 | +| `include/mln/layermanager/raster_particle_layer_factory.hpp` | 位置 | 位置 | +| `include/mln/layermanager/weather_color_layer_factory.hpp` | 位置 | 位置 | +| `include/mln/map/map.hpp` | 文本 | 文本 | +| `include/mln/renderer/layers/weather_color_texture_lifecycle.hpp` | 位置 | 位置 | +| `include/mln/renderer/render_segment.hpp` | 位置 | 位置 | +| `include/mln/renderer/retained_underlay.hpp` | 位置 | 位置 | +| `include/mln/shaders/gl/particle_gpu_validation.hpp` | 位置 | 位置 | +| `include/mln/shaders/gl/retained_underlay_composite.hpp` | 位置 | 位置 | +| `include/mln/shaders/mtl/particle_gpu_validation.hpp` | 位置 | 位置 | +| `include/mln/shaders/mtl/retained_underlay_composite.hpp` | 位置 | 位置 | +| `include/mln/shaders/particle_gpu_validation_ubo.hpp` | 位置 | 位置 | +| `include/mln/style/layers/particle_validation_layer.hpp` | 位置 | 位置 | +| `include/mln/style/layers/raster_particle_layer.hpp` | 位置 | 位置 | +| `include/mln/style/layers/weather_color_layer.hpp` | 位置 | 位置 | +| `include/mln/style/layers/weather_color_stop.hpp` | 位置 | 位置 | +| `include/mln/style/sources/raster_array_source.hpp` | 位置 | 位置 | +| `include/mln/style/types.hpp` | 文本 | 文本 | +| `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp` | 文本 | 文本 | +| `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/FeatureOverviewActivity.kt` | 文本 | 文本 | +| `platform/android/MapLibreAndroidTestApp/src/main/res/values/strings.xml` | 文本 | 文本 | +| `platform/android/VERSION` | 文本 | 文本 | +| `platform/android/buildSrc/src/main/kotlin/maplibre.gradle-publish.gradle.kts` | 文本 | 文本 | +| `platform/darwin/src/MLNStyleLayerManager.mm` | — | 文本 | +| `platform/default/src/mln/layermanager/layer_manager.cpp` | 文本 | 文本 | +| `platform/ios/src/MLNMapView.mm` | 文本 | 文本 | +| `src/mln/gl/offscreen_texture.cpp` | 文本 | 文本 | +| `src/mln/layermanager/particle_validation_layer_factory.cpp` | 位置 | 位置 | +| `src/mln/layermanager/raster_particle_layer_factory.cpp` | 位置 | 位置 | +| `src/mln/layermanager/weather_color_layer_factory.cpp` | 位置 | 位置 | +| `src/mln/map/map_impl.cpp` | 文本 | 文本 | +| `src/mln/map/map_impl.hpp` | 文本 | 文本 | +| `src/mln/mtl/render_pass.cpp` | 文本 | 文本 | +| `src/mln/mtl/renderer_backend.cpp` | 文本 | 文本 | +| `src/mln/renderer/continuous_layer_range.hpp` | 位置 | 位置 | +| `src/mln/renderer/layers/render_particle_validation_layer.cpp` | 位置 | 位置 | +| `src/mln/renderer/layers/render_particle_validation_layer.hpp` | 位置 | 位置 | +| `src/mln/renderer/layers/render_raster_particle_layer.hpp` | 位置 | 位置 | +| `src/mln/renderer/layers/render_weather_color_layer.cpp` | 位置 | 位置 | +| `src/mln/renderer/layers/render_weather_color_layer.hpp` | 位置 | 位置 | +| `src/mln/renderer/particle_fast_path.hpp` | 位置 | 位置 | +| `src/mln/renderer/particle_viewport_reseed.hpp` | 位置 | 位置 | +| `src/mln/renderer/render_orchestrator.cpp` | 文本 | 文本 | +| `src/mln/renderer/render_orchestrator.hpp` | 文本 | 文本 | +| `src/mln/renderer/render_segment_json.hpp` | 位置 | 位置 | +| `src/mln/renderer/render_segment_plan.hpp` | 位置 | 位置 | +| `src/mln/renderer/render_source.cpp` | 文本 | 文本 | +| `src/mln/renderer/renderer.cpp` | 文本 | 文本 | +| `src/mln/renderer/renderer_impl.cpp` | 文本 | 文本 | +| `src/mln/renderer/renderer_impl.hpp` | 文本 | 文本 | +| `src/mln/renderer/retained_underlay_cache.cpp` | 位置 | 位置 | +| `src/mln/renderer/retained_underlay_cache.hpp` | 位置 | 位置 | +| `src/mln/renderer/retained_underlay_runtime_state.hpp` | 位置 | 位置 | +| `src/mln/renderer/sources/render_raster_array_source.hpp` | 位置 | 位置 | +| `src/mln/renderer/update_parameters.hpp` | 文本 | 文本 | +| `src/mln/shaders/mtl/particle_gpu_validation.cpp` | 位置 | 位置 | +| `src/mln/shaders/mtl/retained_underlay_composite.cpp` | 位置 | 位置 | +| `src/mln/shaders/mtl/shader_program.cpp` | 文本 | 文本 | +| `src/mln/shaders/shader_source.cpp` | — | 文本 | +| `src/mln/style/conversion/source.cpp` | 文本 | 文本 | +| `src/mln/style/layers/particle_render_layer_properties.hpp` | 位置 | 位置 | +| `src/mln/style/layers/particle_validation_layer.cpp` | 位置 | 位置 | +| `src/mln/style/layers/particle_validation_layer_impl.hpp` | 位置 | 位置 | +| `src/mln/style/layers/raster_particle_layer.cpp` | 位置 | 位置 | +| `src/mln/style/layers/raster_particle_layer_impl.hpp` | 位置 | 位置 | +| `src/mln/style/layers/weather_color_layer.cpp` | 位置 | 位置 | +| `src/mln/style/layers/weather_color_layer_impl.hpp` | 位置 | 位置 | +| `src/mln/style/sources/raster_array_source.cpp` | 位置 | 位置 | +| `src/mln/style/sources/raster_array_source_impl.hpp` | 位置 | 位置 | +| `test/CMakeLists.txt` | 文本 | 文本 | +| `test/gl/gl_functions.test.cpp` | 文本 | 文本 | +| `test/style/source.test.cpp` | 文本 | 文本 | +| `test/style/style_layer.test.cpp` | 文本 | 文本 | + +## 附录 B:85 个官方既有修改文件逐项清单 + +`+/-` 来自 base→HEAD numstat;行号是首个修改 hunk 的 HEAD 起始行,详细函数解释见正文。主分类不抹除平台桥中的可外提逻辑。A/I:`C` 文本冲突、`D` 修改/删除、`净` 交叉但自动合并、`未交叉` 仅我方变动。旧路径迁移到 mln 后计同一文件。 + +| HEAD 路径:行 | +/- | 主分类 | 当前耦合/处理 | A / I | +| --- | --- | --- | --- | --- | +| `.gitignore:16` | +1/-0 | 构建/平台 | 忽略本地 artifact | 未交叉 / 净 | +| `CMakeLists.txt:183` | +54/-0 | 构建/平台 | 扩展 source/header/shader 清单接入 | C / C | +| `bazel/core.bzl:102` | +67/-0 | 构建/平台 | 扩展 source/header/shader 清单接入 | C / C | +| `cmake/metal.cmake:50` | +4/-0 | 构建/平台 | 扩展 source/header/shader 清单接入 | C / C | +| `cmake/opengl.cmake:40` | +1/-0 | 构建/平台 | 扩展 source/header/shader 清单接入 | C / C | +| `include/mbgl/gfx/context.hpp:85` | +4/-0 | 必要 hook | depth/stencil offscreen 虚接口 | 净 / 净 | +| `include/mbgl/map/map.hpp:17` | +10/-0 | 必要 hook | 公开 retained/segment API | C / C | +| `include/mbgl/mtl/context.hpp:109` | +4/-1 | 必要 hook | depth/stencil virtual override 声明 | 净 / 净 | +| `include/mbgl/mtl/render_pass.hpp:20` | +23/-2 | 深耦合 | 新增 clear helper、vertex texture/sampler 缓存 | 净 / 净 | +| `include/mbgl/renderer/render_target.hpp:37` | +3/-0 | 必要 hook | isRenderable 能力 | 净 / 净 | +| `include/mbgl/shaders/gl/shader_info.hpp:152` | +14/-0 | 必要 hook | 新增 shader/UBO/texture/attribute 注册;生成机制随目标迁移 | 净 / 净 | +| `include/mbgl/shaders/shader_defines.hpp:130` | +33/-0 | 必要 hook | 新增 shader/UBO/texture/attribute 注册;生成机制随目标迁移 | 净 / 净 | +| `include/mbgl/shaders/shader_manifest.hpp:27` | +2/-0 | 必要 hook | 新增 shader/UBO/texture/attribute 注册;生成机制随目标迁移 | D / D | +| `include/mbgl/shaders/shader_source.hpp:33` | +2/-0 | 必要 hook | 新增 shader/UBO/texture/attribute 注册;生成机制随目标迁移 | 净 / 净 | +| `include/mbgl/style/types.hpp:18` | +2/-1 | 必要 hook | RasterArray SourceType | C / C | +| `include/mbgl/vulkan/context.hpp:109` | +4/-1 | 必要 hook | depth/stencil virtual override 声明 | 净 / 净 | +| `include/mbgl/webgpu/context.hpp:46` | +4/-1 | 必要 hook | depth/stencil virtual override 声明 | 净 / 净 | +| `platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp:198` | +4/-0 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 净 / 净 | +| `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp:6` | +453/-0 | 构建/平台 | JNI field/layer/segment 实现可外提,官方类保留注册/生命周期 | C / C | +| `platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp:204` | +49/-0 | 构建/平台 | JNI facade 方法与签名 | 净 / 净 | +| `platform/android/MapLibreAndroid/src/cpp/style/layers/layer_manager.cpp:22` | +7/-0 | 构建/平台 | Android Core-only factory 薄注册 | 净 / 净 | +| `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java:15` | +243/-0 | 构建/平台 | runtime ownership、destroy listener、公开 facade,可外提实现 | 未交叉 / 未交叉 | +| `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java:142` | +60/-0 | 构建/平台 | Native 内部接口表 | 未交叉 / 未交叉 | +| `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java:656` | +221/-0 | 构建/平台 | state guard 与 Native JNI 转发 | 净 / 净 | +| `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/renderer/surfaceview/SurfaceViewMapRenderer.java:4` | +3/-0 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 未交叉 / 未交叉 | +| `platform/android/MapLibreAndroidTestApp/build.gradle.kts:19` | +6/-1 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 净 / 净 | +| `platform/android/MapLibreAndroidTestApp/src/main/AndroidManifest.xml:289` | +13/-0 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 净 / 净 | +| `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/FeatureOverviewActivity.kt:11` | +12/-0 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | C / C | +| `platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_feature_overview.xml:8` | +12/-1 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 未交叉 / 未交叉 | +| `platform/android/MapLibreAndroidTestApp/src/main/res/values/descriptions.xml:4` | +1/-0 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 净 / 净 | +| `platform/android/MapLibreAndroidTestApp/src/main/res/values/strings.xml:11` | +45/-0 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | C / C | +| `platform/android/MapLibreAndroidTestApp/src/main/res/values/titles.xml:28` | +1/-0 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 净 / 净 | +| `platform/android/VERSION:1` | +1/-1 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | C / C | +| `platform/android/buildSrc/src/main/kotlin/maplibre.artifact-settings.gradle.kts:1` | +6/-6 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 未交叉 / 未交叉 | +| `platform/android/buildSrc/src/main/kotlin/maplibre.gradle-publish.gradle.kts:7` | +12/-0 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | C / C | +| `platform/darwin/src/MLNStyleLayerManager.mm:17` | +7/-0 | 构建/平台 | Darwin factory 薄注册 | 净 / C | +| `platform/default/src/mbgl/layermanager/layer_manager.cpp:13` | +7/-0 | 必要 hook | 注册三种新增 LayerFactory | C / C | +| `platform/default/src/mbgl/storage/offline_download.cpp:219` | +8/-0 | 必要 hook | RasterArray 无 offline tileset,switch 明确忽略 | 净 / 净 | +| `platform/ios/BUILD.bazel:152` | +0/-4 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 未交叉 / 未交叉 | +| `platform/ios/app/CMakeLists.txt:27` | +31/-0 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 未交叉 / 未交叉 | +| `platform/ios/app/MBXViewController.mm:19` | +741/-2 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 未交叉 / 净 | +| `platform/ios/bazel/files.bzl:8` | +2/-0 | 构建/平台 | 平台构建/发行/验证 App 或日志配套 | 未交叉 / 未交叉 | +| `platform/ios/src/MLNMapView.h:31` | +64/-0 | 构建/平台 | 公开诊断与 segment 方法 | 未交叉 / 未交叉 | +| `platform/ios/src/MLNMapView.mm:8` | +125/-0 | 构建/平台 | diagnostics ObjC 对象/JSON/Map 转发可委托专用 adapter | C / C | +| `platform/ios/src/Mapbox.h:45` | +1/-0 | 构建/平台 | 导出 MLNParticleFieldRuntime | 未交叉 / 净 | +| `shaders/manifest.json:147` | +14/-0 | 构建/平台 | 扩展 source/header/shader 清单接入 | 未交叉 / 净 | +| `src/mbgl/annotation/annotation_manager.cpp:224` | +6/-0 | 必要 hook | hasPendingChanges 持锁读取 dirty,快路径观察 | 净 / 净 | +| `src/mbgl/annotation/annotation_manager.hpp:45` | +1/-0 | 必要 hook | hasPendingChanges 声明 | 净 / 净 | +| `src/mbgl/gl/context.cpp:439` | +27/-1 | 可外提 | 通用 depth/stencil FBO 能力,独立 backend 补丁/上游化 | 净 / 净 | +| `src/mbgl/gl/context.hpp:137` | +6/-0 | 必要 hook | 通用 offscreen/FBO 声明 | 净 / 净 | +| `src/mbgl/gl/offscreen_texture.cpp:11` | +19/-4 | 可外提 | 通用 attachment 实现,独立 backend 补丁 | C / C | +| `src/mbgl/gl/offscreen_texture.hpp:15` | +3/-1 | 必要 hook | depth/stencil 构造参数 | 净 / 净 | +| `src/mbgl/gl/renderer_backend.cpp:138` | +2/-0 | 必要 hook | 新增 shader/UBO/texture/attribute 注册;生成机制随目标迁移 | 净 / 净 | +| `src/mbgl/map/map.cpp:97` | +32/-0 | 必要 hook | 公开配置/查询委托 runtime 并 onUpdate | 净 / 净 | +| `src/mbgl/map/map_impl.cpp:6` | +10/-0 | 可外提 | 直接识别 RasterArray 并 tickForecastClock | C / C | +| `src/mbgl/map/map_impl.hpp:16` | +3/-0 | 必要 hook | map-scoped runtime state 所有权 | C / C | +| `src/mbgl/mtl/offscreen_texture.cpp:8` | +60/-10 | 可外提 | 通用 attachment/renderability 修复;官方已有 size_ 修复 | 净 / 净 | +| `src/mbgl/mtl/render_pass.cpp:9` | +61/-13 | 深耦合 | clear/store/encoder/vertex binding 与官方 commandBuffer 合并 | C / C | +| `src/mbgl/mtl/renderer_backend.cpp:30` | +4/-0 | 必要 hook | 新增 shader/UBO/texture/attribute 注册;生成机制随目标迁移 | C / C | +| `src/mbgl/mtl/texture2d.cpp:260` | +4/-0 | 深耦合 | 所有 Texture2D::bind 镜像 vertex stage | 净 / 净 | +| `src/mbgl/renderer/layers/render_location_indicator_layer.hpp:47` | +1/-0 | 必要 hook | legacy rendering 分类 | 净 / 净 | +| `src/mbgl/renderer/paint_parameters.cpp:111` | +8/-0 | 必要 hook | beginRenderTarget 清 stencil id/mask cache | 净 / 净 | +| `src/mbgl/renderer/paint_parameters.hpp:115` | +3/-0 | 必要 hook | target boundary 声明 | 净 / 净 | +| `src/mbgl/renderer/render_layer.hpp:109` | +21/-0 | 必要 hook | repaint/粒子能力/legacy/hidden cleanup 默认虚函数 | 净 / 净 | +| `src/mbgl/renderer/render_orchestrator.cpp:2` | +505/-5 | 深耦合 | 依赖/revision/placement/readiness/teardown 与官方调度交织 | C / C | +| `src/mbgl/renderer/render_orchestrator.hpp:18` | +184/-1 | 深耦合 | 上述状态、事件、遍历方法与 mutex | C / C | +| `src/mbgl/renderer/render_source.cpp:11` | +5/-1 | 必要 hook | RasterArray factory 与 nullObserver 恢复 | C / C | +| `src/mbgl/renderer/render_source.hpp:14` | +21/-0 | 可外提 | readiness DTO/诊断方法与正式 source 类相连 | 净 / 净 | +| `src/mbgl/renderer/render_target.cpp:24` | +5/-3 | 必要 hook | isRenderable 与 target boundary | 净 / 净 | +| `src/mbgl/renderer/render_tree.hpp:102` | +11/-0 | 必要 hook | prepared tree 的快帧时间与 repaint 更新 | 净 / 净 | +| `src/mbgl/renderer/renderer.cpp:27` | +14/-1 | 必要 hook | 先快帧、全帧前释放引用树、提交 prepared tree | C / C | +| `src/mbgl/renderer/renderer_impl.cpp:4` | +632/-39 | 深耦合 | segment 决策/资源/执行与 Fast Path 主控制链 | C / C | +| `src/mbgl/renderer/renderer_impl.hpp:4` | +63/-1 | 深耦合 | cache/prepared tree、UpdateParameters 镜像签名所有权 | C / C | +| `src/mbgl/renderer/sources/render_tile_source.cpp:489` | +17/-0 | 可外提 | tilePyramid 计数诊断,可委托 source adapter | 净 / 净 | +| `src/mbgl/renderer/sources/render_tile_source.hpp:26` | +1/-0 | 必要 hook | readiness 查询 override | 净 / 净 | +| `src/mbgl/renderer/update_parameters.hpp:12` | +4/-0 | 必要 hook | 传递 map-scoped retained runtime state | C / C | +| `src/mbgl/shaders/gl/shader_info.cpp:290` | +27/-0 | 必要 hook | 新增 shader/UBO/texture/attribute 注册;生成机制随目标迁移 | 净 / 净 | +| `src/mbgl/shaders/mtl/shader_program.cpp:12` | +22/-9 | 可外提 | pipeline key 纳入 attachment format,独立通用修复 | C / C | +| `src/mbgl/shaders/shader_source.cpp:9` | +40/-37 | 必要 hook | 新增 shader/UBO/texture/attribute 注册;生成机制随目标迁移 | 净 / C | +| `src/mbgl/style/conversion/source.cpp:13` | +106/-0 | 可外提 | 103 行 RasterArray JSON parser 可独立 converter | C / C | +| `src/mbgl/style/types.cpp:15` | +2/-1 | 必要 hook | raster-array 字符串映射 | 净 / 净 | +| `test/CMakeLists.txt:21` | +16/-0 | 测试配套 | 新增类型/生命周期测试或平台条件;本轮未运行 | C / C | +| `test/gl/gl_functions.test.cpp:7` | +11/-0 | 测试配套 | 新增类型/生命周期测试或平台条件;本轮未运行 | C / C | +| `test/style/source.test.cpp:15` | +883/-0 | 测试配套 | 新增类型/生命周期测试或平台条件;本轮未运行 | C / C | +| `test/style/style_layer.test.cpp:19` | +106/-0 | 测试配套 | 新增类型/生命周期测试或平台条件;本轮未运行 | C / C | + +分类合计:构建/平台 32;必要 hook 34;深耦合 7;可外提 8;测试配套 4。各类相加为 85;分类为审计判断,不代表冲突严重性排序。 + +## 附录 C:68 个纯新增 Core 扩展文件 + +“纯新增”是 Git 身份,独立程度仍按正文函数审计;GPU/RenderSegment 新文件也存在官方内部 API 耦合。以下为完整 `src/`、`include/` 新增清单(24 个 particle 子树文件、其余 44 个 layer/source/renderer/shader 等文件)。 + +```text +include/mbgl/layermanager/particle_validation_layer_factory.hpp +include/mbgl/layermanager/raster_particle_layer_factory.hpp +include/mbgl/layermanager/weather_color_layer_factory.hpp +include/mbgl/particle/field/field_binding.hpp +include/mbgl/particle/field/field_data_adapter.hpp +include/mbgl/particle/field/field_profile_contract.hpp +include/mbgl/particle/field/field_profile_contract_json.hpp +include/mbgl/particle/field/field_profile_validator.hpp +include/mbgl/particle/field/field_projection_materialization.hpp +include/mbgl/particle/field/field_runtime.hpp +include/mbgl/particle/field/local_particle_field_feed.hpp +include/mbgl/particle/field/particle_gpu_math.hpp +include/mbgl/particle/ingress/encoded_field_transport.hpp +include/mbgl/particle/ingress/qbin_v2_decoder.hpp +include/mbgl/particle/ingress/qbin_v3_decoder.hpp +include/mbgl/particle/renderer_debug_counters.hpp +include/mbgl/renderer/layers/weather_color_texture_lifecycle.hpp +include/mbgl/renderer/render_segment.hpp +include/mbgl/renderer/retained_underlay.hpp +include/mbgl/shaders/gl/particle_gpu_validation.hpp +include/mbgl/shaders/gl/retained_underlay_composite.hpp +include/mbgl/shaders/mtl/particle_gpu_validation.hpp +include/mbgl/shaders/mtl/retained_underlay_composite.hpp +include/mbgl/shaders/particle_gpu_validation_ubo.hpp +include/mbgl/style/layers/particle_validation_layer.hpp +include/mbgl/style/layers/raster_particle_layer.hpp +include/mbgl/style/layers/weather_color_layer.hpp +include/mbgl/style/layers/weather_color_stop.hpp +include/mbgl/style/sources/raster_array_source.hpp +src/mbgl/layermanager/particle_validation_layer_factory.cpp +src/mbgl/layermanager/raster_particle_layer_factory.cpp +src/mbgl/layermanager/weather_color_layer_factory.cpp +src/mbgl/particle/field/field_binding.cpp +src/mbgl/particle/field/field_data_adapter.cpp +src/mbgl/particle/field/field_profile_contract_json.cpp +src/mbgl/particle/field/field_profile_validator.cpp +src/mbgl/particle/field/field_projection_materialization.cpp +src/mbgl/particle/field/field_runtime.cpp +src/mbgl/particle/field/local_particle_field_feed.cpp +src/mbgl/particle/field/offline_fixture_particle_adapter.cpp +src/mbgl/particle/field/offline_fixture_particle_adapter.hpp +src/mbgl/particle/ingress/qbin_v2_decoder.cpp +src/mbgl/particle/ingress/qbin_v3_decoder.cpp +src/mbgl/renderer/continuous_layer_range.hpp +src/mbgl/renderer/layers/render_particle_validation_layer.cpp +src/mbgl/renderer/layers/render_particle_validation_layer.hpp +src/mbgl/renderer/layers/render_raster_particle_layer.hpp +src/mbgl/renderer/layers/render_weather_color_layer.cpp +src/mbgl/renderer/layers/render_weather_color_layer.hpp +src/mbgl/renderer/particle_fast_path.hpp +src/mbgl/renderer/particle_viewport_reseed.hpp +src/mbgl/renderer/render_segment_json.hpp +src/mbgl/renderer/render_segment_plan.hpp +src/mbgl/renderer/retained_underlay_cache.cpp +src/mbgl/renderer/retained_underlay_cache.hpp +src/mbgl/renderer/retained_underlay_runtime_state.hpp +src/mbgl/renderer/sources/render_raster_array_source.hpp +src/mbgl/shaders/mtl/particle_gpu_validation.cpp +src/mbgl/shaders/mtl/retained_underlay_composite.cpp +src/mbgl/style/layers/particle_render_layer_properties.hpp +src/mbgl/style/layers/particle_validation_layer.cpp +src/mbgl/style/layers/particle_validation_layer_impl.hpp +src/mbgl/style/layers/raster_particle_layer.cpp +src/mbgl/style/layers/raster_particle_layer_impl.hpp +src/mbgl/style/layers/weather_color_layer.cpp +src/mbgl/style/layers/weather_color_layer_impl.hpp +src/mbgl/style/sources/raster_array_source.cpp +src/mbgl/style/sources/raster_array_source_impl.hpp +``` + +## 附录 D:可复现命令、退出状态与证据摘要 + +以下 Python 仅执行 Git 读取和 merge-tree 干跑,并将输出/object 写入新临时目录,不执行真实 merge/rebase 或更新源码/index/ref。在同工作区与 Git 2.54.0 下运行;若原对象不足/网络不可用,保留退出码并报告阻塞,不将空 stdout 算零冲突。本轮未设置 merge.renames/directoryRenames/renameLimit 或 diff.renames/renameLimit 覆盖;检查这些配置未发现显式条目。 + +```python +import os, pathlib, subprocess, tempfile, json, re, collections, hashlib +BASE = "18545ca10d296704b2cba675b182ff2c8da24588" +OURS = "630ec657687bad4200c059bbccbb950d9c6f7ce9" +TARGETS = { + "android": "c7506d6aff3800e946f89e1a51432fc22d1a6534", + "ios": "01791ad02c1bfe3b22c42a97619b972aba962f82", +} +root = pathlib.Path(tempfile.mkdtemp(prefix="upstream-001-reproduce-")) +(root / "objects").mkdir() +original = subprocess.check_output([ + "git", "rev-parse", "--path-format=absolute", "--git-path", "objects" +], text=True).strip() +env = dict(os.environ, GIT_OBJECT_DIRECTORY=str(root / "objects"), + GIT_ALTERNATE_OBJECT_DIRECTORIES=original) +def run(name, args): + r = subprocess.run(["git", *args], capture_output=True, env=env) + (root / (name + ".stdout")).write_bytes(r.stdout) + (root / (name + ".stderr")).write_bytes(r.stderr) + print(name, "exit", r.returncode, + "stdout-sha256", hashlib.sha256(r.stdout).hexdigest()) + return r +def paths(r): + if r.returncode != 0: + raise RuntimeError("路径 diff 未完成,停止计数") + return {line.split("\t")[-1] for line in r.stdout.decode().splitlines()} +ours = paths(run("ours-paths", ["diff", "--no-renames", "--name-status", BASE, OURS])) +for label, target in TARGETS.items(): + run(label + "-base", ["merge-base", OURS, target]) + run(label + "-diff", ["diff", "--name-status", BASE, target]) + upstream = paths(run(label + "-paths", ["diff", "--no-renames", "--name-status", BASE, target])) + overlap = ours & upstream + r = run(label + "-merge", ["merge-tree", "--write-tree", "--messages", OURS, target]) + if r.returncode not in (0, 1) or not r.stdout: + print(label, "BLOCKED,冲突数未知") + continue + lines = r.stdout.decode().splitlines() + unmerged = {m.group(1) for line in lines + if (m := re.match(r"[0-7]{6} [0-9a-f]+ [123]\t(.+)$", line))} + kinds = collections.Counter(m.group(1) for line in lines + if (m := re.match(r"CONFLICT \(([^)]+)\)", line))) + clean = {p for p in overlap if p not in unmerged + and p.replace("/mbgl/", "/mln/") not in unmerged} + print(label, "tree", lines[0], "intersection", len(overlap), + "unmerged", len(unmerged), "types", dict(kinds), "clean-overlap", len(clean)) + (root / (label + "-conflict-paths.txt")).write_text("\n".join(sorted(unmerged)) + "\n") +print("证据目录", root) +``` + +本轮临时对象库:`/private/tmp/upstream-001-audit-20261009/objects`;只读 alternate:`/Users/tei/dev/MapLibreParticleLab/.git/objects`。这是审计 worktree 原本共享的 Git object 来源,未操作其它工作树源码或状态。 + +| 证据文件(临时目录内) | SHA-256 | 说明 | +| --- | --- | --- | +| `ours-no-renames.txt` | `7e63240f4460f8c502dfec535b9fcbb0f11872ae221b87b550d68a05bb70bf48` | Git 路径证据 | +| `android-rename-aware.txt` | `fa42160229b3407eeaa13fd90d0dfb9c4d7425e8f0dfa860b01db1ff7f18984c` | Git 路径证据 | +| `ios-rename-aware.txt` | `e836b7699dc32c8300f8b0bfdcad16be17eeb526c48057d9a16cfa7734f32e61` | Git 路径证据 | +| `android-isolated-default.stdout` | `3526b74fc8152e61f72641d571c16e9055500deb1cd31540f2eacf26047b4fb6` | 标准干跑,exit 1;复跑一致 | +| `ios-isolated-default.stdout` | `d553091afaec4f381991bfbf6e60cd0d887177d5bccd172fac4f46e70f347261` | 标准干跑,exit 1;复跑一致 | +| `android-conflict-paths.txt` | `4c61654d97bee7bca76879d9b828db8ca5c1d4a0610e3cfb6d43edeb73029039` | Git 路径证据 | +| `ios-conflict-paths.txt` | `013ebe7121eaea79bf8491a1e3a55198e87acc7865b7b27bda6fdf81da993235` | Git 路径证据 | + +首轮失败见 `android/ios-diff.stderr`、`android/ios-merge.stderr`;原目录权限失败见 `android/ios-merge-no-renames.stderr`;成功命令与 rc 见对应 `.json`;复跑比对见 `android/ios-repeat.json`。对照禁用 rename 的 tree 为 Android `59de9836b81e90c3fef4a5a0377454bd464f5ef9`、iOS `b419b471131eb4fa3244e5cc3082ee7f2bd8e1e9`,仅用于证明 rename 口径影响,不用于迁移决策。 + +已观察退出状态:标准干跑两线均 1(冲突),路径 diff/merge-base 0;原始失败 128;报告最终静态检查见下节。Codex 进程最终状态和 Git 发布回执由外层执行器/RDC 独立补证。 + +## 附录 E:最终静态收尾记录 + +- 报告内复现脚本已原样提取并执行,脚本退出 **0**;其中两次 merge-tree 各退出 **1**,Android 76 / iOS 78 个路径与正文完全一致。该脚本退出 0 只表示采集/计数完成,不是升级 PASS。复现临时目录 `/tmp/upstream-001-reproduce-58mf60um`,采集日志位于主证据目录 `embedded-reproduce.stdout/.stderr`。 +- `git diff --check`:退出 **0**,无输出;tracked 与 staged 的 `git diff --name-only` 均为空,退出 0。 +- 未跟踪报告另用 `git diff --no-index --check /dev/null <报告>` 检查:退出 **1**,stdout/stderr 均为空;这里 1 表示新增文件有差异,没有空白错误,不解释为测试失败或运行成功。 +- 报告要求的 11 个章节齐全,Markdown 围栏闭合;完整本地路径:行引用的文件均存在。冲突附录为 78 个并集路径,官方修改清单 85 个,新增 Core 清单 68 个。 +- 收尾 HEAD 仍为 `630ec657687bad4200c059bbccbb950d9c6f7ce9`;工作树只有以下两份未跟踪文档,没有源码或已跟踪文件改动: + +```text +?? docs/40_任务/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex任务_v1.md +?? docs/报告/UPSTREAM-001_MapLibre官方升级兼容性架构审计_Codex完成报告_v1.md +``` + +任务书 SHA-256:`2e2ba4b5b1e94bedf0846cea616611eab758467a3e9ffaf01a4c8691fc01c112`。本轮没有编辑任务书,未 commit/push。最终外层 Codex 进程退出码仍待执行器回执,**Git 收尾待 RDC 验收**。 + +## 附录 F:RDC Git 收尾回执(2026-10-09 JST) + +本节为 Codex 完成后 RDC 独立验收补记;前述“Git 收尾待 RDC 验收”与“未 commit/push”均指 Codex 自身退出时的阶段事实,不再代表本附录写入后的状态。 + +- 审计结果:**PASS(仅只读架构审计与文档核验)**。目标升级结论仍为 **CONDITIONAL GO**,任何 Native/RN 构建、设备运行与发行均 **NOT RUN**。 +- Codex 实际模型:**gpt-6-astra / high**;JSONL `turn.completed`,外部执行器退出码 **0**。 +- 独立验收:报告 12 个必须章节存在;Android 76、iOS 78 条冲突复核一致;`git diff --check` 通过。 +- 功能文档提交:`2b4075b60ba7c7265e39e18168522bf7089f3a60`。 +- Commit message:`docs: audit upstream MapLibre Native upgrade compatibility`。 +- 推送结果:**PASS**;`origin/particle/main` 从 `630ec657687bad4200c059bbccbb950d9c6f7ce9` 快进至上述文档提交,远端 SHA 回读一致。 +- 本次补记作为后续独立文档提交推送;其最终 SHA 以远端 `origin/particle/main` 回读为准,不在提交内容中自引用。 +- 本次没有修改任何产品源码、测试实现或用户原有 Mac 工作树。 diff --git a/docs/报告/V15_iOS_WeatherColor纹理生命周期修复_Codex完成报告_v1.md b/docs/报告/V15_iOS_WeatherColor纹理生命周期修复_Codex完成报告_v1.md new file mode 100644 index 000000000..1dc8465f3 --- /dev/null +++ b/docs/报告/V15_iOS_WeatherColor纹理生命周期修复_Codex完成报告_v1.md @@ -0,0 +1,92 @@ +# V15 iOS WeatherColor 纹理生命周期修复 Codex 完成报告 v1 + +日期:2026-09-27(JST) +结论:**IMPLEMENTED / RDC HOST VALIDATION PASS / WAITING FOR MANUAL IPAD VALIDATION**。源码修复与测试代码已交付;Codex 未运行构建或测试,RDC 独立完成 OpenGL/Metal host 验证。不能据此标记 iPad PASS。 + +## 1. 执行配置与 Git 基线 + +- 实际执行模型与档位:**GPT-6 Luna High**(按本轮用户指定记录;未切换或升级)。 +- 仓库:`/Users/tei/dev/MapLibreParticleLab`;机器:Mac mini,Darwin 24.6.0,x86_64。 +- 分支:`particle/main`;开工/当前 HEAD:`cd71640a84901212ce4681e296f027befdff4ee0`。 +- V15 tag `maplibre-particle/rn/13.4.1-particle.15` peeled commit 与 HEAD 相同;`origin/particle/main` 与 HEAD 相同。 +- manifest 中的 V15 `sourceCommit` 是 `1fdc5df625d405233703136eb2dc12032d932da2`;后续 finalize release 生成的 V15 tag 指向 HEAD `cd71640a84901212ce4681e296f027befdff4ee0`。两者角色不同且本任务相关已跟踪源码相同,因此不存在基线冲突。任务书保留原样,tag/发行内容未修改。 +- 开工工作树只存在用户提供且未跟踪的 V15 任务书;本轮始终保留该文件且未纳入提交。源码阶段没有执行 reset/clean,也未更改版本/tag/发行包。 +- `platform/android/VERSION` 为 `13.4.1-particle.15`;`platform/ios/VERSION` 当前内容为 `6.28.0`。本轮未改版本坐标。 + +## 2. 根因链和修复 + +源码复核确认:`RenderWeatherColorLayer::update()` 上传/替换 Resources 中的纹理,但旧 Drawable 仅在 wrap 区间变化时重建;Metal `LayerGroup::upload()` 先于 `render()`;旧 Tweaker 在 `render()` 阶段才将新纹理写入 Drawable。`gfx::Drawable::setTexture()` 仅更换资源指针,不会补做前序 upload,因此新 Metal texture 可能在 draw 时仍为 dirty。IPS 不包含 Drawable/Texture ID,不能据此声称事故中的确切对象身份已由日志证明。 + +修复在 update 阶段比较 Drawable 原有与期望的 T0/T1 实际纹理指针。wrap 范围或任一 endpoint 指针变化时,当场重建该层 drawables,确保 builder 在本帧 upload 前安装纹理。Tweaker 不再绑定纹理,只更新 UBO 动态值。`assert(!textureDirty)` 和 Metal 通用绑定逻辑未改。 + +指针转移结果: + +| 场景 | 指针比较 | 预期动作 | +|---|---|---| +| T0/T1 均不变 | 两指针相同 | 不重建、不重传 | +| 仅 T0 或仅 T1 被替换 | 一指针不同 | update 阶段重建 | +| 两端均替换 | 两指针不同 | update 阶段重建 | +| 旧 T0/T1 交叉复用 | 顺序变化 | update 阶段重建 | +| T0=T1 | 相同共享指针 | 保持时不重建;从双端切换/反向时重建 | +| 仅 mix/opacity/stops 改变 | endpoint 不参与这些参数 | Tweaker 更新 UBO,不重建/重传 | +| wrap 改变 | endpoint 可相同 | 重建 wrap drawables | +| Resources 清空后恢复 | null → 新端点且 wrap 哨兵失配 | 新建资源与 drawables | + +## 3. 实际修改文件 + +- `include/mbgl/renderer/layers/weather_color_texture_lifecycle.hpp`:新增 drawable endpoint/wrap 重建判定 helper。 +- `src/mbgl/renderer/layers/render_weather_color_layer.cpp`:update 阶段检测 endpoint 指针并重建;Tweaker 删除 draw 阶段纹理绑定。 +- `test/renderer/weather_color_texture_lifecycle.test.cpp`:新增六组指针/生命周期决策测试。 +- `test/CMakeLists.txt`:将新测试加入 CMake test target;Bazel 通过既有测试 glob 收录。 +- `docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md`:新增本阶段记录。 +- `粒子开发记录.md`:追加本轮事实。 +- 本 Codex 完成报告。 + +没有更改 V15 任务书、V12/V13、JS/RN、NavSea 宿主 API、Android、release pipeline、release tag/archive/manifest/version、粒子 advection/State/Trail/Retained、公共 API/ABI、Layer ID、Z-order、shader slot 或 Metal assert。 + +## 4. 检查和验证结果 + +| 检查 | 结果 | +|---|---| +| 源码调用链只读复核 | 已完成;事实和限制如上 | +| `git diff --check` | **PASS**(已跟踪 diff 无 whitespace error;新增文件另行查看,未纳入该命令的 Git diff 范围) | +| RDC 首次 OpenGL host 编译 | **FAIL**:`test/renderer/weather_color_texture_lifecycle.test.cpp:60:17` 的 `nullptr` 参数无法参与模板类型推导;RDC 同时反馈 production `render_weather_color_layer.cpp` object 已编译 | +| OpenGL host 修正 | 已将该断言调用写为 `weatherColorDrawableBindingsChanged(...)`,仅修正测试 helper 的模板类型推导 | +| RDC OpenGL host build retry | **PASS**, exit 0 | +| RDC OpenGL 定向 GTest | **PASS**, 6/6 | +| RDC Metal host 编译 | **PASS**, exit 0 | +| RDC Metal host 定向 GTest | **PASS**, 6/6 | +| iOS/Xcode build、签名、安装 | 未执行,用户按项目边界手工完成 | +| iPad Metal 运行、崩溃断言/内存/纹理计数 | 未执行,等待人工集中验收 | +| Android/Gradle/APK/ADB/Jest/JS | 未执行 | +| scoped commit/push | 已完成,详见 §7 | +| release pipeline | 未执行 | + +## 5. RDC Mac 精确命令 + +按 OpenGL → Metal host 顺序执行;每个 build 成功后运行该新增测试: + +```bash +cmake --build build-step088-opengl --target mbgl-test-runner --parallel 3 +build-step088-opengl/mbgl-test-runner --gtest_filter='WeatherColorTextureLifecycle.*' +cmake --build build-step097-metal-host --target mbgl-test-runner --parallel 3 +build-step097-metal-host/mbgl-test-runner --gtest_filter='WeatherColorTextureLifecycle.*' +``` + +RDC 日志目录:`/tmp/navsea_v15_weathercolor_validation/`。日志文件:`opengl_build.log`(首轮失败)、`opengl_build_retry.log`(重试成功)、`opengl_test.log`(6/6)、`metal_host_build.log`(成功)、`metal_host_test.log`(6/6)。未从回传摘要/日志记录到耗时,因此不补写耗时。Metal host 结果不是 iOS 真机通过。 + +## 6. 限制、剩余风险与人工验收 + +- 新测试覆盖 endpoint 判定矩阵,不是 drawable/Metal GPU 端到端模拟;`upload()` 到 draw 的实际时序仍需 RDC Mac host 验证和 iPad 验收。 +- 任务书引用的事故 IPS 与外部调查报告未在本机读取;没有推断其中未提供的内容。源代码链是可推导的候选根因,而非 IPS 已证明纹理 ID。 +- iPad 需使用后续正确版本 Native 手工编译;先核对二进制 UUID,再执行冷启动风场、风→浪→风、风→雨→潮→风、邻帧/第九帧淘汰后重入、快速 Seek、天气关闭重开、平移缩放、前后台和旋转。检查 assert/crash、WeatherColor/Particle 画面、上传/重建次数、内存及异常日志。本船放大、上层闪烁和 Android 场景独立验收。 +- 未发现 `STEP_RECORD.md`;本轮按根规则更新 Step 记录并追加 `粒子开发记录.md`。 +- 当前结论为源码已实施、RDC OpenGL/Metal host 定向验证 PASS,等待用户手工 iPad 验收;host PASS 不代表设备 PASS。iPad 实测通过前状态不得提升为 CLOSED/PASS。 + +## 7. Git 收尾 + +- scoped 实现提交:`5af91fe7a105f0ac047e18db1dc063c9ffd69d44`,message:`fix: 修复 V15 WeatherColor Metal 纹理生命周期`;仅含本任务源码、测试、Step、报告和开发记录共 7 个文件。 +- 已推送至 `origin/particle/main`。推送后执行 `git ls-remote origin refs/heads/particle/main`,读回 `5af91fe7a105f0ac047e18db1dc063c9ffd69d44`,与实现提交相同。 +- 验证结果补录提交:`4712e1752af2a8238def5839c8754270de4195c9`,message:`docs: 记录 V15 WeatherColor RDC 验证与推送`;仅包含 Step、完成报告、开发记录。推送后远端 HEAD 回读为该提交。 +- 用户提供的未跟踪 V15 任务书未加入提交。release tag、版本坐标和发行包未修改。 +- 本报告与 Step 后续补录此 Git 事实;设备状态仍为 **WAITING FOR MANUAL IPAD VALIDATION**。 diff --git a/include/mln/gfx/context.hpp b/include/mln/gfx/context.hpp index 81e71d931..4b9b401ca 100644 --- a/include/mln/gfx/context.hpp +++ b/include/mln/gfx/context.hpp @@ -82,6 +82,10 @@ public: virtual void reduceMemoryUsage() = 0; virtual std::unique_ptr createOffscreenTexture(Size, TextureChannelDataType) = 0; + virtual std::unique_ptr createOffscreenTexture(Size, + TextureChannelDataType, + bool, + bool) = 0; template Renderbuffer createRenderbuffer(const Size size) { diff --git a/include/mln/layermanager/particle_validation_layer_factory.hpp b/include/mln/layermanager/particle_validation_layer_factory.hpp new file mode 100644 index 000000000..a65bb126a --- /dev/null +++ b/include/mln/layermanager/particle_validation_layer_factory.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace mln { + +// VALIDATION ONLY: factory for the 30-01 Core RenderLayer experiment. +class ParticleValidationLayerFactory final : public LayerFactory { +public: + const style::LayerTypeInfo* getTypeInfo() const noexcept override; + std::unique_ptr createLayer(const std::string&, + const style::conversion::Convertible&) noexcept override; + std::unique_ptr createRenderLayer(Immutable) noexcept override; +}; + +} // namespace mln diff --git a/include/mln/layermanager/raster_particle_layer_factory.hpp b/include/mln/layermanager/raster_particle_layer_factory.hpp new file mode 100644 index 000000000..f9de1f92f --- /dev/null +++ b/include/mln/layermanager/raster_particle_layer_factory.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace mln { + +class RasterParticleLayerFactory final : public LayerFactory { +public: + const style::LayerTypeInfo* getTypeInfo() const noexcept override; + std::unique_ptr createLayer(const std::string&, + const style::conversion::Convertible&) noexcept override; + std::unique_ptr createRenderLayer(Immutable) noexcept override; +}; + +} // namespace mln diff --git a/include/mln/layermanager/weather_color_layer_factory.hpp b/include/mln/layermanager/weather_color_layer_factory.hpp new file mode 100644 index 000000000..6057cf081 --- /dev/null +++ b/include/mln/layermanager/weather_color_layer_factory.hpp @@ -0,0 +1,10 @@ +#pragma once +#include +namespace mln { +class WeatherColorLayerFactory final : public LayerFactory { +public: + const style::LayerTypeInfo* getTypeInfo() const noexcept override; + std::unique_ptr createLayer(const std::string&, const style::conversion::Convertible&) noexcept override; + std::unique_ptr createRenderLayer(Immutable) noexcept override; +}; +} // namespace mln diff --git a/include/mln/map/map.hpp b/include/mln/map/map.hpp index 127804033..bbcc6802d 100644 --- a/include/mln/map/map.hpp +++ b/include/mln/map/map.hpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include @@ -55,6 +57,14 @@ public: /// Triggers a repaint. void triggerRepaint(); + /** Enables or disables the map-scoped retained static underlay optimization. */ + void setRetainedUnderlayEnabled(bool); + bool setContinuousLayerRange(std::string startLayerId, std::string endLayerId); + bool setRenderSegmentPlan(RenderSegmentPlan); + RenderSegmentDiagnostics getRenderSegmentDiagnostics() const; + bool isRetainedUnderlayEnabled() const; + RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() const; + style::Style& getStyle(); const style::Style& getStyle() const; diff --git a/include/mln/mtl/context.hpp b/include/mln/mtl/context.hpp index 4eb9b3065..a53b945f4 100644 --- a/include/mln/mtl/context.hpp +++ b/include/mln/mtl/context.hpp @@ -106,7 +106,10 @@ public: void setDirtyState() override; - std::unique_ptr createOffscreenTexture(Size, gfx::TextureChannelDataType, bool, bool); + std::unique_ptr createOffscreenTexture(Size, + gfx::TextureChannelDataType, + bool, + bool) override; std::unique_ptr createOffscreenTexture(Size, gfx::TextureChannelDataType) override; diff --git a/include/mln/mtl/render_pass.hpp b/include/mln/mtl/render_pass.hpp index e96d64376..336b5babb 100644 --- a/include/mln/mtl/render_pass.hpp +++ b/include/mln/mtl/render_pass.hpp @@ -17,6 +17,19 @@ class BufferResource; class CommandEncoder; class Context; +namespace detail { +struct ClearedAttachments { + bool color = false; + bool depth = false; + bool stencil = false; +}; + +ClearedAttachments applyRenderPassClears(MTL::RenderPassDescriptor&, + const std::optional&, + const std::optional&, + const std::optional&); +} // namespace detail + class RenderPass final : public gfx::RenderPass { public: RenderPass(CommandEncoder&, const char* name, const gfx::RenderPassDescriptor&); @@ -36,10 +49,16 @@ public: /// Apply the given stencil reference value, if different from the current value void setStencilReference(int32_t referenceValue); - /// Bind a texture to the fragment location + /// Bind a texture to the vertex location. + void setVertexTexture(const MTLTexturePtr&, int32_t location); + + /// Set the sampler for a vertex texture binding. + void setVertexSamplerState(const MTLSamplerStatePtr&, int32_t location); + + /// Bind a texture to the fragment location. void setFragmentTexture(const MTLTexturePtr&, int32_t location); - /// Set the sampler for a texture binding + /// Set the sampler for a fragment texture binding. void setFragmentSamplerState(const MTLSamplerStatePtr&, int32_t location); /// Set the render pipeline state @@ -90,6 +109,8 @@ private: std::array, maxBinds> vertexBinds; std::array, maxBinds> fragmentBinds; + std::array vertexTextureBindings; + std::array vertexSamplerStates; std::array fragmentTextureBindings; std::array fragmentSamplerStates; diff --git a/include/mln/particle/field/field_binding.hpp b/include/mln/particle/field/field_binding.hpp new file mode 100644 index 000000000..66510025a --- /dev/null +++ b/include/mln/particle/field/field_binding.hpp @@ -0,0 +1,132 @@ +#pragma once + +#include + +#include +#include +#include + +namespace mln::particle { + +// Host-owned binding intent. FieldPlaneKey identity is deliberately only its +// semanticID/groupID pair; component is validated from the selected plane. +struct ColorBinding { + std::string fieldKey; + FieldPlaneKey plane; + FieldValueClass expectedValueClass = FieldValueClass::Scalar; + uint16_t expectedUnitID = FieldUnitNone; + + bool operator==(const ColorBinding&) const; +}; + +enum class ParticleBindingMode : uint8_t { VectorComponents, DirectionAngle }; + +struct ParticleBinding { + ParticleBindingMode mode = ParticleBindingMode::VectorComponents; + std::string fieldKey; + FieldPlaneKey uPlane; + FieldPlaneKey vPlane; + FieldPlaneKey directionPlane; + uint16_t expectedUnitID = FieldUnitNone; + + // Equality includes only the selectors consumed by mode; selector identity + // remains semanticID/groupID, while component is a resolved-plane assertion. + bool operator==(const ParticleBinding&) const; +}; + +struct FieldSampleState { + uint64_t requestedForecastTimeMs = 0; + float mix = 0.0f; +}; + +// The stable coordinate of a FieldSeries interpolation endpoint. Revisions +// are deliberately excluded: projection identities add only the content that +// their binding actually consumes. +struct ProjectionFrameCoordinate { + std::string fieldID; + uint64_t validTimeMs = 0; + + bool operator==(const ProjectionFrameCoordinate&) const; +}; + +struct ColorProjectionContentIdentity { + ColorBinding binding; + ProjectionFrameCoordinate t0Coordinate; + ProjectionFrameCoordinate t1Coordinate; + std::string t0Plane; + std::string t1Plane; + std::string t0Validity; + std::string t1Validity; + + bool operator==(const ColorProjectionContentIdentity&) const; +}; + +struct ParticleProjectionContentIdentity { + ParticleBinding binding; + ProjectionFrameCoordinate t0Coordinate; + ProjectionFrameCoordinate t1Coordinate; + std::string t0FirstPlane; + std::string t1FirstPlane; + std::string t0SecondPlane; + std::string t1SecondPlane; + std::string t0Validity; + std::string t1Validity; + + bool operator==(const ParticleProjectionContentIdentity&) const; +}; + +struct ColorProjection { + ColorProjectionContentIdentity contentIdentity; + FieldSampleState sampleState; + std::shared_ptr t0Frame; + std::shared_ptr t1Frame; + std::shared_ptr> t0PlaneStorage; + std::shared_ptr> t1PlaneStorage; + std::shared_ptr> t0ValidityStorage; + std::shared_ptr> t1ValidityStorage; +}; + +struct ParticleProjection { + ParticleProjectionContentIdentity contentIdentity; + FieldSampleState sampleState; + std::shared_ptr t0Frame; + std::shared_ptr t1Frame; + std::shared_ptr> t0FirstPlaneStorage; + std::shared_ptr> t1FirstPlaneStorage; + std::shared_ptr> t0SecondPlaneStorage; + std::shared_ptr> t1SecondPlaneStorage; + std::shared_ptr> t0ValidityStorage; + std::shared_ptr> t1ValidityStorage; +}; + +enum class BindingProjectionError : uint8_t { + Accepted, + InvalidBinding, + FieldSeriesMissing, + WindowUnavailable, + PlaneMissing, + PlaneClassMismatch, + PlaneUnitMismatch, + PlaneComponentMismatch, + PlaneGroupMismatch, +}; + +struct ColorProjectionResult { + BindingProjectionError error = BindingProjectionError::InvalidBinding; + std::optional projection; + bool accepted() const { return error == BindingProjectionError::Accepted; } +}; + +struct ParticleProjectionResult { + BindingProjectionError error = BindingProjectionError::InvalidBinding; + std::optional projection; + bool accepted() const { return error == BindingProjectionError::Accepted; } +}; + +class BindingProjectionResolver final { +public: + static ColorProjectionResult resolve(const EncodedFieldStore&, const ColorBinding&, uint64_t requestedForecastTimeMs); + static ParticleProjectionResult resolve(const EncodedFieldStore&, const ParticleBinding&, uint64_t requestedForecastTimeMs); +}; + +} // namespace mln::particle diff --git a/include/mln/particle/field/field_data_adapter.hpp b/include/mln/particle/field/field_data_adapter.hpp new file mode 100644 index 000000000..6b80c470f --- /dev/null +++ b/include/mln/particle/field/field_data_adapter.hpp @@ -0,0 +1,182 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace mln::particle { + +enum class VectorSemantics : uint8_t { Velocity, Direction }; + +constexpr float DirectionAdvectionEpsilon = 0.00001f; + +// Mirrors the shader's canonical component-wise T0/T1 resolution. Keeping +// this small, provider-neutral helper in Core gives fixture tests an exact +// CPU oracle; the renderer still performs the authoritative GPU operation. +inline bool resolveParticleAdvection(const VectorSemantics semantics, + const std::array& t0, + const std::array& t1, + const float timeMix, + const float advectionScale, + std::array& result) { + const std::array mixed{{t0[0] + (t1[0] - t0[0]) * timeMix, + t0[1] + (t1[1] - t0[1]) * timeMix}}; + if (!std::isfinite(mixed[0]) || !std::isfinite(mixed[1])) return false; + const float length = std::hypot(mixed[0], mixed[1]); + if (!std::isfinite(length) || length <= DirectionAdvectionEpsilon) return false; + if (semantics == VectorSemantics::Direction) { + if (!std::isfinite(advectionScale) || advectionScale <= 0.0f) return false; + result = {{mixed[0] / length * advectionScale, mixed[1] / length * advectionScale}}; + } else { + // VELOCITY intentionally does not consume advectionScale. + result = mixed; + } + return true; +} + +// A v2 payload is deliberately split into unambiguous planes. vector is +// interleaved eastward-U/northward-V; scalar is one float per cell; validity +// is the one shared logical mask for every present channel. +struct ParticleFieldPayloadBundle { + std::optional> scalar; + std::optional> vector; + std::optional> validity; +}; + +struct ParticleFieldPlaneDescriptor { + uint64_t byteLength = 0; + std::string encoding = "float32-le"; + std::string layout; + std::optional noData; +}; + +// Provider-neutral description of the canonical field bytes consumed by Core. +// v1 fields below are retained only for the thin vector-only compatibility +// ingress. v2 uses scalar/vector/validity plane descriptors instead. +struct ParticleFieldDescriptor { + std::string fieldID; + std::string fieldType; + std::string displayDomain; + // Assigned by the producer before decoding. The adapter never invents a revision. + uint64_t generation = 0; + uint64_t validTime = 0; + uint32_t width = 0; + uint32_t height = 0; + double west = -180.0; + double south = -85.0; + double east = 180.0; + double north = 85.0; + float noData = -9999.0f; + uint64_t dataVersion = 0; + uint64_t byteLength = 0; + uint32_t stride = 0; + std::string layout = "interleaved-uv"; + std::string encoding = "float32-le"; + std::string fieldSha256; + std::optional scalar; + std::optional vector; + std::optional validity; + std::optional vectorSemantics; +}; + +struct ParticleFieldSnapshot { + std::string fieldID; + std::string fieldType; + std::string displayDomain; + uint64_t validTime = 0; + uint32_t width = 0; + uint32_t height = 0; + double west = -180.0; + double south = -85.0; + double east = 180.0; + double north = 85.0; + float noData = -9999.0f; + uint64_t dataVersion = 0; + uint64_t generation = 0; + std::string fieldSha256; + std::vector scalar; + std::vector vector; + std::vector valid; + std::optional vectorSemantics; + // Internal interpretation identity. Never part of the public field ABI. + std::string gpuEncodingIdentity; + + bool isValid() const; + bool isTimeInterpolationCompatibleWith(const ParticleFieldSnapshot&) const; + bool hasScalar() const { return !scalar.empty(); } + bool hasVector() const { return !vector.empty(); } +}; + +inline bool fieldIdentityChanged(const ParticleFieldSnapshot& current, const ParticleFieldSnapshot& next) { + return current.generation != next.generation || current.fieldID != next.fieldID || + current.dataVersion != next.dataVersion; +} + +inline bool sameFieldGpuContent(const ParticleFieldSnapshot& current, const ParticleFieldSnapshot& next) { + // Content identity is the decoded plane bundle, not producer identity, + // generation, dataVersion, or an optional legacy aggregate hash. + return current.isTimeInterpolationCompatibleWith(next) && current.scalar == next.scalar && + current.vector == next.vector && current.valid == next.valid; +} + +// A GPU slot may be reused only for the same immutable endpoint. A matching +// producer revision alone is insufficient, and equal bytes at another valid +// time must not silently inherit the previous endpoint's provenance. +inline bool sameFieldGpuEndpoint(const ParticleFieldSnapshot& a, const ParticleFieldSnapshot& b) { + return a.fieldID == b.fieldID && a.generation == b.generation && a.dataVersion == b.dataVersion && + a.validTime == b.validTime && a.fieldType == b.fieldType && a.displayDomain == b.displayDomain && + a.vectorSemantics == b.vectorSemantics && a.noData == b.noData && + a.fieldSha256 == b.fieldSha256 && a.gpuEncodingIdentity == b.gpuEncodingIdentity && + a.width == b.width && a.height == b.height && a.west == b.west && a.south == b.south && + a.east == b.east && a.north == b.north && a.vector == b.vector && a.valid == b.valid; +} + +enum class GpuEndpointSource : uint8_t { Upload, OldT0, OldT1 }; + +inline GpuEndpointSource gpuEndpointSource(const ParticleFieldSnapshot& oldT0, + const ParticleFieldSnapshot& oldT1, + const ParticleFieldSnapshot& wanted) { + if (sameFieldGpuEndpoint(oldT0, wanted)) return GpuEndpointSource::OldT0; + if (sameFieldGpuEndpoint(oldT1, wanted)) return GpuEndpointSource::OldT1; + return GpuEndpointSource::Upload; +} + +struct ParticleFieldTimeWindow { + ParticleFieldSnapshot t0; + ParticleFieldSnapshot t1; + + bool isValid() const { return t0.isValid() && t1.isValid() && t0.isTimeInterpolationCompatibleWith(t1); } +}; + +enum class ParticleFieldAdapterError : uint8_t { + None, + InvalidDescriptor, + UnsupportedFormat, + DimensionMismatch, + PayloadSizeMismatch, + InvalidTimestampOrIdentity, + NonFiniteData, + DecodeValidationFailure, +}; + +const char* particleFieldAdapterErrorName(ParticleFieldAdapterError); + +struct ParticleFieldAdapterResult { + std::optional snapshot; + ParticleFieldAdapterError error = ParticleFieldAdapterError::None; + std::string message; + + explicit operator bool() const { return snapshot.has_value(); } +}; + +class ParticleFieldDataAdapter final { +public: + static ParticleFieldAdapterResult decode(const ParticleFieldDescriptor&, const ParticleFieldPayloadBundle&); + // Legacy v1 vector-only normalization entry point. + static ParticleFieldAdapterResult decode(const ParticleFieldDescriptor&, const std::vector&, uint64_t generation); +}; + +} // namespace mln::particle diff --git a/include/mln/particle/field/field_profile_contract.hpp b/include/mln/particle/field/field_profile_contract.hpp new file mode 100644 index 000000000..67949cfa5 --- /dev/null +++ b/include/mln/particle/field/field_profile_contract.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include + +namespace mln::particle { + +enum class FieldValueClass : uint8_t { + Scalar = 0, + VelocityComponent = 1, + DirectionAngle = 2, + Validity = 3, +}; + +enum class FieldComponent : uint8_t { + None = 0, + UEast = 1, + VNorth = 2, + WUp = 3, +}; + +enum class FieldTimeRelation : uint8_t { + Any = 0, + ValidGteReference = 1, + ValidEqReference = 2, +}; + +// Core unit identifiers reserved by Contract Schema v1. Scalar unit identifiers +// deliberately remain opaque to the Native runtime. +constexpr uint16_t FieldUnitNone = 0; +constexpr uint16_t FieldUnitMetersPerSecond = 1; +constexpr uint16_t FieldUnitDegrees = 4; + +struct FieldPlaneRequirement { + uint16_t semanticID = 0; + FieldValueClass valueClass = FieldValueClass::Scalar; + uint16_t unitID = 0; + uint16_t groupID = 0; + FieldComponent component = FieldComponent::None; + bool requiredFlag = true; +}; + +struct ResolvedFieldProfileContract { + uint16_t contractSchemaVersion = 1; + std::string profileID; + uint32_t profileVersion = 0; + bool allowAdditionalPlanes = true; + bool sharedValidityRequired = true; + FieldTimeRelation timeRelation = FieldTimeRelation::Any; + std::vector requiredPlanes; + + bool isStructurallyValid() const; +}; + +} // namespace mln::particle diff --git a/include/mln/particle/field/field_profile_contract_json.hpp b/include/mln/particle/field/field_profile_contract_json.hpp new file mode 100644 index 000000000..6eca0db01 --- /dev/null +++ b/include/mln/particle/field/field_profile_contract_json.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include +#include + +namespace mln::particle { + +// Parses only the frozen V4 control-plane JSON schema. QBIN decoding and +// profile/frame business validation intentionally remain in their Core owners. +std::optional parseResolvedFieldProfileContractJson(const std::string& json); + +} // namespace mln::particle diff --git a/include/mln/particle/field/field_profile_validator.hpp b/include/mln/particle/field/field_profile_validator.hpp new file mode 100644 index 000000000..b2f7b8a4c --- /dev/null +++ b/include/mln/particle/field/field_profile_validator.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include + +namespace mln::particle { + +// Structured outcome of Contract Schema v1 validation. This API deliberately +// validates only generic field metadata; it has no provider or product logic. +enum class FieldProfileValidationCode : uint8_t { + Accepted, + UnsupportedContractSchema, + InvalidContract, + InvalidFrame, + MissingRequiredPlane, + PlaneDescriptorMismatch, + UndeclaredRequiredPlane, + AdditionalPlaneNotAllowed, + InvalidVelocityGroup, + // A reserved Native Core semanticID was declared with a descriptor that + // conflicts with its protocol-defined physical meaning. + SemanticContractMismatch, + InvalidCoreUnit, + UnsupportedSharedValidityMode, + TimeRelationMismatch, +}; + +struct FieldProfileValidationResult { + FieldProfileValidationCode code = FieldProfileValidationCode::InvalidContract; + std::string detail; + + bool accepted() const { return code == FieldProfileValidationCode::Accepted; } +}; + +class FieldProfileValidator final { +public: + static FieldProfileValidationResult validate(const ResolvedFieldProfileContract&, const DecodedFieldFrame&); +}; + +} // namespace mln::particle diff --git a/include/mln/particle/field/field_projection_materialization.hpp b/include/mln/particle/field/field_projection_materialization.hpp new file mode 100644 index 000000000..8838d1297 --- /dev/null +++ b/include/mln/particle/field/field_projection_materialization.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace mln::particle { + +// Renderer-neutral canonical snapshots. QBIN stays confined to ingress and +// field Core; renderers receive only the existing canonical field shape. +enum class MaterializedColorValueSource : uint8_t { + Scalar, + VectorMagnitude, +}; + +struct MaterializedColorProjection { + std::shared_ptr t0; + std::shared_ptr t1; + ColorProjectionContentIdentity contentIdentity; + FieldSampleState sampleState; + MaterializedColorValueSource valueSource = MaterializedColorValueSource::Scalar; +}; + +struct MaterializedParticleProjection { + std::shared_ptr t0; + std::shared_ptr t1; + ParticleProjectionContentIdentity contentIdentity; + FieldSampleState sampleState; +}; + +std::optional materializeColorProjection(const ColorProjection&); +std::optional materializeParticleProjection(const ParticleProjection&); + +} // namespace mln::particle diff --git a/include/mln/particle/field/field_runtime.hpp b/include/mln/particle/field/field_runtime.hpp new file mode 100644 index 000000000..51aa98762 --- /dev/null +++ b/include/mln/particle/field/field_runtime.hpp @@ -0,0 +1,177 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace mln::particle { + +enum class FieldDType : uint8_t { + Int16 = 1, + UInt16 = 2, + UInt8 = 3, + Int8 = 4, + Float32 = 5, + Bitset = 6, +}; + +struct FrameContentIdentity { + std::string fieldID; + uint64_t generation = 0; + uint64_t dataVersion = 0; + uint64_t validTimeMs = 0; + + bool isValid() const; + bool operator==(const FrameContentIdentity&) const; + bool operator!=(const FrameContentIdentity& other) const { return !(*this == other); } +}; + +struct FieldPlaneKey { + uint16_t semanticID = 0; + uint16_t groupID = 0; + // Component is a plane descriptor property, not part of the QBIN V2 + // plane identity. A Field may contain only one (semanticID, groupID). + FieldComponent component = FieldComponent::None; + + bool operator==(const FieldPlaneKey&) const; + bool operator<(const FieldPlaneKey&) const; +}; + +struct DecodedFieldPlane { + FieldPlaneKey key; + FieldDType dtype = FieldDType::Float32; + FieldValueClass valueClass = FieldValueClass::Scalar; + uint16_t unitID = 0; + // REQUIRED is transport metadata retained by the provider-neutral runtime. + bool requiredFlag = false; + uint32_t count = 0; + float scale = 1.0f; + float offset = 0.0f; + std::string contentIdentity; + std::shared_ptr> storage; + + bool isStructurallyValid(uint64_t cellCount) const; + bool isInterpolationCompatibleWith(const DecodedFieldPlane&) const; +}; + +struct DecodedFieldGrid { + uint8_t layout = 1; + uint8_t projection = 0; + uint8_t sampleOrder = 0; + uint8_t gridRegistration = 0; + uint32_t width = 0; + uint32_t height = 0; + double west = -180.0; + double south = -85.0; + double east = 180.0; + double north = 85.0; + + bool isValid() const; + bool operator==(const DecodedFieldGrid&) const; +}; + +struct DecodedFieldFrame { + FrameContentIdentity frameContentIdentity; + uint64_t referenceTimeMs = 0; + // Validation provenance retained so a stored frame cannot be interpolated + // across two otherwise byte-compatible but semantically different profiles. + std::string resolvedProfileID; + uint32_t resolvedProfileVersion = 0; + DecodedFieldGrid grid; + // One frame-wide validity domain. The descriptor and payload share storage; + // projections can retain the same payload without copying it. + DecodedFieldPlane validityPlane; + std::string validityContentIdentity; + std::shared_ptr> validityBits; + std::vector planes; + + bool isStructurallyValid() const; + bool isSeriesCompatibleWith(const DecodedFieldFrame&) const; + bool isInterpolationCompatibleWith(const DecodedFieldFrame&) const; + bool hasSameContentAs(const DecodedFieldFrame&) const; + // Raw decoded plane payload plus the shared validity bitset; excludes + // separately materialized CPU snapshots and renderer/GPU resources. + uint64_t decodedByteSize() const; +}; + +enum class FieldSeriesSubmitResult : uint8_t { + Inserted, + ReplacedRevision, + Duplicate, + RejectedInvalidFieldKey, + RejectedInvalidFrame, + RejectedIdentityConflict, + RejectedStaleRevision, + RejectedIncompatibleFrame, + RejectedCapacity, +}; + +struct FieldSeriesWindow { + std::shared_ptr t0; + std::shared_ptr t1; + float mix = 0.0f; + uint64_t requestedForecastTimeMs = 0; +}; + +class FieldSeries final { +public: + explicit FieldSeries(size_t capacity = 8); + + FieldSeriesSubmitResult submit(std::shared_ptr); + std::optional resolve(uint64_t requestedForecastTimeMs) const; + size_t size() const { return frames.size(); } + size_t capacity() const { return capacity_; } + uint64_t decodedByteSize() const; + const std::vector>& entries() const { return frames; } + +private: + size_t capacity_; + std::vector> frames; +}; + +enum class ForecastPlaybackState : uint8_t { Paused, Playing }; + +class ForecastClock final { +public: + bool setRequestedForecastTime(uint64_t timestampMs); + bool setPlaybackSpeed(float value); + void play() { state_ = ForecastPlaybackState::Playing; } + void pause() { state_ = ForecastPlaybackState::Paused; } + bool advance(uint64_t elapsedMs); + + uint64_t requestedForecastTime() const { return requestedForecastTimeMs_; } + float playbackSpeed() const { return playbackSpeed_; } + ForecastPlaybackState playbackState() const { return state_; } + +private: + uint64_t requestedForecastTimeMs_ = 0; + // The supported playback rates are exact multiples of half a millisecond. + // Keep the sub-millisecond half-unit so short HALF ticks do not round up. + uint8_t fractionalHalfMillisecond_ = 0; + float playbackSpeed_ = 1.0f; + ForecastPlaybackState state_ = ForecastPlaybackState::Paused; +}; + +class EncodedFieldStore final { +public: + explicit EncodedFieldStore(size_t perSeriesCapacity = 8) : perSeriesCapacity_(perSeriesCapacity) {} + + FieldSeriesSubmitResult submit(const std::string& fieldKey, std::shared_ptr); + std::optional resolve(const std::string& fieldKey, uint64_t requestedForecastTimeMs) const; + const FieldSeries* find(const std::string& fieldKey) const; + bool clear(const std::string& fieldKey); + void clearAll() { series.clear(); } + size_t fieldCount() const { return series.size(); } + uint64_t decodedByteSize() const; + +private: + size_t perSeriesCapacity_; + std::unordered_map series; +}; + +} // namespace mln::particle diff --git a/include/mln/particle/field/local_particle_field_feed.hpp b/include/mln/particle/field/local_particle_field_feed.hpp new file mode 100644 index 000000000..5498d3d0a --- /dev/null +++ b/include/mln/particle/field/local_particle_field_feed.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include +#include + +namespace mln::particle { + +// Decodes the small provider-neutral local feed contract. File/asset loading +// remains outside this class; callers provide metadata text and payload bytes. +class LocalParticleFieldFeed final { +public: + static ParticleFieldAdapterResult load(const std::string& metadataText, const std::vector& payload); + static ParticleFieldAdapterResult load(const std::string& metadataText, const ParticleFieldPayloadBundle& payloads); +}; + +} // namespace mln::particle diff --git a/include/mln/particle/field/particle_gpu_math.hpp b/include/mln/particle/field/particle_gpu_math.hpp new file mode 100644 index 000000000..b86a3c6d8 --- /dev/null +++ b/include/mln/particle/field/particle_gpu_math.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace mln::particle { + +// Internal RGBA8 UNORM field representation. Nearest sampling and highp +// shader arithmetic reconstruct the byte with floor(sample*255 + 0.5). +inline uint8_t encodeSignedVelocity8(const float value, const float scale, const bool valid) { + if (!valid || !std::isfinite(value) || !std::isfinite(scale) || scale <= 0.0f) return 128; + return static_cast(128 + static_cast( + std::lround(std::clamp(value / scale, -1.0f, 1.0f) * 127.0f))); +} + +inline float decodeSignedVelocity8(const uint8_t byte, const float scale) { + return static_cast(static_cast(byte) - 128) * scale / 127.0f; +} + +inline std::array mercatorVelocityDelta(const double eastMps, + const double northMps, + const double latitudeDegrees, + const double visualSeconds) { + constexpr double circumference = 40075016.68557849; + constexpr double radiansPerDegree = 3.14159265358979323846 / 180.0; + const double cosine = std::max(std::cos(latitudeDegrees * radiansPerDegree), 0.01); + return {{eastMps * visualSeconds / (circumference * cosine), + -northMps * visualSeconds / (circumference * cosine)}}; +} + +} // namespace mln::particle diff --git a/include/mln/particle/ingress/encoded_field_transport.hpp b/include/mln/particle/ingress/encoded_field_transport.hpp new file mode 100644 index 000000000..28a96ca28 --- /dev/null +++ b/include/mln/particle/ingress/encoded_field_transport.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace mln::particle::ingress { + +// Transport-only bound for JNI byte-array copying; decoder limits remain independent. +constexpr std::size_t MaxEncodedFieldTransportBytes = 64u * 1024u * 1024u; + +constexpr bool isValidEncodedFieldTransportSize(const std::size_t size) { + return size >= 1u && size <= MaxEncodedFieldTransportBytes; +} + +} // namespace mln::particle::ingress diff --git a/include/mln/particle/ingress/qbin_v2_decoder.hpp b/include/mln/particle/ingress/qbin_v2_decoder.hpp new file mode 100644 index 000000000..4872b5e67 --- /dev/null +++ b/include/mln/particle/ingress/qbin_v2_decoder.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace mln::particle::ingress { + +struct EncodedFieldRuntimeMetadata { + std::string fieldID; + uint64_t generation = 0; + uint64_t dataVersion = 0; +}; + +enum class QbinDecodeErrorCode : uint8_t { + Accepted, + InvalidRuntimeMetadata, + InputTooSmall, + BadMagic, + UnsupportedVersion, + UnsupportedLayout, + UnsupportedProjection, + UnsupportedSampleOrder, + UnsupportedGridRegistration, + InvalidHeader, + InvalidBounds, + InvalidTime, + LimitExceeded, + LengthMismatch, + ReservedNonZero, + InvalidDescriptor, + NonCanonicalDescriptorOrder, + DuplicatePlaneKey, + NonCanonicalPacking, + NonZeroPadding, + CrcMismatch, + InvalidValidity, + InvalidCellPayload, + InvalidFrame, + MalformedRice, + ResidualOutOfRange, +}; + +using QbinV2DecodeErrorCode = QbinDecodeErrorCode; + +struct QbinV2DecodeResult { + QbinDecodeErrorCode code = QbinDecodeErrorCode::InvalidHeader; + std::string detail; + std::shared_ptr frame; + bool accepted() const { return code == QbinDecodeErrorCode::Accepted && static_cast(frame); } +}; + +class QbinV2Decoder final { +public: + static QbinV2DecodeResult decode(const EncodedFieldRuntimeMetadata&, std::span); +}; + +enum class EncodedFieldIngressCode : uint8_t { Accepted, DecodeRejected, ContractMissing, ContractRejected }; + +struct EncodedFieldIngressResult { + EncodedFieldIngressCode code = EncodedFieldIngressCode::DecodeRejected; + std::string detail; + QbinDecodeErrorCode decodeCode = QbinDecodeErrorCode::InvalidHeader; + FieldProfileValidationCode contractCode = FieldProfileValidationCode::InvalidContract; + std::shared_ptr frame; + bool accepted() const { return code == EncodedFieldIngressCode::Accepted && static_cast(frame); } +}; + +class EncodedFieldIngress final { +public: + static EncodedFieldIngressResult decodeAndValidate(const EncodedFieldRuntimeMetadata&, + const ResolvedFieldProfileContract* contract, + std::span); +}; + +} // namespace mln::particle::ingress diff --git a/include/mln/particle/ingress/qbin_v3_decoder.hpp b/include/mln/particle/ingress/qbin_v3_decoder.hpp new file mode 100644 index 000000000..be8fad98f --- /dev/null +++ b/include/mln/particle/ingress/qbin_v3_decoder.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace mln::particle::ingress { + +using QbinV3DecodeErrorCode = QbinDecodeErrorCode; + +struct QbinV3DecodeResult { + QbinDecodeErrorCode code = QbinDecodeErrorCode::InvalidHeader; + std::string detail; + std::shared_ptr frame; + + bool accepted() const { + return code == QbinDecodeErrorCode::Accepted && static_cast(frame); + } +}; + +class QbinV3Decoder final { +public: + static constexpr uint32_t BlockWidth = 16; + static constexpr uint32_t BlockHeight = 16; + + static QbinV3DecodeResult decode(const EncodedFieldRuntimeMetadata&, std::span); +}; + +} // namespace mln::particle::ingress diff --git a/include/mln/particle/renderer_debug_counters.hpp b/include/mln/particle/renderer_debug_counters.hpp new file mode 100644 index 000000000..b55b5d7a2 --- /dev/null +++ b/include/mln/particle/renderer_debug_counters.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include + +namespace mln::particle { + +// Debug/Validation-only cumulative renderer observability. These atomics are +// intentionally not connected to Host APIs and compile to no-ops in release. +enum class RendererDebugCounter : uint8_t { + WeatherColorNeedsRepaint, + ParticleNeedsRepaint, + WeatherColorUpdate, + ParticleUpdate, + ParticleSimulationAdvance, + WeatherColorDrawableRebuild, + ParticleDrawableRebuild, + FieldTextureUpload, + GpuResourceCreate, + GpuResourceRelease, + TrailTargetCreateOrResize, + V4ProjectionMaterialize, + FieldWindowRevision, + FullRendererFrame, + RetainedUnderlayBuild, + RetainedUnderlayReuse, + RetainedUnderlayFallback, + RetainedUnderlayInvalidation, + RetainedUnderlayStaticGroupDraw, + RetainedDynamicGroupDraw, + ParticleFastFrameHit, + ParticleFastFrameMiss, + ParticleFullFrame, + Count, +}; + +#ifndef NDEBUG +struct RendererDebugCounterSnapshot { + uint64_t weatherColorNeedsRepaint = 0; + uint64_t particleNeedsRepaint = 0; + uint64_t weatherColorUpdate = 0; + uint64_t particleUpdate = 0; + uint64_t particleSimulationAdvance = 0; + uint64_t weatherColorDrawableRebuild = 0; + uint64_t particleDrawableRebuild = 0; + uint64_t fieldTextureUpload = 0; + uint64_t gpuResourceCreate = 0; + uint64_t gpuResourceRelease = 0; + uint64_t trailTargetCreateOrResize = 0; + uint64_t v4ProjectionMaterialize = 0; + uint64_t fieldWindowRevision = 0; + uint64_t fullRendererFrame = 0; + uint64_t retainedUnderlayBuild = 0; + uint64_t retainedUnderlayReuse = 0; + uint64_t retainedUnderlayFallback = 0; + uint64_t retainedUnderlayInvalidation = 0; + uint64_t retainedUnderlayStaticGroupDraw = 0; + uint64_t retainedDynamicGroupDraw = 0; + uint64_t particleFastFrameHit = 0; + uint64_t particleFastFrameMiss = 0; + uint64_t particleFullFrame = 0; +}; + +inline std::atomic_uint64_t* rendererDebugCounterStorage() { + static std::atomic_uint64_t counters[static_cast(RendererDebugCounter::Count)]{}; + return counters; +} + +inline void incrementRendererDebugCounter(const RendererDebugCounter counter) { + rendererDebugCounterStorage()[static_cast(counter)].fetch_add(1, std::memory_order_relaxed); +} + +inline RendererDebugCounterSnapshot rendererDebugCounterSnapshot() { + const auto* values = rendererDebugCounterStorage(); + RendererDebugCounterSnapshot result; +#define MLN_LOAD_RENDERER_COUNTER(field, counter) \ + result.field = values[static_cast(RendererDebugCounter::counter)].load(std::memory_order_relaxed) + MLN_LOAD_RENDERER_COUNTER(weatherColorNeedsRepaint, WeatherColorNeedsRepaint); + MLN_LOAD_RENDERER_COUNTER(particleNeedsRepaint, ParticleNeedsRepaint); + MLN_LOAD_RENDERER_COUNTER(weatherColorUpdate, WeatherColorUpdate); + MLN_LOAD_RENDERER_COUNTER(particleUpdate, ParticleUpdate); + MLN_LOAD_RENDERER_COUNTER(particleSimulationAdvance, ParticleSimulationAdvance); + MLN_LOAD_RENDERER_COUNTER(weatherColorDrawableRebuild, WeatherColorDrawableRebuild); + MLN_LOAD_RENDERER_COUNTER(particleDrawableRebuild, ParticleDrawableRebuild); + MLN_LOAD_RENDERER_COUNTER(fieldTextureUpload, FieldTextureUpload); + MLN_LOAD_RENDERER_COUNTER(gpuResourceCreate, GpuResourceCreate); + MLN_LOAD_RENDERER_COUNTER(gpuResourceRelease, GpuResourceRelease); + MLN_LOAD_RENDERER_COUNTER(trailTargetCreateOrResize, TrailTargetCreateOrResize); + MLN_LOAD_RENDERER_COUNTER(v4ProjectionMaterialize, V4ProjectionMaterialize); + MLN_LOAD_RENDERER_COUNTER(fieldWindowRevision, FieldWindowRevision); + MLN_LOAD_RENDERER_COUNTER(fullRendererFrame, FullRendererFrame); + MLN_LOAD_RENDERER_COUNTER(retainedUnderlayBuild, RetainedUnderlayBuild); + MLN_LOAD_RENDERER_COUNTER(retainedUnderlayReuse, RetainedUnderlayReuse); + MLN_LOAD_RENDERER_COUNTER(retainedUnderlayFallback, RetainedUnderlayFallback); + MLN_LOAD_RENDERER_COUNTER(retainedUnderlayInvalidation, RetainedUnderlayInvalidation); + MLN_LOAD_RENDERER_COUNTER(retainedUnderlayStaticGroupDraw, RetainedUnderlayStaticGroupDraw); + MLN_LOAD_RENDERER_COUNTER(retainedDynamicGroupDraw, RetainedDynamicGroupDraw); + MLN_LOAD_RENDERER_COUNTER(particleFastFrameHit, ParticleFastFrameHit); + MLN_LOAD_RENDERER_COUNTER(particleFastFrameMiss, ParticleFastFrameMiss); + MLN_LOAD_RENDERER_COUNTER(particleFullFrame, ParticleFullFrame); +#undef MLN_LOAD_RENDERER_COUNTER + return result; +} +#else +inline void incrementRendererDebugCounter(RendererDebugCounter) {} +#endif + +} // namespace mln::particle diff --git a/include/mln/renderer/layers/weather_color_texture_lifecycle.hpp b/include/mln/renderer/layers/weather_color_texture_lifecycle.hpp new file mode 100644 index 000000000..9d8adb6dd --- /dev/null +++ b/include/mln/renderer/layers/weather_color_texture_lifecycle.hpp @@ -0,0 +1,14 @@ +#pragma once + +namespace mln { + +template +constexpr bool weatherColorDrawableBindingsChanged(const Texture* previous0, + const Texture* previous1, + const Texture* next0, + const Texture* next1, + bool wrapsChanged) noexcept { + return wrapsChanged || previous0 != next0 || previous1 != next1; +} + +} // namespace mln diff --git a/include/mln/renderer/render_segment.hpp b/include/mln/renderer/render_segment.hpp new file mode 100644 index 000000000..f8b61c86f --- /dev/null +++ b/include/mln/renderer/render_segment.hpp @@ -0,0 +1,321 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace mln { + +enum class RenderSegmentStrategy : uint8_t { Retained, Continuous }; + +inline const char* renderSegmentStrategyText(RenderSegmentStrategy strategy) noexcept { + return strategy == RenderSegmentStrategy::Retained ? "RETAINED" : "CONTINUOUS"; +} + +struct RenderSegmentDefinition { + RenderSegmentStrategy strategy = RenderSegmentStrategy::Continuous; + std::optional startLayerId; + + bool operator==(const RenderSegmentDefinition&) const = default; +}; + +struct RenderSegmentPlan { + uint32_t schemaVersion = 1; + std::vector segments; + + bool operator==(const RenderSegmentPlan&) const = default; +}; + +struct StyleLayerDiagnostics { + int32_t layerIndex = -1; + std::string layerId; + std::string layerType; + bool visible = false; + std::optional sourceId; + std::optional sourceLayer; + + bool operator==(const StyleLayerDiagnostics&) const = default; +}; + +// The style layer list is the single dependency source for diagnostics and rendering. +struct RenderSegmentDependencies { + std::vector sourceIds; + std::vector globalPrerequisites; + bool unknown = false; +}; + +inline RenderSegmentDependencies renderSegmentDependencies(const std::vector& layers, + int32_t begin, + int32_t endExclusive) { + RenderSegmentDependencies result; + if (begin < 0 || endExclusive < begin || endExclusive > static_cast(layers.size())) { + result.unknown = true; + return result; + } + std::set sources; + std::set prerequisites; + for (int32_t i = begin; i < endExclusive; ++i) { + const auto& layer = layers[static_cast(i)]; + if (layer.sourceId) sources.insert(*layer.sourceId); + if (layer.layerType == "symbol") prerequisites.insert("symbol/glyph/image/placement"); + if (layer.layerType == "custom" || layer.layerType == "custom-drawable") result.unknown = true; + if (layer.layerType != "background" && layer.layerType != "custom" && + layer.layerType != "custom-drawable" && !layer.sourceId) result.unknown = true; + } + result.sourceIds.assign(sources.begin(), sources.end()); + result.globalPrerequisites.assign(prerequisites.begin(), prerequisites.end()); + return result; +} + +// Placement capability is a property of the style layer, not of this frame's +// buckets or placementData. Invalid ranges fail closed. +inline bool renderSegmentHasPlacementCapableLayer(const std::vector& 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; } + void advanceSource(const std::string& sourceId) { sourceRevisions[sourceId] = ++sequence; } + std::optional revisionFor(const RenderSegmentDependencies& dependencies) const { + if (dependencies.unknown) return std::nullopt; + uint64_t revision = globalEpoch; + for (const auto& id : dependencies.sourceIds) { + const auto found = sourceRevisions.find(id); + if (found == sourceRevisions.end()) return std::nullopt; + for (const unsigned char ch : id) revision = (revision ^ ch) * 1099511628211ULL; + revision = (revision ^ found->second) * 1099511628211ULL; + } + return revision; + } +private: + uint64_t globalEpoch = 1; + uint64_t sequence = 1; + // Retain tombstones: remove + recreate of the same ID always receives a newer identity. + std::unordered_map sourceRevisions; +}; + +struct RenderSegmentSourceStatus { bool loaded = false; bool fading = false; }; +struct RenderSegmentSourceAssessment { + bool ready = false; + bool stable = false; + const char* reason = "unknown-dependency"; +}; + +template +RenderSegmentSourceAssessment assessRenderSegmentSources(const RenderSegmentDependencies& dependencies, + Lookup lookup) { + RenderSegmentSourceAssessment result; + if (dependencies.unknown) return result; + for (const auto& id : dependencies.sourceIds) { + const std::optional source = lookup(id); + if (!source) { result.reason = "missing-source"; return result; } + if (!source->loaded) { result.reason = "source-not-loaded"; return result; } + if (source->fading) { result.ready = true; result.reason = "source-fading"; return result; } + } + result.ready = true; + result.stable = true; + result.reason = "none"; + return result; +} + +struct RenderSourceReadinessDiagnostics { + std::string sourceId; + std::string sourceType; + bool enabled = false; + bool isLoaded = false; + bool tileBacked = false; + std::optional tileCount; + std::optional incompleteTileCount; + std::optional loadedTileCount; + bool hasFadingTiles = false; + uint64_t lastSourceMutationSequence = 0; + uint64_t lastTileChangedSequence = 0; + uint64_t lastTileActionSequence = 0; + std::string lastTileAction{"none"}; + std::string lastMutationReason{"none"}; + + bool operator==(const RenderSourceReadinessDiagnostics&) const = default; +}; + +struct RenderMutationDiagnostics { + uint64_t sequence = 0; + uint64_t contentRevision = 0; + std::string eventType{"none"}; + std::string reason{"none"}; + std::optional sourceId; + std::vector affectedSegmentIndices; + std::string dependencyScope{"none"}; + + bool operator==(const RenderMutationDiagnostics&) const = default; +}; + +struct RendererReadinessDiagnostics { + bool styleLoaded = false; + bool imageManagerLoaded = false; + bool renderOrchestratorLoaded = false; + bool spriteLoaded = false; + bool hasFadingTiles = false; + uint64_t contentRevision = 0; + std::vector sources; + RenderMutationDiagnostics lastMutation; + + bool operator==(const RendererReadinessDiagnostics&) const = default; +}; + +struct RenderSegmentCacheDiagnostics { + std::string cacheState{"disabled"}; + uint64_t contentRevision = 0; + uint64_t build = 0; + uint64_t reuse = 0; + uint64_t invalidation = 0; + uint64_t fallbackCount = 0; + std::string finalDecision{"disabled"}; + std::string lastFallbackReason{"none"}; + bool sourceReady = false; + bool sourceStable = false; + bool supported = false; + uint64_t sourceRevision = 0; + std::string lastInvalidationReason{"none"}; + uint64_t textureCreate = 0; + uint64_t compositeCreate = 0; + uint64_t resourceRelease = 0; +}; + +struct RenderSegmentDiagnosticsEntry { + uint32_t segmentIndex = 0; + RenderSegmentStrategy strategy = RenderSegmentStrategy::Continuous; + std::optional configuredStartLayerId; + int32_t beginIndex = -1; + int32_t endExclusive = -1; + std::optional resolvedStartLayerId; + std::optional resolvedEndLayerId; + std::vector layers; + std::vector dependentSourceIds; + std::vector unknownDependencies; + bool placementCapable = false; + bool placementGatePassed = false; + std::string actualExecution{"not-rendered"}; + std::optional cache; +}; + +struct RenderSegmentDiagnostics { + uint32_t schemaVersion = 1; + bool enabled = true; + bool configured = false; + bool valid = false; + std::string configSource{"unconfigured/legacy"}; + uint64_t configRevision = 0; + uint64_t topologyRevision = 0; + uint32_t styleLayerCount = 0; + uint32_t segmentCount = 0; + uint32_t retainedSegmentCount = 0; + uint32_t continuousSegmentCount = 0; + bool coverageComplete = false; + bool fallback = false; + std::string lastReason{"not-configured"}; + bool fastPathEligible = false; + bool fastPathActive = false; + std::string fastPathFallbackReason{"no-prepared-tree"}; + 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; + bool spriteLoaded = false; + bool hasFadingTiles = false; + uint64_t contentRevision = 0; + std::vector sources; + RenderMutationDiagnostics lastMutation; + std::vector styleLayers; + std::vector segments; +}; + +struct ResolvedRenderSegment { + uint32_t segmentIndex = 0; + RenderSegmentStrategy strategy = RenderSegmentStrategy::Continuous; + std::optional configuredStartLayerId; + int32_t beginIndex = 0; + int32_t endExclusive = 0; + + bool operator==(const ResolvedRenderSegment&) const = default; +}; + +struct ResolvedRenderSegmentPlan { + bool configured = false; + bool valid = false; + std::string configSource{"unconfigured/legacy"}; + uint64_t configRevision = 0; + std::string reason{"not-configured"}; + int32_t layerCount = 0; + std::vector segments; + + bool operator==(const ResolvedRenderSegmentPlan&) const = default; +}; + +// This is the main-buffer traversal used by the renderer. A failed retained range +// is drawn live exactly once; neighboring cached ranges remain composites. +template +void visitRenderSegmentExecution(const ResolvedRenderSegmentPlan& plan, + Cached cached, + Composite composite, + Live live) { + if (!plan.valid) { + live(0, plan.layerCount); + return; + } + for (const auto& segment : plan.segments) { + if (segment.strategy == RenderSegmentStrategy::Retained && cached(segment.segmentIndex)) { + composite(segment.segmentIndex); + } else { + live(segment.beginIndex, segment.endExclusive); + } + } +} + +} // namespace mln diff --git a/include/mln/renderer/render_target.hpp b/include/mln/renderer/render_target.hpp index 20eb23412..7571c52f4 100644 --- a/include/mln/renderer/render_target.hpp +++ b/include/mln/renderer/render_target.hpp @@ -34,6 +34,9 @@ public: /// Get the render target texture const gfx::Texture2DPtr& getTexture(); + /// Create and validate the backing offscreen framebuffer. + bool isRenderable(); + /// @brief Add a layer group to the render target /// @param replace Flag to replace if exists /// @return whether added diff --git a/include/mln/renderer/retained_underlay.hpp b/include/mln/renderer/retained_underlay.hpp new file mode 100644 index 000000000..170d687e4 --- /dev/null +++ b/include/mln/renderer/retained_underlay.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include + +namespace mln { + +struct RetainedUnderlayDiagnostics { + bool enabled = true; + uint64_t build = 0; + uint64_t reuse = 0; + uint64_t fallback = 0; + uint64_t invalidation = 0; + std::string lastReason; + bool rangeConfigured = false; + bool rangeValid = false; + std::string startLayerId; + std::string endLayerId; + int32_t startIndex = -1; + int32_t endIndex = -1; + int32_t continuousBegin = -1; + int32_t continuousEndExclusive = -1; + bool bottomPresent = false; + std::string bottomCacheState{"disabled"}; + uint64_t bottomBuild = 0; + uint64_t bottomReuse = 0; + uint64_t bottomInvalidation = 0; + std::string bottomLastInvalidationReason{"none"}; + uint64_t bottomTextureCreate = 0; + uint64_t bottomCompositeCreate = 0; + uint64_t bottomResourceRelease = 0; + bool topPresent = false; + std::string topCacheState{"disabled"}; + uint64_t topBuild = 0; + uint64_t topReuse = 0; + uint64_t topInvalidation = 0; + std::string topLastInvalidationReason{"none"}; + uint64_t topTextureCreate = 0; + uint64_t topCompositeCreate = 0; + uint64_t topResourceRelease = 0; + bool rangeFallback = false; + bool fastPathEligible = false; + bool fastPathActive = false; + std::string fastPathFallbackReason{"no-prepared-tree"}; + uint64_t fastPathHit = 0; + uint64_t fastPathMiss = 0; + uint64_t fullCpuFrame = 0; +}; + +struct ContinuousLayerRangeConfig { + bool configured = false; + std::string startLayerId; + std::string endLayerId; + uint64_t revision = 0; +}; + +struct ContinuousLayerRangeResolution { + bool configured = false; + bool valid = false; + std::string startLayerId; + std::string endLayerId; + int32_t startIndex = -1; + int32_t endIndex = -1; + uint64_t configRevision = 0; + std::string reason{"not-configured"}; + + int32_t begin() const noexcept { return startIndex; } + int32_t endExclusive() const noexcept { return valid ? endIndex + 1 : -1; } + bool bottomPresent() const noexcept { return valid && startIndex > 0; } + bool topPresent(int32_t layerCount) const noexcept { return valid && endExclusive() < layerCount; } +}; + +} // namespace mln diff --git a/include/mln/shaders/gl/particle_gpu_validation.hpp b/include/mln/shaders/gl/particle_gpu_validation.hpp new file mode 100644 index 000000000..9d2f4cb10 --- /dev/null +++ b/include/mln/shaders/gl/particle_gpu_validation.hpp @@ -0,0 +1,417 @@ +// Generated code, do not modify this file! +#pragma once +#include + +namespace mln { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "ParticleGpuValidationShader"; + static constexpr const char* vertex = R"(layout (location = 0) in vec2 a_pos; + +out vec2 v_uv; +out vec2 v_world; +out vec2 v_particle_velocity; +out float v_particle_valid; + +layout (std140) uniform ParticleGpuValidationUBO { + highp vec4 u_pass; + highp vec4 u_marker; + highp vec4 u_mask; + highp vec4 u_visual; + highp vec4 u_field_bounds; + highp vec4 u_respawn_bounds; + highp vec4 u_field_scale; + highp vec4 u_advection; + highp vec4 u_particle_color; + highp vec4 u_trail_color; + highp vec4 u_weather_stops[8]; + highp vec4 u_weather_colors[8]; + highp vec4 u_weather_ramp; + highp mat4 u_matrix; +}; + +uniform sampler2D u_state_texture; +uniform sampler2D u_previous; +uniform highp sampler2D u_field_texture; +uniform highp sampler2D u_next_field_texture; + +vec2 fieldUv(vec2 world) { + return vec2((world.x - u_field_bounds.x) / (u_field_bounds.z - u_field_bounds.x), + (world.y - u_field_bounds.y) / (u_field_bounds.w - u_field_bounds.y)); +} + +bool validFieldUv(vec2 uv) { + return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0))); +} + +bool resolveAdvection(vec4 field0, vec4 field1, out vec2 advection) { + vec2 mixed = mix(((floor(field0.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x, + ((floor(field1.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y, + u_marker.x); + if (any(isnan(mixed)) || any(greaterThan(abs(mixed), vec2(3.4e38)))) return false; + if (u_advection.x > 0.5) { + float directionLength = length(mixed); + if (isnan(directionLength) || directionLength > 3.4e38 || directionLength <= u_advection.z) return false; + advection = mixed / directionLength * u_advection.y; + } else { + // VELOCITY remains the established component-wise m/s path. + advection = mixed; + } + return true; +} + +void main() { + v_uv = a_pos; + v_world = a_pos; + v_particle_velocity = vec2(0.0); + v_particle_valid = 1.0; + if (u_pass.x > 4.5) { + gl_Position = u_matrix * vec4(a_pos * 8192.0, 0.0, 1.0); + } else if (u_pass.x < 1.5) { + gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0); + } else if (u_pass.x > 3.5) { + // a_pos.x packs State texel X plus one endpoint. The formal particle + // path draws the StateRead -> StateWrite displacement in current map + // clip space; it does not synthesize a fixed world-length streak. + float endpoint = mod(a_pos.x, 2.0) * 2.0 - 1.0; + vec2 stateIndex = vec2(floor(a_pos.x * 0.5), a_pos.y); + vec2 stateUV = (stateIndex + vec2(0.5)) / u_mask.xy; + vec4 previousState = texture(u_previous, stateUV); + vec4 currentState = texture(u_state_texture, stateUV); + vec2 currentPosition = currentState.rg; + vec2 uv = fieldUv(currentPosition); + vec4 field0 = texture(u_field_texture, uv); + vec4 field1 = texture(u_next_field_texture, uv); + vec2 advection; + bool valid = validFieldUv(uv) && field0.a >= 0.5 && field1.a >= 0.5 && + resolveAdvection(field0, field1, advection); + vec2 mercatorVelocity = vec2(advection.x, -advection.y); + float speed = length(mercatorVelocity); + vec2 segmentPosition = endpoint < 0.0 ? previousState.rg : currentPosition; + v_uv = stateUV; + v_world = currentPosition; + v_particle_velocity = mercatorVelocity; + // A zero alpha is written by State on a respawn frame. This suppresses + // oldPosition -> newRandomPosition cross-screen segments. + v_particle_valid = valid && speed > 0.00001 && currentState.a > 0.5 && + distance(previousState.rg, currentPosition) > 0.000000001 + ? 1.0 + : 0.0; + if (v_particle_valid < 0.5) { + gl_Position = vec4(-2.0, -2.0, 0.0, 1.0); + } else { + gl_Position = u_matrix * vec4(segmentPosition * 8192.0, 0.0, 1.0); + } + } else { + if (u_mask.z > 0.5) { + // Trail targets are viewport-sized screen-space history buffers. + gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0); + } else { + // VALIDATION ONLY: a_pos is canonical z0 world geometry; u_matrix + // is PaintParameters::matrixForTile() for the selected world copy. + gl_Position = u_matrix * vec4(a_pos * 8192.0, 0.0, 1.0); + } + } +} +)"; + static constexpr const char* fragment = R"(// The shared GL prelude defaults fragment float to mediump. Byte reconstruction +// from RGBA8 UNORM and Mercator state require highp on ES 3.x. +precision highp float; + +layout (std140) uniform ParticleGpuValidationUBO { + highp vec4 u_pass; + highp vec4 u_marker; + highp vec4 u_mask; + highp vec4 u_visual; + highp vec4 u_field_bounds; + highp vec4 u_respawn_bounds; + highp vec4 u_field_scale; + highp vec4 u_advection; + highp vec4 u_particle_color; + highp vec4 u_trail_color; + highp vec4 u_weather_stops[8]; + highp vec4 u_weather_colors[8]; + highp vec4 u_weather_ramp; + highp mat4 u_matrix; +}; + +uniform sampler2D u_previous; +uniform sampler2D u_mask_texture; +uniform highp sampler2D u_field_texture; +uniform highp sampler2D u_next_field_texture; +uniform sampler2D u_state_texture; +in vec2 v_uv; +in vec2 v_world; +in vec2 v_particle_velocity; +in float v_particle_valid; + +vec3 particleSpeedColor(float speed) { + float t = clamp(speed * 0.75, 0.0, 1.0); + vec3 slow = vec3(0.10, 0.45, 1.00); + vec3 medium = vec3(0.10, 0.95, 0.72); + vec3 fast = vec3(1.00, 0.48, 0.12); + return mix(mix(slow, medium, smoothstep(0.0, 0.55, t)), fast, smoothstep(0.45, 1.0, t)); +} + +vec2 fieldUv(vec2 world) { + return vec2((world.x - u_field_bounds.x) / (u_field_bounds.z - u_field_bounds.x), + (world.y - u_field_bounds.y) / (u_field_bounds.w - u_field_bounds.y)); +} + +bool validFieldUv(vec2 uv) { + return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0))); +} + +const float EARTH_CIRCUMFERENCE_METERS = 40075016.68557849; + +float mercatorLatitudeRadians(float worldY) { + return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY))); +} + +vec3 weatherDefaultValueColor(float value) { + // Historical default ramp endpoints are 0 and 30 generic field-value + // units. Hosts provide explicit stops whenever another scale is needed. + float t = clamp(value / 30.0, 0.0, 1.0); + vec3 slow = vec3(0.08, 0.18, 0.65); + vec3 medium = vec3(0.08, 0.80, 0.78); + vec3 fast = vec3(0.98, 0.22, 0.10); + return mix(mix(slow, medium, smoothstep(0.0, 0.55, t)), fast, smoothstep(0.45, 1.0, t)); +} + +float particleHash(vec2 value) { + return fract(sin(dot(value, vec2(127.1, 311.7))) * 43758.5453123); +} + +vec2 respawnSeed(vec2 particleUV, vec2 entropy, float attempt) { + vec2 salt = entropy * vec2(13.17, 19.91) + particleUV * vec2(97.23, 53.71) + attempt; + vec2 random = vec2(particleHash(salt), particleHash(salt.yx + 41.29)); + return mix(u_respawn_bounds.xy, u_respawn_bounds.zw, random); +} + +bool outsideRespawnBounds(vec2 position) { + return position.x < u_respawn_bounds.x || position.x > u_respawn_bounds.z || + position.y < u_respawn_bounds.y || position.y > u_respawn_bounds.w; +} + +bool fieldAdvectionAt(vec2 position, out vec2 advection) { + vec2 uv = fieldUv(position); + vec4 field0 = texture(u_field_texture, uv); + vec4 field1 = texture(u_next_field_texture, uv); + vec2 mixed = mix(((floor(field0.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x, + ((floor(field1.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y, + u_marker.x); + if (!validFieldUv(uv) || field0.a < 0.5 || field1.a < 0.5 || any(isnan(mixed)) || + any(greaterThan(abs(mixed), vec2(3.4e38)))) return false; + if (u_advection.x > 0.5) { + float directionLength = length(mixed); + if (isnan(directionLength) || directionLength > 3.4e38 || directionLength <= u_advection.z) return false; + advection = mixed / directionLength * u_advection.y; + return true; + } + // VELOCITY keeps its previous component-wise interpolation and magnitude. + advection = mixed; + return length(advection) > 0.00001; +} + +vec4 weatherRampColor(float value) { + int count = int(u_weather_ramp.x); + if (count < 2) return vec4(weatherDefaultValueColor(value), 1.0); + vec4 color = u_weather_colors[0]; + for (int i = 1; i < 8; ++i) { + if (i >= count) break; + float lo = u_weather_stops[i - 1].x; + float hi = u_weather_stops[i].x; + if (value <= hi) return mix(color, u_weather_colors[i], clamp((value - lo) / (hi - lo), 0.0, 1.0)); + color = u_weather_colors[i]; + } + return color; +} + +void main() { + float mode = u_pass.x; + + if (mode > 4.5) { + vec2 uv = fieldUv(v_world); + if (!validFieldUv(uv)) discard; + vec4 field0 = texture(u_field_texture, uv); + vec4 field1 = texture(u_next_field_texture, uv); + if (field0.a < 0.5 || field1.a < 0.5) discard; + float fieldValue; + if (u_weather_ramp.y > 0.5) { + vec2 vectorValue = mix(((floor(field0.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x, + ((floor(field1.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y, + u_marker.x); + if (any(isnan(vectorValue))) discard; + fieldValue = length(vectorValue); + } else { + fieldValue = mix(((floor(field0.r * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x, + ((floor(field1.r * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y, + u_marker.x); + if (isnan(fieldValue)) discard; + } + vec4 color = weatherRampColor(fieldValue); + float alpha = color.a * u_visual.x; + fragColor = vec4(color.rgb * alpha, alpha); + return; + } + + vec4 previous = texture(u_previous, v_uv); + + if (mode < 0.5) { + // State A/B samples the uploaded canonical Particle Field. + // RG stores Web Mercator world position, B stores age, and the Field + // texture encodes canonical U-east/V-north at that geographic sample. + // u_mask.w is the explicit State lifecycle flag: new RenderTargets are + // cleared opaque by MapLibre, so sampled alpha cannot identify a seed. + if (u_mask.z > 0.5) { + // Formal particles use GPU-resident lifecycle state. Each slot keeps + // its age in B; A=0 marks this frame as a respawn so Trail does not + // connect the retired position to the new randomized one. + bool initialSeed = u_mask.w > 0.5; + float lifecycleHash = particleHash(v_uv + vec2(17.31, 83.17)); + float maxAge = u_marker.z * mix(0.75, 1.25, lifecycleHash); + float initialAgeHash = particleHash(v_uv + vec2(71.19, 29.47)); + bool respawn = initialSeed; + // Lifecycle is wall-clock based: zoom-aware visual advection and + // forecast playback must not change respawn cadence. + float age = initialSeed ? initialAgeHash * maxAge : previous.b + u_pass.w; + vec2 position = previous.rg; + vec2 advection; + bool valid = !respawn && fieldAdvectionAt(position, advection); + // The field bounds remain the only sampling domain. Once the + // camera is stable, slots already outside the expanded viewport + // may recycle into its separate preferred respawn domain. + if (!valid || age >= maxAge || + (u_field_scale.z > 0.5 && !initialSeed && outsideRespawnBounds(position))) { + respawn = true; + } + + if (!respawn) { + float latitude = mercatorLatitudeRadians(position.y); + vec2 displacement = vec2(advection.x * u_pass.y / (EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)), + -advection.y * u_pass.y / + (EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01))); + vec2 nextPosition = position + displacement; + if (fieldAdvectionAt(nextPosition, advection)) { + position = nextPosition; + } else { + respawn = true; + } + } + + if (respawn) { + bool found = false; + // Fixed, bounded retries keep the State pass deterministic and + // avoid a CPU upload or an unbounded shader loop. + for (int attempt = 0; attempt < 3; ++attempt) { + vec2 candidate = respawnSeed(v_uv, previous.rg, float(attempt)); + if (fieldAdvectionAt(candidate, advection)) { + position = candidate; + found = true; + break; + } + } + if (!found) { + // Keep a deterministic in-bounds candidate and retry next + // frame; alpha still prevents an invalid visible segment. + position = respawnSeed(v_uv, previous.rg, 3.0); + } + if (!initialSeed) { + age = 0.0; + } + } + fragColor = vec4(position, age, respawn ? 0.0 : 1.0); + return; + } + + vec2 seed = mix(u_field_bounds.xy, u_field_bounds.zw, v_uv); + vec2 position = u_mask.w > 0.5 ? seed : previous.rg; + // 40-02: decode each independent Field texture, then interpolate the + // velocity on GPU. u_marker.x is the clamped Layer timeMix property. + vec2 uv = fieldUv(position); + vec4 field0 = texture(u_field_texture, uv); + vec4 field1 = texture(u_next_field_texture, uv); + vec2 advection; + if (!fieldAdvectionAt(position, advection)) { + position = seed; + advection = vec2(0.0); + } + // U/V remain canonical metres per second. u_pass.y is the explicit + // visual simulation delta in seconds; the validation clock may run + // faster than wall time, but it never changes the field contract. + // Both east and north metres scale with sec(latitude) in Web Mercator. + // V is north-positive while world y is southward. + float latitude = mercatorLatitudeRadians(position.y); + float dx = advection.x * u_pass.y / (EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)); + float dy = -advection.y * u_pass.y / + (EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)); + position = position + vec2(dx, dy); + if (!validFieldUv(fieldUv(position)) || any(isnan(advection))) position = seed; + fragColor = vec4(position, fract(previous.b + 0.03125 * u_pass.y), 1.0); + } else if (mode < 1.5) { + // Formal Trail first fades the previous target. Its full-particle point + // draw follows in the same target and consumes every State texel. + if (u_mask.z > 0.5) { + // trailFade remains the UI's retention-at-60Hz value. Raising it + // to elapsed wall time makes visual persistence FPS-independent. + float fadeFactor = pow(clamp(u_visual.w, 0.0001, 1.0), u_pass.y * u_marker.w); + // R is history coverage, G marks the current segment, B its speed. + // Apply independent Host colours only at composite time. + fragColor = vec4(previous.r * fadeFactor, 0.0, 0.0, 1.0); + return; + } + + // LEGACY VALIDATION DISPLAY: retain the original four deterministic + // samples for the historical ParticleValidationLayer only. + float marker = 0.0; + marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.125, 0.125)).rg))); + marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.375, 0.375)).rg))); + marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.625, 0.625)).rg))); + marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.875, 0.875)).rg))); + vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0); + fragColor = vec4(min(color, vec3(1.0)), 1.0); + } else if (mode > 3.5) { + if (v_particle_valid < 0.5) discard; + float speed = length(v_particle_velocity); + fragColor = vec4(1.0, 1.0, clamp(speed / 30.0, 0.0, 1.0), 1.0); + } else { + // The formal RasterParticle layer displays the Trail texture written by + // the current State -> Trail pass. Legacy ParticleValidation keeps its + // independent 30-03 stripe/anchor diagnostic display below. + float intensity = max(previous.r, max(previous.g, previous.b)); + float ocean = texture(u_mask_texture, v_world).a; + if (u_pass.z > 0.5 && ocean < 0.5) discard; + if (u_mask.z > 0.5) { + bool current = previous.g > 0.5; + vec4 base = current ? u_particle_color : u_trail_color; + if (current && u_marker.y > 0.5) base.rgb = particleSpeedColor(previous.b * 30.0); + float alpha = clamp(previous.r * base.a * (current ? u_visual.x : u_visual.z), 0.0, 1.0); + fragColor = vec4(base.rgb * alpha, alpha); + return; + } + + // LEGACY VALIDATION DISPLAY: preserve the frozen 30-03 + // stripe/anchor/world-wrap diagnostic for ParticleValidationLayer. + float stripe = 0.55 + 0.45 * sin((v_world.x + v_world.y) * 100.0 + u_pass.y * 0.07); + // A=(0°,0°), B=(-90°,approximately 0°), C/D straddle ±180°. + float anchors = 0.0; + anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.50, 0.50)))); + anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.25, 0.50)))); + anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.02, 0.50)))); + anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.98, 0.50)))); + vec3 color = u_pass.w < 1.5 ? vec3(0.10, 0.70, 1.00) : vec3(0.35, 1.00, 0.45); + fragColor = vec4(mix(color * stripe, vec3(1.00, 0.82, 0.15), anchors), + 0.28 + min(intensity, 0.35) * 0.35 + anchors * 0.35); + } + +#ifdef OVERDRAW_INSPECTOR + fragColor = vec4(1.0); +#endif +} +)"; +}; + +} // namespace shaders +} // namespace mln diff --git a/include/mln/shaders/gl/retained_underlay_composite.hpp b/include/mln/shaders/gl/retained_underlay_composite.hpp new file mode 100644 index 000000000..93a89bcad --- /dev/null +++ b/include/mln/shaders/gl/retained_underlay_composite.hpp @@ -0,0 +1,29 @@ +// Generated code, do not modify this file! +#pragma once +#include + +namespace mln { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "RetainedUnderlayCompositeShader"; + static constexpr const char* vertex = R"(layout (location = 0) in vec2 a_pos; +out vec2 v_uv; + +void main() { + gl_Position = vec4(a_pos.x * 2.0 - 1.0, 1.0 - a_pos.y * 2.0, 0.0, 1.0); + v_uv = vec2(a_pos.x, 1.0 - a_pos.y); +} +)"; + static constexpr const char* fragment = R"(in vec2 v_uv; +uniform sampler2D u_image; + +void main() { + fragColor = texture(u_image, v_uv); +} +)"; +}; + +} // namespace shaders +} // namespace mln diff --git a/include/mln/shaders/gl/shader_info.hpp b/include/mln/shaders/gl/shader_info.hpp index 8da7b6961..ecbf1c442 100644 --- a/include/mln/shaders/gl/shader_info.hpp +++ b/include/mln/shaders/gl/shader_info.hpp @@ -149,6 +149,20 @@ struct ShaderInfo { static const std::vector textures; }; +template <> +struct ShaderInfo { + static const std::vector attributes; + static const std::vector uniformBlocks; + static const std::vector textures; +}; + +template <> +struct ShaderInfo { + static const std::vector attributes; + static const std::vector uniformBlocks; + static const std::vector textures; +}; + template <> struct ShaderInfo { static const std::vector attributes; diff --git a/include/mln/shaders/mtl/particle_gpu_validation.hpp b/include/mln/shaders/mtl/particle_gpu_validation.hpp new file mode 100644 index 000000000..6b2d05c40 --- /dev/null +++ b/include/mln/shaders/mtl/particle_gpu_validation.hpp @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include + +#include + +namespace mln::shaders { + +// This is intentionally a backend-only translation of the formal particle +// shader. ParticleGpuValidationUBO remains the single C++ ABI shared by GL and +// Metal; the MSL declaration below is kept vec4/mat4-only to preserve its +// alignas(16), 31 * 16-byte contract. +constexpr auto particleGpuValidationShaderPrelude = R"( +enum { + idParticleGpuValidationUBO = drawableReservedUBOCount, + particleGpuValidationUBOCount +}; +struct alignas(16) ParticleGpuValidationUBO { + float4 pass; float4 marker; float4 mask; float4 visual; + float4 fieldBounds; float4 respawnBounds; float4 fieldScale; float4 advection; + float4 particleColor; float4 trailColor; float4 weatherStops[8]; + float4 weatherColors[8]; float4 weatherRamp; float4x4 matrix; +}; +static_assert(sizeof(ParticleGpuValidationUBO) == 31 * 16, "wrong size"); +)"; + +template <> +struct ShaderSource { + static constexpr auto name = "ParticleGpuValidationShader"; + static constexpr auto vertexMainFunction = "vertexMain"; + static constexpr auto fragmentMainFunction = "fragmentMain"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + static constexpr auto prelude = particleGpuValidationShaderPrelude; + static constexpr auto source = R"( +struct VertexStage { short2 pos [[attribute(0)]]; }; +struct FragmentStage { float4 position [[position, invariant]]; float2 uv; float2 world; float2 velocity; float valid; }; + +float2 fieldUv(float2 world, device const ParticleGpuValidationUBO& u) { + return float2((world.x-u.fieldBounds.x)/(u.fieldBounds.z-u.fieldBounds.x), + (world.y-u.fieldBounds.y)/(u.fieldBounds.w-u.fieldBounds.y)); +} +bool validFieldUv(float2 uv) { return all(uv >= float2(0.0)) && all(uv <= float2(1.0)); } +bool finite2(float2 value) { return all(isfinite(value)) && all(abs(value) <= float2(3.4e38)); } +bool resolveAdvection(float4 a, float4 b, device const ParticleGpuValidationUBO& u, thread float2& out) { + float2 mixed=mix(((floor(a.rg*255.0+0.5)-128.0)/127.0)*u.fieldScale.x,((floor(b.rg*255.0+0.5)-128.0)/127.0)*u.fieldScale.y,u.marker.x); + if (!finite2(mixed)) return false; + if (u.advection.x > .5) { float l=length(mixed); if (!isfinite(l)||l>3.4e38||l<=u.advection.z) return false; out=mixed/l*u.advection.y; } + else { out=mixed; return length(out)>0.00001; } + return true; +} +float particleHash(float2 v) { return fract(sin(dot(v,float2(127.1,311.7)))*43758.5453123); } +float2 respawnSeed(float2 uv, float2 entropy, float attempt, device const ParticleGpuValidationUBO& u) { + float2 salt=entropy*float2(13.17,19.91)+uv*float2(97.23,53.71)+attempt; + return mix(u.respawnBounds.xy,u.respawnBounds.zw,float2(particleHash(salt),particleHash(salt.yx+41.29))); +} +float mercatorLatitudeRadians(float y) { return atan(sinh(M_PI_F*(1.0-2.0*y))); } + +FragmentStage vertex vertexMain(thread const VertexStage in [[stage_in]], + device const ParticleGpuValidationUBO& u [[buffer(idParticleGpuValidationUBO)]], + texture2d previous [[texture(0)]], texture2d field [[texture(2)]], + texture2d nextField [[texture(3)]], texture2d state [[texture(4)]], + sampler previousSampler [[sampler(0)]], sampler fieldSampler [[sampler(2)]], + sampler nextFieldSampler [[sampler(3)]], sampler stateSampler [[sampler(4)]]) { + // a_pos is an unnormalized short2, matching the OpenGL contract. + float2 pos=float2(in.pos); + FragmentStage out={}; out.uv=pos; out.world=pos; out.valid=1.0; + if (u.pass.x > 5.5) { + float corner=glMod(pos.x,4.0); + float2 index=float2(floor(pos.x*.25),pos.y); + float2 stateUv=(index+.5)/u.mask.xy; + float4 old=previous.sample(previousSampler,stateUv), current=state.sample(stateSampler,stateUv); + out.uv=stateUv; out.world=current.rg; + bool stateOk=current.a>.5&&distance(old.rg,current.rg)>1e-9; + float2 uv=fieldUv(current.rg,u); + float4 a=field.sample(fieldSampler,uv), b=nextField.sample(nextFieldSampler,uv); + float2 advection; + bool ok=validFieldUv(uv)&&a.a>=.5&&b.a>=.5&&resolveAdvection(a,b,u,advection); + out.velocity=float2(advection.x,-advection.y); + out.valid=stateOk&&ok&&length(out.velocity)>0.00001?1.0:0.0; + float4 clip=u.matrix*float4(current.rg*8192.0,0,1); + float2 cornerOffset=corner<.5?float2(-1,-1):(corner<1.5?float2(1,-1):(corner<2.5?float2(-1,1):float2(1,1))); + float2 halfPixelNdc=float2(1.0/max(u.fieldScale.w,1.0),1.0/max(u.advection.w,1.0)); + clip.xy+=cornerOffset*halfPixelNdc*clip.w; + out.position=out.valid<.5?float4(-2,-2,0,1):clip; + } else if (u.pass.x > 4.5) { + out.position=u.matrix*float4(pos*8192.0,0,1); + } else if (u.pass.x < 1.5) { + // Metal render targets are addressed from the top-left. Flip the + // fullscreen varying so the same logical State/Trail texel is sampled + // on the next ping-pong pass. Particle texel indices remain unflipped. + out.uv=float2(pos.x,1.0-pos.y); + out.position=float4(pos*2.0-1.0,0,1); + } else if (u.pass.x > 3.5) { + float endpoint=glMod(pos.x,2.0)*2.0-1.0; + float2 index=float2(floor(pos.x*.5),pos.y); + float2 stateUv=(index+.5)/u.mask.xy; + float4 old=previous.sample(previousSampler,stateUv), current=state.sample(stateSampler,stateUv); + out.uv=stateUv; out.world=current.rg; + bool stateOk=current.a>.5&&distance(old.rg,current.rg)>1e-9; + float2 uv=fieldUv(current.rg,u); float4 a=field.sample(fieldSampler,uv), b=nextField.sample(nextFieldSampler,uv); float2 advection; + bool ok=validFieldUv(uv)&&a.a>=.5&&b.a>=.5&&resolveAdvection(a,b,u,advection); + out.velocity=float2(advection.x,-advection.y); + out.valid=stateOk&&ok&&length(out.velocity)>0.00001?1.0:0.0; + out.position=out.valid<.5?float4(-2,-2,0,1):u.matrix*float4((endpoint<0?old.rg:current.rg)*8192.0,0,1); + } else { + if (u.mask.z>.5) { + // Composite samples the viewport-sized Trail render target. + out.uv=float2(pos.x,1.0-pos.y); + out.position=float4(pos*2.0-1.0,0,1); + } else { + out.position=u.matrix*float4(pos*8192.0,0,1); + } + } + return out; +} + +float3 speedColor(float speed) { float t=clamp(speed*.75,0.0,1.0); return mix(mix(float3(.10,.45,1),float3(.10,.95,.72),smoothstep(0.0,.55,t)),float3(1,.48,.12),smoothstep(.45,1,t)); } +float4 ramp(float value, device const ParticleGpuValidationUBO& u) { int count=int(u.weatherRamp.x); if(count<2){float t=clamp(value/30.0,0.0,1.0);return float4(mix(mix(float3(.08,.18,.65),float3(.08,.80,.78),smoothstep(0.,.55,t)),float3(.98,.22,.10),smoothstep(.45,1.,t)),1); } float4 c=u.weatherColors[0]; for(int i=1;i<8&&i previous [[texture(0)]], texture2d mask [[texture(1)]], texture2d field [[texture(2)]], texture2d nextField [[texture(3)]], texture2d state [[texture(4)]], + sampler previousSampler [[sampler(0)]], sampler maskSampler [[sampler(1)]], sampler fieldSampler [[sampler(2)]], sampler nextFieldSampler [[sampler(3)]], sampler stateSampler [[sampler(4)]]) { + float mode=u.pass.x; + if(mode>5.5){if(in.valid<.5)discard_fragment();float speed=length(in.velocity);return float4(1,1,clamp(speed/30.,0.,1.),1);} + if(mode>4.5){float2 uv=fieldUv(in.world,u); if(!validFieldUv(uv)) discard_fragment(); float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);if(a.a<.5||b.a<.5)discard_fragment();float value=u.weatherRamp.y>.5?length(mix(((floor(a.rg*255.+.5)-128.)/127.)*u.fieldScale.x,((floor(b.rg*255.+.5)-128.)/127.)*u.fieldScale.y,u.marker.x)):mix(((floor(a.r*255.+.5)-128.)/127.)*u.fieldScale.x,((floor(b.r*255.+.5)-128.)/127.)*u.fieldScale.y,u.marker.x);if(!isfinite(value))discard_fragment();float4 c=ramp(value,u);float alpha=c.a*u.visual.x;return float4(c.rgb*alpha,alpha);} + float4 old=previous.sample(previousSampler,in.uv); + if(mode<.5){ + float2 position=u.mask.w>.5?mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv):old.rg; float age=old.b; bool initial=u.mask.w>.5; bool respawn=initial; float2 advection; float maxAge=u.marker.z*mix(.75,1.25,particleHash(in.uv+float2(17.31,83.17))); + if(u.mask.z>.5){age=initial?particleHash(in.uv+float2(71.19,29.47))*maxAge:old.b+u.pass.w;float2 uv=fieldUv(position,u);float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);bool ok=validFieldUv(uv)&&a.a>=.5&&b.a>=.5&&resolveAdvection(a,b,u,advection);respawn=initial||!ok||age>=maxAge||(u.fieldScale.z>.5&&!initial&&(position.xu.respawnBounds.z||position.yu.respawnBounds.w));if(!respawn){float lat=mercatorLatitudeRadians(position.y);float2 n=position+float2(advection.x*u.pass.y/(40075016.68557849*max(cos(lat),.01)),-advection.y*u.pass.y/(40075016.68557849*max(cos(lat),.01)));float2 nuv=fieldUv(n,u);float4 na=field.sample(fieldSampler,nuv),nb=nextField.sample(nextFieldSampler,nuv);if(validFieldUv(nuv)&&na.a>=.5&&nb.a>=.5&&resolveAdvection(na,nb,u,advection))position=n;else respawn=true;}if(respawn){bool found=false;for(int i=0;i<3;i++){float2 c=respawnSeed(in.uv,old.rg,float(i),u);float2 cuv=fieldUv(c,u);float4 ca=field.sample(fieldSampler,cuv),cb=nextField.sample(nextFieldSampler,cuv);if(validFieldUv(cuv)&&ca.a>=.5&&cb.a>=.5&&resolveAdvection(ca,cb,u,advection)){position=c;found=true;break;}}if(!found)position=respawnSeed(in.uv,old.rg,3.,u);if(!initial)age=0.;}return float4(position.x,position.y,age,respawn?0.:1.);} + float2 uv=fieldUv(position,u);float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);if(!validFieldUv(uv)||a.a<.5||b.a<.5||!resolveAdvection(a,b,u,advection)){position=mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv);advection=float2(0);}float lat=mercatorLatitudeRadians(position.y);position+=float2(advection.x*u.pass.y/(40075016.68557849*max(cos(lat),.01)),-advection.y*u.pass.y/(40075016.68557849*max(cos(lat),.01)));if(!validFieldUv(fieldUv(position,u))||!finite2(advection))position=mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv);return float4(position.x,position.y,fract(old.b+.03125*u.pass.y),1.); + } + if(mode<1.5){if(u.mask.z>.5){float f=pow(clamp(u.visual.w,.0001,1.),u.pass.y*u.marker.w);return float4(old.r*f,0,0,1);}float marker=0.;float2 samples[4]={float2(.125),float2(.375),float2(.625),float2(.875)};for(int i=0;i<4;i++)marker=max(marker,1.-smoothstep(0.,u.marker.z,distance(in.uv,state.sample(stateSampler,samples[i]).rg)));return float4(min(old.rgb*u.marker.w+marker*float3(.08,.55,1),float3(1)),1);} + if(mode>3.5){if(in.valid<.5)discard_fragment();float speed=length(in.velocity);return float4(1,1,clamp(speed/30.,0.,1.),1);} + float ocean=mask.sample(maskSampler,in.world).a;if(u.pass.z>.5&&ocean<.5)discard_fragment();if(u.mask.z>.5){bool current=old.g>.5;float4 base=current?u.particleColor:u.trailColor;if(current&&u.marker.y>.5)base.rgb=speedColor(old.b*30.);float alpha=clamp(old.r*base.a*(current?u.visual.x:u.visual.z),0.,1.);return float4(base.rgb*alpha,alpha);}float intensity=max(old.r,max(old.g,old.b));float stripe=.55+.45*sin((in.world.x+in.world.y)*100.+u.pass.y*.07);float anchors=0.;float2 a[4]={float2(.5,.5),float2(.25,.5),float2(.02,.5),float2(.98,.5)};for(int i=0;i<4;i++)anchors=max(anchors,1.-smoothstep(0.,.008,distance(in.world,a[i])));float3 color=u.pass.w<1.5?float3(.10,.70,1.):float3(.35,1.,.45);return float4(mix(color*stripe,float3(1.,.82,.15),anchors),.28+min(intensity,.35)*.35+anchors*.35); +} +)"; +}; +} // namespace mln::shaders diff --git a/include/mln/shaders/mtl/retained_underlay_composite.hpp b/include/mln/shaders/mtl/retained_underlay_composite.hpp new file mode 100644 index 000000000..1d5348ed8 --- /dev/null +++ b/include/mln/shaders/mtl/retained_underlay_composite.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +namespace mln::shaders { + +template <> +struct ShaderSource { + static constexpr auto name = "RetainedUnderlayCompositeShader"; + static constexpr auto vertexMainFunction = "vertexMain"; + static constexpr auto fragmentMainFunction = "fragmentMain"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + static constexpr auto prelude = R"()"; + static constexpr auto source = R"( +struct VertexStage { short2 pos [[attribute(0)]]; }; +struct FragmentStage { float4 position [[position, invariant]]; float2 uv; }; + +FragmentStage vertex vertexMain(thread const VertexStage in [[stage_in]]) { + const float2 p = float2(in.pos); + return { .position = float4(p.x * 2.0 - 1.0, 1.0 - p.y * 2.0, 0.0, 1.0), .uv = p }; +} + +half4 fragment fragmentMain(FragmentStage in [[stage_in]], + texture2d image [[texture(0)]], + sampler imageSampler [[sampler(0)]]) { + return half4(image.sample(imageSampler, in.uv)); +} +)"; +}; + +} // namespace mln::shaders diff --git a/include/mln/shaders/particle_gpu_validation_ubo.hpp b/include/mln/shaders/particle_gpu_validation_ubo.hpp new file mode 100644 index 000000000..0c4b20e52 --- /dev/null +++ b/include/mln/shaders/particle_gpu_validation_ubo.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +namespace mln { +namespace shaders { + +// VALIDATION ONLY: 30-02 State/Trail and 30-03 Camera/Mask proof parameters. +struct alignas(16) ParticleGpuValidationUBO { + std::array pass; + std::array marker; + std::array mask; + // Formal RasterParticle visual controls: particle opacity, canonical + // streak length, trail opacity, and Trail A/B fade multiplier. + std::array visual; + // west, north, east, south in normalized Web Mercator world coordinates. + // Field textures are regional; they must never be sampled as global UVs. + std::array fieldBounds; + // Preferred respawn domain. It is the expanded stable viewport intersected + // with fieldBounds, or fieldBounds when that intersection is unusable. + std::array respawnBounds; + // Per-texture signed vector decoding scales. WeatherColor also uses these + // for its explicitly selected generic scalar/value plane. + // z is viewportDensityActive for the formal validation path. + std::array fieldScale; + // x: Direction semantics flag; y: Direction-only advection scale; + // z: mixed-direction fail-closed epsilon; w: reserved. + std::array advection; + // Host-owned base colours. They are independent from WeatherColor stops. + std::array particleColor; + std::array trailColor; + std::array, 8> weatherStops; + std::array, 8> weatherColors; + std::array weatherRamp; + std::array matrix; +}; +static_assert(sizeof(ParticleGpuValidationUBO) == 31 * 16); + +} // namespace shaders +} // namespace mln diff --git a/include/mln/shaders/shader_defines.hpp b/include/mln/shaders/shader_defines.hpp index 9cf6a7cdb..760e04316 100644 --- a/include/mln/shaders/shader_defines.hpp +++ b/include/mln/shaders/shader_defines.hpp @@ -127,6 +127,11 @@ enum { heatmapTextureUBOCount }; +enum { + idParticleGpuValidationUBO = getEnumValue(drawableReservedUBOCount, layerUBOStartId), + particleGpuValidationUBOCount +}; + enum { idHillshadeEvaluatedPropsUBO = getEnumValue(hillshadeLayerSSBOCount, layerUBOStartId), hillshadeLayerUBOCount @@ -279,6 +284,7 @@ static constexpr uint32_t maxUBOCountPerShader = std::max({static_cast static_cast(fillUBOCount), static_cast(fillExtrusionUBOCount), static_cast(heatmapTextureUBOCount), + static_cast(particleGpuValidationUBOCount), static_cast(heatmapUBOCount), static_cast(hillshadePrepareUBOCount), static_cast(hillshadeUBOCount), @@ -343,6 +349,20 @@ enum { heatmapTextureCount }; +enum { + idParticleGpuValidationPreviousTexture, + idParticleGpuValidationMaskTexture, + idParticleGpuValidationFieldTexture, + idParticleGpuValidationNextFieldTexture, + idParticleGpuValidationStateTexture, + particleGpuValidationTextureCount +}; + +enum { + idRetainedUnderlayCompositeTexture, + retainedUnderlayCompositeTextureCount +}; + enum { idHillshadeImageTexture, hillshadeTextureCount @@ -387,6 +407,8 @@ static constexpr uint32_t maxTextureCountPerShader = std::max({static_cast(fillTextureCount), static_cast(fillExtrusionTextureCount), static_cast(heatmapTextureCount), + static_cast(particleGpuValidationTextureCount), + static_cast(retainedUnderlayCompositeTextureCount), static_cast(hillshadeTextureCount), static_cast(colorReliefTextureCount), static_cast(lineTextureCount), @@ -400,6 +422,16 @@ enum { backgroundVertexAttributeCount }; +enum { + idParticleGpuValidationPosVertexAttribute, + particleGpuValidationVertexAttributeCount +}; + +enum { + idRetainedUnderlayCompositePosVertexAttribute, + retainedUnderlayCompositeVertexAttributeCount +}; + enum { idCirclePosVertexAttribute, @@ -581,6 +613,7 @@ enum { static constexpr uint32_t maxAttributeCountPerShader = std::max({ static_cast(backgroundVertexAttributeCount), + static_cast(particleGpuValidationVertexAttributeCount), static_cast(circleVertexAttributeCount), static_cast(clippingMaskVertexAttributeCount), static_cast(collisionVertexAttributeCount), diff --git a/include/mln/shaders/shader_manifest.hpp b/include/mln/shaders/shader_manifest.hpp index a0011a54d..627d2745f 100644 --- a/include/mln/shaders/shader_manifest.hpp +++ b/include/mln/shaders/shader_manifest.hpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/include/mln/shaders/shader_source.hpp b/include/mln/shaders/shader_source.hpp index d9282c568..c86a9b987 100644 --- a/include/mln/shaders/shader_source.hpp +++ b/include/mln/shaders/shader_source.hpp @@ -30,6 +30,8 @@ enum class BuiltIn { FillExtrusionPatternInstancedShader, HeatmapShader, HeatmapTextureShader, + ParticleGpuValidationShader, + RetainedUnderlayCompositeShader, HillshadePrepareShader, ColorReliefShader, HillshadeShader, diff --git a/include/mln/style/layers/particle_validation_layer.hpp b/include/mln/style/layers/particle_validation_layer.hpp new file mode 100644 index 000000000..72662b54b --- /dev/null +++ b/include/mln/style/layers/particle_validation_layer.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +namespace mln { +namespace style { + +// VALIDATION ONLY: minimal source-less Core layer for 30-01 architecture validation. +class ParticleValidationLayer final : public Layer { +public: + explicit ParticleValidationLayer(const std::string& layerID); + ~ParticleValidationLayer() override; + + bool isActive() const; + void setActive(bool); + uint8_t getMaskMode() const; + void setMaskMode(uint8_t); + uint8_t getFixtureMode() const; + void setFixtureMode(uint8_t); + + class Impl; + const Impl& impl() const; + Mutable mutableImpl() const; + +private: + explicit ParticleValidationLayer(Immutable); + + std::optional setPropertyInternal(const std::string&, const conversion::Convertible&) override; + StyleProperty getProperty(const std::string&) const override; + Value serialize() const override; + std::unique_ptr cloneRef(const std::string& id) const override; + Mutable mutableBaseImpl() const override; +}; + +} // namespace style +} // namespace mln diff --git a/include/mln/style/layers/raster_particle_layer.hpp b/include/mln/style/layers/raster_particle_layer.hpp new file mode 100644 index 000000000..23497fad4 --- /dev/null +++ b/include/mln/style/layers/raster_particle_layer.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include +#include + +namespace mln::style { + +class RasterParticleLayer final : public Layer { +public: + RasterParticleLayer(const std::string& layerID, const std::string& sourceID); + ~RasterParticleLayer() override; + bool isActive() const; + void setActive(bool); + uint8_t getMaskMode() const; + void setMaskMode(uint8_t); + float getTimeMix() const; + void setTimeMix(float); + uint32_t getParticleCount() const; + void setParticleCount(uint32_t); + float getLineWidth() const; + void setLineWidth(float); + float getParticleLength() const; + void setParticleLength(float); + float getParticleOpacity() const; + void setParticleOpacity(float); + float getTrailOpacity() const; + void setTrailOpacity(float); + float getTrailFade() const; + void setTrailFade(float); + float getAdvectionScale() const; + void setAdvectionScale(float); + bool isSpeedColorEnabled() const; + void setSpeedColorEnabled(bool); + std::array getParticleColor() const; + void setParticleColor(std::array); + std::array getTrailColor() const; + void setTrailColor(std::array); + class Impl; + const Impl& impl() const; + Mutable mutableImpl() const; + +private: + explicit RasterParticleLayer(Immutable); + std::optional setPropertyInternal(const std::string&, const conversion::Convertible&) override; + StyleProperty getProperty(const std::string&) const override; + Value serialize() const override; + std::unique_ptr cloneRef(const std::string&) const override; + Mutable mutableBaseImpl() const override; +}; + +} // namespace mln::style diff --git a/include/mln/style/layers/weather_color_layer.hpp b/include/mln/style/layers/weather_color_layer.hpp new file mode 100644 index 000000000..15c7473c4 --- /dev/null +++ b/include/mln/style/layers/weather_color_layer.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include + +namespace mln::style { + +// The renderer must be told which canonical plane produces color values. +// Defaulting to VectorMagnitude preserves the v1 Wind color behavior. +enum class FieldValueSource : uint8_t { Scalar, VectorMagnitude }; + +// A field-speed colour display. It deliberately remains distinct from +// RasterParticleLayer: both consume the same canonical field source but own +// separate GPU resources and draw passes. +class WeatherColorLayer final : public Layer { +public: + WeatherColorLayer(const std::string& layerID, const std::string& sourceID); + ~WeatherColorLayer() override; + float getOpacity() const; + void setOpacity(float); + const std::vector& getColorStops() const; + bool setColorStops(std::vector); + FieldValueSource getFieldValueSource() const; + void setFieldValueSource(FieldValueSource); + class Impl; + const Impl& impl() const; + Mutable mutableImpl() const; + +private: + explicit WeatherColorLayer(Immutable); + std::optional setPropertyInternal(const std::string&, const conversion::Convertible&) override; + StyleProperty getProperty(const std::string&) const override; + Value serialize() const override; + std::unique_ptr cloneRef(const std::string&) const override; + Mutable mutableBaseImpl() const override; +}; + +} // namespace mln::style diff --git a/include/mln/style/layers/weather_color_stop.hpp b/include/mln/style/layers/weather_color_stop.hpp new file mode 100644 index 000000000..562fc4a17 --- /dev/null +++ b/include/mln/style/layers/weather_color_stop.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +namespace mln::style { + +// Straight normalized RGBA validation. Particle colours do not depend on ramps. +inline bool validWeatherRGBA(const std::array& color) { + for (float c : color) if (!std::isfinite(c) || c < 0.0f || c > 1.0f) return false; + return true; +} +struct WeatherColorStop { + // A generic field value. Its unit and meaning come from the explicitly + // selected field value source, never from this color ramp. + float value; + std::array color; + bool operator==(const WeatherColorStop& other) const { return value == other.value && color == other.color; } +}; +constexpr size_t MaxWeatherColorStops = 8; +inline bool validWeatherColorStops(const std::vector& stops) { + if (stops.size() < 2 || stops.size() > MaxWeatherColorStops) return false; + for (size_t i = 0; i < stops.size(); ++i) { + if (!std::isfinite(stops[i].value) || !validWeatherRGBA(stops[i].color) || + (i && stops[i - 1].value >= stops[i].value)) return false; + } + return true; +} +} // namespace mln::style diff --git a/include/mln/style/sources/raster_array_source.hpp b/include/mln/style/sources/raster_array_source.hpp new file mode 100644 index 000000000..068dcd34d --- /dev/null +++ b/include/mln/style/sources/raster_array_source.hpp @@ -0,0 +1,327 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mln { +class AsyncRequest; + +namespace style { + +// A Source-owned, preloaded validation candidate. Production runtimes can use +// updateRuntimeFieldWindow() directly with an already decoded snapshot instead. +struct RasterArrayRuntimeFieldDescriptor { + std::string fixtureID; + std::string metadataURL; + std::string fieldURL; + // Validation-only logical time in milliseconds. A production adapter owns + // its own timeline metadata; this keeps fixtures deterministic. + uint64_t timestamp = 0; +}; + +// A provider-neutral local feed pair. The source only transports the two +// resources; LocalParticleFieldFeed owns metadata decoding and validation. +struct RasterArrayLocalFieldDescriptor { + std::string metadataURL; + std::string fieldURL; +}; + +enum class RasterArrayPlaybackState : uint8_t { + Ready, + Playing, + Paused, + Waiting, + Ended +}; + +enum class RasterArrayTargetFps : uint8_t { + Auto = 0, + Fps30 = 30, + Fps60 = 60 +}; + +struct RasterArrayTimelineFrame { + std::shared_ptr t0; + std::shared_ptr t1; + float mix = 0.0f; + size_t t0Index = 0; + size_t t1Index = 0; + uint64_t windowRevision = 0; + uint64_t cadenceRevision = 0; + RasterArrayPlaybackState state = RasterArrayPlaybackState::Ready; +}; + +struct RasterArrayCadenceAdvance { + RasterArrayTimelineFrame frame; + bool advanced = false; +}; + +// A backend-neutral cadence gate for renderer-owned visual simulation. Unlike +// the legacy timeline it has no field entries and never advances forecast +// time; Android/iOS may merely request a future MapLibre frame at its deadline. +class RasterArrayRendererCadence final { +public: + bool consumeIfDue(std::chrono::steady_clock::time_point); + bool cadenceDue(std::chrono::steady_clock::time_point) const; + void setTargetFps(RasterArrayTargetFps); + RasterArrayTargetFps targetFps() const; + uint64_t revision() const; + +private: + mutable std::mutex mutex; + RasterArrayTargetFps targetFps_ = RasterArrayTargetFps::Auto; + std::chrono::steady_clock::time_point lastTick{}; + uint64_t revision_ = 0; +}; + +// Source-owned playback state. Android only sends low-frequency commands; +// render frames obtain an immutable T0/T1 pair and scalar mix from Core. +class RasterArrayTimelineRuntime { +public: + struct Entry { + std::shared_ptr field; + uint64_t timestamp = 0; + }; + + explicit RasterArrayTimelineRuntime(std::vector); + bool append(Entry); + // Atomically claims one shared cadence deadline and advances wall-time + // playback at most once. Either weather renderer may consume it. + RasterArrayCadenceAdvance advanceIfDue(std::chrono::steady_clock::time_point); + RasterArrayTimelineFrame tick(); + bool play(); + bool pause(); + bool setSpeed(float); + bool seek(size_t); + bool seekTimestamp(uint64_t); + bool step(int); + bool injectGap(); + bool injectInvalid(); + bool recover(); + bool setTargetFps(RasterArrayTargetFps); + RasterArrayTargetFps targetFps() const; + bool cadenceDue(std::chrono::steady_clock::time_point) const; + void markCadenceTick(std::chrono::steady_clock::time_point); + RasterArrayTimelineFrame frame() const; + float playbackSpeed() const; + size_t size() const; + Entry lastEntry() const; + size_t cacheCapacity() const { return cacheCapacity_; } + +private: + RasterArrayTimelineFrame frameLocked() const; + bool moveToLocked(size_t); + mutable std::mutex mutex; + std::vector entries; + size_t t0 = 0; + float mix = 0.0f; + float speed = 1.0f; + RasterArrayPlaybackState state = RasterArrayPlaybackState::Ready; + bool gap = false; + bool invalid = false; + RasterArrayPlaybackState stateBeforeWait = RasterArrayPlaybackState::Paused; + uint64_t windowRevision = 1; + uint64_t cadenceRevision = 0; + std::chrono::steady_clock::time_point lastTick{}; + std::chrono::steady_clock::time_point lastCadenceTick{}; + std::chrono::steady_clock::time_point lastCadenceLog{}; + uint64_t cadenceTickCount = 0; + RasterArrayTargetFps targetFps_ = RasterArrayTargetFps::Auto; + const size_t cacheCapacity_ = 8; +}; + +enum class RasterArrayRuntimeFieldUpdateResult : uint8_t { + Applied, + Noop, + RejectedNotLoaded, + RejectedInvalidSnapshot, + RejectedIncompatibleSnapshot, + RejectedNonMonotonicGeneration, + RejectedCandidateNotReady, + RejectedNoCandidate, +}; + +// Stable low-frequency Host -> Core submission result. Positive values are +// accepted outcomes, while negative values are structured fail-closed errors. +enum class RasterArrayHostFieldSubmitResult : int8_t { + RejectedSourceMode = -1, + RejectedAdapter = -2, + RejectedIncompatibleSnapshot = -3, + RejectedNonMonotonicSnapshot = -4, + RejectedCapacity = -5, + AcceptedPendingInitialWindow = 1, + InitialWindowReady = 2, + AcceptedNextSnapshot = 3, + Duplicate = 4, +}; + +const char* rasterArrayHostFieldSubmitResultName(RasterArrayHostFieldSubmitResult); + +// Stable outcome for the isolated QBIN V4 Host ingress. This is deliberately +// separate from the legacy V3 host-field result above: V4 does not populate a +// renderer-facing T0/T1 timeline. +enum class RasterArrayEncodedFieldSubmitResult : int8_t { + RejectedDecode = -1, + RejectedContract = -2, + RejectedInvalidFieldKey = -3, + RejectedIdentityConflict = -4, + RejectedStaleRevision = -5, + RejectedIncompatible = -6, + RejectedCapacity = -7, + AcceptedInserted = 1, + AcceptedReplacedRevision = 2, + Duplicate = 3, +}; + +const char* rasterArrayEncodedFieldSubmitResultName(RasterArrayEncodedFieldSubmitResult); + +class RasterArraySource final : public Source { +public: + RasterArraySource(std::string id, + std::string fixtureID, + std::string metadataURL, + std::string fieldURL, + std::string nextFixtureID = {}, + std::string nextMetadataURL = {}, + std::string nextFieldURL = {}, + std::vector runtimeFields = {}); + RasterArraySource(std::string id, std::vector localFields); + // Creates an empty Source ingress. The Host submits one canonical Field at + // a time as metadata plus contiguous interleaved U/V bytes. + explicit RasterArraySource(std::string id); + ~RasterArraySource() override; + + class Impl; + const Impl& impl() const; + void loadDescription(FileSource&) override; + // Advances the authoritative Source time window from T0/T1 to T1/newT1. + // The caller transfers an immutable, already decoded Field snapshot; the + // renderer observes the resulting Source change and owns GPU reuse/upload. + RasterArrayRuntimeFieldUpdateResult updateRuntimeFieldWindow(particle::ParticleFieldSnapshot newT1); + // Thin Core-owned submit point for a producer-supplied initial window. + RasterArrayRuntimeFieldUpdateResult submitInitialRuntimeFieldWindow(particle::ParticleFieldSnapshot t0, + particle::ParticleFieldSnapshot t1); + // Validation convenience: advance using the next Source-owned preloaded + // candidate. Android supplies no T0/T1 state and no GPU resource here. + RasterArrayRuntimeFieldUpdateResult advanceRuntimeFieldWindow(); + // Validation convenience: resubmit the current T1 through the formal + // update API. This must return Noop without changing the Source window. + RasterArrayRuntimeFieldUpdateResult repeatRuntimeFieldWindow(); + RasterArrayHostFieldSubmitResult submitHostField(const std::string& metadataText, const std::vector& payload); + // v2 ingress preserves explicit scalar/vector/validity plane presence. + RasterArrayHostFieldSubmitResult submitHostField(const std::string& metadataText, + const particle::ParticleFieldPayloadBundle& payloads); + // Clears producer-owned Host ingress state while retaining the Source and + // its Style layers for a subsequent T0/T1 submission. + bool clearHostFields(); + + // V4 QBIN ingress is a parallel, Core-only owner. It retains per-field + // series and a requested forecast clock but never writes V3 T0/T1/MIX or + // renderer snapshots. + RasterArrayEncodedFieldSubmitResult submitEncodedField( + const std::string& fieldKey, + const particle::ingress::EncodedFieldRuntimeMetadata& runtimeMetadata, + const particle::ResolvedFieldProfileContract& contract, + const std::vector& rawQbinBytes); + void setColorBinding(particle::ColorBinding); + void setParticleBinding(particle::ParticleBinding); + bool setRequestedForecastTime(uint64_t timestampMs); + void setForecastPlaybackState(particle::ForecastPlaybackState); + bool setForecastPlaybackSpeed(float); + bool advanceForecastClock(uint64_t elapsedMs); + // Called once per Map update on the owner thread; render layers only read + // the resulting immutable projection and never advance this clock. + bool tickForecastClock(std::chrono::steady_clock::time_point now); + particle::ColorProjectionResult resolveColorProjection() const; + particle::ParticleProjectionResult resolveParticleProjection() const; + bool clearEncodedField(const std::string& fieldKey); + void clearAllEncodedFields(); + // Explicit terminal cleanup for an attached WeatherRuntime. Unlike an ordinary + // V4 window clear, this also drops bindings, forecast clock and V3 host ingress. + // Source and Style layers remain installed for the next runtime generation. + bool resetWeatherRuntimeState(); + // Producer-facing timestamp seek. Core alone selects T0/T1 and MIX. + bool seekRuntimeTimelineToTimestamp(uint64_t timestamp); + // Low-frequency JNI commands for the Source-owned validation timeline. + bool controlRuntimeTimeline(int command, float value = 0.0f); + bool setRuntimeTargetFps(uint8_t targetFps); + uint8_t runtimeTargetFps() const; + bool supportsLayerType(const LayerTypeInfo*) const override; + mapbox::base::WeakPtr makeWeakPtr() override { return weakFactory.makeWeakPtr(); } + +protected: + Mutable createMutable() const noexcept override; + +private: + RasterArrayHostFieldSubmitResult submitDecodedHostField(particle::ParticleFieldSnapshot snapshot); + struct RuntimeFieldCandidate { + RasterArrayRuntimeFieldDescriptor descriptor; + std::unique_ptr metadataRequest; + std::unique_ptr fieldRequest; + std::optional metadata; + std::optional> fieldBytes; + std::optional snapshot; + }; + struct LocalFieldCandidate { + RasterArrayLocalFieldDescriptor descriptor; + std::unique_ptr metadataRequest; + std::unique_ptr fieldRequest; + std::optional metadata; + std::optional> fieldBytes; + std::optional snapshot; + }; + + void tryLoad(); + void tryLoadRuntimeField(size_t); + void tryLoadLocalField(size_t); + void tryInitializeTimeline(); + // Re-resolve and materialize the independent V4 render view. This never + // writes legacy timeline/field state. + void refreshV4RendererState(); + RasterArrayRuntimeFieldUpdateResult rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult, + const std::string& reason) const; + + std::unique_ptr metadataRequest; + std::unique_ptr fieldRequest; + std::unique_ptr nextMetadataRequest; + std::unique_ptr nextFieldRequest; + std::optional metadata; + std::optional> fieldBytes; + std::optional nextMetadata; + std::optional> nextFieldBytes; + std::vector runtimeFields; + std::vector localFields; + std::optional pendingHostField; + size_t nextRuntimeField = 0; + uint64_t runtimeUpdateSequence = 0; + std::shared_ptr timeline; + RasterArrayTargetFps targetFps = RasterArrayTargetFps::Auto; + // Deliberately independent from `timeline`: V4 bindings retain Host intent + // across clear operations and resolve their own field-keyed windows. + particle::EncodedFieldStore encodedFieldStore; + particle::ForecastClock forecastClock; + std::chrono::steady_clock::time_point lastForecastTick{}; + // A Source change synchronously invokes Style::onUpdate, which re-enters + // Map::onUpdate. Prevent a second forecast tick during the first notification. + bool forecastTickInProgress = false; + std::optional colorBinding; + std::optional particleBinding; + mapbox::base::WeakPtrFactory weakFactory{this}; +}; + +template <> +inline bool Source::is() const { + return getType() == SourceType::RasterArray; +} + +} // namespace style +} // namespace mln diff --git a/include/mln/style/types.hpp b/include/mln/style/types.hpp index 9a4e689b7..0a73aec5c 100644 --- a/include/mln/style/types.hpp +++ b/include/mln/style/types.hpp @@ -16,7 +16,8 @@ enum class SourceType : uint8_t { Annotations, Image, CustomVector, - CustomMVTVector + CustomMVTVector, + RasterArray }; enum class VisibilityType : bool { diff --git a/include/mln/vulkan/context.hpp b/include/mln/vulkan/context.hpp index 3bfe73632..f763d9131 100644 --- a/include/mln/vulkan/context.hpp +++ b/include/mln/vulkan/context.hpp @@ -106,7 +106,10 @@ public: void setDirtyState() override {} - std::unique_ptr createOffscreenTexture(Size, gfx::TextureChannelDataType, bool, bool); + std::unique_ptr createOffscreenTexture(Size, + gfx::TextureChannelDataType, + bool, + bool) override; std::unique_ptr createOffscreenTexture(Size, gfx::TextureChannelDataType) override; diff --git a/include/mln/webgpu/context.hpp b/include/mln/webgpu/context.hpp index bf66c6e67..f356520a2 100644 --- a/include/mln/webgpu/context.hpp +++ b/include/mln/webgpu/context.hpp @@ -43,7 +43,10 @@ public: void reduceMemoryUsage() override; // Resource creation - std::unique_ptr createOffscreenTexture(Size, gfx::TextureChannelDataType, bool, bool); + std::unique_ptr createOffscreenTexture(Size, + gfx::TextureChannelDataType, + bool, + bool) override; std::unique_ptr createOffscreenTexture(Size, gfx::TextureChannelDataType) override; std::unique_ptr createCommandEncoder() override; gfx::VertexAttributeArrayPtr createVertexAttributeArray() const override; diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090912/conversion_summary.json b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/conversion_summary.json new file mode 100644 index 000000000..a7015f208 --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/conversion_summary.json @@ -0,0 +1,35 @@ +{ + "grid": { + "width": 6, + "height": 4 + }, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "validTime": 1788955200000, + "generation": 1, + "source": { + "kind": "decoder-contract fallback", + "notReal": true, + "referenceTime": "2026090912", + "targetTime": "2026090912", + "说明": "本机未实现 PMTiles reader;此数值不是正式 WeatherV2 栅格。" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "09484872f71b85d73159e9ce51ef4c9d0abc8620f9e2c4f1155fd8b33acf2a61" + } + }, + "validCount": 24, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090912/metadata.json b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/metadata.json new file mode 100644 index 000000000..c14094471 --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/metadata.json @@ -0,0 +1,50 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "rain-decoder-fixture", + "generation": 1, + "dataVersion": 1, + "validTime": 1788955200000, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "grid": { + "width": 6, + "height": 4 + }, + "validity": { + "encoding": "uint8", + "byteLength": 24 + }, + "source": { + "kind": "decoder-contract fallback", + "notReal": true, + "referenceTime": "2026090912", + "targetTime": "2026090912", + "说明": "本机未实现 PMTiles reader;此数值不是正式 WeatherV2 栅格。" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 96, + "noData": -9999.0 + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "09484872f71b85d73159e9ce51ef4c9d0abc8620f9e2c4f1155fd8b33acf2a61" + } + } +} diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090912/scalar.f32 b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/scalar.f32 new file mode 100644 index 000000000..7d5c9df7c Binary files /dev/null and b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/scalar.f32 differ diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090912/validity.u8 b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/validity.u8 new file mode 100644 index 000000000..e03959b11 --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090912/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090918/conversion_summary.json b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/conversion_summary.json new file mode 100644 index 000000000..9e103084c --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/conversion_summary.json @@ -0,0 +1,35 @@ +{ + "grid": { + "width": 6, + "height": 4 + }, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "validTime": 1788976800000, + "generation": 2, + "source": { + "kind": "decoder-contract fallback", + "notReal": true, + "referenceTime": "2026090912", + "targetTime": "2026090918", + "说明": "本机未实现 PMTiles reader;此数值不是正式 WeatherV2 栅格。" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "74a93cc48d3875f0abd73b1ee14cc0f1a7293cd7dc7ef716a15725a162588855" + } + }, + "validCount": 24, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090918/metadata.json b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/metadata.json new file mode 100644 index 000000000..7fc6736ba --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/metadata.json @@ -0,0 +1,50 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "rain-decoder-fixture", + "generation": 2, + "dataVersion": 2, + "validTime": 1788976800000, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "grid": { + "width": 6, + "height": 4 + }, + "validity": { + "encoding": "uint8", + "byteLength": 24 + }, + "source": { + "kind": "decoder-contract fallback", + "notReal": true, + "referenceTime": "2026090912", + "targetTime": "2026090918", + "说明": "本机未实现 PMTiles reader;此数值不是正式 WeatherV2 栅格。" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 96, + "noData": -9999.0 + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "74a93cc48d3875f0abd73b1ee14cc0f1a7293cd7dc7ef716a15725a162588855" + } + } +} diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090918/scalar.f32 b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/scalar.f32 new file mode 100644 index 000000000..99097bd2d Binary files /dev/null and b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/scalar.f32 differ diff --git a/integration/particle-v3-fields/rain-decoder-fixture/2026090918/validity.u8 b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/validity.u8 new file mode 100644 index 000000000..e03959b11 --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/2026090918/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/rain-decoder-fixture/dataset.json b/integration/particle-v3-fields/rain-decoder-fixture/dataset.json new file mode 100644 index 000000000..ea2c9fce2 --- /dev/null +++ b/integration/particle-v3-fields/rain-decoder-fixture/dataset.json @@ -0,0 +1,26 @@ +{ + "schema": "maplibre-particle-test-dataset/v2", + "datasetId": "rain-decoder-fixture", + "sourceProvenance": "非真实场值:仅 decoder-contract fallback,不能当作 WeatherV2 实测栅格", + "frames": [ + { + "label": "T0", + "path": "2026090912", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32" + }, + { + "label": "T1", + "path": "2026090918", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32" + } + ], + "expectedFieldValueSource": "SCALAR", + "particleExpected": false, + "vectorSemantics": null, + "suggestedAdvectionScale": 1.0, + "说明": "离线 canonical fixture;Runtime 只接收 metadata 与本地 plane URI。" +} diff --git a/integration/particle-v3-fields/tide-real/2026090901/conversion_summary.json b/integration/particle-v3-fields/tide-real/2026090901/conversion_summary.json new file mode 100644 index 000000000..3eaa450ba --- /dev/null +++ b/integration/particle-v3-fields/tide-real/2026090901/conversion_summary.json @@ -0,0 +1,66 @@ +{ + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "validTime": 1788915600000, + "generation": 1, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090901/current_vectors.bin", + "sourceSha256": "4de8b9843fcba13b452428cde7f1cf2b0a85ec0eb775c454fc6350dcdd527b13", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090901/current_vectors.bin.header.json", + "headerSha256": "5bde915b08ae4e5e41d0a45b50b0a4a1d4fbed5c7e513b00fd3d988d42697070", + "referenceTime": "2026090900", + "targetTime": "2026090901", + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "binMagic": "WVECBIN1", + "binFields": [ + { + "name": "u", + "dtype": "float32" + }, + { + "name": "v", + "dtype": "float32" + }, + { + "name": "speed", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 263169, + "sha256": "14e745cea79d2cb85b66ef8fc5e19c28df8f912fb975f9281becc2ce6562e597" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 2105352, + "sha256": "a0c767bde25d16a0e6beb30017f430d48ce95ba1a896bda477ba3f078af386cd" + } + }, + "validCount": 146779, + "invalidCount": 116390 +} diff --git a/integration/particle-v3-fields/tide-real/2026090901/metadata.json b/integration/particle-v3-fields/tide-real/2026090901/metadata.json new file mode 100644 index 000000000..42675cc9c --- /dev/null +++ b/integration/particle-v3-fields/tide-real/2026090901/metadata.json @@ -0,0 +1,85 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "tide-real", + "generation": 1, + "dataVersion": 1, + "validTime": 1788915600000, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "grid": { + "width": 513, + "height": 513 + }, + "validity": { + "encoding": "uint8", + "byteLength": 263169 + }, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090901/current_vectors.bin", + "sourceSha256": "4de8b9843fcba13b452428cde7f1cf2b0a85ec0eb775c454fc6350dcdd527b13", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090901/current_vectors.bin.header.json", + "headerSha256": "5bde915b08ae4e5e41d0a45b50b0a4a1d4fbed5c7e513b00fd3d988d42697070", + "referenceTime": "2026090900", + "targetTime": "2026090901", + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "binMagic": "WVECBIN1", + "binFields": [ + { + "name": "u", + "dtype": "float32" + }, + { + "name": "v", + "dtype": "float32" + }, + { + "name": "speed", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 2105352, + "noData": -9999.0, + "semantics": "VELOCITY", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 263169, + "sha256": "14e745cea79d2cb85b66ef8fc5e19c28df8f912fb975f9281becc2ce6562e597" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 2105352, + "sha256": "a0c767bde25d16a0e6beb30017f430d48ce95ba1a896bda477ba3f078af386cd" + } + } +} diff --git a/integration/particle-v3-fields/tide-real/2026090901/validity.u8 b/integration/particle-v3-fields/tide-real/2026090901/validity.u8 new file mode 100644 index 000000000..1d199ebc5 Binary files /dev/null and b/integration/particle-v3-fields/tide-real/2026090901/validity.u8 differ diff --git a/integration/particle-v3-fields/tide-real/2026090901/vector.f32 b/integration/particle-v3-fields/tide-real/2026090901/vector.f32 new file mode 100644 index 000000000..0fd8bfa59 Binary files /dev/null and b/integration/particle-v3-fields/tide-real/2026090901/vector.f32 differ diff --git a/integration/particle-v3-fields/tide-real/2026090902/conversion_summary.json b/integration/particle-v3-fields/tide-real/2026090902/conversion_summary.json new file mode 100644 index 000000000..624eb9bc5 --- /dev/null +++ b/integration/particle-v3-fields/tide-real/2026090902/conversion_summary.json @@ -0,0 +1,66 @@ +{ + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "validTime": 1788919200000, + "generation": 2, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090902/current_vectors.bin", + "sourceSha256": "2410eb208e11fdb73db9b8395b70cc5377dd078a4494ae3a67dbfa2f07280d34", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090902/current_vectors.bin.header.json", + "headerSha256": "b0001a7976dd3a6c9e626faea96d68519bf3e2d5e56cc5ce1aedfa41accf1b99", + "referenceTime": "2026090900", + "targetTime": "2026090902", + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "binMagic": "WVECBIN1", + "binFields": [ + { + "name": "u", + "dtype": "float32" + }, + { + "name": "v", + "dtype": "float32" + }, + { + "name": "speed", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 263169, + "sha256": "14e745cea79d2cb85b66ef8fc5e19c28df8f912fb975f9281becc2ce6562e597" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 2105352, + "sha256": "a144307fdfd1172b6f2da8af0dc50ff27a4444da3989149c9bec2d1d73c55f21" + } + }, + "validCount": 146779, + "invalidCount": 116390 +} diff --git a/integration/particle-v3-fields/tide-real/2026090902/metadata.json b/integration/particle-v3-fields/tide-real/2026090902/metadata.json new file mode 100644 index 000000000..7bb877170 --- /dev/null +++ b/integration/particle-v3-fields/tide-real/2026090902/metadata.json @@ -0,0 +1,85 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "tide-real", + "generation": 2, + "dataVersion": 2, + "validTime": 1788919200000, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "grid": { + "width": 513, + "height": 513 + }, + "validity": { + "encoding": "uint8", + "byteLength": 263169 + }, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090902/current_vectors.bin", + "sourceSha256": "2410eb208e11fdb73db9b8395b70cc5377dd078a4494ae3a67dbfa2f07280d34", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/cmems/tide/2026090900/current_bin/2026090902/current_vectors.bin.header.json", + "headerSha256": "b0001a7976dd3a6c9e626faea96d68519bf3e2d5e56cc5ce1aedfa41accf1b99", + "referenceTime": "2026090900", + "targetTime": "2026090902", + "grid": { + "width": 513, + "height": 513 + }, + "bounds": { + "west": 90.0, + "south": 0.0, + "east": 180.0, + "north": 66.51326044311186 + }, + "binMagic": "WVECBIN1", + "binFields": [ + { + "name": "u", + "dtype": "float32" + }, + { + "name": "v", + "dtype": "float32" + }, + { + "name": "speed", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 2105352, + "noData": -9999.0, + "semantics": "VELOCITY", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 263169, + "sha256": "14e745cea79d2cb85b66ef8fc5e19c28df8f912fb975f9281becc2ce6562e597" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 2105352, + "sha256": "a144307fdfd1172b6f2da8af0dc50ff27a4444da3989149c9bec2d1d73c55f21" + } + } +} diff --git a/integration/particle-v3-fields/tide-real/2026090902/validity.u8 b/integration/particle-v3-fields/tide-real/2026090902/validity.u8 new file mode 100644 index 000000000..1d199ebc5 Binary files /dev/null and b/integration/particle-v3-fields/tide-real/2026090902/validity.u8 differ diff --git a/integration/particle-v3-fields/tide-real/2026090902/vector.f32 b/integration/particle-v3-fields/tide-real/2026090902/vector.f32 new file mode 100644 index 000000000..5a14440a2 Binary files /dev/null and b/integration/particle-v3-fields/tide-real/2026090902/vector.f32 differ diff --git a/integration/particle-v3-fields/tide-real/dataset.json b/integration/particle-v3-fields/tide-real/dataset.json new file mode 100644 index 000000000..47696b519 --- /dev/null +++ b/integration/particle-v3-fields/tide-real/dataset.json @@ -0,0 +1,26 @@ +{ + "schema": "maplibre-particle-test-dataset/v2", + "datasetId": "tide-real", + "sourceProvenance": "真实本地 WeatherV2 CMEMS WVECBIN1;仅 U/V VELOCITY", + "frames": [ + { + "label": "T0", + "path": "2026090901", + "metadata": "metadata.json", + "validity": "validity.u8", + "vector": "vector.f32" + }, + { + "label": "T1", + "path": "2026090902", + "metadata": "metadata.json", + "validity": "validity.u8", + "vector": "vector.f32" + } + ], + "expectedFieldValueSource": "VECTOR_MAGNITUDE", + "particleExpected": true, + "vectorSemantics": "VELOCITY", + "suggestedAdvectionScale": 1.0, + "说明": "离线 canonical fixture;Runtime 只接收 metadata 与本地 plane URI。" +} diff --git a/integration/particle-v3-fields/wave-real/2026090918/conversion_summary.json b/integration/particle-v3-fields/wave-real/2026090918/conversion_summary.json new file mode 100644 index 000000000..3d1c0e381 --- /dev/null +++ b/integration/particle-v3-fields/wave-real/2026090918/conversion_summary.json @@ -0,0 +1,82 @@ +{ + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "validTime": 1788976800000, + "generation": 1, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090918/waves_particles.bin", + "sourceSha256": "5fadc8c4b67c0e111903df93603436a2aca94f736e2cefde3ec3df1011263c14", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090918/waves_particles.bin.header.json", + "headerSha256": "cd7b3b2a8c508ae5b5a021496311613c7e82cf857587c20f8d74c0a12738f307", + "referenceTime": "2026090912", + "targetTime": "2026090918", + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "binMagic": "WAVEBIN1", + "binFields": [ + { + "name": "height_m", + "dtype": "float32", + "unit": "m" + }, + { + "name": "period_s", + "dtype": "float32", + "unit": "s" + }, + { + "name": "direction_from_degree", + "dtype": "float32", + "unit": "degree" + }, + { + "name": "unit_to_u", + "dtype": "float32" + }, + { + "name": "unit_to_v", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "e94dba4163df1468ede4cd6cdb720162328b0bdf3053a868855cb16a424bd3a6" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 1577988, + "sha256": "0824b72e3b737d22fcaf4ff3447b6737fe12cbaa2a44d233f5b1710f81586d57" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "640dfb7126db6117507fb864aebc2ecfb3dade2726ff0592e7c87d7a3e912276" + } + }, + "validCount": 295196, + "invalidCount": 99301 +} diff --git a/integration/particle-v3-fields/wave-real/2026090918/metadata.json b/integration/particle-v3-fields/wave-real/2026090918/metadata.json new file mode 100644 index 000000000..fbc63b804 --- /dev/null +++ b/integration/particle-v3-fields/wave-real/2026090918/metadata.json @@ -0,0 +1,106 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wave-real", + "generation": 1, + "dataVersion": 1, + "validTime": 1788976800000, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "grid": { + "width": 513, + "height": 769 + }, + "validity": { + "encoding": "uint8", + "byteLength": 394497 + }, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090918/waves_particles.bin", + "sourceSha256": "5fadc8c4b67c0e111903df93603436a2aca94f736e2cefde3ec3df1011263c14", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090918/waves_particles.bin.header.json", + "headerSha256": "cd7b3b2a8c508ae5b5a021496311613c7e82cf857587c20f8d74c0a12738f307", + "referenceTime": "2026090912", + "targetTime": "2026090918", + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "binMagic": "WAVEBIN1", + "binFields": [ + { + "name": "height_m", + "dtype": "float32", + "unit": "m" + }, + { + "name": "period_s", + "dtype": "float32", + "unit": "s" + }, + { + "name": "direction_from_degree", + "dtype": "float32", + "unit": "degree" + }, + { + "name": "unit_to_u", + "dtype": "float32" + }, + { + "name": "unit_to_v", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 1577988, + "noData": -9999.0 + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 3155976, + "noData": -9999.0, + "semantics": "DIRECTION", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "e94dba4163df1468ede4cd6cdb720162328b0bdf3053a868855cb16a424bd3a6" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 1577988, + "sha256": "0824b72e3b737d22fcaf4ff3447b6737fe12cbaa2a44d233f5b1710f81586d57" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "640dfb7126db6117507fb864aebc2ecfb3dade2726ff0592e7c87d7a3e912276" + } + } +} diff --git a/integration/particle-v3-fields/wave-real/2026090918/scalar.f32 b/integration/particle-v3-fields/wave-real/2026090918/scalar.f32 new file mode 100644 index 000000000..0fb376c65 Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090918/scalar.f32 differ diff --git a/integration/particle-v3-fields/wave-real/2026090918/validity.u8 b/integration/particle-v3-fields/wave-real/2026090918/validity.u8 new file mode 100644 index 000000000..f6b2a2287 Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090918/validity.u8 differ diff --git a/integration/particle-v3-fields/wave-real/2026090918/vector.f32 b/integration/particle-v3-fields/wave-real/2026090918/vector.f32 new file mode 100644 index 000000000..f9e0ab92c Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090918/vector.f32 differ diff --git a/integration/particle-v3-fields/wave-real/2026090919/conversion_summary.json b/integration/particle-v3-fields/wave-real/2026090919/conversion_summary.json new file mode 100644 index 000000000..953772a7b --- /dev/null +++ b/integration/particle-v3-fields/wave-real/2026090919/conversion_summary.json @@ -0,0 +1,82 @@ +{ + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "validTime": 1788980400000, + "generation": 2, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090919/waves_particles.bin", + "sourceSha256": "c084c8ae4b9e601007b3a852c25ac3607c01a4de69dcd87fc875116eccf9d633", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090919/waves_particles.bin.header.json", + "headerSha256": "72b3d79880b2ccd7055b5d1011401cba7938f7e7eb12e81ea6f259319626f421", + "referenceTime": "2026090912", + "targetTime": "2026090919", + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "binMagic": "WAVEBIN1", + "binFields": [ + { + "name": "height_m", + "dtype": "float32", + "unit": "m" + }, + { + "name": "period_s", + "dtype": "float32", + "unit": "s" + }, + { + "name": "direction_from_degree", + "dtype": "float32", + "unit": "degree" + }, + { + "name": "unit_to_u", + "dtype": "float32" + }, + { + "name": "unit_to_v", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "e94dba4163df1468ede4cd6cdb720162328b0bdf3053a868855cb16a424bd3a6" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 1577988, + "sha256": "99a70bc6e07311536844c9b6b481281e05af5f3b1cd1961e2aeb4696a614f789" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "7871320366889daedd4cc4b057f97d321947a29813dd9b55d5df4be1416022ea" + } + }, + "validCount": 295196, + "invalidCount": 99301 +} diff --git a/integration/particle-v3-fields/wave-real/2026090919/metadata.json b/integration/particle-v3-fields/wave-real/2026090919/metadata.json new file mode 100644 index 000000000..07a2d1ccc --- /dev/null +++ b/integration/particle-v3-fields/wave-real/2026090919/metadata.json @@ -0,0 +1,106 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wave-real", + "generation": 2, + "dataVersion": 2, + "validTime": 1788980400000, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "grid": { + "width": 513, + "height": 769 + }, + "validity": { + "encoding": "uint8", + "byteLength": 394497 + }, + "source": { + "kind": "WeatherV2 formal BIN decoded offline", + "sourcePath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090919/waves_particles.bin", + "sourceSha256": "c084c8ae4b9e601007b3a852c25ac3607c01a4de69dcd87fc875116eccf9d633", + "headerPath": "/root/sourceserver/weatherV2/src/dev/_runtime/dev_pipeline_data/datasets/ecmwf-wam/waves/2026090912/wave_bin/2026090919/waves_particles.bin.header.json", + "headerSha256": "72b3d79880b2ccd7055b5d1011401cba7938f7e7eb12e81ea6f259319626f421", + "referenceTime": "2026090912", + "targetTime": "2026090919", + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "binMagic": "WAVEBIN1", + "binFields": [ + { + "name": "height_m", + "dtype": "float32", + "unit": "m" + }, + { + "name": "period_s", + "dtype": "float32", + "unit": "s" + }, + { + "name": "direction_from_degree", + "dtype": "float32", + "unit": "degree" + }, + { + "name": "unit_to_u", + "dtype": "float32" + }, + { + "name": "unit_to_v", + "dtype": "float32" + }, + { + "name": "valid", + "dtype": "uint8" + } + ] + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 1577988, + "noData": -9999.0 + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 3155976, + "noData": -9999.0, + "semantics": "DIRECTION", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "e94dba4163df1468ede4cd6cdb720162328b0bdf3053a868855cb16a424bd3a6" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 1577988, + "sha256": "99a70bc6e07311536844c9b6b481281e05af5f3b1cd1961e2aeb4696a614f789" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "7871320366889daedd4cc4b057f97d321947a29813dd9b55d5df4be1416022ea" + } + } +} diff --git a/integration/particle-v3-fields/wave-real/2026090919/scalar.f32 b/integration/particle-v3-fields/wave-real/2026090919/scalar.f32 new file mode 100644 index 000000000..e25d74ae0 Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090919/scalar.f32 differ diff --git a/integration/particle-v3-fields/wave-real/2026090919/validity.u8 b/integration/particle-v3-fields/wave-real/2026090919/validity.u8 new file mode 100644 index 000000000..f6b2a2287 Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090919/validity.u8 differ diff --git a/integration/particle-v3-fields/wave-real/2026090919/vector.f32 b/integration/particle-v3-fields/wave-real/2026090919/vector.f32 new file mode 100644 index 000000000..e2219c860 Binary files /dev/null and b/integration/particle-v3-fields/wave-real/2026090919/vector.f32 differ diff --git a/integration/particle-v3-fields/wave-real/dataset.json b/integration/particle-v3-fields/wave-real/dataset.json new file mode 100644 index 000000000..12a98c98e --- /dev/null +++ b/integration/particle-v3-fields/wave-real/dataset.json @@ -0,0 +1,28 @@ +{ + "schema": "maplibre-particle-test-dataset/v2", + "datasetId": "wave-real", + "sourceProvenance": "真实本地 WeatherV2 WAVEBIN1;height_m + 正规化 unit_to_u/v,period 不进入 canonical", + "frames": [ + { + "label": "T0", + "path": "2026090918", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32", + "vector": "vector.f32" + }, + { + "label": "T1", + "path": "2026090919", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32", + "vector": "vector.f32" + } + ], + "expectedFieldValueSource": "SCALAR", + "particleExpected": true, + "vectorSemantics": "DIRECTION", + "suggestedAdvectionScale": 1.75, + "说明": "离线 canonical fixture;Runtime 只接收 metadata 与本地 plane URI。" +} diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010100/conversion_summary.json b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/conversion_summary.json new file mode 100644 index 000000000..6be5e7c89 --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/conversion_summary.json @@ -0,0 +1,38 @@ +{ + "grid": { + "width": 6, + "height": 4 + }, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "validTime": 1767225600000, + "generation": 1, + "source": { + "kind": "synthetic independence oracle", + "notReal": true, + "targetTime": "2026010100" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "09484872f71b85d73159e9ce51ef4c9d0abc8620f9e2c4f1155fd8b33acf2a61" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 192, + "sha256": "e34af2311cfe54f9a33aa51b33021eee2df99622903a913aeca1bc4ce1d941a7" + } + }, + "validCount": 24, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010100/metadata.json b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/metadata.json new file mode 100644 index 000000000..b96dd99db --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/metadata.json @@ -0,0 +1,62 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wave-synthetic-independence", + "generation": 1, + "dataVersion": 1, + "validTime": 1767225600000, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "grid": { + "width": 6, + "height": 4 + }, + "validity": { + "encoding": "uint8", + "byteLength": 24 + }, + "source": { + "kind": "synthetic independence oracle", + "notReal": true, + "targetTime": "2026010100" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 96, + "noData": -9999.0 + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 192, + "noData": -9999.0, + "semantics": "DIRECTION", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "09484872f71b85d73159e9ce51ef4c9d0abc8620f9e2c4f1155fd8b33acf2a61" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 192, + "sha256": "e34af2311cfe54f9a33aa51b33021eee2df99622903a913aeca1bc4ce1d941a7" + } + } +} diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010100/scalar.f32 b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/scalar.f32 new file mode 100644 index 000000000..7d5c9df7c Binary files /dev/null and b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/scalar.f32 differ diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010100/validity.u8 b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/validity.u8 new file mode 100644 index 000000000..e03959b11 --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010100/vector.f32 b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/vector.f32 new file mode 100644 index 000000000..7a52e7c9d Binary files /dev/null and b/integration/particle-v3-fields/wave-synthetic-independence/2026010100/vector.f32 differ diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010106/conversion_summary.json b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/conversion_summary.json new file mode 100644 index 000000000..9a5f9180f --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/conversion_summary.json @@ -0,0 +1,38 @@ +{ + "grid": { + "width": 6, + "height": 4 + }, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "validTime": 1767247200000, + "generation": 2, + "source": { + "kind": "synthetic independence oracle", + "notReal": true, + "targetTime": "2026010106" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "74a93cc48d3875f0abd73b1ee14cc0f1a7293cd7dc7ef716a15725a162588855" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 192, + "sha256": "e34af2311cfe54f9a33aa51b33021eee2df99622903a913aeca1bc4ce1d941a7" + } + }, + "validCount": 24, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010106/metadata.json b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/metadata.json new file mode 100644 index 000000000..676476efe --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/metadata.json @@ -0,0 +1,62 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wave-synthetic-independence", + "generation": 2, + "dataVersion": 2, + "validTime": 1767247200000, + "bounds": { + "west": 122.0, + "south": 20.0, + "east": 150.0, + "north": 48.0 + }, + "grid": { + "width": 6, + "height": 4 + }, + "validity": { + "encoding": "uint8", + "byteLength": 24 + }, + "source": { + "kind": "synthetic independence oracle", + "notReal": true, + "targetTime": "2026010106" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "scalar": { + "encoding": "float32-le", + "byteLength": 96, + "noData": -9999.0 + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 192, + "noData": -9999.0, + "semantics": "DIRECTION", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 24, + "sha256": "5b8b4d29020ea5b1bc427c40a0cab2bf944be057ec482110f1d12b68008cd286" + }, + "scalar.f32": { + "path": "scalar.f32", + "byteLength": 96, + "sha256": "74a93cc48d3875f0abd73b1ee14cc0f1a7293cd7dc7ef716a15725a162588855" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 192, + "sha256": "e34af2311cfe54f9a33aa51b33021eee2df99622903a913aeca1bc4ce1d941a7" + } + } +} diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010106/scalar.f32 b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/scalar.f32 new file mode 100644 index 000000000..99097bd2d Binary files /dev/null and b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/scalar.f32 differ diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010106/validity.u8 b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/validity.u8 new file mode 100644 index 000000000..e03959b11 --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/wave-synthetic-independence/2026010106/vector.f32 b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/vector.f32 new file mode 100644 index 000000000..7a52e7c9d Binary files /dev/null and b/integration/particle-v3-fields/wave-synthetic-independence/2026010106/vector.f32 differ diff --git a/integration/particle-v3-fields/wave-synthetic-independence/dataset.json b/integration/particle-v3-fields/wave-synthetic-independence/dataset.json new file mode 100644 index 000000000..b427c93ba --- /dev/null +++ b/integration/particle-v3-fields/wave-synthetic-independence/dataset.json @@ -0,0 +1,28 @@ +{ + "schema": "maplibre-particle-test-dataset/v2", + "datasetId": "wave-synthetic-independence", + "sourceProvenance": "固定 synthetic;height 与 direction 独立", + "frames": [ + { + "label": "T0", + "path": "2026010100", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32", + "vector": "vector.f32" + }, + { + "label": "T1", + "path": "2026010106", + "metadata": "metadata.json", + "validity": "validity.u8", + "scalar": "scalar.f32", + "vector": "vector.f32" + } + ], + "expectedFieldValueSource": "SCALAR", + "particleExpected": true, + "vectorSemantics": "DIRECTION", + "suggestedAdvectionScale": 2.5, + "说明": "离线 canonical fixture;Runtime 只接收 metadata 与本地 plane URI。" +} diff --git a/integration/particle-v3-fields/wind-real/2026080700/conversion_summary.json b/integration/particle-v3-fields/wind-real/2026080700/conversion_summary.json new file mode 100644 index 000000000..d4cb05602 --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080700/conversion_summary.json @@ -0,0 +1,35 @@ +{ + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "validTime": 1786060800000, + "generation": 1786060800000, + "source": { + "kind": "WeatherV2 ECMWF HRES WVECBIN1 canonical v1 upgraded offline", + "referenceTime": "2026080600", + "targetTime": "2026080700", + "originalPayloadSha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283", + "validityDerivation": "finite U/V and both components != -9999.0" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "3100f7cc4f6b7357e857ec10f159c43a092cfaad8eed6327043406579f5808ee" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283" + } + }, + "validCount": 394497, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/wind-real/2026080700/metadata.json b/integration/particle-v3-fields/wind-real/2026080700/metadata.json new file mode 100644 index 000000000..cfbd9c707 --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080700/metadata.json @@ -0,0 +1,54 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wind-real", + "generation": 1786060800000, + "dataVersion": 1786060800000, + "validTime": 1786060800000, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "grid": { + "width": 513, + "height": 769 + }, + "validity": { + "encoding": "uint8", + "byteLength": 394497 + }, + "source": { + "kind": "WeatherV2 ECMWF HRES WVECBIN1 canonical v1 upgraded offline", + "referenceTime": "2026080600", + "targetTime": "2026080700", + "originalPayloadSha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283", + "validityDerivation": "finite U/V and both components != -9999.0" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 3155976, + "noData": -9999.0, + "semantics": "VELOCITY", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "3100f7cc4f6b7357e857ec10f159c43a092cfaad8eed6327043406579f5808ee" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283" + } + } +} diff --git a/integration/particle-v3-fields/wind-real/2026080700/validity.u8 b/integration/particle-v3-fields/wind-real/2026080700/validity.u8 new file mode 100644 index 000000000..b038f6342 --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080700/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/wind-real/2026080700/vector.f32 b/integration/particle-v3-fields/wind-real/2026080700/vector.f32 new file mode 100644 index 000000000..21fb3645d Binary files /dev/null and b/integration/particle-v3-fields/wind-real/2026080700/vector.f32 differ diff --git a/integration/particle-v3-fields/wind-real/2026080706/conversion_summary.json b/integration/particle-v3-fields/wind-real/2026080706/conversion_summary.json new file mode 100644 index 000000000..19bbde179 --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080706/conversion_summary.json @@ -0,0 +1,35 @@ +{ + "grid": { + "width": 513, + "height": 769 + }, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "validTime": 1786082400000, + "generation": 1786082400000, + "source": { + "kind": "WeatherV2 ECMWF HRES WVECBIN1 canonical v1 upgraded offline", + "referenceTime": "2026080600", + "targetTime": "2026080706", + "originalPayloadSha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451", + "validityDerivation": "finite U/V and both components != -9999.0" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "3100f7cc4f6b7357e857ec10f159c43a092cfaad8eed6327043406579f5808ee" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451" + } + }, + "validCount": 394497, + "invalidCount": 0 +} diff --git a/integration/particle-v3-fields/wind-real/2026080706/metadata.json b/integration/particle-v3-fields/wind-real/2026080706/metadata.json new file mode 100644 index 000000000..82acd38ec --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080706/metadata.json @@ -0,0 +1,54 @@ +{ + "schema": "maplibre-particle-field/v2", + "fieldId": "wind-real", + "generation": 1786082400000, + "dataVersion": 1786082400000, + "validTime": 1786082400000, + "bounds": { + "west": 112.5, + "south": 0.0, + "east": 157.5, + "north": 55.77657301866769 + }, + "grid": { + "width": 513, + "height": 769 + }, + "validity": { + "encoding": "uint8", + "byteLength": 394497 + }, + "source": { + "kind": "WeatherV2 ECMWF HRES WVECBIN1 canonical v1 upgraded offline", + "referenceTime": "2026080600", + "targetTime": "2026080706", + "originalPayloadSha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451", + "validityDerivation": "finite U/V and both components != -9999.0" + }, + "canonical": { + "origin": "north-west", + "rowOrder": "north-to-south", + "columnOrder": "west-to-east" + }, + "vector": { + "layout": "interleaved-uv", + "encoding": "float32-le", + "byteLength": 3155976, + "noData": -9999.0, + "semantics": "VELOCITY", + "uPositive": "east", + "vPositive": "north" + }, + "planes": { + "validity.u8": { + "path": "validity.u8", + "byteLength": 394497, + "sha256": "3100f7cc4f6b7357e857ec10f159c43a092cfaad8eed6327043406579f5808ee" + }, + "vector.f32": { + "path": "vector.f32", + "byteLength": 3155976, + "sha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451" + } + } +} diff --git a/integration/particle-v3-fields/wind-real/2026080706/validity.u8 b/integration/particle-v3-fields/wind-real/2026080706/validity.u8 new file mode 100644 index 000000000..b038f6342 --- /dev/null +++ b/integration/particle-v3-fields/wind-real/2026080706/validity.u8 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/particle-v3-fields/wind-real/2026080706/vector.f32 b/integration/particle-v3-fields/wind-real/2026080706/vector.f32 new file mode 100644 index 000000000..5f143c03f Binary files /dev/null and b/integration/particle-v3-fields/wind-real/2026080706/vector.f32 differ diff --git a/integration/particle-v3-fields/wind-real/dataset.json b/integration/particle-v3-fields/wind-real/dataset.json new file mode 100644 index 000000000..1f9dee61d --- /dev/null +++ b/integration/particle-v3-fields/wind-real/dataset.json @@ -0,0 +1,26 @@ +{ + "schema": "maplibre-particle-test-dataset/v2", + "datasetId": "wind-real", + "sourceProvenance": "真实本地 WeatherV2 Wind canonical v1 输出;validity 由 U/V noData 与有限性派生", + "frames": [ + { + "label": "T0", + "path": "2026080700", + "metadata": "metadata.json", + "validity": "validity.u8", + "vector": "vector.f32" + }, + { + "label": "T1", + "path": "2026080706", + "metadata": "metadata.json", + "validity": "validity.u8", + "vector": "vector.f32" + } + ], + "expectedFieldValueSource": "VECTOR_MAGNITUDE", + "particleExpected": true, + "vectorSemantics": "VELOCITY", + "suggestedAdvectionScale": 1.0, + "说明": "离线 canonical fixture;Runtime 只接收 metadata 与本地 plane URI。" +} diff --git a/integration/react-native/MapLibreParticlePackageConsumerTestHost/App.tsx b/integration/react-native/MapLibreParticlePackageConsumerTestHost/App.tsx new file mode 100644 index 000000000..4bb78c401 --- /dev/null +++ b/integration/react-native/MapLibreParticlePackageConsumerTestHost/App.tsx @@ -0,0 +1,88 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Camera, Map } from "@maplibre/maplibre-react-native"; +import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native"; +import { FieldRuntime, type FieldColorStop, type RuntimeSnapshot, type TargetFps } from "@navsea/maplibre-particle-rn"; +import { materializeRealWeatherFrames, realWeatherFrames } from "./src/realWeatherDataset"; +import { weatherTestStyle } from "./src/weatherStyle"; + +const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false }; +const initialVisual = { count: "4096", width: "1.5", length: "0.012", opacity: "0.9", trailOpacity: "0.8", trailFade: "0.94", fieldOpacity: "0.62" }; +const frozenStops: FieldColorStop[] = [ + { value: 0, red: 0.08, green: 0.2, blue: 0.8, alpha: 0.72 }, { value: 8, red: 0, green: 0.85, blue: 0.9, alpha: 0.72 }, + { value: 16, red: 1, green: 0.85, blue: 0.05, alpha: 0.72 }, { value: 30, red: 0.9, green: 0.08, blue: 0.08, alpha: 0.72 }, +]; + +export default function App() { + const [reactTag, setReactTag] = useState(null); + const [snapshot, setSnapshot] = useState(initialSnapshot); + const [result, setResult] = useState("Style is loading"); + const [basemap, setBasemap] = useState("LOADING"); + const [frames, setFrames] = useState>({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" }); + const [transport, setTransport] = useState("PENDING: FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); + const [fieldVisible, setFieldVisible] = useState(true); + const [particleVisible, setParticleVisible] = useState(true); + const [visual, setVisual] = useState(initialVisual); + + const refresh = useCallback(async (tag: number) => setSnapshot(await FieldRuntime.getState(tag)), []); + const attach = useCallback(async (tag: number) => { + try { await FieldRuntime.attach(tag); await refresh(tag); setReactTag(tag); setResult("FieldRuntime attached; style ready"); } + catch (error) { setResult(`style/attach error: ${String(error)}`); } + }, [refresh]); + const onStyleReady = useCallback((event: unknown) => { + const tag = findNodeHandle((event as { target?: unknown }).target as never); + if (tag != null) void attach(tag); + }, [attach]); + useEffect(() => () => { if (reactTag != null) void FieldRuntime.detach(reactTag).catch(() => undefined); }, [reactTag]); + + const command = useCallback(async (name: string, operation: (tag: number) => Promise) => { + if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready"); + try { const value = await operation(reactTag); await refresh(reactTag); setResult(`${name}: ${String(value)}`); } + catch (error) { setResult(`${name}: ${String(error)}`); } + }, [reactTag, refresh]); + + const loadRealWeather = useCallback(async () => { + if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready"); + const expected = ["ACCEPTED_PENDING_INITIAL_WINDOW", "INITIAL_WINDOW_READY", "ACCEPTED_NEXT_SNAPSHOT", "ACCEPTED_NEXT_SNAPSHOT"]; + try { + setTransport("LOADING: materializing Expo Asset local file:// URIs"); setFrames({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" }); + if (await FieldRuntime.clearFields(reactTag) !== "ACCEPTED") throw new Error("clearFields was rejected"); + const localFrames = await materializeRealWeatherFrames(); + for (let index = 0; index < localFrames.length; index += 1) { + const frame = localFrames[index]; const submit = await FieldRuntime.submitField(reactTag, frame.metadataJson, frame.payloadFileUri); + setFrames((current) => ({ ...current, [frame.label]: submit })); + if (submit !== expected[index]) throw new Error(`${frame.label} expected ${expected[index]}, got ${submit}`); + } + const native = await FieldRuntime.getState(reactTag); setSnapshot(native); + if (native.state !== "READY") throw new Error(`Native FieldRuntime expected READY, got ${native.state}`); + setTransport("READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); setResult("T0-T3 accepted in strict producer timestamp order; Native runtime READY"); + } catch (error) { setTransport(`ERROR: ${String(error)}`); setResult(`LOAD REAL WEATHER T0-T3: ${String(error)}`); } + }, [reactTag]); + + const updateVisual = useCallback((key: keyof typeof initialVisual, value: string) => setVisual((current) => ({ ...current, [key]: value })), []); + const applyVisual = useCallback(() => void command("particle visual", async (tag) => { + const results = await Promise.all([ + FieldRuntime.setParticleCount(tag, Number(visual.count)), FieldRuntime.setParticleLineWidth(tag, Number(visual.width)), FieldRuntime.setParticleLength(tag, Number(visual.length)), FieldRuntime.setParticleOpacity(tag, Number(visual.opacity)), FieldRuntime.setTrailOpacity(tag, Number(visual.trailOpacity)), FieldRuntime.setTrailFade(tag, Number(visual.trailFade)), FieldRuntime.setFieldOpacity(tag, Number(visual.fieldOpacity)), + ]); + if (results.some((value) => value !== "ACCEPTED")) throw new Error(results.join(", ")); return "ACCEPTED"; + }), [command, visual]); + + const controls = useMemo(() => [ + ["PLAY", (tag: number) => FieldRuntime.setPlaybackState(tag, "PLAY")], ["PAUSE", (tag: number) => FieldRuntime.setPlaybackState(tag, "PAUSE")], ["HALF", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "HALF")], ["NORMAL", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "NORMAL")], ["DOUBLE", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "DOUBLE")], + ["FPS AUTO", (tag: number) => FieldRuntime.setTargetFps(tag, "AUTO" as TargetFps)], ["FPS 30", (tag: number) => FieldRuntime.setTargetFps(tag, "FPS_30" as TargetFps)], ["FPS 60", (tag: number) => FieldRuntime.setTargetFps(tag, "FPS_60" as TargetFps)], ["Field stops", (tag: number) => FieldRuntime.setFieldColorStops(tag, frozenStops)], ["Particle color", (tag: number) => FieldRuntime.setParticleColor(tag, 1, 1, 1, 0.95)], ["Trail color", (tag: number) => FieldRuntime.setTrailColor(tag, 0.35, 0.85, 1, 0.8)], + ] as const, []); + + return + setBasemap("READY")} onDidFailLoadingMap={() => { setBasemap("ERROR"); setResult("map loading error"); }}> + + Basemap: GSI {basemap}Dataset: ECMWF HRES 20260807 · 513×769 · 3.01 MiB/frameRuntime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}REAL DATA TRANSPORT = {transport} + {realWeatherFrames.map((frame) => {frame.label} {frame.cycle}: {frames[frame.label]})} + {result}