feat: add particle static CPU fast path
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:
@@ -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
|
||||
|
||||
@@ -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。
|
||||
211
docs/50_Step记录/STEP-090_Particle-only_Static_CPU_Fast_Path_v1.md
Normal file
211
docs/50_Step记录/STEP-090_Particle-only_Static_CPU_Fast_Path_v1.md
Normal 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 阶段。
|
||||
@@ -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。
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -42,6 +42,7 @@ public:
|
||||
void onStyleLoaded();
|
||||
|
||||
void updateData();
|
||||
bool hasPendingChanges();
|
||||
|
||||
void addTile(AnnotationTile&);
|
||||
void removeTile(AnnotationTile&);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
135
src/mbgl/renderer/particle_fast_path.hpp
Normal file
135
src/mbgl/renderer/particle_fast_path.hpp
Normal 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
|
||||
@@ -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; }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user