Skip to content

Commit

Permalink
Allow GPU memory limit configuration + show a warning when limit got hit
Browse files Browse the repository at this point in the history
  • Loading branch information
wonder-sk authored and nyalldawson committed Sep 28, 2023
1 parent 4676af8 commit 859a851
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 51 deletions.
7 changes: 7 additions & 0 deletions python/3d/auto_generated/qgs3dmapscene.sip.in
Expand Up @@ -167,6 +167,13 @@ Emitted when the FPS counter is activated or deactivated
Emitted when the viewed 2D extent seen by the 3D camera has changed

.. versionadded:: 3.26
%End

void gpuMemoryLimitReached();
%Docstring
Emitted when one of the entities reaches its GPU memory limit
and it is not possible to lower the GPU memory use by unloading
data that's not currently needed.
%End

public slots:
Expand Down
29 changes: 6 additions & 23 deletions src/3d/chunks/qgschunkedentity_p.cpp
Expand Up @@ -17,13 +17,6 @@

#include <QElapsedTimer>
#include <QVector4D>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <Qt3DRender/QBuffer>
typedef Qt3DRender::QBuffer Qt3DQBuffer;
#else
#include <Qt3DCore/QBuffer>
typedef Qt3DCore::QBuffer Qt3DQBuffer;
#endif

#include "qgs3dutils.h"
#include "qgschunkboundsentity_p.h"
Expand Down Expand Up @@ -226,9 +219,12 @@ void QgsChunkedEntity::handleSceneUpdate( const SceneState &state )

