feat: add particle static CPU fast path
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-09-23 08:22:24 +09:00
parent 98e1d2d977
commit 4e3d27e1f3
18 changed files with 1247 additions and 38 deletions

View File

@@ -180,6 +180,7 @@ list(APPEND SRC_FILES
${PROJECT_SOURCE_DIR}/src/mbgl/gfx/uniform_buffer.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/gfx/vertex_attribute.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/render_target.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/particle_fast_path.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/retained_underlay_cache.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/retained_underlay_cache.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/retained_underlay_runtime_state.hpp

View File

@@ -0,0 +1,239 @@
# STEP-090 Particle-only Static CPU Fast Path Codex 任务 v1
## 目标
基于 STEP-088 retained underlay GPU cache 与 STEP-089 static revision/invalidation gate,真正减少 Particle continuous repaint 下静态 PBF/vector 的 CPU processing。
当前已实测:
- PURE PBF idle:RenderThread ~0%
- PURE PARTICLE 2048 / FPS30:RenderThread ~10.2%
- production Style + 60 个 PBF layer hidden + Particle:RenderThread ~25.5%
- production PBF + Particle:RenderThread ~96~100%
- STEP-088 仅省 static GPU draw,RenderThread 仍 ~96%
- STEP-089 已保证:
- steady state staticRevision/buildRevision 稳定;
- camera/style/tile 等真实变化会 revision bump;
- transient drawable/topology churn 不误触发。
因此 STEP-090 的目标是:
> 当当前帧仅由 Particle animation 触发,且静态状态 revision/topology/camera/viewport/placement 等均稳定时,跳过不必要的静态 PBF/vector CPU pipeline,复用上一帧已准备好的 render state / RenderTree,并只执行 Particle 所需的动态 render。
## 基线
- branch: `particle/main`
- baseline HEAD: `98e1d2d977ba9e007ba313c14d63bfa54ab9d23b`
- STEP-089: CLOSED / PASS
## 核心约束
### 1. Fast path 必须在 createRenderTree 之前判定
当前:
`Renderer::render(UpdateParameters)`
→ `RenderOrchestrator::createRenderTree()`
→ `renderTree->prepare()`
→ `Renderer::Impl::render()`
如果 fast path 只放在 `Renderer::Impl::render()`,已经无法节省 createRenderTree/source update/prepare,不满足 STEP-090。
### 2. 仅允许 Particle-only frame
必须内部区分:
- Particle-only animation frame;
- 其他任何会影响 map/render tree 的更新。
Fast path 只在能够证明“除了 Particle animation,没有其他更新需要处理”时使用。
不得新增 Host/RN 手工调用:
- 不要 `particle.renderFastFrame()`
- 不要 `cacheMapNow()`
- 不要 `invalidateBasemap()`
这是 MapLibre Native 内部自动行为。
### 3. 安全 gate
至少要求:
- retained underlay runtime enabled;
- retained cache valid;
- retained buildRevision == current staticRevision;
- camera / transform unchanged;
- viewport / pixelRatio unchanged;
- styleLoaded;
- dynamic boundary unchanged;
- topologyRevision unchanged;
- no static transition;
- no placement/fade transition affecting correctness;
- no source/tile/image/glyph/style/layer mutation;
- no context reset/debug/3D/custom unsupported;
- no pending change request that would alter render tree;
- no non-particle layer transition/repaint;
- no fading tile;
- no still image request;
- no map mode / special render path that cannot safely reuse。
任何不确定情况必须 full path。
### 4. RenderTree reuse
优先实现一个最小、明确的 cached prepared RenderTree / render state 生命周期:
- full frame 成功创建并 prepare 后,可作为下一次 particle-only fast frame 的候选;
- fast frame 不重新 createRenderTree;
- fast frame 不重新调用 full `RenderTree::prepare()`;
- 复用对象生命周期必须明确,不能悬挂引用;
- full path 创建新 tree 后替换旧缓存;
- revision/context/style reset 时失效。
若 RenderTree 当前设计无法安全长期持有,允许提取一个专用 immutable prepared frame state,但不要大规模重构。
### 5. 动态部分
Particle fast frame必须仍然:
- 正常 advance simulation;
- 正常 render Particle;
- composite retained underlay;
- 正常 present;
- 保持 particle cadence/targetFps 语义。
对于 Particle 之外的动态 layer:
- 如果存在其他 `needsRepaint()` / transition / placement / fading tile,则禁止 fast path,走 full path。
- 不要在本步骤尝试支持 AIS/Ownship 等所有未来动态 layer;先确保 Particle-only fast frame 安全。
WeatherColor:
- 若本身不需要 repaint 且输入未变,可与静态 prepared state 一起复用;
- 若其状态变化,full path。
### 6. Source/PBF processing
Fast path 命中时,目标是明确不执行:
- full `createRenderTree()`
- static PBF/vector source update
- tile pyramid normal per-frame update
- static layer prepare
- static placement orchestration
- static layer group rebuild
允许仍执行 fast frame 所必须的最小 upload/tweaker/render,但要避免重新走静态 source/layer CPU pipeline。
### 7. Runtime ON/OFF
继续使用现有 `retainedUnderlayEnabled` 作为整体优化 gate:
- ON:允许 retained cache + CPU fast path;
- OFF:完全回到原 full-render baseline,用于 A/B;
- 不新增第二个用户开关,除非内部 debug-only 诊断确实必要。
这样同一个 Release 构建仍可通过 RN RendererRuntime 做完整 A/B。
### 8. 诊断
Debug 增加最小 counters/log:
- `ParticleFastFrameHit`
- `ParticleFastFrameMiss`
- `ParticleFullFrame`
- miss reason(debug only)
建议周期日志继续并入 `ParticleRetained:` 或新增稳定前缀 `ParticleFastPath:`,字段至少:
```text
hit=<n>
miss=<n>
full=<n>
reason=<...>
staticRevision=<n>
buildRevision=<n>
```
Release 不做 per-frame 日志。
如现有 diagnostics 适合,可在 Release pull snapshot 中增加累计 hit/miss,但不是强制;不要扩大 RN 高频接口。
### 9. 性能验收目标
不预设固定百分比门槛,但 Android NavSea PBF Test App 必须证明:
- AFTER_CLIP / BEFORE_SYMBOL_CLUSTER 固定 Camera;
- Cache/FastPath ON 时 fast hit 持续增长;
- full frame 不随 Particle frame 一比一增长;
- Particle 仍连续运动;
- static draw 继续不随 particle frame 增长;
- RenderThread CPU 相比 STEP-089 / Cache OFF 有明显下降。
关键基线用于比较:
```text
PURE PARTICLE ~10.2% RenderThread
PBF hidden + Particle ~25.5%
PBF visible + Particle ~96~100%
```
目标方向是让 PBF visible + Particle 大幅向前两者靠近,而不是只减少几个百分点。
### 10. 回退验证
必须验证:
- pan / zoom / rotate → fast path 立即 miss/full;
- tile load/change → full;
- style/layer/source change → full;
- cache OFF → full;
- static state重新稳定后 → fast hit恢复;
- TOP symbol-heavy / placement active → 保守 full/fallback;
- resize/context reset → full;
- no crash / stale map / ordering regression。
## 不做
- 不新增 Host API 触发 fast frame。
- 不把 provider/network/vendor 逻辑放入 Native。
- 不改 RN Weather Service 边界。
- 不做 STEP-091 级别的多动态层通用调度。
- 不做复杂 dependency graph。
- 不做 Jest。
- 不使用 worktree。
## Native 测试
新增 directed tests,至少覆盖:
1. particle-only + stable revision → fast candidate true;
2. revision mismatch → false;
3. camera/viewport mismatch → false;
4. non-particle transition/repaint → false;
5. placement/fade/fading tile → false;
6. cache disabled → false;
7. context/style/source/tile mutation后 → false;
8. full frame重新稳定后 → fast candidate true;
9. cached RenderTree 生命周期/替换不悬挂;
10. 原 12 个 retained tests继续通过。
## Codex 执行规则
- 先完整读取 AGENTS.md、STEP-088 v3、STEP-089 task/Step/报告、当前 renderer.cpp / render_orchestrator / renderer_impl / RenderTree。
- 不提交实施计划,直接调查并实现。
- 不运行 Gradle/ADB/Xcode/大构建。
- 不 commit/push。
- 不触碰无关 dirty/untracked。
- 不使用 worktree。
- 不加 Jest。
- 如果发现当前 RenderTree 不能安全缓存,先在当前架构内采用最小安全方案,不进行无边界重构。
- 如果实现范围明显膨胀到新的架构层,停止并报告阻塞点,不强行完成。
## 完成时必须报告
逐项说明:
- fast path gate 实际位于哪里;
- 如何判断 particle-only;
- 哪些 full-frame 工作被跳过;
- cached RenderTree / prepared state 生命周期;
- 所有 miss/fallback 条件;
- 修改文件;
- 新增 tests;
- RDC 精确验证命令;
- 明确没有新增 Host 手工 fast-frame API。

View File

@@ -0,0 +1,211 @@
# STEP-090 Particle-only Static CPU Fast Path v1
## 状态
**CLOSED / PASS**
STEP-090 已完成 Particle-only Static CPU Fast Path。该实现建立在 STEP-088 retained underlay GPU cache 与 STEP-089 static revision/invalidation gate 之上,在满足严格安全条件时,于 `Renderer::render()` 的 `createRenderTree()` 之前命中 fast path,复用上一帧已 prepare 的 RenderTree,并跳过静态 PBF/vector 的完整 CPU pipeline。
## 核心实现
- fast-path gate 位于 `Renderer::render()`,早于 `RenderOrchestrator::createRenderTree()`。
- full frame 成功后缓存 prepared RenderTree。
- fast frame 命中时:
- 不重新 createRenderTree;
- 不执行完整 RenderTree::prepare;
- 不执行 full static layer update;
- 只更新 Particle 动态 layer;
- 静态 underlay 继续使用 retained cache;
- 保持 Particle simulation / draw / present 正常运行。
- full path 创建新 RenderTree 前先清除旧 prepared tree,避免引用旧 RenderLayer/RenderSource 生命周期。
- context lost / clearData / reduceMemoryUse / style reload 时清除 prepared tree。
- 没有新增 Host/RN 手工 fast-frame API;仍使用已有 retainedUnderlayEnabled 作为整体优化 ON/OFF A/B 开关。
## Particle-only 安全 gate
只有下列条件同时满足才允许 fast path:
- prepared RenderTree 存在;
- retained runtime enabled;
- retained cache valid;
- static/build/content/topology revision 一致;
- Camera / viewport / pixelRatio 一致;
- UpdateParameters 的非时间语义输入一致;
- style loaded;
- dynamic boundary 一致;
- static prefix stable;
- 当前 placement/fade transition 已结束;
- 无 pending renderer changes;
- 无 non-particle transition;
- 无 non-particle needsRepaint;
- 无 fading tiles;
- Particle 当前确实 needsRepaint;
- 非 still-image request;
- Continuous MapMode;
- debug rendering disabled;
- 当前场景支持 fast path;
- annotation 无 pending mutation。
任一不满足均立即走 full path。
## placement/fade 修正
首轮实机显示 fast path 永久被 `placement-or-fade` 阻止。调查确认 prepared RenderTree 上一 full frame 的 `placementChanged / symbolFadeChange` 是历史帧事件状态,不应永久阻止后续已稳定帧。
最终改为以当前 `placementController.hasTransitions(timePoint)` 为权威判据:
- 当前 placement/fade transition 活跃:full path;
- transition 结束且 STEP-089 revision / camera / tile / style 等语义 gate 均稳定:允许 fast path。
这保留了 active symbol fade/placement 的安全回退,同时避免上一帧 placement pass 永久锁死 fast path。
## Android 性能实测
设备:Android 13 / x86_64 / FZG1-2。
场景:NavSea 正式 63 层 PBF Style,Particle=2048。
### AFTER_CLIP
FastPath ON:
```text
PROC_CPU ≈ 27.7%
MAIN_CPU ≈ 2.9%
RENDER_CPU ≈ 21.7%
```
同 APK、同场景、Cache/FastPath OFF:
```text
PROC_CPU ≈ 111.4%
MAIN_CPU ≈ 2.8%
RENDER_CPU ≈ 97.0%
```
RenderThread 从约 97% 降至约 21.7%。
稳定日志:
```text
fastHit 持续增长
fastMiss 固定
particleFull 固定
fastReason=none
buildRevision == staticRevision
retained build 固定
retained reuse 持续增长
```
代表日志:
```text
frame=2880 ... fastHit=2670 fastMiss=210 particleFull=210 fastReason=none
frame=3000 ... fastHit=2790 fastMiss=210 particleFull=210 fastReason=none
frame=3240 ... fastHit=3030 fastMiss=210 particleFull=210 fastReason=none
```
### BEFORE_SYMBOL_CLUSTER
稳定后 fast path 命中。
```text
PROC_CPU ≈ 26.3%
MAIN_CPU ≈ 2.7%
RENDER_CPU ≈ 20.3%
```
说明优化不依赖单一 AFTER_CLIP 边界。
## 安全回退验证
### Cache OFF / ON
Cache OFF:
- enabled=false;
- fastHit 停止增长;
- fastMiss / particleFull 持续增长;
- fastReason=retained-disabled;
- RenderThread 回到约 97%。
重新 Cache ON:
- cache rebuild 一次;
- buildRevision 恢复匹配;
- fastReason=none;
- fastHit 恢复持续增长。
### Camera pan
pan 前:
- staticRevision/buildRevision 稳定;
- particleFull 固定;
- fastHit 持续增长。
pan 时:
- revision 前进;
- build / invalidation / fastMiss / particleFull 增加;
- full path 正常执行。
稳定后:
- build 固定;
- fastHit 恢复持续增长;
- particleFull 再次固定。
### TOP symbol-heavy
TOP 将全部 63 层置于 Particle 下方。
实测:
- retained buildRevision=0;
- reason=static-transition / static-placement;
- fastHit 不增长;
- fastMiss / particleFull 持续增长;
- 保守 full path 生效。
说明 symbol-heavy static prefix 未被错误复用。
### Viewport / orientation
原始设备设置:
- accelerometer_rotation=1;
- user_rotation=0。
强制旋转后:
- style/cache invalid;
- full path;
- 新 RenderThread 创建;
- cache rebuild;
- 随后 fastReason=none,fastHit 恢复。
恢复原始旋转设置后再次经历同样的 full → rebuild → fast-hit 恢复流程。
设备设置最终已恢复:
- accelerometer_rotation=1;
- user_rotation=0。
## 构建与测试
- git diff --check:PASS。
- Mac OpenGL mbgl-test-runner:PASS。
- retained + ParticleFastPath directed tests:21/21 PASS。
- Android x86_64 stripped Debug Validation APK:BUILD SUCCESSFUL,约 57 MB。
- Android 安装:PASS。
- Android A/B CPU:PASS。
- Camera / Cache / TOP / viewport 回退:PASS。
- 异常日志扫描:
- 无 FATAL EXCEPTION;
- 无 ANR;
- 无 SIGSEGV/SIGABRT;
- 无 GL_INVALID / EGL_BAD;
- 无 OOM。
- Metal/iOS mbgl-core:BUILD SUCCEEDED。
- 临时 ParticleFastPathLife / ParticleFastPathDecision 日志:已移除。
- 未新增 Host/RN fast-frame API。
## 结论
STEP-090 达成目标:Particle continuous repaint 不再强制静态 PBF/vector 每帧重新执行完整 CPU render-tree/source/layer pipeline。
在真实 NavSea PBF 测试场景中,RenderThread CPU 从约 97% 降到约 20~22%,已经接近此前 PURE PARTICLE / PBF-hidden 基线级别。
下一阶段不应立即扩展成通用多动态层调度;应先基于当前 Android/iOS 实际集成结果决定是否需要新的 profiling / production hardening 阶段。

View File

@@ -0,0 +1,114 @@
# STEP-090 Particle-only Static CPU Fast Path Codex 完成报告 v1
## 最终结论
**CLOSED / PASS**
本阶段实现并验证了 MapLibre Native 内部自动 Particle-only CPU fast path。
## 完成项
- 已将 fast-path gate 放在 createRenderTree 之前。
- 已实现 prepared RenderTree 跨帧复用。
- 已实现 Particle-only 动态判定。
- 已跳过 full static createRenderTree / prepare / static layer update。
- 已保持 retained underlay GPU cache。
- 已增加 Debug counters:
- ParticleFastFrameHit
- ParticleFastFrameMiss
- ParticleFullFrame
- 已增加周期性 fastReason 诊断。
- 已处理 annotation pending mutation。
- 已处理 non-particle repaint / transition。
- 已处理 placement/fade / fading tile。
- 已处理 camera / viewport / style / source / tile / topology / revision gate。
- 已处理 context lost / clearData / reduceMemoryUse / style reload prepared tree 清理。
- 未新增 Host/RN 手工 fast-frame API。
- 未实现多动态层通用调度。
- 未加入 Jest。
## 首轮实机问题与修复
首轮 Android 实机:
- prepared tree 实际正常跨帧保存;
- 但 fastHit=0;
- 诊断 initially 显示 no-prepared-tree。
调查发现:
1. clearPreparedRenderTree 覆盖了真实 miss reason,导致诊断误导;
2. 实际阻塞为 placement-or-fade;
3. 上一 full frame 的 placementChanged / symbolFadeChange 被当成永久 blocker。
最终修正:
- clear 不再覆盖上一真实 miss reason;
- placement gate 改为当前 `placementController.hasTransitions(timePoint)`;
- 上一 full frame 的 placementChanged / symbolFadeChange 不再永久阻塞;
- current active placement/fade 仍 fail closed。
## Android A/B 性能
### AFTER_CLIP
FastPath ON:
- Process ≈ 27.7%
- Main ≈ 2.9%
- RenderThread ≈ 21.7%
FastPath OFF:
- Process ≈ 111.4%
- Main ≈ 2.8%
- RenderThread ≈ 97.0%
RenderThread 下降约 75 个百分点。
### BEFORE_SYMBOL_CLUSTER
FastPath ON:
- Process ≈ 26.3%
- Main ≈ 2.7%
- RenderThread ≈ 20.3%
## 运行时行为
稳定 fast path:
- fastHit 持续增长;
- particleFull 固定;
- retained build 固定;
- retained reuse 持续增长;
- staticRevision == buildRevision;
- fastReason=none。
Cache OFF:
- fastReason=retained-disabled;
- fastHit 停止;
- particleFull 持续增长;
- CPU 回到 full-path 基线。
Camera:
- 变化时 full;
- revision/build 更新;
- 稳定后 fastHit 自动恢复。
TOP:
- static-transition/static-placement;
- fast path 不命中;
- 保守 full path。
Viewport/orientation:
- resize/context变化时 full/rebuild;
- 稳定后自动恢复 fast path;
- 设备旋转设置最终恢复原值。
## 测试与构建
- OpenGL:PASS。
- Directed tests:21/21 PASS。
- Android APK:BUILD SUCCESSFUL。
- Android 安装/实机:PASS。
- Metal/iOS mbgl-core:BUILD SUCCEEDED。
- git diff --check:PASS。
- 异常日志扫描:PASS。
## Git
将在 scoped commit/push 后补充最终 commit hash。

View File

@@ -29,6 +29,9 @@ enum class RendererDebugCounter : uint8_t {
RetainedUnderlayInvalidation,
RetainedUnderlayStaticGroupDraw,
RetainedDynamicGroupDraw,
ParticleFastFrameHit,
ParticleFastFrameMiss,
ParticleFullFrame,
Count,
};
@@ -54,6 +57,9 @@ struct RendererDebugCounterSnapshot {
uint64_t retainedUnderlayInvalidation = 0;
uint64_t retainedUnderlayStaticGroupDraw = 0;
uint64_t retainedDynamicGroupDraw = 0;
uint64_t particleFastFrameHit = 0;
uint64_t particleFastFrameMiss = 0;
uint64_t particleFullFrame = 0;
};
inline std::atomic_uint64_t* rendererDebugCounterStorage() {
@@ -67,16 +73,34 @@ inline void incrementRendererDebugCounter(const RendererDebugCounter counter) {
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), values[13].load(std::memory_order_relaxed),
values[14].load(std::memory_order_relaxed), values[15].load(std::memory_order_relaxed),
values[16].load(std::memory_order_relaxed), values[17].load(std::memory_order_relaxed),
values[18].load(std::memory_order_relaxed), values[19].load(std::memory_order_relaxed)};
RendererDebugCounterSnapshot result;
#define MLN_LOAD_RENDERER_COUNTER(field, counter) \
result.field = values[static_cast<size_t>(RendererDebugCounter::counter)].load(std::memory_order_relaxed)
MLN_LOAD_RENDERER_COUNTER(weatherColorNeedsRepaint, WeatherColorNeedsRepaint);
MLN_LOAD_RENDERER_COUNTER(particleNeedsRepaint, ParticleNeedsRepaint);
MLN_LOAD_RENDERER_COUNTER(weatherColorUpdate, WeatherColorUpdate);
MLN_LOAD_RENDERER_COUNTER(particleUpdate, ParticleUpdate);
MLN_LOAD_RENDERER_COUNTER(particleSimulationAdvance, ParticleSimulationAdvance);
MLN_LOAD_RENDERER_COUNTER(weatherColorDrawableRebuild, WeatherColorDrawableRebuild);
MLN_LOAD_RENDERER_COUNTER(particleDrawableRebuild, ParticleDrawableRebuild);
MLN_LOAD_RENDERER_COUNTER(fieldTextureUpload, FieldTextureUpload);
MLN_LOAD_RENDERER_COUNTER(gpuResourceCreate, GpuResourceCreate);
MLN_LOAD_RENDERER_COUNTER(gpuResourceRelease, GpuResourceRelease);
MLN_LOAD_RENDERER_COUNTER(trailTargetCreateOrResize, TrailTargetCreateOrResize);
MLN_LOAD_RENDERER_COUNTER(v4ProjectionMaterialize, V4ProjectionMaterialize);
MLN_LOAD_RENDERER_COUNTER(fieldWindowRevision, FieldWindowRevision);
MLN_LOAD_RENDERER_COUNTER(fullRendererFrame, FullRendererFrame);
MLN_LOAD_RENDERER_COUNTER(retainedUnderlayBuild, RetainedUnderlayBuild);
MLN_LOAD_RENDERER_COUNTER(retainedUnderlayReuse, RetainedUnderlayReuse);
MLN_LOAD_RENDERER_COUNTER(retainedUnderlayFallback, RetainedUnderlayFallback);
MLN_LOAD_RENDERER_COUNTER(retainedUnderlayInvalidation, RetainedUnderlayInvalidation);
MLN_LOAD_RENDERER_COUNTER(retainedUnderlayStaticGroupDraw, RetainedUnderlayStaticGroupDraw);
MLN_LOAD_RENDERER_COUNTER(retainedDynamicGroupDraw, RetainedDynamicGroupDraw);
MLN_LOAD_RENDERER_COUNTER(particleFastFrameHit, ParticleFastFrameHit);
MLN_LOAD_RENDERER_COUNTER(particleFastFrameMiss, ParticleFastFrameMiss);
MLN_LOAD_RENDERER_COUNTER(particleFullFrame, ParticleFullFrame);
#undef MLN_LOAD_RENDERER_COUNTER
return result;
}
#else
inline void incrementRendererDebugCounter(RendererDebugCounter) {}

