Skip to content
Permalink
Browse files

Reworked background loading and updating of chunks

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 Jul 31, 2017
1 parent 905222d commit 489a21d06412b00f739316c253174d1bce5a750b
@@ -46,6 +46,7 @@ SET(QGIS_3D_MOC_HDRS
scene.h

chunks/chunkedentity.h
chunks/chunkloader.h

terrain/demterraingenerator.h
terrain/demterraintilegeometry.h
@@ -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;
@@ -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;
@@ -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 )
@@ -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 )
{
@@ -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;
}
@@ -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>
@@ -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
@@ -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.
You can’t perform that action at this time.