diff --git a/docs/30_设计/STEP-085_V4_Renderer性能修复设计_v1.md b/docs/30_设计/STEP-085_V4_Renderer性能修复设计_v1.md new file mode 100644 index 000000000..a0e15a7fe --- /dev/null +++ b/docs/30_设计/STEP-085_V4_Renderer性能修复设计_v1.md @@ -0,0 +1,45 @@ +# STEP-085 V4 Renderer 性能修复设计 v1 + +状态:`IMPLEMENTED / WAITING_FOR_RDC_NATIVE_BUILD` + +## 目标与根因 + +V4 的 `RasterArraySource` 通过独立的 field-keyed window 产出已经 materialize 的 T0/T1/MIX。旧 renderer 将 V4 误当作“没有 timeline”,从而产生两项错误:WeatherColor 的 `needsRepaint()` 永远为 true;Particle 也永远请求下一帧,并在 `prepare()` 将包含大 vector/scalar/valid 数组的 snapshot 按值复制。 + +本设计保持 V3 timeline、V4 Runtime Field Window、QBIN 和所有 RN/Host 公共接口不变,只在 Source 内部和共用 renderer seam 修复 cadence/lifetime。 + +## 共用 cadence 语义 + +新增 Source-owned `RasterArrayRendererCadence`。它只负责 visual simulation 的 deadline,不携带 field、不会变更 T0/T1/MIX,也不会调用 ForecastClock。 + +| targetFps | Core cadence | platform wake | +| --- | --- | --- | +| AUTO | 每个 MapLibre animation frame 可消费一次 | 沿用平台 animation-frame 语义 | +| 30 | 约 33.3ms 才允许一次 simulation | Android Handler / iOS NSTimer 约 34ms 请求一次 repaint | +| 60 | 约 16.7ms 才允许一次 simulation | Android Handler / iOS NSTimer 约 17ms 请求一次 repaint | + +`RenderRasterParticleLayer::needsRepaint()` 仅对 active、visible、field-ready 的 V4 Particle 在 deadline 到期时返回 true。因 camera 而来的额外 frame 仍可 composite 当前 trail,但 `consumeIfDue()` 未成功时不建 State/Trail pass、不推进 ping-pong、不衰减 trail。Forecast PAUSE 不影响该 visual cadence。 + +Android/iOS runtime timer 仅是已有 facade 的“下一帧 wake”薄层;deadline 与是否推进由共用 C++ Source cadence 判定。hidden、clear、disable 和 dispose 都停止 V4 Particle wake。 + +## WeatherColor idle 与 drawable + +V4 WeatherColor 在 PAUSE、field/window/stops/opacity/transform 不变时 `needsRepaint()==false`。它不再以 `sourceTimeline == nullptr` 作为“永远动画”的代理。 + +稳定状态下 WeatherColor drawable 复用既有 LayerGroup drawable。T0/T1 texture、mix、opacity、color stops 与 value source 由 Resources/Tweaker 在 render 时更新 UBO;仅 wrap 集合变化才 `clearDrawables()` 后重建。该方案保留 gfx abstraction,未引入 OpenGL/Metal 直接调用。 + +## V4 snapshot lifetime + +materialize 仍发生在 Source 的 binding/window 真正变化时,但 `Materialized*Projection` 与 renderer `sourceField/sourceNextField` 全部改为 `shared_ptr`。Source Impl 的不可变副本持有 projection,RenderLayer 只复制 shared handle;稳定 window 的 `prepare()` 不再深复制 scalar/vector/valid arrays。旧 V3 optional snapshot 路径仍在 Source 变化时构造独立 handle,未改变其 API。 + +## Debug/Validation observability + +`include/mbgl/particle/renderer_debug_counters.hpp` 在非 release 建立低成本 relaxed atomic 累计器,release 编译为 no-op。覆盖:两 layer 的 needsRepaint/update、particle simulation advance、WeatherColor/Particle drawable rebuild、field texture upload、GPU resource create/release-reset、trail create/resize、V4 projection materialize 与 field-window revision。它不逐帧写普通日志,也不暴露给 NavSea。 + +## ForecastClock 审查 + +全仓确认 `RasterArraySource::advanceForecastClock(elapsedMs)` 只有定义,当前 Android WeatherRuntime、iOS `MLNParticleFieldRuntime` 与 RN bridge 均没有 production caller。故 V4 PLAY 自动推进尚无 owner;本 Step 不将 cadence timer 偷接成第二个 forecast clock。Host 显式 `setEncodedForecastTime` 继续触发 window refresh;自动 forecast playback 应单独设计 owner 后处理。 + +## 生命周期边界 + +本 Step 不改变 `detach()`、dispose、Host clear/destroy contract,也不对 retained allocation/Native Heap 归因。计数器和减少 repaint/copy/rebuild 仅为后续 RDC 采样提供证据。 diff --git a/docs/50_Step记录/STEP-085_V4_Renderer性能修复_v1.md b/docs/50_Step记录/STEP-085_V4_Renderer性能修复_v1.md new file mode 100644 index 000000000..85a9c272f --- /dev/null +++ b/docs/50_Step记录/STEP-085_V4_Renderer性能修复_v1.md @@ -0,0 +1,35 @@ +# STEP-085 V4 Renderer 性能修复 v1 + +状态:`完成(BUILD_NOT_RUN_BY_CODEX — WAITING_FOR_RDC)` + +## 基线 + +- 机器:Mac-mini-tei.lan。 +- worktree:`/Users/tei/dev/MapLibreParticleLab-step085`。 +- branch:`step/085-v4-render-efficiency`。 +- STEP-085 Start HEAD:`bc6b8dd84b4c10e5263d61832661508c02d84d88`。 +- STEP-084 Final HEAD:`bc6b8dd84b4c10e5263d61832661508c02d84d88`,已进入 `origin/particle/main`。 + +## 本轮结果 + +- [x] V4 WeatherColor PAUSE/静态 window 不再因缺少 legacy timeline 永久 `needsRepaint()`。 +- [x] V4 Particle 使用 Source-owned backend-neutral cadence;30/60 gate simulation,AUTO 保持 animation-frame 语义。 +- [x] Forecast PAUSE 与 Particle visual simulation 解耦;camera redraw 未到 deadline 时只 composite。 +- [x] V4 Renderer prepare 改持 shared immutable snapshot handle,稳定 window 不深复制大数组。 +- [x] WeatherColor 稳定 wrap/resource 状态复用 drawable,mix/opacity/stops 由 tweaker UBO 更新。 +- [x] 加入 DEBUG/Validation cumulative counters。 +- [x] Android/iOS existing Runtime thin seam 为 V4 30/60 提供 deadline wake;无新 RN API 或平台专用 Core。 +- [x] 自审将 platform wake 收紧为“已绑定的 fieldKey 已收到 accepted V4 submit”;clear 非粒子 field 不会错误停止仍可运行的粒子,disable 后 re-enable 可恢复既有 V4 field 的 wake。 +- [x] 审查 `advanceForecastClock` production owner;当前未发现,已明确记录而未扩大重构。 +- [x] `git diff --check` 通过。 +- [ ] Native C++/Metal/Android 编译、链接、codegen、artifact、安装、设备和性能采样:`BUILD_NOT_RUN_BY_CODEX — WAITING_FOR_RDC`。 + +## 禁止范围核对 + +未修改 NavSea、provider/network、QBIN V4、FieldProfile/Binding 对外 contract、RN 公共 API、C-001、`detach()` 语义或项目总则。未新增第二套 runtime/timeline,也未引入平台专用 Particle Core。 + +## 已知风险与下一步 + +V4 ForecastClock PLAY 的自动生产推进仍无调用 owner,因此未声称 PLAY 自动推进已经修复。RDC 必须先按完成报告进行 Mac C++/Metal、Android Native/AAR、shader/build graph 编译;成功后用统一颜色+性能真机矩阵验证 30/60/AUTO、PAUSE、speed-color、camera 和 debug counters。 + +本 Step 只完成源码和静态自审;Android Java/Objective-C++ wake 状态调整尚未经过目标平台编译。 diff --git a/docs/报告/STEP-085_V4_Renderer性能修复_Codex完成报告_v1.md b/docs/报告/STEP-085_V4_Renderer性能修复_Codex完成报告_v1.md new file mode 100644 index 000000000..d268f8030 --- /dev/null +++ b/docs/报告/STEP-085_V4_Renderer性能修复_Codex完成报告_v1.md @@ -0,0 +1,52 @@ +# STEP-085 V4 Renderer 性能修复 Codex 完成报告 v1 + +## 1. 执行结论 + +`IMPLEMENTED / STATIC_CHECKS_PASS / BUILD_NOT_RUN_BY_CODEX — WAITING_FOR_RDC` + +Codex model:gpt-5.6-terra。推理强度:high。机器:Mac-mini-tei.lan。仓库:`/Users/tei/dev/MapLibreParticleLab-step085`。 + +## 2. Git 基线 + +- branch:`step/085-v4-render-efficiency` +- STEP-084 Final / STEP-085 Start HEAD:`bc6b8dd84b4c10e5263d61832661508c02d84d88` +- End HEAD、commit、push 与 remote HEAD:本报告随最终 scoped commit 回填,以最终 Git 事实为准。 +- STEP-084 已由 `fix: disable particle speed color by default` 推入 `origin/particle/main`,本 Step 从该更新后的 remote HEAD 创建独立 worktree。 + +## 3. 实际实现 + +1. `RasterArrayRendererCadence` 是 Source-owned shared internal state。它以 monotonic deadline gate V4 Particle simulation,不推进 forecast。Particle active/visible/field-ready 时才请求 repaint;未到 deadline 的 camera frame 只重合成已有 trail。 +2. WeatherColor 不再把 `sourceTimeline == nullptr` 解释为动画。V4 静态 PAUSE 返回 false;legacy playback 仍沿用 timeline cadence。 +3. V4 materialized projection 与 RenderLayer source endpoint 改为 immutable shared pointer。materialization 只在 binding/window refresh 时产生,prepare 不复制完整 `scalar`、`vector`、`valid`。 +4. WeatherColor 只在 wrap 集合变化时重建 drawable;texture/UBO 参数由 persistent Resource/Tweaker 读取,正常 MIX、opacity、stops 和 field endpoint 更新不重建 LayerGroup drawable。 +5. Android `WeatherRuntime` 和 iOS `MLNParticleFieldRuntime` 仅补下一帧 wake:已绑定 particle fieldKey 且该 key 已收到 accepted V4 submit、可见且 target 为 30/60 时才请求 repaint;hidden/clear/disable/dispose 停止,disable 后 re-enable 可恢复既有 V4 field 的 wake。公共 RN/ObjC/Java API 未新增。 +6. 新 debug counters 为 non-spam atomic 累计值;release no-op。覆盖 repaint/update/simulation/drawable/texture/GPU/trail/materialize/window revision。 + +## 4. 根因与语义 + +根因是 V4 具有独立 projection,却被 renderer 的 `sourceTimeline` sentinel 当作“持续动画”;同时 prepare 直接给 value snapshot 赋值。修复后 Forecast PAUSE 只暂停自动 forecast time,Particle visual simulation 仍在 active cadence 下运行。V4 PLAY 自动推进没有 production owner:`advanceForecastClock` 只定义未被生产 Runtime/bridge 调用,本 Step 明确保留为后续设计项。 + +## 5. 静态检查与测试 + +- 已执行:`git diff --check`,通过。 +- 已执行:对 V4 projection/cadence/repaint/materialization/drawable code path 的 `rg` 定向静态审计,确认 no-timeline WeatherColor 不再 permanent repaint,V4 Particle 调用 `consumeIfDue()`。 +- 已执行:平台 wake 静态审计;wake 只使用 Host command bookkeeping,Core `RasterArrayRendererCadence` 仍是 deadline 和 simulation advance 的唯一裁决者。 +- 新增:`RasterArrayV4RendererCadenceGatesVisualSimulationWithoutForecastPlayback` C++ test;V4 materializer tests 调整为 shared snapshot lifetime 断言。 +- 未运行测试二进制:任务书不允许以未编译的 native test 冒充验证,且 Native 编译/链接由 RDC 负责。 + +## 6. 未执行的编译与运行 + +未执行 Bazel、CMake/Ninja/Make、clang/clang++ Native compile、Xcode、Metal/iOS build、Gradle、AAR/APK、artifact、ADB、设备安装、真机或 CPU/GPU/heap 采样。状态为 `BUILD_NOT_RUN_BY_CODEX — WAITING_FOR_RDC`,不代表 Android/iOS/Metal/AAR/性能 PASS。 + +## 7. RDC 后续入口 + +1. 在本 Final HEAD 上执行受影响 C++ renderer/source/test 精确编译和相关 test filter;确认 shader/codegen/build graph。 +2. 执行 `bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic`,并使用当前 iOS Test Host 的 Metal XCFramework 消费链完成 device build。 +3. 按 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md` 构建新的 Android OpenGL AAR、发布新版本坐标、从公网 Maven/npm 消费并构建 Test Host;不得覆盖现有版本。 +4. 统一执行任务书 22/23:A/B/C/D、30/60/AUTO、PAUSE、speed-color OFF/ON/OFF,以及 pan/zoom/bearing/pitch/resize;采集 RenderThread/GPU/jank 与 debug counters。确认静态 WeatherColor idle、camera frame 不误推进、field upload/GPU/trail/drawable 不异常增长。 + +## 8. 已知问题、未完成项 + +- V4 ForecastClock PLAY 自动推进的 production owner 缺失,未在本 Step 引入第二时钟。 +- Native Heap/detach/destroy 未归因或修改。 +- 所有 Native 编译、artifact 和设备验收待 RDC;本次 Android Java / iOS Objective-C++ wake 收紧也待目标平台编译。如发现跨 backend 架构问题,应停止 RDC 扩改并重新进入 Codex 任务。 diff --git a/include/mbgl/particle/field/field_projection_materialization.hpp b/include/mbgl/particle/field/field_projection_materialization.hpp index 53fe3a538..341098f29 100644 --- a/include/mbgl/particle/field/field_projection_materialization.hpp +++ b/include/mbgl/particle/field/field_projection_materialization.hpp @@ -4,21 +4,22 @@ #include #include +#include namespace mbgl::particle { // Renderer-neutral canonical snapshots. QBIN stays confined to ingress and // field Core; renderers receive only the existing canonical field shape. struct MaterializedColorProjection { - ParticleFieldSnapshot t0; - ParticleFieldSnapshot t1; + std::shared_ptr t0; + std::shared_ptr t1; ColorProjectionContentIdentity contentIdentity; FieldSampleState sampleState; }; struct MaterializedParticleProjection { - ParticleFieldSnapshot t0; - ParticleFieldSnapshot t1; + std::shared_ptr t0; + std::shared_ptr t1; ParticleProjectionContentIdentity contentIdentity; FieldSampleState sampleState; }; diff --git a/include/mbgl/particle/renderer_debug_counters.hpp b/include/mbgl/particle/renderer_debug_counters.hpp new file mode 100644 index 000000000..a22d508f8 --- /dev/null +++ b/include/mbgl/particle/renderer_debug_counters.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include + +namespace mbgl::particle { + +// Debug/Validation-only cumulative renderer observability. These atomics are +// intentionally not connected to Host APIs and compile to no-ops in release. +enum class RendererDebugCounter : uint8_t { + WeatherColorNeedsRepaint, + ParticleNeedsRepaint, + WeatherColorUpdate, + ParticleUpdate, + ParticleSimulationAdvance, + WeatherColorDrawableRebuild, + ParticleDrawableRebuild, + FieldTextureUpload, + GpuResourceCreate, + GpuResourceRelease, + TrailTargetCreateOrResize, + V4ProjectionMaterialize, + FieldWindowRevision, + Count, +}; + +#ifndef NDEBUG +struct RendererDebugCounterSnapshot { + uint64_t weatherColorNeedsRepaint = 0; + uint64_t particleNeedsRepaint = 0; + uint64_t weatherColorUpdate = 0; + uint64_t particleUpdate = 0; + uint64_t particleSimulationAdvance = 0; + uint64_t weatherColorDrawableRebuild = 0; + uint64_t particleDrawableRebuild = 0; + uint64_t fieldTextureUpload = 0; + uint64_t gpuResourceCreate = 0; + uint64_t gpuResourceRelease = 0; + uint64_t trailTargetCreateOrResize = 0; + uint64_t v4ProjectionMaterialize = 0; + uint64_t fieldWindowRevision = 0; +}; + +inline std::atomic_uint64_t* rendererDebugCounterStorage() { + static std::atomic_uint64_t counters[static_cast(RendererDebugCounter::Count)]{}; + return counters; +} + +inline void incrementRendererDebugCounter(const RendererDebugCounter counter) { + rendererDebugCounterStorage()[static_cast(counter)].fetch_add(1, std::memory_order_relaxed); +} + +inline RendererDebugCounterSnapshot rendererDebugCounterSnapshot() { + const auto* values = rendererDebugCounterStorage(); + return {values[0].load(std::memory_order_relaxed), values[1].load(std::memory_order_relaxed), + values[2].load(std::memory_order_relaxed), values[3].load(std::memory_order_relaxed), + values[4].load(std::memory_order_relaxed), values[5].load(std::memory_order_relaxed), + values[6].load(std::memory_order_relaxed), values[7].load(std::memory_order_relaxed), + values[8].load(std::memory_order_relaxed), values[9].load(std::memory_order_relaxed), + values[10].load(std::memory_order_relaxed), values[11].load(std::memory_order_relaxed), + values[12].load(std::memory_order_relaxed)}; +} +#else +inline void incrementRendererDebugCounter(RendererDebugCounter) {} +#endif + +} // namespace mbgl::particle diff --git a/include/mbgl/style/sources/raster_array_source.hpp b/include/mbgl/style/sources/raster_array_source.hpp index 9b2b38ca5..3009ee34f 100644 --- a/include/mbgl/style/sources/raster_array_source.hpp +++ b/include/mbgl/style/sources/raster_array_source.hpp @@ -65,6 +65,24 @@ struct RasterArrayCadenceAdvance { bool advanced = false; }; +// A backend-neutral cadence gate for renderer-owned visual simulation. Unlike +// the legacy timeline it has no field entries and never advances forecast +// time; Android/iOS may merely request a future MapLibre frame at its deadline. +class RasterArrayRendererCadence final { +public: + bool consumeIfDue(std::chrono::steady_clock::time_point); + bool cadenceDue(std::chrono::steady_clock::time_point) const; + void setTargetFps(RasterArrayTargetFps); + RasterArrayTargetFps targetFps() const; + uint64_t revision() const; + +private: + mutable std::mutex mutex; + RasterArrayTargetFps targetFps_ = RasterArrayTargetFps::Auto; + std::chrono::steady_clock::time_point lastTick{}; + uint64_t revision_ = 0; +}; + // Source-owned playback state. Android only sends low-frequency commands; // render frames obtain an immutable T0/T1 pair and scalar mix from Core. class RasterArrayTimelineRuntime { diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java index 57dd325ed..4b204866e 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java @@ -7,6 +7,8 @@ import android.os.Handler; import android.os.Looper; import java.util.List; +import java.util.HashSet; +import java.util.Set; import org.json.JSONException; import org.json.JSONObject; @@ -59,6 +61,12 @@ public final class WeatherRuntime { private boolean particleVisible = true; private PlaybackState playbackState = PlaybackState.PAUSE; private TargetFps targetFps = TargetFps.AUTO; + // V4 owns its window in Core, so it never transitions the legacy V3 state + // to READY. This only owns Android's next-frame wake, not a second timeline. + // This is a Host-command bookkeeping hint for scheduling only. Core still + // decides whether a resolved V4 window may actually advance simulation. + @Nullable private String v4ParticleBindingFieldKey; + private final Set submittedEncodedFieldKeys = new HashSet<>(); private boolean disposed; private Handler cadenceHandler; private final Runnable cadenceWake = new Runnable() { @@ -91,7 +99,9 @@ public final class WeatherRuntime { if (!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) || !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldId) || !WeatherRuntimeV4Guards.hasValidEncodedContractUtf8Length(contractJson) || !hasValidEncodedContractProfileId(contractJson) || generation <= 0 || dataVersion < 0 || !WeatherRuntimeV4Guards.hasValidEncodedQbin(qbin)) throw new IllegalArgumentException("invalid encoded field argument"); int code = map.submitRasterArraySourceEncodedField(config.sourceId, fieldKey, fieldId, generation, dataVersion, contractJson, qbin); if (code == 0) throw new IllegalStateException("SOURCE_MISSING"); - return EncodedFieldSubmitResult.fromCode(code); + EncodedFieldSubmitResult result = EncodedFieldSubmitResult.fromCode(code); + if (result.code > 0) { submittedEncodedFieldKeys.add(fieldKey); cancelCadenceWake(); scheduleCadenceWake(); } + return result; } private static boolean hasValidEncodedContractProfileId(String contractJson) { if (contractJson == null) return false; @@ -101,15 +111,15 @@ public final class WeatherRuntime { return false; } } - public CommandResult clearEncodedField(@NonNull String fieldKey) { requireAttached(); return !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) ? CommandResult.REJECTED : encodedCommand(map.clearRasterArraySourceEncodedField(config.sourceId, fieldKey)); } - public CommandResult clearAllEncodedFields() { requireAttached(); return encodedCommand(map.clearAllRasterArraySourceEncodedFields(config.sourceId)); } + public CommandResult clearEncodedField(@NonNull String fieldKey) { requireAttached(); if (!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey)) return CommandResult.REJECTED; CommandResult result = encodedCommand(map.clearRasterArraySourceEncodedField(config.sourceId, fieldKey)); if (result == CommandResult.ACCEPTED) { submittedEncodedFieldKeys.remove(fieldKey); cancelCadenceWake(); scheduleCadenceWake(); } return result; } + public CommandResult clearAllEncodedFields() { requireAttached(); CommandResult result = encodedCommand(map.clearAllRasterArraySourceEncodedFields(config.sourceId)); if (result == CommandResult.ACCEPTED) { submittedEncodedFieldKeys.clear(); cancelCadenceWake(); scheduleCadenceWake(); } return result; } /** V4 binding intent; unit and all mode selectors reach Core unchanged. */ public CommandResult setEncodedColorBinding(@NonNull String fieldKey, int semanticID, int groupID, int valueClass, int expectedUnitID) { requireAttached(); return !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedColorBinding(config.sourceId, fieldKey, semanticID, groupID, valueClass, expectedUnitID)); } /** ABI reserves all selector slots: inactive slots must be exactly (0, 0). */ public CommandResult setEncodedParticleBinding(@NonNull String fieldKey, int mode, int uSemanticID, int uGroupID, int vSemanticID, int vGroupID, int directionSemanticID, int directionGroupID, int expectedUnitID) { - requireAttached(); return !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedParticleBinding(config.sourceId, fieldKey, mode, uSemanticID, uGroupID, vSemanticID, vGroupID, directionSemanticID, directionGroupID, expectedUnitID)); + requireAttached(); if (!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey)) return CommandResult.REJECTED; CommandResult result = encodedCommand(map.setRasterArraySourceEncodedParticleBinding(config.sourceId, fieldKey, mode, uSemanticID, uGroupID, vSemanticID, vGroupID, directionSemanticID, directionGroupID, expectedUnitID)); if (result == CommandResult.ACCEPTED) { v4ParticleBindingFieldKey = fieldKey; cancelCadenceWake(); scheduleCadenceWake(); } return result; } public CommandResult setEncodedForecastTime(long timestamp) { requireAttached(); return timestamp < 0 ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedForecastTime(config.sourceId, timestamp)); } /** State and speed intentionally remain independent V4 Core commands. */ @@ -153,6 +163,8 @@ public final class WeatherRuntime { boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, weatherColorVisible); boolean particle = map.setStyleLayerVisibility(config.particleLayerId, particleVisible); state = stateAfterEnable(state, color && particle); + cancelCadenceWake(); + scheduleCadenceWake(); return result(color && particle); } public CommandResult setWeatherColorVisible(boolean visible) { if (disposed) return CommandResult.REJECTED; weatherColorVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.weatherColorLayerId, visible && state != State.DISABLED)); cancelCadenceWake(); scheduleCadenceWake(); return result; } @@ -211,7 +223,10 @@ public final class WeatherRuntime { private static CommandResult encodedCommand(int code) { if (code == 0) throw new IllegalStateException("SOURCE_MISSING"); return code > 0 ? CommandResult.ACCEPTED : CommandResult.REJECTED; } public enum FieldValueSource { SCALAR(0f), VECTOR_MAGNITUDE(1f); final float code; FieldValueSource(float code) { this.code = code; } } private boolean shouldWakeWeather() { - return !disposed && targetFps != TargetFps.AUTO && state == State.READY && (particleVisible || (weatherColorVisible && playbackState == PlaybackState.PLAY)); + if (disposed || state == State.DISABLED || targetFps == TargetFps.AUTO) return false; + final boolean v4ParticleFieldReady = v4ParticleBindingFieldKey != null && submittedEncodedFieldKeys.contains(v4ParticleBindingFieldKey); + return (v4ParticleFieldReady && particleVisible) || + (state == State.READY && (particleVisible || (weatherColorVisible && playbackState == PlaybackState.PLAY))); } private void scheduleCadenceWake() { if (!shouldWakeWeather()) return; diff --git a/platform/ios/src/MLNParticleFieldRuntime.mm b/platform/ios/src/MLNParticleFieldRuntime.mm index 82f5a016b..2d7756d18 100644 --- a/platform/ios/src/MLNParticleFieldRuntime.mm +++ b/platform/ios/src/MLNParticleFieldRuntime.mm @@ -101,7 +101,13 @@ std::vector charsFromData(NSData *data) { MLNParticleFieldRuntimeTargetFps _targetFps; BOOL _weatherColorVisible; BOOL _particleVisible; + NSString *_v4ParticleBindingFieldKey; + NSMutableSet *_submittedEncodedFieldKeys; + NSTimer *_v4CadenceTimer; } +- (BOOL)isV4ParticleCadenceReady; +- (void)refreshV4CadenceWake; +- (void)runV4CadenceWake:(NSTimer *)timer; - (instancetype)initWithMapView:(MLNMapView *)mapView configuration:(MLNParticleFieldRuntimeConfiguration *)configuration error:(NSError **)error; @@ -120,6 +126,7 @@ std::vector charsFromData(NSData *data) { _targetFps = MLNParticleFieldRuntimeTargetFpsAuto; _weatherColorVisible = YES; _particleVisible = YES; + _submittedEncodedFieldKeys = [NSMutableSet set]; if (![self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { return true; }]) return nil; } return self; @@ -131,10 +138,38 @@ std::vector charsFromData(NSData *data) { - (MLNParticleFieldRuntimeTargetFps)targetFps { return _targetFps; } - (void)dispose { + [_v4CadenceTimer invalidate]; + _v4CadenceTimer = nil; _disposed = YES; _mapView = nil; } +- (void)refreshV4CadenceWake { + [_v4CadenceTimer invalidate]; + _v4CadenceTimer = nil; + if (_disposed || ![self isV4ParticleCadenceReady] || !_particleVisible || + _targetFps == MLNParticleFieldRuntimeTargetFpsAuto) return; + const NSTimeInterval interval = _targetFps == MLNParticleFieldRuntimeTargetFps30 ? 0.034 : 0.017; + _v4CadenceTimer = [NSTimer scheduledTimerWithTimeInterval:interval + target:self + selector:@selector(runV4CadenceWake:) + userInfo:nil + repeats:NO]; +} + +- (void)runV4CadenceWake:(NSTimer *)timer { + if (timer != _v4CadenceTimer) return; + _v4CadenceTimer = nil; + if (_disposed || ![self isV4ParticleCadenceReady] || !_particleVisible) return; + [_mapView triggerRepaint]; + [self refreshV4CadenceWake]; +} + +- (BOOL)isV4ParticleCadenceReady { + return _state != MLNParticleFieldRuntimeStateDisabled && _state != MLNParticleFieldRuntimeStateError && _v4ParticleBindingFieldKey && + [_submittedEncodedFieldKeys containsObject:_v4ParticleBindingFieldKey]; +} + - (BOOL)withCurrentSource:(NSError **)error action:(bool (^)(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &))action { if (_disposed) { setError(error, MLNParticleFieldRuntimeErrorDisposed, @"Particle field runtime has been disposed."); @@ -213,6 +248,10 @@ std::vector charsFromData(NSData *data) { result = static_cast(coreResult); return true; }]; + if (completed && result > MLNParticleEncodedFieldSubmitResultNone) { + [_submittedEncodedFieldKeys addObject:fieldKey]; + [self refreshV4CadenceWake]; + } return completed ? result : MLNParticleEncodedFieldSubmitResultNone; } @@ -220,10 +259,11 @@ std::vector charsFromData(NSData *data) { if (!hasValidIdentifier(fieldKey) || semanticID == 0 || expectedValueClass < MLNParticleFieldValueClassScalar || expectedValueClass > MLNParticleFieldValueClassDirectionAngle) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid encoded color binding."); return NO; } - return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { + const BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { source.setColorBinding({stringFromNSString(fieldKey), {semanticID, groupID}, static_cast(expectedValueClass), expectedUnitID}); return true; }]; + return completed; } - (BOOL)setEncodedParticleBindingWithFieldKey:(NSString *)fieldKey mode:(MLNParticleEncodedParticleBindingMode)mode uSemanticID:(uint16_t)uSemanticID uGroupID:(uint16_t)uGroupID vSemanticID:(uint16_t)vSemanticID vGroupID:(uint16_t)vGroupID directionSemanticID:(uint16_t)directionSemanticID directionGroupID:(uint16_t)directionGroupID expectedUnitID:(uint16_t)expectedUnitID error:(NSError **)error { @@ -231,7 +271,7 @@ std::vector charsFromData(NSData *data) { const bool vector = mode == MLNParticleEncodedParticleBindingModeVectorComponents && uSemanticID != 0 && vSemanticID != 0 && directionSemanticID == 0 && directionGroupID == 0; const bool direction = mode == MLNParticleEncodedParticleBindingModeDirectionAngle && uSemanticID == 0 && uGroupID == 0 && vSemanticID == 0 && vGroupID == 0 && directionSemanticID != 0; if (!vector && !direction) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Inactive particle selectors must be exactly semanticID = 0 and groupID = 0."); return NO; } - return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { + const BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { mbgl::particle::ParticleBinding binding; binding.fieldKey = stringFromNSString(fieldKey); binding.expectedUnitID = expectedUnitID; @@ -240,6 +280,8 @@ std::vector charsFromData(NSData *data) { source.setParticleBinding(std::move(binding)); return true; }]; + if (completed) { _v4ParticleBindingFieldKey = [fieldKey copy]; [self refreshV4CadenceWake]; } + return completed; } - (BOOL)setEncodedForecastTime:(uint64_t)timestamp error:(NSError **)error { @@ -257,11 +299,15 @@ std::vector charsFromData(NSData *data) { - (BOOL)clearEncodedFieldWithFieldKey:(NSString *)fieldKey error:(NSError **)error { if (!hasValidIdentifier(fieldKey)) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid encoded field key."); return NO; } - return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { return source.clearEncodedField(stringFromNSString(fieldKey)); }]; + const BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { return source.clearEncodedField(stringFromNSString(fieldKey)); }]; + if (completed) { [_submittedEncodedFieldKeys removeObject:fieldKey]; [self refreshV4CadenceWake]; } + return completed; } - (BOOL)clearAllEncodedFieldsWithError:(NSError **)error { - return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { source.clearAllEncodedFields(); return true; }]; + const BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { source.clearAllEncodedFields(); return true; }]; + if (completed) { [_submittedEncodedFieldKeys removeAllObjects]; [self refreshV4CadenceWake]; } + return completed; } - (MLNParticleHostFieldSubmitResult)submitWindFieldWithMetadataJSON:(NSString *)metadataJSON payload:(NSData *)payload error:(NSError **)error { @@ -309,16 +355,17 @@ std::vector charsFromData(NSData *data) { else { weather.setVisibility(_weatherColorVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); particle.setVisibility(_particleVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); accepted = true; } return true; }]; if (completed && accepted) _state = enabled ? MLNParticleFieldRuntimeStateWaitingForFields : MLNParticleFieldRuntimeStateDisabled; else if (completed) _state = MLNParticleFieldRuntimeStateError; + [self refreshV4CadenceWake]; return commandResult(completed && accepted); } - (MLNParticleFieldRuntimeCommandResult)setWeatherColorVisible:(BOOL)visible error:(NSError **)error { if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected; _weatherColorVisible = visible; return [self setVisibility:visible weather:YES error:error]; } -- (MLNParticleFieldRuntimeCommandResult)setParticleVisible:(BOOL)visible error:(NSError **)error { if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected; _particleVisible = visible; return [self setVisibility:visible weather:NO error:error]; } +- (MLNParticleFieldRuntimeCommandResult)setParticleVisible:(BOOL)visible error:(NSError **)error { if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected; _particleVisible = visible; MLNParticleFieldRuntimeCommandResult result = [self setVisibility:visible weather:NO error:error]; [self refreshV4CadenceWake]; return result; } - (MLNParticleFieldRuntimeCommandResult)setVisibility:(BOOL)visible weather:(BOOL)weatherLayer error:(NSError **)error { BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &weather, mbgl::style::RasterParticleLayer &particle) { (weatherLayer ? static_cast(weather) : static_cast(particle)).setVisibility(visible && _state != MLNParticleFieldRuntimeStateDisabled ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); return true; }]; return commandResult(completed); } -- (MLNParticleFieldRuntimeCommandResult)setTargetFps:(MLNParticleFieldRuntimeTargetFps)target error:(NSError **)error { uint8_t value = target == MLNParticleFieldRuntimeTargetFps30 ? 30 : target == MLNParticleFieldRuntimeTargetFps60 ? 60 : target == MLNParticleFieldRuntimeTargetFpsAuto ? 0 : 255; if (value == 255) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid target FPS."); return MLNParticleFieldRuntimeCommandResultRejected; } __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.setRuntimeTargetFps(value); return true; }]; if (done && ok) _targetFps = target; return commandResult(done && ok); } +- (MLNParticleFieldRuntimeCommandResult)setTargetFps:(MLNParticleFieldRuntimeTargetFps)target error:(NSError **)error { uint8_t value = target == MLNParticleFieldRuntimeTargetFps30 ? 30 : target == MLNParticleFieldRuntimeTargetFps60 ? 60 : target == MLNParticleFieldRuntimeTargetFpsAuto ? 0 : 255; if (value == 255) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid target FPS."); return MLNParticleFieldRuntimeCommandResultRejected; } __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.setRuntimeTargetFps(value); return true; }]; if (done && ok) { _targetFps = target; [self refreshV4CadenceWake]; } return commandResult(done && ok); } - (MLNParticleFieldRuntimeCommandResult)setForecastTime:(uint64_t)timestamp error:(NSError **)error { if (_state == MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultRejected; __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.seekRuntimeTimelineToTimestamp(timestamp); return true; }]; return commandResult(done && ok); } - (MLNParticleFieldRuntimeCommandResult)setPlaybackState:(MLNParticleFieldRuntimePlaybackState)playback error:(NSError **)error { if (_state == MLNParticleFieldRuntimeStateDisabled || (playback != MLNParticleFieldRuntimePlaybackStatePlay && playback != MLNParticleFieldRuntimePlaybackStatePause)) return MLNParticleFieldRuntimeCommandResultRejected; __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.controlRuntimeTimeline(playback == MLNParticleFieldRuntimePlaybackStatePlay ? 0 : 1); return true; }]; if (done && ok) _playbackState = playback; return commandResult(done && ok); } - (MLNParticleFieldRuntimeCommandResult)setPlaybackSpeed:(float)speed error:(NSError **)error { if (!validFloat(speed) || _state == MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultRejected; __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.controlRuntimeTimeline(2, speed); return true; }]; return commandResult(done && ok); } diff --git a/src/mbgl/particle/field/field_projection_materialization.cpp b/src/mbgl/particle/field/field_projection_materialization.cpp index 80c2884bd..f5aef55f3 100644 --- a/src/mbgl/particle/field/field_projection_materialization.cpp +++ b/src/mbgl/particle/field/field_projection_materialization.cpp @@ -141,24 +141,20 @@ std::optional materializeColorProjection(const Colo const auto* p0 = findPlane(*projection.t0Frame, projection.contentIdentity.binding.plane); const auto* p1 = findPlane(*projection.t1Frame, projection.contentIdentity.binding.plane); if (!p0 || !p1) return {}; - MaterializedColorProjection result{baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane), - baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane), - projection.contentIdentity, - projection.sampleState}; - if (!scalarSnapshot(*projection.t0Frame, *p0, result.t0) || !scalarSnapshot(*projection.t1Frame, *p1, result.t1)) + auto t0 = std::make_shared(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane)); + auto t1 = std::make_shared(baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane)); + if (!scalarSnapshot(*projection.t0Frame, *p0, *t0) || !scalarSnapshot(*projection.t1Frame, *p1, *t1)) return {}; - return result; + return MaterializedColorProjection{std::move(t0), std::move(t1), projection.contentIdentity, projection.sampleState}; } std::optional materializeParticleProjection(const ParticleProjection& projection) { if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {}; - MaterializedParticleProjection result{baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane), - baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1FirstPlane), - projection.contentIdentity, - projection.sampleState}; - if (!vectorSnapshot(*projection.t0Frame, projection.contentIdentity.binding, result.t0) || - !vectorSnapshot(*projection.t1Frame, projection.contentIdentity.binding, result.t1)) + auto t0 = std::make_shared(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane)); + auto t1 = std::make_shared(baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1FirstPlane)); + if (!vectorSnapshot(*projection.t0Frame, projection.contentIdentity.binding, *t0) || + !vectorSnapshot(*projection.t1Frame, projection.contentIdentity.binding, *t1)) return {}; - return result; + return MaterializedParticleProjection{std::move(t0), std::move(t1), projection.contentIdentity, projection.sampleState}; } } // namespace mbgl::particle diff --git a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp index 07d2c26d0..e1a32fd5b 100644 --- a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp +++ b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -405,6 +406,7 @@ public: void resetSequence(const char* reason) { ++resetCount; + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::GpuResourceRelease); Log::Info(Event::Render, "ParticleGPU: event=SIMULATION_RESET reason=" + std::string(reason) + " renderLayerInstanceId=" + std::to_string(renderLayerInstanceId) + " resourceGeneration=" + std::to_string(generation) + @@ -451,6 +453,7 @@ namespace { constexpr uint32_t MaxParticleVerticesPerSegment = std::numeric_limits::max() + 1u; void rebuildParticleDrawResources(const std::shared_ptr& resources) { + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleDrawableRebuild); auto vertices = std::make_shared>(); auto coverageVertices = std::make_shared>(); vertices->reserve(resources->particleCount * 2); @@ -1103,7 +1106,12 @@ bool RenderRasterParticleLayer::needsRepaint() const { if (!properties.active || (formalSource && (!sourceField || !sourceNextField)) || baseImpl->visibility == style::VisibilityType::None) return false; - return !formalSource || !sourceTimeline || sourceTimeline->cadenceDue(std::chrono::steady_clock::now()); + const bool repaint = !formalSource || + (sourceUsesV4Projection + ? sourceV4Cadence && sourceV4Cadence->cadenceDue(std::chrono::steady_clock::now()) + : !sourceTimeline || sourceTimeline->cadenceDue(std::chrono::steady_clock::now())); + if (repaint) particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleNeedsRepaint); + return repaint; } bool RenderRasterParticleLayer::needsHiddenLayerSourceCleanup(const RenderSource& renderSource) const { @@ -1168,22 +1176,26 @@ void RenderRasterParticleLayer::prepare(const LayerPrepareParameters& parameters sourceNextField = projection.t1; sourceProjectionMix = projection.sampleState.mix; sourceTimeline.reset(); + sourceV4Cadence = source.impl().v4Cadence; sourceUsesV4Projection = true; return; } + sourceV4Cadence.reset(); sourceTimeline = source.impl().timeline; if (sourceTimeline) { const auto frame = sourceTimeline->frame(); - sourceField = *frame.t0; - sourceNextField = *frame.t1; + sourceField = frame.t0; + sourceNextField = frame.t1; sourceTimelineWindowRevision = frame.windowRevision; return; } sourceField.reset(); sourceNextField.reset(); if (source.impl().field) { - sourceField = *source.impl().field; - sourceNextField = source.impl().nextField ? *source.impl().nextField : *source.impl().field; + sourceField = std::make_shared(*source.impl().field); + sourceNextField = source.impl().nextField + ? std::make_shared(*source.impl().nextField) + : sourceField; } } @@ -1193,6 +1205,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, const std::shared_ptr&, const RenderTree&, UniqueChangeRequestVec& changes) { + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleUpdate); if (!layerGroup) { if (auto group = context.createLayerGroup(layerIndex, 1, getID())) { setLayerGroup(std::move(group), changes); @@ -1212,6 +1225,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, if (target && target->getLayerGroup(0)) target->getLayerGroup(0)->clearDrawables(); for (const auto& target : gpuResources->trailTargets) if (target && target->getLayerGroup(0)) target->getLayerGroup(0)->clearDrawables(); + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::GpuResourceRelease); gpuResources.reset(); } return; @@ -1224,6 +1238,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, if (layerGroup) static_cast(*layerGroup).clearDrawables(); if (gpuResources) { removeTargets(gpuResources->registeredTargets, changes); + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::GpuResourceRelease); gpuResources.reset(); } return; @@ -1246,15 +1261,18 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, cadenceRevision = frame.cadenceRevision; cadenceAdvanced = frame.cadenceRevision != sourceTimelineCadenceRevision; if (frame.windowRevision != sourceTimelineWindowRevision) { - sourceField = *frame.t0; - sourceNextField = *frame.t1; + sourceField = frame.t0; + sourceNextField = frame.t1; sourceTimelineWindowRevision = frame.windowRevision; } + } else if (formalSource && sourceUsesV4Projection && sourceV4Cadence) { + cadenceAdvanced = sourceV4Cadence->consumeIfDue(std::chrono::steady_clock::now()); } auto resources = gpuResources; if (resources && resources->contextDestroyed) { // VALIDATION ONLY: follow the 30-02 resource-generation rule. No stale // State, Trail, or Mask texture survives a notified context loss. + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::GpuResourceRelease); gpuResources.reset(); resources.reset(); } @@ -1262,6 +1280,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, auto owned = std::make_shared(); owned->renderLayerInstanceId = renderLayerInstanceId; owned->generation = ++nextGpuGeneration; + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::GpuResourceCreate); owned->formalDisplay = formalSource; owned->shader = context.getGenericShader(shaders, ShaderName); owned->vertices = std::make_shared>( @@ -1492,6 +1511,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, .wrapV = gfx::TextureWrapType::Clamp}); scale = fieldVelocityScale(field); ++resources->fieldUploadCount; + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FieldTextureUpload); Log::Info(Event::Render, "ParticleGPU: event=FIELD_UPLOAD reason=NEW_CONTENT fieldUploadCount=" + std::to_string(resources->fieldUploadCount) + @@ -1733,6 +1753,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, return false; } resources->trailSize = viewport; + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::TrailTargetCreateOrResize); resources->trailRead = 0; resources->trailSeeded = false; Log::Info( @@ -1898,7 +1919,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, // Camera-driven frames may recompose existing weather drawables, but do // not advance the shared timeline, State, or Trail before the deadline. - if (formalSource && sourceTimeline && !cadenceAdvanced) { + if (formalSource && (sourceTimeline || sourceUsesV4Projection) && !cadenceAdvanced) { auto& compositeGroup = static_cast(*layerGroup); compositeGroup.clearDrawables(); const auto center = transformState.getLatLng(LatLng::Unwrapped); @@ -1923,6 +1944,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, } const auto stateWrite = resources->stateWrite(); + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleSimulationAdvance); const auto trailWrite = resources->trailWrite(); auto& stateGroup = static_cast(*resources->stateTargets[stateWrite]->getLayerGroup(0)); auto& trailGroup = static_cast(*resources->trailTargets[trailWrite]->getLayerGroup(0)); diff --git a/src/mbgl/renderer/layers/render_raster_particle_layer.hpp b/src/mbgl/renderer/layers/render_raster_particle_layer.hpp index 2415c5324..0fae7a429 100644 --- a/src/mbgl/renderer/layers/render_raster_particle_layer.hpp +++ b/src/mbgl/renderer/layers/render_raster_particle_layer.hpp @@ -33,9 +33,10 @@ private: void markContextDestroyed() override; std::shared_ptr gpuResources; - std::optional sourceField; - std::optional sourceNextField; + std::shared_ptr sourceField; + std::shared_ptr sourceNextField; std::shared_ptr sourceTimeline; + std::shared_ptr sourceV4Cadence; float sourceProjectionMix = 0.0f; bool sourceUsesV4Projection = false; uint64_t sourceTimelineWindowRevision = 0; diff --git a/src/mbgl/renderer/layers/render_weather_color_layer.cpp b/src/mbgl/renderer/layers/render_weather_color_layer.cpp index c72e8cbaf..99e37df9f 100644 --- a/src/mbgl/renderer/layers/render_weather_color_layer.cpp +++ b/src/mbgl/renderer/layers/render_weather_color_layer.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -88,22 +89,18 @@ struct Resources { float scale0 = 1.0f; float scale1 = 1.0f; style::FieldValueSource valueSource = style::FieldValueSource::VectorMagnitude; + float mix = 0.0f; + float opacity = 1.0f; + std::vector stops; + int16_t firstWrap = 0; + int16_t lastWrap = -1; + uint64_t drawableRebuildCount = 0; }; class Tweaker final : public gfx::DrawableTweaker { public: - Tweaker(std::shared_ptr resources_, - float mix_, - float opacity_, - int16_t wrap_, - std::vector stops_, - style::FieldValueSource valueSource_) - : resources(std::move(resources_)), - mix(mix_), - opacity(opacity_), - wrap(wrap_), - stops(std::move(stops_)), - valueSource(valueSource_) {} + Tweaker(std::shared_ptr resources_, int16_t wrap_) + : resources(std::move(resources_)), wrap(wrap_) {} void init(gfx::Drawable&) override {} void execute(gfx::Drawable& drawable, PaintParameters& parameters) override { drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationPreviousTexture); @@ -111,9 +108,9 @@ public: drawable.setTexture(resources->texture1, shaders::idParticleGpuValidationNextFieldTexture); shaders::ParticleGpuValidationUBO ubo = { .pass = {5.0f, 0.0f, 0.0f, 0.0f}, - .marker = {mix, 0.0f, 0.0f, 0.0f}, + .marker = {resources->mix, 0.0f, 0.0f, 0.0f}, .mask = {0, 0, 0, 0}, - .visual = {opacity, 0, 0, 0}, + .visual = {resources->opacity, 0, 0, 0}, .fieldBounds = bounds(resources->t0), .respawnBounds = bounds(resources->t0), .fieldScale = {resources->scale0, resources->scale1, 0, 0}, @@ -127,22 +124,18 @@ public: .weatherRamp = {}, .matrix = util::cast(parameters.matrixForTile({wrap, {0, 0, 0}})), }; - ubo.weatherRamp[0] = static_cast(stops.size()); - ubo.weatherRamp[1] = valueSource == style::FieldValueSource::VectorMagnitude ? 1.0f : 0.0f; - for (size_t i = 0; i < stops.size(); ++i) { - ubo.weatherStops[i][0] = stops[i].value; - ubo.weatherColors[i] = stops[i].color; + ubo.weatherRamp[0] = static_cast(resources->stops.size()); + ubo.weatherRamp[1] = resources->valueSource == style::FieldValueSource::VectorMagnitude ? 1.0f : 0.0f; + for (size_t i = 0; i < resources->stops.size(); ++i) { + ubo.weatherStops[i][0] = resources->stops[i].value; + ubo.weatherColors[i] = resources->stops[i].color; } drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context); } private: std::shared_ptr resources; - float mix; - float opacity; int16_t wrap; - std::vector stops; - style::FieldValueSource valueSource; }; bool same(const particle::ParticleFieldSnapshot& a, const particle::ParticleFieldSnapshot& b) { @@ -163,6 +156,7 @@ bool uploadField(gfx::Context& context, outputTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Linear, .wrapU = gfx::TextureWrapType::Clamp, .wrapV = gfx::TextureWrapType::Clamp}); + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FieldTextureUpload); return true; } } // namespace @@ -180,9 +174,10 @@ void RenderWeatherColorLayer::evaluate(const PropertyEvaluationParameters&) { } bool RenderWeatherColorLayer::needsRepaint() const { if (!isLayerRenderable() || !sourceField || !sourceNextField) return false; - if (!sourceTimeline) return true; - return sourceTimeline->frame().state == style::RasterArrayPlaybackState::Playing && - sourceTimeline->cadenceDue(std::chrono::steady_clock::now()); + const bool repaint = sourceTimeline && sourceTimeline->frame().state == style::RasterArrayPlaybackState::Playing && + sourceTimeline->cadenceDue(std::chrono::steady_clock::now()); + if (repaint) particle::incrementRendererDebugCounter(particle::RendererDebugCounter::WeatherColorNeedsRepaint); + return repaint; } bool RenderWeatherColorLayer::needsHiddenLayerSourceCleanup(const RenderSource& renderSource) const { if (!resources || renderSource.baseImpl->type != style::SourceType::RasterArray) return false; @@ -217,11 +212,13 @@ void RenderWeatherColorLayer::prepare(const LayerPrepareParameters& parameters) sourceTimeline = source.impl().timeline; if (sourceTimeline) { const auto frame = sourceTimeline->frame(); - sourceField = *frame.t0; - sourceNextField = *frame.t1; + sourceField = frame.t0; + sourceNextField = frame.t1; } else if (source.impl().field) { - sourceField = *source.impl().field; - sourceNextField = source.impl().nextField ? *source.impl().nextField : *source.impl().field; + sourceField = std::make_shared(*source.impl().field); + sourceNextField = source.impl().nextField + ? std::make_shared(*source.impl().nextField) + : sourceField; } else { sourceField.reset(); sourceNextField.reset(); @@ -233,6 +230,7 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, const std::shared_ptr&, const RenderTree&, UniqueChangeRequestVec& changes) { + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::WeatherColorUpdate); if (!layerGroup) if (auto group = context.createLayerGroup(layerIndex, 1, getID())) setLayerGroup(std::move(group), changes); if (!sourceField || !sourceNextField) { @@ -261,8 +259,8 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, if (timelineFrame.state == style::RasterArrayPlaybackState::Playing) { timelineFrame = sourceTimeline->advanceIfDue(std::chrono::steady_clock::now()).frame; } - sourceField = *timelineFrame.t0; - sourceNextField = *timelineFrame.t1; + sourceField = timelineFrame.t0; + sourceNextField = timelineFrame.t1; } auto state = std::static_pointer_cast(resources); if (!state) { @@ -331,11 +329,21 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, } const float mix = sourceTimeline ? timelineFrame.mix : (sourceUsesV4Projection ? sourceProjectionMix : 0.0f); const float opacity = static_cast(*baseImpl).opacity; + state->mix = mix; + state->opacity = opacity; + state->stops = static_cast(*baseImpl).colorStops; auto& group = static_cast(*layerGroup); - group.clearDrawables(); const auto center = transformState.getLatLng(LatLng::Unwrapped); const auto centerWrap = static_cast(std::floor((center.longitude() + 180.0) / 360.0)); - for (int16_t wrap = centerWrap - 1; wrap <= centerWrap + 1; ++wrap) { + const int16_t firstWrap = centerWrap - 1; + const int16_t lastWrap = centerWrap + 1; + if (state->firstWrap == firstWrap && state->lastWrap == lastWrap) return; + group.clearDrawables(); + state->firstWrap = firstWrap; + state->lastWrap = lastWrap; + ++state->drawableRebuildCount; + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::WeatherColorDrawableRebuild); + for (int16_t wrap = firstWrap; wrap <= lastWrap; ++wrap) { auto attributes = context.createVertexAttributeArray(); if (const auto& attribute = attributes->set(shaders::idParticleGpuValidationPosVertexAttribute)) attribute->setSharedRawData(state->vertices, @@ -358,13 +366,7 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, builder->setTexture(state->texture0, shaders::idParticleGpuValidationPreviousTexture); builder->setTexture(state->texture0, shaders::idParticleGpuValidationFieldTexture); builder->setTexture(state->texture1, shaders::idParticleGpuValidationNextFieldTexture); - builder->addTweaker( - std::make_shared(state, - mix, - opacity, - wrap, - static_cast(*baseImpl).colorStops, - valueSource)); + builder->addTweaker(std::make_shared(state, wrap)); builder->flush(context); auto drawables = builder->clearDrawables(); if (drawables.size() == 1 && drawables.front()) group.addDrawable(std::move(drawables.front())); diff --git a/src/mbgl/renderer/layers/render_weather_color_layer.hpp b/src/mbgl/renderer/layers/render_weather_color_layer.hpp index c361868c3..424030d8d 100644 --- a/src/mbgl/renderer/layers/render_weather_color_layer.hpp +++ b/src/mbgl/renderer/layers/render_weather_color_layer.hpp @@ -29,8 +29,8 @@ private: void markContextDestroyed() override; std::shared_ptr resources; - std::optional sourceField; - std::optional sourceNextField; + std::shared_ptr sourceField; + std::shared_ptr sourceNextField; std::shared_ptr sourceTimeline; float sourceProjectionMix = 0.0f; bool sourceUsesV4Projection = false; diff --git a/src/mbgl/renderer/sources/render_raster_array_source.hpp b/src/mbgl/renderer/sources/render_raster_array_source.hpp index 8fa272110..8256b29c0 100644 --- a/src/mbgl/renderer/sources/render_raster_array_source.hpp +++ b/src/mbgl/renderer/sources/render_raster_array_source.hpp @@ -27,8 +27,8 @@ public: explicit RenderRasterArraySource(Immutable impl) : RenderSource(std::move(impl)) {} bool isLoaded() const override { - return impl().field.has_value() || impl().v4RendererState.color.has_value() || - impl().v4RendererState.particle.has_value(); + return impl().field.has_value() || static_cast(impl().v4RendererState.color) || + static_cast(impl().v4RendererState.particle); } void update(Immutable value, const std::vector>&, diff --git a/src/mbgl/style/sources/raster_array_source.cpp b/src/mbgl/style/sources/raster_array_source.cpp index c61060c2a..75f23f6ab 100644 --- a/src/mbgl/style/sources/raster_array_source.cpp +++ b/src/mbgl/style/sources/raster_array_source.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -62,11 +63,11 @@ bool sameSnapshot(const particle::ParticleFieldSnapshot& a, const particle::Part particle::sameFieldGpuContent(a, b); } template -bool sameProjection(const std::optional& a, const std::optional& b) { - if (a.has_value() != b.has_value()) return false; +bool sameProjection(const std::shared_ptr& a, const std::shared_ptr& b) { + if (static_cast(a) != static_cast(b)) return false; return !a || (a->contentIdentity == b->contentIdentity && a->sampleState.mix == b->sampleState.mix && a->sampleState.requestedForecastTimeMs == b->sampleState.requestedForecastTimeMs && - sameSnapshot(a->t0, b->t0) && sameSnapshot(a->t1, b->t1)); + a->t0 && b->t0 && a->t1 && b->t1 && sameSnapshot(*a->t0, *b->t0) && sameSnapshot(*a->t1, *b->t1)); } } // namespace @@ -136,6 +137,40 @@ RasterArrayTimelineFrame RasterArrayTimelineRuntime::frame() const { return frameLocked(); } +bool RasterArrayRendererCadence::consumeIfDue(const std::chrono::steady_clock::time_point now) { + std::lock_guard lock(mutex); + if (targetFps_ != RasterArrayTargetFps::Auto && lastTick.time_since_epoch().count() != 0) { + const auto interval = std::chrono::duration(1.0 / static_cast(targetFps_)); + if (now - lastTick < interval) return false; + } + lastTick = now; + ++revision_; + return true; +} + +bool RasterArrayRendererCadence::cadenceDue(const std::chrono::steady_clock::time_point now) const { + std::lock_guard lock(mutex); + if (targetFps_ == RasterArrayTargetFps::Auto || lastTick.time_since_epoch().count() == 0) return true; + const auto interval = std::chrono::duration(1.0 / static_cast(targetFps_)); + return now - lastTick >= interval; +} + +void RasterArrayRendererCadence::setTargetFps(const RasterArrayTargetFps target) { + std::lock_guard lock(mutex); + targetFps_ = target; + lastTick = {}; +} + +RasterArrayTargetFps RasterArrayRendererCadence::targetFps() const { + std::lock_guard lock(mutex); + return targetFps_; +} + +uint64_t RasterArrayRendererCadence::revision() const { + std::lock_guard lock(mutex); + return revision_; +} + float RasterArrayTimelineRuntime::playbackSpeed() const { std::lock_guard lock(mutex); return speed; @@ -461,16 +496,23 @@ void RasterArraySource::refreshV4RendererState() { Impl::V4RendererState next; if (colorBinding) { const auto resolved = resolveColorProjection(); - if (resolved.accepted()) next.color = particle::materializeColorProjection(*resolved.projection); + if (const auto materialized = resolved.accepted() ? particle::materializeColorProjection(*resolved.projection) + : std::nullopt) + next.color = std::make_shared(std::move(*materialized)); + if (next.color) particle::incrementRendererDebugCounter(particle::RendererDebugCounter::V4ProjectionMaterialize); } if (particleBinding) { const auto resolved = resolveParticleProjection(); - if (resolved.accepted()) next.particle = particle::materializeParticleProjection(*resolved.projection); + if (const auto materialized = resolved.accepted() ? particle::materializeParticleProjection(*resolved.projection) + : std::nullopt) + next.particle = std::make_shared(std::move(*materialized)); + if (next.particle) particle::incrementRendererDebugCounter(particle::RendererDebugCounter::V4ProjectionMaterialize); } const auto& current = impl().v4RendererState; if (sameProjection(current.color, next.color) && sameProjection(current.particle, next.particle)) return; auto updated = makeMutable(impl()); updated->v4RendererState = std::move(next); + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FieldWindowRevision); baseImpl = std::move(updated); loaded = true; observer->onSourceChanged(*this); @@ -1119,6 +1161,7 @@ bool RasterArraySource::setRuntimeTargetFps(const uint8_t requested) { } targetFps = next; if (timeline) timeline->setTargetFps(next); + if (impl().v4Cadence) impl().v4Cadence->setTargetFps(next); observer->onSourceChanged(*this); return true; } diff --git a/src/mbgl/style/sources/raster_array_source_impl.hpp b/src/mbgl/style/sources/raster_array_source_impl.hpp index e7e47d504..c7ce1a71e 100644 --- a/src/mbgl/style/sources/raster_array_source_impl.hpp +++ b/src/mbgl/style/sources/raster_array_source_impl.hpp @@ -10,8 +10,8 @@ namespace mbgl::style { class RasterArraySource::Impl final : public Source::Impl { public: struct V4RendererState { - std::optional color; - std::optional particle; + std::shared_ptr color; + std::shared_ptr particle; }; Impl(std::string sourceID, std::string fixtureID_, @@ -48,6 +48,7 @@ public: localFields(other.localFields), hostFieldSubmission(other.hostFieldSubmission), timeline(other.timeline), + v4Cadence(other.v4Cadence), v4RendererState(other.v4RendererState), field(std::move(field_)) {} Impl(const Impl& other, particle::ParticleFieldSnapshot field_, particle::ParticleFieldSnapshot nextField_) @@ -66,6 +67,7 @@ public: std::vector localFields; bool hostFieldSubmission = false; std::shared_ptr timeline; + std::shared_ptr v4Cadence = std::make_shared(); // Immutable renderer-facing V4 state. It is intentionally separate from // the legacy V3 timeline/field pair. V4RendererState v4RendererState; diff --git a/test/source/raster_array_v4_host_api.test.cpp b/test/source/raster_array_v4_host_api.test.cpp index cda62e8da..0f0838321 100644 --- a/test/source/raster_array_v4_host_api.test.cpp +++ b/test/source/raster_array_v4_host_api.test.cpp @@ -409,14 +409,19 @@ TEST(Source, RasterArrayV4RendererStateMaterializesBindingsAndMixWithoutTouching const auto& color = *source.impl().v4RendererState.color; EXPECT_FLOAT_EQ(particles.sampleState.mix, 0.5f); EXPECT_FLOAT_EQ(color.sampleState.mix, 0.5f); - EXPECT_EQ(particles.t0.vector, (std::vector{1, 5, 2, 6, 3, 7, 4, 8})); - EXPECT_EQ(particles.t0.valid, (std::vector{1, 1, 1, 1})); - EXPECT_EQ(color.t0.scalar, (std::vector{1, 2, 3, 4})); + ASSERT_TRUE(particles.t0); + ASSERT_TRUE(color.t0); + EXPECT_EQ(particles.t0->vector, (std::vector{1, 5, 2, 6, 3, 7, 4, 8})); + EXPECT_EQ(particles.t0->valid, (std::vector{1, 1, 1, 1})); + EXPECT_EQ(color.t0->scalar, (std::vector{1, 2, 3, 4})); const auto particleIdentity = particles.contentIdentity; + const auto retainedParticleT0 = particles.t0; ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 4)); ASSERT_TRUE(source.impl().v4RendererState.particle); EXPECT_EQ(source.impl().v4RendererState.particle->contentIdentity, particleIdentity); EXPECT_FLOAT_EQ(source.impl().v4RendererState.particle->sampleState.mix, 0.25f); + ASSERT_TRUE(retainedParticleT0); + EXPECT_EQ(retainedParticleT0->vector, (std::vector{1, 5, 2, 6, 3, 7, 4, 8})); EXPECT_FALSE(source.impl().field); EXPECT_FALSE(source.impl().nextField); EXPECT_FALSE(source.impl().timeline); @@ -457,15 +462,21 @@ TEST(Source, RasterArrayV4MaterializerUsesExplicitLittleEndianScaleOffsetAndStab projection.sampleState = {Base + Hour / 2, 0.5f}; projection.t0Frame = t0; projection.t1Frame = t1; - const auto materialized = particle::materializeColorProjection(projection); + auto materialized = particle::materializeColorProjection(projection); ASSERT_TRUE(materialized); - EXPECT_EQ(materialized->t0.scalar, (std::vector{4.0f, -3.0f, 7.5f, 9.0f})); - EXPECT_EQ(materialized->t0.valid, (std::vector{1, 0, 1, 1})); - EXPECT_EQ(materialized->t0.generation, 17u); - EXPECT_EQ(materialized->t0.dataVersion, 23u); - EXPECT_EQ(materialized->t0.fieldSha256, "plane-t0"); - EXPECT_EQ(materialized->t1.generation, 18u); - EXPECT_EQ(materialized->t1.dataVersion, 24u); + ASSERT_TRUE(materialized->t0); + ASSERT_TRUE(materialized->t1); + EXPECT_EQ(materialized->t0->scalar, (std::vector{4.0f, -3.0f, 7.5f, 9.0f})); + EXPECT_EQ(materialized->t0->valid, (std::vector{1, 0, 1, 1})); + EXPECT_EQ(materialized->t0->generation, 17u); + EXPECT_EQ(materialized->t0->dataVersion, 23u); + EXPECT_EQ(materialized->t0->fieldSha256, "plane-t0"); + EXPECT_EQ(materialized->t1->generation, 18u); + EXPECT_EQ(materialized->t1->dataVersion, 24u); + const auto retainedT0 = materialized->t0; + materialized.reset(); + ASSERT_TRUE(retainedT0); + EXPECT_EQ(retainedT0->scalar, (std::vector{4.0f, -3.0f, 7.5f, 9.0f})); } TEST(Source, RasterArrayV4MaterializerDecodesDirectionToUInt16LittleEndian) { @@ -512,14 +523,15 @@ TEST(Source, RasterArrayV4MaterializerDecodesDirectionToUInt16LittleEndian) { projection.t1Frame = t1; const auto materialized = particle::materializeParticleProjection(projection); ASSERT_TRUE(materialized); - ASSERT_EQ(materialized->t0.vector.size(), 8u); - EXPECT_NEAR(materialized->t0.vector[0], 0.0f, 1e-5f); - EXPECT_NEAR(materialized->t0.vector[1], 1.0f, 1e-5f); - EXPECT_NEAR(materialized->t0.vector[2], 1.0f, 1e-5f); - EXPECT_NEAR(materialized->t0.vector[3], 0.0f, 1e-5f); - EXPECT_NEAR(materialized->t0.vector[4], 0.0f, 1e-5f); - EXPECT_NEAR(materialized->t0.vector[5], -1.0f, 1e-5f); - EXPECT_NEAR(materialized->t0.vector[6], -1.0f, 1e-5f); - EXPECT_NEAR(materialized->t0.vector[7], 0.0f, 1e-5f); - EXPECT_EQ(materialized->t0.vectorSemantics, particle::VectorSemantics::Direction); + ASSERT_TRUE(materialized->t0); + ASSERT_EQ(materialized->t0->vector.size(), 8u); + EXPECT_NEAR(materialized->t0->vector[0], 0.0f, 1e-5f); + EXPECT_NEAR(materialized->t0->vector[1], 1.0f, 1e-5f); + EXPECT_NEAR(materialized->t0->vector[2], 1.0f, 1e-5f); + EXPECT_NEAR(materialized->t0->vector[3], 0.0f, 1e-5f); + EXPECT_NEAR(materialized->t0->vector[4], 0.0f, 1e-5f); + EXPECT_NEAR(materialized->t0->vector[5], -1.0f, 1e-5f); + EXPECT_NEAR(materialized->t0->vector[6], -1.0f, 1e-5f); + EXPECT_NEAR(materialized->t0->vector[7], 0.0f, 1e-5f); + EXPECT_EQ(materialized->t0->vectorSemantics, particle::VectorSemantics::Direction); } diff --git a/test/style/source.test.cpp b/test/style/source.test.cpp index 9d3259397..040f72c08 100644 --- a/test/style/source.test.cpp +++ b/test/style/source.test.cpp @@ -884,6 +884,29 @@ TEST(Source, RasterArrayTimelineCadenceUsesOneSharedDeterministicDeadline) { EXPECT_TRUE(timeline.cadenceDue(start)); } +TEST(Source, RasterArrayV4RendererCadenceGatesVisualSimulationWithoutForecastPlayback) { + style::RasterArrayRendererCadence cadence; + const auto start = std::chrono::steady_clock::time_point{} + std::chrono::seconds(1); + cadence.setTargetFps(style::RasterArrayTargetFps::Fps30); + EXPECT_TRUE(cadence.cadenceDue(start)); + EXPECT_TRUE(cadence.consumeIfDue(start)); + EXPECT_FALSE(cadence.consumeIfDue(start)); + EXPECT_FALSE(cadence.cadenceDue(start + std::chrono::milliseconds(33))); + EXPECT_TRUE(cadence.cadenceDue(start + std::chrono::milliseconds(34))); + EXPECT_TRUE(cadence.consumeIfDue(start + std::chrono::milliseconds(34))); + const auto revisionAt30 = cadence.revision(); + + cadence.setTargetFps(style::RasterArrayTargetFps::Fps60); + EXPECT_TRUE(cadence.consumeIfDue(start + std::chrono::milliseconds(34))); + EXPECT_FALSE(cadence.consumeIfDue(start + std::chrono::milliseconds(50))); + EXPECT_TRUE(cadence.consumeIfDue(start + std::chrono::milliseconds(51))); + EXPECT_GT(cadence.revision(), revisionAt30); + + cadence.setTargetFps(style::RasterArrayTargetFps::Auto); + EXPECT_TRUE(cadence.consumeIfDue(start + std::chrono::milliseconds(51))); + EXPECT_TRUE(cadence.consumeIfDue(start + std::chrono::milliseconds(51))); +} + TEST(Source, RasterArrayRuntimeFieldWindowAdvancesWithoutReplacingTheSource) { SourceTest test; const auto eastMetadata = fixtureBytes("uniform-east", "metadata.json"); diff --git a/粒子开发记录.md b/粒子开发记录.md index eeac62bbf..0d4e285be 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -1,5 +1,13 @@ # 粒子开发记录 +## 2026-09-19 — STEP-085 V4 Renderer cadence 与稳定资源复用 + +- V4 WeatherColor 不再因无 legacy timeline 永久 repaint;Particle 改用 Source-owned shared cadence gate,30/60 的 simulation 仅在 deadline 推进,PAUSE forecast 不冻结可见粒子 visual simulation。 +- V4 materialized T0/T1 改为 immutable shared handle,renderer prepare 不再按 frame 深复制大 snapshot;WeatherColor 稳定状态复用 drawable,动态 UBO 参数不触发重建。 +- Android/iOS Runtime 只为 V4 30/60 future frame 提供 wake;deadline/推进仍由共用 C++ Core 决定。ForecastClock production advance owner 未找到,未擅自创建第二播放时钟。 +- 收尾自审把 wake 条件收紧为“particle binding 对应 fieldKey 已 accepted submit”;clear 无关 field 不影响仍然 ready 的粒子,disable 后 re-enable 可重新启动既有 V4 field 的 wake。以上仅静态审计,Android/iOS 编译留给 RDC。 +- 新增 DEBUG/Validation counters。仅完成静态检查;Native/Metal/Android 编译、artifact 与设备性能验收均等待 RDC。 + ## 2026-09-19 — STEP-084 正式粒子速度着色默认关闭 - 正式 `RasterParticleLayer` 与初始 `ParticleGpuResources` 的 `speedColorEnabled` 默认值均改为 `false`,使未显式配置时严格使用宿主 `particleColor`,避免内置高速橙红色带在首次资源创建前覆盖粒子颜色。