diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md index 32dae8020..acb0b48f5 100644 --- a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md @@ -32,3 +32,16 @@ Core 仍拥有 Timeline、Playback 和 Runtime Field Window。颜色层不 tick Codex 未执行 Android Gradle、APK build/install、ADB、App 启动、设备/模拟器测试或 Jest。Linux/OpenGL 受影响 translation unit 精确编译与相关定向测试结果见完成报告。 Android 仍需用户一次性手工综合验收,步骤见 `docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md`。已知 Validation UI 的 MIX 文本滞后以及 WAITING/INVALID 可见性仍为 NON-BLOCKING,不在本阶段单独修复。 + +## 首轮 Android 视觉修复(仍待复验) + +首轮实机确认真实 WeatherV2 T0–T3 Host submission、513×769 canonical Field、WeatherColorLayer 与 REAL WEATHER composite 均可工作;本轮只修复 Validation 显示与粒子物理位移表达。 + +- REAL WEATHER 的 raster basemap 从 OSM 换为 GSI 标准地图:`https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png`。该 source 与 weather/particle source 独立,GSI tile 不可用时仍为 fail-soft;`REAL_WEATHER_COMPOSITE_READY` 明确记录 `basemap=GSI failSoft=true`。 +- 底部入口变为 `TIME | VIS | COUNT | MAP | REAL WEATHER | MORE`。REAL WEATHER 不再位于 MORE:首次点击使用既有 Host Submission 提交真实 T0–T3、fit bounds、开启专属低频 `PARTICLE TUNING` 菜单;已激活时再次点击只显示/关闭菜单,不 reload Style、Dataset、Field 或 Timeline。激活状态以按钮 selected/yellow 显示。 +- REAL WEATHER 专属默认值为 P=2048、W=0.75、L=0.0040、O=0.75、T=0.55、F=0.88;合成场景退出时恢复进入前的旧 validation visual state,故未改变 synthetic 场景默认值。HUD 与调参菜单读取同一实际 runtime state,二级菜单关闭后 HUD 仍显示。 +- Tuning 用低频离散 `-/+` 操作覆盖 particleCount、lineWidth、streakLength、particleOpacity、trailOpacity、trailFade,并提供 RESET DEFAULT。非 particleCount 参数仅通过既有 layer property 更新;不 Style reload、不重新 submit Field、不 reset Timeline/Window。particleCount 允许仅重建 State/draw resources。 +- 为支持任务规定的 2048/512/8192 档,Core 明确接受这些 validation presets,State texture 使用 `ceil(sqrt(count))`,只为实际 count 建 particle vertices;并将 visual property 下限扩到 lineWidth=0.25、streakLength=0.0005。 +- 旧 shader 用无单位 `velocity * frame * 0.00004`,使真实 m/s 没有可审计的 geography conversion。现改为 U/V m/s → Web Mercator world displacement:`dx=U*dt/40075016.6856`、`dy=-V*dt/(40075016.6856*cos(latitude))`。`dt` 是稳定 wall delta(1/240–0.05s)乘 6000 simulation-seconds/real-second 的 Validation visual clock;它不改 canonical U/V、validTime、Timeline mix 或 Playback ownership,只使日本 zoom 下真实 m/s 有可见流线速度。低频 `ParticleMotion` 日志记录 sampleU/V、dt、visualSpeedScale、估算 world delta 和 simulationFrame,无 GPU readback。 + +本轮未修改 STEP-054 assets、Adapter、Host canonical contract、Timeline、Runtime Field Window、Renderer provider/network 边界或 WeatherColorLayer 数据链。状态继续为 **IMPLEMENTED / WAITING_ANDROID_VALIDATION**;新人工验收见 `docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v2.md`。 diff --git a/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v2.md b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v2.md new file mode 100644 index 000000000..3c1f6487a --- /dev/null +++ b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v2.md @@ -0,0 +1,47 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化人工验收 v2 + +状态:等待用户手工 Android 验收。Codex 不执行本文件中的 Gradle、APK、ADB 或设备操作。 + +## 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 "REAL_WEATHER GSI ParticleTuning ParticleMotion ParticleTimeline WINDOW_BOUNDARY FIELD_REUSE FIELD_UPLOAD DUPLICATE INVALID simulationRebuild trailCleared" +``` + +## 入口与默认画面 + +1. 进入 `Particle Core Layer Validation`。 +2. 底栏应为 `TIME | VIS | COUNT | MAP | REAL WEATHER | MORE`;MORE 中不应再有 REAL WEATHER。 +3. 点击 `REAL WEATHER`。确认 GSI 日本及周边地图、半透明 WeatherColorLayer 和细密粒子共同出现,且日志有 `REAL_WEATHER_COMPOSITE_READY ... basemap=GSI`。 +4. HUD 应持续显示:`P 2048 · W 0.75 · L 0.0040 · O 0.75 · T 0.55 · F 0.88`。粒子应明显持续运动,而不是粗大橙色块状轨迹。 + +GSI tile 若网络不可用,记录 `BASEMAP`/GSI 错误即可;WeatherColorLayer、粒子、Timeline 不得因此黑屏或 crash。 + +## 调参、RESET 与暂停 + +再次点击已激活的 `REAL WEATHER`,确认仅打开/关闭 `PARTICLE TUNING`,不出现 `STYLE_LOAD reason=REAL_WEATHER`、不重新加载 Dataset 或提交 Host fields。使用各 `-/+` 项分别改变 P/W/L/O/T/F:HUD 必须立刻显示实际生效值,且菜单关闭后仍显示。 + +- P 可在 256、512、1024、2048、4096、8192、16384 间调整;允许只见 particle State/draw resources 重建。 +- W/L/O/T/F 调整不得产生 STYLE_LOAD、Field upload、Timeline reset、Runtime Window reset 或 Weather dataset reload。 +- `RESET DEFAULT` 后 HUD 恢复 P=2048、W=0.75、L=0.0040、O=0.75、T=0.55、F=0.88。 +- 不点 PLAY 或在 `TIME → PAUSE` 后,Timeline cursor/MIX 可冻结,但粒子仍应在当前固定风场运动。 + +## Timeline、边界与地图交互 + +1. `TIME → PLAY / RESTART`,确认 READY、PLAY、自动 MIX 和 T0→T3。 +2. 观察至少两次 `production→production` boundary:T0/T1→T1/T2→T2/T3。每次应有 `FIELD_REUSE from=OLD_T1`、只上传 `NEW_T1`、`simulationRebuild=false`、`trailCleared=false`。 +3. 检查每约 1 秒的 `ParticleMotion`:sampleU/sampleV 非零时,有 `dt`、`visualSpeedScale=6000` 和非零 `estimatedStateDeltaWorld`;此日志为 CPU 估算,不做持续 GPU readback。 +4. Pan、Zoom、Rotate,并进行 Activity Rotation:GSI、色场和粒子保持地理对齐;旋转后应自动恢复,无需点击按钮。 + +## 负向与保护检查 + +- `MORE → HOST DUPLICATE`:当前 Host window 返回 NOOP,fieldUploadCount/simulationResetCount 无增量。 +- `MORE → HOST INVALID`:Adapter reject,保持最后有效 Field,Renderer/色场连续,无黑屏、无 crash。 +- 本次没有 Weather Service、HTTP、React Native 或 Jest;真实 WeatherV2 仍来自 Android asset。 + +保存日志中的 GSI、ParticleTuning、ParticleMotion、Timeline/Boundary、Duplicate/Invalid 片段及任何视觉问题。MIX 文字可能滞后、WAITING/INVALID 视觉不够明显仍是 NON-BLOCKING observability,应以 Core 日志判断。 diff --git a/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v2.md b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v2.md new file mode 100644 index 000000000..fe063d56e --- /dev/null +++ b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v2.md @@ -0,0 +1,45 @@ +# STEP-055 真实 WeatherV2 风场综合可视化修复 Codex 完成报告 v2 + +## 基线与结果 + +- Starting HEAD:`8a0873a5fb2c8867c7e2a126b4d4c185077dd6f9` +- 分支:`particle/main` +- 状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** +- 本轮针对首轮 Android 实机视觉问题修复;Android Gradle、APK、ADB、App/设备测试与 Jest 均未执行。 + +## 已完成 + +- REAL WEATHER basemap 改为 GSI 标准地图,并以 `basemap=GSI failSoft=true` 记录独立底图配置;不引入 key、商业 SDK 或天气网络。 +- REAL WEATHER 现在是 MAP 旁的一级菜单,MORE 不再重复。首次点击提交现有真实 T0–T3 Host dataset、fit bounds 并打开 tuning;再次点击只切换菜单。退出真实场景恢复进入前的 synthetic visual state。 +- 真实场默认 P=2048、W=0.75、L=0.0040、O=0.75、T=0.55、F=0.88;HUD 和 `PARTICLE TUNING` 使用同一状态。离散低频调参/RESET 不重载 Style、不重提 Field、不重置 Timeline 或 Runtime Window;只有 P 允许 State/draw resource recreate。 +- Core 粒子数支持明确的 validation presets 256/512/1024/2048/4096/8192/16384(并保留既有高档位);State grid 改为 `ceil(sqrt(count))`,剩余 texels不绘制。lineWidth/streakLength 下限分别为 0.25/0.0005。 + +## m/s 位移结论 + +此前 State shader 使用 `position += velocity * frame * 0.00004`:它没有 m/s、秒、地球尺度或纬度解释,真实数据虽能推进 A/B,但无法审计真实地理位移,粗宽 trail 也掩盖小位移。 + +现链路保持 canonical U=eastward、V=northward、单位 m/s:wall delta 限制为 1/240–0.05s,作为明确视觉仿真时钟乘 6000 simulation-seconds/real-second;shader 使用 `dx=U*dt/40075016.6856` 与 `dy=-V*dt/(40075016.6856*cos(latitude))` 写入 Web Mercator world state。6000 是 rendering-only time dilation,用于在日本 validation zoom 下将真实 m/s 的亚像素 real-time displacement 显示成可观察流线;它不修改 payload、Adapter、Snapshot、validTime、MIX 或 Timeline playback。低频 `ParticleMotion` 仅报告 CPU field sample 与同公式估算 delta,无 GPU→CPU readback。 + +Pause 仍只冻结 forecast Timeline;Particle State update 保持 active,因此固定风场下粒子继续运动。 + +## 架构保护与未做项 + +未修改 STEP-054 WeatherV2 assets、ParticleFieldDescriptor/Snapshot/DataAdapter、Host Submission contract、Timeline/Playback、Runtime Field Window 或 WeatherColorLayer 数据链。继续保持 A/B→B/C→C/D、OLD_T1→NEW_T0 reuse、only NEW_T1 upload、MIX property-only、正常 boundary `simulationRebuild=false`/`trailCleared=false`;未在 Renderer 引入 provider/network/vendor 逻辑,未接 Weather Service、React Native 或 Jest。 + +Rotation/Viewport 只纳入本轮人工验收,未扩大实现范围。MIX 文本滞后及 WAITING/INVALID 状态视觉仍为 NON-BLOCKING observability。 + +## Linux / 静态验证 + +- `node shaders/generate_shader_code.mjs`:PASS。 +- OpenGL CMake 受影响 Core TU 精确编译:`render_particle_validation_layer.cpp`、`raster_particle_layer.cpp`、`render_weather_color_layer.cpp`、`raster_array_source.cpp`:PASS。 +- `test/style/style_layer.test.cpp` 以同一 OpenGL CMake 参数精确编译:PASS(含 2048 preset 与 visual bounds 更新)。 +- `mbgl-test-runner` 构建已启动以复用现有 target,但在进入无关 Core translation units 前按本任务定向边界停止;不作为通过声明。 +- `git diff --check`:待文档完成后的最终检查。 + +## Android 后续一次验收 + +按 [人工验收指令 v2](../60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v2.md) 在 Windows 11 只做一次构建/install。重点检查 GSI、默认细密且明显运动的粒子、HUD/tuning/RESET、Pause 下 simulation、ParticleMotion、T0→T3 两次 boundary、Pan/Zoom/Rotate/Rotation、Duplicate、Invalid 和 Renderer continuity。 + +## Git 收尾 + +本报告将在本任务相关代码和文档提交后补充实际 commit、push 与 Final HEAD。无关未跟踪的历史 diff、任务书和日志保持未触碰、未 stage。 diff --git a/include/mbgl/shaders/gl/particle_gpu_validation.hpp b/include/mbgl/shaders/gl/particle_gpu_validation.hpp index 773c5024b..02b9a81b1 100644 --- a/include/mbgl/shaders/gl/particle_gpu_validation.hpp +++ b/include/mbgl/shaders/gl/particle_gpu_validation.hpp @@ -120,6 +120,12 @@ bool validFieldUv(vec2 uv) { return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0))); } +const float EARTH_CIRCUMFERENCE_METERS = 40075016.68557849; + +float mercatorLatitudeRadians(float worldY) { + return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY))); +} + vec3 weatherSpeedColor(float speed) { // Fixed 0..30 m/s scale: no per-frame or per-dataset auto stretching. float t = clamp(speed / 30.0, 0.0, 1.0); @@ -168,10 +174,16 @@ void main() { position = seed; velocity = vec2(0.0); } - // Field V remains north-positive. Mercator Y increases southward, so - // convert only at the physical-velocity -> map-displacement boundary. - vec2 mercatorVelocity = vec2(velocity.x, -velocity.y); - position = position + mercatorVelocity * u_pass.y * 0.00004; + // U/V remain canonical metres per second. u_pass.y is the explicit + // visual simulation delta in seconds; the validation clock may run + // faster than wall time, but it never changes the field contract. + // 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 / + (EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)); + position = position + vec2(dx, dy); if (!validFieldUv(fieldUv(position)) || any(isnan(velocity))) position = seed; fragColor = vec4(position, fract(previous.b + 0.03125 * u_pass.y), 1.0); } else if (mode < 1.5) { 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 49356e73c..a8401ded8 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 @@ -1,6 +1,7 @@ package org.maplibre.android.testapp.activity.particle import android.os.Bundle +import android.graphics.Color import android.util.Log import android.widget.Button import android.widget.PopupMenu @@ -37,6 +38,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var productionFeedMode = false private var hostSubmissionMode = false private var realWeatherMode = false + private var realWeatherTuningMenu: PopupMenu? = null + private var realWeatherMenuAnchor: android.view.View? = null + private var openRealWeatherTuningAfterStyle = false + private var preRealWeatherVisuals: ParticleVisuals? = null private var styleLoadEpoch = 0L private var styleReady = false // Display mirror only; Core remains the playback state authority. @@ -69,6 +74,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { mapView = findViewById(R.id.mapView) status = findViewById(R.id.validationStatus) + mapView.addOnDidFailLoadingMapListener { errorMessage -> + if (realWeatherMode) { + Log.w(TAG, "ParticleField: event=BASEMAP_TILE_ERROR provider=GSI reason=$errorMessage failSoft=true") + } + } mapView.onCreate(savedInstanceState) mapView.getMapAsync { map -> maplibreMap = map @@ -78,6 +88,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { .zoom(2.0) .build() } + updateRealWeatherButtonState() loadValidationStyle(active = active, enabled = enabled, reason = "INITIAL") } @@ -103,6 +114,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { if (this::maplibreMap.isInitialized) setValidationProperties(active = active, maskMode = MASK_OCEAN_B) } findViewById