feat: bring up particle renderer on ios metal
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -47,6 +47,7 @@ list(APPEND
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/color_relief.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/line.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/location_indicator.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/particle_gpu_validation.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/raster.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/shader_group.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/shader_program.hpp
|
||||
@@ -90,6 +91,7 @@ list(APPEND
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/color_relief.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/line.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/location_indicator.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/particle_gpu_validation.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/raster.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/symbol.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/widevector.cpp
|
||||
|
||||
43
docs/30_设计/STEP-081_iOS_Metal_Renderer与真机Smoke入口设计_v1.md
Normal file
43
docs/30_设计/STEP-081_iOS_Metal_Renderer与真机Smoke入口设计_v1.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# STEP-081 iOS Metal Renderer 与真机 Smoke 入口设计 v1
|
||||
|
||||
状态:进行中 / WAITING_RDC_DEVICE_BUILD。本文只记录 Codex 静态实现;未进行 iPhoneOS 编译或真机运行。
|
||||
|
||||
## 目标和边界
|
||||
|
||||
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 |
|
||||
|
||||
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,允许重复创建。
|
||||
|
||||
## 后续边界
|
||||
|
||||
下一轮如需 iOS RN,必须另行设计桥接;不得把本 App 的 private Core seam 提升为 SDK 或 NavSea API。iPhoneOS 编译、Metal runtime compilation、资源生命周期和视觉验收均由 RDC/GPT 在真实设备完成。
|
||||
254
docs/40_任务/STEP-081_iOS_Metal_Renderer与真机Smoke入口_Codex任务_v1.md
Normal file
254
docs/40_任务/STEP-081_iOS_Metal_Renderer与真机Smoke入口_Codex任务_v1.md
Normal file
@@ -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 上做运行验收。
|
||||
38
docs/50_Step记录/STEP-081_iOS_Metal_Renderer与真机Smoke入口_v1.md
Normal file
38
docs/50_Step记录/STEP-081_iOS_Metal_Renderer与真机Smoke入口_v1.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# STEP-081 iOS Metal Renderer 与真机 Smoke 入口 v1
|
||||
|
||||
状态:进行中 / WAITING_RDC_DEVICE_BUILD
|
||||
|
||||
## 目标
|
||||
|
||||
为既有 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。
|
||||
- [ ] iPhoneOS device build、Metal runtime compilation、真实 iPhone/iPad smoke:待 RDC/GPT。
|
||||
|
||||
## 允许范围与限制
|
||||
|
||||
修改仅涉及 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 中打开 Settings → Runtime Styling → `Particle / QBIN V4 Smoke (T0)`,选择 T0、midpoint、T1;使用 `Clear / release` 后再次选择任一时间验证重建。预期日志包含 `ParticleQbinV4Smoke: event=FIELD_READY`、`WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY`、粒子 `ParticleStateTarget: event=CREATE_SUCCESS` / `WeatherRuntime: event=PARTICLE_RESOURCE_CREATE`(若 backend 进入该 pass)。
|
||||
|
||||
## 构建与阻塞
|
||||
|
||||
`NOT_RUN_BY_DESIGN — Codex prohibited from building`。无 iOS PASS、Metal PASS 或真机 PASS 声明。若 device runtime shader compile 失败,先检查 `include/mbgl/shaders/mtl/particle_gpu_validation.hpp`、对应 `.cpp` binding 表、`renderer_backend.cpp` registry 和 `cmake/metal.cmake` source list。
|
||||
|
||||
## 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。
|
||||
@@ -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`。
|
||||
106
include/mbgl/shaders/mtl/particle_gpu_validation.hpp
Normal file
106
include/mbgl/shaders/mtl/particle_gpu_validation.hpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
|
||||
#include <mbgl/shaders/particle_gpu_validation_ubo.hpp>
|
||||
#include <mbgl/shaders/shader_source.hpp>
|
||||
#include <mbgl/shaders/mtl/shader_program.hpp>
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace mbgl::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<BuiltIn::ParticleGpuValidationShader, gfx::Backend::Type::Metal> {
|
||||
static constexpr auto name = "ParticleGpuValidationShader";
|
||||
static constexpr auto vertexMainFunction = "vertexMain";
|
||||
static constexpr auto fragmentMainFunction = "fragmentMain";
|
||||
static const std::array<AttributeInfo, 1> attributes;
|
||||
static constexpr std::array<AttributeInfo, 0> instanceAttributes{};
|
||||
static const std::array<TextureInfo, 5> 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((a.rg*2.0-1.0)*u.fieldScale.x,(b.rg*2.0-1.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<float> previous [[texture(0)]], texture2d<float> field [[texture(2)]],
|
||||
texture2d<float> nextField [[texture(3)]], texture2d<float> state [[texture(4)]],
|
||||
sampler previousSampler [[sampler(0)]], sampler fieldSampler [[sampler(2)]],
|
||||
sampler nextFieldSampler [[sampler(3)]], sampler stateSampler [[sampler(4)]]) {
|
||||
float2 pos=float2(in.pos)/8192.0;
|
||||
FragmentStage out={}; out.uv=pos; out.world=pos; out.valid=1.0;
|
||||
if (u.pass.x > 4.5) out.position=u.matrix*float4(pos*8192.0,0,1);
|
||||
else if (u.pass.x < 1.5) out.position=float4(pos*2.0-1.0,0,1);
|
||||
else if (u.pass.x > 3.5) {
|
||||
float endpoint=glMod(pos.x*8192.0,2.0)*2.0-1.0;
|
||||
float2 index=float2(floor(pos.x*8192.0*.5),pos.y*8192.0);
|
||||
float2 stateUv=(index+.5)/u.mask.xy;
|
||||
float4 old=previous.sample(previousSampler,stateUv), current=state.sample(stateSampler,stateUv);
|
||||
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.uv=stateUv; out.world=current.rg; out.velocity=float2(advection.x,-advection.y);
|
||||
out.valid=ok&&length(out.velocity)>0.00001&¤t.a>.5&&distance(old.rg,current.rg)>1e-9 ? 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 out.position=u.mask.z>.5 ? float4(pos*2.0-1.0,0,1) : 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<count;i++){float lo=u.weatherStops[i-1].x,hi=u.weatherStops[i].x;if(value<=hi)return mix(c,u.weatherColors[i],clamp((value-lo)/(hi-lo),0.,1.));c=u.weatherColors[i];}return c; }
|
||||
|
||||
float4 fragment fragmentMain(FragmentStage in [[stage_in]], device const ParticleGpuValidationUBO& u [[buffer(idParticleGpuValidationUBO)]],
|
||||
texture2d<float> previous [[texture(0)]], texture2d<float> mask [[texture(1)]], texture2d<float> field [[texture(2)]], texture2d<float> nextField [[texture(3)]], texture2d<float> 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>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((a.rg*2.-1.)*u.fieldScale.x,(b.rg*2.-1.)*u.fieldScale.y,u.marker.x)):mix((a.r*2.-1.)*u.fieldScale.x,(b.r*2.-1.)*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.x<u.respawnBounds.x||position.x>u.respawnBounds.z||position.y<u.respawnBounds.y||position.y>u.respawnBounds.w));if(!respawn){float lat=mercatorLatitudeRadians(position.y);float2 n=position+float2(advection.x*u.pass.y/40075016.68557849,-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,-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 mbgl::shaders
|
||||
@@ -48,6 +48,19 @@ add_custom_command(TARGET app POST_BUILD
|
||||
COMMAND ls "$<TARGET_BUNDLE_DIR:app>"
|
||||
)
|
||||
|
||||
# STEP-081 test-only QBIN V4 fixture handoff. The canonical files remain in
|
||||
# the RN Test Host; this copies them into the app bundle without a new format.
|
||||
add_custom_command(TARGET app POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_BUNDLE_DIR:app>/ParticleSmokeFixtures"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${CMAKE_SOURCE_DIR}/integration/react-native/MapLibreParticleRNTestHost/assets/qbin-golden/wind/T0.qbin"
|
||||
"$<TARGET_BUNDLE_DIR:app>/ParticleSmokeFixtures/T0.qbin"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${CMAKE_SOURCE_DIR}/integration/react-native/MapLibreParticleRNTestHost/assets/qbin-golden/wind/T1.qbin"
|
||||
"$<TARGET_BUNDLE_DIR:app>/ParticleSmokeFixtures/T1.qbin"
|
||||
COMMENT "Copying STEP-081 Particle QBIN V4 smoke fixtures..."
|
||||
)
|
||||
|
||||
if(TARGET custom-layer-examples)
|
||||
target_link_libraries(
|
||||
app
|
||||
|
||||
@@ -16,6 +16,16 @@
|
||||
|
||||
#import "MLNMapView_Private.h"
|
||||
|
||||
#include <mbgl/particle/field/field_binding.hpp>
|
||||
#include <mbgl/map/map.hpp>
|
||||
#include <mbgl/style/style.hpp>
|
||||
#include <mbgl/style/layers/raster_particle_layer.hpp>
|
||||
#include <mbgl/style/layers/weather_color_layer.hpp>
|
||||
#include <mbgl/style/sources/raster_array_source.hpp>
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
#import "CustomStyleLayerExample.h"
|
||||
|
||||
#import "ExampleCustomDrawableStyleLayer.h"
|
||||
@@ -117,7 +127,8 @@ typedef NS_ENUM(NSInteger, MBXSettingsRuntimeStylingRows) {
|
||||
MBXSettingsRuntimeStylingLineGradient,
|
||||
MBXSettingsRuntimeStylingCustomDrawableLayer,
|
||||
MBXSettingsRuntimeStylingAddFoursquarePOIsPMTiles,
|
||||
MBXSettingsRuntimeStylingHeatmapLayer
|
||||
MBXSettingsRuntimeStylingHeatmapLayer,
|
||||
MBXSettingsRuntimeStylingParticleQbinV4Smoke
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
|
||||
@@ -244,6 +255,9 @@ CLLocationCoordinate2D randomWorldCoordinate(void) {
|
||||
@property (nonatomic) NSMutableArray<UIWindow *> *helperWindows;
|
||||
@property (nonatomic) NSMutableArray<UIView *> *contentInsetsOverlays;
|
||||
@property (nonatomic, copy) void (^locationBlock)(void);
|
||||
- (void)showParticleQbinV4SmokeMenu;
|
||||
- (void)installParticleQbinV4SmokeAtTime:(uint64_t)requestedTime label:(NSString *)label;
|
||||
- (void)clearParticleQbinV4Smoke;
|
||||
@end
|
||||
|
||||
@interface MLNMapView (MBXViewController)
|
||||
@@ -503,7 +517,8 @@ CLLocationCoordinate2D randomWorldCoordinate(void) {
|
||||
@"Style Route line with gradient",
|
||||
@"Add Custom Drawable Layer",
|
||||
@"Add FourSquare POIs PMTiles Layer",
|
||||
@"Add Earthquake Heatmap"
|
||||
@"Add Earthquake Heatmap",
|
||||
@"Particle / QBIN V4 Smoke (T0)"
|
||||
]];
|
||||
break;
|
||||
case MBXSettingsMiscellaneous:
|
||||
@@ -737,6 +752,9 @@ CLLocationCoordinate2D randomWorldCoordinate(void) {
|
||||
case MBXSettingsRuntimeStylingHeatmapLayer:
|
||||
[self addEarthquakeHeatmap];
|
||||
break;
|
||||
case MBXSettingsRuntimeStylingParticleQbinV4Smoke:
|
||||
[self showParticleQbinV4SmokeMenu];
|
||||
break;
|
||||
default:
|
||||
NSAssert(NO, @"All runtime styling setting rows should be implemented");
|
||||
break;
|
||||
@@ -1160,6 +1178,89 @@ CLLocationCoordinate2D randomWorldCoordinate(void) {
|
||||
[self.mapView.style addLayer:circleLayer];
|
||||
}
|
||||
|
||||
// MARK: - STEP-081 Particle / QBIN V4 smoke (test app only)
|
||||
|
||||
- (void)showParticleQbinV4SmokeMenu {
|
||||
UIAlertController *menu = [UIAlertController alertControllerWithTitle:@"Particle / QBIN V4 Smoke"
|
||||
message:@"Wind Golden QBIN T0/T1. This is a private Core test seam."
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
void (^selectTime)(uint64_t, NSString *) = ^(uint64_t timestamp, NSString *label) {
|
||||
[weakSelf installParticleQbinV4SmokeAtTime:timestamp label:label];
|
||||
};
|
||||
[menu addAction:[UIAlertAction actionWithTitle:@"Load T0" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) { selectTime(1789239600000ULL, @"T0"); }]];
|
||||
[menu addAction:[UIAlertAction actionWithTitle:@"Load midpoint (mix 0.5)" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) { selectTime(1789241400000ULL, @"MIDPOINT"); }]];
|
||||
[menu addAction:[UIAlertAction actionWithTitle:@"Load T1" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) { selectTime(1789243200000ULL, @"T1"); }]];
|
||||
[menu addAction:[UIAlertAction actionWithTitle:@"Clear / release" style:UIAlertActionStyleDestructive handler:^(__unused UIAlertAction *a) { [weakSelf clearParticleQbinV4Smoke]; }]];
|
||||
[menu addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
[self presentViewController:menu animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)installParticleQbinV4SmokeAtTime:(uint64_t)requestedTime label:(NSString *)label {
|
||||
[self clearParticleQbinV4Smoke];
|
||||
NSString *(^fixturePath)(NSString *) = ^NSString *(NSString *name) {
|
||||
return [[NSBundle mainBundle] pathForResource:name ofType:@"qbin" inDirectory:@"ParticleSmokeFixtures"];
|
||||
};
|
||||
const NSString *t0Path = fixturePath(@"T0");
|
||||
const NSString *t1Path = fixturePath(@"T1");
|
||||
NSData *t0Data = t0Path ? [NSData dataWithContentsOfFile:t0Path] : nil;
|
||||
NSData *t1Data = t1Path ? [NSData dataWithContentsOfFile:t1Path] : nil;
|
||||
if (!t0Data || !t1Data) {
|
||||
NSLog(@"ParticleQbinV4Smoke: event=FIXTURE_MISSING expected=ParticleSmokeFixtures/T0.qbin,T1.qbin");
|
||||
return;
|
||||
}
|
||||
|
||||
auto& style = [self.mapView mbglMap].getStyle();
|
||||
auto source = std::make_unique<mbgl::style::RasterArraySource>("particle-qbin-v4-smoke-source");
|
||||
auto *sourcePtr = source.get();
|
||||
mbgl::particle::ResolvedFieldProfileContract contract;
|
||||
contract.profileID = "wind";
|
||||
contract.profileVersion = 1;
|
||||
contract.timeRelation = mbgl::particle::FieldTimeRelation::ValidGteReference;
|
||||
contract.requiredPlanes = {{1, mbgl::particle::FieldValueClass::VelocityComponent, 1, 1, mbgl::particle::FieldComponent::UEast, true},
|
||||
{2, mbgl::particle::FieldValueClass::VelocityComponent, 1, 1, mbgl::particle::FieldComponent::VNorth, true},
|
||||
{3, mbgl::particle::FieldValueClass::Validity, 0, 0, mbgl::particle::FieldComponent::None, true}};
|
||||
const auto bytes = [](NSData *data) {
|
||||
const auto *begin = static_cast<const uint8_t *>(data.bytes);
|
||||
return std::vector<uint8_t>(begin, begin + data.length);
|
||||
};
|
||||
const auto r0 = sourcePtr->submitEncodedField("fixture.wind", {"fixture-wind", 1, 1}, contract, bytes(t0Data));
|
||||
const auto r1 = sourcePtr->submitEncodedField("fixture.wind", {"fixture-wind", 2, 2}, contract, bytes(t1Data));
|
||||
sourcePtr->setColorBinding({"fixture.wind", {1, 1, mbgl::particle::FieldComponent::UEast}, mbgl::particle::FieldValueClass::VelocityComponent, 1});
|
||||
sourcePtr->setParticleBinding({mbgl::particle::ParticleBindingMode::VectorComponents, "fixture.wind", {1, 1, mbgl::particle::FieldComponent::UEast}, {2, 1, mbgl::particle::FieldComponent::VNorth}, {}, 1});
|
||||
sourcePtr->setRequestedForecastTime(requestedTime);
|
||||
if (r0 != mbgl::style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted ||
|
||||
r1 != mbgl::style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted) {
|
||||
NSLog(@"ParticleQbinV4Smoke: event=FIELD_REJECTED T0=%d T1=%d", int(r0), int(r1));
|
||||
return;
|
||||
}
|
||||
|
||||
auto weather = std::make_unique<mbgl::style::WeatherColorLayer>("particle-qbin-v4-smoke-weather", "particle-qbin-v4-smoke-source");
|
||||
weather->setOpacity(.82f);
|
||||
weather->setFieldValueSource(mbgl::style::FieldValueSource::VectorMagnitude);
|
||||
weather->setColorStops({{0.f, {0.05f, 0.18f, 0.65f, 1.f}}, {8.f, {0.05f, 0.75f, 0.78f, 1.f}}, {20.f, {0.98f, 0.22f, 0.10f, 1.f}}});
|
||||
auto particles = std::make_unique<mbgl::style::RasterParticleLayer>("particle-qbin-v4-smoke-particles", "particle-qbin-v4-smoke-source");
|
||||
particles->setParticleCount(4096);
|
||||
particles->setParticleOpacity(.95f);
|
||||
particles->setTrailOpacity(.75f);
|
||||
particles->setTrailFade(.94f);
|
||||
particles->setSpeedColorEnabled(true);
|
||||
style.addSource(std::move(source));
|
||||
style.addLayer(std::move(weather));
|
||||
style.addLayer(std::move(particles));
|
||||
[self.mapView setNeedsRerender];
|
||||
NSLog(@"ParticleQbinV4Smoke: event=FIELD_READY state=%@ requestedTime=%llu source=fixture.wind layers=weather-color,raster-particle", label, requestedTime);
|
||||
}
|
||||
|
||||
- (void)clearParticleQbinV4Smoke {
|
||||
auto& style = [self.mapView mbglMap].getStyle();
|
||||
if (style.getLayer("particle-qbin-v4-smoke-particles")) style.removeLayer("particle-qbin-v4-smoke-particles");
|
||||
if (style.getLayer("particle-qbin-v4-smoke-weather")) style.removeLayer("particle-qbin-v4-smoke-weather");
|
||||
if (style.getSource("particle-qbin-v4-smoke-source")) style.removeSource("particle-qbin-v4-smoke-source");
|
||||
[self.mapView setNeedsRerender];
|
||||
NSLog(@"ParticleQbinV4Smoke: event=CLEAR_RELEASED");
|
||||
}
|
||||
|
||||
// Earthquake Heatmap
|
||||
|
||||
- (void)addEarthquakeHeatmap {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <mbgl/shaders/mtl/hillshade_prepare.hpp>
|
||||
#include <mbgl/shaders/mtl/line.hpp>
|
||||
#include <mbgl/shaders/mtl/location_indicator.hpp>
|
||||
#include <mbgl/shaders/mtl/particle_gpu_validation.hpp>
|
||||
#include <mbgl/shaders/mtl/raster.hpp>
|
||||
#include <mbgl/shaders/mtl/symbol.hpp>
|
||||
#include <mbgl/shaders/mtl/widevector.hpp>
|
||||
@@ -131,6 +132,7 @@ void RendererBackend::initShaders(gfx::ShaderRegistry& shaders, const ProgramPar
|
||||
shaders::BuiltIn::LinePatternShader,
|
||||
shaders::BuiltIn::LocationIndicatorShader,
|
||||
shaders::BuiltIn::LocationIndicatorTexturedShader,
|
||||
shaders::BuiltIn::ParticleGpuValidationShader,
|
||||
shaders::BuiltIn::RasterShader,
|
||||
shaders::BuiltIn::SymbolIconShader,
|
||||
shaders::BuiltIn::SymbolSDFShader,
|
||||
|
||||
14
src/mbgl/shaders/mtl/particle_gpu_validation.cpp
Normal file
14
src/mbgl/shaders/mtl/particle_gpu_validation.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#include <mbgl/shaders/mtl/particle_gpu_validation.hpp>
|
||||
#include <mbgl/shaders/shader_defines.hpp>
|
||||
|
||||
namespace mbgl::shaders {
|
||||
using Source = ShaderSource<BuiltIn::ParticleGpuValidationShader, gfx::Backend::Type::Metal>;
|
||||
const std::array<AttributeInfo, 1> Source::attributes = {
|
||||
AttributeInfo{0, gfx::AttributeDataType::Short2, particleGpuValidationUBOCount, idParticleGpuValidationPosVertexAttribute},
|
||||
};
|
||||
const std::array<TextureInfo, 5> Source::textures = {
|
||||
TextureInfo{0, idParticleGpuValidationPreviousTexture}, TextureInfo{1, idParticleGpuValidationMaskTexture},
|
||||
TextureInfo{2, idParticleGpuValidationFieldTexture}, TextureInfo{3, idParticleGpuValidationNextFieldTexture},
|
||||
TextureInfo{4, idParticleGpuValidationStateTexture},
|
||||
};
|
||||
} // namespace mbgl::shaders
|
||||
@@ -816,3 +816,4 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
|
||||
- Win11 实际构建发现并修正 JNI particle binding declaration 参数数目与 Kotlin `validUInt16`/`validSafeInteger` 编译表达式;Debug APK 成功生成、安装并启动。Metro 固定监听 8091,App 仍访问 localhost:8081,正确 reverse 为 `8081 -> 8091`。
|
||||
- Test Host 的 8 个 V4 QBIN Golden Fixture(Wind/Wave/Current/Rain,各 T0/T1)已由用户人工确认功能正常;V4 调用经 `submitEncodedField` 进入 `RasterArraySource::submitEncodedField` / `QbinV2Decoder`,不是 JS 解码。
|
||||
- 当前入口文档已整理:`docs/00_项目总则/当前基线与开发路线_v2.md`、`docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`、`docs/70_发布/Android_RN_QBINV4_13.4.1-particle.3_发布基线_v1.md`。旧 STEP-060~072 与 STEP-080 v1 保留历史事实,但不再作为当前构建命令来源。
|
||||
# STEP-081(2026-09-17):新增 Metal `ParticleGpuValidationShader` 与 iOS Test App Wind QBIN V4 private smoke 入口。Codex 后已完成 GPT/RDC 独立静态审查,并修正 Metal State 输出精度、VELOCITY 零向量 fail-closed 与 iPad Smoke 菜单入口;iPhoneOS device build、Metal runtime compile 与真机验收仍待 RDC,状态 WAITING_RDC_DEVICE_BUILD。
|
||||
|
||||
Reference in New Issue
Block a user