diff --git a/CMakeLists.txt b/CMakeLists.txt index 390312c3d..c115a6ad1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/docs/40_任务/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex任务_v1.md b/docs/40_任务/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex任务_v1.md new file mode 100644 index 000000000..a792d8b5d --- /dev/null +++ b/docs/40_任务/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex任务_v1.md @@ -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= +miss= +full= +reason=<...> +staticRevision= +buildRevision= +``` + +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。 diff --git a/docs/50_Step记录/STEP-090_Particle-only_Static_CPU_Fast_Path_v1.md b/docs/50_Step记录/STEP-090_Particle-only_Static_CPU_Fast_Path_v1.md new file mode 100644 index 000000000..ee93f6961 --- /dev/null +++ b/docs/50_Step记录/STEP-090_Particle-only_Static_CPU_Fast_Path_v1.md @@ -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 阶段。 diff --git a/docs/报告/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex完成报告_v1.md b/docs/报告/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex完成报告_v1.md new file mode 100644 index 000000000..f4e7aa744 --- /dev/null +++ b/docs/报告/STEP-090_Particle-only_Static_CPU_Fast_Path_Codex完成报告_v1.md @@ -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。 diff --git a/include/mbgl/particle/renderer_debug_counters.hpp b/include/mbgl/particle/renderer_debug_counters.hpp index 278c637f2..5d5dd1fb2 100644 --- a/include/mbgl/particle/renderer_debug_counters.hpp +++ b/include/mbgl/particle/renderer_debug_counters.hpp @@ -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(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) {} diff --git a/src/mbgl/annotation/annotation_manager.cpp b/src/mbgl/annotation/annotation_manager.cpp index 351a2ae3a..6cbb108d6 100644 --- a/src/mbgl/annotation/annotation_manager.cpp +++ b/src/mbgl/annotation/annotation_manager.cpp @@ -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); diff --git a/src/mbgl/annotation/annotation_manager.hpp b/src/mbgl/annotation/annotation_manager.hpp index e1451a970..6265078dc 100644 --- a/src/mbgl/annotation/annotation_manager.hpp +++ b/src/mbgl/annotation/annotation_manager.hpp @@ -42,6 +42,7 @@ public: void onStyleLoaded(); void updateData(); + bool hasPendingChanges(); void addTile(AnnotationTile&); void removeTile(AnnotationTile&); diff --git a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp index e1a32fd5b..aff71d488 100644 --- a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp +++ b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp @@ -1102,10 +1102,7 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) { } bool RenderRasterParticleLayer::needsRepaint() const { - const auto& properties = static_cast(*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(*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(renderSource); diff --git a/src/mbgl/renderer/layers/render_raster_particle_layer.hpp b/src/mbgl/renderer/layers/render_raster_particle_layer.hpp index 6452a737d..b20b71fc2 100644 --- a/src/mbgl/renderer/layers/render_raster_particle_layer.hpp +++ b/src/mbgl/renderer/layers/render_raster_particle_layer.hpp @@ -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; diff --git a/src/mbgl/renderer/particle_fast_path.hpp b/src/mbgl/renderer/particle_fast_path.hpp new file mode 100644 index 000000000..a7226e818 --- /dev/null +++ b/src/mbgl/renderer/particle_fast_path.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include +#include + +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 ParticleFastPathPreparedTree { +public: + T* get() const noexcept { return tree.get(); } + bool empty() const noexcept { return !tree; } + void replace(std::unique_ptr next) { tree = std::move(next); } + void reset() { tree.reset(); } + +private: + std::unique_ptr tree; +}; + +} // namespace mbgl diff --git a/src/mbgl/renderer/render_layer.hpp b/src/mbgl/renderer/render_layer.hpp index 1087206b2..461843894 100644 --- a/src/mbgl/renderer/render_layer.hpp +++ b/src/mbgl/renderer/render_layer.hpp @@ -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; } diff --git a/src/mbgl/renderer/render_orchestrator.cpp b/src/mbgl/renderer/render_orchestrator.cpp index 3206b9df7..8188185f2 100644 --- a/src/mbgl/renderer/render_orchestrator.cpp +++ b/src/mbgl/renderer/render_orchestrator.cpp @@ -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, + const RenderTree& renderTree) { + MLN_TRACE_FUNC(); + + const auto& items = renderTree.getLayerRenderItemMap(); + std::vector> 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) { diff --git a/src/mbgl/renderer/render_orchestrator.hpp b/src/mbgl/renderer/render_orchestrator.hpp index 5089af6c5..dff783a42 100644 --- a/src/mbgl/renderer/render_orchestrator.hpp +++ b/src/mbgl/renderer/render_orchestrator.hpp @@ -54,6 +54,15 @@ class LayerProperties; using ImmutableLayer = Immutable; +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&, const RenderTree&); + void updateParticleLayers(gfx::ShaderRegistry&, + gfx::Context&, + const TransformState&, + const std::shared_ptr&, + const RenderTree&); void processChanges(); diff --git a/src/mbgl/renderer/render_tree.hpp b/src/mbgl/renderer/render_tree.hpp index 25ed16be8..b8017ebaa 100644 --- a/src/mbgl/renderer/render_tree.hpp +++ b/src/mbgl/renderer/render_tree.hpp @@ -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: diff --git a/src/mbgl/renderer/renderer.cpp b/src/mbgl/renderer/renderer.cpp index 987dedd50..2315769e5 100644 --- a/src/mbgl/renderer/renderer.cpp +++ b/src/mbgl/renderer/renderer.cpp @@ -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) 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(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(); } diff --git a/src/mbgl/renderer/renderer_impl.cpp b/src/mbgl/renderer/renderer_impl.cpp index df1ad8ea0..6d040d7ca 100644 --- a/src/mbgl/renderer/renderer_impl.cpp +++ b/src/mbgl/renderer/renderer_impl.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -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) { +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) { + 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, + const std::shared_ptr& 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, + 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(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; diff --git a/src/mbgl/renderer/renderer_impl.hpp b/src/mbgl/renderer/renderer_impl.hpp index c7153057e..3ec032454 100644 --- a/src/mbgl/renderer/renderer_impl.hpp +++ b/src/mbgl/renderer/renderer_impl.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #if MLN_RENDER_BACKEND_METAL @@ -9,6 +10,7 @@ #endif // MLN_RENDER_BACKEND_METAL #include +#include #include namespace mbgl { @@ -42,8 +44,11 @@ private: void setObserver(RendererObserver*); - void render(const RenderTree&, const std::shared_ptr&); + void render(const RenderTree&, const std::shared_ptr&, bool particleFastFrame); void synchronizeRetainedUnderlayState(const std::shared_ptr&); + bool tryRenderParticleFastFrame(const std::shared_ptr&); + void commitPreparedRenderTree(std::unique_ptr, const std::shared_ptr&); + void clearPreparedRenderTree(); void reduceMemoryUse(); @@ -59,9 +64,53 @@ private: std::unique_ptr retainedUnderlayCache; RetainedUnderlayRevisionTracker retainedUnderlayRevisionTracker; RetainedUnderlayTopologyTracker retainedUnderlayTopologyTracker; + ParticleFastPathPreparedTree 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 transitionDuration; + std::optional 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 particlePreparedSignature; + uint64_t particlePreparedContentRevision = 0; + uint64_t particlePreparedTopologyRevision = 0; + uint64_t particlePreparedStaticRevision = 0; + std::optional particlePreparedBoundary; + Size particlePreparedBackendSize; + bool particlePreparedSceneSupported = false; + ParticleFastPathMissReason particleFastPathLastReason = ParticleFastPathMissReason::NoPreparedTree; + enum class RenderState { Never, Partial, diff --git a/test/renderer/retained_underlay_cache.test.cpp b/test/renderer/retained_underlay_cache.test.cpp index 169ae8ed1..c5ee2e248 100644 --- a/test/renderer/retained_underlay_cache.test.cpp +++ b/test/renderer/retained_underlay_cache.test.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -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 slot; + slot.replace(std::make_unique(1, destroyed)); + EXPECT_EQ(slot.get()->value, 1); + slot.replace(std::make_unique(2, destroyed)); + EXPECT_EQ(destroyed, 1); + EXPECT_EQ(slot.get()->value, 2); + slot.reset(); + EXPECT_EQ(destroyed, 2); + EXPECT_TRUE(slot.empty()); +}