43 KiB
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. 核心决策摘要
- 唯一正式主路线:新增独立 Core
RasterParticleLayer、RenderRasterParticleLayer和隔离的mbgl::particle模块,进入 Style/RenderTree,通过 MapLibre gfx 执行 GPU simulation、trail 和最终合成。 CustomLayerHost与CustomDrawableLayer不作为正式粒子引擎宿主;需要时只可用于一次性技术探针,不能进入公共 API、产品依赖或长期双轨。- 新增
RasterArraySource作为 Field 数据边界;WeatherColorLayer 与 RasterParticleLayer 可引用同一 Field snapshot 和 Mask,但拥有不同 RenderLayer、GPU 状态和生命周期。 - 每帧由 MapLibre render loop 驱动;活动粒子通过 Core render state 持续声明
needsRepaint,不建立第二套 UI/JS 动画循环。 - Camera/Matrix 只从当前 render frame 的
PaintParameters/TransformParameters/TransformState读取。 - 第一版 GPU simulation 使用跨 backend 的 render-pass ping-pong,不以 compute 为前置;compute 仅作为后续 capability-gated 优化。
- 第一阶段正式视觉范围包含粒子、颜色映射和屏幕空间 trail;最小验证可先无 trail 验证状态更新,但在第一阶段冻结前必须通过 trail 验证。
- 第一阶段实现/日常验证范围是 Android OpenGL;Android Vulkan 在第一阶段冻结前做 gfx 兼容 gate;iOS OpenGL/Metal 保留相同 Core contract,在独立后续阶段验证。
- 下一阶段选择 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. 粒子系统总体架构图
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 控制,推荐:
Base Map
→ WeatherColorLayer
→ RasterParticleLayer
→ Navigation Reference / coast / labels
→ AIS / Route / Ownship / Alarm
WeatherColor 保持定量色场;导航信息通过更高 Style Layer 显示,不以降低天气层透明度作为默认策略。
5. Core 模块边界与建议目录
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最终合成属于Translucentpass,按 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 目录固定包含:
particle-fixtures/<fixture-id>/
├─ metadata.json
├─ field.f32
└─ mask.u8 仅 OCEAN_ONLY/Mask 场景必需
metadata.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": "<sha256>"
}
}
实际 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-8601timestamp,确定性 synthetic fixture 可令其为null。
9.3 Mask metadata
OCEAN_ONLY 场景把 mask 写为:
{
"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 数据流
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. 线程模型
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<const ParticleFieldSnapshot>
│ 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<const ParticleFieldSnapshot>/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 同帧同步
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。
- enable 或资源 ready 时,Controller 通过现有 Core invalidate/
triggerRepaint路径唤醒第一帧。 RENDERING且可见、Field/GPU ready 时,Particle Render 在当前 RenderTree 中声明needsRepaint=true,使 MapLibre 请求下一帧。- 地图静止但粒子活动时,以上机制持续;不依赖 camera transition。
- disable、Layer 不可见、App background、View 不可见、Field 不可用、ERROR 或 layer removal 时停止额外 repaint。
- App foreground/context restore 后,只有资源重新 READY 且 layer enabled/visible 才恢复。
- 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。
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 共用图
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. 生命周期状态机
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. 每帧渲染链图
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 薄接入
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)的 rawlong 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 薄接入
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 方法签名:
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 同步图
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 模块边界图
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 不得自行改变:
- 主路线是独立 Core
RasterParticleLayer/RenderRasterParticleLayer,进入 Style/RenderTree/gfx;CustomLayer/CustomDrawable 不是正式宿主。 - Android/iOS 共用 C++
mbgl::particleField、Simulation、Render、GPU resource contract、Mask、Controller 和状态机;平台只有类型 peer、backend lifecycle 与 capability 薄接入。 RasterArraySource是 Field 数据边界;Field snapshot immutable、revisioned、canonical U-east/V-north/SI unit。- WeatherColorLayer 与 RasterParticleLayer 是不同 Layer/RenderLayer/GPU 状态/动画生命周期,只共享数据 snapshot、grid/time metadata 和 Mask。
- Wind=
GLOBAL;Wave/Tide/Current=OCEAN_ONLY;OCEAN_ONLY 统一共享 Land/Ocean Mask,缺失时 fail closed。 - fixture contract v1 使用
metadata.json + field.f32 + optional/required mask.u8、float32 little-endian interleaved UV、uint8 mask、checksum 和明确 grid/bounds/NoData。 - frame driver 是 MapLibre render loop;active layer 贡献
needsRepaint,没有独立 UI/JS 主时钟。 - Camera/Matrix 来自同一 render frame 的 Paint/Transform parameters;position state 与 Camera 无关。
- GPU simulation 的第一共同基线是 render-pass State A/B ping-pong;正式 CPU 每粒子循环和 compute-only 第一版均拒绝。
- 第一阶段正式视觉包含粒子、颜色映射和 screen-space Trail A/B;Camera 变化清 trail,不做 v1 trail reprojection。
- GPU resource 只由 render thread/gfx 创建;context loss 保留 CPU snapshot、丢弃 GPU generation并重建空 trail。
- Android 第一阶段主验证 OpenGL,冻结前必须做 Vulkan gfx gate;iOS OpenGL/Metal 使用同一 Core contract,后续 Mac/iOS 专项验证。
- 公共 API 不暴露 backend handle、raw host pointer或每帧大数组;数据更新通过 Source immutable revision。
- 下一阶段只执行第 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 或项目总则;未运行会写工程状态的构建/测试。