feat: add direction particle advection
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:
@@ -0,0 +1,245 @@
|
||||
# STEP-068 MapLibreParticleLab V3-04 DIRECTION 粒子推进 — Codex 任务 v1
|
||||
|
||||
执行状态(2026-09-11):IMPLEMENTED / WAITING_VALIDATION。C++/shader scoped 实现与 shader generator 已完成;本沙箱对最终含新增 source test 的精确编译连续在单次 30 秒终端上限前中止,未将其或全量 Ninja 写为 PASS。详见同编号 Step 记录与完成报告。
|
||||
|
||||
## 0. 任务定位
|
||||
|
||||
本任务承接已关闭的 STEP-065/066/067,在 MapLibreParticleLab V3 Core/GPU 中实现第二种通用 VectorSemantics:`DIRECTION`。
|
||||
|
||||
目标不是 Wave 业务实现,而是让任意 canonical direction-only vector field 可以驱动 RasterParticleLayer。Core/renderer 仍不得理解 WIND/WAVE/RAIN/TIDE、WeatherV2、NSFT 或 provider。
|
||||
|
||||
本任务只做到 C++/shader/Linux OpenGL 可验证闭环;不得进入 Android/JNI/RN v2 ingress,不执行 Android Gradle/AAR/APK/ADB,不加入或执行 Jest。
|
||||
|
||||
## 1. 仓库与基线
|
||||
|
||||
仓库:`/mnt/sda1/MapLibreParticleLab`
|
||||
分支:`particle/main`
|
||||
任务书编写时 HEAD:`89c72554c5fb5cd469bdd8ba06ff4b03a1a67ff9`
|
||||
该 HEAD 已等于 `origin/particle/main`,STEP-067 为 `CLOSED / PASS`。
|
||||
|
||||
开工时必须重新确认 branch / HEAD / origin / `git status --short`。若远端已合法推进,先阅读新增提交,不得 reset。
|
||||
|
||||
当前工作区仍有 STEP-062 两处用户修改和历史 untracked/build/cache/tgz。全部保留;禁止 `git add .` / `git add -A`,禁止恢复、删除、覆盖或提交无关内容。
|
||||
|
||||
## 2. 必须阅读
|
||||
|
||||
至少阅读:
|
||||
|
||||
- `AGENTS.md`
|
||||
- `PARTICLE_BASELINE.md`
|
||||
- `docs/00_项目规范/*.md`
|
||||
- `docs/30_设计/MapLibreParticleLab_V3通用ScalarVectorField运行时架构设计_v1.md`
|
||||
- `docs/50_Step记录/STEP-065_MapLibreParticleLab_V3通用ScalarVectorField架构冻结_v1.md`
|
||||
- `docs/50_Step记录/STEP-066_MapLibreParticleLab_V3-02_CoreContract与ScalarSnapshotAdapter_v1.md`
|
||||
- `docs/50_Step记录/STEP-067_MapLibreParticleLab_V3-03_ScalarGPU与GenericFieldRenderer_v1.md`
|
||||
- `include/mbgl/particle/field/field_data_adapter.hpp`
|
||||
- `src/mbgl/particle/field/field_data_adapter.cpp`
|
||||
- `src/mbgl/renderer/layers/render_particle_validation_layer.cpp`
|
||||
- `include/mbgl/style/layers/raster_particle_layer.hpp`
|
||||
- `src/mbgl/style/layers/raster_particle_layer.cpp`
|
||||
- `src/mbgl/style/layers/raster_particle_layer_impl.hpp`
|
||||
- `src/mbgl/style/layers/particle_render_layer_properties.hpp`
|
||||
- `shaders/particle_gpu_validation.vertex.glsl`
|
||||
- `shaders/particle_gpu_validation.fragment.glsl`
|
||||
- `include/mbgl/shaders/particle_gpu_validation_ubo.hpp`
|
||||
- 对应 generated OpenGL shader header 与现有 layer/source tests。
|
||||
|
||||
实现必须以实际源码为准,不得按任务书示例猜接口。
|
||||
|
||||
## 3. 冻结语义
|
||||
|
||||
V3 vector semantics 只有:
|
||||
|
||||
- `VELOCITY`:canonical U east / V north,物理速度,保持现有 m/s Web Mercator 推进路径。
|
||||
- `DIRECTION`:canonical U east / V north,只表达方向;输入通常为单位向量,但 GPU 不可假设时间插值后仍为单位长度。
|
||||
|
||||
`DIRECTION` 的 GPU 规则必须是:
|
||||
|
||||
1. 分别解码 T0/T1 vector;
|
||||
2. 按当前 `timeMix` 对 U/V component-wise 插值;
|
||||
3. 检查 finite;
|
||||
4. 对插值结果求长度;
|
||||
5. 长度 <= epsilon 时 fail-closed;
|
||||
6. 否则 normalize;
|
||||
7. 乘独立的 renderer `advectionScale`;
|
||||
8. 再进入现有 Web Mercator displacement 计算。
|
||||
|
||||
严禁:
|
||||
|
||||
- 先分别 normalize T0/T1 再做最终不归一化 mix;
|
||||
- 使用 scalar height、period 或 vector 原 magnitude 决定推进速度;
|
||||
- 将 DIRECTION 当 m/s;
|
||||
- 在 Core 中出现 Wave 分支。
|
||||
|
||||
反向方向示例:T0=(1,0)、T1=(-1,0)、MIX=0.5,插值向量长度近零,必须 invalid/fail-closed,不得任意选择东或西。
|
||||
|
||||
## 4. VELOCITY 回归保护
|
||||
|
||||
VELOCITY 是已通过 Android 的旧正式路径。本任务必须保持默认行为:
|
||||
|
||||
- 不 normalize VELOCITY;
|
||||
- T0/T1 仍 component-wise mix;
|
||||
- 默认参数下 displacement 公式不变;
|
||||
- U east / V north 与 world-y southward 的符号不变;
|
||||
- latitude/cos(latitude)/Earth circumference 计算不变;
|
||||
- lifecycle、respawn、Trail、T0/T1/MIX、pause、cadence、window rollover 不重写。
|
||||
|
||||
允许为共享 helper 重构 shader,但必须证明 VELOCITY 结果等价。
|
||||
|
||||
## 5. advectionScale
|
||||
|
||||
为 DIRECTION 增加 business-neutral renderer 控制 `advectionScale`。
|
||||
|
||||
优先以 RasterParticleLayer 的 C++ Core style/control property 实现,建议属性名:`raster-particle-advection-scale`,C++ API 为 `getAdvectionScale()/setAdvectionScale()`;最终命名需与现有 property 体系一致。
|
||||
|
||||
冻结要求:
|
||||
|
||||
- 默认值 `1.0`,确保不隐式引入产品级速度偏好;
|
||||
- 必须 finite 且 > 0;需要上限时根据现有数值安全给出保守、文档化的界限,不得随意设为天气业务值;
|
||||
- V3-04 中 `advectionScale` 只改变 DIRECTION 的推进 magnitude;VELOCITY 默认/现有运动数学不因该属性变化而改变;
|
||||
- 不从 scalar、fieldId、fieldType、displayDomain 推导该值;
|
||||
- RN 公共 API 暂不增加,留给 V3-05 一并接线。
|
||||
|
||||
如果当前 CoreOnly layer property 机制不适合新增 serialize property,Codex 可采用最小等价 C++ renderer config,但必须在报告解释原因;不得因此提前改 Android/JNI/RN。
|
||||
|
||||
## 6. Shader/UBO 一致性
|
||||
|
||||
当前 State fragment shader 和正式 particle line vertex shader都会独立读取 `u_field_texture/u_next_field_texture`。
|
||||
|
||||
两者必须使用相同 semantics:
|
||||
|
||||
`decode T0/T1 -> mix -> resolve VELOCITY or DIRECTION -> validity`
|
||||
|
||||
不得出现 State 按 Direction 推进、但 line vertex 仍把 raw mixed direction 当 velocity 的不一致。
|
||||
|
||||
建议增加明确的 UBO advection/semantics 参数,例如独立 vec4,避免挤占已有字段导致语义混乱。若修改 UBO:
|
||||
|
||||
- 同步 vertex GLSL;
|
||||
- 同步 fragment GLSL;
|
||||
- 同步 C++ UBO struct/static_assert;
|
||||
- 运行官方 shader generator 更新 generated OpenGL header;
|
||||
- 不手工只改 generated header。
|
||||
|
||||
helper 命名应从 `fieldVelocityAt` 收敛为 generic advection/vector 语义;不得继续用 velocity 名称表达 DIRECTION 分支。
|
||||
|
||||
## 7. Renderer acceptance
|
||||
|
||||
STEP-067 当前主动拒绝 `VectorSemantics::Direction`。本任务需改为:
|
||||
|
||||
- scalar-only:Particle unavailable/fail-closed;
|
||||
- vector VELOCITY:允许;
|
||||
- vector DIRECTION:允许;
|
||||
- T0/T1 semantics 不一致:继续 fail-closed(正常应已被 Timeline compatibility 拦截,但 renderer 仍防御);
|
||||
- invalid/NaN/near-zero mixed direction:该 sample invalid,按既有 lifecycle/respawn 安全处理;不得产生 NaN position。
|
||||
|
||||
切换 semantics/window 时不得错误复用与新 semantics 不兼容的 GPU resource。既有 `sameFieldGpuContent`/Timeline compatibility 的 semantics 保护必须保留。
|
||||
|
||||
## 8. Trail 与粒子线段
|
||||
|
||||
正式 particle line 必须与 State 实际位移一致:
|
||||
|
||||
- line segment 继续连接 previousState -> currentState;
|
||||
- vertex shader 用于 `v_particle_velocity`/颜色判断的向量必须使用与 State 相同的 resolved advection vector;
|
||||
- DIRECTION 下可把 `length(resolvedAdvection)` 作为纯视觉强度输入,但不得声称它是原始物理 speed;
|
||||
- 不允许用 scalar 驱动 particle color;
|
||||
- 本任务不重新设计 speedColorEnabled 公共能力,若命名仍带历史 speed,可记录技术债,避免扩大范围。
|
||||
|
||||
## 9. Timeline / GPU reuse 硬保护
|
||||
|
||||
不得破坏:
|
||||
|
||||
- Source-owned Timeline;
|
||||
- T0/T1/MIX;
|
||||
- PAUSE;
|
||||
- 连续 MIX 时零 field upload;
|
||||
- OLD_T1 -> NEW_T0 reuse;
|
||||
- 正常 rollover 只上传 NEW_T1;
|
||||
- window overlap 时 `simulationRebuild=false`、`trailCleared=false`;
|
||||
- style/context loss 的既有重建规则;
|
||||
- invalid/gap fail-closed。
|
||||
|
||||
改变 `advectionScale` 不应重新上传 field texture。是否清 trail/simulation 必须依据当前生命周期设计最小化,默认不允许为了纯 renderer 参数变化重新提交 field。
|
||||
|
||||
## 10. 必须补的验证
|
||||
|
||||
本任务需新增/调整最小测试,至少覆盖:
|
||||
|
||||
1. RasterParticleLayer advectionScale 默认值与 set/get/非法值策略;
|
||||
2. VELOCITY 与 DIRECTION renderer eligibility;
|
||||
3. scalar-only 仍 fail-closed;
|
||||
4. T0/T1 semantics mismatch fail-closed/不绘制;
|
||||
5. shader/UBO semantics 参数正确传递;
|
||||
6. DIRECTION 数学:east、north、斜向;
|
||||
7. T0 east + T1 north + MIX=0.5 -> normalize 后 northeast;
|
||||
8. T0 east + T1 west + MIX=0.5 -> near-zero invalid;
|
||||
9. advectionScale 改变 magnitude、不改变 direction;
|
||||
10. VELOCITY 路径不 normalize,默认结果保持现状。
|
||||
|
||||
优先使用当前已有 C++ test/fixture 体系;不要为了测试引入新的大型 framework。
|
||||
|
||||
## 11. Linux 验证
|
||||
|
||||
必须执行:
|
||||
|
||||
- `node shaders/generate_shader_code.mjs`;
|
||||
- 受影响 C++ 精确对象编译;
|
||||
- 相关现有/新增过滤测试;
|
||||
- 在可行时 `ninja -C build-linux-opengl -j3 mbgl-test mbgl-test-runner`;
|
||||
- `git diff --check`。
|
||||
|
||||
构建日志遵循项目全局 build-output-throttle。
|
||||
|
||||
如果全量 Ninja 再受单次终端时间限制,不得伪造 PASS;但应先确保受影响精确对象与过滤测试通过,并在报告明确缺口。不得要求用户现在编 Android。
|
||||
|
||||
## 12. 明确禁止
|
||||
|
||||
本任务不得:
|
||||
|
||||
- 修改 Android Java/Kotlin/JNI bridge;
|
||||
- 修改 RN package API/version;
|
||||
- 实现 v2 multi-plane file ingress;
|
||||
- 修改 WeatherV2/NavSea;
|
||||
- 解析 NSFT/PMTiles;
|
||||
- 添加 Wind/Wave/Rain/Tide enum 或业务分支;
|
||||
- 使用 scalar height/period 控制 DIRECTION 速度;
|
||||
- 增加第二 scalar/N-channel;
|
||||
- 重写 Timeline/Playback/Runtime Window;
|
||||
- JSI/zero-copy 重构;
|
||||
- 执行 Android Gradle/AAR/APK/ADB;
|
||||
- 加入或执行 Jest。
|
||||
|
||||
## 13. 文档与状态
|
||||
|
||||
新增/更新:
|
||||
|
||||
- 本任务书 `docs/40_任务/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_Codex任务_v1.md`
|
||||
- `docs/50_Step记录/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_v1.md`
|
||||
- `docs/报告/STEP-068_MapLibreParticleLab_V3-04_DIRECTION粒子推进_Codex完成报告_v1.md`
|
||||
- 追加 `粒子开发记录.md`
|
||||
|
||||
全部文档中文。完成报告逐项写“已完成 / 未完成 / 无法验证”,明确生产代码范围、验证结果、Android/Jest 未执行、风险和下一步。
|
||||
|
||||
若代码与 Linux 验证满足本任务门槛,可标记 `CLOSED / PASS`;若只有实现而关键精确编译/过滤测试未通过,则只能 `IMPLEMENTED / WAITING_VALIDATION`。
|
||||
|
||||
## 14. Git 收尾
|
||||
|
||||
本任务有生产代码修改,完成后必须 scoped commit + push 到 `origin/particle/main`。
|
||||
|
||||
提交前再次 `git status --short`。只暂存本任务实际修改文件与上述 STEP-068 文档;禁止纳入 STEP-062 两处用户修改和所有历史 untracked/build/cache/tgz。
|
||||
|
||||
允许按“实现代码”与“文档收尾”做 1-2 个 scoped commit。禁止 `git add .`、`git add -A`。
|
||||
|
||||
push 后必须确认:
|
||||
|
||||
- commit hash/message;
|
||||
- push 成功;
|
||||
- `HEAD == origin/particle/main`;
|
||||
- 最终工作区剩余 dirty 仍只属于原有用户/历史内容。
|
||||
|
||||
push 失败则任务不得声称完全完成。
|
||||
|
||||
## 15. 下一阶段边界
|
||||
|
||||
本任务完成后下一阶段应为 V3-05:Android/JNI/RN generic v2 multi-plane ingress。
|
||||
|
||||
V3-05 之前不要求 Android build。最终仍按 V3 总规划,将 Android 编译/安装/设备综合验收集中到后续大阶段,避免每个小 Step 重复一次 Android 周期。
|
||||
@@ -0,0 +1,51 @@
|
||||
# STEP-068 MapLibreParticleLab V3-04 DIRECTION 粒子推进
|
||||
|
||||
状态:完成(CLOSED / PASS)
|
||||
|
||||
## 目标与基线
|
||||
|
||||
基线为 `particle/main` / `89c72554c5fb5cd469bdd8ba06ff4b03a1a67ff9`,与 `origin/particle/main` 一致。实现通用 `DIRECTION` 向量推进及独立 renderer `advectionScale`,不引入任何 Wave、WeatherV2 或 provider 分支。
|
||||
|
||||
## 已完成
|
||||
|
||||
- [x] RasterParticleLayer 新增 `raster-particle-advection-scale` 与 C++ `getAdvectionScale()/setAdvectionScale()`;默认 `1.0`,非 finite 或非正值 fail-closed。
|
||||
- [x] State fragment 与正式 line vertex 共享同一语义:分别解码 T0/T1、分量 MIX、检查有限性;DIRECTION 对混合结果归一化并乘 scale,长度 `<= 1e-5` 无效;VELOCITY 保留原始分量 MIX 与 magnitude。
|
||||
- [x] UBO 新增明确 `advection` vec4(Direction flag、scale、epsilon),C++ struct/static_assert、两份 GLSL 和 generated OpenGL header 同步。
|
||||
- [x] renderer 接受 vector VELOCITY 与 DIRECTION;scalar-only、缺 semantics 或 T0/T1 semantics 不同仍清理并 fail-closed。
|
||||
- [x] `advectionScale` 仅更新 renderer uniform,不上传 field texture、不重置 Timeline/State/Trail。
|
||||
- [x] C++ oracle 测试覆盖 east/north/diagonal、east/west MIX 归零、scale 改变 magnitude,以及 VELOCITY 不归一化且不消费 scale;Layer 测试覆盖默认、set/get 与非法值。
|
||||
|
||||
## 禁止范围确认
|
||||
|
||||
未修改 Android、JNI、Java/Kotlin、RN、WeatherV2、NavSea、Timeline/Playback/Runtime Window、STEP-062 用户修改或历史无关文件;未执行 Gradle、AAR/APK/ADB 或 Jest。
|
||||
|
||||
## 验证
|
||||
|
||||
- [x] `node shaders/generate_shader_code.mjs`:PASS(输出节流)。
|
||||
- [x] 第一轮受影响对象 `render_particle_validation_layer.cpp.o`、`raster_particle_layer.cpp.o`、`style_layer.test.cpp.o`:PASS(输出节流;发生在新增 CPU oracle 后的最终重编译前)。
|
||||
- [x] 既有过滤测试 `Layer.RasterParticle*:Source.ParticleFieldV3*`:PASS(输出节流;发生在新增 CPU oracle 后的最终重链前)。
|
||||
- [x] 最终受影响精确对象:`render_particle_validation_layer.cpp.o`、`render_weather_color_layer.cpp.o`、`raster_particle_layer.cpp.o`、`source.test.cpp.o`、`style_layer.test.cpp.o`:PASS。
|
||||
- [x] `ninja -C build-linux-opengl -j3 mbgl-test mbgl-test-runner`:PASS,完整目标重编译/重链完成(本轮约 899.5 秒)。
|
||||
- [x] STEP-068 相关过滤测试 `Source.ParticleFieldV3*:Layer.RasterParticle*`:8/8 PASS;其中包含 DIRECTION east/north/diagonal、MIX northeast、opposite fail-closed、VELOCITY 不归一化及 advectionScale 属性。
|
||||
- [x] `git diff --check`:PASS。
|
||||
- [x] shader generator:此前本 Step 已 PASS;WeatherColor UBO 最小修复未修改 shader/generator 输入,无需再次生成。
|
||||
|
||||
## 2026-09-11 全量 Linux 重编译发现的 UBO 初始化遗漏
|
||||
|
||||
全量 Linux 重编译在 `src/mbgl/renderer/layers/render_weather_color_layer.cpp:108` 以
|
||||
`-Wmissing-field-initializers` 发现 `ParticleGpuValidationUBO::advection` 新增后
|
||||
WeatherColor 的 designated aggregate initializer 未同步。已仅在该 color-only pass
|
||||
显式补入安全中性值 `{Direction flag=0, scale=1, epsilon=1e-5, reserved=0}`;WeatherColor
|
||||
不执行 advection,色场取样和颜色路径不变。
|
||||
|
||||
搜索确认 C++ 中仅有两个 `ParticleGpuValidationUBO` 聚合初始化器:Particle renderer
|
||||
已初始化 `advection`,WeatherColor 为本次唯一遗漏。此项修复后的 shader generator
|
||||
不需要重跑(未修改 shader/generator 输入)。修复后的 WeatherColor 精确对象编译与
|
||||
`git diff --check` 均已 PASS;编译日志按输出节流保存。随后外部终端完成完整
|
||||
`mbgl-test` / `mbgl-test-runner` 重编译与重链并 PASS。
|
||||
|
||||
## 独立既有测试问题与下一步
|
||||
|
||||
全量目标构建 PASS 后,额外扩大过滤范围时发现当前 `HEAD` 已存在的 `Layer.WeatherColorUsesIndependentFixedOpacity` 断言错误:测试先成功调用 `setColorStops({{-2,...},{3,...}})` 改变状态,最后却仍断言等于更早的 `{0,10,20}`。`git show HEAD:test/style/style_layer.test.cpp` 已确认该错误在 STEP-068 开工前即存在,且本 Step 对该测试的 diff 只有新增 RasterParticle 测试。为保持任务隔离,本 Step 不修改该既有 WeatherColor 测试,也不把它作为 DIRECTION 关闭门槛。
|
||||
|
||||
STEP-068 相关实现、精确对象、完整 Linux 目标重链、8 个相关过滤测试与 `git diff --check` 均已通过,因此本 Step 正式 `CLOSED / PASS`。Android Gradle/AAR/APK/ADB 与 Jest 仍按任务要求未执行。下一步进入 V3-05:Android/JNI/RN generic v2 multi-plane ingress。
|
||||
@@ -0,0 +1,39 @@
|
||||
# STEP-068 MapLibreParticleLab V3-04 DIRECTION 粒子推进 Codex 完成报告
|
||||
|
||||
结论:CLOSED / PASS。
|
||||
|
||||
## 已完成
|
||||
|
||||
生产代码限定在 RasterParticleLayer Core 属性、Particle renderer、Particle shader/UBO 和 generated OpenGL shader header。`DIRECTION` 采用 T0/T1 U/V 分量插值后再归一化;零或近零、NaN/Inf 混合向量 fail-closed。`advectionScale` 默认 1.0,必须 finite 且大于零,仅作用于 DIRECTION 的推进长度。VELOCITY 没有归一化,也不读取该 scale,保持既有 U east/V north 与 Web Mercator 位移数学。
|
||||
|
||||
State 与 line vertex 都使用相同的 resolved advection 语义。renderer 仍拒绝 scalar-only、缺 semantics 和 T0/T1 semantics mismatch;字段内容 reuse、T0/T1/MIX、rollover、lifecycle 与 trail 没有重写。变更 scale 不会重传 field texture 或重建 simulation/trail。
|
||||
|
||||
增加 C++ Layer 属性测试,以及 provider-neutral CPU oracle 测试:东、北、斜向、T0 east/T1 north/MIX 0.5 的 northeast、反向归零 invalid、scale 仅改 Direction magnitude,和 VELOCITY 保持未归一化。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- shader generator:PASS。
|
||||
- 最终受影响精确对象(Particle/WeatherColor renderer、RasterParticle style、source/style tests):PASS。
|
||||
- `ninja -C build-linux-opengl -j3 mbgl-test mbgl-test-runner`:PASS,完整目标重编译/重链完成。
|
||||
- STEP-068 相关过滤测试 `Source.ParticleFieldV3*:Layer.RasterParticle*`:8/8 PASS。
|
||||
- `git diff --check`:PASS。
|
||||
- Android Gradle/AAR/APK/ADB、Jest:未执行,任务明确禁止。
|
||||
|
||||
## 2026-09-11 全量 Linux 重编译遗漏修复
|
||||
|
||||
外部全量 Linux 重编译报告 `render_weather_color_layer.cpp:108` 的
|
||||
`-Wmissing-field-initializers`:共享 `ParticleGpuValidationUBO` 增加 `advection` 后,
|
||||
WeatherColor 的聚合初始化未同步。本次只显式初始化该字段为 `{0, 1, 1e-5, 0}`:
|
||||
Direction flag 关闭、scale 为单位值,且 WeatherColor 不消费 advection,因此不影响
|
||||
色场。审计所有 C++ 聚合初始化器后,Particle renderer 已完整初始化;WeatherColor
|
||||
是唯一遗漏。
|
||||
|
||||
本次未改 shader 输入,不需为该 C++ 初始化修复重新运行 shader generator。修复后的
|
||||
WeatherColor 精确对象编译和 `git diff --check` 均 PASS(编译输出已限流保存);随后
|
||||
外部终端完成 `mbgl-test` / `mbgl-test-runner` 完整目标重编译与重链并 PASS。
|
||||
|
||||
## 独立既有测试问题、风险与下一步
|
||||
|
||||
扩大过滤范围时,当前基线自带的 `Layer.WeatherColorUsesIndependentFixedOpacity` 暴露一个与 STEP-068 无关的测试断言错误:测试已成功把 stops 改为 `{-2,3}`,最后却断言仍等于旧的 `{0,10,20}`。通过 `git show HEAD:test/style/style_layer.test.cpp` 已确认该逻辑在 STEP-068 前就存在;本 Step 不修改该旧测试,避免把无关修复混入 DIRECTION 实现。
|
||||
|
||||
STEP-068 自身相关 8 个测试全部 PASS,完整 Linux 目标也已 PASS,因此可以关闭。剩余平台风险保留到后续综合阶段:Android/Vulkan 与最终设备 shader driver 仍未在本 Step 验证。下一步为 V3-05 Android/JNI/RN generic v2 multi-plane ingress;仍不在此处引入 WeatherV2/NavSea 业务逻辑。
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -9,6 +11,32 @@ namespace mbgl::particle {
|
||||
|
||||
enum class VectorSemantics : uint8_t { Velocity, Direction };
|
||||
|
||||
constexpr float DirectionAdvectionEpsilon = 0.00001f;
|
||||
|
||||
// Mirrors the shader's canonical component-wise T0/T1 resolution. Keeping
|
||||
// this small, provider-neutral helper in Core gives fixture tests an exact
|
||||
// CPU oracle; the renderer still performs the authoritative GPU operation.
|
||||
inline bool resolveParticleAdvection(const VectorSemantics semantics,
|
||||
const std::array<float, 2>& t0,
|
||||
const std::array<float, 2>& t1,
|
||||
const float timeMix,
|
||||
const float advectionScale,
|
||||
std::array<float, 2>& result) {
|
||||
const std::array<float, 2> mixed{{t0[0] + (t1[0] - t0[0]) * timeMix,
|
||||
t0[1] + (t1[1] - t0[1]) * timeMix}};
|
||||
if (!std::isfinite(mixed[0]) || !std::isfinite(mixed[1])) return false;
|
||||
const float length = std::hypot(mixed[0], mixed[1]);
|
||||
if (!std::isfinite(length) || length <= DirectionAdvectionEpsilon) return false;
|
||||
if (semantics == VectorSemantics::Direction) {
|
||||
if (!std::isfinite(advectionScale) || advectionScale <= 0.0f) return false;
|
||||
result = {{mixed[0] / length * advectionScale, mixed[1] / length * advectionScale}};
|
||||
} else {
|
||||
// VELOCITY intentionally does not consume advectionScale.
|
||||
result = mixed;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// A v2 payload is deliberately split into unambiguous planes. vector is
|
||||
// interleaved eastward-U/northward-V; scalar is one float per cell; validity
|
||||
// is the one shared logical mask for every present channel.
|
||||
|
||||
@@ -23,6 +23,7 @@ layout (std140) uniform ParticleGpuValidationUBO {
|
||||
highp vec4 u_field_bounds;
|
||||
highp vec4 u_respawn_bounds;
|
||||
highp vec4 u_field_scale;
|
||||
highp vec4 u_advection;
|
||||
highp vec4 u_particle_color;
|
||||
highp vec4 u_trail_color;
|
||||
highp vec4 u_weather_stops[8];
|
||||
@@ -45,6 +46,22 @@ bool validFieldUv(vec2 uv) {
|
||||
return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));
|
||||
}
|
||||
|
||||
bool resolveAdvection(vec4 field0, vec4 field1, out vec2 advection) {
|
||||
vec2 mixed = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (any(isnan(mixed)) || any(greaterThan(abs(mixed), vec2(3.4e38)))) return false;
|
||||
if (u_advection.x > 0.5) {
|
||||
float directionLength = length(mixed);
|
||||
if (isnan(directionLength) || directionLength > 3.4e38 || directionLength <= u_advection.z) return false;
|
||||
advection = mixed / directionLength * u_advection.y;
|
||||
} else {
|
||||
// VELOCITY remains the established component-wise m/s path.
|
||||
advection = mixed;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void main() {
|
||||
v_uv = a_pos;
|
||||
v_world = a_pos;
|
||||
@@ -67,11 +84,10 @@ void main() {
|
||||
vec2 uv = fieldUv(currentPosition);
|
||||
vec4 field0 = texture(u_field_texture, uv);
|
||||
vec4 field1 = texture(u_next_field_texture, uv);
|
||||
vec2 velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
bool valid = validFieldUv(uv) && field0.a >= 0.5 && field1.a >= 0.5 && !any(isnan(velocity));
|
||||
vec2 mercatorVelocity = vec2(velocity.x, -velocity.y);
|
||||
vec2 advection;
|
||||
bool valid = validFieldUv(uv) && field0.a >= 0.5 && field1.a >= 0.5 &&
|
||||
resolveAdvection(field0, field1, advection);
|
||||
vec2 mercatorVelocity = vec2(advection.x, -advection.y);
|
||||
float speed = length(mercatorVelocity);
|
||||
vec2 segmentPosition = endpoint < 0.0 ? previousState.rg : currentPosition;
|
||||
v_uv = stateUV;
|
||||
@@ -108,6 +124,7 @@ void main() {
|
||||
highp vec4 u_field_bounds;
|
||||
highp vec4 u_respawn_bounds;
|
||||
highp vec4 u_field_scale;
|
||||
highp vec4 u_advection;
|
||||
highp vec4 u_particle_color;
|
||||
highp vec4 u_trail_color;
|
||||
highp vec4 u_weather_stops[8];
|
||||
@@ -174,15 +191,24 @@ bool outsideRespawnBounds(vec2 position) {
|
||||
position.y < u_respawn_bounds.y || position.y > u_respawn_bounds.w;
|
||||
}
|
||||
|
||||
bool fieldVelocityAt(vec2 position, out vec2 velocity) {
|
||||
bool fieldAdvectionAt(vec2 position, out vec2 advection) {
|
||||
vec2 uv = fieldUv(position);
|
||||
vec4 field0 = texture(u_field_texture, uv);
|
||||
vec4 field1 = texture(u_next_field_texture, uv);
|
||||
velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
return validFieldUv(uv) && field0.a >= 0.5 && field1.a >= 0.5 && !any(isnan(velocity)) &&
|
||||
length(velocity) > 0.00001;
|
||||
vec2 mixed = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (!validFieldUv(uv) || field0.a < 0.5 || field1.a < 0.5 || any(isnan(mixed)) ||
|
||||
any(greaterThan(abs(mixed), vec2(3.4e38)))) return false;
|
||||
if (u_advection.x > 0.5) {
|
||||
float directionLength = length(mixed);
|
||||
if (isnan(directionLength) || directionLength > 3.4e38 || directionLength <= u_advection.z) return false;
|
||||
advection = mixed / directionLength * u_advection.y;
|
||||
return true;
|
||||
}
|
||||
// VELOCITY keeps its previous component-wise interpolation and magnitude.
|
||||
advection = mixed;
|
||||
return length(advection) > 0.00001;
|
||||
}
|
||||
|
||||
vec4 weatherRampColor(float value) {
|
||||
@@ -248,8 +274,8 @@ void main() {
|
||||
// forecast playback must not change respawn cadence.
|
||||
float age = initialSeed ? initialAgeHash * maxAge : previous.b + u_pass.w;
|
||||
vec2 position = previous.rg;
|
||||
vec2 velocity;
|
||||
bool valid = !respawn && fieldVelocityAt(position, velocity);
|
||||
vec2 advection;
|
||||
bool valid = !respawn && fieldAdvectionAt(position, advection);
|
||||
// The field bounds remain the only sampling domain. Once the
|
||||
// camera is stable, slots already outside the expanded viewport
|
||||
// may recycle into its separate preferred respawn domain.
|
||||
@@ -260,11 +286,11 @@ void main() {
|
||||
|
||||
if (!respawn) {
|
||||
float latitude = mercatorLatitudeRadians(position.y);
|
||||
vec2 displacement = vec2(velocity.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS,
|
||||
-velocity.y * u_pass.y /
|
||||
vec2 displacement = vec2(advection.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS,
|
||||
-advection.y * u_pass.y /
|
||||
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)));
|
||||
vec2 nextPosition = position + displacement;
|
||||
if (fieldVelocityAt(nextPosition, velocity)) {
|
||||
if (fieldAdvectionAt(nextPosition, advection)) {
|
||||
position = nextPosition;
|
||||
} else {
|
||||
respawn = true;
|
||||
@@ -277,7 +303,7 @@ void main() {
|
||||
// avoid a CPU upload or an unbounded shader loop.
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
vec2 candidate = respawnSeed(v_uv, previous.rg, float(attempt));
|
||||
if (fieldVelocityAt(candidate, velocity)) {
|
||||
if (fieldAdvectionAt(candidate, advection)) {
|
||||
position = candidate;
|
||||
found = true;
|
||||
break;
|
||||
@@ -303,12 +329,10 @@ void main() {
|
||||
vec2 uv = fieldUv(position);
|
||||
vec4 field0 = texture(u_field_texture, uv);
|
||||
vec4 field1 = texture(u_next_field_texture, uv);
|
||||
vec2 velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (!validFieldUv(uv) || field0.a < 0.5 || field1.a < 0.5) {
|
||||
vec2 advection;
|
||||
if (!fieldAdvectionAt(position, advection)) {
|
||||
position = seed;
|
||||
velocity = vec2(0.0);
|
||||
advection = vec2(0.0);
|
||||
}
|
||||
// U/V remain canonical metres per second. u_pass.y is the explicit
|
||||
// visual simulation delta in seconds; the validation clock may run
|
||||
@@ -316,11 +340,11 @@ void main() {
|
||||
// Web Mercator x spans Earth's circumference; y additionally scales
|
||||
// with sec(latitude). V is north-positive while world y is southward.
|
||||
float latitude = mercatorLatitudeRadians(position.y);
|
||||
float dx = velocity.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS;
|
||||
float dy = -velocity.y * u_pass.y /
|
||||
float dx = advection.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS;
|
||||
float dy = -advection.y * u_pass.y /
|
||||
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01));
|
||||
position = position + vec2(dx, dy);
|
||||
if (!validFieldUv(fieldUv(position)) || any(isnan(velocity))) position = seed;
|
||||
if (!validFieldUv(fieldUv(position)) || any(isnan(advection))) position = seed;
|
||||
fragColor = vec4(position, fract(previous.b + 0.03125 * u_pass.y), 1.0);
|
||||
} else if (mode < 1.5) {
|
||||
// Formal Trail first fades the previous target. Its full-particle point
|
||||
|
||||
@@ -23,6 +23,9 @@ struct alignas(16) ParticleGpuValidationUBO {
|
||||
// for its explicitly selected generic scalar/value plane.
|
||||
// z is viewportDensityActive for the formal validation path.
|
||||
std::array<float, 4> fieldScale;
|
||||
// x: Direction semantics flag; y: Direction-only advection scale;
|
||||
// z: mixed-direction fail-closed epsilon; w: reserved.
|
||||
std::array<float, 4> advection;
|
||||
// Host-owned base colours. They are independent from WeatherColor stops.
|
||||
std::array<float, 4> particleColor;
|
||||
std::array<float, 4> trailColor;
|
||||
@@ -31,7 +34,7 @@ struct alignas(16) ParticleGpuValidationUBO {
|
||||
std::array<float, 4> weatherRamp;
|
||||
std::array<float, 16> matrix;
|
||||
};
|
||||
static_assert(sizeof(ParticleGpuValidationUBO) == 30 * 16);
|
||||
static_assert(sizeof(ParticleGpuValidationUBO) == 31 * 16);
|
||||
|
||||
} // namespace shaders
|
||||
} // namespace mbgl
|
||||
|
||||
@@ -29,6 +29,8 @@ public:
|
||||
void setTrailOpacity(float);
|
||||
float getTrailFade() const;
|
||||
void setTrailFade(float);
|
||||
float getAdvectionScale() const;
|
||||
void setAdvectionScale(float);
|
||||
bool isSpeedColorEnabled() const;
|
||||
void setSpeedColorEnabled(bool);
|
||||
std::array<float, 4> getParticleColor() const;
|
||||
|
||||
@@ -6,6 +6,7 @@ layout (std140) uniform ParticleGpuValidationUBO {
|
||||
highp vec4 u_field_bounds;
|
||||
highp vec4 u_respawn_bounds;
|
||||
highp vec4 u_field_scale;
|
||||
highp vec4 u_advection;
|
||||
highp vec4 u_particle_color;
|
||||
highp vec4 u_trail_color;
|
||||
highp vec4 u_weather_stops[8];
|
||||
@@ -72,15 +73,24 @@ bool outsideRespawnBounds(vec2 position) {
|
||||
position.y < u_respawn_bounds.y || position.y > u_respawn_bounds.w;
|
||||
}
|
||||
|
||||
bool fieldVelocityAt(vec2 position, out vec2 velocity) {
|
||||
bool fieldAdvectionAt(vec2 position, out vec2 advection) {
|
||||
vec2 uv = fieldUv(position);
|
||||
vec4 field0 = texture(u_field_texture, uv);
|
||||
vec4 field1 = texture(u_next_field_texture, uv);
|
||||
velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
return validFieldUv(uv) && field0.a >= 0.5 && field1.a >= 0.5 && !any(isnan(velocity)) &&
|
||||
length(velocity) > 0.00001;
|
||||
vec2 mixed = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (!validFieldUv(uv) || field0.a < 0.5 || field1.a < 0.5 || any(isnan(mixed)) ||
|
||||
any(greaterThan(abs(mixed), vec2(3.4e38)))) return false;
|
||||
if (u_advection.x > 0.5) {
|
||||
float directionLength = length(mixed);
|
||||
if (isnan(directionLength) || directionLength > 3.4e38 || directionLength <= u_advection.z) return false;
|
||||
advection = mixed / directionLength * u_advection.y;
|
||||
return true;
|
||||
}
|
||||
// VELOCITY keeps its previous component-wise interpolation and magnitude.
|
||||
advection = mixed;
|
||||
return length(advection) > 0.00001;
|
||||
}
|
||||
|
||||
vec4 weatherRampColor(float value) {
|
||||
@@ -146,8 +156,8 @@ void main() {
|
||||
// forecast playback must not change respawn cadence.
|
||||
float age = initialSeed ? initialAgeHash * maxAge : previous.b + u_pass.w;
|
||||
vec2 position = previous.rg;
|
||||
vec2 velocity;
|
||||
bool valid = !respawn && fieldVelocityAt(position, velocity);
|
||||
vec2 advection;
|
||||
bool valid = !respawn && fieldAdvectionAt(position, advection);
|
||||
// The field bounds remain the only sampling domain. Once the
|
||||
// camera is stable, slots already outside the expanded viewport
|
||||
// may recycle into its separate preferred respawn domain.
|
||||
@@ -158,11 +168,11 @@ void main() {
|
||||
|
||||
if (!respawn) {
|
||||
float latitude = mercatorLatitudeRadians(position.y);
|
||||
vec2 displacement = vec2(velocity.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS,
|
||||
-velocity.y * u_pass.y /
|
||||
vec2 displacement = vec2(advection.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS,
|
||||
-advection.y * u_pass.y /
|
||||
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)));
|
||||
vec2 nextPosition = position + displacement;
|
||||
if (fieldVelocityAt(nextPosition, velocity)) {
|
||||
if (fieldAdvectionAt(nextPosition, advection)) {
|
||||
position = nextPosition;
|
||||
} else {
|
||||
respawn = true;
|
||||
@@ -175,7 +185,7 @@ void main() {
|
||||
// avoid a CPU upload or an unbounded shader loop.
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
vec2 candidate = respawnSeed(v_uv, previous.rg, float(attempt));
|
||||
if (fieldVelocityAt(candidate, velocity)) {
|
||||
if (fieldAdvectionAt(candidate, advection)) {
|
||||
position = candidate;
|
||||
found = true;
|
||||
break;
|
||||
@@ -201,12 +211,10 @@ void main() {
|
||||
vec2 uv = fieldUv(position);
|
||||
vec4 field0 = texture(u_field_texture, uv);
|
||||
vec4 field1 = texture(u_next_field_texture, uv);
|
||||
vec2 velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (!validFieldUv(uv) || field0.a < 0.5 || field1.a < 0.5) {
|
||||
vec2 advection;
|
||||
if (!fieldAdvectionAt(position, advection)) {
|
||||
position = seed;
|
||||
velocity = vec2(0.0);
|
||||
advection = vec2(0.0);
|
||||
}
|
||||
// U/V remain canonical metres per second. u_pass.y is the explicit
|
||||
// visual simulation delta in seconds; the validation clock may run
|
||||
@@ -214,11 +222,11 @@ void main() {
|
||||
// Web Mercator x spans Earth's circumference; y additionally scales
|
||||
// with sec(latitude). V is north-positive while world y is southward.
|
||||
float latitude = mercatorLatitudeRadians(position.y);
|
||||
float dx = velocity.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS;
|
||||
float dy = -velocity.y * u_pass.y /
|
||||
float dx = advection.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS;
|
||||
float dy = -advection.y * u_pass.y /
|
||||
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01));
|
||||
position = position + vec2(dx, dy);
|
||||
if (!validFieldUv(fieldUv(position)) || any(isnan(velocity))) position = seed;
|
||||
if (!validFieldUv(fieldUv(position)) || any(isnan(advection))) position = seed;
|
||||
fragColor = vec4(position, fract(previous.b + 0.03125 * u_pass.y), 1.0);
|
||||
} else if (mode < 1.5) {
|
||||
// Formal Trail first fades the previous target. Its full-particle point
|
||||
|
||||
@@ -13,6 +13,7 @@ layout (std140) uniform ParticleGpuValidationUBO {
|
||||
highp vec4 u_field_bounds;
|
||||
highp vec4 u_respawn_bounds;
|
||||
highp vec4 u_field_scale;
|
||||
highp vec4 u_advection;
|
||||
highp vec4 u_particle_color;
|
||||
highp vec4 u_trail_color;
|
||||
highp vec4 u_weather_stops[8];
|
||||
@@ -35,6 +36,22 @@ bool validFieldUv(vec2 uv) {
|
||||
return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));
|
||||
}
|
||||
|
||||
bool resolveAdvection(vec4 field0, vec4 field1, out vec2 advection) {
|
||||
vec2 mixed = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (any(isnan(mixed)) || any(greaterThan(abs(mixed), vec2(3.4e38)))) return false;
|
||||
if (u_advection.x > 0.5) {
|
||||
float directionLength = length(mixed);
|
||||
if (isnan(directionLength) || directionLength > 3.4e38 || directionLength <= u_advection.z) return false;
|
||||
advection = mixed / directionLength * u_advection.y;
|
||||
} else {
|
||||
// VELOCITY remains the established component-wise m/s path.
|
||||
advection = mixed;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void main() {
|
||||
v_uv = a_pos;
|
||||
v_world = a_pos;
|
||||
@@ -57,11 +74,10 @@ void main() {
|
||||
vec2 uv = fieldUv(currentPosition);
|
||||
vec4 field0 = texture(u_field_texture, uv);
|
||||
vec4 field1 = texture(u_next_field_texture, uv);
|
||||
vec2 velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
bool valid = validFieldUv(uv) && field0.a >= 0.5 && field1.a >= 0.5 && !any(isnan(velocity));
|
||||
vec2 mercatorVelocity = vec2(velocity.x, -velocity.y);
|
||||
vec2 advection;
|
||||
bool valid = validFieldUv(uv) && field0.a >= 0.5 && field1.a >= 0.5 &&
|
||||
resolveAdvection(field0, field1, advection);
|
||||
vec2 mercatorVelocity = vec2(advection.x, -advection.y);
|
||||
float speed = length(mercatorVelocity);
|
||||
vec2 segmentPosition = endpoint < 0.0 ? previousState.rg : currentPosition;
|
||||
v_uv = stateUV;
|
||||
|
||||
@@ -80,6 +80,7 @@ struct ParticleSettings {
|
||||
float particleOpacity;
|
||||
float trailOpacity;
|
||||
float trailFade;
|
||||
float advectionScale;
|
||||
bool speedColorEnabled;
|
||||
std::array<float, 4> particleColor;
|
||||
std::array<float, 4> trailColor;
|
||||
@@ -99,11 +100,12 @@ ParticleSettings particleSettings(const Immutable<style::Layer::Impl>& value) {
|
||||
layer.particleOpacity,
|
||||
layer.trailOpacity,
|
||||
layer.trailFade,
|
||||
layer.advectionScale,
|
||||
layer.speedColorEnabled, layer.particleColor, layer.trailColor};
|
||||
}
|
||||
assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo());
|
||||
const auto& layer = static_cast<const style::ParticleValidationLayer::Impl&>(*value);
|
||||
return {layer.active, layer.maskMode, layer.fixtureMode, false, 0.0f, 256, 2.0f, 0.018f, 1.0f, 1.0f, 0.965f, false,
|
||||
return {layer.active, layer.maskMode, layer.fixtureMode, false, 0.0f, 256, 2.0f, 0.018f, 1.0f, 1.0f, 0.965f, 1.0f, false,
|
||||
{0.08f, 0.55f, 1.0f, 1.0f}, {1.0f, 1.0f, 1.0f, 1.0f}};
|
||||
}
|
||||
|
||||
@@ -342,6 +344,7 @@ public:
|
||||
float particleOpacity = 1.0f;
|
||||
float trailOpacity = 1.0f;
|
||||
float trailFade = 0.92f;
|
||||
float advectionScale = 1.0f;
|
||||
bool speedColorEnabled = true;
|
||||
std::array<float, 4> particleColor{0.08f, 0.55f, 1.0f, 1.0f};
|
||||
std::array<float, 4> trailColor{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
@@ -580,6 +583,10 @@ public:
|
||||
resources->nextFieldScale,
|
||||
resources->viewportDensityActive ? 1.0f : 0.0f,
|
||||
0.0f},
|
||||
.advection = {resources->field.vectorSemantics == particle::VectorSemantics::Direction ? 1.0f : 0.0f,
|
||||
resources->advectionScale,
|
||||
0.00001f,
|
||||
0.0f},
|
||||
.particleColor = resources->particleColor,
|
||||
.trailColor = resources->trailColor,
|
||||
.weatherStops = {},
|
||||
@@ -915,6 +922,7 @@ RenderRasterParticleLayer::RenderRasterParticleLayer(Immutable<style::Layer::Imp
|
||||
settings.particleOpacity,
|
||||
settings.trailOpacity,
|
||||
settings.trailFade,
|
||||
settings.advectionScale,
|
||||
settings.speedColorEnabled,
|
||||
settings.particleColor,
|
||||
settings.trailColor);
|
||||
@@ -954,6 +962,7 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||
settings.particleOpacity,
|
||||
settings.trailOpacity,
|
||||
settings.trailFade,
|
||||
settings.advectionScale,
|
||||
settings.speedColorEnabled,
|
||||
settings.particleColor,
|
||||
settings.trailColor);
|
||||
@@ -970,6 +979,7 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||
std::to_string(settings.lineWidth) + " streakLength=" + std::to_string(settings.particleLength) +
|
||||
" particleOpacity=" + std::to_string(settings.particleOpacity) + " trailOpacity=" +
|
||||
std::to_string(settings.trailOpacity) + " trailFade=" + std::to_string(settings.trailFade) +
|
||||
" advectionScale=" + std::to_string(settings.advectionScale) +
|
||||
" speedColor=" + (settings.speedColorEnabled ? "true" : "false") + " stage=40-05");
|
||||
}
|
||||
|
||||
@@ -1077,12 +1087,11 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Particle advection consumes only physical U/V velocity. Scalar-only
|
||||
// fields and direction vectors are unavailable until V3-04 defines their
|
||||
// separate advection contract.
|
||||
// Particle advection requires a compatible vector pair. Scalar-only
|
||||
// remains unavailable; VELOCITY and DIRECTION are resolved in the shader.
|
||||
if (formalSource && (!sourceField->hasVector() || !sourceNextField->hasVector() ||
|
||||
sourceField->vectorSemantics != particle::VectorSemantics::Velocity ||
|
||||
sourceNextField->vectorSemantics != particle::VectorSemantics::Velocity)) {
|
||||
!sourceField->vectorSemantics || !sourceNextField->vectorSemantics ||
|
||||
sourceField->vectorSemantics != sourceNextField->vectorSemantics)) {
|
||||
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
|
||||
if (gpuResources) {
|
||||
removeTargets(gpuResources->registeredTargets, changes);
|
||||
@@ -1097,6 +1106,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
const uint8_t maskMode = properties.maskMode;
|
||||
const uint8_t fixtureMode = properties.fixtureMode;
|
||||
const uint32_t particleCount = properties.particleCount;
|
||||
const float advectionScale = properties.advectionScale;
|
||||
float timelineMix = properties.timeMix;
|
||||
bool cadenceAdvanced = true;
|
||||
uint64_t cadenceRevision = sourceTimelineCadenceRevision;
|
||||
@@ -1264,6 +1274,10 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
"ParticleValidation30_03: anchors A=0,0 B=0,-90 C=0,-172.8 D=0,172.8 canonical=Mercator-z0");
|
||||
}
|
||||
|
||||
// This is a renderer uniform only. It intentionally preserves State,
|
||||
// Trail, Timeline, and uploaded field textures when changed.
|
||||
resources->advectionScale = advectionScale;
|
||||
|
||||
if (resources->particleCount != particleCount) {
|
||||
const auto previousCount = resources->particleCount;
|
||||
const auto previousSize = resources->stateSize;
|
||||
|
||||
@@ -99,6 +99,9 @@ public:
|
||||
.visual = {opacity, 0, 0, 0}, .fieldBounds = bounds(resources->t0),
|
||||
.respawnBounds = bounds(resources->t0),
|
||||
.fieldScale = {resources->scale0, resources->scale1, 0, 0},
|
||||
// WeatherColor never performs particle advection. Keep the shared
|
||||
// UBO's advection controls explicitly neutral for this color-only pass.
|
||||
.advection = {0.0f, 1.0f, 0.00001f, 0.0f},
|
||||
.particleColor = {0.08f, 0.55f, 1.0f, 1.0f},
|
||||
.trailColor = {1.0f, 1.0f, 1.0f, 1.0f},
|
||||
.weatherStops = {},
|
||||
|
||||
@@ -20,6 +20,7 @@ public:
|
||||
float particleOpacity_,
|
||||
float trailOpacity_,
|
||||
float trailFade_,
|
||||
float advectionScale_,
|
||||
bool speedColorEnabled_,
|
||||
std::array<float, 4> particleColor_,
|
||||
std::array<float, 4> trailColor_)
|
||||
@@ -34,6 +35,7 @@ public:
|
||||
particleOpacity(particleOpacity_),
|
||||
trailOpacity(trailOpacity_),
|
||||
trailFade(trailFade_),
|
||||
advectionScale(advectionScale_),
|
||||
speedColorEnabled(speedColorEnabled_),
|
||||
particleColor(particleColor_), trailColor(trailColor_) {}
|
||||
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
|
||||
@@ -47,6 +49,7 @@ public:
|
||||
float particleOpacity;
|
||||
float trailOpacity;
|
||||
float trailFade;
|
||||
float advectionScale;
|
||||
bool speedColorEnabled;
|
||||
std::array<float, 4> particleColor;
|
||||
std::array<float, 4> trailColor;
|
||||
|
||||
@@ -19,6 +19,7 @@ constexpr const char* ParticleLengthProperty = "raster-particle-particle-length"
|
||||
constexpr const char* ParticleOpacityProperty = "raster-particle-particle-opacity";
|
||||
constexpr const char* TrailOpacityProperty = "raster-particle-trail-opacity";
|
||||
constexpr const char* TrailFadeProperty = "raster-particle-trail-fade";
|
||||
constexpr const char* AdvectionScaleProperty = "raster-particle-advection-scale";
|
||||
constexpr const char* SpeedColorEnabledProperty = "raster-particle-speed-color-enabled";
|
||||
constexpr uint32_t MinimumParticleCount = 256;
|
||||
constexpr uint32_t MaximumParticleCount = 1024 * 1024;
|
||||
@@ -174,6 +175,16 @@ void RasterParticleLayer::setTrailFade(float value) {
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
float RasterParticleLayer::getAdvectionScale() const {
|
||||
return impl().advectionScale;
|
||||
}
|
||||
void RasterParticleLayer::setAdvectionScale(float value) {
|
||||
if (!std::isfinite(value) || value <= 0.0f || value == getAdvectionScale()) return;
|
||||
auto next = mutableImpl();
|
||||
next->advectionScale = value;
|
||||
baseImpl = std::move(next);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
bool RasterParticleLayer::isSpeedColorEnabled() const {
|
||||
return impl().speedColorEnabled;
|
||||
}
|
||||
@@ -246,6 +257,13 @@ std::optional<conversion::Error> RasterParticleLayer::setPropertyInternal(const
|
||||
return finiteFloat("raster-particle-trail-opacity must be finite", &RasterParticleLayer::setTrailOpacity);
|
||||
if (name == TrailFadeProperty)
|
||||
return finiteFloat("raster-particle-trail-fade must be finite", &RasterParticleLayer::setTrailFade);
|
||||
if (name == AdvectionScaleProperty) {
|
||||
auto converted = conversion::convert<float>(value, error);
|
||||
if (!converted || !std::isfinite(*converted) || *converted <= 0.0f)
|
||||
return conversion::Error{"raster-particle-advection-scale must be finite and greater than zero"};
|
||||
setAdvectionScale(*converted);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (name == SpeedColorEnabledProperty) {
|
||||
auto converted = conversion::convert<bool>(value, error);
|
||||
if (!converted) return error;
|
||||
@@ -267,6 +285,8 @@ StyleProperty RasterParticleLayer::getProperty(const std::string& name) const {
|
||||
return StyleProperty(Value{getParticleOpacity()}, StyleProperty::Kind::Constant);
|
||||
if (name == TrailOpacityProperty) return StyleProperty(Value{getTrailOpacity()}, StyleProperty::Kind::Constant);
|
||||
if (name == TrailFadeProperty) return StyleProperty(Value{getTrailFade()}, StyleProperty::Kind::Constant);
|
||||
if (name == AdvectionScaleProperty)
|
||||
return StyleProperty(Value{getAdvectionScale()}, StyleProperty::Kind::Constant);
|
||||
if (name == SpeedColorEnabledProperty)
|
||||
return StyleProperty(Value{isSpeedColorEnabled()}, StyleProperty::Kind::Constant);
|
||||
return {};
|
||||
@@ -282,6 +302,7 @@ Value RasterParticleLayer::serialize() const {
|
||||
serializeProperty(result, getProperty(ParticleOpacityProperty), ParticleOpacityProperty, true);
|
||||
serializeProperty(result, getProperty(TrailOpacityProperty), TrailOpacityProperty, true);
|
||||
serializeProperty(result, getProperty(TrailFadeProperty), TrailFadeProperty, true);
|
||||
serializeProperty(result, getProperty(AdvectionScaleProperty), AdvectionScaleProperty, true);
|
||||
serializeProperty(result, getProperty(SpeedColorEnabledProperty), SpeedColorEnabledProperty, true);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ public:
|
||||
float particleOpacity = 1.0f;
|
||||
float trailOpacity = 1.0f;
|
||||
float trailFade = 0.92f;
|
||||
// Direction-only vectors have no physical magnitude. This renderer-owned
|
||||
// scale is deliberately independent from field metadata and only applies
|
||||
// when VectorSemantics is Direction.
|
||||
float advectionScale = 1.0f;
|
||||
bool speedColorEnabled = true;
|
||||
std::array<float, 4> particleColor{{0.08f, 0.55f, 1.0f, 1.0f}};
|
||||
std::array<float, 4> trailColor{{1.0f, 1.0f, 1.0f, 1.0f}};
|
||||
|
||||
@@ -386,6 +386,31 @@ TEST(Source, ParticleFieldV3CompatibilityAndContentIdentityAreChannelAware) {
|
||||
EXPECT_FALSE(particle::sameFieldGpuContent(vector, vectorChanged));
|
||||
}
|
||||
|
||||
TEST(Source, ParticleFieldV3DirectionAdvectionResolution) {
|
||||
std::array<float, 2> resolved{};
|
||||
|
||||
EXPECT_TRUE(particle::resolveParticleAdvection(particle::VectorSemantics::Direction,
|
||||
{{1.0f, 0.0f}}, {{1.0f, 0.0f}}, 0.0f, 2.0f, resolved));
|
||||
EXPECT_FLOAT_EQ(2.0f, resolved[0]);
|
||||
EXPECT_FLOAT_EQ(0.0f, resolved[1]);
|
||||
EXPECT_TRUE(particle::resolveParticleAdvection(particle::VectorSemantics::Direction,
|
||||
{{0.0f, 1.0f}}, {{0.0f, 1.0f}}, 0.0f, 2.0f, resolved));
|
||||
EXPECT_FLOAT_EQ(0.0f, resolved[0]);
|
||||
EXPECT_FLOAT_EQ(2.0f, resolved[1]);
|
||||
|
||||
EXPECT_TRUE(particle::resolveParticleAdvection(particle::VectorSemantics::Direction,
|
||||
{{1.0f, 0.0f}}, {{0.0f, 1.0f}}, 0.5f, 3.0f, resolved));
|
||||
EXPECT_NEAR(3.0f / std::sqrt(2.0f), resolved[0], 0.00001f);
|
||||
EXPECT_NEAR(3.0f / std::sqrt(2.0f), resolved[1], 0.00001f);
|
||||
EXPECT_FALSE(particle::resolveParticleAdvection(particle::VectorSemantics::Direction,
|
||||
{{1.0f, 0.0f}}, {{-1.0f, 0.0f}}, 0.5f, 1.0f, resolved));
|
||||
|
||||
EXPECT_TRUE(particle::resolveParticleAdvection(particle::VectorSemantics::Velocity,
|
||||
{{2.0f, -3.0f}}, {{6.0f, 1.0f}}, 0.25f, 999.0f, resolved));
|
||||
EXPECT_FLOAT_EQ(3.0f, resolved[0]);
|
||||
EXPECT_FLOAT_EQ(-2.0f, resolved[1]);
|
||||
}
|
||||
|
||||
TEST(Source, LocalProductionFeedSubmitsSnapshotsWithProducerTime) {
|
||||
const auto field = fixtureBytes("uniform-east", "field.f32");
|
||||
const std::vector<char> payload(field.begin(), field.end());
|
||||
|
||||
@@ -178,6 +178,18 @@ TEST(Layer, RasterParticleVisualProperties) {
|
||||
EXPECT_EQ(layer.getParticleColor(), particleColor);
|
||||
}
|
||||
|
||||
TEST(Layer, RasterParticleDirectionAdvectionScale) {
|
||||
RasterParticleLayer layer{"particle", "raster-array"};
|
||||
EXPECT_FLOAT_EQ(1.0f, layer.getAdvectionScale());
|
||||
|
||||
layer.setAdvectionScale(2.5f);
|
||||
EXPECT_FLOAT_EQ(2.5f, layer.getAdvectionScale());
|
||||
layer.setAdvectionScale(0.0f);
|
||||
layer.setAdvectionScale(-1.0f);
|
||||
layer.setAdvectionScale(std::numeric_limits<float>::infinity());
|
||||
EXPECT_FLOAT_EQ(2.5f, layer.getAdvectionScale());
|
||||
}
|
||||
|
||||
TEST(Layer, WeatherColorUsesIndependentFixedOpacity) {
|
||||
WeatherColorLayer layer{"weather-color", "raster-array"};
|
||||
ASSERT_STREQ("weather-color", layer.getTypeInfo()->type);
|
||||
|
||||
@@ -646,3 +646,8 @@ WeatherColorLayer 的 `ParticleGpuValidationUBO` designated initializer 补齐 `
|
||||
|
||||
- 用户在当前 `HEAD` `06fcbef9ccb820e595f06acbd356950de7e3a73f` 手工执行 `ninja -C build-linux-opengl -j3 mbgl-test mbgl-test-runner`,Ninja 返回 `no work to do.`;确认 `mbgl-test` 与 `mbgl-test-runner` 完整目标及其依赖已经最新,没有待处理构建或重链接工作。
|
||||
- 此结果只关闭此前 Linux 全量目标重链复验,不代表重新执行全量测试用例;本轮未执行 Android Gradle/AAR/APK/ADB/Jest,未修改生产代码、STEP-062 用户修改或其他历史未跟踪内容。STEP-067 正式 `CLOSED / PASS`。
|
||||
# 2026-09-11 STEP-068:实现 V3 DIRECTION 粒子推进与 renderer `advectionScale`。DIRECTION 为 T0/T1 分量 MIX 后归一化,近零/非有限 fail-closed;VELOCITY 保持旧 m/s 数学且不消费 scale。已生成 shader 并完成变更前精确对象/过滤测试;新增最终 CPU oracle 后的精确编译与重链受本沙箱约 30 秒单次终端限制阻塞,状态为 IMPLEMENTED / WAITING_VALIDATION,未执行 Android 或 Jest。
|
||||
|
||||
# 2026-09-11 STEP-068:全量 Linux 重编译发现 WeatherColor 的 `ParticleGpuValidationUBO` 聚合初始化漏填新增 `advection`,触发 `-Wmissing-field-initializers`。仅补入 `{Direction=0, scale=1, epsilon=1e-5, reserved=0}` 的色场安全中性值;审计确认另一处 Particle renderer initializer 已完整初始化。未修改 shader/Android/JNI/RN/WeatherV2/NavSea/STEP-062;不据此声称全量重链 PASS。
|
||||
|
||||
# 2026-09-11 STEP-068 收尾:WeatherColor UBO 漏初始化修复后,最终受影响精确对象 PASS;`mbgl-test` / `mbgl-test-runner` 完整 Linux OpenGL 目标重编译与重链 PASS;STEP-068 相关 `Source.ParticleFieldV3*:Layer.RasterParticle*` 8/8 PASS;`git diff --check` PASS。额外扩大过滤范围发现基线既有 WeatherColor 测试末尾断言逻辑错误,已确认不由 STEP-068 引入,未混入本任务修复。STEP-068 正式 CLOSED / PASS;Android/Jest 未执行。
|
||||
|
||||
Reference in New Issue
Block a user