int QgsChunkedEntity::unloadNodes()
{
double usedGpuMemory = QgsChunkedEntity::calculateEntityGpuMemorySize( this );
double usedGpuMemory = Qgs3DUtils::calculateEntityGpuMemorySize( this );
if ( usedGpuMemory <= mGpuMemoryLimit )
{
setHasReachedGpuMemoryLimit( false );
return 0;
}

QgsDebugMsgLevel( QStringLiteral( "Going to unload nodes to free GPU memory (used: %1 MB, limit: %2 MB)" ).arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );

Expand All @@ -247,7 +243,7 @@ int QgsChunkedEntity::unloadNodes()
{
QgsChunkListEntry *entryPrev = entry->prev;
mReplacementQueue->takeEntry( entry );
usedGpuMemory -= QgsChunkedEntity::calculateEntityGpuMemorySize( entry->chunk->entity() );
usedGpuMemory -= Qgs3DUtils::calculateEntityGpuMemorySize( entry->chunk->entity() );
mActiveNodes.removeOne( entry->chunk );
entry->chunk->unloadChunk(); // also deletes the entry
++unloaded;
Expand All @@ -261,6 +257,7 @@ int QgsChunkedEntity::unloadNodes()

if ( usedGpuMemory > mGpuMemoryLimit )
{
setHasReachedGpuMemoryLimit( true );
QgsDebugMsgLevel( QStringLiteral( "Unable to unload enough nodes to free GPU memory (used: %1 MB, limit: %2 MB)" ).arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
}

Expand Down Expand Up @@ -738,20 +735,6 @@ void QgsChunkedEntity::cancelActiveJobs()
}
}

double QgsChunkedEntity::calculateEntityGpuMemorySize( Qt3DCore::QEntity *entity )
{
long long usedGpuMemory = 0;
for ( Qt3DQBuffer *buffer : entity->findChildren<Qt3DQBuffer *>() )
{
usedGpuMemory += buffer->data().size();
}
for ( Qt3DRender::QTexture2D *tex : entity->findChildren<Qt3DRender::QTexture2D *>() )
{
// TODO : lift the assumption that the texture is RGBA
usedGpuMemory += tex->width() * tex->height() * 4;
}
return usedGpuMemory / 1024.0 / 1024.0;
}

QVector<QgsRayCastingUtils::RayHit> QgsChunkedEntity::rayIntersection( const QgsRayCastingUtils::Ray3D &ray, const QgsRayCastingUtils::RayCastContext &context ) const
{
Expand Down
15 changes: 0 additions & 15 deletions src/3d/chunks/qgschunkedentity_p.h
Expand Up @@ -93,20 +93,6 @@ class QgsChunkedEntity : public Qgs3DMapSceneEntity
//! Returns the root node of the whole quadtree hierarchy of nodes
QgsChunkNode *rootNode() const { return mRootNode; }

/**
* Sets the limit of the GPU memory used to render the entity
* \since QGIS 3.26
*/
void setGpuMemoryLimit( double gpuMemoryLimit ) { mGpuMemoryLimit = gpuMemoryLimit; }

/**
* Returns the limit of the GPU memory used to render the entity in megabytes
* \since QGIS 3.26
*/
double gpuMemoryLimit() const { return mGpuMemoryLimit; }

static double calculateEntityGpuMemorySize( Qt3DCore::QEntity *entity );

/**
* Checks if \a ray intersects the entity by using the specified parameters in \a context and returns information about the hits.
* This method is typically used by map tools that need to identify the exact location on a 3d entity that the mouse cursor points at,
Expand Down Expand Up @@ -182,7 +168,6 @@ class QgsChunkedEntity : public Qgs3DMapSceneEntity
bool mIsValid = true;

int mPrimitivesBudget = std::numeric_limits<int>::max();
double mGpuMemoryLimit = 500.0; // in megabytes
};

/// @endcond
Expand Down
4 changes: 4 additions & 0 deletions src/3d/qgs3dmapscene.cpp
Expand Up @@ -371,6 +371,8 @@ void Qgs3DMapScene::updateScene()
for ( Qgs3DMapSceneEntity *entity : std::as_const( mSceneEntities ) )
{
entity->handleSceneUpdate( sceneState_( mEngine ) );
if ( entity->hasReachedGpuMemoryLimit() )
emit gpuMemoryLimitReached();
}

updateSceneState();
Expand Down Expand Up @@ -446,6 +448,8 @@ void Qgs3DMapScene::onFrameTriggered( float dt )
{
QgsDebugMsgLevel( QStringLiteral( "need for update" ), 2 );
entity->handleSceneUpdate( sceneState_( mEngine ) );
if ( entity->hasReachedGpuMemoryLimit() )
emit gpuMemoryLimitReached();
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/3d/qgs3dmapscene.h
Expand Up @@ -230,6 +230,13 @@ class _3D_EXPORT Qgs3DMapScene : public QObject
*/
void viewed2DExtentFrom3DChanged( QVector<QgsPointXY> extent );

/**
* Emitted when one of the entities reaches its GPU memory limit
* and it is not possible to lower the GPU memory use by unloading
* data that's not currently needed.
*/
void gpuMemoryLimitReached();

public slots:
//! Updates the temporale entities
void updateTemporal();
Expand Down
26 changes: 25 additions & 1 deletion src/3d/qgs3dmapsceneentity_p.h
Expand Up @@ -32,6 +32,7 @@
#include <QMatrix4x4>

#include "qgsrange.h"
#include "qgssettings.h"

#define SIP_NO_FILE

Expand All @@ -48,7 +49,10 @@ class Qgs3DMapSceneEntity : public Qt3DCore::QEntity
//! Constructs a chunked entity
Qgs3DMapSceneEntity( Qt3DCore::QNode *parent = nullptr )
: Qt3DCore::QEntity( parent )
{}
{
const QgsSettings settings;
mGpuMemoryLimit = settings.value( QStringLiteral( "map3d/gpuMemoryLimit" ), 500.0, QgsSettings::App ).toDouble();
}

//! Records some bits about the scene (context for handleSceneUpdate() method)
struct SceneState
Expand All @@ -71,12 +75,32 @@ class Qgs3DMapSceneEntity : public Qt3DCore::QEntity
//! Returns the near to far plane range for the entity using the specified \a viewMatrix
virtual QgsRange<float> getNearFarPlaneRange( const QMatrix4x4 &viewMatrix ) const { Q_UNUSED( viewMatrix ) return QgsRange<float>( 1e9, 0 ); }


//! Sets the limit of the GPU memory used to render the entity
void setGpuMemoryLimit( double gpuMemoryLimit ) { mGpuMemoryLimit = gpuMemoryLimit; }

//! Returns the limit of the GPU memory used to render the entity in megabytes
double gpuMemoryLimit() const { return mGpuMemoryLimit; }

//! Returns whether the entity has reached GPU memory limit
bool hasReachedGpuMemoryLimit() const { return mHasReachedGpuMemoryLimit; }

protected:
//! Sets whether the GPU memory limit has been reached
void setHasReachedGpuMemoryLimit( bool reached ) { mHasReachedGpuMemoryLimit = reached; }

signals:
//! Emitted when the number of pending jobs changes (some jobs have finished or some jobs have been just created)
void pendingJobsCountChanged();

//! Emitted when a new 3D entity has been created. Other components can use that to do extra work
void newEntityCreated( Qt3DCore::QEntity *entity );

protected:
//! Limit how much GPU memory this entity can use
double mGpuMemoryLimit = 500.0; // in megabytes
//! Whether the entity is currently over the GPU memory limit (used to report a warning to the user)
bool mHasReachedGpuMemoryLimit = false;
};

/// @endcond
Expand Down
25 changes: 25 additions & 0 deletions src/3d/qgs3dutils.cpp
Expand Up @@ -48,6 +48,14 @@
#include <Qt3DExtras/QPhongMaterial>
#include <Qt3DRender/QRenderSettings>

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <Qt3DRender/QBuffer>
typedef Qt3DRender::QBuffer Qt3DQBuffer;
#else
#include <Qt3DCore/QBuffer>
typedef Qt3DCore::QBuffer Qt3DQBuffer;
#endif

// declared here as Qgs3DTypes has no cpp file
const char *Qgs3DTypes::PROP_NAME_3D_RENDERER_FLAG = "PROP_NAME_3D_RENDERER_FLAG";

Expand Down Expand Up @@ -143,6 +151,23 @@ QImage Qgs3DUtils::captureSceneDepthBuffer( QgsAbstract3DEngine &engine, Qgs3DMa
return resImage;
}


double Qgs3DUtils::calculateEntityGpuMemorySize( Qt3DCore::QEntity *entity )
{
long long usedGpuMemory = 0;
for ( Qt3DQBuffer *buffer : entity->findChildren<Qt3DQBuffer *>() )
{
usedGpuMemory += buffer->data().size();
}
for ( Qt3DRender::QTexture2D *tex : entity->findChildren<Qt3DRender::QTexture2D *>() )
{
// TODO : lift the assumption that the texture is RGBA
usedGpuMemory += tex->width() * tex->height() * 4;
}
return usedGpuMemory / 1024.0 / 1024.0;
}


bool Qgs3DUtils::exportAnimation( const Qgs3DAnimationSettings &animationSettings,
Qgs3DMapSettings &mapSettings,
int framesPerSecond,
Expand Down
7 changes: 7 additions & 0 deletions src/3d/qgs3dutils.h
Expand Up @@ -73,6 +73,13 @@ class _3D_EXPORT Qgs3DUtils
*/
static QImage captureSceneDepthBuffer( QgsAbstract3DEngine &engine, Qgs3DMapScene *scene );

/**
* Calculates approximate usage of GPU memory by an entity
* \return GPU memory usage in megabytes
* \since QGIS 3.34
*/
static double calculateEntityGpuMemorySize( Qt3DCore::QEntity *entity );

/**
* Captures 3D animation frames to the selected folder
*
Expand Down
19 changes: 19 additions & 0 deletions src/app/3d/qgs3dmapcanvaswidget.cpp
Expand Up @@ -228,6 +228,9 @@ Qgs3DMapCanvasWidget::Qgs3DMapCanvasWidget( const QString &name, bool isDocked )
mAnimationWidget = new Qgs3DAnimationWidget( this );
mAnimationWidget->setVisible( false );

mMessageBar = new QgsMessageBar( this );
mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed );

QHBoxLayout *topLayout = new QHBoxLayout;
topLayout->setContentsMargins( 0, 0, 0, 0 );
topLayout->setSpacing( style()->pixelMetric( QStyle::PM_LayoutHorizontalSpacing ) );
Expand All @@ -251,6 +254,7 @@ Qgs3DMapCanvasWidget::Qgs3DMapCanvasWidget( const QString &name, bool isDocked )
layout->setContentsMargins( 0, 0, 0, 0 );
layout->setSpacing( 0 );
layout->addLayout( topLayout );
layout->addWidget( mMessageBar );
layout->addWidget( mCanvas );
layout->addWidget( mAnimationWidget );

Expand Down Expand Up @@ -366,6 +370,7 @@ void Qgs3DMapCanvasWidget::setMapSettings( Qgs3DMapSettings *map )
mCanvas->setMap( map );

connect( mCanvas->scene(), &Qgs3DMapScene::totalPendingJobsCountChanged, this, &Qgs3DMapCanvasWidget::onTotalPendingJobsCountChanged );
connect( mCanvas->scene(), &Qgs3DMapScene::gpuMemoryLimitReached, this, &Qgs3DMapCanvasWidget::onGpuMemoryLimitReached );

mAnimationWidget->setCameraController( mCanvas->scene()->cameraController() );
mAnimationWidget->setMap( map );
Expand Down Expand Up @@ -648,3 +653,17 @@ void Qgs3DMapCanvasWidget::onExtentChanged()
mViewExtentHighlight->closePoints();
}
}

void Qgs3DMapCanvasWidget::onGpuMemoryLimitReached()
{
// let's report this issue just once, rather than spamming user if this happens repeatedly
if ( mGpuMemoryLimitReachedReported )
return;

const QgsSettings settings;
double memLimit = settings.value( QStringLiteral( "map3d/gpuMemoryLimit" ), 500.0, QgsSettings::App ).toDouble();
mMessageBar->pushMessage( tr( "A map layer has used all graphics memory allowed (%1 MB). "
"You may want to lower the amount of detail in the scene, or increase the limit in the options." )
.arg( memLimit ), Qgis::MessageLevel::Warning );
mGpuMemoryLimitReachedReported = true;
}
4 changes: 4 additions & 0 deletions src/app/3d/qgs3dmapcanvaswidget.h
Expand Up @@ -37,6 +37,7 @@ class Qgs3DMapToolIdentify;
class Qgs3DMapToolMeasureLine;
class QgsMapCanvas;
class QgsDockableWidgetHelper;
class QgsMessageBar;
class QgsRubberBand;

class APP_EXPORT Qgs3DMapCanvasWidget : public QWidget
Expand Down Expand Up @@ -92,6 +93,7 @@ class APP_EXPORT Qgs3DMapCanvasWidget : public QWidget
void onViewed2DExtentFrom3DChanged( QVector<QgsPointXY> extent );
void onViewFrustumVisualizationEnabledChanged();
void onExtentChanged();
void onGpuMemoryLimitReached();

private:
QString mCanvasName;
Expand Down Expand Up @@ -121,6 +123,8 @@ class APP_EXPORT Qgs3DMapCanvasWidget : public QWidget
QObjectUniquePtr< QgsRubberBand > mViewFrustumHighlight;
QObjectUniquePtr< QgsRubberBand > mViewExtentHighlight;
QPointer<QDialog> mConfigureDialog;
QgsMessageBar *mMessageBar = nullptr;
bool mGpuMemoryLimitReachedReported = false;
};

#endif // QGS3DMAPCANVASWIDGET_H
4 changes: 4 additions & 0 deletions src/app/3d/qgs3doptions.cpp
Expand Up @@ -55,6 +55,8 @@ Qgs3DOptionsWidget::Qgs3DOptionsWidget( QWidget *parent )

mCameraMovementSpeed->setValue( settings.value( QStringLiteral( "map3d/defaultMovementSpeed" ), 5, QgsSettings::App ).toDouble() );
spinCameraFieldOfView->setValue( settings.value( QStringLiteral( "map3d/defaultFieldOfView" ), 45, QgsSettings::App ).toInt() );

mGpuMemoryLimit->setValue( settings.value( QStringLiteral( "map3d/gpuMemoryLimit" ), 500.0, QgsSettings::App ).toDouble() );
}

void Qgs3DOptionsWidget::apply()
Expand All @@ -65,6 +67,8 @@ void Qgs3DOptionsWidget::apply()
settings.setValue( QStringLiteral( "map3d/defaultProjection" ), static_cast< Qt3DRender::QCameraLens::ProjectionType >( cboCameraProjectionType->currentData().toInt() ), QgsSettings::App );
settings.setValue( QStringLiteral( "map3d/defaultMovementSpeed" ), mCameraMovementSpeed->value(), QgsSettings::App );
settings.setValue( QStringLiteral( "map3d/defaultFieldOfView" ), spinCameraFieldOfView->value(), QgsSettings::App );

settings.setValue( QStringLiteral( "map3d/gpuMemoryLimit" ), mGpuMemoryLimit->value(), QgsSettings::App );
}


Expand Down

0 comments on commit 859a851

Please sign in to comment.