diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v6.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v6.md new file mode 100644 index 000000000..542fbe962 --- /dev/null +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v6.md @@ -0,0 +1,16 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 v6 + +状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION** + +Starting HEAD:`067bfdc38baa32d6e474a502d67bc0910d18e247` + +## 低缩放可见性与 Trail 稳定性修复 + +- visual scale 使用 `effectiveScale = clamp(1200 * 2^(9 - zoom), 300, 153600)`;simulation step 再限制为最多 `2400` seconds/frame。典型 60 FPS:z2≈2400(cap)、z5≈320、z9≈20 simulation seconds/frame。 +- canonical U/V=m/s 与 m/s→Web Mercator 物理换算未改;只是 validation visual clock 依据 zoom 保持可见的屏幕位移。 +- 生命周期改为 real seconds:base=12,hash jitter 后为 9–15 real seconds;State B 使用 wall delta,不再使用 simulation delta,因此 zoom、Timeline speed、Timeline Pause 不改变寿命。 +- 普通 camera transform 首次触发 `CAMERA_INVALIDATE`,暂停 Trail segment 累积而 State 继续;连续操作不重复重建 texture;150ms 稳定后 `CAMERA_STABLE_RESUME`。仅真正 viewport size 改变才 `VIEWPORT_RESIZE` 并重新分配 Trail A/B。 + +State Float、Trail UByte、真实 previous→current segment、respawn/hash stagger、time-based fade、canonical、Timeline、Runtime Window、WeatherColorLayer 和 REAL WEATHER UI 保持不变。 + +shader generator、Linux OpenGL 精确编译、既有非 Jest style layer 精确编译和 `git diff --check` 已通过;Android 与 Jest 未执行。 diff --git a/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v6.md b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v6.md new file mode 100644 index 000000000..1ed68be2e --- /dev/null +++ b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v6.md @@ -0,0 +1,20 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化人工验收指令 v6 + +环境:Windows 11;设备 `192.168.1.146`。Android 构建、安装和设备操作由用户手工执行。 + +```powershell +cd platform\android +$env:RENDERER='opengl'; $env:BUILDTYPE='Debug'; make apackage --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 | Select-String 'Particle(Motion|Lifecycle|Trail|Timeline|GPU|StateTarget)' +``` + +1. REAL WEATHER 自动 fit(z≈2–3):粒子应持续可见和运动,不应只剩 WeatherColorLayer,也不得回到固定粗橙色短棍。 +2. z≈5:粒子仍可见,`ParticleMotion` 检查 `referenceZoom=9`、有效 scale 和 simulation delta。 +3. z≈9:不应飞跃或高频越界 respawn;日志中的 simulation delta 不得超过 2400。 +4. Timeline Pause:forecast 不推进,Particle simulation 与 wall-time lifecycle 继续;`ParticleLifecycle` 应为 `lifecycleBaseRealSeconds=12`,不含 simulation lifecycle 字段。 +5. 连续 pinch zoom/pan/rotate:每段交互最多一次 `CAMERA_INVALIDATE`,停止约 150ms 后一条 `CAMERA_STABLE_RESUME`;State/Field/Timeline 不重置。只有实际 size 变化才出现 `VIEWPORT_RESIZE`。 +6. PLAY 至 T3:继续确认 OLD_T1→NEW_T0 reuse、only NEW_T1 upload、`simulationRebuild=false`、`trailCleared=false`。 + +所有 ADB 命令必须显式使用 `adb -s 192.168.1.146 ...`。若失败,保留完整 Particle 日志;不要通过改回 world-length streak 或提高固定 speed scale 规避。 diff --git a/include/mbgl/shaders/gl/particle_gpu_validation.hpp b/include/mbgl/shaders/gl/particle_gpu_validation.hpp index 0d6267108..2bf5febfa 100644 --- a/include/mbgl/shaders/gl/particle_gpu_validation.hpp +++ b/include/mbgl/shaders/gl/particle_gpu_validation.hpp @@ -201,7 +201,9 @@ void main() { float maxAge = u_marker.z * mix(0.75, 1.25, lifecycleHash); float initialAgeHash = particleHash(v_uv + vec2(71.19, 29.47)); bool respawn = initialSeed; - float age = initialSeed ? initialAgeHash * maxAge : previous.b + u_pass.y; + // Lifecycle is wall-clock based: zoom-aware visual advection and + // 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); diff --git a/shaders/particle_gpu_validation.fragment.glsl b/shaders/particle_gpu_validation.fragment.glsl index 8474156d7..c79252e87 100644 --- a/shaders/particle_gpu_validation.fragment.glsl +++ b/shaders/particle_gpu_validation.fragment.glsl @@ -105,7 +105,9 @@ void main() { float maxAge = u_marker.z * mix(0.75, 1.25, lifecycleHash); float initialAgeHash = particleHash(v_uv + vec2(71.19, 29.47)); bool respawn = initialSeed; - float age = initialSeed ? initialAgeHash * maxAge : previous.b + u_pass.y; + // Lifecycle is wall-clock based: zoom-aware visual advection and + // 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); diff --git a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp index 5ccc6e7c3..dc8580626 100644 --- a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp +++ b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp @@ -53,8 +53,13 @@ constexpr float EarthCircumferenceMeters = 40075016.68557849f; // moves about 100 m (roughly 0.65 px at z9), while a 30 m/s sample remains // below a four-pixel segment at that zoom. constexpr float VisualSimulationSecondsPerRealSecond = 1200.0f; -constexpr float ParticleLifecycleSeconds = 14400.0f; +constexpr float VisualSimulationReferenceZoom = 9.0f; +constexpr float MinVisualSimulationSecondsPerRealSecond = 300.0f; +constexpr float MaxVisualSimulationSecondsPerRealSecond = 153600.0f; +constexpr float MaxSimulationDeltaSeconds = 2400.0f; +constexpr float ParticleLifecycleRealSeconds = 12.0f; constexpr float TrailFadeReferenceFramesPerSecond = 60.0f; +constexpr auto TrailCameraStableDelay = std::chrono::milliseconds(150); std::atomic_uint64_t nextGpuGeneration{0}; std::atomic_uint64_t nextMaskGeneration{0}; @@ -248,11 +253,13 @@ public: bool worldWrapLogged = false; bool antimeridianLogged = false; bool trailCameraKnown = false; + bool trailCameraInvalidated = false; uint64_t lastCameraLogFrame = 0; std::chrono::steady_clock::time_point lastPlaybackHeartbeat{}; std::chrono::steady_clock::time_point lastSimulationTick{}; std::chrono::steady_clock::time_point lastMotionLog{}; std::chrono::steady_clock::time_point lastLifecycleLog{}; + std::chrono::steady_clock::time_point lastTrailCameraChange{}; uint64_t maskGeneration = 0; uint64_t fieldGeneration = 0; uint64_t gpuFieldGeneration = 0; @@ -393,6 +400,8 @@ public: gfx::Texture2DPtr mask_, gfx::Texture2DPtr stateTexture_, float simulationStep_, + float wallDeltaSeconds_, + float effectiveVisualSpeedScale_, int16_t worldWrap_, uint8_t maskMode_) : state(std::move(state_)), @@ -404,6 +413,8 @@ public: mask(std::move(mask_)), stateTexture(std::move(stateTexture_)), simulationStep(simulationStep_), + wallDeltaSeconds(wallDeltaSeconds_), + effectiveVisualSpeedScale(effectiveVisualSpeedScale_), worldWrap(worldWrap_), maskMode(maskMode_) {} @@ -454,9 +465,15 @@ public: Log::Info(Event::Render, "ParticleMotion: fieldId=" + resources->field.fieldID + " sampleU=" + std::to_string(u) + " sampleV=" + std::to_string(v) + - " dt=" + std::to_string(simulationStep) + - " visualSpeedScale=" + std::to_string(VisualSimulationSecondsPerRealSecond) + + " zoom=" + std::to_string(parameters.state.getZoom()) + + " referenceZoom=" + std::to_string(VisualSimulationReferenceZoom) + + " effectiveVisualSpeedScale=" + std::to_string(effectiveVisualSpeedScale) + + " wallDeltaSeconds=" + std::to_string(wallDeltaSeconds) + + " simulationDeltaSeconds=" + std::to_string(simulationStep) + " estimatedStateDeltaWorld=" + std::to_string(dx) + "," + std::to_string(dy) + + " estimatedPixelDelta=" + + std::to_string(std::sqrt(dx * dx + dy * dy) * 512.0f * + std::exp2(static_cast(parameters.state.getZoom()))) + " simulationFrame=" + std::to_string(resources->committedFrame)); } } @@ -465,9 +482,7 @@ public: Log::Info(Event::Render, "ParticleLifecycle: configuredSlots=" + std::to_string(resources->particleCount) + " initialAge=HASH_STAGGERED maxAge=HASH_JITTERED_0.75_1.25 " - "lifecycleBaseSimulationSeconds=" + std::to_string(ParticleLifecycleSeconds) + - " lifecycleBaseRealSeconds=" + - std::to_string(ParticleLifecycleSeconds / VisualSimulationSecondsPerRealSecond) + + "lifecycleBaseRealSeconds=" + std::to_string(ParticleLifecycleRealSeconds) + " respawn=GPU_HASH_ON_INVALID_OR_AGE_EXPIRED" " fieldId=" + resources->field.fieldID + " simulationFrame=" + std::to_string(resources->committedFrame) + " readback=disabled"); @@ -494,10 +509,13 @@ public: // be an explicit lifecycle signal, never inferred from sampled alpha. const bool seedState = pass == ValidationPass::State && !resources->stateSeeded; const shaders::ParticleGpuValidationUBO ubo = { - .pass = {static_cast(pass), frame, maskMode == 0 ? 0.0f : 1.0f, static_cast(maskMode)}, + .pass = {static_cast(pass), + frame, + maskMode == 0 ? 0.0f : 1.0f, + resources->formalDisplay ? wallDeltaSeconds : static_cast(maskMode)}, .marker = {markerX, resources->speedColorEnabled ? 1.0f : 0.0f, - resources->formalDisplay ? ParticleLifecycleSeconds : 0.035f, + resources->formalDisplay ? ParticleLifecycleRealSeconds : 0.035f, resources->formalDisplay ? TrailFadeReferenceFramesPerSecond : 0.965f}, .mask = {static_cast(resources->stateSize.width), static_cast(resources->stateSize.height), @@ -633,6 +651,8 @@ private: gfx::Texture2DPtr mask; gfx::Texture2DPtr stateTexture; float simulationStep; + float wallDeltaSeconds; + float effectiveVisualSpeedScale; int16_t worldWrap; uint8_t maskMode; }; @@ -693,7 +713,9 @@ bool buildQuad(gfx::Context& context, const uint8_t writeSlot, const int16_t worldWrap, const uint8_t maskMode, - const bool clearGroup) { + const bool clearGroup, + const float wallDeltaSeconds = 0.0f, + const float effectiveVisualSpeedScale = VisualSimulationSecondsPerRealSecond) { if (clearGroup) { group.clearDrawables(); } @@ -734,7 +756,19 @@ bool buildQuad(gfx::Context& context, builder->setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture); } builder->addTweaker(std::make_shared( - resources, pass, readSlot, writeSlot, source, target, mask, stateTexture, simulationStep, worldWrap, maskMode)); + resources, + pass, + readSlot, + writeSlot, + source, + target, + mask, + stateTexture, + simulationStep, + wallDeltaSeconds, + effectiveVisualSpeedScale, + worldWrap, + maskMode)); // DrawableBuilder owns the current drawable until flush finalizes it. builder->flush(context); @@ -793,6 +827,8 @@ bool buildParticlePoints(gfx::Context& context, resources->maskTexture, currentStateTexture, 0.0f, + 0.0f, + VisualSimulationSecondsPerRealSecond, 0, 0)); builder->flush(context); @@ -1355,26 +1391,45 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, const auto viewport = validViewportSize(transformState.getSize()); const auto trailCamera = transformState.getLatLng(LatLng::Unwrapped); - const auto cameraChanged = resources->formalDisplay && resources->trailCameraKnown && - (resources->trailSize != viewport || - std::abs(resources->trailCameraLatitude - trailCamera.latitude()) > 0.00001 || - std::abs(resources->trailCameraLongitude - trailCamera.longitude()) > 0.00001 || - std::abs(resources->trailCameraZoom - transformState.getZoom()) > 0.0001 || - std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001 || - std::abs(resources->trailCameraPitch - transformState.getPitch()) > 0.0001); - if (cameraChanged) { - const auto reason = resources->trailSize != viewport ? "resize" - : std::abs(resources->trailCameraZoom - transformState.getZoom()) > 0.0001 ? "zoom" - : std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001 ? "rotate" - : std::abs(resources->trailCameraPitch - transformState.getPitch()) > 0.0001 ? "pitch" - : "pan"; + const auto cameraNow = std::chrono::steady_clock::now(); + const bool viewportChanged = resources->formalDisplay && resources->trailTargets[0] && resources->trailTargets[1] && + resources->trailSize != viewport; + const bool transformChanged = resources->formalDisplay && resources->trailCameraKnown && + (std::abs(resources->trailCameraLatitude - trailCamera.latitude()) > 0.00001 || + std::abs(resources->trailCameraLongitude - trailCamera.longitude()) > 0.00001 || + std::abs(resources->trailCameraZoom - transformState.getZoom()) > 0.0001 || + std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001 || + std::abs(resources->trailCameraPitch - transformState.getPitch()) > 0.0001); + if (viewportChanged) { + const auto previousSize = resources->trailSize; resources->trailTargets = {}; resources->trailSize = {}; resources->trailRead = 0; resources->trailSeeded = false; + resources->trailCameraInvalidated = true; + resources->lastTrailCameraChange = cameraNow; Log::Info(Event::Render, - "ParticleTrail: event=CAMERA_RESET reason=" + std::string(reason) + " statePreserved=true " - "fieldResubmit=false timelineReset=false"); + "ParticleTrail: event=VIEWPORT_RESIZE old=" + std::to_string(previousSize.width) + "x" + + std::to_string(previousSize.height) + " new=" + std::to_string(viewport.width) + "x" + + std::to_string(viewport.height) + " statePreserved=true fieldResubmit=false timelineReset=false"); + } else if (transformChanged) { + if (!resources->trailCameraInvalidated) { + const auto reason = std::abs(resources->trailCameraZoom - transformState.getZoom()) > 0.0001 ? "zoom" + : std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001 ? "rotate" + : std::abs(resources->trailCameraPitch - transformState.getPitch()) > 0.0001 ? "pitch" + : "pan"; + resources->trailCameraInvalidated = true; + resources->trailSeeded = false; + Log::Info(Event::Render, + "ParticleTrail: event=CAMERA_INVALIDATE reason=" + std::string(reason) + + " statePreserved=true fieldResubmit=false timelineReset=false"); + } + resources->lastTrailCameraChange = cameraNow; + } else if (resources->trailCameraInvalidated && + cameraNow - resources->lastTrailCameraChange >= TrailCameraStableDelay) { + resources->trailCameraInvalidated = false; + Log::Info(Event::Render, + "ParticleTrail: event=CAMERA_STABLE_RESUME statePreserved=true fieldResubmit=false timelineReset=false"); } resources->trailCameraKnown = true; resources->trailCameraLatitude = trailCamera.latitude(); @@ -1463,7 +1518,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, 0, maskMode, true); - const bool particlesBuilt = !resources->formalDisplay || + const bool particlesBuilt = !resources->formalDisplay || resources->trailCameraInvalidated || buildParticlePoints(context, resources, trailGroup, @@ -1562,8 +1617,14 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, } resources->lastSimulationTick = now; const float trailWallDeltaSeconds = std::clamp(wallDeltaSeconds, 0.0f, 0.250f); - const float visualSimulationDeltaSeconds = - std::clamp(wallDeltaSeconds, 1.0f / 240.0f, 0.050f) * VisualSimulationSecondsPerRealSecond; + const float zoom = static_cast(transformState.getZoom()); + const float effectiveVisualSpeedScale = std::clamp( + VisualSimulationSecondsPerRealSecond * std::exp2(VisualSimulationReferenceZoom - zoom), + MinVisualSimulationSecondsPerRealSecond, + MaxVisualSimulationSecondsPerRealSecond); + const float visualSimulationDeltaSeconds = std::min( + std::clamp(wallDeltaSeconds, 1.0f / 240.0f, 0.050f) * effectiveVisualSpeedScale, + MaxSimulationDeltaSeconds); const bool stateBuilt = buildQuad(context, resources, @@ -1579,7 +1640,9 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, stateWrite, 0, maskMode, - true); + true, + trailWallDeltaSeconds, + effectiveVisualSpeedScale); const bool trailBuilt = buildQuad(context, resources, trailGroup, @@ -1594,8 +1657,10 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, trailWrite, 0, maskMode, - true); - const bool particlesBuilt = !resources->formalDisplay || + true, + trailWallDeltaSeconds, + effectiveVisualSpeedScale); + const bool particlesBuilt = !resources->formalDisplay || resources->trailCameraInvalidated || buildParticlePoints(context, resources, trailGroup, diff --git a/粒子开发记录.md b/粒子开发记录.md index 8845d209f..220d5300b 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -432,3 +432,6 @@ ## STEP-055 生命周期错峰小修复 粒子基础生命周期调整为 14400 simulation seconds(12 real seconds),首次 seed 与 maxAge 都由稳定 hash 错峰,消除同步生命周期到期。日志改用 configuredSlots 并说明无 GPU readback;其他粒子视觉与数据链语义未改。 +## STEP-055 低缩放可见性与 Trail 稳定性修复 + +Particle visual clock 改为基于 zoom 的有界 scale 与 2400s/frame clamp;State lifecycle 改为 12s base 的 wall-time hash jitter。普通 camera 交互只 invalidate 一次并在稳定后恢复 Trail,只有 resize 重分配 Trail texture。canonical 与 Core 时间线边界未改,待 Android 验收。