View File

@@ -221,6 +221,12 @@ void AnnotationManager::updateData() {
}
}
bool AnnotationManager::hasPendingChanges() {
CHECK_ANNOTATIONS_ENABLED_AND_RETURN(false);
std::scoped_lock lock(mutex);
return dirty;
}
void AnnotationManager::addTile(AnnotationTile& tile) {
CHECK_ANNOTATIONS_ENABLED_AND_RETURN_NOARG();
std::scoped_lock lock(mutex);

View File

@@ -42,6 +42,7 @@ public:
void onStyleLoaded();
void updateData();
bool hasPendingChanges();
void addTile(AnnotationTile&);
void removeTile(AnnotationTile&);

View File

@@ -1102,10 +1102,7 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
}
bool RenderRasterParticleLayer::needsRepaint() const {
const auto& properties = static_cast<const style::ParticleRenderLayerProperties&>(*evaluatedProperties);
if (!properties.active || (formalSource && (!sourceField || !sourceNextField)) ||
baseImpl->visibility == style::VisibilityType::None)
return false;
if (!isParticleAnimationActive()) return false;
const bool repaint = !formalSource ||
(sourceUsesV4Projection
? sourceV4Cadence && sourceV4Cadence->cadenceDue(std::chrono::steady_clock::now())
@@ -1114,6 +1111,14 @@ bool RenderRasterParticleLayer::needsRepaint() const {
return repaint;
}
bool RenderRasterParticleLayer::isParticleAnimationActive() const {
const auto& properties = static_cast<const style::ParticleRenderLayerProperties&>(*evaluatedProperties);
if (!properties.active || (formalSource && (!sourceField || !sourceNextField)) ||
baseImpl->visibility == style::VisibilityType::None)
return false;
return true;
}
bool RenderRasterParticleLayer::needsHiddenLayerSourceCleanup(const RenderSource& renderSource) const {
if (!formalSource || !gpuResources || renderSource.baseImpl->type != style::SourceType::RasterArray) return false;
const auto& source = static_cast<const RenderRasterArraySource&>(renderSource);

View File

@@ -27,6 +27,8 @@ private:
bool hasTransition() const override { return false; }
bool hasCrossfade() const override { return false; }
bool needsRepaint() const override;
bool isParticleAnimationLayer() const override { return true; }
bool isParticleAnimationActive() const override;
bool startsRetainedDynamicRange() const override { return true; }
void prepare(const LayerPrepareParameters&) override;
void markLayerRenderable(bool, UniqueChangeRequestVec&) override;

View File

@@ -0,0 +1,135 @@
#pragma once
#include <cstdint>
#include <memory>
namespace mbgl {
enum class ParticleFastPathMissReason : uint8_t {
None,
NoPreparedTree,
RetainedDisabled,
CacheInvalid,
RevisionMismatch,
ContentMutation,
CameraChanged,
ViewportChanged,
UpdateParametersChanged,
StyleNotLoaded,
BoundaryChanged,
TopologyChanged,
StaticUnstable,
PlacementOrFade,
PendingChanges,
NonParticleTransition,
NonParticleRepaint,
FadingTiles,
NoParticleRepaint,
StillImageRequest,
UnsupportedMode,
DebugOptions,
UnsupportedScene,
AnnotationMutation,
};
inline const char* particleFastPathMissReasonText(ParticleFastPathMissReason reason) noexcept {
switch (reason) {
case ParticleFastPathMissReason::None: return "none";
case ParticleFastPathMissReason::NoPreparedTree: return "no-prepared-tree";
case ParticleFastPathMissReason::RetainedDisabled: return "retained-disabled";
case ParticleFastPathMissReason::CacheInvalid: return "cache-invalid";
case ParticleFastPathMissReason::RevisionMismatch: return "revision-mismatch";
case ParticleFastPathMissReason::ContentMutation: return "content-mutation";
case ParticleFastPathMissReason::CameraChanged: return "camera-changed";
case ParticleFastPathMissReason::ViewportChanged: return "viewport-changed";
case ParticleFastPathMissReason::UpdateParametersChanged: return "update-parameters-changed";
case ParticleFastPathMissReason::StyleNotLoaded: return "style-not-loaded";
case ParticleFastPathMissReason::BoundaryChanged: return "boundary-changed";
case ParticleFastPathMissReason::TopologyChanged: return "topology-changed";
case ParticleFastPathMissReason::StaticUnstable: return "static-unstable";
case ParticleFastPathMissReason::PlacementOrFade: return "placement-or-fade";
case ParticleFastPathMissReason::PendingChanges: return "pending-changes";
case ParticleFastPathMissReason::NonParticleTransition: return "non-particle-transition";
case ParticleFastPathMissReason::NonParticleRepaint: return "non-particle-repaint";
case ParticleFastPathMissReason::FadingTiles: return "fading-tiles";
case ParticleFastPathMissReason::NoParticleRepaint: return "no-particle-repaint";
case ParticleFastPathMissReason::StillImageRequest: return "still-image-request";
case ParticleFastPathMissReason::UnsupportedMode: return "unsupported-mode";
case ParticleFastPathMissReason::DebugOptions: return "debug-options";
case ParticleFastPathMissReason::UnsupportedScene: return "unsupported-scene";
case ParticleFastPathMissReason::AnnotationMutation: return "annotation-mutation";
}
return "unknown";
}
struct ParticleFastPathGateInput {
bool hasPreparedTree = false;
bool retainedEnabled = false;
bool cacheValid = false;
bool revisionMatches = false;
bool contentRevisionMatches = false;
bool cameraMatches = false;
bool viewportMatches = false;
bool updateParametersMatch = false;
bool styleLoaded = false;
bool boundaryMatches = false;
bool topologyMatches = false;
bool staticStable = false;
bool hasPlacementTransition = false;
bool hasPendingChanges = false;
bool hasNonParticleTransition = false;
bool hasNonParticleRepaint = false;
bool hasFadingTiles = false;
bool particleNeedsRepaint = false;
bool stillImageRequest = false;
bool continuousMode = false;
bool debugDisabled = false;
bool supportedScene = false;
bool annotationMutation = false;
};
struct ParticleFastPathDecision {
bool candidate = false;
ParticleFastPathMissReason reason = ParticleFastPathMissReason::None;
};
inline ParticleFastPathDecision evaluateParticleFastPath(const ParticleFastPathGateInput& input) noexcept {
if (!input.hasPreparedTree) return {false, ParticleFastPathMissReason::NoPreparedTree};
if (!input.retainedEnabled) return {false, ParticleFastPathMissReason::RetainedDisabled};
if (!input.cacheValid) return {false, ParticleFastPathMissReason::CacheInvalid};
if (!input.revisionMatches) return {false, ParticleFastPathMissReason::RevisionMismatch};
if (!input.contentRevisionMatches) return {false, ParticleFastPathMissReason::ContentMutation};
if (!input.viewportMatches) return {false, ParticleFastPathMissReason::ViewportChanged};
if (!input.cameraMatches) return {false, ParticleFastPathMissReason::CameraChanged};
if (!input.updateParametersMatch) return {false, ParticleFastPathMissReason::UpdateParametersChanged};
if (!input.styleLoaded) return {false, ParticleFastPathMissReason::StyleNotLoaded};
if (!input.boundaryMatches) return {false, ParticleFastPathMissReason::BoundaryChanged};
if (!input.topologyMatches) return {false, ParticleFastPathMissReason::TopologyChanged};
if (!input.staticStable) return {false, ParticleFastPathMissReason::StaticUnstable};
if (input.hasPlacementTransition) return {false, ParticleFastPathMissReason::PlacementOrFade};
if (input.hasPendingChanges) return {false, ParticleFastPathMissReason::PendingChanges};
if (input.hasNonParticleTransition) return {false, ParticleFastPathMissReason::NonParticleTransition};
if (input.hasNonParticleRepaint) return {false, ParticleFastPathMissReason::NonParticleRepaint};
if (input.hasFadingTiles) return {false, ParticleFastPathMissReason::FadingTiles};
if (!input.particleNeedsRepaint) return {false, ParticleFastPathMissReason::NoParticleRepaint};
if (input.stillImageRequest) return {false, ParticleFastPathMissReason::StillImageRequest};
if (!input.continuousMode) return {false, ParticleFastPathMissReason::UnsupportedMode};
if (!input.debugDisabled) return {false, ParticleFastPathMissReason::DebugOptions};
if (!input.supportedScene) return {false, ParticleFastPathMissReason::UnsupportedScene};
if (input.annotationMutation) return {false, ParticleFastPathMissReason::AnnotationMutation};
return {true, ParticleFastPathMissReason::None};
}
template <class T>
class ParticleFastPathPreparedTree {
public:
T* get() const noexcept { return tree.get(); }
bool empty() const noexcept { return !tree; }
void replace(std::unique_ptr<T> next) { tree = std::move(next); }
void reset() { tree.reset(); }
private:
std::unique_ptr<T> tree;
};
} // namespace mbgl

View File

@@ -109,6 +109,12 @@ public:
// Returns true when an active animated layer needs another MapLibre frame.
virtual bool needsRepaint() const { return false; }
// Renderer-internal classification used by the retained-underlay CPU fast
// path. It is intentionally narrower than startsRetainedDynamicRange():
// WeatherColor starts the dynamic range but is not a Particle animation.
virtual bool isParticleAnimationLayer() const { return false; }
virtual bool isParticleAnimationActive() const { return false; }
// Renderer-internal boundary for retained static-underlay rendering. This is
// deliberately not exposed through the style or Host APIs.
virtual bool startsRetainedDynamicRange() const { return false; }

View File

@@ -929,6 +929,27 @@ bool RenderOrchestrator::retainedUnderlayHasUnsupportedLayer(int32_t boundary) c
return false;
}
ParticleFastPathDynamicState RenderOrchestrator::particleFastPathDynamicState(TimePoint timePoint) const {
ParticleFastPathDynamicState result;
result.blockingTransition = renderLight.hasTransition();
for (const auto& entry : renderLayers) {
const auto& layer = *entry.second;
result.blockingTransition = result.blockingTransition || layer.hasTransition();
if (layer.isLayerRenderable() && layer.isParticleAnimationActive()) result.particleNeedsRepaint = true;
if (layer.needsRepaint()) {
if (!layer.isParticleAnimationLayer()) {
result.nonParticleNeedsRepaint = true;
}
}
}
result.placementTransition = placementController.hasTransitions(timePoint);
for (const auto& entry : renderSources) {
result.fadingTiles = result.fadingTiles || entry.second->hasFadingTiles();
}
result.pendingChanges = !pendingChanges.empty();
return result;
}
bool RenderOrchestrator::isLoaded() const {
MLN_TRACE_FUNC();
@@ -1090,6 +1111,33 @@ void RenderOrchestrator::updateLayers(gfx::ShaderRegistry& shaders,
addChanges(changes);
}
void RenderOrchestrator::updateParticleLayers(gfx::ShaderRegistry& shaders,
gfx::Context& context,
const TransformState& state,
const std::shared_ptr<UpdateParameters>& updateParameters,
const RenderTree& renderTree) {
MLN_TRACE_FUNC();
const auto& items = renderTree.getLayerRenderItemMap();
std::vector<std::unique_ptr<ChangeRequest>> changes;
changes.reserve(8);
for (const auto& item : items) {
auto& renderLayer = item.layer.get();
if (!renderLayer.isParticleAnimationLayer()) continue;
#if MLN_RENDER_BACKEND_OPENGL
if (androidGoldfishMitigationEnabled) {
renderLayer.removeAllDrawables();
}
#endif
try {
renderLayer.update(shaders, context, state, updateParameters, renderTree, changes);
} catch (...) {
observer->onRenderError(std::current_exception());
}
}
addChanges(changes);
}
void RenderOrchestrator::processChanges() {
auto localChanges = std::move(pendingChanges);
for (auto& change : localChanges) {

View File

@@ -54,6 +54,15 @@ class LayerProperties;
using ImmutableLayer = Immutable<style::Layer::Impl>;
struct ParticleFastPathDynamicState {
bool particleNeedsRepaint = false;
bool nonParticleNeedsRepaint = false;
bool blockingTransition = false;
bool placementTransition = false;
bool fadingTiles = false;
bool pendingChanges = false;
};
class RenderOrchestrator final : public GlyphManagerObserver, public ImageManagerObserver, public RenderSourceObserver {
public:
RenderOrchestrator(bool backgroundLayerAsColor_,
@@ -113,6 +122,7 @@ public:
bool retainedUnderlayUsesPlacement(int32_t boundary) const;
bool retainedUnderlayIsStable(TimePoint, int32_t boundary) const;
bool retainedUnderlayHasUnsupportedLayer(int32_t boundary) const;
ParticleFastPathDynamicState particleFastPathDynamicState(TimePoint) const;
uint64_t getRetainedUnderlayContentRevision() const noexcept {
return retainedUnderlayContentRevision.load(std::memory_order_acquire);
}
@@ -218,6 +228,11 @@ public:
const TransformState&,
const std::shared_ptr<UpdateParameters>&,
const RenderTree&);
void updateParticleLayers(gfx::ShaderRegistry&,
gfx::Context&,
const TransformState&,
const std::shared_ptr<UpdateParameters>&,
const RenderTree&);
void processChanges();

View File

@@ -99,6 +99,17 @@ public:
// Parameters
const RenderTreeParameters& getParameters() const { return *parameters; }
// Rebase the cached prepared tree for a Particle-only frame. All semantic
// inputs are checked by Renderer before this is called; only frame-local
// time/repaint bookkeeping is allowed to change here.
void prepareParticleFastFrame(TimePoint timePoint) {
parameters->timePoint = timePoint;
parameters->needsRepaint = true;
parameters->placementChanged = false;
parameters->symbolFadeChange = 0.0f;
startTime = util::MonotonicTimer::now().count();
}
double getElapsedTime() const { return util::MonotonicTimer::now().count() - startTime; }
protected:

View File

@@ -24,6 +24,7 @@ Renderer::~Renderer() {
}
void Renderer::markContextLost() {
impl->clearPreparedRenderTree();
impl->orchestrator.markContextLost();
}
@@ -39,12 +40,21 @@ void Renderer::render(const std::shared_ptr<UpdateParameters>& updateParameters)
const bool styleChanged = impl->styleLoaded && !updateParameters->styleLoaded;
impl->styleLoaded = updateParameters->styleLoaded;
if (!impl->dynamicTextureAtlas || styleChanged) {
if (styleChanged) impl->clearPreparedRenderTree();
auto& context = impl->backend.getContext();
impl->dynamicTextureAtlas = std::make_unique<gfx::DynamicTextureAtlas>(context);
}
if (impl->tryRenderParticleFastFrame(updateParameters)) return;
// A full createRenderTree may replace RenderLayer/RenderSource instances.
// Drop the old prepared tree first so its reference-based render items can
// never outlive the orchestrator objects they describe.
impl->clearPreparedRenderTree();
if (auto renderTree = impl->orchestrator.createRenderTree(updateParameters, impl->dynamicTextureAtlas)) {
renderTree->prepare();
impl->render(*renderTree, updateParameters);
impl->render(*renderTree, updateParameters, false);
impl->commitPreparedRenderTree(std::move(renderTree), updateParameters);
} else {
impl->clearPreparedRenderTree();
}
}
@@ -162,11 +172,13 @@ bool Renderer::getTileCacheEnabled() const {
void Renderer::reduceMemoryUse() {
gfx::BackendScope guard{impl->backend};
impl->clearPreparedRenderTree();
impl->reduceMemoryUse();
impl->orchestrator.reduceMemoryUse();
}
void Renderer::clearData() {
impl->clearPreparedRenderTree();
impl->orchestrator.clearData();
}

View File

@@ -1,6 +1,7 @@
#include <mbgl/renderer/renderer_impl.hpp>
#include <mbgl/geometry/line_atlas.hpp>
#include <mbgl/annotation/annotation_manager.hpp>
#include <mbgl/gfx/backend_scope.hpp>
#include <mbgl/gfx/context.hpp>
#include <mbgl/gfx/cull_face_mode.hpp>
@@ -58,7 +59,7 @@ RendererObserver& nullObserver() {
#ifndef NDEBUG
constexpr uint64_t RetainedCounterLogIntervalFrames = 120;
void logRetainedRendererCounters(const RetainedUnderlayCache& cache) {
void logRetainedRendererCounters(const RetainedUnderlayCache& cache, ParticleFastPathMissReason fastPathReason) {
const auto detailed = particle::rendererDebugCounterSnapshot();
const auto retained = cache.getDiagnostics();
if (detailed.fullRendererFrame == 0 ||
@@ -79,7 +80,11 @@ void logRetainedRendererCounters(const RetainedUnderlayCache& cache) {
<< " buildRevision=" << cache.getBuildRevision()
<< " staticDraw=" << detailed.retainedUnderlayStaticGroupDraw
<< " dynamicDraw=" << detailed.retainedDynamicGroupDraw
<< " particleAdvance=" << detailed.particleSimulationAdvance;
<< " particleAdvance=" << detailed.particleSimulationAdvance
<< " fastHit=" << detailed.particleFastFrameHit
<< " fastMiss=" << detailed.particleFastFrameMiss
<< " particleFull=" << detailed.particleFullFrame
<< " fastReason=" << particleFastPathMissReasonText(fastPathReason);
Log::Info(Event::Render, message.str());
}
#endif
@@ -130,9 +135,153 @@ void Renderer::Impl::synchronizeRetainedUnderlayState(
retainedUnderlayCache->synchronizeRuntimeState(state);
}
void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<UpdateParameters>& updateParameters) {
Renderer::Impl::ParticleFastPathUpdateSignature Renderer::Impl::makeParticleFastPathUpdateSignature(
const UpdateParameters& parameters) const {
ParticleFastPathUpdateSignature result;
result.viewport = parameters.transformState.getSize();
parameters.transformState.getProjMatrix(result.cameraMatrix);
result.pixelRatio = parameters.pixelRatio;
result.mode = parameters.mode;
result.debugOptions = parameters.debugOptions;
result.styleLoaded = parameters.styleLoaded;
result.spriteLoaded = parameters.spriteLoaded;
result.transformChanging = parameters.transformState.isChanging();
result.gestureInProgress = parameters.transformState.isGestureInProgress();
result.light = parameters.light.get();
result.images = parameters.images.get();
result.sources = parameters.sources.get();
result.layers = parameters.layers.get();
result.fontFaces = parameters.fontFaces.get();
result.fileSource = parameters.fileSource.get();
result.glyphURL = parameters.glyphURL;
result.transitionDuration = parameters.transitionOptions.duration;
result.transitionDelay = parameters.transitionOptions.delay;
result.placementTransitions = parameters.transitionOptions.enablePlacementTransitions;
result.prefetchZoomDelta = parameters.prefetchZoomDelta;
result.crossSourceCollisions = parameters.crossSourceCollisions;
result.fastPFOREnabled = parameters.fastPFOREnabled;
result.tileLodMinRadius = parameters.tileLodMinRadius;
result.tileLodScale = parameters.tileLodScale;
result.tileLodPitchThreshold = parameters.tileLodPitchThreshold;
result.tileLodZoomShift = parameters.tileLodZoomShift;
result.tileLodMode = parameters.tileLodMode;
return result;
}
bool Renderer::Impl::particleFastPathNonCameraInputsMatch(const ParticleFastPathUpdateSignature& lhs,
const ParticleFastPathUpdateSignature& rhs) {
return lhs.pixelRatio == rhs.pixelRatio && lhs.mode == rhs.mode && lhs.debugOptions == rhs.debugOptions &&
lhs.styleLoaded == rhs.styleLoaded && lhs.spriteLoaded == rhs.spriteLoaded &&
lhs.transformChanging == rhs.transformChanging && lhs.gestureInProgress == rhs.gestureInProgress &&
lhs.light == rhs.light && lhs.images == rhs.images && lhs.sources == rhs.sources && lhs.layers == rhs.layers &&
lhs.fontFaces == rhs.fontFaces && lhs.fileSource == rhs.fileSource && lhs.glyphURL == rhs.glyphURL &&
lhs.transitionDuration == rhs.transitionDuration && lhs.transitionDelay == rhs.transitionDelay &&
lhs.placementTransitions == rhs.placementTransitions && lhs.prefetchZoomDelta == rhs.prefetchZoomDelta &&
lhs.crossSourceCollisions == rhs.crossSourceCollisions && lhs.fastPFOREnabled == rhs.fastPFOREnabled &&
lhs.tileLodMinRadius == rhs.tileLodMinRadius && lhs.tileLodScale == rhs.tileLodScale &&
lhs.tileLodPitchThreshold == rhs.tileLodPitchThreshold && lhs.tileLodZoomShift == rhs.tileLodZoomShift &&
lhs.tileLodMode == rhs.tileLodMode;
}
bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptr<UpdateParameters>& updateParameters) {
const auto signature = makeParticleFastPathUpdateSignature(*updateParameters);
const auto dynamicState = orchestrator.particleFastPathDynamicState(updateParameters->timePoint);
bool annotationMutation = false;
if (auto guard = updateParameters->annotationManager.lock(); updateParameters->annotationManager) {
annotationMutation = updateParameters->annotationManager->hasPendingChanges();
}
const auto retained = retainedUnderlayCache->getDiagnostics();
const auto currentBoundary = orchestrator.getRetainedDynamicBoundary();
const bool hasSignature = particlePreparedSignature.has_value();
const bool viewportMatches = hasSignature && particlePreparedSignature->viewport == signature.viewport &&
particlePreparedBackendSize == backend.getDefaultRenderable().getSize();
const bool cameraMatches = hasSignature && particlePreparedSignature->cameraMatrix == signature.cameraMatrix;
const bool updateParametersMatch =
hasSignature && particleFastPathNonCameraInputsMatch(*particlePreparedSignature, signature);
const bool revisionMatches =
retainedUnderlayCache->getStaticRevision() == retainedUnderlayRevisionTracker.current() &&
retainedUnderlayCache->getBuildRevision() == retainedUnderlayRevisionTracker.current() &&
particlePreparedStaticRevision == retainedUnderlayRevisionTracker.current();
const bool boundaryMatches = particlePreparedBoundary == currentBoundary;
const bool topologyMatches = particlePreparedTopologyRevision == retainedUnderlayTopologyTracker.current();
const bool contentRevisionMatches =
particlePreparedContentRevision == orchestrator.getRetainedUnderlayContentRevision();
const bool staticStable =
currentBoundary && orchestrator.retainedUnderlayIsStable(updateParameters->timePoint, *currentBoundary);
const ParticleFastPathGateInput input{
.hasPreparedTree = !particlePreparedTree.empty(),
.retainedEnabled = retained.enabled,
.cacheValid = retainedUnderlayCache->isValid(),
.revisionMatches = revisionMatches,
.contentRevisionMatches = contentRevisionMatches,
.cameraMatches = cameraMatches,
.viewportMatches = viewportMatches,
.updateParametersMatch = updateParametersMatch,
.styleLoaded = updateParameters->styleLoaded,
.boundaryMatches = boundaryMatches,
.topologyMatches = topologyMatches,
.staticStable = staticStable,
// placementChanged/symbolFadeChange belong to the previous prepared
// full frame. The current controller transition is the authoritative
// semantic state: while active we fail closed, and once settled the
// old full-frame snapshot must not permanently block the fast path.
.hasPlacementTransition = dynamicState.placementTransition,
.hasPendingChanges = dynamicState.pendingChanges,
.hasNonParticleTransition = dynamicState.blockingTransition,
.hasNonParticleRepaint = dynamicState.nonParticleNeedsRepaint,
.hasFadingTiles = dynamicState.fadingTiles,
.particleNeedsRepaint = dynamicState.particleNeedsRepaint,
.stillImageRequest = updateParameters->stillImageRequest,
.continuousMode = updateParameters->mode == MapMode::Continuous,
.debugDisabled = updateParameters->debugOptions == MapDebugOptions::NoDebug,
.supportedScene = particlePreparedSceneSupported,
.annotationMutation = annotationMutation,
};
const auto decision = evaluateParticleFastPath(input);
particleFastPathLastReason = decision.reason;
if (!decision.candidate) {
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleFastFrameMiss);
return false;
}
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleFastFrameHit);
particlePreparedTree.get()->prepareParticleFastFrame(updateParameters->timePoint);
render(*particlePreparedTree.get(), updateParameters, true);
return true;
}
void Renderer::Impl::commitPreparedRenderTree(std::unique_ptr<RenderTree> renderTree,
const std::shared_ptr<UpdateParameters>& updateParameters) {
particlePreparedSignature = makeParticleFastPathUpdateSignature(*updateParameters);
particlePreparedContentRevision = orchestrator.getRetainedUnderlayContentRevision();
particlePreparedTopologyRevision = retainedUnderlayTopologyTracker.current();
particlePreparedStaticRevision = retainedUnderlayRevisionTracker.current();
particlePreparedBoundary = orchestrator.getRetainedDynamicBoundary();
particlePreparedBackendSize = backend.getDefaultRenderable().getSize();
particlePreparedTree.replace(std::move(renderTree));
}
void Renderer::Impl::clearPreparedRenderTree() {
particlePreparedTree.reset();
particlePreparedSignature.reset();
particlePreparedContentRevision = 0;
particlePreparedTopologyRevision = 0;
particlePreparedStaticRevision = 0;
particlePreparedBoundary.reset();
particlePreparedBackendSize = {};
particlePreparedSceneSupported = false;
}
void Renderer::Impl::render(const RenderTree& renderTree,
const std::shared_ptr<UpdateParameters>& updateParameters,
bool particleFastFrame) {
MLN_TRACE_FUNC();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FullRendererFrame);
if (!particleFastFrame) {
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleFullFrame);
}
auto& context = backend.getContext();
context.setObserver(this);
@@ -273,7 +422,7 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
// - UPLOAD PASS -------------------------------------------------------------------------------
// Uploads all required buffers and images before we do any actual rendering.
{
if (!particleFastFrame) {
const auto uploadPass = parameters.encoder->createUploadPass("upload",
parameters.backend.getDefaultRenderable());
#if !defined(NDEBUG)
@@ -295,8 +444,13 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
// - LAYER GROUP UPDATE ------------------------------------------------------------------------
// Updates all layer groups and process changes
if (staticData && staticData->shaders) {
orchestrator.updateLayers(
*staticData->shaders, context, renderTreeParameters.transformParams.state, updateParameters, renderTree);
if (particleFastFrame) {
orchestrator.updateParticleLayers(
*staticData->shaders, context, renderTreeParameters.transformParams.state, updateParameters, renderTree);
} else {
orchestrator.updateLayers(
*staticData->shaders, context, renderTreeParameters.transformParams.state, updateParameters, renderTree);
}
}
orchestrator.processChanges();
@@ -314,7 +468,8 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
});
retainedKey = RetainedUnderlayKey{size, *retainedBoundary, retainedCameraMatrix};
}
const auto retainedTopologyRevision = retainedUnderlayTopologyTracker.observe(retainedTopology);
const auto retainedTopologyRevision = particleFastFrame ? retainedUnderlayTopologyTracker.current()
: retainedUnderlayTopologyTracker.observe(retainedTopology);
const auto retainedStaticGroupCount = retainedTopology.staticPrefix.size();
bool legacyBelowBoundary = false;
if (retainedBoundary) {
@@ -359,6 +514,7 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
renderTreeParameters.debugOptions == MapDebugOptions::NoDebug &&
retainedStaticStable && retainedPlacementStable &&
!legacyBelowBoundary && !retainedUnsupportedStaticLayer;
if (!particleFastFrame) particlePreparedSceneSupported = retainedSupported;
const char* retainedFallbackReason = !retainedBoundary ? "no-boundary"
: retainedStaticGroupCount == 0 ? "empty-static-prefix"
: !renderTreeParameters.loaded ? "style-not-loaded"
@@ -394,29 +550,44 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
const auto debugGroup = uploadPass->createDebugGroup("layerGroup-upload");
#endif
// Update the debug layer groups
orchestrator.updateDebugLayerGroups(renderTree, parameters);
// Update the debug layer groups only on the full path. Debug rendering
// is excluded by the fast-path gate.
if (!particleFastFrame) orchestrator.updateDebugLayerGroups(renderTree, parameters);
// Tweakers are run in the upload pass so they can set up uniforms.
parameters.currentLayer = 0;
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) {
layerGroup.runTweakers(renderTree, parameters);
parameters.currentLayer++;
});
parameters.currentLayer = 0;
orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) {
layerGroup.runTweakers(renderTree, parameters);
parameters.currentLayer++;
});
if (particleFastFrame && retainedBoundary) {
orchestrator.visitLayerGroupsFrom(*retainedBoundary, [&](LayerGroupBase& layerGroup, size_t ordinal) {
parameters.currentLayer = static_cast<uint32_t>(ordinal);
layerGroup.runTweakers(renderTree, parameters);
});
} else {
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) {
layerGroup.runTweakers(renderTree, parameters);
parameters.currentLayer++;
});
parameters.currentLayer = 0;
orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) {
layerGroup.runTweakers(renderTree, parameters);
parameters.currentLayer++;
});
}
// Give the layers a chance to upload
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); });
if (particleFastFrame && retainedBoundary) {
orchestrator.visitLayerGroupsFrom(
*retainedBoundary, [&](LayerGroupBase& layerGroup, size_t) { layerGroup.upload(*uploadPass); });
} else {
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); });
}
// Give the render targets a chance to upload
orchestrator.visitRenderTargets([&](RenderTarget& renderTarget) { renderTarget.upload(*uploadPass); });
// Upload the Debug layer group
orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); });
if (!particleFastFrame) {
orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); });
}
if (useRetainedUnderlay) retainedUnderlayCache->upload(*uploadPass);
}
@@ -594,6 +765,7 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
};
const auto drawableDebugOverlays = [&] {
if (particleFastFrame) return;
// Renders debug overlays.
{
const auto debugGroup(parameters.renderPass->createDebugGroup("debug"));
@@ -619,7 +791,14 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
drawableDebugOverlays();
// Give the layers a chance to do cleanup
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.postRender(orchestrator, parameters); });
if (particleFastFrame && retainedBoundary) {
orchestrator.visitLayerGroupsFrom(*retainedBoundary, [&](LayerGroupBase& layerGroup, size_t) {
layerGroup.postRender(orchestrator, parameters);
});
} else {
orchestrator.visitLayerGroups(
[&](LayerGroupBase& layerGroup) { layerGroup.postRender(orchestrator, parameters); });
}
context.unbindGlobalUniformBuffers(*parameters.renderPass);
// Ends the RenderPass
@@ -662,7 +841,7 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
}
#ifndef NDEBUG
logRetainedRendererCounters(*retainedUnderlayCache);
logRetainedRendererCounters(*retainedUnderlayCache, particleFastPathLastReason);
#endif
frameCount += 1;

