diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h index 3071acb29..1028d2be2 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h @@ -188,6 +188,11 @@ class CESIUM3DTILESSELECTION_API Tile final { return gsl::span(this->_children); } + /** + * Clears the list of this tile's children. + */ + void clearChildren() { this->_children.clear(); } + /** * @brief Assigns the given child tiles to this tile. * @@ -475,6 +480,16 @@ class CESIUM3DTILESSELECTION_API Tile final { */ bool isEmptyContent() const noexcept; + /** + * @brief Determines if this tile has unknown content. + */ + bool isUnknownContent() const noexcept; + + /** + * Determines if this tile and all of its children are ready to unload. + */ + bool isReadyToUnload() const noexcept; + /** * @brief get the loader that is used to load the tile content. */ @@ -535,6 +550,7 @@ class CESIUM3DTILESSELECTION_API Tile final { std::vector _rasterTiles; friend class TilesetContentManager; + friend class Tileset; friend class MockTilesetContentManagerTestFixture; public: diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index fea035987..168d5df96 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -428,6 +428,7 @@ class CESIUM3DTILESSELECTION_API Tileset final { void _unloadCachedTiles(double timeBudget) noexcept; void _markTileVisited(Tile& tile) noexcept; + void _unloadPendingChildren(Tile& tile) noexcept; void _updateLodTransitions( const FrameState& frameState, @@ -497,6 +498,7 @@ class CESIUM3DTILESSELECTION_API Tileset final { std::vector _workerThreadLoadQueue; Tile::LoadedLinkedList _loadedTiles; + std::list _externalTilesPendingClear; // Holds computed distances, to avoid allocating them on the heap during tile // selection. diff --git a/Cesium3DTilesSelection/src/Tile.cpp b/Cesium3DTilesSelection/src/Tile.cpp index 932dd5b0d..2258f2c65 100644 --- a/Cesium3DTilesSelection/src/Tile.cpp +++ b/Cesium3DTilesSelection/src/Tile.cpp @@ -217,6 +217,26 @@ bool Tile::isEmptyContent() const noexcept { return this->_content.isEmptyContent(); } +bool Tile::isUnknownContent() const noexcept { + return this->_content.isUnknownContent(); +} + +bool Tile::isReadyToUnload() const noexcept { + if (this->getState() != TileLoadState::ContentLoaded && + this->getState() != TileLoadState::Done && + this->getState() != TileLoadState::Unloaded) { + return false; + } + + for (const Tile& child : this->_children) { + if (!child.isReadyToUnload()) { + return false; + } + } + + return true; +} + TilesetContentLoader* Tile::getLoader() const noexcept { return this->_pLoader; } diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index a815e3d0f..85c63412b 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -89,7 +89,6 @@ Tileset::Tileset( ionAssetEndpointUrl)} {} Tileset::~Tileset() noexcept { - this->_pTilesetContentManager->unloadAll(); if (this->_externals.pTileOcclusionProxyPool) { this->_externals.pTileOcclusionProxyPool->destroyPool(); } @@ -1468,7 +1467,32 @@ void Tileset::_processMainThreadLoadQueue() { this->_mainThreadLoadQueue.clear(); } +void Tileset::_unloadPendingChildren(Tile& tile) noexcept { + for (Tile& childTile : tile.getChildren()) { + this->_loadedTiles.remove(childTile); + this->_externalTilesPendingClear.remove(&childTile); + childTile.setState(TileLoadState::Unloaded); + this->_unloadPendingChildren(childTile); + } + + tile.clearChildren(); +} + void Tileset::_unloadCachedTiles(double timeBudget) noexcept { + // Clear children of external tilesets unloaded last frame + Tile* pPendingExternalTile; + while (!this->_externalTilesPendingClear.empty()) { + pPendingExternalTile = this->_externalTilesPendingClear.front(); + this->_externalTilesPendingClear.pop_front(); + // We need to remove children recursively, as children of this tile might + // also be in the _externalTilesPendingClear list + this->_unloadPendingChildren(*pPendingExternalTile); + pPendingExternalTile->setState(TileLoadState::Unloaded); + } + + // Clear list of pending external tiles + this->_externalTilesPendingClear.clear(); + const int64_t maxBytes = this->getOptions().maximumCachedBytes; const Tile* pRootTile = this->_pTilesetContentManager->getRootTile(); @@ -1499,10 +1523,18 @@ void Tileset::_unloadCachedTiles(double timeBudget) noexcept { Tile* pNext = this->_loadedTiles.next(*pTile); + // Check for external content before unloading, as an unloaded tile will + // always have Unknown content set + const bool wasExternalTile = pTile->isExternalContent(); const bool removed = this->_pTilesetContentManager->unloadTileContent(*pTile); if (removed) { this->_loadedTiles.remove(*pTile); + if (wasExternalTile) { + // The Unreal implementation, at the least, requires a frame between a + // tile being unloaded and its pointers becoming invalidated. + this->_externalTilesPendingClear.push_back(pTile); + } } pTile = pNext; @@ -1516,6 +1548,25 @@ void Tileset::_unloadCachedTiles(double timeBudget) noexcept { void Tileset::_markTileVisited(Tile& tile) noexcept { this->_loadedTiles.insertAtTail(tile); + + // If the tile is present in _externalTilesPendingClear, it needs to be removed since we're still using it. + // This way lets us do the find and remove in one search. + auto it = std::find(this->_externalTilesPendingClear.begin(), this->_externalTilesPendingClear.end(), &tile); + if (it == this->_externalTilesPendingClear.end()) { + // Tile isn't in _externalTilesPendingClear, nothing to do. + return; + } + + // Actually remove the tile from the pending list + this->_externalTilesPendingClear.erase(it); + + if (tile.getState() == TileLoadState::Unloaded && + !tile.getChildren().empty()) { + // We were going to clear this tile's children, but it's still in use, so we + // should restore it to Done instead. + tile.setState(TileLoadState::Done); + tile.setContentShouldContinueUpdating(false); + } } void Tileset::addTileToLoadQueue( diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 99907e63f..3734c35d4 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -72,15 +72,6 @@ struct ContentKindSetter { void* pRenderResources; }; -void unloadTileRecursively( - Tile& tile, - TilesetContentManager& tilesetContentManager) { - tilesetContentManager.unloadTileContent(tile); - for (Tile& child : tile.getChildren()) { - unloadTileRecursively(child, tilesetContentManager); - } -} - bool anyRasterOverlaysNeedLoading(const Tile& tile) noexcept { for (const RasterMappedTo3DTile& mapped : tile.getMappedRasterTiles()) { const RasterOverlayTile* pLoading = mapped.getLoadingTile(); @@ -1046,6 +1037,27 @@ void TilesetContentManager::updateTileContent( } } +bool TilesetContentManager::handleUpsampledTileChildren(Tile& tile) { + for (Tile& child : tile.getChildren()) { + if (child.getState() == TileLoadState::ContentLoading && + std::holds_alternative( + child.getTileID())) { + // Yes, a child is upsampling from this tile, so it may be using the + // tile's content from another thread via lambda capture. We can't unload + // it right now. So mark the tile as in the process of unloading and stop + // here. + tile.setState(TileLoadState::Unloading); + return false; + } + + if (!this->handleUpsampledTileChildren(child)) { + return false; + } + } + + return true; +} + bool TilesetContentManager::unloadTileContent(Tile& tile) { TileLoadState state = tile.getState(); if (state == TileLoadState::Unloaded) { @@ -1057,9 +1069,16 @@ bool TilesetContentManager::unloadTileContent(Tile& tile) { } TileContent& content = tile.getContent(); + bool isReadyToUnload = tile.isReadyToUnload(); + bool isExternalContent = tile.isExternalContent(); + + // don't unload external or empty tile while children are still loading + if ((isExternalContent || content.isEmptyContent()) && !isReadyToUnload) { + return false; + } - // don't unload external or empty tile - if (content.isExternalContent() || content.isEmptyContent()) { + // Don't unload this tile if children are still upsampling + if (!this->handleUpsampledTileChildren(tile)) { return false; } @@ -1070,31 +1089,19 @@ bool TilesetContentManager::unloadTileContent(Tile& tile) { } tile.getMappedRasterTiles().clear(); - // Unload the renderer resources and clear any raster overlay tiles. We can do - // this even if the tile can't be fully unloaded because this tile's geometry - // is being using by an async upsample operation (checked below). - switch (state) { - case TileLoadState::ContentLoaded: - unloadContentLoadedState(tile); - break; - case TileLoadState::Done: - unloadDoneState(tile); - break; - default: - break; - } - - // Are any children currently being upsampled from this tile? - for (const Tile& child : tile.getChildren()) { - if (child.getState() == TileLoadState::ContentLoading && - std::holds_alternative( - child.getTileID())) { - // Yes, a child is upsampling from this tile, so it may be using the - // tile's content from another thread via lambda capture. We can't unload - // it right now. So mark the tile as in the process of unloading and stop - // here. - tile.setState(TileLoadState::Unloading); - return false; + if (!tile.isEmptyContent() && !tile.isUnknownContent()) { + // Unload the renderer resources and clear any raster overlay tiles. We can + // do this even if the tile can't be fully unloaded because this tile's + // geometry is being using by an async upsample operation (checked below). + switch (state) { + case TileLoadState::ContentLoaded: + unloadContentLoadedState(tile); + break; + case TileLoadState::Done: + unloadDoneState(tile); + break; + default: + break; } } @@ -1109,7 +1116,7 @@ void TilesetContentManager::unloadAll() { // TODO: use the linked-list of loaded tiles instead of walking the entire // tile tree. if (this->_pRootTile) { - unloadTileRecursively(*this->_pRootTile, *this); + this->unloadTileContent(*this->_pRootTile); } } @@ -1447,8 +1454,10 @@ void TilesetContentManager::updateDoneState( void TilesetContentManager::unloadContentLoadedState(Tile& tile) { TileContent& content = tile.getContent(); TileRenderContent* pRenderContent = content.getRenderContent(); - CESIUM_ASSERT( - pRenderContent && "Tile must have render content to be unloaded"); + if (pRenderContent == nullptr) { + // No resources we need to clean up + return; + } void* pWorkerRenderResources = pRenderContent->getRenderResources(); this->_externals.pPrepareRendererResources->free( @@ -1461,8 +1470,10 @@ void TilesetContentManager::unloadContentLoadedState(Tile& tile) { void TilesetContentManager::unloadDoneState(Tile& tile) { TileContent& content = tile.getContent(); TileRenderContent* pRenderContent = content.getRenderContent(); - CESIUM_ASSERT( - pRenderContent && "Tile must have render content to be unloaded"); + if (pRenderContent == nullptr) { + // No resources to clean up + return; + } void* pMainThreadRenderResources = pRenderContent->getRenderResources(); this->_externals.pPrepareRendererResources->free( diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index d750b2207..fec9d9974 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -65,6 +65,7 @@ class TilesetContentManager void updateTileContent(Tile& tile, const TilesetOptions& tilesetOptions); bool unloadTileContent(Tile& tile); + bool handleUpsampledTileChildren(Tile& tile); void waitUntilIdle();