diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v2.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v2.md new file mode 100644 index 000000000..8a80369f1 --- /dev/null +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v2.md @@ -0,0 +1,30 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 v2 + +状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +Starting HEAD:`37c9aa31da1d2b3495c8802e6fd2f6b999562b06` + +## 本轮问题与根因 + +Android 已确认真实 WeatherV2 Field、Host submission、WeatherColorLayer、Timeline State A/B 调度、非零 U/V 与非零 m/s→Web Mercator 位移均存在,但 Timeline Pause 下粒子仍肉眼静止。 + +根因是 Particle State A/B 与 Trail A/B 共用 `createValidationTarget()` 的 `UnsignedByte` target。State RG 保存的是 Web Mercator world position;RGBA8 的一个量化步长约为 `1/255=0.00392 world`,远大于日志中每个真实 Weather simulation step 的 `0.000003–0.000008 world`,写回后不能累积有效位移。 + +## 修复 + +- State A/B target 改为 `TextureChannelDataType::Float`,包括初次创建、context/resource recreation 和 particleCount 变更的 replacement targets。 +- Trail A/B 保持 `TextureChannelDataType::UnsignedByte`,继续是 viewport-sized 颜色/透明度 target;没有升级 WeatherColorLayer 或任意无关 full-screen target。 +- `RenderTarget` 新增 `isRenderable()`,在 Particle target 创建时立即驱动现有 `OffscreenTexture → GL texture → framebuffer` 创建/检查。Android OpenGL 已有映射链为 `Float → RGBA32F + GL_FLOAT → glFramebufferTexture2D → checkFramebuffer()`;无需新增 backend 抽象。 +- Float State FBO 不能创建时不回退到 UByte:`ParticleStateTarget: event=CREATE_FAILED ... dataType=Float` 后停止该 State resource 创建;count change 同样明确返回 `STATE_RESOURCE_CREATE_FAILED dataType=Float`。 +- 每个成功 target 输出 `CREATE_SUCCESS`,Trail pair 就绪时另输出一次 `ParticleStateTarget: stateAType=Float stateBType=Float trailAType=UnsignedByte trailBType=UnsignedByte`。 +- Shader 注释校正为 State RG 保存 `Web Mercator world position`,不是 field-normalized position。 + +## 冻结边界 + +未修改 canonical Field、STEP-054 assets、Host Submission Contract、Timeline、Playback、Runtime Field Window、WeatherColorLayer、U/V=m/s 或 `visualSpeedScale=6000`。继续保持 Timeline Pause 只冻结 forecast 时间、Particle simulation 在当前 Field 中运行;Window reuse、NEW_T1-only、MIX property-only、normal boundary `simulationRebuild=false` / `trailCleared=false` 不变。 + +## 验证与待验收 + +已执行 shader 生成、Linux OpenGL 受影响 Core 精确编译、相关测试源精确编译和 `git diff --check`;未执行 Android Gradle/APK/ADB/App/设备或 Jest。 + +下一轮人工验收的第一优先项是:进入 REAL WEATHER、保持 Timeline Pause,确认 Float State 类型日志后粒子中心位置连续运动。若仍完全不动,停止调节 6000,改以单独任务调查 GPU State readback / State Update→Draw 链。 diff --git a/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v3.md b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v3.md new file mode 100644 index 000000000..15b8c7fdf --- /dev/null +++ b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v3.md @@ -0,0 +1,35 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化人工验收 v3 + +状态:等待用户手工 Android 验收。Codex 不执行 Android Gradle、APK、ADB、App 启动或设备测试。 + +## Windows 11 构建与日志 + +在 Windows 11 项目目录手工执行: + +```powershell +./gradlew :MapLibreAndroidTestApp:assembleOpenGLDebug --console=plain +adb -s 192.168.1.146 install -r +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 logcat | findstr /I "ParticleStateTarget ParticleMotion REAL_WEATHER ParticleTimeline FIELD_REUSE FIELD_UPLOAD simulationRebuild trailCleared" +``` + +## 第一优先:State 精度 + +1. 进入 `Particle Core Layer Validation → REAL WEATHER`,确认 GSI、真实色场、HUD 默认 `P 2048 · W 0.75 · L 0.0040 · O 0.75 · T 0.55 · F 0.88`。 +2. **不要点击 PLAY,或在 `TIME → PAUSE` 后观察。** Particle position 必须在固定真实风场中连续、明显移动;Timeline cursor/MIX 冻结不应暂停 simulation。 +3. 日志必须有: + +```text +ParticleStateTarget: stateAType=Float stateBType=Float trailAType=UnsignedByte trailBType=UnsignedByte +ParticleStateTarget: event=CREATE_SUCCESS kind=STATE dataType=Float +ParticleMotion: ... sampleU=... sampleV=... dt=... visualSpeedScale=6000 estimatedStateDeltaWorld=... +``` + +4. 若有 `CREATE_FAILED kind=STATE dataType=Float`,记录完整错误;Particle State 应 fail-closed,不得静默以 UByte 继续。 +5. 若 Float 日志正确但粒子仍完全不动,**停止调节 visualSpeedScale=6000**,保存日志并进入后续 GPU State texel readback / State Update→Draw 定向任务。 + +## 继续综合验收 + +在第一优先项通过后,沿用 v2 的 REAL WEATHER tuning/RESET、PLAY T0→T3、两次 Window Boundary、Duplicate/Invalid、Pan/Zoom/Rotate/Activity Rotation 检查。正常边界仍应为 OLD_T1→NEW_T0 reuse、only NEW_T1 upload、`simulationRebuild=false`、`trailCleared=false`。 + +本轮没有 Weather Service、React Native、Jest 或 canonical 数据变化。 diff --git a/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v3.md b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v3.md new file mode 100644 index 000000000..5d00e3d4d --- /dev/null +++ b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v3.md @@ -0,0 +1,39 @@ +# STEP-055 真实 WeatherV2 风场 Particle State 纹理精度修复 Codex 完成报告 v3 + +## 基线、范围与根因 + +- Starting HEAD:`37c9aa31da1d2b3495c8802e6fd2f6b999562b06` +- 分支:`particle/main` +- 状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +真实 Weather State shader 已计算非零 m/s→Web Mercator 位移且 State A/B ping-pong 持续,但 State RG world coordinate 写回被共用的 RGBA8/UnsignedByte target 量化。`1/255≈0.00392 world` 大于 Android 已观测的 `0.000003–0.000008 world` 单步位移,故每步无法积累,是“simulation 在跑但粒子静止”的确定性根因。 + +## 修改 + +- `render_particle_validation_layer.cpp`:State A/B 初始创建与 particleCount replacement 均为 `TextureChannelDataType::Float`;Trail A/B 仍为 `UnsignedByte`。创建函数显式接收 data type/kind,并在 FBO 创建失败时 log error、拒绝 target,无 UByte fallback。 +- `render_target.hpp/.cpp`:新增最小 `RenderTarget::isRenderable()` forwarding,使 target 创建即可验证当前后端实际 FBO。 +- `particle_gpu_validation.fragment.glsl`:只修正 RG 为 Web Mercator world position 的注释;未改 m/s conversion 或 `visualSpeedScale=6000`。 +- 生成的 OpenGL shader header 同步更新。 + +Android OpenGL 既有实现已经支持 Float:`gl::Context::createRenderTarget → RenderTarget → gl::OffscreenTexture → Texture2D(RGBA, Float) → GL_RGBA32F/GL_FLOAT → glFramebufferTexture2D → checkFramebuffer()`。因此未修改 Android/OpenGL backend;若设备 FBO 不完整,现有 `isRenderable()` 返回失败,Particle target 记录具体 Float failure 并 fail-closed。 + +运行时日志包括每个成功 `CREATE_SUCCESS`,以及 State/Trail pair summary:`stateAType=Float stateBType=Float trailAType=UnsignedByte trailBType=UnsignedByte`。这基于已实际完成 backend FBO validation,而非仅打印请求值。 + +## 架构保护 + +未修改 canonical Field、U/V 语义、STEP-054 assets、Host submission、Timeline、Runtime Field Window、WeatherColorLayer 或 6000 visual speed scale。没有新增 GPU readback、网络、Provider、React Native、Jest 或 Android UI 变化。 + +## 验证 + +- `node shaders/generate_shader_code.mjs`:PASS;generated OpenGL header 与 fragment source 同步。 +- Linux OpenGL CMake 精确编译:`render_target.cpp`、`render_particle_validation_layer.cpp`、`gl/offscreen_texture.cpp`、`gl/texture2d.cpp`:PASS。 +- 现有非 Jest `test/style/style_layer.test.cpp`:以相同 OpenGL CMake 参数精确编译 PASS;本轮没有适合脱离实际 GL context 的 Float FBO 单元 runner,因此 FBO runtime 完整性保留 Android 实机检查。 +- `git diff --check`:PASS(最终提交前将再次执行)。 +- Android Gradle/APK/ADB/App/设备:未执行。 +- Jest:未执行,未修改。 + +人工验收说明:[v3](../60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v3.md)。第一门槛是 Timeline Pause 下 Float State 粒子连续运动;若仍不动,不调 6000,转入后续 GPU State 读回诊断。 + +## Git + +本报告将在功能提交及 push 后记录实际 commit、push result 与 Final HEAD。用户既有未跟踪文件未触碰、未 stage。 diff --git a/include/mbgl/renderer/render_target.hpp b/include/mbgl/renderer/render_target.hpp index 45ce5a67e..d6fc0e973 100644 --- a/include/mbgl/renderer/render_target.hpp +++ b/include/mbgl/renderer/render_target.hpp @@ -34,6 +34,9 @@ public: /// Get the render target texture const gfx::Texture2DPtr& getTexture(); + /// Create and validate the backing offscreen framebuffer. + bool isRenderable(); + /// @brief Add a layer group to the render target /// @param replace Flag to replace if exists /// @return whether added diff --git a/include/mbgl/shaders/gl/particle_gpu_validation.hpp b/include/mbgl/shaders/gl/particle_gpu_validation.hpp index 02b9a81b1..742af3ea2 100644 --- a/include/mbgl/shaders/gl/particle_gpu_validation.hpp +++ b/include/mbgl/shaders/gl/particle_gpu_validation.hpp @@ -155,8 +155,8 @@ void main() { vec4 previous = texture(u_previous, v_uv); if (mode < 0.5) { - // 40-01: State A/B samples the uploaded OfflineFixture Particle Field. - // RG stores field-normalized position, B stores age, and the Field + // State A/B samples the uploaded canonical Particle Field. + // RG stores Web Mercator world position, B stores age, and the Field // texture encodes canonical U-east/V-north at that geographic sample. // u_mask.w is the explicit State lifecycle flag: new RenderTargets are // cleared opaque by MapLibre, so sampled alpha cannot identify a seed. diff --git a/shaders/particle_gpu_validation.fragment.glsl b/shaders/particle_gpu_validation.fragment.glsl index 590ee3cb2..96231f39b 100644 --- a/shaders/particle_gpu_validation.fragment.glsl +++ b/shaders/particle_gpu_validation.fragment.glsl @@ -70,8 +70,8 @@ void main() { vec4 previous = texture(u_previous, v_uv); if (mode < 0.5) { - // 40-01: State A/B samples the uploaded OfflineFixture Particle Field. - // RG stores field-normalized position, B stores age, and the Field + // State A/B samples the uploaded canonical Particle Field. + // RG stores Web Mercator world position, B stores age, and the Field // texture encodes canonical U-east/V-north at that geographic sample. // u_mask.w is the explicit State lifecycle flag: new RenderTargets are // cleared opaque by MapLibre, so sampled alpha cannot identify a seed. diff --git a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp index e2803d092..e00709e9d 100644 --- a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp +++ b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp @@ -105,6 +105,18 @@ const char* slotName(const uint8_t slot) { return slot == 0 ? "A" : "B"; } +const char* textureDataTypeName(const gfx::TextureChannelDataType type) { + switch (type) { + case gfx::TextureChannelDataType::UnsignedByte: + return "UnsignedByte"; + case gfx::TextureChannelDataType::HalfFloat: + return "HalfFloat"; + case gfx::TextureChannelDataType::Float: + return "Float"; + } + return "Unknown"; +} + Size validViewportSize(const Size size) { return {std::max(1, size.width), std::max(1, size.height)}; } @@ -601,17 +613,29 @@ private: RenderTargetPtr createValidationTarget(gfx::Context& context, const Size size, const std::string& name, - const gfx::TextureFilterType filter) { - auto target = context.createRenderTarget(size, gfx::TextureChannelDataType::UnsignedByte); - if (!target || !target->getTexture()) { + const gfx::TextureFilterType filter, + const gfx::TextureChannelDataType type, + const char* targetKind) { + auto target = context.createRenderTarget(size, type); + if (!target || !target->isRenderable() || !target->getTexture()) { + Log::Error(Event::Render, + "ParticleStateTarget: event=CREATE_FAILED kind=" + std::string(targetKind) + + " dataType=" + textureDataTypeName(type) + " name=" + name); return {}; } target->getTexture()->setSamplerConfiguration( {.filter = filter, .wrapU = gfx::TextureWrapType::Clamp, .wrapV = gfx::TextureWrapType::Clamp}); auto group = context.createLayerGroup(0, 1, name); if (!group || !target->addLayerGroup(std::move(group), true)) { + Log::Error(Event::Render, + "ParticleStateTarget: event=CREATE_FAILED kind=" + std::string(targetKind) + + " dataType=" + textureDataTypeName(type) + " name=" + name + " reason=LAYER_GROUP"); return {}; } + Log::Info(Event::Render, + "ParticleStateTarget: event=CREATE_SUCCESS kind=" + std::string(targetKind) + + " dataType=" + textureDataTypeName(type) + " name=" + name + " size=" + + std::to_string(size.width) + "x" + std::to_string(size.height)); return target; } @@ -927,10 +951,16 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, rebuildParticleDrawResources(owned); for (uint8_t i = 0; i < 2; ++i) { owned->stateTargets[i] = createValidationTarget( - context, owned->stateSize, getID() + "-state-" + slotName(i), gfx::TextureFilterType::Nearest); + context, + owned->stateSize, + getID() + "-state-" + slotName(i), + gfx::TextureFilterType::Nearest, + gfx::TextureChannelDataType::Float, + "STATE"); } if (!owned->shader || !owned->stateTargets[0] || !owned->stateTargets[1]) { - Log::Error(Event::Render, "ParticleValidation30_02: State resource creation failed"); + Log::Error(Event::Render, + "ParticleStateTarget: event=CREATE_FAILED kind=STATE dataType=Float reason=FRAMEBUFFER_UNAVAILABLE"); return; } owned->maskFixture = makeCoastalMaskFixture(); @@ -1059,11 +1089,16 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, std::array replacementTargets; for (uint8_t i = 0; i < 2; ++i) { replacementTargets[i] = createValidationTarget( - context, requestedSize, getID() + "-state-" + slotName(i), gfx::TextureFilterType::Nearest); + context, + requestedSize, + getID() + "-state-" + slotName(i), + gfx::TextureFilterType::Nearest, + gfx::TextureChannelDataType::Float, + "STATE"); } if (!replacementTargets[0] || !replacementTargets[1]) { Log::Error(Event::Render, - "ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=STATE_RESOURCE_CREATE_FAILED " + "ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=STATE_RESOURCE_CREATE_FAILED dataType=Float " "requestedParticleCount=" + std::to_string(particleCount) + " effectiveParticleCount=" + std::to_string(resources->particleCount) + @@ -1286,7 +1321,12 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, resources->trailTargets = {}; for (uint8_t i = 0; i < 2; ++i) { resources->trailTargets[i] = createValidationTarget( - context, viewport, getID() + "-trail-" + slotName(i), gfx::TextureFilterType::Linear); + context, + viewport, + getID() + "-trail-" + slotName(i), + gfx::TextureFilterType::Linear, + gfx::TextureChannelDataType::UnsignedByte, + "TRAIL"); } if (!resources->trailTargets[0] || !resources->trailTargets[1]) { Log::Error(Event::Render, "ParticleGPU: Trail resource creation failed"); @@ -1295,6 +1335,12 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, resources->trailSize = viewport; resources->trailRead = 0; resources->trailSeeded = false; + Log::Info(Event::Render, + "ParticleStateTarget: stateAType=Float stateBType=Float trailAType=UnsignedByte trailBType=UnsignedByte " + "stateGrid=" + std::to_string(resources->stateSize.width) + "x" + + std::to_string(resources->stateSize.height) + " trailSize=" + + std::to_string(viewport.width) + "x" + std::to_string(viewport.height) + + " resourceGeneration=" + std::to_string(resources->generation)); Log::Info(Event::Render, "ParticleGPU: Trail A/B " + std::string(previousSize.isEmpty() ? "create" : "resize") + " resourceGeneration=" + std::to_string(resources->generation) + diff --git a/src/mbgl/renderer/render_target.cpp b/src/mbgl/renderer/render_target.cpp index 81254ba07..496fbf054 100644 --- a/src/mbgl/renderer/render_target.cpp +++ b/src/mbgl/renderer/render_target.cpp @@ -21,6 +21,10 @@ const gfx::Texture2DPtr& RenderTarget::getTexture() { return offscreenTexture->getTexture(); }; +bool RenderTarget::isRenderable() { + return offscreenTexture && offscreenTexture->isRenderable(); +} + bool RenderTarget::addLayerGroup(LayerGroupBasePtr layerGroup, const bool replace) { const auto index = layerGroup->getLayerIndex(); const auto result = layerGroupsByLayerIndex.insert(std::make_pair(index, LayerGroupBasePtr{})); diff --git a/粒子开发记录.md b/粒子开发记录.md index b54aa29fe..b72d0a57b 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -417,3 +417,9 @@ - REAL WEATHER 首次加载采用 P=2048、W=.75、L=.0040、O=.75、T=.55、F=.88,并提供同状态 HUD、低频 `PARTICLE TUNING` 调整与 RESET;不重载 Style/Field/Timeline,只有容量调整会重建粒子 State/draw resources。 - 将原无单位 `velocity * 0.00004` 位移替换为 canonical m/s 到 Web Mercator world 的 Earth-circumference/cos(latitude) 换算;6000 simulation-seconds/real-second 是独立的 validation visual clock,不污染 U/V、validTime 或 Timeline。新增低频 `ParticleMotion` CPU 估算日志,未做 GPU readback。 - Linux OpenGL 受影响 Core/style test translation unit 精确编译与 shader generator 通过;Android Gradle/APK/ADB/Jest 未执行。等待用户按人工验收 v2 做一次综合复验。 + +## 2026-09-05 — STEP-055 Particle State Float 精度修复(IMPLEMENTED / WAITING_ANDROID_VALIDATION) + +- Android 日志已将“真实 U/V、非零 world displacement、State A/B ping-pong 均存在但粒子静止”收敛为 RGBA8 State 量化:单步 `3e-6–8e-6 world` 小于 UByte `1/255` 的可表示间隔。 +- State A/B 现在为 Float,Trail A/B 保持 UnsignedByte;初次创建、context recreation 与 particleCount replacement 均通过同一 Float target 路径,FBO 创建失败明确 fail-closed,不静默降级。 +- 现有 OpenGL `Float → RGBA32F/GL_FLOAT → FBO check` 支持直接复用,未改 backend、canonical/Timeline/Window/WeatherColorLayer 或 visualSpeedScale=6000。下一次 Android 的首要检查是 Timeline Pause 下真实固定场粒子是否持续移动,并核对 `ParticleStateTarget` 类型日志。