Skip to content

Commit

Permalink
Reworked background loading and updating of chunks
Browse files Browse the repository at this point in the history
Before there was a dedicated thread for loading and it was not great because map rendering
requests were started from worker thread which is not potentially dangerous. Updates of map
were done in main thread, blocking user interface - ugly!

Now everything is handled more cleanly - there is one queue of jobs (two types: load chunk / update chunk),
things are started asynchronously from main thread. No dedicated thread / mutex / wait condition.
Better management of states of chunk nodes - state changes are very explicit which is a good thing.
  • Loading branch information
wonder-sk committed Sep 15, 2017
1 parent 905222d commit 489a21d
Show file tree
Hide file tree
Showing 18 changed files with 645 additions and 231 deletions.
1 change: 1 addition & 0 deletions src/3d/CMakeLists.txt
Expand Up @@ -46,6 +46,7 @@ SET(QGIS_3D_MOC_HDRS
scene.h

chunks/chunkedentity.h
chunks/chunkloader.h

terrain/demterraingenerator.h
terrain/demterraintilegeometry.h
Expand Down
199 changes: 122 additions & 77 deletions src/3d/chunks/chunkedentity.cpp
Expand Up @@ -91,33 +91,28 @@ ChunkedEntity::ChunkedEntity( const AABB &rootBbox, float rootError, float tau,
rootNode = new ChunkNode( 0, 0, 0, rootBbox, rootError );
chunkLoaderQueue = new ChunkList;
replacementQueue = new ChunkList;

loaderThread = new LoaderThread( chunkLoaderQueue, loaderMutex, loaderWaitCondition );
connect( loaderThread, &LoaderThread::nodeLoaded, this, &ChunkedEntity::onNodeLoaded );
loaderThread->start();
}


ChunkedEntity::~ChunkedEntity()
{
loaderThread->setStopping( true );
loaderWaitCondition.wakeOne(); // may be waiting
loaderThread->wait();
delete loaderThread;
// derived classes have to make sure that any pending active job has finished / been cancelled
// before getting to this destructor - here it would be too late to cancel them
// (e.g. objects required for loading/updating have been deleted already)
Q_ASSERT( !activeJob );

// clean up any pending load requests
while ( !chunkLoaderQueue->isEmpty() )
{
ChunkListEntry *entry = chunkLoaderQueue->takeFirst();
ChunkNode *node = entry->chunk;

delete entry;
delete node->loader;

// unload node that is in "loading" state
node->state = ChunkNode::Skeleton;
node->loaderQueueEntry = nullptr;
node->loader = nullptr;
if ( node->state == ChunkNode::QueuedForLoad )
node->cancelQueuedForLoad();
else if ( node->state == ChunkNode::QueuedForUpdate )
node->cancelQueuedForUpdate();
else
Q_ASSERT( false ); // impossible!
}

delete chunkLoaderQueue;
Expand All @@ -137,9 +132,13 @@ ChunkedEntity::~ChunkedEntity()
//delete chunkLoaderFactory;
}

#include <QElapsedTimer>

void ChunkedEntity::update( const SceneState &state )
{
QElapsedTimer t;
t.start();

QSet<ChunkNode *> activeBefore = QSet<ChunkNode *>::fromList( activeNodes );
activeNodes.clear();
frustumCulled = 0;
Expand Down Expand Up @@ -184,9 +183,13 @@ void ChunkedEntity::update( const SceneState &state )
bboxesEntity->setBoxes( bboxes );
}

// start a job from queue if there is anything waiting
if ( !activeJob )
startJob();

needsUpdate = false; // just updated

qDebug() << "update: active " << activeNodes.count() << " enabled " << enabled << " disabled " << disabled << " | culled " << frustumCulled << " | loading " << chunkLoaderQueue->count() << " loaded " << replacementQueue->count() << " | unloaded " << unloaded;
qDebug() << "update: active " << activeNodes.count() << " enabled " << enabled << " disabled " << disabled << " | culled " << frustumCulled << " | loading " << chunkLoaderQueue->count() << " loaded " << replacementQueue->count() << " | unloaded " << unloaded << " elapsed " << t.elapsed() << "ms";
}

void ChunkedEntity::setShowBoundingBoxes( bool enabled )
Expand All @@ -205,6 +208,32 @@ void ChunkedEntity::setShowBoundingBoxes( bool enabled )
}
}

