diff --git a/CMakeLists.txt b/CMakeLists.txt index f194b9b8d..de2d01689 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -213,11 +213,13 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/offline_fixture_particle_adapter.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/offline_fixture_particle_adapter.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/field_data_adapter.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/local_particle_field_feed.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/shader_program_base.cpp ) list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mbgl/particle/field/field_data_adapter.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/particle/field/local_particle_field_feed.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/actor/actor_ref.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/actor/actor.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/actor/aspiring_actor.hpp 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..1c10d5023 --- /dev/null +++ b/docs/50_Step记录/STEP-051_Local_Production-like_Field_Feed与Core_Snapshot_Submit_Bridge_v1.md @@ -0,0 +1,47 @@ +# 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`。 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..09494994c --- /dev/null +++ b/docs/报告/STEP-051_Local_Production-like_Field_Feed与Core_Snapshot_Submit_Bridge_Codex完成报告_v1.md @@ -0,0 +1,69 @@ +# 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 收尾 + +- commit hash:待本次提交完成后回填。 +- commit message:`feat: add production-like particle field feed` +- push result:待执行。 +- remote branch:`origin/particle/main` +- Final HEAD:待 push 后回填。 + +## 当前状态与 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/include/mbgl/particle/field/field_data_adapter.hpp b/include/mbgl/particle/field/field_data_adapter.hpp index ebc6c6ea9..640937358 100644 --- a/include/mbgl/particle/field/field_data_adapter.hpp +++ b/include/mbgl/particle/field/field_data_adapter.hpp @@ -13,6 +13,8 @@ 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; diff --git a/include/mbgl/particle/field/local_particle_field_feed.hpp b/include/mbgl/particle/field/local_particle_field_feed.hpp new file mode 100644 index 000000000..18f26eb80 --- /dev/null +++ b/include/mbgl/particle/field/local_particle_field_feed.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include +#include + +namespace mbgl::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); +}; + +} // namespace mbgl::particle diff --git a/include/mbgl/style/sources/raster_array_source.hpp b/include/mbgl/style/sources/raster_array_source.hpp index f1af9589e..40dfa16a6 100644 --- a/include/mbgl/style/sources/raster_array_source.hpp +++ b/include/mbgl/style/sources/raster_array_source.hpp @@ -26,6 +26,13 @@ struct RasterArrayRuntimeFieldDescriptor { 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, @@ -105,6 +112,7 @@ public: std::string nextMetadataURL = {}, std::string nextFieldURL = {}, std::vector runtimeFields = {}); + RasterArraySource(std::string id, std::vector localFields); ~RasterArraySource() override; class Impl; @@ -114,6 +122,9 @@ public: // 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(); @@ -137,9 +148,18 @@ private: 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(); RasterArrayRuntimeFieldUpdateResult rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult, const std::string& reason) const; @@ -153,6 +173,7 @@ private: std::optional nextMetadata; std::optional> nextFieldBytes; std::vector runtimeFields; + std::vector localFields; size_t nextRuntimeField = 0; uint64_t runtimeUpdateSequence = 0; std::shared_ptr timeline; diff --git a/particle-production/field-00.json b/particle-production/field-00.json new file mode 100644 index 000000000..2ce97fb1c --- /dev/null +++ b/particle-production/field-00.json @@ -0,0 +1,13 @@ +{ + "schema": "maplibre-particle-field/v1", + "fieldId": "production-wind-t00", + "fieldType": "WIND", + "displayDomain": "GLOBAL", + "generation": 1, + "validTime": 202609040000, + "dataVersion": 1, + "bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, + "grid": {"width": 16, "height": 16}, + "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, + "payload": {"byteLength": 2048} +} diff --git a/particle-production/field-01.json b/particle-production/field-01.json new file mode 100644 index 000000000..4d1c77c3f --- /dev/null +++ b/particle-production/field-01.json @@ -0,0 +1,13 @@ +{ + "schema": "maplibre-particle-field/v1", + "fieldId": "production-wind-t01", + "fieldType": "WIND", + "displayDomain": "GLOBAL", + "generation": 2, + "validTime": 202609040100, + "dataVersion": 2, + "bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, + "grid": {"width": 16, "height": 16}, + "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, + "payload": {"byteLength": 2048} +} diff --git a/particle-production/field-02.json b/particle-production/field-02.json new file mode 100644 index 000000000..7985abaed --- /dev/null +++ b/particle-production/field-02.json @@ -0,0 +1,13 @@ +{ + "schema": "maplibre-particle-field/v1", + "fieldId": "production-wind-t02", + "fieldType": "WIND", + "displayDomain": "GLOBAL", + "generation": 3, + "validTime": 202609040200, + "dataVersion": 3, + "bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, + "grid": {"width": 16, "height": 16}, + "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, + "payload": {"byteLength": 2048} +} diff --git a/particle-production/field-03.json b/particle-production/field-03.json new file mode 100644 index 000000000..b9b7d8200 --- /dev/null +++ b/particle-production/field-03.json @@ -0,0 +1,13 @@ +{ + "schema": "maplibre-particle-field/v1", + "fieldId": "production-wind-t03", + "fieldType": "WIND", + "displayDomain": "GLOBAL", + "generation": 4, + "validTime": 202609040300, + "dataVersion": 4, + "bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, + "grid": {"width": 16, "height": 16}, + "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, + "payload": {"byteLength": 2048} +} diff --git a/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-00.json b/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-00.json new file mode 100644 index 000000000..9668d38de --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-00.json @@ -0,0 +1,4 @@ +{ + "schema": "maplibre-particle-field/v1", "fieldId": "production-wind-t00", "fieldType": "WIND", "displayDomain": "GLOBAL", "generation": 1, "validTime": 202609040000, "dataVersion": 1, + "bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, "grid": {"width": 16, "height": 16}, "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, "payload": {"byteLength": 2048} +} diff --git a/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-01.json b/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-01.json new file mode 100644 index 000000000..b32d16274 --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-01.json @@ -0,0 +1,4 @@ +{ + "schema": "maplibre-particle-field/v1", "fieldId": "production-wind-t01", "fieldType": "WIND", "displayDomain": "GLOBAL", "generation": 2, "validTime": 202609040100, "dataVersion": 2, + "bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, "grid": {"width": 16, "height": 16}, "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, "payload": {"byteLength": 2048} +} diff --git a/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-02.json b/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-02.json new file mode 100644 index 000000000..e9c1198e9 --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-02.json @@ -0,0 +1,4 @@ +{ + "schema": "maplibre-particle-field/v1", "fieldId": "production-wind-t02", "fieldType": "WIND", "displayDomain": "GLOBAL", "generation": 3, "validTime": 202609040200, "dataVersion": 3, + "bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, "grid": {"width": 16, "height": 16}, "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, "payload": {"byteLength": 2048} +} diff --git a/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-03.json b/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-03.json new file mode 100644 index 000000000..b1a3348df --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-production/field-03.json @@ -0,0 +1,4 @@ +{ + "schema": "maplibre-particle-field/v1", "fieldId": "production-wind-t03", "fieldType": "WIND", "displayDomain": "GLOBAL", "generation": 4, "validTime": 202609040300, "dataVersion": 4, + "bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, "grid": {"width": 16, "height": 16}, "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, "payload": {"byteLength": 2048} +} diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index 18d93cee9..dcf40b049 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -32,6 +32,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var trailFade = DEFAULT_TRAIL_FADE private var speedColorEnabled = true private var timeWindowMode = false + private var productionFeedMode = false private var styleLoadEpoch = 0L private var styleReady = false // Display mirror only; Core remains the playback state authority. @@ -97,21 +98,25 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } findViewById