Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #4091 from nyalldawson/render_cache
QgsMapRendererCache improvements
  • Loading branch information
nyalldawson committed Feb 1, 2017
2 parents 54d8d11 + c84ea71 commit f720106
Show file tree
Hide file tree
Showing 10 changed files with 374 additions and 82 deletions.
8 changes: 8 additions & 0 deletions doc/api_break.dox
Expand Up @@ -1335,6 +1335,14 @@ be returned instead of a null pointer if no transformation is required.
- destinationSrsChanged() was renamed to destinationCrsChanged()
- getCompositionMode(), getBlendModeEnum() and BlendMode enum have been moved to QgsPainting utility class.

QgsMapRendererCache {#qgis_api_break_3_0_QgsMapRendererCache}
-------------------

- All protected members have been made private. This class is not designed to be subclassed.
- setCacheImage no longer uses layer IDs for cache keys. Cache keys can now be any arbitrary string.
A new parameter for setCacheImage is used to list all layers on which the cache image is dependent. This
allows for cache images which have either no layer dependencies or multiple layer dependencies.


QgsMapRendererJob {#qgis_api_break_3_0_QgsMapRendererJob}
-----------------
Expand Down
31 changes: 4 additions & 27 deletions python/core/qgsmaprenderercache.sip
@@ -1,15 +1,3 @@

/**
* This class is responsible for keeping cache of rendered images of individual layers.
*
* Once a layer has rendered image stored in the cache (using setCacheImage(...)),
* the cache listens to repaintRequested() signals from layer. If triggered, the cache
* removes the rendered image (and disconnects from the layer).
*
* The class is thread-safe (multiple classes can access the same instance safely).
*
* @note added in 2.4
*/
class QgsMapRendererCache : QObject
{
%TypeHeaderCode
Expand All @@ -20,27 +8,16 @@ class QgsMapRendererCache : QObject

QgsMapRendererCache();

//! invalidate the cache contents
void clear();

//! initialize cache: set new parameters and erase cache if parameters have changed
//! @return flag whether the parameters are the same as last time
bool init( const QgsRectangle& extent, double scale );

//! set cached image for the specified layer ID
void setCacheImage( const QString& layerId, const QImage& img );
void setCacheImage( const QString& cacheKey, const QImage& image, const QList< QgsMapLayer* >& dependentLayers = QList< QgsMapLayer* >() );

//! get cached image for the specified layer ID. Returns null image if it is not cached.
QImage cacheImage( const QString& layerId );
bool hasCacheImage( const QString& cacheKey ) const;

//! remove layer from the cache
void clearCacheImage( const QString& layerId );
QImage cacheImage( const QString& cacheKey ) const;

protected slots:
//! remove layer (that emitted the signal) from the cache
void layerRequestedRepaint();
void clearCacheImage( const QString& cacheKey );

protected:
//! invalidate cache contents (without locking)
void clearInternal();
};
5 changes: 5 additions & 0 deletions src/core/qgsmaplayerlistutils.h
Expand Up @@ -93,6 +93,11 @@ inline static QgsMapLayer* _qgis_findLayer( const QList< QgsMapLayer*> layers, c
}
}

inline uint qHash( const QPointer< QgsMapLayer >& key )
{
return qHash( key ? key->id() : QString() );
}

///@endcond

#endif // QGSMAPLAYERLISTUTILS_H
102 changes: 79 additions & 23 deletions src/core/qgsmaprenderercache.cpp
Expand Up @@ -15,8 +15,8 @@

#include "qgsmaprenderercache.h"

#include "qgsproject.h"
#include "qgsmaplayer.h"
#include "qgsmaplayerlistutils.h"

QgsMapRendererCache::QgsMapRendererCache()
{
Expand All @@ -35,16 +35,45 @@ void QgsMapRendererCache::clearInternal()
mScale = 0;

// make sure we are disconnected from all layers
QMap<QString, QImage>::const_iterator it = mCachedImages.constBegin();
for ( ; it != mCachedImages.constEnd(); ++it )
Q_FOREACH ( const QPointer< QgsMapLayer >& layer, mConnectedLayers )
{
QgsMapLayer* layer = QgsProject::instance()->mapLayer( it.key() );
if ( layer )
if ( layer.data() )
{
disconnect( layer, &QgsMapLayer::repaintRequested, this, &QgsMapRendererCache::layerRequestedRepaint );
disconnect( layer.data(), &QgsMapLayer::repaintRequested, this, &QgsMapRendererCache::layerRequestedRepaint );
}
}
mCachedImages.clear();
mConnectedLayers.clear();
}

void QgsMapRendererCache::dropUnusedConnections()
{
QSet< QPointer< QgsMapLayer > > stillDepends = dependentLayers();
QSet< QPointer< QgsMapLayer > > disconnects = mConnectedLayers.subtract( stillDepends );
Q_FOREACH ( const QPointer< QgsMapLayer >& layer, disconnects )
{
if ( layer.data() )
{
disconnect( layer.data(), &QgsMapLayer::repaintRequested, this, &QgsMapRendererCache::layerRequestedRepaint );
}
}

mConnectedLayers = stillDepends;
}

QSet<QPointer<QgsMapLayer> > QgsMapRendererCache::dependentLayers() const
{
QSet< QPointer< QgsMapLayer > > result;
QMap<QString, CacheParameters>::const_iterator it = mCachedImages.constBegin();
for ( ; it != mCachedImages.constEnd(); ++it )
{
Q_FOREACH ( const QPointer< QgsMapLayer >& l, it.value().dependentLayers )
{
if ( l.data() )
result << l;
}
}
return result;
}

bool QgsMapRendererCache::init( const QgsRectangle& extent, double scale )
Expand All @@ -65,41 +94,68 @@ bool QgsMapRendererCache::init( const QgsRectangle& extent, double scale )
return false;
}

