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

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

View File

@@ -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;