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();
227 u
"update: active %1 enabled %2 disabled %3 | culled %4 | loading %5 loaded %6 | unloaded %7 elapsed %8ms"_s.arg( mActiveNodes.count() )
230 .arg( mFrustumCulled )
231 .arg( mChunkLoaderQueue->count() )
232 .arg( mReplacementQueue->count() )
241int QgsChunkedEntity::unloadNodes()
244 if ( usedGpuMemory <= mGpuMemoryLimit )
246 setHasReachedGpuMemoryLimit(
false );
250 QgsDebugMsgLevel( u
"Going to unload nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
256 QgsChunkListEntry *entry = mReplacementQueue->last();
257 while ( entry && usedGpuMemory > mGpuMemoryLimit )
263 if ( entry->chunk->parent() && !hasAnyActiveChildren( entry->chunk->parent(), mActiveNodes ) )
265 QgsChunkListEntry *entryPrev = entry->prev;
266 mReplacementQueue->takeEntry( entry );
268 mActiveNodes.removeOne( entry->chunk );
269 entry->chunk->unloadChunk();
279 if ( usedGpuMemory > mGpuMemoryLimit )
281 setHasReachedGpuMemoryLimit(
true );
282 QgsDebugMsgLevel( u
"Unable to unload enough nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
289QgsRange<float> QgsChunkedEntity::getNearFarPlaneRange(
const QMatrix4x4 &viewMatrix )
const
291 QList<QgsChunkNode *> activeEntityNodes = activeNodes();
296 if ( activeEntityNodes.empty() )
297 activeEntityNodes << rootNode();
302 for ( QgsChunkNode *node : std::as_const( activeEntityNodes ) )
310 fnear = std::min( fnear, bboxfnear );
311 ffar = std::max( ffar, bboxffar );
316void QgsChunkedEntity::setShowBoundingBoxes(
bool enabled )
318 if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
323 mBboxesEntity =
new QgsChunkBoundsEntity( mRootNode->box3D().center(),
this );
327 mBboxesEntity->deleteLater();
328 mBboxesEntity =
nullptr;
332void QgsChunkedEntity::updateNodes(
const QList<QgsChunkNode *> &nodes, QgsChunkQueueJobFactory *updateJobFactory )
334 for ( QgsChunkNode *node : nodes )
336 if ( node->state() == QgsChunkNode::QueuedForUpdate )
338 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
339 node->cancelQueuedForUpdate();
341 else if ( node->state() == QgsChunkNode::Updating )
343 cancelActiveJob( node->updater() );
345 else if ( node->state() == QgsChunkNode::Skeleton || node->state() == QgsChunkNode::QueuedForLoad )
350 else if ( node->state() == QgsChunkNode::Loading )
353 cancelActiveJob( node->loader() );
354 requestResidency( node );
358 Q_ASSERT( node->state() == QgsChunkNode::Loaded );
360 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
361 node->setQueuedForUpdate( entry, updateJobFactory );
362 mChunkLoaderQueue->insertLast( entry );
369void QgsChunkedEntity::pruneLoaderQueue(
const SceneContext &sceneContext )
371 QList<QgsChunkNode *> toRemoveFromLoaderQueue;
376 QgsChunkListEntry *e = mChunkLoaderQueue->first();
379 Q_ASSERT( e->chunk->state() == QgsChunkNode::QueuedForLoad || e->chunk->state() == QgsChunkNode::QueuedForUpdate );
383 toRemoveFromLoaderQueue.append( e->chunk );
389 for ( QgsChunkNode *n : toRemoveFromLoaderQueue )
391 mChunkLoaderQueue->takeEntry( n->loaderQueueEntry() );
392 if ( n->state() == QgsChunkNode::QueuedForLoad )
394 n->cancelQueuedForLoad();
398 n->cancelQueuedForUpdate();
399 mReplacementQueue->takeEntry( n->replacementQueueEntry() );
404 if ( !toRemoveFromLoaderQueue.isEmpty() )
406 QgsDebugMsgLevel( u
"Pruned %1 chunks in loading queue"_s.arg( toRemoveFromLoaderQueue.count() ), 2 );
411int QgsChunkedEntity::pendingJobsCount()
const
413 return mChunkLoaderQueue->count() + mActiveJobs.count();
416struct ResidencyRequest
418 QgsChunkNode *node =
nullptr;
421 ResidencyRequest() =
default;
422 ResidencyRequest( QgsChunkNode *n,
float d,
int l )
431 bool operator()(
const ResidencyRequest &request,
const ResidencyRequest &otherRequest )
const
433 if ( request.level == otherRequest.level )
434 return request.dist > otherRequest.dist;
435 return request.level > otherRequest.level;
437} ResidencyRequestSorter;
439void QgsChunkedEntity::update( QgsChunkNode *root,
const SceneContext &sceneContext )
441 QSet<QgsChunkNode *> nodes;
442 QVector<ResidencyRequest> residencyRequests;
444 using slotItem = std::pair<QgsChunkNode *, float>;
445 auto cmp_funct = [](
const slotItem &p1,
const slotItem &p2 ) {
return p1.second <= p2.second; };
446 int renderedCount = 0;
447 std::priority_queue<slotItem, std::vector<slotItem>,
decltype( cmp_funct )> pq( cmp_funct );
449 pq.push( std::make_pair( root, screenSpaceError( rootBbox, root->error(), sceneContext ) ) );
450 while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
452 slotItem s = pq.top();
454 QgsChunkNode *node = s.first;
464 if ( !node->hasChildrenPopulated() )
474 if ( mChunkLoaderFactory->canCreateChildren( node ) )
476 node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
480 mChunkLoaderFactory->prepareChildren( node );
486 double dist = bbox.
center().distanceToPoint( sceneContext.cameraPos );
487 residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
489 if ( !node->entity() && node->hasData() )
494 bool becomesActive =
false;
497 if ( node->childCount() == 0 )
501 becomesActive =
true;
503 else if ( mTau > 0 && screenSpaceError( bbox, node->error(), sceneContext ) <= mTau && node->hasData() )
506 becomesActive =
true;
520 becomesActive =
true;
522 QgsChunkNode *
const *children = node->children();
523 for (
int i = 0; i < node->childCount(); ++i )
526 if ( children[i]->entity() || !children[i]->hasData() )
529 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
537 double dist = childBbox.
center().distanceToPoint( sceneContext.cameraPos );
538 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
547 if ( node->allChildChunksResident( mCurrentTime ) )
549 QgsChunkNode *
const *children = node->children();
550 for (
int i = 0; i < node->childCount(); ++i )
553 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
558 becomesActive =
true;
560 QgsChunkNode *
const *children = node->children();
561 for (
int i = 0; i < node->childCount(); ++i )
564 double dist = childBbox.
center().distanceToPoint( sceneContext.cameraPos );
565 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
571 if ( becomesActive && node->entity() )
573 mActiveNodes << node;
577 nodes.remove( node->parent() );
578 renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
580 renderedCount += mChunkLoaderFactory->primitivesCount( node );
581 nodes.insert( node );
586 std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
587 for (
const auto &request : residencyRequests )
588 requestResidency( request.node );
591void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
593 if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
595 Q_ASSERT( node->replacementQueueEntry() );
596 Q_ASSERT( node->entity() );
597 mReplacementQueue->takeEntry( node->replacementQueueEntry() );
598 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
600 else if ( node->state() == QgsChunkNode::QueuedForLoad )
603 Q_ASSERT( node->loaderQueueEntry() );
604 Q_ASSERT( !node->loader() );
605 if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
607 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
608 mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
611 else if ( node->state() == QgsChunkNode::Loading )
615 else if ( node->state() == QgsChunkNode::Skeleton )
617 if ( !node->hasData() )
621 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
622 node->setQueuedForLoad( entry );
623 mChunkLoaderQueue->insertFirst( entry );
626 Q_ASSERT(
false &&
"impossible!" );
630void QgsChunkedEntity::onActiveJobFinished()
632 int oldJobsCount = pendingJobsCount();
634 QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
636 Q_ASSERT( mActiveJobs.contains( job ) );
638 QgsChunkNode *node = job->chunk();
640 if ( node->state() == QgsChunkNode::Loading )
642 QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job );
644 Q_ASSERT( node->loader() == loader );
646 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Load"_s );
648 QgsScopedEvent e(
"3D", QString(
"create" ) );
650 Qt3DCore::QEntity *entity = node->loader()->createEntity(
this );
660 mActiveNodes << node;
663 node->setLoaded( entity );
665 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
667 emit newEntityCreated( entity );
671 node->setHasData(
false );
672 node->cancelLoading();
680 Q_ASSERT( node->state() == QgsChunkNode::Updating );
686 if ( QgsChunkLoader *nodeUpdater = qobject_cast<QgsChunkLoader *>( node->updater() ) )
688 Qt3DCore::QEntity *newEntity = nodeUpdater->createEntity(
this );
689 node->replaceEntity( newEntity );
690 emit newEntityCreated( newEntity );
693 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Update"_s );
698 mActiveJobs.removeOne( job );
704 if ( pendingJobsCount() != oldJobsCount )
705 emit pendingJobsCountChanged();
708void QgsChunkedEntity::startJobs()
710 while ( mActiveJobs.count() < 4 && !mChunkLoaderQueue->isEmpty() )
712 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
714 QgsChunkNode *node = entry->chunk;
717 QgsChunkQueueJob *job = startJob( node );
718 if ( !job->isFinished() )
719 mActiveJobs.append( job );
723QgsChunkQueueJob *QgsChunkedEntity::startJob( QgsChunkNode *node )
725 if ( node->state() == QgsChunkNode::QueuedForLoad )
727 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncBegin, u
"Load"_s );
729 QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
730 connect( loader, &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
732 node->setLoading( loader );
735 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
737 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncBegin, u
"Update"_s );
740 connect( node->updater(), &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
741 node->updater()->start();
742 return node->updater();
751void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob *job )
755 QgsChunkNode *node = job->chunk();
756 disconnect( job, &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
758 if ( node->state() == QgsChunkNode::Loading )
761 node->cancelLoading();
763 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Load"_s );
765 else if ( node->state() == QgsChunkNode::Updating )
768 node->cancelUpdating();
770 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Update"_s );
778 mActiveJobs.removeOne( job );
782void QgsChunkedEntity::cancelActiveJobs()
784 while ( !mActiveJobs.isEmpty() )
786 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)