26#include <QElapsedTimer>
31#include "moc_qgschunkedentity.cpp"
33using namespace Qt::StringLiterals;
38static float screenSpaceError(
const QgsAABB &nodeBbox,
float nodeError,
const QgsChunkedEntity::SceneContext &sceneContext )
52static bool hasAnyActiveChildren( QgsChunkNode *node, QList<QgsChunkNode *> &activeNodes )
54 for (
int i = 0; i < node->childCount(); ++i )
56 QgsChunkNode *child = node->children()[i];
57 if ( child->entity() && activeNodes.contains( child ) )
59 if ( hasAnyActiveChildren( child, activeNodes ) )
66QgsChunkedEntity::QgsChunkedEntity(
Qgs3DMapSettings *mapSettings,
float tau, QgsChunkLoaderFactory *loaderFactory,
bool ownsFactory,
int primitiveBudget, Qt3DCore::QNode *parent )
67 : Qgs3DMapSceneEntity( mapSettings, parent )
69 , mChunkLoaderFactory( loaderFactory )
70 , mOwnsFactory( ownsFactory )
71 , mPrimitivesBudget( primitiveBudget )
73 mRootNode = loaderFactory->createRootNode();
74 mChunkLoaderQueue =
new QgsChunkList;
75 mReplacementQueue =
new QgsChunkList;
78 connect( loaderFactory, &QgsChunkLoaderFactory::childrenPrepared,
this, [
this] {
79 setNeedsUpdate(
true );
80 emit pendingJobsCountChanged();
85QgsChunkedEntity::~QgsChunkedEntity()
90 Q_ASSERT( mActiveJobs.isEmpty() );
93 while ( !mChunkLoaderQueue->isEmpty() )
95 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
96 QgsChunkNode *node = entry->chunk;
98 if ( node->state() == QgsChunkNode::QueuedForLoad )
99 node->cancelQueuedForLoad();
100 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
101 node->cancelQueuedForUpdate();
106 delete mChunkLoaderQueue;
108 while ( !mReplacementQueue->isEmpty() )
110 QgsChunkListEntry *entry = mReplacementQueue->takeFirst();
113 entry->chunk->unloadChunk();
116 delete mReplacementQueue;
121 delete mChunkLoaderFactory;
126void QgsChunkedEntity::handleSceneUpdate(
const SceneContext &sceneContext )
136 pruneLoaderQueue( sceneContext );
141 int oldJobsCount = pendingJobsCount();
143 QSet<QgsChunkNode *> activeBefore = qgis::listToSet( mActiveNodes );
144 mActiveNodes.clear();
146 mCurrentTime = QTime::currentTime();
148 update( mRootNode, sceneContext );
151 int enabled = 0, disabled = 0, unloaded = 0;
154 for ( QgsChunkNode *node : std::as_const( mActiveNodes ) )
156 if ( activeBefore.contains( node ) )
158 activeBefore.remove( node );
162 if ( !node->entity() )
164 QgsDebugError(
"Active node has null entity - this should never happen!" );
167 node->entity()->setEnabled(
true );
170 const QList<QgsGeoTransform *> transforms = node->entity()->findChildren<QgsGeoTransform *>();
171 for ( QgsGeoTransform *transform : transforms )
173 transform->setOrigin( mMapSettings->origin() );
183 for ( QgsChunkNode *node : activeBefore )
185 if ( !node->entity() )
187 QgsDebugError(
"Active node has null entity - this should never happen!" );
190 node->entity()->setEnabled(
false );
199 unloaded = unloadNodes();
206 QList<QgsBox3D> bboxes;
207 for ( QgsChunkNode *n : std::as_const( mActiveNodes ) )
208 bboxes << n->box3D();
209 mBboxesEntity->setBoxes( bboxes );
215 mNeedsUpdate =
false;
217 if ( pendingJobsCount() != oldJobsCount )
218 emit pendingJobsCountChanged();
220 QgsDebugMsgLevel( u
"update: active %1 enabled %2 disabled %3 | culled %4 | loading %5 loaded %6 | unloaded %7 elapsed %8ms"_s.arg( mActiveNodes.count() ).arg( enabled ).arg( disabled ).arg( mFrustumCulled ).arg( mChunkLoaderQueue->count() ).arg( mReplacementQueue->count() ).arg( unloaded ).arg( t.elapsed() ), 2 );
224int QgsChunkedEntity::unloadNodes()
227 if ( usedGpuMemory <= mGpuMemoryLimit )
229 setHasReachedGpuMemoryLimit(
false );
233 QgsDebugMsgLevel( u
"Going to unload nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
239 QgsChunkListEntry *entry = mReplacementQueue->last();
240 while ( entry && usedGpuMemory > mGpuMemoryLimit )
246 if ( entry->chunk->parent() && !hasAnyActiveChildren( entry->chunk->parent(), mActiveNodes ) )
248 QgsChunkListEntry *entryPrev = entry->prev;
249 mReplacementQueue->takeEntry( entry );
251 mActiveNodes.removeOne( entry->chunk );
252 entry->chunk->unloadChunk();
262 if ( usedGpuMemory > mGpuMemoryLimit )
264 setHasReachedGpuMemoryLimit(
true );
265 QgsDebugMsgLevel( u
"Unable to unload enough nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
272QgsRange<float> QgsChunkedEntity::getNearFarPlaneRange(
const QMatrix4x4 &viewMatrix )
const
274 QList<QgsChunkNode *> activeEntityNodes = activeNodes();
279 if ( activeEntityNodes.empty() )
280 activeEntityNodes << rootNode();
285 for ( QgsChunkNode *node : std::as_const( activeEntityNodes ) )
293 fnear = std::min( fnear, bboxfnear );
294 ffar = std::max( ffar, bboxffar );
299void QgsChunkedEntity::setShowBoundingBoxes(
bool enabled )
301 if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
306 mBboxesEntity =
new QgsChunkBoundsEntity( mRootNode->box3D().center(),
this );
310 mBboxesEntity->deleteLater();
311 mBboxesEntity =
nullptr;
315void QgsChunkedEntity::updateNodes(
const QList<QgsChunkNode *> &nodes, QgsChunkQueueJobFactory *updateJobFactory )
317 for ( QgsChunkNode *node : nodes )
319 if ( node->state() == QgsChunkNode::QueuedForUpdate )
321 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
322 node->cancelQueuedForUpdate();
324 else if ( node->state() == QgsChunkNode::Updating )
326 cancelActiveJob( node->updater() );
328 else if ( node->state() == QgsChunkNode::Skeleton || node->state() == QgsChunkNode::QueuedForLoad )
333 else if ( node->state() == QgsChunkNode::Loading )
336 cancelActiveJob( node->loader() );
337 requestResidency( node );
341 Q_ASSERT( node->state() == QgsChunkNode::Loaded );
343 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
344 node->setQueuedForUpdate( entry, updateJobFactory );
345 mChunkLoaderQueue->insertLast( entry );
352void QgsChunkedEntity::pruneLoaderQueue(
const SceneContext &sceneContext )
354 QList<QgsChunkNode *> toRemoveFromLoaderQueue;
359 QgsChunkListEntry *e = mChunkLoaderQueue->first();
362 Q_ASSERT( e->chunk->state() == QgsChunkNode::QueuedForLoad || e->chunk->state() == QgsChunkNode::QueuedForUpdate );
366 toRemoveFromLoaderQueue.append( e->chunk );
372 for ( QgsChunkNode *n : toRemoveFromLoaderQueue )
374 mChunkLoaderQueue->takeEntry( n->loaderQueueEntry() );
375 if ( n->state() == QgsChunkNode::QueuedForLoad )
377 n->cancelQueuedForLoad();
381 n->cancelQueuedForUpdate();
382 mReplacementQueue->takeEntry( n->replacementQueueEntry() );
387 if ( !toRemoveFromLoaderQueue.isEmpty() )
389 QgsDebugMsgLevel( u
"Pruned %1 chunks in loading queue"_s.arg( toRemoveFromLoaderQueue.count() ), 2 );
394int QgsChunkedEntity::pendingJobsCount()
const
396 return mChunkLoaderQueue->count() + mActiveJobs.count();
399struct ResidencyRequest
401 QgsChunkNode *node =
nullptr;
404 ResidencyRequest() =
default;
418 bool operator()(
const ResidencyRequest &request,
const ResidencyRequest &otherRequest )
const
420 if ( request.level == otherRequest.level )
421 return request.dist > otherRequest.dist;
422 return request.level > otherRequest.level;
424} ResidencyRequestSorter;
426void QgsChunkedEntity::update( QgsChunkNode *root,
const SceneContext &sceneContext )
428 QSet<QgsChunkNode *> nodes;
429 QVector<ResidencyRequest> residencyRequests;
431 using slotItem = std::pair<QgsChunkNode *, float>;
432 auto cmp_funct = [](
const slotItem &p1,
const slotItem &p2 ) {
433 return p1.second <= p2.second;
435 int renderedCount = 0;
436 std::priority_queue<slotItem, std::vector<slotItem>,
decltype( cmp_funct )> pq( cmp_funct );
438 pq.push( std::make_pair( root, screenSpaceError( rootBbox, root->error(), sceneContext ) ) );
439 while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
441 slotItem s = pq.top();
443 QgsChunkNode *node = s.first;
453 if ( !node->hasChildrenPopulated() )
463 if ( mChunkLoaderFactory->canCreateChildren( node ) )
465 node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
469 mChunkLoaderFactory->prepareChildren( node );
475 double dist = bbox.
center().distanceToPoint( sceneContext.cameraPos );
476 residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
478 if ( !node->entity() && node->hasData() )
483 bool becomesActive =
false;
486 if ( node->childCount() == 0 )
490 becomesActive =
true;
492 else if ( mTau > 0 && screenSpaceError( bbox, node->error(), sceneContext ) <= mTau && node->hasData() )
495 becomesActive =
true;
509 becomesActive =
true;
511 QgsChunkNode *
const *children = node->children();
512 for (
int i = 0; i < node->childCount(); ++i )
515 if ( children[i]->entity() || !children[i]->hasData() )
518 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
526 double dist = childBbox.
center().distanceToPoint( sceneContext.cameraPos );
527 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
536 if ( node->allChildChunksResident( mCurrentTime ) )
538 QgsChunkNode *
const *children = node->children();
539 for (
int i = 0; i < node->childCount(); ++i )
542 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
547 becomesActive =
true;
549 QgsChunkNode *
const *children = node->children();
550 for (
int i = 0; i < node->childCount(); ++i )
553 double dist = childBbox.
center().distanceToPoint( sceneContext.cameraPos );
554 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
560 if ( becomesActive && node->entity() )
562 mActiveNodes << node;
566 nodes.remove( node->parent() );
567 renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
569 renderedCount += mChunkLoaderFactory->primitivesCount( node );
570 nodes.insert( node );
575 std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
576 for (
const auto &request : residencyRequests )
577 requestResidency( request.node );
580void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
582 if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
584 Q_ASSERT( node->replacementQueueEntry() );
585 Q_ASSERT( node->entity() );
586 mReplacementQueue->takeEntry( node->replacementQueueEntry() );
587 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
589 else if ( node->state() == QgsChunkNode::QueuedForLoad )
592 Q_ASSERT( node->loaderQueueEntry() );
593 Q_ASSERT( !node->loader() );
594 if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
596 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
597 mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
600 else if ( node->state() == QgsChunkNode::Loading )
604 else if ( node->state() == QgsChunkNode::Skeleton )
606 if ( !node->hasData() )
610 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
611 node->setQueuedForLoad( entry );
612 mChunkLoaderQueue->insertFirst( entry );
615 Q_ASSERT(
false &&
"impossible!" );
619void QgsChunkedEntity::onActiveJobFinished()
621 int oldJobsCount = pendingJobsCount();
623 QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
625 Q_ASSERT( mActiveJobs.contains( job ) );
627 QgsChunkNode *node = job->chunk();
629 if ( node->state() == QgsChunkNode::Loading )
631 QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job );
633 Q_ASSERT( node->loader() == loader );
635 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u
"3D"_s, u
"Load "_s + node->tileId().text(), node->tileId().text() );
636 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u
"3D"_s, u
"Load"_s, node->tileId().text() );
638 QgsEventTracing::ScopedEvent e(
"3D", QString(
"create" ) );
640 Qt3DCore::QEntity *entity = node->loader()->createEntity(
this );
650 mActiveNodes << node;
653 node->setLoaded( entity );
655 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
657 emit newEntityCreated( entity );
661 node->setHasData(
false );
662 node->cancelLoading();
670 Q_ASSERT( node->state() == QgsChunkNode::Updating );
676 if ( QgsChunkLoader *nodeUpdater = qobject_cast<QgsChunkLoader *>( node->updater() ) )
678 Qt3DCore::QEntity *newEntity = nodeUpdater->createEntity(
this );
679 node->replaceEntity( newEntity );
680 emit newEntityCreated( newEntity );
683 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u
"3D"_s, u
"Update"_s, node->tileId().text() );
688 mActiveJobs.removeOne( job );
694 if ( pendingJobsCount() != oldJobsCount )
695 emit pendingJobsCountChanged();
698void QgsChunkedEntity::startJobs()
700 while ( mActiveJobs.count() < 4 && !mChunkLoaderQueue->isEmpty() )
702 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
704 QgsChunkNode *node = entry->chunk;
707 QgsChunkQueueJob *job = startJob( node );
708 if ( !job->isFinished() )
709 mActiveJobs.append( job );
713QgsChunkQueueJob *QgsChunkedEntity::startJob( QgsChunkNode *node )
715 if ( node->state() == QgsChunkNode::QueuedForLoad )
717 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, u
"3D"_s, u
"Load"_s, node->tileId().text() );
718 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, u
"3D"_s, u
"Load "_s + node->tileId().text(), node->tileId().text() );
720 QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
721 connect( loader, &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
723 node->setLoading( loader );
726 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
728 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, u
"3D"_s, u
"Update"_s, node->tileId().text() );
731 connect( node->updater(), &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
732 node->updater()->start();
733 return node->updater();
742void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob *job )
746 QgsChunkNode *node = job->chunk();
747 disconnect( job, &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
749 if ( node->state() == QgsChunkNode::Loading )
752 node->cancelLoading();
754 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u
"3D"_s, u
"Load "_s + node->tileId().text(), node->tileId().text() );
755 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u
"3D"_s, u
"Load"_s, node->tileId().text() );
757 else if ( node->state() == QgsChunkNode::Updating )
760 node->cancelUpdating();
762 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u
"3D"_s, u
"Update"_s, node->tileId().text() );
770 mActiveJobs.removeOne( job );
774void QgsChunkedEntity::cancelActiveJobs()
776 while ( !mActiveJobs.isEmpty() )
778 cancelActiveJob( mActiveJobs.takeFirst() );
@ Additive
When tile is refined its content should be used alongside its children simultaneously.
static QgsAABB mapToWorldExtent(const QgsRectangle &extent, double zMin, double zMax, const QgsVector3D &mapOrigin)
Converts map extent to axis aligned bounding box in 3D world coordinates.
static double calculateEntityGpuMemorySize(Qt3DCore::QEntity *entity)
Calculates approximate usage of GPU memory by an entity.
static bool isCullable(const QgsAABB &bbox, const QMatrix4x4 &viewProjectionMatrix)
Returns true if bbox is completely outside the current viewing volume.
static float screenSpaceError(float epsilon, float distance, int screenSize, float fov)
This routine approximately calculates how an error (epsilon) of an object in world coordinates at giv...
static void computeBoundingBoxNearFarPlanes(const QgsAABB &bbox, const QMatrix4x4 &viewMatrix, float &fnear, float &ffar)
This routine computes nearPlane farPlane from the closest and farthest corners point of bounding box ...
Axis-aligned bounding box - in world coords.
QVector3D center() const
Returns coordinates of the center of the box.
float distanceFromPoint(float x, float y, float z) const
Returns shortest distance from the box to a point.
A template based class for storing ranges (lower to upper values).
A representation of a ray in 3D.
Responsible for defining parameters of the ray casting operations in 3D map canvases.
#define QgsDebugMsgLevel(str, level)
#define QgsDebugError(str)