From 1343929474796b1313489ee9fecbf23b949580a7 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 4 Sep 2026 08:17:29 +0800 Subject: [PATCH] fix: keep particle timeline playback ticking --- .../STEP-047_Runtime_Timeline与Playback_v3.md | 15 +++++++++++ ...ntime_Timeline与Playback_Codex完成报告_v3.md | 7 +++++ .../style/sources/raster_array_source.hpp | 1 + .../render_particle_validation_layer.cpp | 26 +++++++++++++++++++ .../style/sources/raster_array_source.cpp | 5 ++++ 粒子开发记录.md | 5 ++++ 6 files changed, 59 insertions(+) create mode 100644 docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v3.md create mode 100644 docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v3.md diff --git a/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v3.md b/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v3.md new file mode 100644 index 000000000..6744a06aa --- /dev/null +++ b/docs/50_Step记录/STEP-047_Runtime_Timeline与Playback_v3.md @@ -0,0 +1,15 @@ +# STEP-047 Playback Tick 根因修复 + +状态:IMPLEMENTED / WAITING FOR ANDROID RE-VALIDATION + +## Android Validation Round 2:PARTIAL / NEEDS FIX + +PLAY 和 speed JNI command 均返回 SUCCESS;短暂的 `TIME_MIX_UPDATE` 后 mix 停止,但 State/Trail/Composite 与 RenderThread 持续运行。根因是 `RasterArrayTimelineRuntime::tick()` 只在 `RenderRasterParticleLayer::update()` 中调用;该函数只在 render tree/update 生命周期进入,不是每个已构建 drawable frame。 + +## 修复 + +将 Source-owned timeline tick 接入既有 `ParticleGpuValidationTweaker::execute()` 的 State pass。State drawable 每个正式粒子 simulation frame 执行一次;Trail、Composite 与 world copy 不 tick。因此不需要 Android Timer、每帧 JNI、新线程或循环。Tweaker 将本帧 Core mix 写回现有 resources UBO;现有 layer update 仍负责资源/窗口变更。 + +新增约每秒一次 `PLAYBACK_TICK` heartbeat,含 state、speed、cursor、mix、window、fieldUploadCount、simulationResetCount。 + +Round 3 P1--P4 待用户 Android 验证。未执行 Android Gradle、ADB 或 Jest。 diff --git a/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v3.md b/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v3.md new file mode 100644 index 000000000..bb942846f --- /dev/null +++ b/docs/报告/40-06_Runtime_Timeline与Playback_Codex完成报告_v3.md @@ -0,0 +1,7 @@ +# 40-06 / STEP-047 Playback Tick 根因修复报告 + +状态:IMPLEMENTED / WAITING FOR ANDROID RE-VALIDATION + +旧 tick 位于 `RenderRasterParticleLayer::update()`,其不是连续 drawable-frame hook,故 PLAY 后只有短暂 cursor/mix 更新。新的 tick 位于 State pass `ParticleGpuValidationTweaker::execute()`;粒子已有 continuous repaint/render 就是其驱动来源。没有 Android timer、每帧 JNI、线程或 while-playing loop。 + +PLAY/PAUSE/RESUME/speed 的 monotonic anchor 继续由 `RasterArrayTimelineRuntime` 持有;每帧仅更新 mix,不上传 Field、不 reload Style、不 reset simulation 或清 Trail。窗口边界仍由既有 layer update/reuse 路径处理。 diff --git a/include/mbgl/style/sources/raster_array_source.hpp b/include/mbgl/style/sources/raster_array_source.hpp index d9f6c041e..f1af9589e 100644 --- a/include/mbgl/style/sources/raster_array_source.hpp +++ b/include/mbgl/style/sources/raster_array_source.hpp @@ -64,6 +64,7 @@ public: bool injectInvalid(); bool recover(); RasterArrayTimelineFrame frame() const; + float playbackSpeed() const; size_t cacheCapacity() const { return cacheCapacity_; } private: diff --git a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp index ec0a3147e..761ef8017 100644 --- a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp +++ b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -198,6 +199,7 @@ public: bool worldWrapLogged = false; bool antimeridianLogged = false; uint64_t lastCameraLogFrame = 0; + std::chrono::steady_clock::time_point lastPlaybackHeartbeat{}; uint64_t maskGeneration = 0; uint64_t fieldGeneration = 0; uint64_t gpuFieldGeneration = 0; @@ -228,6 +230,7 @@ public: gfx::Texture2DPtr stateTexture; particle::ParticleFieldSnapshot field; particle::ParticleFieldSnapshot nextField; + std::shared_ptr timeline; std::shared_ptr fieldImage; std::shared_ptr nextFieldImage; std::shared_ptr maskFixture; @@ -351,6 +354,26 @@ public: return; } + // update() is not called for every drawable frame. State executes once + // per particle simulation frame, making it the appropriate existing + // Core render lifecycle for the Source-owned monotonic playback clock. + if (pass == ValidationPass::State && resources->timeline) { + const auto frame = resources->timeline->tick(); + resources->timeMix = frame.mix; + const auto now = std::chrono::steady_clock::now(); + if (now - resources->lastPlaybackHeartbeat >= std::chrono::seconds(1)) { + resources->lastPlaybackHeartbeat = now; + Log::Info( + Event::Render, + "ParticleTimeline: event=PLAYBACK_TICK state=" + std::to_string(static_cast(frame.state)) + + " speed=" + std::to_string(resources->timeline->playbackSpeed()) + + " cursor=" + std::to_string(frame.t0Index) + " mix=" + std::to_string(frame.mix) + + " window=" + std::to_string(frame.t0Index) + "/" + std::to_string(frame.t1Index) + + " fieldUploadCount=" + std::to_string(resources->fieldUploadCount) + + " simulationResetCount=" + std::to_string(resources->resetCount)); + } + } + drawable.setTexture(source, shaders::idParticleGpuValidationPreviousTexture); if (mask) { drawable.setTexture(mask, shaders::idParticleGpuValidationMaskTexture); @@ -1113,6 +1136,9 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, return; } } + // Keep the shared Core runtime alive in the per-frame drawable tweaker. + // This is a pointer to Source-owned state, not a second timeline. + resources->timeline = sourceTimeline; const auto requestedTimeMix = std::clamp(timelineMix, 0.0f, 1.0f); if (resources->timeMix != requestedTimeMix) { resources->timeMix = requestedTimeMix; diff --git a/src/mbgl/style/sources/raster_array_source.cpp b/src/mbgl/style/sources/raster_array_source.cpp index 42d915189..537b8fe48 100644 --- a/src/mbgl/style/sources/raster_array_source.cpp +++ b/src/mbgl/style/sources/raster_array_source.cpp @@ -46,6 +46,11 @@ RasterArrayTimelineFrame RasterArrayTimelineRuntime::frame() const { return frameLocked(); } +float RasterArrayTimelineRuntime::playbackSpeed() const { + std::lock_guard lock(mutex); + return speed; +} + bool RasterArrayTimelineRuntime::moveToLocked(const size_t nextT0) { if (nextT0 + 1 >= entries.size()) return false; t0 = nextT0; diff --git a/粒子开发记录.md b/粒子开发记录.md index 3888bacb6..550c96d02 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -337,3 +337,8 @@ - Round 1 为 `PARTIAL / NEEDS FIX`:主链、自动 MIX/window reuse 和紧凑 UI 通过,但 timeline 过短、END/seek 语义、异常入口、VIS 可达性与 cache 可观测性不足。 - Core validation timeline 改为约 200 秒;END PLAY restart、PLAYING/PAUSED seek 状态保持、GAP/INVALID fail-closed 和 RECOVER 保留此前状态。TIME 直接暴露 NORMAL/GAP/INVALID/RECOVER,VIS 将 COLOR 前置并使用明显的 validation 步长。 - 仍保持 Source 有界 CPU snapshot preload、GPU 仅 T0/T1、相邻窗口 reuse 与 STEP-046 visual property-only。状态为 `IMPLEMENTED / WAITING FOR ANDROID RE-VALIDATION`。 + +## 2026-09-04 — 40-06 / STEP-047 Playback Tick 根因修复 + +- Android Round 2 确认 PLAY/SPEED bridge 成功、RenderThread 持续,但 Timeline 只短暂推进。根因是 tick 错接到非持续的 RenderLayer update。 +- tick 已移到每个正式 State drawable frame 的 Core tweaker;没有 Android timer、每帧 JNI、线程或自旋。新增低频 PLAYBACK_TICK heartbeat;状态继续 `IMPLEMENTED / WAITING FOR ANDROID RE-VALIDATION`。