void ChunkedEntity::updateNodes( const QList<ChunkNode *> &nodes, ChunkQueueJobFactory *updateJobFactory )
{
Q_FOREACH ( ChunkNode *node, nodes )
{
if ( node->state == ChunkNode::QueuedForUpdate )
{
chunkLoaderQueue->takeEntry( node->loaderQueueEntry );
node->cancelQueuedForUpdate();
}
else if ( node->state == ChunkNode::Updating )
{
cancelActiveJob(); // we have currently just one active job so that must be it
}

Q_ASSERT( node->state == ChunkNode::Loaded );

ChunkListEntry *entry = new ChunkListEntry( node );
node->setQueuedForUpdate( entry, updateJobFactory );
chunkLoaderQueue->insertLast( entry );
}

// trigger update
if ( !activeJob )
startJob();
}


void ChunkedEntity::update( ChunkNode *node, const SceneState &state )
{
Expand Down Expand Up @@ -260,108 +289,124 @@ void ChunkedEntity::update( ChunkNode *node, const SceneState &state )

void ChunkedEntity::requestResidency( ChunkNode *node )
{
if ( node->state == ChunkNode::Loaded )
if ( node->state == ChunkNode::Loaded || node->state == ChunkNode::QueuedForUpdate || node->state == ChunkNode::Updating )
{
Q_ASSERT( node->replacementQueueEntry );
Q_ASSERT( node->entity );
replacementQueue->takeEntry( node->replacementQueueEntry );
replacementQueue->insertFirst( node->replacementQueueEntry );
}
else if ( node->state == ChunkNode::Loading )
else if ( node->state == ChunkNode::QueuedForLoad )
{
// move to the front of loading queue
loaderMutex.lock();
Q_ASSERT( node->loaderQueueEntry );
Q_ASSERT( node->loader );
Q_ASSERT( !node->loader );
if ( node->loaderQueueEntry->prev || node->loaderQueueEntry->next )
{
chunkLoaderQueue->takeEntry( node->loaderQueueEntry );
chunkLoaderQueue->insertFirst( node->loaderQueueEntry );
}
else
{
// the entry is being currently processed by the loading thread
// (or it is at the head of 1-entry list)
}
loaderMutex.unlock();
}
else if ( node->state == ChunkNode::Loading )
{
// the entry is being currently processed - nothing to do really
}
else if ( node->state == ChunkNode::Skeleton )
{
// add to the loading queue
loaderMutex.lock();
ChunkListEntry *entry = new ChunkListEntry( node );
node->setLoading( chunkLoaderFactory->createChunkLoader( node ), entry );
node->setQueuedForLoad( entry );
chunkLoaderQueue->insertFirst( entry );
if ( chunkLoaderQueue->count() == 1 )
loaderWaitCondition.wakeOne();
loaderMutex.unlock();
}
else
Q_ASSERT( false && "impossible!" );
}

void ChunkedEntity::onNodeLoaded( ChunkNode *node )

void ChunkedEntity::onActiveJobFinished()
{
Qt3DCore::QEntity *entity = node->loader->createEntity( this );
ChunkQueueJob *job = qobject_cast<ChunkQueueJob *>( sender() );
Q_ASSERT( job );
Q_ASSERT( job == activeJob );

loaderMutex.lock();
ChunkListEntry *entry = node->loaderQueueEntry;
ChunkNode *node = job->chunk();

// load into node (should be in main thread again)
node->setLoaded( entity, entry );
loaderMutex.unlock();
if ( ChunkLoader *loader = qobject_cast<ChunkLoader *>( job ) )
{
Q_ASSERT( node->state == ChunkNode::Loading );
Q_ASSERT( node->loader == loader );

replacementQueue->insertFirst( entry );
// mark as loaded + create entity
Qt3DCore::QEntity *entity = node->loader->createEntity( this );

// now we need an update!
needsUpdate = true;
}
// load into node (should be in main thread again)
node->setLoaded( entity );

replacementQueue->insertFirst( node->replacementQueueEntry );

// -------
// now we need an update!
needsUpdate = true;
}
else
{
Q_ASSERT( node->state == ChunkNode::Updating );
node->setUpdated();
}

// cleanup the job that has just finished
activeJob->deleteLater();
activeJob = nullptr;

LoaderThread::LoaderThread( ChunkList *list, QMutex &mutex, QWaitCondition &waitCondition )
: loadList( list )
, mutex( mutex )
, waitCondition( waitCondition )
, stopping( false )
{
// start another job - if any
startJob();
}

void LoaderThread::run()
void ChunkedEntity::startJob()
{
while ( 1 )
{
ChunkListEntry *entry = nullptr;
mutex.lock();
if ( loadList->isEmpty() )
waitCondition.wait( &mutex );

// we can get woken up also when we need to stop
if ( stopping )
{
mutex.unlock();
break;
}

Q_ASSERT( !loadList->isEmpty() );
entry = loadList->takeFirst();
mutex.unlock();
Q_ASSERT( !activeJob );
if ( chunkLoaderQueue->isEmpty() )
return;

qDebug() << "[THR] loading! " << entry->chunk->x << " | " << entry->chunk->y << " | " << entry->chunk->z;
ChunkListEntry *entry = chunkLoaderQueue->takeFirst();
Q_ASSERT( entry );
ChunkNode *node = entry->chunk;
delete entry;

entry->chunk->loader->load();
if ( node->state == ChunkNode::QueuedForLoad )
{
ChunkLoader *loader = chunkLoaderFactory->createChunkLoader( node );
connect( loader, &ChunkQueueJob::finished, this, &ChunkedEntity::onActiveJobFinished );
node->setLoading( loader );
activeJob = loader;
}
else if ( node->state == ChunkNode::QueuedForUpdate )
{
node->setUpdating();
connect( node->updater, &ChunkQueueJob::finished, this, &ChunkedEntity::onActiveJobFinished );
activeJob = node->updater;
}
else
Q_ASSERT( false ); // not possible
}

qDebug() << "[THR] done!";
void ChunkedEntity::cancelActiveJob()
{
Q_ASSERT( activeJob );

emit nodeLoaded( entry->chunk );
ChunkNode *node = activeJob->chunk();

if ( stopping )
{
// this chunk we just processed will not be processed anymore because we are shutting down everything
// so at least put it back into the loader queue so that we can clean up the chunk
loadList->insertFirst( entry );
}
if ( qobject_cast<ChunkLoader *>( activeJob ) )
{
// return node back to skeleton
node->cancelLoading();
}
else
{
// return node back to loaded state
node->cancelUpdating();
}

activeJob->cancel();
activeJob->deleteLater();
activeJob = nullptr;
}
44 changes: 14 additions & 30 deletions src/3d/chunks/chunkedentity.h
Expand Up @@ -2,15 +2,14 @@
#define CHUNKEDENTITY_H

#include <Qt3DCore/QEntity>
#include <QMutex>
#include <QWaitCondition>

class AABB;
class ChunkNode;
class ChunkList;
class ChunkQueueJob;
class ChunkLoaderFactory;
class ChunkBoundsEntity;
class LoaderThread;
class ChunkQueueJobFactory;

#include <QVector3D>
#include <QMatrix4x4>
Expand Down Expand Up @@ -43,16 +42,24 @@ class ChunkedEntity : public Qt3DCore::QEntity

void setShowBoundingBoxes( bool enabled );

//! update already loaded nodes (add to the queue)
void updateNodes( const QList<ChunkNode *> &nodes, ChunkQueueJobFactory *updateJobFactory );

protected:
void cancelActiveJob();

private:
void update( ChunkNode *node, const SceneState &state );

//! make sure that the chunk will be loaded soon (if not loaded yet) and not unloaded anytime soon (if loaded already)
void requestResidency( ChunkNode *node );

void startJob();

private slots:
void onNodeLoaded( ChunkNode *node );
void onActiveJobFinished();

private:
protected:
//! root node of the quadtree hierarchy
ChunkNode *rootNode;
//! max. allowed screen space error
Expand All @@ -78,32 +85,9 @@ class ChunkedEntity : public Qt3DCore::QEntity

ChunkBoundsEntity *bboxesEntity;

LoaderThread *loaderThread;
QMutex loaderMutex;
QWaitCondition loaderWaitCondition;
//! job that is currently being processed (asynchronously in a worker thread)
ChunkQueueJob *activeJob = nullptr;
};


#include <QThread>

class LoaderThread : public QThread
{
Q_OBJECT
public:
LoaderThread( ChunkList *list, QMutex &mutex, QWaitCondition &waitCondition );

void setStopping( bool stop ) { stopping = stop; }

void run() override;

signals:
void nodeLoaded( ChunkNode *node );

private:
ChunkList *loadList;
QMutex &mutex;
QWaitCondition &waitCondition;
bool stopping;
};

#endif // CHUNKEDENTITY_H

0 comments on commit 489a21d

Please sign in to comment.