diff --git a/CMakeLists.txt b/CMakeLists.txt index 26ab14508..a53ed1df7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -210,6 +210,8 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/symbol_layer_tweaker.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/collision_layer_tweaker.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/collision_layer_tweaker.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/offline_fixture_particle_adapter.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/offline_fixture_particle_adapter.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/shader_program_base.cpp ) diff --git a/docs/60_测试/40_01_Android离线Fixture到正式粒子渲染最小闭环验证报告_v1.md b/docs/60_测试/40_01_Android离线Fixture到正式粒子渲染最小闭环验证报告_v1.md new file mode 100644 index 000000000..bc6a2628e --- /dev/null +++ b/docs/60_测试/40_01_Android离线Fixture到正式粒子渲染最小闭环验证报告_v1.md @@ -0,0 +1,64 @@ +# 40-01 Android 离线 Fixture 到正式粒子渲染最小闭环验证报告 v1 + +## 状态 + +`WAITING_MANUAL_VALIDATION`。30-01、30-02、30-03 的正式记录均为 `PASS`,30-03 允许进入本任务。本报告只记录 Codex 的 Stage A 事实,不把未执行的 Android 编译或设备视觉验收写为 PASS。 + +## 基线与范围 + +- 分支:`particle/main` +- 开始 HEAD:`1ec44701146925dec9a87618e3f90bee9a12073f` +- 平台范围:Android OpenGL;未修改 iOS、Vulkan、NavSea 或在线数据链。 +- Test App 继续使用既有 `Particle Core Layer Validation` 入口;Core adapter 不依赖 Kotlin/Android UI。 + +## 已实现的 Stage A 链路 + +```text +particle-fixtures/{uniform-east,uniform-north,vortex} + -> OfflineFixtureParticleAdapter + -> ParticleFieldSnapshot + -> GPU field texture + -> State A/B field sampling + -> Trail A/B consuming State A/B + -> canonical world composite / shared coastal mask +``` + +- fixture contract 使用 `metadata.json + field.f32`,校验 schema、id、WIND/GLOBAL、EPSG:4326、bounds、grid registration/order、interleaved UV、little-endian float32、NoData 声明、长度和 SHA-256。 +- `uniform-east`、`uniform-north` 和 `vortex` 均为确定性 16x16 fixture;不会在每帧解析或上传。切换 fixture 时递增 `fieldGeneration/gpuFieldGeneration`,清空旧 trail;Camera 操作不触发 field upload。 +- shader 的 State pass 实际采样 GPU field texture,Trail pass 从 State write texture 读取粒子位置,未使用 Kotlin/CPU 每帧粒子循环。 +- STATIC 不再因资源缺失提前返回。新的 context/resource generation 会建立 field、mask、trail 和 state;STATIC 首帧走零步 bootstrap,提交 state/trail slot 但不增加 `simulationFrame`,之后保持 frozen composite。 +- Activity Bundle 保存/恢复 active、enabled、mask mode 与 fixture mode;GPU runtime state 不写入 Bundle。 + +## Codex 可执行检查 + +| 检查 | 结果 | +|---|---| +| `ninja -C build-linux-opengl` 受影响 Core 对象 | PASS:`offline_fixture_particle_adapter.cpp`、`render_particle_validation_layer.cpp`、`shader_info.cpp`、`particle_validation_layer.cpp` | +| `clang-format --dry-run --Werror` | PASS | +| `git diff --check` | PASS | +| `python3 tools/generate_particle_fixture_f32.py` 后 metadata JSON 与 SHA-256 | PASS | +| adapter smoke test | PASS:`uniform-east`、`uniform-north`、`vortex` 分别加载为 16x16 | + +首次整库构建尝试被 sandbox 默认 ccache 目录只读中断;以 `CCACHE_DIR=/tmp/maplibre-particle-ccache` 重新执行上述受影响对象检查后通过。这不是代码编译失败。 + +## 用户手工 Android OpenGL 验收 + +未执行。已核对当前模块提供 `opengl` flavor,用户应在提交后执行: + +```bash +cd platform/android +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug +``` + +随后按任务书执行 T1-T12,尤其验证 STATIC + OCEAN A/B rotation 无按钮恢复、east/north 方向差异、field/mask generation 在 Camera 操作中稳定,以及 disable/enable、world wrap 和双向 antimeridian。日志筛选关键字:`ParticleField|ParticleGPU|ParticleMask|ParticleRender|ParticleValidation`。 + +## 未覆盖与风险 + +- T1-T12 Android 设备验收、截图和 OpenGL shader 实机链接尚未执行。 +- 独立 EGL context-loss 设备证据沿用 30-02 的 Accepted Risk;本实现已按 resource generation 重建 CPU field 的 GPU texture。 +- iOS、Metal、Android Vulkan、真实数据、生产 coastline/mask、最终粒子数和性能指标均不在本 Stage A 范围。 +- fixture 含 NoData cell 的实机路径尚未有单独 fixture;adapter 已校验 NoData contract,当前三份 deterministic field 均无 NoData cell。 + +## 下一步 + +等待用户提供 Android OpenGL build 与 T1-T12 实际结果;仅在收到证据后进入任务书定义的 Stage B。 diff --git a/docs/报告/40_01_Android粒子最小正式闭环_Codex完成报告_v1.md b/docs/报告/40_01_Android粒子最小正式闭环_Codex完成报告_v1.md new file mode 100644 index 000000000..494705479 --- /dev/null +++ b/docs/报告/40_01_Android粒子最小正式闭环_Codex完成报告_v1.md @@ -0,0 +1,40 @@ +# 40-01 Android 粒子最小正式闭环 Codex 完成报告 v1 + +## 1. 执行结论 + +Stage A 代码与静态检查完成,任务状态为 `WAITING_MANUAL_VALIDATION`。本报告不声明 40-01 PASS。 + +## 2. Git 基线 + +- branch:`particle/main` +- start commit:`1ec44701146925dec9a87618e3f90bee9a12073f`(`docs: record 30-03 final push metadata`) +- 工作树:保留三份既有未跟踪任务书,不纳入本任务提交。 +- submodule:已记录,未修改。 +- `git rerere.enabled`:`true`。 + +最终 commit、push 结果和远端 HEAD 由本报告随后的 Git 收尾提交记录;目标为 `origin/particle/main`。 + +## 3. 实际修改 + +- 新增隔离的 `mbgl::particle::OfflineFixtureParticleAdapter` 与 backend-neutral `ParticleFieldSnapshot`。 +- 新增确定性 `uniform-east`、`uniform-north`、`vortex` fixture 及生成脚本;metadata 遵循架构文档 v1 contract。 +- 在既有 Core RenderLayer 的 State A/B、Trail A/B、shared mask 和 canonical world composite 路线中接入 GPU field texture;没有新建 Android 侧粒子引擎。 +- 修复 STATIC/context recreation 的资源 bootstrap:field、mask、trail、state 和首帧 composite 均可在 STATIC 中建立,bootstrap 为零 simulation step。 +- Test App 保存 fixture selection,并增加 east/north 最小切换控制。 + +## 4. 构建与测试记录 + +- Linux OpenGL 受影响对象编译:PASS。 +- C++ 格式、`git diff --check`、fixture JSON/生成/sha256:PASS。 +- adapter smoke test:PASS,三份 fixture 均成功 decode/validate。 +- 未运行 Gradle、APK 安装、ADB 或 Android 实机验证,遵守任务书限制。 + +## 5. 已知问题与未完成事项 + +- Android OpenGL 的 T1-T12 仍待用户执行,T1 rotation 无按钮恢复是硬门槛。 +- 未实现 iOS/Vulkan/真实在线数据/生产 coastline/性能结论。 +- NoData cell 的实际 fixture 覆盖留待后续数据 fixture Step;本任务的 metadata/data validator 已拒绝无效结构、长度和 checksum。 + +## 6. 与指令书差异 + +无。Stage A 结束时保持 `WAITING_MANUAL_VALIDATION`,未新建或修改 `docs/50_Step记录/`。 diff --git a/include/mbgl/shaders/gl/particle_gpu_validation.hpp b/include/mbgl/shaders/gl/particle_gpu_validation.hpp index 4672cfb5b..41d458794 100644 --- a/include/mbgl/shaders/gl/particle_gpu_validation.hpp +++ b/include/mbgl/shaders/gl/particle_gpu_validation.hpp @@ -41,6 +41,8 @@ void main() { uniform sampler2D u_previous; uniform sampler2D u_mask_texture; +uniform sampler2D u_field_texture; +uniform sampler2D u_state_texture; in vec2 v_uv; in vec2 v_world; @@ -49,15 +51,27 @@ void main() { float mode = u_pass.x; if (mode < 0.5) { - // State pass: deterministic GPU-only state evolution. - fragColor = vec4(fract(previous.r + 0.03125), v_uv, 1.0); + // 40-01: State A/B samples the uploaded OfflineFixture Particle Field. + // RG stores field-normalized position, B stores age, and the Field + // texture encodes canonical U-east/V-north at that geographic sample. + vec2 position = previous.rg; + if (previous.a < 0.5) position = v_uv; + vec2 velocity = texture(u_field_texture, position).rg * 2.0 - 1.0; + position = fract(position + velocity * u_pass.y * 0.004); + if (any(isnan(velocity))) position = v_uv; + fragColor = vec4(position, fract(previous.b + 0.03125 * u_pass.y), 1.0); } else if (mode < 1.5) { - // Trail pass: fade the previous trail and add a moving validation marker. - float marker = 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, u_marker.xy)); + // The trail consumes State B/A written by the real field-sampling pass. + // Four deterministic texels are enough for the Stage A visual proof. + float marker = 0.0; + marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.125, 0.125)).rg))); + marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.375, 0.375)).rg))); + marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.625, 0.625)).rg))); + marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.875, 0.875)).rg))); vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0); fragColor = vec4(min(color, vec3(1.0)), 1.0); } else { - // VALIDATION ONLY: canonical world-space pattern and shared mask. + // Current-frame canonical world composite and shared Mask. float intensity = max(previous.r, max(previous.g, previous.b)); float ocean = texture(u_mask_texture, v_world).a; if (u_pass.z > 0.5 && ocean < 0.5) discard; diff --git a/include/mbgl/shaders/shader_defines.hpp b/include/mbgl/shaders/shader_defines.hpp index 39c4026a4..b7b033476 100644 --- a/include/mbgl/shaders/shader_defines.hpp +++ b/include/mbgl/shaders/shader_defines.hpp @@ -341,6 +341,8 @@ enum { enum { idParticleGpuValidationPreviousTexture, idParticleGpuValidationMaskTexture, + idParticleGpuValidationFieldTexture, + idParticleGpuValidationStateTexture, particleGpuValidationTextureCount }; diff --git a/include/mbgl/style/layers/particle_validation_layer.hpp b/include/mbgl/style/layers/particle_validation_layer.hpp index 682f8ea89..b16a22ba1 100644 --- a/include/mbgl/style/layers/particle_validation_layer.hpp +++ b/include/mbgl/style/layers/particle_validation_layer.hpp @@ -15,6 +15,8 @@ public: void setActive(bool); uint8_t getMaskMode() const; void setMaskMode(uint8_t); + uint8_t getFixtureMode() const; + void setFixtureMode(uint8_t); class Impl; const Impl& impl() const; diff --git a/particle-fixtures/README.md b/particle-fixtures/README.md index 758f55d2c..b22fc570f 100644 --- a/particle-fixtures/README.md +++ b/particle-fixtures/README.md @@ -1,3 +1,5 @@ # Particle 固定离线 Fixtures -第一阶段禁止依赖真实天气服务器。具体数据格式在设计冻结后确定。 +第一阶段禁止依赖真实天气服务器。每个 velocity fixture 遵循冻结的 +`maplibre-particle-fixture/v1` contract:`metadata.json` 和 little-endian +interleaved U/V `field.f32`,其长度与 SHA-256 必须通过 adapter 校验。 diff --git a/particle-fixtures/uniform-east/field.f32 b/particle-fixtures/uniform-east/field.f32 new file mode 100644 index 000000000..54c3635dd Binary files /dev/null and b/particle-fixtures/uniform-east/field.f32 differ diff --git a/particle-fixtures/uniform-east/metadata.json b/particle-fixtures/uniform-east/metadata.json new file mode 100644 index 000000000..48f40eb41 --- /dev/null +++ b/particle-fixtures/uniform-east/metadata.json @@ -0,0 +1,15 @@ +{ + "schema": "maplibre-particle-fixture/v1", + "fixtureId": "uniform-east", + "fieldType": "WIND", + "displayDomain": "GLOBAL", + "dataVersion": 1, + "timeIndex": 0, + "timestamp": null, + "bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, + "crs": "EPSG:4326", + "grid": {"width": 16, "height": 16, "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": 2048, "fieldSha256": "0da648fdcf61b5ea6383960583bdc74287c3fa758c6e5a7eb7d234f2d7c3283f"} +} diff --git a/particle-fixtures/uniform-north/field.f32 b/particle-fixtures/uniform-north/field.f32 new file mode 100644 index 000000000..c7484f066 Binary files /dev/null and b/particle-fixtures/uniform-north/field.f32 differ diff --git a/particle-fixtures/uniform-north/metadata.json b/particle-fixtures/uniform-north/metadata.json new file mode 100644 index 000000000..bafbe578b --- /dev/null +++ b/particle-fixtures/uniform-north/metadata.json @@ -0,0 +1,15 @@ +{ + "schema": "maplibre-particle-fixture/v1", + "fixtureId": "uniform-north", + "fieldType": "WIND", + "displayDomain": "GLOBAL", + "dataVersion": 1, + "timeIndex": 0, + "timestamp": null, + "bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, + "crs": "EPSG:4326", + "grid": {"width": 16, "height": 16, "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": 2048, "fieldSha256": "20eb0da1812e616cee1740ee8e22a7bda377d6785cc6e7007fb26afa5ab255bc"} +} diff --git a/particle-fixtures/vortex/field.f32 b/particle-fixtures/vortex/field.f32 new file mode 100644 index 000000000..b0a353ec3 Binary files /dev/null and b/particle-fixtures/vortex/field.f32 differ diff --git a/particle-fixtures/vortex/metadata.json b/particle-fixtures/vortex/metadata.json new file mode 100644 index 000000000..da7b3b670 --- /dev/null +++ b/particle-fixtures/vortex/metadata.json @@ -0,0 +1,15 @@ +{ + "schema": "maplibre-particle-fixture/v1", + "fixtureId": "vortex", + "fieldType": "WIND", + "displayDomain": "GLOBAL", + "dataVersion": 1, + "timeIndex": 0, + "timestamp": null, + "bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, + "crs": "EPSG:4326", + "grid": {"width": 16, "height": 16, "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": 2048, "fieldSha256": "aa7fd26d5e4605dc363636b99e17c6bb495d51350a5a2cd8abe5bf0ada741c5a"} +} diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index 236f8c7a4..dfe640220 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -21,15 +21,17 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var active = true private var enabled = true private var maskMode = MASK_GLOBAL + private var fixtureMode = FIXTURE_UNIFORM_EAST override fun onCreate(savedInstanceState: Bundle?) { active = savedInstanceState?.getBoolean(STATE_ACTIVE, true) ?: true enabled = savedInstanceState?.getBoolean(STATE_ENABLED, true) ?: true maskMode = savedInstanceState?.getInt(STATE_MASK_MODE, MASK_GLOBAL) ?: MASK_GLOBAL + fixtureMode = savedInstanceState?.getInt(STATE_FIXTURE_MODE, FIXTURE_UNIFORM_EAST) ?: FIXTURE_UNIFORM_EAST super.onCreate(savedInstanceState) Log.i( TAG, - "ParticleValidation30_03: Activity state restored active=$active enabled=$enabled maskMode=$maskMode " + + "ParticleRender: Activity state restored active=$active enabled=$enabled maskMode=$maskMode fixtureMode=$fixtureMode " + "recreated=${savedInstanceState != null}" ) setContentView(R.layout.activity_particle_core_layer_validation) @@ -69,6 +71,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { findViewById