From 43105b339b43b8b3d35c68aaf40aa3902bc6b58d Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Wed, 16 Aug 2023 22:49:44 +0200 Subject: [PATCH] Fix unloading strategy when we hit GPU memory limit for entity Previously we would remove any nodes that were at the end of the replacement queue, which was causing some bad behavior with nearly infinite loops unloading and reloading chunks. The strategy has been updated to only remove nodes that we know will not be requested again in the next scene update. --- src/3d/chunks/qgschunkedentity_p.cpp | 78 +++++++++++++++++++++++----- src/3d/chunks/qgschunkedentity_p.h | 2 + 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/3d/chunks/qgschunkedentity_p.cpp b/src/3d/chunks/qgschunkedentity_p.cpp index 34e29a2b2c28..e214e5b305b9 100644 --- a/src/3d/chunks/qgschunkedentity_p.cpp +++ b/src/3d/chunks/qgschunkedentity_p.cpp @@ -51,6 +51,21 @@ static float screenSpaceError( QgsChunkNode *node, const QgsChunkedEntity::Scene return sse; } + +static bool hasAnyActiveChildren( QgsChunkNode *node, QList &activeNodes ) +{ + for ( int i = 0; i < node->childCount(); ++i ) + { + QgsChunkNode *child = node->children()[i]; + if ( child->entity() && activeNodes.contains( child ) ) + return true; + if ( hasAnyActiveChildren( child, activeNodes ) ) + return true; + } + return false; +} + + QgsChunkedEntity::QgsChunkedEntity( float tau, QgsChunkLoaderFactory *loaderFactory, bool ownsFactory, int primitiveBudget, Qt3DCore::QNode *parent ) : Qgs3DMapSceneEntity( parent ) , mTau( tau ) @@ -111,6 +126,7 @@ QgsChunkedEntity::~QgsChunkedEntity() } } + void QgsChunkedEntity::handleSceneUpdate( const SceneState &state ) { if ( !mIsValid ) @@ -173,20 +189,13 @@ void QgsChunkedEntity::handleSceneUpdate( const SceneState &state ) #endif } - double usedGpuMemory = QgsChunkedEntity::calculateEntityGpuMemorySize( this ); - - // unload those that are over the limit for replacement - // TODO: what to do when our cache is too small and nodes are being constantly evicted + loaded again - while ( usedGpuMemory > mGpuMemoryLimit ) - { - QgsChunkListEntry *entry = mReplacementQueue->takeLast(); - usedGpuMemory -= QgsChunkedEntity::calculateEntityGpuMemorySize( entry->chunk->entity() ); - mActiveNodes.removeOne( entry->chunk ); - entry->chunk->unloadChunk(); // also deletes the entry + // if this entity's loaded nodes are using more GPU memory than allowed, + // let's try to unload those that are not needed right now #ifdef QGISDEBUG - ++unloaded; + unloaded = unloadNodes(); +#else + unloadNodes(); #endif - } if ( mBboxesEntity ) { @@ -214,6 +223,51 @@ void QgsChunkedEntity::handleSceneUpdate( const SceneState &state ) .arg( t.elapsed() ), 2 ); } + +int QgsChunkedEntity::unloadNodes() +{ + double usedGpuMemory = QgsChunkedEntity::calculateEntityGpuMemorySize( this ); + if ( usedGpuMemory <= mGpuMemoryLimit ) + return 0; + + QgsDebugMsgLevel( QStringLiteral( "Going to unload nodes to free GPU memory (used: %1 MB, limit: %2 MB)" ).arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 ); + + int unloaded = 0; + + // unload nodes starting from the back of the queue with currently loaded + // nodes - i.e. those that have been least recently used + QgsChunkListEntry *entry = mReplacementQueue->last(); + while ( entry && usedGpuMemory > mGpuMemoryLimit ) + { + // not all nodes are safe to unload: we do not want to unload nodes + // that are currently active, or have their descendants active or their + // siblings or their descendants are active (because in the next scene + // update, these would be very likely loaded again, making the unload worthless) + if ( entry->chunk->parent() && !hasAnyActiveChildren( entry->chunk->parent(), mActiveNodes ) ) + { + QgsChunkListEntry *entryPrev = entry->prev; + mReplacementQueue->takeEntry( entry ); + usedGpuMemory -= QgsChunkedEntity::calculateEntityGpuMemorySize( entry->chunk->entity() ); + mActiveNodes.removeOne( entry->chunk ); + entry->chunk->unloadChunk(); // also deletes the entry + ++unloaded; + entry = entryPrev; + } + else + { + entry = entry->prev; + } + } + + if ( usedGpuMemory > mGpuMemoryLimit ) + { + QgsDebugMsgLevel( QStringLiteral( "Unable to unload enough nodes to free GPU memory (used: %1 MB, limit: %2 MB)" ).arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 ); + } + + return unloaded; +} + + QgsRange QgsChunkedEntity::getNearFarPlaneRange( const QMatrix4x4 &viewMatrix ) const { QList activeEntityNodes = activeNodes(); diff --git a/src/3d/chunks/qgschunkedentity_p.h b/src/3d/chunks/qgschunkedentity_p.h index e3c28689d555..a6aa6e6d0072 100644 --- a/src/3d/chunks/qgschunkedentity_p.h +++ b/src/3d/chunks/qgschunkedentity_p.h @@ -147,6 +147,8 @@ class QgsChunkedEntity : public Qgs3DMapSceneEntity void startJobs(); QgsChunkQueueJob *startJob( QgsChunkNode *node ); + int unloadNodes(); + private slots: void onActiveJobFinished();