void QgsMapRendererCache::setCacheImage( const QString& layerId, const QImage& img )
void QgsMapRendererCache::setCacheImage( const QString& cacheKey, const QImage& image, const QList<QgsMapLayer*>& dependentLayers )
{
QMutexLocker lock( &mMutex );
mCachedImages[layerId] = img;

CacheParameters params;
params.cachedImage = image;

// connect to the layer to listen to layer's repaintRequested() signals
QgsMapLayer* layer = QgsProject::instance()->mapLayer( layerId );
if ( layer )
Q_FOREACH ( QgsMapLayer* layer, dependentLayers )
{
connect( layer, &QgsMapLayer::repaintRequested, this, &QgsMapRendererCache::layerRequestedRepaint );
if ( layer )
{
params.dependentLayers << layer;
if ( !mConnectedLayers.contains( QPointer< QgsMapLayer >( layer ) ) )
{
connect( layer, &QgsMapLayer::repaintRequested, this, &QgsMapRendererCache::layerRequestedRepaint );
mConnectedLayers << layer;
}
}
}

mCachedImages[cacheKey] = params;
}

QImage QgsMapRendererCache::cacheImage( const QString& layerId )
bool QgsMapRendererCache::hasCacheImage( const QString& cacheKey ) const
{
return mCachedImages.contains( cacheKey );
}

QImage QgsMapRendererCache::cacheImage( const QString& cacheKey ) const
{
QMutexLocker lock( &mMutex );
return mCachedImages.value( layerId );
return mCachedImages.value( cacheKey ).cachedImage;
}

void QgsMapRendererCache::layerRequestedRepaint()
{
QgsMapLayer* layer = qobject_cast<QgsMapLayer*>( sender() );
if ( layer )
clearCacheImage( layer->id() );
}
if ( !layer )
return;

void QgsMapRendererCache::clearCacheImage( const QString& layerId )
{
QMutexLocker lock( &mMutex );

mCachedImages.remove( layerId );

QgsMapLayer* layer = QgsProject::instance()->mapLayer( layerId );
if ( layer )
// check through all cached images to clear any which depend on this layer
QMap<QString, CacheParameters>::iterator it = mCachedImages.begin();
for ( ; it != mCachedImages.end(); )
{
disconnect( layer, &QgsMapLayer::repaintRequested, this, &QgsMapRendererCache::layerRequestedRepaint );
if ( !it.value().dependentLayers.contains( layer ) )
{
++it;
continue;
}

it = mCachedImages.erase( it );
}
dropUnusedConnections();
}

void QgsMapRendererCache::clearCacheImage( const QString& cacheKey )
{
QMutexLocker lock( &mMutex );

mCachedImages.remove( cacheKey );
dropUnusedConnections();
}
93 changes: 70 additions & 23 deletions src/core/qgsmaprenderercache.h
Expand Up @@ -22,14 +22,17 @@
#include <QMutex>

#include "qgsrectangle.h"
#include "qgsmaplayer.h"


/** \ingroup core
* This class is responsible for keeping cache of rendered images of individual layers.
* This class is responsible for keeping cache of rendered images resulting from
* a map rendering job.
*
* Once a layer has rendered image stored in the cache (using setCacheImage(...)),
* the cache listens to repaintRequested() signals from layer. If triggered, the cache
* removes the rendered image (and disconnects from the layer).
* Once a job has a rendered image stored in the cache (using setCacheImage(...)),
* the cache listens to repaintRequested() signals from dependent layers.
* If triggered, the cache removes the rendered image (and disconnects from the
* layers).
*
* The class is thread-safe (multiple classes can access the same instance safely).
*
Expand All @@ -42,35 +45,79 @@ class CORE_EXPORT QgsMapRendererCache : public QObject

QgsMapRendererCache();

//! invalidate the cache contents
/**
* Invalidates the cache contents, clearing all cached images.
* @see clearCacheImage()
*/
void clear();

//! initialize cache: set new parameters and erase cache if parameters have changed
//! @return flag whether the parameters are the same as last time
/**
* Initialize cache: set new parameters and clears the cache if any
* parameters have changed since last initialization.
* @return flag whether the parameters are the same as last time
*/
bool init( const QgsRectangle& extent, double scale );

