28#include <QElapsedTimer>
33#include "moc_qgschunkedentity.cpp"
35using namespace Qt::StringLiterals;
39static float screenSpaceError(
const QgsAABB &nodeBbox,
float nodeError,
const QgsChunkedEntity::SceneContext &sceneContext )
53static bool hasAnyActiveChildren( QgsChunkNode *node, QList<QgsChunkNode *> &activeNodes )
55 for (
int i = 0; i < node->childCount(); ++i )
57 QgsChunkNode *child = node->children()[i];
58 if ( child->entity() && activeNodes.contains( child ) )
60 if ( hasAnyActiveChildren( child, activeNodes ) )
66static void addTileTraceEvent( QObject &self, QgsChunkNode &node, QgsEventTracing::EventType eventType, QString name )
68 QgsEventTracing::addEvent( eventType, u
"3D"_s, name + u
" "_s + node.tileId().text(), u
"%1 %2"_s.arg( self.objectName(), node.tileId().text() ) );
71QgsChunkedEntity::QgsChunkedEntity(
Qgs3DMapSettings *mapSettings,
float tau, QgsChunkLoaderFactory *loaderFactory,
bool ownsFactory,
int primitiveBudget, Qt3DCore::QNode *parent )
72 : Qgs3DMapSceneEntity( mapSettings, parent )
74 , mChunkLoaderFactory( loaderFactory )
75 , mOwnsFactory( ownsFactory )
76 , mPrimitivesBudget( primitiveBudget )
78 mRootNode = loaderFactory->createRootNode();
79 mChunkLoaderQueue =
new QgsChunkList;
80 mReplacementQueue =
new QgsChunkList;
83 connect( loaderFactory, &QgsChunkLoaderFactory::childrenPrepared,
this, [
this] {
84 setNeedsUpdate(
true );
85 emit pendingJobsCountChanged();
90QgsChunkedEntity::~QgsChunkedEntity()
95 Q_ASSERT( mActiveJobs.isEmpty() );
98 while ( !mChunkLoaderQueue->isEmpty() )
100 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
101 QgsChunkNode *node = entry->chunk;
103 if ( node->state() == QgsChunkNode::QueuedForLoad )
104 node->cancelQueuedForLoad();
105 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
106 node->cancelQueuedForUpdate();
111 delete mChunkLoaderQueue;
113 while ( !mReplacementQueue->isEmpty() )
115 QgsChunkListEntry *entry = mReplacementQueue->takeFirst();
118 entry->chunk->unloadChunk();
121 delete mReplacementQueue;
126 delete mChunkLoaderFactory;
131void QgsChunkedEntity::handleSceneUpdate(
const SceneContext &sceneContext )
141 pruneLoaderQueue( sceneContext );
146 int oldJobsCount = pendingJobsCount();
148 QSet<QgsChunkNode *> activeBefore = qgis::listToSet( mActiveNodes );
149 mActiveNodes.clear();
151 mCurrentTime = QTime::currentTime();
153 update( mRootNode, sceneContext );
156 int enabled = 0, disabled = 0, unloaded = 0;
159 for ( QgsChunkNode *node : std::as_const( mActiveNodes ) )
161 if ( activeBefore.contains( node ) )
163 activeBefore.remove( node );
167 if ( !node->entity() )
169 QgsDebugError(
"Active node has null entity - this should never happen!" );
172 node->entity()->setEnabled(
true );
175 const QList<QgsGeoTransform *> transforms = node->entity()->findChildren<QgsGeoTransform *>();
176 for ( QgsGeoTransform *transform : transforms )
178 transform->setOrigin( mMapSettings->origin() );
188 for ( QgsChunkNode *node : activeBefore )
190 if ( !node->entity() )
192 QgsDebugError(
"Active node has null entity - this should never happen!" );
195 node->entity()->setEnabled(
false );
204 unloaded = unloadNodes();
211 QList<QgsBox3D> bboxes;
212 for ( QgsChunkNode *n : std::as_const( mActiveNodes ) )
213 bboxes << n->box3D();
214 mBboxesEntity->setBoxes( bboxes );
220 mNeedsUpdate =
false;
222 if ( pendingJobsCount() != oldJobsCount )
223 emit pendingJobsCountChanged();
226 u
"update: active %1 enabled %2 disabled %3 | culled %4 | loading %5 loaded %6 | unloaded %7 elapsed %8ms"_s.arg( mActiveNodes.count() )
229 .arg( mFrustumCulled )
230 .arg( mChunkLoaderQueue->count() )
231 .arg( mReplacementQueue->count() )
239int QgsChunkedEntity::unloadNodes()
242 if ( usedGpuMemory <= mGpuMemoryLimit )
244 setHasReachedGpuMemoryLimit(
false );
248 QgsDebugMsgLevel( u
"Going to unload nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
254 QgsChunkListEntry *entry = mReplacementQueue->last();
255 while ( entry && usedGpuMemory > mGpuMemoryLimit )
261 if ( entry->chunk->parent() && !hasAnyActiveChildren( entry->chunk->parent(), mActiveNodes ) )
263 QgsChunkListEntry *entryPrev = entry->prev;
264 mReplacementQueue->takeEntry( entry );
266 mActiveNodes.removeOne( entry->chunk );
267 entry->chunk->unloadChunk();
277 if ( usedGpuMemory > mGpuMemoryLimit )
279 setHasReachedGpuMemoryLimit(
true );
280 QgsDebugMsgLevel( u
"Unable to unload enough nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
287QgsRange<float> QgsChunkedEntity::getNearFarPlaneRange(
const QMatrix4x4 &viewMatrix )
const
289 QList<QgsChunkNode *> activeEntityNodes = activeNodes();
294 if ( activeEntityNodes.empty() )
295 activeEntityNodes << rootNode();
300 for ( QgsChunkNode *node : std::as_const( activeEntityNodes ) )
308 fnear = std::min( fnear, bboxfnear );
309 ffar = std::max( ffar, bboxffar );
314void QgsChunkedEntity::setShowBoundingBoxes(
bool enabled )
316 if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
321 mBboxesEntity =
new QgsChunkBoundsEntity( mRootNode->box3D().center(),
this );
325 mBboxesEntity->deleteLater();
326 mBboxesEntity =
nullptr;
330void QgsChunkedEntity::updateNodes(
const QList<QgsChunkNode *> &nodes, QgsChunkQueueJobFactory *updateJobFactory )
332 for ( QgsChunkNode *node : nodes )
334 if ( node->state() == QgsChunkNode::QueuedForUpdate )
336 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
337 node->cancelQueuedForUpdate();
339 else if ( node->state() == QgsChunkNode::Updating )
341 cancelActiveJob( node->updater() );
343 else if ( node->state() == QgsChunkNode::Skeleton || node->state() == QgsChunkNode::QueuedForLoad )
348 else if ( node->state() == QgsChunkNode::Loading )
351 cancelActiveJob( node->loader() );
352 requestResidency( node );
356 Q_ASSERT( node->state() == QgsChunkNode::Loaded );
358 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
359 node->setQueuedForUpdate( entry, updateJobFactory );
360 mChunkLoaderQueue->insertLast( entry );
367void QgsChunkedEntity::pruneLoaderQueue(
const SceneContext &sceneContext )
369 QList<QgsChunkNode *> toRemoveFromLoaderQueue;
374 QgsChunkListEntry *e = mChunkLoaderQueue->first();
377 Q_ASSERT( e->chunk->state() == QgsChunkNode::QueuedForLoad || e->chunk->state() == QgsChunkNode::QueuedForUpdate );
381 toRemoveFromLoaderQueue.append( e->chunk );
387 for ( QgsChunkNode *n : toRemoveFromLoaderQueue )
389 mChunkLoaderQueue->takeEntry( n->loaderQueueEntry() );
390 if ( n->state() == QgsChunkNode::QueuedForLoad )
392 n->cancelQueuedForLoad();
396 n->cancelQueuedForUpdate();
397 mReplacementQueue->takeEntry( n->replacementQueueEntry() );
402 if ( !toRemoveFromLoaderQueue.isEmpty() )
404 QgsDebugMsgLevel( u
"Pruned %1 chunks in loading queue"_s.arg( toRemoveFromLoaderQueue.count() ), 2 );
409int QgsChunkedEntity::pendingJobsCount()
const
411 return mChunkLoaderQueue->count() + mActiveJobs.count();
414struct ResidencyRequest
416 QgsChunkNode *node =
nullptr;
419 ResidencyRequest() =
default;
420 ResidencyRequest( QgsChunkNode *n,
float d,
int l )
429 bool operator()(
const ResidencyRequest &request,
const ResidencyRequest &otherRequest )
const
431 if ( request.level == otherRequest.level )
432 return request.dist > otherRequest.dist;
433 return request.level > otherRequest.level;
435} ResidencyRequestSorter;
437void QgsChunkedEntity::update( QgsChunkNode *root,
const SceneContext &sceneContext )
439 QSet<QgsChunkNode *> nodes;
440 QVector<ResidencyRequest> residencyRequests;
442 using slotItem = std::pair<QgsChunkNode *, float>;
443 auto cmp_funct = [](
const slotItem &p1,
const slotItem &p2 ) {
return p1.second <= p2.second; };
444 int renderedCount = 0;
445 std::priority_queue<slotItem, std::vector<slotItem>,
decltype( cmp_funct )> pq( cmp_funct );
447 pq.push( std::make_pair( root, screenSpaceError( rootBbox, root->error(), sceneContext ) ) );
448 while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
450 slotItem s = pq.top();
452 QgsChunkNode *node = s.first;
462 if ( !node->hasChildrenPopulated() )
472 if ( mChunkLoaderFactory->canCreateChildren( node ) )
474 node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
478 mChunkLoaderFactory->prepareChildren( node );
484 double dist = bbox.
center().distanceToPoint( sceneContext.cameraPos );
485 residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
487 if ( !node->entity() && node->hasData() )
492 bool becomesActive =
false;
495 if ( node->childCount() == 0 )
499 becomesActive =
true;
501 else if ( mTau > 0 && screenSpaceError( bbox, node->error(), sceneContext ) <= mTau && node->hasData() )
504 becomesActive =
true;
518 becomesActive =
true;
520 QgsChunkNode *
const *children = node->children();
521 for (
int i = 0; i < node->childCount(); ++i )
524 if ( children[i]->entity() || !children[i]->hasData() )
527 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
535 double dist = childBbox.
center().distanceToPoint( sceneContext.cameraPos );
536 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
545 if ( node->allChildChunksResident( mCurrentTime ) )
547 QgsChunkNode *
const *children = node->children();
548 for (
int i = 0; i < node->childCount(); ++i )
551 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
556 becomesActive =
true;
558 QgsChunkNode *
const *children = node->children();
559 for (
int i = 0; i < node->childCount(); ++i )
562 double dist = childBbox.
center().distanceToPoint( sceneContext.cameraPos );
563 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
569 if ( becomesActive && node->entity() )
571 mActiveNodes << node;
575 nodes.remove( node->parent() );
576 renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
578 renderedCount += mChunkLoaderFactory->primitivesCount( node );
579 nodes.insert( node );
584 std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
585 for (
const auto &request : residencyRequests )
586 requestResidency( request.node );
589void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
591 if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
593 Q_ASSERT( node->replacementQueueEntry() );
594 Q_ASSERT( node->entity() );
595 mReplacementQueue->takeEntry( node->replacementQueueEntry() );
596 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
598 else if ( node->state() == QgsChunkNode::QueuedForLoad )
601 Q_ASSERT( node->loaderQueueEntry() );
602 Q_ASSERT( !node->loader() );
603 if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
605 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
606 mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
609 else if ( node->state() == QgsChunkNode::Loading )
613 else if ( node->state() == QgsChunkNode::Skeleton )
615 if ( !node->hasData() )
619 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
620 node->setQueuedForLoad( entry );
621 mChunkLoaderQueue->insertFirst( entry );
624 Q_ASSERT(
false &&
"impossible!" );
628void QgsChunkedEntity::onActiveJobFinished()
630 int oldJobsCount = pendingJobsCount();
632 QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
634 Q_ASSERT( mActiveJobs.contains( job ) );
636 QgsChunkNode *node = job->chunk();
638 if ( node->state() == QgsChunkNode::Loading )
640 QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job );
642 Q_ASSERT( node->loader() == loader );
644 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Load"_s );
646 QgsScopedEvent e(
"3D", QString(
"create" ) );
648 Qt3DCore::QEntity *entity = node->loader()->createEntity(
this );
658 mActiveNodes << node;
661 node->setLoaded( entity );
663 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
665 emit newEntityCreated( entity );
669 node->setHasData(
false );
670 node->cancelLoading();
678 Q_ASSERT( node->state() == QgsChunkNode::Updating );
684 if ( QgsChunkLoader *nodeUpdater = qobject_cast<QgsChunkLoader *>( node->updater() ) )
686 Qt3DCore::QEntity *newEntity = nodeUpdater->createEntity(
this );
687 node->replaceEntity( newEntity );
688 emit newEntityCreated( newEntity );
691 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Update"_s );
696 mActiveJobs.removeOne( job );
702 if ( pendingJobsCount() != oldJobsCount )
703 emit pendingJobsCountChanged();
706void QgsChunkedEntity::startJobs()
708 while ( mActiveJobs.count() < 4 && !mChunkLoaderQueue->isEmpty() )
710 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
712 QgsChunkNode *node = entry->chunk;
715 QgsChunkQueueJob *job = startJob( node );
716 if ( !job->isFinished() )
717 mActiveJobs.append( job );
721QgsChunkQueueJob *QgsChunkedEntity::startJob( QgsChunkNode *node )
723 if ( node->state() == QgsChunkNode::QueuedForLoad )
725 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncBegin, u
"Load"_s );
727 QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
728 connect( loader, &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
730 node->setLoading( loader );
733 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
735 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncBegin, u
"Update"_s );
738 connect( node->updater(), &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
739 node->updater()->start();
740 return node->updater();
749void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob *job )
753 QgsChunkNode *node = job->chunk();
754 disconnect( job, &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
756 if ( node->state() == QgsChunkNode::Loading )
759 node->cancelLoading();
761 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Load"_s );
763 else if ( node->state() == QgsChunkNode::Updating )
766 node->cancelUpdating();
768 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Update"_s );
776 mActiveJobs.removeOne( job );
780void QgsChunkedEntity::cancelActiveJobs()
782 while ( !mActiveJobs.isEmpty() )
784 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)