From 1e8eca79125e36284d8893e3134203d3f5289ce7 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 4 Sep 2026 18:35:49 +0800 Subject: [PATCH] feat: add host particle field submission contract --- ...eld与Host_MapLibre_Submission_Contract_v1.md | 109 +++++++++++ ...e_Field与Host_Submission_Codex完成报告_v1.md | 137 ++++++++++++++ .../particle/field/field_data_adapter.hpp | 3 + .../style/sources/raster_array_source.hpp | 24 +++ .../src/cpp/native_map_view.cpp | 24 +++ .../src/cpp/native_map_view.hpp | 4 + .../maplibre/android/maps/MapLibreMap.java | 18 ++ .../org/maplibre/android/maps/NativeMap.java | 2 + .../maplibre/android/maps/NativeMapView.java | 10 + .../ParticleCoreLayerValidationActivity.kt | 83 ++++++++- ...ctivity_particle_core_layer_validation.xml | 2 +- src/mbgl/style/conversion/source.cpp | 8 + .../style/sources/raster_array_source.cpp | 172 +++++++++++++++++- .../sources/raster_array_source_impl.hpp | 5 + test/style/source.test.cpp | 154 ++++++++++++++++ 粒子开发记录.md | 9 + 16 files changed, 756 insertions(+), 8 deletions(-) create mode 100644 docs/50_Step记录/STEP-053_Windy-style_Canonical_Field与Host_MapLibre_Submission_Contract_v1.md create mode 100644 docs/报告/STEP-053_Windy-style_Field与Host_Submission_Codex完成报告_v1.md 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..26a4d081f --- /dev/null +++ b/docs/50_Step记录/STEP-053_Windy-style_Canonical_Field与Host_MapLibre_Submission_Contract_v1.md @@ -0,0 +1,109 @@ +# STEP-053 Windy-style Canonical Field 与 Host→MapLibre Submission Contract + +- 状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION` +- 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`。 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..4160b5c99 --- /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`:最终提交前执行。 + +最初新增 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:待 Git 收尾后回填。 +- commit message:`feat: add host particle field submission contract` +- push result:待执行。 +- remote branch:`origin/particle/main` +- Final HEAD:待 push 后回填。 +- 无关 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/include/mbgl/particle/field/field_data_adapter.hpp b/include/mbgl/particle/field/field_data_adapter.hpp index 640937358..0afcc8f35 100644 --- a/include/mbgl/particle/field/field_data_adapter.hpp +++ b/include/mbgl/particle/field/field_data_adapter.hpp @@ -8,6 +8,9 @@ namespace mbgl::particle { // Provider-neutral description of the canonical field bytes consumed by Core. +// maplibre-particle-field/v1 is a north-west-origin regular grid: rows run +// north-to-south, columns west-to-east, and cell = y * width + x. The first +// float of each interleaved pair is eastward U; the second is northward V. // Provider names, URLs and transport metadata deliberately do not cross this boundary. struct ParticleFieldDescriptor { std::string fieldID; diff --git a/include/mbgl/style/sources/raster_array_source.hpp b/include/mbgl/style/sources/raster_array_source.hpp index 40dfa16a6..d19b025c1 100644 --- a/include/mbgl/style/sources/raster_array_source.hpp +++ b/include/mbgl/style/sources/raster_array_source.hpp @@ -61,6 +61,7 @@ public: }; explicit RasterArrayTimelineRuntime(std::vector); + bool append(Entry); RasterArrayTimelineFrame tick(); bool play(); bool pause(); @@ -72,6 +73,8 @@ public: bool recover(); RasterArrayTimelineFrame frame() const; float playbackSpeed() const; + size_t size() const; + Entry lastEntry() const; size_t cacheCapacity() const { return cacheCapacity_; } private: @@ -102,6 +105,22 @@ enum class RasterArrayRuntimeFieldUpdateResult : uint8_t { 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); + class RasterArraySource final : public Source { public: RasterArraySource(std::string id, @@ -113,6 +132,9 @@ public: 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; @@ -131,6 +153,7 @@ public: // 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); // Low-frequency JNI commands for the Source-owned validation timeline. bool controlRuntimeTimeline(int command, float value = 0.0f); bool supportsLayerType(const LayerTypeInfo*) const override; @@ -174,6 +197,7 @@ private: std::optional> nextFieldBytes; std::vector runtimeFields; std::vector localFields; + std::optional pendingHostField; size_t nextRuntimeField = 0; uint64_t runtimeUpdateSequence = 0; std::shared_ptr timeline; diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp index a6f39375e..45a5f9267 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp @@ -1204,6 +1204,29 @@ jni::jboolean NativeMapView::repeatRasterArraySourceFieldWindow(JNIEnv& env, con return success; } +jni::jint NativeMapView::submitRasterArraySourceHostField(JNIEnv& env, + const jni::String& sourceId, + const jni::String& metadata, + const jni::Array& payload) { + const auto id = jni::Make(env, sourceId); + auto* source = dynamic_cast(map->getStyle().getSource(id)); + if (!source) { + mbgl::Log::Error(mbgl::Event::JNI, + "ParticleField: event=HOST_FIELD_SUBMIT_REJECTED reason=SOURCE_NOT_FOUND sourceId=" + id); + return static_cast(mbgl::style::RasterArrayHostFieldSubmitResult::RejectedSourceMode); + } + NullCheck(env, &payload); + std::vector bytes(payload.Length(env)); + if (!bytes.empty()) { + jni::GetArrayRegion(env, *payload, 0, bytes.size(), reinterpret_cast(bytes.data())); + } + const auto result = source->submitHostField(jni::Make(env, metadata), bytes); + mbgl::Log::Info(mbgl::Event::JNI, + "ParticleField: event=HOST_FIELD_SUBMIT_BRIDGE sourceId=" + id + + " result=" + mbgl::style::rasterArrayHostFieldSubmitResultName(result)); + return static_cast(result); +} + jni::jboolean NativeMapView::controlRasterArraySourceTimeline(JNIEnv& env, const jni::String& sourceId, const jni::jint command, @@ -1604,6 +1627,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) { METHOD(&NativeMapView::setStyleLayerFloatProperty, "nativeSetStyleLayerFloatProperty"), METHOD(&NativeMapView::advanceRasterArraySourceFieldWindow, "nativeAdvanceRasterArraySourceFieldWindow"), METHOD(&NativeMapView::repeatRasterArraySourceFieldWindow, "nativeRepeatRasterArraySourceFieldWindow"), + METHOD(&NativeMapView::submitRasterArraySourceHostField, "nativeSubmitRasterArraySourceHostField"), METHOD(&NativeMapView::controlRasterArraySourceTimeline, "nativeControlRasterArraySourceTimeline"), METHOD(&NativeMapView::addLayer, "nativeAddLayer"), METHOD(&NativeMapView::addLayerAbove, "nativeAddLayerAbove"), diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp index 0664eddbc..7c67bd446 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp @@ -284,6 +284,10 @@ public: jni::jboolean setStyleLayerFloatProperty(JNIEnv&, const jni::String&, const jni::String&, jni::jfloat); jni::jboolean advanceRasterArraySourceFieldWindow(JNIEnv&, const jni::String&); jni::jboolean repeatRasterArraySourceFieldWindow(JNIEnv&, const jni::String&); + jni::jint submitRasterArraySourceHostField(JNIEnv&, + const jni::String&, + const jni::String&, + const jni::Array&); jni::jboolean controlRasterArraySourceTimeline(JNIEnv&, const jni::String&, jni::jint, jni::jfloat); void addLayer(JNIEnv&, jlong, const jni::String&); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java index 669da053d..ce43e7fdd 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java @@ -301,11 +301,29 @@ public final class MapLibreMap { return nativeMapView.repeatRasterArraySourceFieldWindow(sourceId); } + /** + * Submits one canonical particle Field as provider-neutral metadata plus contiguous binary U/V bytes. + * This is a low-frequency ingress; Timeline and Runtime Field Window ownership remain in Core. + */ + public int submitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload) { + return nativeMapView.submitRasterArraySourceHostField(sourceId, metadata, payload); + } + /** Sends a low-frequency playback command to the Core-owned raster-array timeline. */ public boolean controlRasterArraySourceTimeline(String sourceId, int command, float value) { return nativeMapView.controlRasterArraySourceTimeline(sourceId, command, value); } + public static final int HOST_FIELD_REJECTED_SOURCE_MODE = -1; + public static final int HOST_FIELD_REJECTED_ADAPTER = -2; + public static final int HOST_FIELD_REJECTED_INCOMPATIBLE = -3; + public static final int HOST_FIELD_REJECTED_NON_MONOTONIC = -4; + public static final int HOST_FIELD_REJECTED_CAPACITY = -5; + public static final int HOST_FIELD_ACCEPTED_PENDING_INITIAL_WINDOW = 1; + public static final int HOST_FIELD_INITIAL_WINDOW_READY = 2; + public static final int HOST_FIELD_ACCEPTED_NEXT_SNAPSHOT = 3; + public static final int HOST_FIELD_DUPLICATE_NOOP = 4; + // Style /** diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java index 8e810354c..18326e416 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java @@ -145,6 +145,8 @@ interface NativeMap { boolean repeatRasterArraySourceFieldWindow(String sourceId); + int submitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload); + boolean controlRasterArraySourceTimeline(String sourceId, int command, float value); boolean removeLayer(@NonNull String layerId); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java index ff647f3c7..b448a64ea 100755 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java @@ -968,6 +968,13 @@ final class NativeMapView implements NativeMap { && nativeRepeatRasterArraySourceFieldWindow(sourceId); } + @Override + public int submitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload) { + return checkState("submitRasterArraySourceHostField") + ? MapLibreMap.HOST_FIELD_REJECTED_SOURCE_MODE + : nativeSubmitRasterArraySourceHostField(sourceId, metadata, payload); + } + @Override public boolean controlRasterArraySourceTimeline(String sourceId, int command, float value) { return !checkState("controlRasterArraySourceTimeline") @@ -1714,6 +1721,9 @@ final class NativeMapView implements NativeMap { @Keep private native boolean nativeRepeatRasterArraySourceFieldWindow(String sourceId); + @Keep + private native int nativeSubmitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload); + @Keep private native boolean nativeControlRasterArraySourceTimeline(String sourceId, int command, float value); 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 372224ad9..c4152cb6e 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 @@ -33,6 +33,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var speedColorEnabled = true private var timeWindowMode = false private var productionFeedMode = false + private var hostSubmissionMode = false private var styleLoadEpoch = 0L private var styleReady = false // Display mirror only; Core remains the playback state authority. @@ -99,24 +100,28 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { findViewById