//! set cached image for the specified layer ID
void setCacheImage( const QString& layerId, const QImage& img );

//! get cached image for the specified layer ID. Returns null image if it is not cached.
QImage cacheImage( const QString& layerId );
/**
* Set the cached \a image for a particular \a cacheKey. The \a cacheKey usually
* matches the QgsMapLayer::id() which the image is a render of.
* A list of \a dependentLayers should be passed containing all layer
* on which this cache image is dependent. If any of these layers triggers a
* repaint then the cache image will be cleared.
* @see cacheImage()
*/
void setCacheImage( const QString& cacheKey, const QImage& image, const QList< QgsMapLayer* >& dependentLayers = QList< QgsMapLayer* >() );

/**
* Returns true if the cache contains an image with the specified \a cacheKey.
* @note added in QGIS 3.0
* @see cacheImage()
*/
bool hasCacheImage( const QString& cacheKey ) const;

/**
* Returns the cached image for the specified \a cacheKey. The \a cacheKey usually
* matches the QgsMapLayer::id() which the image is a render of.
* Returns a null image if it is not cached.
* @see setCacheImage()
* @see hasCacheImage()
*/
QImage cacheImage( const QString& cacheKey ) const;

/**
* Removes an image from the cache with matching \a cacheKey.
* @see clear()
*/
void clearCacheImage( const QString& cacheKey );

private slots:
//! Remove layer (that emitted the signal) from the cache
void layerRequestedRepaint();

//! remove layer from the cache
void clearCacheImage( const QString& layerId );
private:

protected slots:
//! remove layer (that emitted the signal) from the cache
void layerRequestedRepaint();
struct CacheParameters
{
QImage cachedImage;
QList< QPointer< QgsMapLayer > > dependentLayers;
};

protected:
//! invalidate cache contents (without locking)
//! Invalidate cache contents (without locking)
void clearInternal();

protected:
QMutex mMutex;
//! Disconnects from layers we no longer care about
void dropUnusedConnections();

QSet< QPointer< QgsMapLayer > > dependentLayers() const;

mutable QMutex mMutex;
QgsRectangle mExtent;
double mScale;
QMap<QString, QImage> mCachedImages;
double mScale = 0;

//! Map of cache key to cache parameters
QMap<QString, CacheParameters> mCachedImages;
//! List of all layers on which this cache is currently connected
QSet< QPointer< QgsMapLayer > > mConnectedLayers;
};


Expand Down
12 changes: 6 additions & 6 deletions src/core/qgsmaprendererjob.cpp
Expand Up @@ -243,7 +243,7 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter* painter, QgsLabelingEn
{
job.opacity = 1.0 - vl->layerTransparency() / 100.0;
}
job.layerId = ml->id();
job.layer = ml;
job.renderingTime = -1;

job.context = QgsRenderContext::fromMapSettings( mSettings );
Expand All @@ -257,7 +257,7 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter* painter, QgsLabelingEn
job.context.setFeatureFilterProvider( mFeatureFilterProvider );

// if we can use the cache, let's do it and avoid rendering!
if ( mCache && !mCache->cacheImage( ml->id() ).isNull() )
if ( mCache && mCache->hasCacheImage( ml->id() ) )
{
job.cached = true;
job.img = new QImage( mCache->cacheImage( ml->id() ) );
Expand Down Expand Up @@ -323,10 +323,10 @@ void QgsMapRendererJob::cleanupJobs( LayerRenderJobs& jobs )
delete job.context.painter();
job.context.setPainter( nullptr );

if ( mCache && !job.cached && !job.context.renderingStopped() )
if ( mCache && !job.cached && !job.context.renderingStopped() && job.layer )
{
QgsDebugMsg( "caching image for " + job.layerId );
mCache->setCacheImage( job.layerId, *job.img );
QgsDebugMsg( "caching image for " + ( job.layer ? job.layer->id() : QString() ) );
mCache->setCacheImage( job.layer->id(), *job.img, QList< QgsMapLayer* >() << job.layer );
}

delete job.img;
Expand Down Expand Up @@ -380,7 +380,7 @@ void QgsMapRendererJob::logRenderingTime( const LayerRenderJobs& jobs )

QMultiMap<int, QString> elapsed;
Q_FOREACH ( const LayerRenderJob& job, jobs )
elapsed.insert( job.renderingTime, job.layerId );
elapsed.insert( job.renderingTime, job.layer ? job.layer->id() : QString() );

QList<int> tt( elapsed.uniqueKeys() );
qSort( tt.begin(), tt.end(), qGreater<int>() );
Expand Down
2 changes: 1 addition & 1 deletion src/core/qgsmaprendererjob.h
Expand Up @@ -50,7 +50,7 @@ struct LayerRenderJob
QPainter::CompositionMode blendMode;
double opacity;
bool cached; // if true, img already contains cached image from previous rendering
QString layerId;
QPointer< QgsMapLayer > layer;
int renderingTime; //!< Time it took to render the layer in ms (it is -1 if not rendered or still rendering)
};

Expand Down

0 comments on commit f720106

Please sign in to comment.