diff --git a/CHANGES.md b/CHANGES.md index 212dd409e..901471a44 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,7 @@ - Fixed parsing URIs that have a scheme followed by `:` instead of `://`. - Fixed decoding of KHR_mesh_quantization normalized values. +- `Tile` children of external tilesets will now be cleared when the external tileset is unloaded, fixing a memory leak that happened as a result of these `Tile` skeletons accumulating over time. - Requests headers specified in `TilesetOptions` are now included in tile content requests. Previously they were only included in the root tileset.json / layer.json request. - Fixed a crash when loading a `tileset.json` without a valid root tile. diff --git a/CMakeLists.txt b/CMakeLists.txt index e9be6d31e..431cb8b2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -185,6 +185,7 @@ option(CESIUM_TESTS_ENABLED "Whether to enable tests" ON) option(CESIUM_GLM_STRICT_ENABLED "Whether to force strict GLM compile definitions." ON) option(CESIUM_DISABLE_DEFAULT_ELLIPSOID "Whether to disable the WGS84 default value for ellipsoid parameters across cesium-native." OFF) option(CESIUM_MSVC_STATIC_RUNTIME_ENABLED "Whether to enable static linking for MSVC runtimes" OFF) +option(CESIUM_DEBUG_TILE_UNLOADING "Whether to enable tracking of tile _doNotUnloadSubtreeCount modifications for tile unloading debugging." OFF) if (CESIUM_MSVC_STATIC_RUNTIME_ENABLED) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h index 78c08a56c..16dacd9b1 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h @@ -19,9 +19,34 @@ #include #include +#ifdef CESIUM_DEBUG_TILE_UNLOADING +#include +#endif + namespace Cesium3DTilesSelection { class TilesetContentLoader; +#ifdef CESIUM_DEBUG_TILE_UNLOADING +class TileDoNotUnloadSubtreeCountTracker { +private: + struct Entry { + std::string reason; + bool increment; + int32_t newCount; + }; + +public: + static void addEntry( + const uint64_t id, + bool increment, + const std::string& reason, + int32_t newCount); + +private: + static std::unordered_map> _entries; +}; +#endif + /** * The current state of this tile in the loading process. */ @@ -188,6 +213,13 @@ class CESIUM3DTILESSELECTION_API Tile final { return std::span(this->_children); } + /** + * @brief Clears the children of this tile. + * + * This function is not supposed to be called by clients. + */ + void clearChildren() noexcept; + /** * @brief Assigns the given child tiles to this tile. * @@ -485,7 +517,53 @@ class CESIUM3DTILESSELECTION_API Tile final { */ TileLoadState getState() const noexcept; + /** + * @brief Returns the internal count denoting that the tile and its ancestors + * should not be unloaded. + * + * This function is not supposed to be called by clients. + */ + int32_t getDoNotUnloadSubtreeCount() const noexcept { + return this->_doNotUnloadSubtreeCount; + } + + /** + * @brief Increments the internal count denoting that the tile and its + * ancestors should not be unloaded. + * + * This function is not supposed to be called by clients. + */ + void incrementDoNotUnloadSubtreeCount(const char* reason) noexcept; + + /** + * @brief Decrements the internal count denoting that the tile and its + * ancestors should not be unloaded. + * + * This function is not supposed to be called by clients. + */ + void decrementDoNotUnloadSubtreeCount(const char* reason) noexcept; + + /** + * @brief Increments the internal count denoting that the tile and its + * ancestors should not be unloaded starting with this tile's parent. + * + * This function is not supposed to be called by clients. + */ + void incrementDoNotUnloadSubtreeCountOnParent(const char* reason) noexcept; + + /** + * @brief Decrements the internal count denoting that the tile and its + * ancestors should not be unloaded starting with this tile's parent. + * + * This function is not supposed to be called by clients. + */ + void decrementDoNotUnloadSubtreeCountOnParent(const char* reason) noexcept; + private: + void incrementDoNotUnloadSubtreeCount(const std::string& reason) noexcept; + + void decrementDoNotUnloadSubtreeCount(const std::string& reason) noexcept; + struct TileConstructorImpl {}; template < typename... TileContentArgs, @@ -548,6 +626,10 @@ class CESIUM3DTILESSELECTION_API Tile final { // mapped raster overlay std::vector _rasterTiles; + // Number of existing claims on this tile preventing it and its parent + // external tileset (if any) from being unloaded from the tree. + int32_t _doNotUnloadSubtreeCount = 0; + friend class TilesetContentManager; friend class MockTilesetContentManagerTestFixture; diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index c643db240..2bbc11d05 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -471,6 +471,7 @@ class CESIUM3DTILESSELECTION_API Tileset final { void _processWorkerThreadLoadQueue(); void _processMainThreadLoadQueue(); + void _clearChildrenRecursively(Tile* pTile) noexcept; void _unloadCachedTiles(double timeBudget) noexcept; void _markTileVisited(Tile& tile) noexcept; diff --git a/Cesium3DTilesSelection/src/Tile.cpp b/Cesium3DTilesSelection/src/Tile.cpp index 82e7f1343..34108134f 100644 --- a/Cesium3DTilesSelection/src/Tile.cpp +++ b/Cesium3DTilesSelection/src/Tile.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -16,12 +17,41 @@ #include #include +#ifdef CESIUM_DEBUG_TILE_UNLOADING +#include +#endif + using namespace CesiumGeometry; using namespace CesiumGeospatial; using namespace CesiumUtility; using namespace std::string_literals; namespace Cesium3DTilesSelection { +#ifdef CESIUM_DEBUG_TILE_UNLOADING +std::unordered_map< + std::string, + std::vector> + TileDoNotUnloadSubtreeCountTracker::_entries; + +void TileDoNotUnloadSubtreeCountTracker::addEntry( + uint64_t id, + bool increment, + const std::string& reason, + int32_t newCount) { + const std::string idString = fmt::format("{:x}", id); + const auto foundIt = + TileDoNotUnloadSubtreeCountTracker::_entries.find(idString); + if (foundIt != TileDoNotUnloadSubtreeCountTracker::_entries.end()) { + foundIt->second.push_back(Entry{reason, increment, newCount}); + } else { + std::vector entries{Entry{reason, increment, newCount}}; + + TileDoNotUnloadSubtreeCountTracker::_entries.insert( + {idString, std::move(entries)}); + } +} +#endif + Tile::Tile(TilesetContentLoader* pLoader) noexcept : Tile(TileConstructorImpl{}, TileLoadState::Unloaded, pLoader) {} @@ -80,7 +110,11 @@ Tile::Tile(Tile&& rhs) noexcept _content(std::move(rhs._content)), _pLoader{rhs._pLoader}, _loadState{rhs._loadState}, - _mightHaveLatentChildren{rhs._mightHaveLatentChildren} { + _mightHaveLatentChildren{rhs._mightHaveLatentChildren}, + _rasterTiles(std::move(rhs._rasterTiles)), + // See the move assignment operator for an explanation of why we copy + // `_doNotUnloadSubtreeCount` here. + _doNotUnloadSubtreeCount(rhs._doNotUnloadSubtreeCount) { // since children of rhs will have the parent pointed to rhs, // we will reparent them to this tile as rhs will be destroyed after this for (Tile& tile : this->_children) { @@ -111,7 +145,23 @@ Tile& Tile::operator=(Tile&& rhs) noexcept { this->_content = std::move(rhs._content); this->_pLoader = rhs._pLoader; this->_loadState = rhs._loadState; + this->_rasterTiles = std::move(rhs._rasterTiles); this->_mightHaveLatentChildren = rhs._mightHaveLatentChildren; + + // A "count" in the `rhs` could, in theory, represent an external + // pointer that references that Tile. In that case, we wouldn't want to copy + // that "count" to this tile because the target of that pointer is not going + // to change over to this Tile. + + // However, when a "count" represents loaded content in this tile's subtree, + // that _will_ move over, and so it's essential we copy that count over to + // the target. + + // There's no way to tell the difference between these two cases. However, + // as a practical matter, we take pains to avoid having pointers to Tiles + // that we're moving out of, and so we can safely assume that all "counts" + // refer to loaded subtree content instead of pointers. + this->_doNotUnloadSubtreeCount = rhs._doNotUnloadSubtreeCount; } return *this; @@ -122,9 +172,33 @@ void Tile::createChildTiles(std::vector&& children) { throw std::runtime_error("Children already created."); } + const int32_t prevDoNotUnloadSubtreeCount = this->_doNotUnloadSubtreeCount; this->_children = std::move(children); for (Tile& tile : this->_children) { tile.setParent(this); + // If a tile is created with children that are already ContentLoaded, we + // bypassed the normal route that _doNotUnloadSubtreeCount would be + // incremented by. We have to manually increment it or else we will see a + // mismatch when trying to unload the tile and fail the assertion. + if (tile.getState() == TileLoadState::ContentLoaded) { + ++this->_doNotUnloadSubtreeCount; + } + + // Add the child's count to our count, as it might represent a tile lower + // down on the tree that's loaded that we can't see from here. None of the + // children should have other references to their tile pointer at this + // moment so this count should just represent loaded children. + this->_doNotUnloadSubtreeCount += tile._doNotUnloadSubtreeCount; + } + + const int32_t addedDoNotUnloadSubtreeCount = + this->_doNotUnloadSubtreeCount - prevDoNotUnloadSubtreeCount; + if (addedDoNotUnloadSubtreeCount > 0) { + Tile* pParent = this->getParent(); + while (pParent != nullptr) { + pParent->_doNotUnloadSubtreeCount += addedDoNotUnloadSubtreeCount; + pParent = pParent->getParent(); + } } } @@ -243,4 +317,82 @@ void Tile::setMightHaveLatentChildren(bool mightHaveLatentChildren) noexcept { this->_mightHaveLatentChildren = mightHaveLatentChildren; } +void Tile::clearChildren() noexcept { + CESIUM_ASSERT(this->_doNotUnloadSubtreeCount == 0); + this->_children.clear(); +} + +void Tile::incrementDoNotUnloadSubtreeCount( + [[maybe_unused]] const char* reason) noexcept { +#ifdef CESIUM_DEBUG_TILE_UNLOADING + const std::string reasonStr = fmt::format( + "Initiator ID: {:x}, {}", + reinterpret_cast(this), + reason); + this->incrementDoNotUnloadSubtreeCount(reasonStr); +#else + this->incrementDoNotUnloadSubtreeCount(std::string()); +#endif +} + +void Tile::decrementDoNotUnloadSubtreeCount( + [[maybe_unused]] const char* reason) noexcept { +#ifdef CESIUM_DEBUG_TILE_UNLOADING + const std::string reasonStr = fmt::format( + "Initiator ID: {:x}, {}", + reinterpret_cast(this), + reason); + this->decrementDoNotUnloadSubtreeCount(reasonStr); +#else + this->decrementDoNotUnloadSubtreeCount(std::string()); +#endif +} + +void Tile::incrementDoNotUnloadSubtreeCountOnParent( + const char* reason) noexcept { + if (this->getParent() != nullptr) { + this->getParent()->incrementDoNotUnloadSubtreeCount(reason); + } +} + +void Tile::decrementDoNotUnloadSubtreeCountOnParent( + const char* reason) noexcept { + if (this->getParent() != nullptr) { + this->getParent()->decrementDoNotUnloadSubtreeCount(reason); + } +} + +void Tile::incrementDoNotUnloadSubtreeCount( + [[maybe_unused]] const std::string& reason) noexcept { + Tile* pTile = this; + while (pTile != nullptr) { + ++pTile->_doNotUnloadSubtreeCount; +#ifdef CESIUM_DEBUG_TILE_UNLOADING + TileDoNotUnloadSubtreeCountTracker::addEntry( + reinterpret_cast(pTile), + true, + std::string(reason), + pTile->_doNotUnloadSubtreeCount); +#endif + pTile = pTile->getParent(); + } +} + +void Tile::decrementDoNotUnloadSubtreeCount( + [[maybe_unused]] const std::string& reason) noexcept { + CESIUM_ASSERT(this->_doNotUnloadSubtreeCount > 0); + Tile* pTile = this; + while (pTile != nullptr) { + --pTile->_doNotUnloadSubtreeCount; +#ifdef CESIUM_DEBUG_TILE_UNLOADING + TileDoNotUnloadSubtreeCountTracker::addEntry( + reinterpret_cast(pTile), + false, + std::string(reason), + pTile->_doNotUnloadSubtreeCount); +#endif + pTile = pTile->getParent(); + } +} + } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index 2e0a41f52..fe4a8de85 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -251,6 +251,8 @@ void Tileset::_updateLodTransitions( if (!pRenderContent) { // This tile is done fading out and was immediately kicked from the // cache. + (*tileIt)->decrementDoNotUnloadSubtreeCount( + "Tileset::_updateLodTransitions done fading out"); tileIt = result.tilesFadingOut.erase(tileIt); continue; } @@ -262,6 +264,8 @@ void Tileset::_updateLodTransitions( if (selectionResult == TileSelectionState::Result::Rendered) { // This tile will already be on the render list. pRenderContent->setLodTransitionFadePercentage(0.0f); + (*tileIt)->decrementDoNotUnloadSubtreeCount( + "Tileset::_updateLodTransitions in render list"); tileIt = result.tilesFadingOut.erase(tileIt); continue; } @@ -273,6 +277,8 @@ void Tileset::_updateLodTransitions( // The client will already have had a chance to stop rendering the tile // last frame. pRenderContent->setLodTransitionFadePercentage(0.0f); + (*tileIt)->decrementDoNotUnloadSubtreeCount( + "Tileset::_updateLodTransitions done fading out"); tileIt = result.tilesFadingOut.erase(tileIt); continue; } @@ -323,6 +329,11 @@ Tileset::updateViewOffline(const std::vector& frustums) { this->updateView(frustums, 0.0f); } + for (Tile* pTile : this->_updateResult.tilesFadingOut) { + pTile->decrementDoNotUnloadSubtreeCount( + "Tileset::updateViewOffline clear tilesFadingOut"); + } + this->_updateResult.tilesFadingOut.clear(); std::unordered_set uniqueTilesToRenderThisFrame( @@ -335,6 +346,8 @@ Tileset::updateViewOffline(const std::vector& frustums) { if (pRenderContent) { pRenderContent->setLodTransitionFadePercentage(1.0f); this->_updateResult.tilesFadingOut.insert(tile); + tile->incrementDoNotUnloadSubtreeCount( + "Tileset::updateViewOffline start fading out"); } } } @@ -368,6 +381,10 @@ Tileset::updateView(const std::vector& frustums, float deltaTime) { result.maxDepthVisited = 0; if (!_options.enableLodTransitionPeriod) { + for (Tile* pTile : this->_updateResult.tilesFadingOut) { + pTile->decrementDoNotUnloadSubtreeCount( + "Tileset::updateView clear tilesFadingOut"); + } result.tilesFadingOut.clear(); } @@ -630,6 +647,7 @@ void markTileNonRendered( (lastResult == TileSelectionState::Result::Refined && tile.getRefine() == TileRefine::Add)) { result.tilesFadingOut.insert(&tile); + tile.incrementDoNotUnloadSubtreeCount("markTileNonRendered fading out"); TileRenderContent* pRenderContent = tile.getContent().getRenderContent(); if (pRenderContent) { pRenderContent->setLodTransitionFadePercentage(0.0f); @@ -1623,6 +1641,20 @@ void Tileset::_processMainThreadLoadQueue() { this->_mainThreadLoadQueue.clear(); } +void Tileset::_clearChildrenRecursively(Tile* pTile) noexcept { + // Iterate through all children, calling this method recursively to make sure + // children are all removed from _loadedTiles. + for (Tile& child : pTile->getChildren()) { + CESIUM_ASSERT(child.getState() == TileLoadState::Unloaded); + CESIUM_ASSERT(child.getDoNotUnloadSubtreeCount() == 0); + CESIUM_ASSERT(child.getContent().isUnknownContent()); + this->_loadedTiles.remove(child); + this->_clearChildrenRecursively(&child); + } + + pTile->clearChildren(); +} + void Tileset::_unloadCachedTiles(double timeBudget) noexcept { const int64_t maxBytes = this->getOptions().maximumCachedBytes; @@ -1637,6 +1669,8 @@ void Tileset::_unloadCachedTiles(double timeBudget) noexcept { : (start + std::chrono::microseconds( static_cast(1000.0 * timeBudget))); + std::vector tilesNeedingChildrenCleared; + while (this->getTotalDataBytes() > maxBytes) { if (pTile == nullptr || pTile == pRootTile) { // We've either removed all tiles or the next tile is the root. @@ -1654,12 +1688,16 @@ void Tileset::_unloadCachedTiles(double timeBudget) noexcept { Tile* pNext = this->_loadedTiles.next(*pTile); - const bool removed = + const UnloadTileContentResult removed = this->_pTilesetContentManager->unloadTileContent(*pTile); - if (removed) { + if (removed != UnloadTileContentResult::Keep) { this->_loadedTiles.remove(*pTile); } + if (removed == UnloadTileContentResult::RemoveAndClearChildren) { + tilesNeedingChildrenCleared.emplace_back(pTile); + } + pTile = pNext; auto time = std::chrono::system_clock::now(); @@ -1667,6 +1705,13 @@ void Tileset::_unloadCachedTiles(double timeBudget) noexcept { break; } } + + if (!tilesNeedingChildrenCleared.empty()) { + for (Tile* pTileToClear : tilesNeedingChildrenCleared) { + CESIUM_ASSERT(pTileToClear->getDoNotUnloadSubtreeCount() == 0); + this->_clearChildrenRecursively(pTileToClear); + } + } } void Tileset::_markTileVisited(Tile& tile) noexcept { diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 6750ef025..69182b268 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -1037,6 +1037,9 @@ void TilesetContentManager::loadTileContent( } } + tile.incrementDoNotUnloadSubtreeCount( + "TilesetContentManager::loadTileContent begin"); + // map raster overlay to tile std::vector projections = mapOverlaysToTile(tile, this->_overlayCollection, tilesetOptions); @@ -1108,15 +1111,22 @@ void TilesetContentManager::loadTileContent( }) .thenInMainThread([&tile, thiz](TileLoadResultAndRenderResources&& pair) { setTileContent(tile, std::move(pair.result), pair.pRenderResources); + tile.decrementDoNotUnloadSubtreeCount( + "TilesetContentManager::loadTileContent done loading"); thiz->notifyTileDoneLoading(&tile); }) .catchInMainThread([pLogger = this->_externals.pLogger, &tile, thiz]( std::exception&& e) { + tile.getMappedRasterTiles().clear(); + tile.setState(TileLoadState::Failed); + + tile.decrementDoNotUnloadSubtreeCount( + "TilesetContentManager::loadTileContent error while loading"); thiz->notifyTileDoneLoading(&tile); SPDLOG_LOGGER_ERROR( pLogger, - "An unexpected error occurs when loading tile: {}", + "An unexpected error occurred when loading tile: {}", e.what()); }); } @@ -1162,21 +1172,42 @@ void TilesetContentManager::createLatentChildrenIfNecessary( } } -bool TilesetContentManager::unloadTileContent(Tile& tile) { +UnloadTileContentResult TilesetContentManager::unloadTileContent(Tile& tile) { TileLoadState state = tile.getState(); if (state == TileLoadState::Unloaded) { - return true; + return UnloadTileContentResult::Remove; } if (state == TileLoadState::ContentLoading) { - return false; + return UnloadTileContentResult::Keep; } TileContent& content = tile.getContent(); - // don't unload external or empty tile - if (content.isExternalContent() || content.isEmptyContent()) { - return false; + // We can unload empty content at any time. + if (content.isEmptyContent()) { + notifyTileUnloading(&tile); + content.setContentKind(TileUnknownContent{}); + tile.setState(TileLoadState::Unloaded); + tile.decrementDoNotUnloadSubtreeCountOnParent( + "TilesetContentManager::unloadTileContent unload empty content"); + return UnloadTileContentResult::Remove; + } + + if (content.isExternalContent()) { + // Tile with external content that still has references to its pointer or to + // its children's pointers - we can't unload. + // We also, of course, don't want to unload the root tile. + if (tile.getParent() == nullptr || tile.getDoNotUnloadSubtreeCount() > 0) { + return UnloadTileContentResult::Keep; + } + + notifyTileUnloading(&tile); + content.setContentKind(TileUnknownContent{}); + tile.setState(TileLoadState::Unloaded); + tile.decrementDoNotUnloadSubtreeCountOnParent( + "TilesetContentManager::unloadTileContent unload external content"); + return UnloadTileContentResult::RemoveAndClearChildren; } // Detach raster tiles first so that the renderer's tile free @@ -1210,15 +1241,19 @@ bool TilesetContentManager::unloadTileContent(Tile& tile) { // it right now. So mark the tile as in the process of unloading and stop // here. tile.setState(TileLoadState::Unloading); - return false; + return UnloadTileContentResult::Keep; } } // If we make it this far, the tile's content will be fully unloaded. notifyTileUnloading(&tile); + if (!content.isUnknownContent()) { + tile.decrementDoNotUnloadSubtreeCountOnParent( + "TilesetContentManager::unloadTileContent unload render content"); + } content.setContentKind(TileUnknownContent{}); tile.setState(TileLoadState::Unloaded); - return true; + return UnloadTileContentResult::Remove; } void TilesetContentManager::unloadAll() { @@ -1401,6 +1436,11 @@ void TilesetContentManager::setTileContent( pWorkerRenderResources}, std::move(result.contentKind)); + if (!tile.getContent().isUnknownContent()) { + tile.incrementDoNotUnloadSubtreeCountOnParent( + "TilesetContentManager::setTileContent"); + } + if (result.tileInitializer) { result.tileInitializer(tile); } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index f8c749482..769c61692 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -18,6 +18,26 @@ namespace Cesium3DTilesSelection { +/** + * @brief Represents the result of calling \ref + * TilesetContentManager::unloadTileContent. + */ +enum class UnloadTileContentResult : uint8_t { + /** + * @brief The tile should remain in the loaded tiles list. + */ + Keep = 0, + /** + * @brief The tile should be removed from the loaded tiles list. + */ + Remove = 1, + /** + * @brief The tile should be removed from the loaded tiles list and have its + * children cleared. + */ + RemoveAndClearChildren = 3 +}; + class TilesetSharedAssetSystem; class TilesetContentManager @@ -86,7 +106,7 @@ class TilesetContentManager Tile& tile, const TilesetOptions& tilesetOptions); - bool unloadTileContent(Tile& tile); + UnloadTileContentResult unloadTileContent(Tile& tile); void waitUntilIdle(); diff --git a/Cesium3DTilesSelection/src/TilesetHeightQuery.cpp b/Cesium3DTilesSelection/src/TilesetHeightQuery.cpp index b8d27b379..810a572f9 100644 --- a/Cesium3DTilesSelection/src/TilesetHeightQuery.cpp +++ b/Cesium3DTilesSelection/src/TilesetHeightQuery.cpp @@ -117,6 +117,25 @@ TilesetHeightQuery::TilesetHeightQuery( candidateTiles(), previousCandidateTiles() {} +Cesium3DTilesSelection::TilesetHeightQuery::~TilesetHeightQuery() { + for (Tile* pTile : candidateTiles) { + pTile->decrementDoNotUnloadSubtreeCount( + "TilesetHeightQuery::~TilesetHeightQuery destructing candidateTiles"); + } + + for (Tile* pTile : additiveCandidateTiles) { + pTile->decrementDoNotUnloadSubtreeCount( + "TilesetHeightQuery::~TilesetHeightQuery " + "destructing additiveCandidateTiles"); + } + + for (Tile* pTile : previousCandidateTiles) { + pTile->decrementDoNotUnloadSubtreeCount( + "TilesetHeightQuery::~TilesetHeightQuery " + "destructing previousCandidateTiles"); + } +} + void TilesetHeightQuery::intersectVisibleTile( Tile* pTile, std::vector& outWarnings) { @@ -189,9 +208,14 @@ void TilesetHeightQuery::findCandidateTiles( *contentBoundingVolume, this->ray, this->inputPosition, - this->ellipsoid)) + this->ellipsoid)) { + pTile->incrementDoNotUnloadSubtreeCount( + "TilesetHeightQuery::findCandidateTiles add to candidateTiles"); this->candidateTiles.push_back(pTile); + } } else { + pTile->incrementDoNotUnloadSubtreeCount( + "TilesetHeightQuery::findCandidateTiles add to candidateTiles"); this->candidateTiles.push_back(pTile); } } else { @@ -205,9 +229,16 @@ void TilesetHeightQuery::findCandidateTiles( *contentBoundingVolume, this->ray, this->inputPosition, - this->ellipsoid)) + this->ellipsoid)) { + pTile->incrementDoNotUnloadSubtreeCount( + "TilesetHeightQuery::findCandidateTiles add to " + "additiveCandidateTiles"); this->additiveCandidateTiles.push_back(pTile); + } } else { + pTile->incrementDoNotUnloadSubtreeCount( + "TilesetHeightQuery::findCandidateTiles add to " + "additiveCandidateTiles"); this->additiveCandidateTiles.push_back(pTile); } } @@ -257,7 +288,22 @@ void TilesetHeightQuery::findCandidateTiles( } } + // Decrement doNotUnloadCount for tiles currently in the queue, as the queue + // will be overwritten after this. + for (Tile* pTile : heightQueryLoadQueue) { + pTile->decrementDoNotUnloadSubtreeCount( + "TilesetHeightRequest::processHeightRequests clear from " + "heightQueryLoadQueue"); + } + heightQueryLoadQueue.assign(tileLoadSet.begin(), tileLoadSet.end()); + + // Track the pointers in the load queue in doNotUnloadCount + for (Tile* pTile : heightQueryLoadQueue) { + pTile->incrementDoNotUnloadSubtreeCount( + "TilesetHeightRequest::processHeightRequests assign to " + "heightQueryLoadQueue"); + } } void Cesium3DTilesSelection::TilesetHeightRequest::failHeightRequests( @@ -325,6 +371,12 @@ bool TilesetHeightRequest::tryCompleteHeightRequest( // frame. std::swap(query.candidateTiles, query.previousCandidateTiles); + for (Tile* pTile : query.candidateTiles) { + pTile->decrementDoNotUnloadSubtreeCount( + "TilesetHeightRequest::tryCompleteHeightRequest clear " + "candidateTiles"); + } + query.candidateTiles.clear(); for (Tile* pCandidate : query.previousCandidateTiles) { @@ -337,6 +389,9 @@ bool TilesetHeightRequest::tryCompleteHeightRequest( markTileVisited(loadedTiles, pCandidate); // Check again next frame to see if this tile has children. + pCandidate->incrementDoNotUnloadSubtreeCount( + "TilesetHeightRequest::tryCompleteHeightRequest add to " + "candidateTiles"); query.candidateTiles.emplace_back(pCandidate); } } diff --git a/Cesium3DTilesSelection/src/TilesetHeightQuery.h b/Cesium3DTilesSelection/src/TilesetHeightQuery.h index f8e883f1d..b8407479e 100644 --- a/Cesium3DTilesSelection/src/TilesetHeightQuery.h +++ b/Cesium3DTilesSelection/src/TilesetHeightQuery.h @@ -31,6 +31,8 @@ class TilesetHeightQuery { const CesiumGeospatial::Cartographic& position, const CesiumGeospatial::Ellipsoid& ellipsoid); + ~TilesetHeightQuery(); + /** * @brief The original input position for which the height is to be queried. */ diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp index 69349f56b..3c03893b8 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp @@ -633,31 +633,17 @@ std::optional parseTileJsonRecursively( } } - if (contentUri) { - Tile tile{¤tLoader}; - tile.setTileID(contentUri); - tile.setTransform(tileTransform); - tile.setBoundingVolume(tileBoundingVolume); - tile.setViewerRequestVolume(tileViewerRequestVolume); - tile.setGeometricError(tileGeometricError); - tile.setRefine(tileRefine); - tile.setContentBoundingVolume(tileContentBoundingVolume); - tile.createChildTiles(std::move(childTiles)); - - return tile; - } else { - Tile tile{¤tLoader, TileEmptyContent{}}; - tile.setTileID(""); - tile.setTransform(tileTransform); - tile.setBoundingVolume(tileBoundingVolume); - tile.setViewerRequestVolume(tileViewerRequestVolume); - tile.setGeometricError(tileGeometricError); - tile.setRefine(tileRefine); - tile.setContentBoundingVolume(tileContentBoundingVolume); - tile.createChildTiles(std::move(childTiles)); - - return tile; - } + Tile tile{¤tLoader}; + tile.setTileID(contentUri ? contentUri : std::string{}); + tile.setTransform(tileTransform); + tile.setBoundingVolume(tileBoundingVolume); + tile.setViewerRequestVolume(tileViewerRequestVolume); + tile.setGeometricError(tileGeometricError); + tile.setRefine(tileRefine); + tile.setContentBoundingVolume(tileContentBoundingVolume); + tile.createChildTiles(std::move(childTiles)); + + return tile; } TilesetContentLoaderResult parseTilesetJson( @@ -960,6 +946,25 @@ TilesetJsonLoader::loadTileContent(const TileLoadInput& loadInput) { const auto& pLogger = loadInput.pLogger; const auto& requestHeaders = loadInput.requestHeaders; const auto& contentOptions = loadInput.contentOptions; + + // If the URL is empty, this tile is empty content and we don't need to make a + // web request to complete the loading process (in fact, a web request would + // produce incorrect results as it would just be a request for _baseUrl). + if (url->empty()) { + return loadInput.asyncSystem.createResolvedFuture( + TileLoadResult{ + TileEmptyContent{}, + this->_upAxis, + std::nullopt, + std::nullopt, + std::nullopt, + pAssetAccessor, + nullptr, + {}, + TileLoadResultState::Success, + ellipsoid}); + } + std::string resolvedUrl = CesiumUtility::Uri::resolve(this->_baseUrl, *url, true); return pAssetAccessor->get(asyncSystem, resolvedUrl, requestHeaders) diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index 408a11669..c9a602730 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -843,12 +843,12 @@ TEST_CASE("Test tile state machine") { // trying to unload parent while upsampled children is loading while put the // tile into the Unloading state but not unload the render content. - CHECK(!pManager->unloadTileContent(tile)); + CHECK(pManager->unloadTileContent(tile) == UnloadTileContentResult::Keep); CHECK(tile.getState() == TileLoadState::Unloading); CHECK(tile.isRenderContent()); // Unloading again will have the same result. - CHECK(!pManager->unloadTileContent(tile)); + CHECK(pManager->unloadTileContent(tile) == UnloadTileContentResult::Keep); CHECK(tile.getState() == TileLoadState::Unloading); CHECK(tile.isRenderContent()); @@ -863,13 +863,15 @@ TEST_CASE("Test tile state machine") { // trying to unload parent will work now since the upsampled tile is already // in the main thread - CHECK(pManager->unloadTileContent(tile)); + CHECK(pManager->unloadTileContent(tile) == UnloadTileContentResult::Remove); CHECK(tile.getState() == TileLoadState::Unloaded); CHECK(!tile.isRenderContent()); CHECK(!tile.getContent().getRenderContent()); // unload upsampled tile: ContentLoaded -> Done - CHECK(pManager->unloadTileContent(upsampledTile)); + CHECK( + pManager->unloadTileContent(upsampledTile) == + UnloadTileContentResult::Remove); CHECK(upsampledTile.getState() == TileLoadState::Unloaded); CHECK(!upsampledTile.isRenderContent()); CHECK(!upsampledTile.getContent().getRenderContent()); diff --git a/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp b/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp index aaf4c31af..2aa9b4ee1 100644 --- a/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp @@ -333,6 +333,11 @@ TEST_CASE("Test creating tileset json loader") { } SUBCASE("Tileset with empty tile") { + std::shared_ptr pMockAssetAccessor = + std::make_shared( + std::map>()); + AsyncSystem asyncSystem{std::make_shared()}; + auto loaderResult = createTilesetJsonLoader( testDataPath / "MultipleKindsOfTilesets" / "EmptyTileTileset.json"); CHECK(!loaderResult.errors.hasErrors()); @@ -342,7 +347,20 @@ TEST_CASE("Test creating tileset json loader") { CHECK(pRootTile->getGeometricError() == Approx(70.0)); CHECK(pRootTile->getChildren().size() == 1); - const Tile& child = pRootTile->getChildren().front(); + Tile& child = pRootTile->getChildren().front(); + auto future = loaderResult.pLoader->loadTileContent(TileLoadInput{ + child, + {}, + asyncSystem, + pMockAssetAccessor, + spdlog::default_logger(), + {}}); + TileLoadResult result = future.wait(); + REQUIRE(result.state == TileLoadResultState::Success); + TileEmptyContent* emptyContent = + std::get_if(&result.contentKind); + REQUIRE(emptyContent); + child.getContent().setContentKind(*emptyContent); CHECK(child.isEmptyContent()); // check loader up axis diff --git a/cmake/macros/configure_cesium_library.cmake b/cmake/macros/configure_cesium_library.cmake index b64a7ef3f..2e13fe82b 100644 --- a/cmake/macros/configure_cesium_library.cmake +++ b/cmake/macros/configure_cesium_library.cmake @@ -26,6 +26,14 @@ function(configure_cesium_library targetName) ) endif() + if (CESIUM_DEBUG_TILE_UNLOADING) + target_compile_definitions( + ${targetName} + PUBLIC + CESIUM_DEBUG_TILE_UNLOADING + ) + endif() + if (BUILD_SHARED_LIBS) target_compile_definitions( ${targetName} diff --git a/doc/topics/selection-algorithm-details.md b/doc/topics/selection-algorithm-details.md index 8b913c2d6..279d43c76 100644 --- a/doc/topics/selection-algorithm-details.md +++ b/doc/topics/selection-algorithm-details.md @@ -243,10 +243,7 @@ Cesium Native supports implicit tiling by lazily transforming the implicit repre Implicit [loaders](#tileset-content-loader), such as `ImplicitQuadtreeLoader`, `ImplicitOctreeLoader`, and `LayerJsonTerrainLoader`, implement this method by determining in their own way whether this tile has any children, and creating them if it does. In some cases, extra asynchronous work, like downloading subtree availability files, may be necessary to determine if children exist. In that case, the `createTileChildren` will return [TileLoadResultState::RetryLater](\ref Cesium3DTilesSelection::TileLoadResultState::RetryLater) to signal that children may exist, but they can't be created yet. The selection algorithm will try again next frame if the tile's children are still needed. -Currently, a `Tile` instance, once created, will not be destroyed until the entire [Tileset](\ref Cesium3DTilesSelection::Tileset) is destroyed. This is true for `Tile` instances created explicitly from `tileset.json` as well as `Tile` instances created lazily by the implicit loaders. This is convenient because we don't need to worry about a `Tile` instance vanishing unexpectedly, but it can cause a slow increase in memory usage over time. - -> [!note] -> The above refers to `Tile` instances, _not_ their content. Content is unloaded when it is no longer needed. This is important because content is by far the largest portion of a tile. +These `Tile` instances may be destroyed in the future, so it is important not to hold onto pointers to them. However, a `Tile` instance will not be destroyed if it was returned in `ViewUpdateResult` in the last call to `updateView`, nor if it is currently being used for any height queries. Before a `Tile` is destroyed, its [renderer resources](#rendering-3d-tiles) will be freed. ## Additional Topics Not Yet Covered {#additional-topics}