9.5 KiB
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。
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
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
{
"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才公开 v2payloadRefs,旧.2ABI 可继续作为 legacy ingress,Native/AAR coordinate 独立推进。
Renderer Contract(与数据分离)
FieldSnapshot 只回答“有什么”;renderer config 显式回答“怎样画”:
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。