fix: close STEP-085 V4 renderer efficiency
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
45
docs/30_设计/STEP-085_V4_Renderer性能修复设计_v1.md
Normal file
45
docs/30_设计/STEP-085_V4_Renderer性能修复设计_v1.md
Normal file
@@ -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<const ParticleFieldSnapshot>`。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 采样提供证据。
|
||||
35
docs/50_Step记录/STEP-085_V4_Renderer性能修复_v1.md
Normal file
35
docs/50_Step记录/STEP-085_V4_Renderer性能修复_v1.md
Normal file
@@ -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 状态调整尚未经过目标平台编译。
|
||||
52
docs/报告/STEP-085_V4_Renderer性能修复_Codex完成报告_v1.md
Normal file
52
docs/报告/STEP-085_V4_Renderer性能修复_Codex完成报告_v1.md
Normal file
@@ -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 任务。
|
||||
@@ -4,21 +4,22 @@
|
||||
#include <mbgl/particle/field/field_data_adapter.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
|
||||
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<const ParticleFieldSnapshot> t0;
|
||||
std::shared_ptr<const ParticleFieldSnapshot> t1;
|
||||
ColorProjectionContentIdentity contentIdentity;
|
||||
FieldSampleState sampleState;
|
||||
};
|
||||
|
||||
struct MaterializedParticleProjection {
|
||||
ParticleFieldSnapshot t0;
|
||||
ParticleFieldSnapshot t1;
|
||||
std::shared_ptr<const ParticleFieldSnapshot> t0;
|
||||
std::shared_ptr<const ParticleFieldSnapshot> t1;
|
||||
ParticleProjectionContentIdentity contentIdentity;
|
||||
FieldSampleState sampleState;
|
||||
};
|
||||
|
||||
68
include/mbgl/particle/renderer_debug_counters.hpp
Normal file
68
include/mbgl/particle/renderer_debug_counters.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
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<size_t>(RendererDebugCounter::Count)]{};
|
||||
return counters;
|
||||
}
|
||||
|
||||
inline void incrementRendererDebugCounter(const RendererDebugCounter counter) {
|
||||
rendererDebugCounterStorage()[static_cast<size_t>(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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String> 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;
|
||||
|
||||
@@ -101,7 +101,13 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
MLNParticleFieldRuntimeTargetFps _targetFps;
|
||||
BOOL _weatherColorVisible;
|
||||
BOOL _particleVisible;
|
||||
NSString *_v4ParticleBindingFieldKey;
|
||||
NSMutableSet<NSString *> *_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<char> 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<char> 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<char> charsFromData(NSData *data) {
|
||||
result = static_cast<MLNParticleEncodedFieldSubmitResult>(coreResult);
|
||||
return true;
|
||||
}];
|
||||
if (completed && result > MLNParticleEncodedFieldSubmitResultNone) {
|
||||
[_submittedEncodedFieldKeys addObject:fieldKey];
|
||||
[self refreshV4CadenceWake];
|
||||
}
|
||||
return completed ? result : MLNParticleEncodedFieldSubmitResultNone;
|
||||
}
|
||||
|
||||
@@ -220,10 +259,11 @@ std::vector<char> 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<mbgl::particle::FieldValueClass>(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<char> 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<char> 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<char> 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<char> 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<mbgl::style::Layer &>(weather) : static_cast<mbgl::style::Layer &>(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); }
|
||||
|
||||
@@ -141,24 +141,20 @@ std::optional<MaterializedColorProjection> 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<ParticleFieldSnapshot>(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(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<MaterializedParticleProjection> 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<ParticleFieldSnapshot>(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(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
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <mbgl/gfx/texture2d.hpp>
|
||||
#include <mbgl/map/transform_state.hpp>
|
||||
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
|
||||
#include <mbgl/particle/renderer_debug_counters.hpp>
|
||||
#include <mbgl/renderer/buckets/heatmap_bucket.hpp>
|
||||
#include <mbgl/renderer/change_request.hpp>
|
||||
#include <mbgl/renderer/layer_group.hpp>
|
||||
@@ -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<uint16_t>::max() + 1u;
|
||||
|
||||
void rebuildParticleDrawResources(const std::shared_ptr<ParticleGpuResources>& resources) {
|
||||
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleDrawableRebuild);
|
||||
auto vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>();
|
||||
auto coverageVertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>();
|
||||
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<const particle::ParticleFieldSnapshot>(*source.impl().field);
|
||||
sourceNextField = source.impl().nextField
|
||||
? std::make_shared<const particle::ParticleFieldSnapshot>(*source.impl().nextField)
|
||||
: sourceField;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1193,6 +1205,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
const std::shared_ptr<UpdateParameters>&,
|
||||
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&>(*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<ParticleGpuResources>();
|
||||
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<gfx::VertexVector<HeatmapTextureLayoutVertex>>(
|
||||
@@ -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&>(*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<LayerGroup&>(*resources->stateTargets[stateWrite]->getLayerGroup(0));
|
||||
auto& trailGroup = static_cast<LayerGroup&>(*resources->trailTargets[trailWrite]->getLayerGroup(0));
|
||||
|
||||
@@ -33,9 +33,10 @@ private:
|
||||
void markContextDestroyed() override;
|
||||
|
||||
std::shared_ptr<ParticleGpuResources> gpuResources;
|
||||
std::optional<particle::ParticleFieldSnapshot> sourceField;
|
||||
std::optional<particle::ParticleFieldSnapshot> sourceNextField;
|
||||
std::shared_ptr<const particle::ParticleFieldSnapshot> sourceField;
|
||||
std::shared_ptr<const particle::ParticleFieldSnapshot> sourceNextField;
|
||||
std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline;
|
||||
std::shared_ptr<style::RasterArrayRendererCadence> sourceV4Cadence;
|
||||
float sourceProjectionMix = 0.0f;
|
||||
bool sourceUsesV4Projection = false;
|
||||
uint64_t sourceTimelineWindowRevision = 0;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <mbgl/gfx/shader_registry.hpp>
|
||||
#include <mbgl/gfx/texture2d.hpp>
|
||||
#include <mbgl/gfx/vertex_attribute.hpp>
|
||||
#include <mbgl/particle/renderer_debug_counters.hpp>
|
||||
#include <mbgl/renderer/buckets/heatmap_bucket.hpp>
|
||||
#include <mbgl/renderer/layer_group.hpp>
|
||||
#include <mbgl/renderer/render_static_data.hpp>
|
||||
@@ -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<style::WeatherColorStop> 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> resources_,
|
||||
float mix_,
|
||||
float opacity_,
|
||||
int16_t wrap_,
|
||||
std::vector<style::WeatherColorStop> 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> 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<float>(parameters.matrixForTile({wrap, {0, 0, 0}})),
|
||||
};
|
||||
ubo.weatherRamp[0] = static_cast<float>(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<float>(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> resources;
|
||||
float mix;
|
||||
float opacity;
|
||||
int16_t wrap;
|
||||
std::vector<style::WeatherColorStop> 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<const particle::ParticleFieldSnapshot>(*source.impl().field);
|
||||
sourceNextField = source.impl().nextField
|
||||
? std::make_shared<const particle::ParticleFieldSnapshot>(*source.impl().nextField)
|
||||
: sourceField;
|
||||
} else {
|
||||
sourceField.reset();
|
||||
sourceNextField.reset();
|
||||
@@ -233,6 +230,7 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders,
|
||||
const std::shared_ptr<UpdateParameters>&,
|
||||
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>(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<const style::WeatherColorLayer::Impl&>(*baseImpl).opacity;
|
||||
state->mix = mix;
|
||||
state->opacity = opacity;
|
||||
state->stops = static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).colorStops;
|
||||
auto& group = static_cast<LayerGroup&>(*layerGroup);
|
||||
group.clearDrawables();
|
||||
const auto center = transformState.getLatLng(LatLng::Unwrapped);
|
||||
const auto centerWrap = static_cast<int16_t>(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<Tweaker>(state,
|
||||
mix,
|
||||
opacity,
|
||||
wrap,
|
||||
static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).colorStops,
|
||||
valueSource));
|
||||
builder->addTweaker(std::make_shared<Tweaker>(state, wrap));
|
||||
builder->flush(context);
|
||||
auto drawables = builder->clearDrawables();
|
||||
if (drawables.size() == 1 && drawables.front()) group.addDrawable(std::move(drawables.front()));
|
||||
|
||||
@@ -29,8 +29,8 @@ private:
|
||||
void markContextDestroyed() override;
|
||||
|
||||
std::shared_ptr<void> resources;
|
||||
std::optional<particle::ParticleFieldSnapshot> sourceField;
|
||||
std::optional<particle::ParticleFieldSnapshot> sourceNextField;
|
||||
std::shared_ptr<const particle::ParticleFieldSnapshot> sourceField;
|
||||
std::shared_ptr<const particle::ParticleFieldSnapshot> sourceNextField;
|
||||
std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline;
|
||||
float sourceProjectionMix = 0.0f;
|
||||
bool sourceUsesV4Projection = false;
|
||||
|
||||
@@ -27,8 +27,8 @@ public:
|
||||
explicit RenderRasterArraySource(Immutable<style::RasterArraySource::Impl> 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<bool>(impl().v4RendererState.color) ||
|
||||
static_cast<bool>(impl().v4RendererState.particle);
|
||||
}
|
||||
void update(Immutable<style::Source::Impl> value,
|
||||
const std::vector<Immutable<style::LayerProperties>>&,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
|
||||
#include <mbgl/particle/field/local_particle_field_feed.hpp>
|
||||
#include <mbgl/particle/renderer_debug_counters.hpp>
|
||||
#include <mbgl/storage/file_source.hpp>
|
||||
#include <mbgl/storage/resource.hpp>
|
||||
#include <mbgl/style/source_observer.hpp>
|
||||
@@ -62,11 +63,11 @@ bool sameSnapshot(const particle::ParticleFieldSnapshot& a, const particle::Part
|
||||
particle::sameFieldGpuContent(a, b);
|
||||
}
|
||||
template <class Projection>
|
||||
bool sameProjection(const std::optional<Projection>& a, const std::optional<Projection>& b) {
|
||||
if (a.has_value() != b.has_value()) return false;
|
||||
bool sameProjection(const std::shared_ptr<const Projection>& a, const std::shared_ptr<const Projection>& b) {
|
||||
if (static_cast<bool>(a) != static_cast<bool>(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<std::mutex> lock(mutex);
|
||||
if (targetFps_ != RasterArrayTargetFps::Auto && lastTick.time_since_epoch().count() != 0) {
|
||||
const auto interval = std::chrono::duration<double>(1.0 / static_cast<uint8_t>(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<std::mutex> lock(mutex);
|
||||
if (targetFps_ == RasterArrayTargetFps::Auto || lastTick.time_since_epoch().count() == 0) return true;
|
||||
const auto interval = std::chrono::duration<double>(1.0 / static_cast<uint8_t>(targetFps_));
|
||||
return now - lastTick >= interval;
|
||||
}
|
||||
|
||||
void RasterArrayRendererCadence::setTargetFps(const RasterArrayTargetFps target) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
targetFps_ = target;
|
||||
lastTick = {};
|
||||
}
|
||||
|
||||
RasterArrayTargetFps RasterArrayRendererCadence::targetFps() const {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return targetFps_;
|
||||
}
|
||||
|
||||
uint64_t RasterArrayRendererCadence::revision() const {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return revision_;
|
||||
}
|
||||
|
||||
float RasterArrayTimelineRuntime::playbackSpeed() const {
|
||||
std::lock_guard<std::mutex> 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<const particle::MaterializedColorProjection>(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<const particle::MaterializedParticleProjection>(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>(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;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ namespace mbgl::style {
|
||||
class RasterArraySource::Impl final : public Source::Impl {
|
||||
public:
|
||||
struct V4RendererState {
|
||||
std::optional<particle::MaterializedColorProjection> color;
|
||||
std::optional<particle::MaterializedParticleProjection> particle;
|
||||
std::shared_ptr<const particle::MaterializedColorProjection> color;
|
||||
std::shared_ptr<const particle::MaterializedParticleProjection> 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<RasterArrayLocalFieldDescriptor> localFields;
|
||||
bool hostFieldSubmission = false;
|
||||
std::shared_ptr<RasterArrayTimelineRuntime> timeline;
|
||||
std::shared_ptr<RasterArrayRendererCadence> v4Cadence = std::make_shared<RasterArrayRendererCadence>();
|
||||
// Immutable renderer-facing V4 state. It is intentionally separate from
|
||||
// the legacy V3 timeline/field pair.
|
||||
V4RendererState v4RendererState;
|
||||
|
||||
@@ -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<float>{1, 5, 2, 6, 3, 7, 4, 8}));
|
||||
EXPECT_EQ(particles.t0.valid, (std::vector<uint8_t>{1, 1, 1, 1}));
|
||||
EXPECT_EQ(color.t0.scalar, (std::vector<float>{1, 2, 3, 4}));
|
||||
ASSERT_TRUE(particles.t0);
|
||||
ASSERT_TRUE(color.t0);
|
||||
EXPECT_EQ(particles.t0->vector, (std::vector<float>{1, 5, 2, 6, 3, 7, 4, 8}));
|
||||
EXPECT_EQ(particles.t0->valid, (std::vector<uint8_t>{1, 1, 1, 1}));
|
||||
EXPECT_EQ(color.t0->scalar, (std::vector<float>{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<float>{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<float>{4.0f, -3.0f, 7.5f, 9.0f}));
|
||||
EXPECT_EQ(materialized->t0.valid, (std::vector<uint8_t>{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<float>{4.0f, -3.0f, 7.5f, 9.0f}));
|
||||
EXPECT_EQ(materialized->t0->valid, (std::vector<uint8_t>{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<float>{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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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`,避免内置高速橙红色带在首次资源创建前覆盖粒子颜色。
|
||||
|
||||
Reference in New Issue
Block a user