29#include <QElapsedTimer>
34#include "moc_qgschunkedentity.cpp"
36using namespace Qt::StringLiterals;
40static float screenSpaceError(
const QgsAABB &nodeBbox,
float nodeError,
const QgsChunkedEntity::SceneContext &sceneContext )
54static bool hasAnyActiveChildren( QgsChunkNode *node, QList<QgsChunkNode *> &activeNodes )
56 for (
int i = 0; i < node->childCount(); ++i )
58 QgsChunkNode *child = node->children()[i];
59 if ( child->entity() && activeNodes.contains( child ) )
61 if ( hasAnyActiveChildren( child, activeNodes ) )
67static void addTileTraceEvent( QObject &self, QgsChunkNode &node, QgsEventTracing::EventType eventType, QString name )
69 QgsEventTracing::addEvent( eventType, u
"3D"_s, name + u
" "_s + node.tileId().text(), u
"%1 %2"_s.arg( self.objectName(), node.tileId().text() ) );
72QgsChunkedEntity::QgsChunkedEntity(
Qgs3DMapSettings *mapSettings,
float tau, QgsChunkLoader *loader,
bool ownsLoader,
int primitiveBudget, Qt3DCore::QNode *parent )
73 : Qgs3DMapSceneEntity( mapSettings, parent )
75 , mChunkLoader( loader )
76 , mOwnsLoader( ownsLoader )
77 , mPrimitivesBudget( primitiveBudget )
79 mRootNode.reset( loader->createRootNode() );
80 mChunkLoaderQueue = std::make_unique<QgsChunkList>();
81 mReplacementQueue = std::make_unique<QgsChunkList>();
85QgsChunkedEntity::~QgsChunkedEntity()
88 while ( !mChunkLoaderQueue->isEmpty() )
90 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
91 QgsChunkNode *node = entry->chunk;
93 if ( node->state() == QgsChunkNode::QueuedForLoad )
94 node->cancelQueuedForLoad();
95 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
96 node->cancelQueuedForUpdate();
101 while ( !mReplacementQueue->isEmpty() )
103 QgsChunkListEntry *entry = mReplacementQueue->takeFirst();
106 entry->chunk->unloadChunk();
116void QgsChunkedEntity::handleSceneUpdate(
const SceneContext &sceneContext )
126 pruneLoaderQueue( sceneContext );
131 int oldJobsCount = pendingJobsCount();
133 QSet<QgsChunkNode *> activeBefore = qgis::listToSet( mActiveNodes );
134 mActiveNodes.clear();
136 mCurrentTime = QTime::currentTime();
138 update( mRootNode.get(), sceneContext );
141 int enabled = 0, disabled = 0, unloaded = 0;
144 for ( QgsChunkNode *node : std::as_const( mActiveNodes ) )
146 if ( activeBefore.contains( node ) )
148 activeBefore.remove( node );
152 if ( !node->entity() )
154 QgsDebugError(
"Active node has null entity - this should never happen!" );
157 node->entity()->setEnabled(
true );
160 const QList<QgsGeoTransform *> transforms = node->entity()->findChildren<QgsGeoTransform *>();
161 for ( QgsGeoTransform *transform : transforms )
163 transform->setOrigin( mMapSettings->origin() );
173 for ( QgsChunkNode *node : activeBefore )
175 if ( !node->entity() )
177 QgsDebugError(
"Active node has null entity - this should never happen!" );
180 node->entity()->setEnabled(
false );
189 unloaded = unloadNodes();
196 QList<QgsBox3D> bboxes;
197 for ( QgsChunkNode *n : std::as_const( mActiveNodes ) )
198 bboxes << n->box3D();
199 mBboxesEntity->setBoxes( bboxes );
203 mNeedsUpdate =
false;
207 if ( pendingJobsCount() != oldJobsCount )
208 emit pendingJobsCountChanged();
212 u
"update: active %1 enabled %2 disabled %3 | culled %4 | loading %5 loaded %6 | unloaded %7 elapsed %8ms"_s.arg( mActiveNodes.count() )
215 .arg( mFrustumCulled )
216 .arg( mChunkLoaderQueue->count() )
217 .arg( mReplacementQueue->count() )
226int QgsChunkedEntity::unloadNodes()
229 if ( usedGpuMemory <= mGpuMemoryLimit )
231 setHasReachedGpuMemoryLimit(
false );
235 QgsDebugMsgLevel( u
"Going to unload nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
241 QgsChunkListEntry *entry = mReplacementQueue->last();
242 while ( entry && usedGpuMemory > mGpuMemoryLimit )
248 if ( entry->chunk->parent() && !hasAnyActiveChildren( entry->chunk->parent(), mActiveNodes ) )
250 QgsChunkListEntry *entryPrev = entry->prev;
251 mReplacementQueue->takeEntry( entry );
253 mActiveNodes.removeOne( entry->chunk );
254 entry->chunk->unloadChunk();
264 if ( usedGpuMemory > mGpuMemoryLimit )
266 setHasReachedGpuMemoryLimit(
true );
267 QgsDebugMsgLevel( u
"Unable to unload enough nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
274QgsRange<float> QgsChunkedEntity::getNearFarPlaneRange(
const QMatrix4x4 &viewMatrix )
const
276 QList<QgsChunkNode *> activeEntityNodes = activeNodes();
281 if ( activeEntityNodes.empty() )
282 activeEntityNodes << rootNode();
287 for ( QgsChunkNode *node : std::as_const( activeEntityNodes ) )
295 fnear = std::min( fnear, bboxfnear );
296 ffar = std::max( ffar, bboxffar );
301void QgsChunkedEntity::setShowBoundingBoxes(
bool enabled )
303 if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
308 mBboxesEntity =
new QgsChunkBoundsEntity( mRootNode->box3D().center(),
this );
312 mBboxesEntity->deleteLater();
313 mBboxesEntity =
nullptr;
317void QgsChunkedEntity::updateNodes(
const QList<QgsChunkNode *> &nodes )
319 for ( QgsChunkNode *node : nodes )
321 if ( node->state() == QgsChunkNode::QueuedForUpdate )
323 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
324 node->cancelQueuedForUpdate();
326 else if ( node->state() == QgsChunkNode::Updating )
328 auto job = node->updateJob();
330 cancelActiveJob( *job );
332 else if ( node->state() == QgsChunkNode::Skeleton || node->state() == QgsChunkNode::QueuedForLoad )
337 else if ( node->state() == QgsChunkNode::Loading )
340 auto job = node->loaderJob();
342 cancelActiveJob( *job );
343 requestResidency( node );
347 Q_ASSERT( node->state() == QgsChunkNode::Loaded );
349 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
350 node->setQueuedForUpdate( entry );
351 mChunkLoaderQueue->insertLast( entry );
358void QgsChunkedEntity::pruneLoaderQueue(
const SceneContext &sceneContext )
360 QList<QgsChunkNode *> toRemoveFromLoaderQueue;
365 QgsChunkListEntry *e = mChunkLoaderQueue->first();
368 Q_ASSERT( e->chunk->state() == QgsChunkNode::QueuedForLoad || e->chunk->state() == QgsChunkNode::QueuedForUpdate );
372 toRemoveFromLoaderQueue.append( e->chunk );
378 for ( QgsChunkNode *n : toRemoveFromLoaderQueue )
380 mChunkLoaderQueue->takeEntry( n->loaderQueueEntry() );
381 if ( n->state() == QgsChunkNode::QueuedForLoad )
383 n->cancelQueuedForLoad();
387 n->cancelQueuedForUpdate();
388 mReplacementQueue->takeEntry( n->replacementQueueEntry() );
393 if ( !toRemoveFromLoaderQueue.isEmpty() )
395 QgsDebugMsgLevel( u
"Pruned %1 chunks in loading queue"_s.arg( toRemoveFromLoaderQueue.count() ), 2 );
400int QgsChunkedEntity::pendingJobsCount()
const
402 return mChunkLoaderQueue->count() + mActiveJobs.size();
405struct ResidencyRequest
407 QgsChunkNode *node =
nullptr;
410 ResidencyRequest() =
default;
411 ResidencyRequest( QgsChunkNode *n,
float d,
int l )
420 bool operator()(
const ResidencyRequest &request,
const ResidencyRequest &otherRequest )
const
422 if ( request.level == otherRequest.level )
423 return request.dist > otherRequest.dist;
424 return request.level > otherRequest.level;
426} ResidencyRequestSorter;
428void QgsChunkedEntity::update( QgsChunkNode *root,
const SceneContext &sceneContext )
430 QSet<QgsChunkNode *> nodes;
431 QVector<ResidencyRequest> residencyRequests;
433 using slotItem = std::pair<QgsChunkNode *, float>;
434 auto cmp_funct = [](
const slotItem &p1,
const slotItem &p2 ) {
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;
454 double dist = bbox.
center().distanceToPoint( sceneContext.cameraPos );
455 residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
457 if ( !node->entity() && node->hasData() )
464 if ( !node->hasChildrenPopulated() && !node->creatingChildren() )
466 node->setCreatingChildren(
true );
467 mActiveJobs.push_back( std::make_unique<QgsChunkQueueJob>() );
468 QgsChunkQueueJob &job = *mActiveJobs.back();
470 job.type = QgsChunkQueueJob::Type::CreateChildren;
471 QFuture<QVector<QgsChunkNode *>> origFuture = mChunkLoader->createChildren( node );
472 job.future = origFuture;
473 origFuture.then(
this, [
this, &job, node]( QVector<QgsChunkNode *> res ) {
474 node->populateChildren( res );
475 eraseJobFromList( job );
478 emit pendingJobsCountChanged();
482 bool becomesActive =
false;
484 if ( node->childCount() == 0 )
488 becomesActive =
true;
490 else if ( mTau > 0 && screenSpaceError( bbox, node->error(), sceneContext ) <= mTau && node->hasData() )
493 becomesActive =
true;
507 becomesActive =
true;
509 QgsChunkNode *
const *children = node->children();
510 for (
int i = 0; i < node->childCount(); ++i )
513 if ( children[i]->entity() || !children[i]->hasData() )
516 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
524 double dist = childBbox.
center().distanceToPoint( sceneContext.cameraPos );
525 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
534 if ( node->allChildChunksResident( mCurrentTime ) )
536 QgsChunkNode *
const *children = node->children();
537 for (
int i = 0; i < node->childCount(); ++i )
540 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
545 becomesActive =
true;
547 QgsChunkNode *
const *children = node->children();
548 for (
int i = 0; i < node->childCount(); ++i )
551 double dist = childBbox.
center().distanceToPoint( sceneContext.cameraPos );
552 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
558 if ( becomesActive && node->entity() )
560 mActiveNodes << node;
564 nodes.remove( node->parent() );
565 renderedCount -= mChunkLoader->primitivesCount( node->parent() );
567 renderedCount += mChunkLoader->primitivesCount( node );
568 nodes.insert( node );
573 std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
574 for (
const auto &request : residencyRequests )
575 requestResidency( request.node );
578void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
580 if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
582 Q_ASSERT( node->replacementQueueEntry() );
583 Q_ASSERT( node->entity() );
584 mReplacementQueue->takeEntry( node->replacementQueueEntry() );
585 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
587 else if ( node->state() == QgsChunkNode::QueuedForLoad )
590 Q_ASSERT( node->loaderQueueEntry() );
591 Q_ASSERT( !node->loaderJob() );
592 if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
594 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
595 mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
598 else if ( node->state() == QgsChunkNode::Loading )
602 else if ( node->state() == QgsChunkNode::Skeleton )
604 if ( !node->hasData() )
608 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
609 node->setQueuedForLoad( entry );
610 mChunkLoaderQueue->insertFirst( entry );
613 Q_ASSERT(
false &&
"impossible!" );
616void QgsChunkedEntity::eraseJobFromList( QgsChunkQueueJob &job )
618 auto it = std::find_if( mActiveJobs.begin(), mActiveJobs.end(), [&job]( std::unique_ptr<QgsChunkQueueJob> &j ) { return j.get() == &job; } );
619 if ( it != mActiveJobs.end() )
620 mActiveJobs.erase( it );
623void QgsChunkedEntity::onActiveLoadJobFinished( QgsChunkQueueJob &job, QgsChunkLoaderResult &result )
625 int oldJobsCount = pendingJobsCount();
626 QgsChunkNode *node = job.node;
628 if ( node->state() == QgsChunkNode::Loading )
630 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Load"_s );
632 QgsScopedEvent e(
"3D", QString(
"create" ) );
634 Qt3DCore::QEntity *entity = result.createEntity(
this );
644 mActiveNodes << node;
647 node->setLoaded( entity );
649 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
651 emit newEntityCreated( entity );
655 node->setHasData(
false );
656 node->cancelLoading();
664 Q_ASSERT( node->state() == QgsChunkNode::Updating );
666 Qt3DCore::QEntity *newEntity = result.createEntity(
this );
667 if ( newEntity != node->entity() )
669 node->replaceEntity( newEntity );
670 emit newEntityCreated( newEntity );
673 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Update"_s );
678 eraseJobFromList( job );
683 if ( pendingJobsCount() != oldJobsCount )
684 emit pendingJobsCountChanged();
687void QgsChunkedEntity::startJobs()
689 while ( mActiveJobs.size() < 4 && !mChunkLoaderQueue->isEmpty() )
691 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
693 QgsChunkNode *node = entry->chunk;
700void QgsChunkedEntity::startJob( QgsChunkNode *node )
702 mActiveJobs.push_back( std::make_unique<QgsChunkQueueJob>() );
703 QgsChunkQueueJob &job = *mActiveJobs.back();
705 QFuture<QgsChunkLoaderResult> origFuture;
707 if ( node->state() == QgsChunkNode::QueuedForLoad )
709 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncBegin, u
"Load"_s );
710 origFuture = mChunkLoader->loadChunk( node );
711 job.future = origFuture;
712 job.type = QgsChunkQueueJob::Type::Load;
713 node->setLoading( job );
715 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
717 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncBegin, u
"Update"_s );
718 origFuture = mChunkLoader->updateChunk( node );
719 job.future = origFuture;
720 job.type = QgsChunkQueueJob::Type::Update;
721 node->setUpdating( job );
728 origFuture.then(
this, [
this, &job]( QgsChunkLoaderResult res ) { onActiveLoadJobFinished( job, res ); } );
731void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob &job )
733 QgsChunkNode *node = job.node;
737 case QgsChunkQueueJob::Type::Load:
739 Q_ASSERT( node->state() == QgsChunkNode::Loading );
741 node->cancelLoading();
743 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Load"_s );
746 case QgsChunkQueueJob::Type::Update:
748 Q_ASSERT( node->state() == QgsChunkNode::Updating );
750 node->cancelUpdating();
752 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Update"_s );
755 case QgsChunkQueueJob::Type::CreateChildren:
757 node->setCreatingChildren(
false );
763 eraseJobFromList( job );
766void QgsChunkedEntity::cancelActiveJobs()
768 while ( !mActiveJobs.empty() )
770 cancelActiveJob( *mActiveJobs.back() );
@ 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.
static void cancelChain(QFuture< void > future)
Runs future.cancelChain() if supported by Qt version, future.cancel() otherwise.
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)