View File

@@ -1,6 +1,7 @@
#pragma once
#include <mbgl/renderer/render_orchestrator.hpp>
#include <mbgl/renderer/particle_fast_path.hpp>
#include <mbgl/gfx/context_observer.hpp>
#if MLN_RENDER_BACKEND_METAL
@@ -9,6 +10,7 @@
#endif // MLN_RENDER_BACKEND_METAL
#include <memory>
#include <optional>
#include <string>
namespace mbgl {
@@ -42,8 +44,11 @@ private:
void setObserver(RendererObserver*);
void render(const RenderTree&, const std::shared_ptr<UpdateParameters>&);
void render(const RenderTree&, const std::shared_ptr<UpdateParameters>&, bool particleFastFrame);
void synchronizeRetainedUnderlayState(const std::shared_ptr<RetainedUnderlayRuntimeState>&);
bool tryRenderParticleFastFrame(const std::shared_ptr<UpdateParameters>&);
void commitPreparedRenderTree(std::unique_ptr<RenderTree>, const std::shared_ptr<UpdateParameters>&);
void clearPreparedRenderTree();
void reduceMemoryUse();
@@ -59,9 +64,53 @@ private:
std::unique_ptr<RetainedUnderlayCache> retainedUnderlayCache;
RetainedUnderlayRevisionTracker retainedUnderlayRevisionTracker;
RetainedUnderlayTopologyTracker retainedUnderlayTopologyTracker;
ParticleFastPathPreparedTree<RenderTree> particlePreparedTree;
gfx::DynamicTextureAtlasPtr dynamicTextureAtlas;
bool styleLoaded = false;
struct ParticleFastPathUpdateSignature {
Size viewport;
mat4 cameraMatrix{};
float pixelRatio = 1.0f;
MapMode mode = MapMode::Continuous;
MapDebugOptions debugOptions = MapDebugOptions::NoDebug;
bool styleLoaded = false;
bool spriteLoaded = false;
bool transformChanging = false;
bool gestureInProgress = false;
const void* light = nullptr;
const void* images = nullptr;
const void* sources = nullptr;
const void* layers = nullptr;
const void* fontFaces = nullptr;
const void* fileSource = nullptr;
std::string glyphURL;
std::optional<Duration> transitionDuration;
std::optional<Duration> transitionDelay;
bool placementTransitions = true;
uint8_t prefetchZoomDelta = 0;
bool crossSourceCollisions = false;
bool fastPFOREnabled = false;
double tileLodMinRadius = 0;
double tileLodScale = 0;
double tileLodPitchThreshold = 0;
double tileLodZoomShift = 0;
TileLodMode tileLodMode = TileLodMode::Default;
};
ParticleFastPathUpdateSignature makeParticleFastPathUpdateSignature(const UpdateParameters&) const;
static bool particleFastPathNonCameraInputsMatch(const ParticleFastPathUpdateSignature&,
const ParticleFastPathUpdateSignature&);
std::optional<ParticleFastPathUpdateSignature> particlePreparedSignature;
uint64_t particlePreparedContentRevision = 0;
uint64_t particlePreparedTopologyRevision = 0;
uint64_t particlePreparedStaticRevision = 0;
std::optional<int32_t> particlePreparedBoundary;
Size particlePreparedBackendSize;
bool particlePreparedSceneSupported = false;
ParticleFastPathMissReason particleFastPathLastReason = ParticleFastPathMissReason::NoPreparedTree;
enum class RenderState {
Never,
Partial,

View File

@@ -1,4 +1,5 @@
#include <mbgl/renderer/retained_underlay_cache.hpp>
#include <mbgl/renderer/particle_fast_path.hpp>
#include <gtest/gtest.h>
@@ -31,6 +32,34 @@ RetainedUnderlayRevisionState revisionState(Size size = {640, 480}, int32_t boun
result.cameraMatrix = key(size, boundary).cameraMatrix;
return result;
}
ParticleFastPathGateInput fastPathInput() {
return {
.hasPreparedTree = true,
.retainedEnabled = true,
.cacheValid = true,
.revisionMatches = true,
.contentRevisionMatches = true,
.cameraMatches = true,
.viewportMatches = true,
.updateParametersMatch = true,
.styleLoaded = true,
.boundaryMatches = true,
.topologyMatches = true,
.staticStable = true,
.hasPlacementTransition = false,
.hasPendingChanges = false,
.hasNonParticleTransition = false,
.hasNonParticleRepaint = false,
.hasFadingTiles = false,
.particleNeedsRepaint = true,
.stillImageRequest = false,
.continuousMode = true,
.debugDisabled = true,
.supportedScene = true,
.annotationMutation = false,
};
}
} // namespace
TEST(RetainedDynamicBoundary, SelectsFirstCandidate) {
@@ -310,3 +339,125 @@ TEST(RetainedUnderlayCache, RuntimeControlDisablesAndReenablesMapScopedCache) {
EXPECT_EQ(cache.evaluate(key(), 1, true), RetainedUnderlayDecision::Build);
EXPECT_TRUE(state->snapshot().enabled);
}
TEST(ParticleFastPath, ParticleOnlyStableRevisionIsCandidate) {
const auto decision = evaluateParticleFastPath(fastPathInput());
EXPECT_TRUE(decision.candidate);
EXPECT_EQ(decision.reason, ParticleFastPathMissReason::None);
}
TEST(ParticleFastPath, RevisionCameraAndViewportMismatchReject) {
auto input = fastPathInput();
input.revisionMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::RevisionMismatch);
input = fastPathInput();
input.cameraMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::CameraChanged);
input = fastPathInput();
input.viewportMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::ViewportChanged);
}
TEST(ParticleFastPath, NonParticleTransitionAndRepaintReject) {
auto input = fastPathInput();
input.hasNonParticleTransition = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::NonParticleTransition);
input = fastPathInput();
input.hasNonParticleRepaint = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::NonParticleRepaint);
}
TEST(ParticleFastPath, SettledCurrentPlacementAllowsCandidateAfterPlacementFullFrame) {
// A full frame may have run placement and captured placementChanged=true.
// That previous-frame event is deliberately not a fast-path gate input;
// only the controller's current transition state is authoritative.
auto input = fastPathInput();
input.hasPlacementTransition = false;
EXPECT_TRUE(evaluateParticleFastPath(input).candidate);
}
TEST(ParticleFastPath, ActivePlacementFadeAndFadingTilesReject) {
auto input = fastPathInput();
input.hasPlacementTransition = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::PlacementOrFade);
input = fastPathInput();
input.hasFadingTiles = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::FadingTiles);
}
TEST(ParticleFastPath, CacheDisabledAndSemanticMutationReject) {
auto input = fastPathInput();
input.retainedEnabled = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::RetainedDisabled);
input = fastPathInput();
input.cacheValid = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::CacheInvalid);
input = fastPathInput();
input.contentRevisionMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::ContentMutation);
input = fastPathInput();
input.updateParametersMatch = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::UpdateParametersChanged);
input = fastPathInput();
input.topologyMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::TopologyChanged);
input = fastPathInput();
input.styleLoaded = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::StyleNotLoaded);
}
TEST(ParticleFastPath, PendingStillAndUnsupportedPathsReject) {
auto input = fastPathInput();
input.hasPendingChanges = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::PendingChanges);
input = fastPathInput();
input.stillImageRequest = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::StillImageRequest);
input = fastPathInput();
input.supportedScene = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::UnsupportedScene);
}
TEST(ParticleFastPath, FullFrameCanRestabilizeCandidate) {
auto input = fastPathInput();
input.contentRevisionMatches = false;
EXPECT_FALSE(evaluateParticleFastPath(input).candidate);
input.contentRevisionMatches = true;
input.revisionMatches = true;
input.cacheValid = true;
input.staticStable = true;
EXPECT_TRUE(evaluateParticleFastPath(input).candidate);
}
TEST(ParticleFastPath, PreparedTreeReplacementOwnsLifetime) {
struct TrackedTree {
TrackedTree(int value_, int& destroyed_)
: value(value_), destroyed(destroyed_) {}
~TrackedTree() { ++destroyed; }
int value;
int& destroyed;
};
int destroyed = 0;
ParticleFastPathPreparedTree<TrackedTree> slot;
slot.replace(std::make_unique<TrackedTree>(1, destroyed));
EXPECT_EQ(slot.get()->value, 1);
slot.replace(std::make_unique<TrackedTree>(2, destroyed));
EXPECT_EQ(destroyed, 1);
EXPECT_EQ(slot.get()->value, 2);
slot.reset();
EXPECT_EQ(destroyed, 2);
EXPECT_TRUE(slot.empty());
}