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.reset( loaderFactory->createRootNode() );
79 mChunkLoaderQueue = std::make_unique<QgsChunkList>();
80 mReplacementQueue = std::make_unique<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 while ( !mReplacementQueue->isEmpty() )
113 QgsChunkListEntry *entry = mReplacementQueue->takeFirst();
116 entry->chunk->unloadChunk();
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.get(), 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();
222 u
"update: active %1 enabled %2 disabled %3 | culled %4 | loading %5 loaded %6 | unloaded %7 elapsed %8ms"_s.arg( mActiveNodes.count() )
225 .arg( mFrustumCulled )
226 .arg( mChunkLoaderQueue->count() )
227 .arg( mReplacementQueue->count() )
236int QgsChunkedEntity::unloadNodes()
239 if ( usedGpuMemory <= mGpuMemoryLimit )
241 setHasReachedGpuMemoryLimit(
false );
245 QgsDebugMsgLevel( u
"Going to unload nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
251 QgsChunkListEntry *entry = mReplacementQueue->last();
252 while ( entry && usedGpuMemory > mGpuMemoryLimit )
258 if ( entry->chunk->parent() && !hasAnyActiveChildren( entry->chunk->parent(), mActiveNodes ) )
260 QgsChunkListEntry *entryPrev = entry->prev;
261 mReplacementQueue->takeEntry( entry );
263 mActiveNodes.removeOne( entry->chunk );
264 entry->chunk->unloadChunk();
274 if ( usedGpuMemory > mGpuMemoryLimit )
276 setHasReachedGpuMemoryLimit(
true );
277 QgsDebugMsgLevel( u
"Unable to unload enough nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
284QgsRange<float> QgsChunkedEntity::getNearFarPlaneRange(
const QMatrix4x4 &viewMatrix )
const
286 QList<QgsChunkNode *> activeEntityNodes = activeNodes();
291 if ( activeEntityNodes.empty() )
292 activeEntityNodes << rootNode();
297 for ( QgsChunkNode *node : std::as_const( activeEntityNodes ) )
305 fnear = std::min( fnear, bboxfnear );
306 ffar = std::max( ffar, bboxffar );
311void QgsChunkedEntity::setShowBoundingBoxes(
bool enabled )
313 if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
318 mBboxesEntity =
new QgsChunkBoundsEntity( mRootNode->box3D().center(),
this );
322 mBboxesEntity->deleteLater();
323 mBboxesEntity =
nullptr;
327void QgsChunkedEntity::updateNodes(
const QList<QgsChunkNode *> &nodes, QgsChunkQueueJobFactory *updateJobFactory )
329 for ( QgsChunkNode *node : nodes )
331 if ( node->state() == QgsChunkNode::QueuedForUpdate )
333 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
334 node->cancelQueuedForUpdate();
336 else if ( node->state() == QgsChunkNode::Updating )
338 cancelActiveJob( node->updater() );
340 else if ( node->state() == QgsChunkNode::Skeleton || node->state() == QgsChunkNode::QueuedForLoad )
345 else if ( node->state() == QgsChunkNode::Loading )
348 cancelActiveJob( node->loader() );
349 requestResidency( node );
353 Q_ASSERT( node->state() == QgsChunkNode::Loaded );
355 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
356 node->setQueuedForUpdate( entry, updateJobFactory );
357 mChunkLoaderQueue->insertLast( entry );
364void QgsChunkedEntity::pruneLoaderQueue(
const SceneContext &sceneContext )
366 QList<QgsChunkNode *> toRemoveFromLoaderQueue;
371 QgsChunkListEntry *e = mChunkLoaderQueue->first();
374 Q_ASSERT( e->chunk->state() == QgsChunkNode::QueuedForLoad || e->chunk->state() == QgsChunkNode::QueuedForUpdate );
378 toRemoveFromLoaderQueue.append( e->chunk );
384 for ( QgsChunkNode *n : toRemoveFromLoaderQueue )
386 mChunkLoaderQueue->takeEntry( n->loaderQueueEntry() );
387 if ( n->state() == QgsChunkNode::QueuedForLoad )
389 n->cancelQueuedForLoad();
393 n->cancelQueuedForUpdate();
394 mReplacementQueue->takeEntry( n->replacementQueueEntry() );
399 if ( !toRemoveFromLoaderQueue.isEmpty() )
401 QgsDebugMsgLevel( u
"Pruned %1 chunks in loading queue"_s.arg( toRemoveFromLoaderQueue.count() ), 2 );
406int QgsChunkedEntity::pendingJobsCount()
const
408 return mChunkLoaderQueue->count() + mActiveJobs.count();
411struct ResidencyRequest
413 QgsChunkNode *node =
nullptr;
416 ResidencyRequest() =
default;
417 ResidencyRequest( QgsChunkNode *n,
float d,
int l )
426 bool operator()(
const ResidencyRequest &request,
const ResidencyRequest &otherRequest )
const
428 if ( request.level == otherRequest.level )
429 return request.dist > otherRequest.dist;
430 return request.level > otherRequest.level;
432} ResidencyRequestSorter;
434void QgsChunkedEntity::update( QgsChunkNode *root,
const SceneContext &sceneContext )
436 QSet<QgsChunkNode *> nodes;
437 QVector<ResidencyRequest> residencyRequests;
439 using slotItem = std::pair<QgsChunkNode *, float>;
440 auto cmp_funct = [](
const slotItem &p1,
const slotItem &p2 ) {
return p1.second <= p2.second; };
441 int renderedCount = 0;
442 std::priority_queue<slotItem, std::vector<slotItem>,
decltype( cmp_funct )> pq( cmp_funct );
444 pq.push( std::make_pair( root, screenSpaceError( rootBbox, root->error(), sceneContext ) ) );
445 while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
447 slotItem s = pq.top();
449 QgsChunkNode *node = s.first;
459 if ( !node->hasChildrenPopulated() )
469 if ( mChunkLoaderFactory->canCreateChildren( node ) )
471 node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
475 mChunkLoaderFactory->prepareChildren( node );
481 double dist = bbox.
center().distanceToPoint( sceneContext.cameraPos );
482 residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
484 if ( !node->entity() && node->hasData() )
489 bool becomesActive =
false;
492 if ( node->childCount() == 0 )
496 becomesActive =
true;
498 else if ( mTau > 0 && screenSpaceError( bbox, node->error(), sceneContext ) <= mTau && node->hasData() )
501 becomesActive =
true;
515 becomesActive =
true;
517 QgsChunkNode *
const *children = node->children();
518 for (
int i = 0; i < node->childCount(); ++i )
521 if ( children[i]->entity() || !children[i]->hasData() )
524 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
532 double dist = childBbox.
center().distanceToPoint( sceneContext.cameraPos );
533 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
542 if ( node->allChildChunksResident( mCurrentTime ) )
544 QgsChunkNode *
const *children = node->children();
545 for (
int i = 0; i < node->childCount(); ++i )
548 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
553 becomesActive =
true;
555 QgsChunkNode *
const *children = node->children();
556 for (
int i = 0; i < node->childCount(); ++i )
559 double dist = childBbox.
center().distanceToPoint( sceneContext.cameraPos );
560 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
566 if ( becomesActive && node->entity() )
568 mActiveNodes << node;
572 nodes.remove( node->parent() );
573 renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
575 renderedCount += mChunkLoaderFactory->primitivesCount( node );
576 nodes.insert( node );
581 std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
582 for (
const auto &request : residencyRequests )
583 requestResidency( request.node );
586void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
588 if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
590 Q_ASSERT( node->replacementQueueEntry() );
591 Q_ASSERT( node->entity() );
592 mReplacementQueue->takeEntry( node->replacementQueueEntry() );
593 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
595 else if ( node->state() == QgsChunkNode::QueuedForLoad )
598 Q_ASSERT( node->loaderQueueEntry() );
599 Q_ASSERT( !node->loader() );
600 if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
602 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
603 mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
606 else if ( node->state() == QgsChunkNode::Loading )
610 else if ( node->state() == QgsChunkNode::Skeleton )
612 if ( !node->hasData() )
616 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
617 node->setQueuedForLoad( entry );
618 mChunkLoaderQueue->insertFirst( entry );
621 Q_ASSERT(
false &&
"impossible!" );
625void QgsChunkedEntity::onActiveJobFinished()
627 int oldJobsCount = pendingJobsCount();
629 QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
631 Q_ASSERT( mActiveJobs.contains( job ) );
633 QgsChunkNode *node = job->chunk();
635 if ( node->state() == QgsChunkNode::Loading )
637 QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job );
639 Q_ASSERT( node->loader() == loader );
641 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Load"_s );
643 QgsScopedEvent e(
"3D", QString(
"create" ) );
645 Qt3DCore::QEntity *entity = node->loader()->createEntity(
this );
655 mActiveNodes << node;
658 node->setLoaded( entity );
660 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
662 emit newEntityCreated( entity );
666 node->setHasData(
false );
667 node->cancelLoading();
675 Q_ASSERT( node->state() == QgsChunkNode::Updating );
681 if ( QgsChunkLoader *nodeUpdater = qobject_cast<QgsChunkLoader *>( node->updater() ) )
683 Qt3DCore::QEntity *newEntity = nodeUpdater->createEntity(
this );
684 node->replaceEntity( newEntity );
685 emit newEntityCreated( newEntity );
688 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Update"_s );
693 mActiveJobs.removeOne( job );
699 if ( pendingJobsCount() != oldJobsCount )
700 emit pendingJobsCountChanged();
703void QgsChunkedEntity::startJobs()
705 while ( mActiveJobs.count() < 4 && !mChunkLoaderQueue->isEmpty() )
707 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
709 QgsChunkNode *node = entry->chunk;
712 QgsChunkQueueJob *job = startJob( node );
713 if ( !job->isFinished() )
714 mActiveJobs.append( job );
718QgsChunkQueueJob *QgsChunkedEntity::startJob( QgsChunkNode *node )
720 if ( node->state() == QgsChunkNode::QueuedForLoad )
722 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncBegin, u
"Load"_s );
724 QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
725 connect( loader, &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
727 node->setLoading( loader );
730 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
732 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncBegin, u
"Update"_s );
735 connect( node->updater(), &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
736 node->updater()->start();
737 return node->updater();
746void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob *job )
750 QgsChunkNode *node = job->chunk();
751 disconnect( job, &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
753 if ( node->state() == QgsChunkNode::Loading )
756 node->cancelLoading();
758 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Load"_s );
760 else if ( node->state() == QgsChunkNode::Updating )
763 node->cancelUpdating();
765 addTileTraceEvent( *
this, *node, QgsEventTracing::AsyncEnd, u
"Update"_s );
773 mActiveJobs.removeOne( job );
777void QgsChunkedEntity::cancelActiveJobs()
779 while ( !mActiveJobs.isEmpty() )
781 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)