Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unload content of external tilesets to save memory usage #876

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ class CESIUM3DTILESSELECTION_API Tile final {
return gsl::span<const Tile>(this->_children);
}

/**
* Clears the list of this tile's children.
*/
void clearChildren() { this->_children.clear(); }

/**
* @brief Assigns the given child tiles to this tile.
*
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -535,6 +550,7 @@ class CESIUM3DTILESSELECTION_API Tile final {
std::vector<RasterMappedTo3DTile> _rasterTiles;

friend class TilesetContentManager;
friend class Tileset;
friend class MockTilesetContentManagerTestFixture;

public:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,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,
Expand Down Expand Up @@ -485,6 +486,7 @@ class CESIUM3DTILESSELECTION_API Tileset final {
std::vector<TileLoadTask> _workerThreadLoadQueue;

Tile::LoadedLinkedList _loadedTiles;
Tile::LoadedLinkedList _externalTilesPendingClear;

// Holds computed distances, to avoid allocating them on the heap during tile
// selection.
Expand Down
20 changes: 20 additions & 0 deletions Cesium3DTilesSelection/src/Tile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
35 changes: 34 additions & 1 deletion Cesium3DTilesSelection/src/Tileset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Tileset::Tileset(
_externals,
_options,
RasterOverlayCollection{_loadedTiles, externals},
_loadedTiles,
std::vector<CesiumAsync::IAssetAccessor::THeader>{},
std::move(pCustomLoader),
std::move(pRootTile))} {}
Expand All @@ -66,6 +67,7 @@ Tileset::Tileset(
_externals,
_options,
RasterOverlayCollection{_loadedTiles, externals},
_loadedTiles,
url)} {}

Tileset::Tileset(
Expand All @@ -84,12 +86,12 @@ Tileset::Tileset(
_externals,
_options,
RasterOverlayCollection{_loadedTiles, externals},
_loadedTiles,
ionAssetID,
ionAccessToken,
ionAssetEndpointUrl)} {}

Tileset::~Tileset() noexcept {
this->_pTilesetContentManager->unloadAll();
if (this->_externals.pTileOcclusionProxyPool) {
this->_externals.pTileOcclusionProxyPool->destroyPool();
}
Expand Down Expand Up @@ -1461,7 +1463,30 @@ void Tileset::_processMainThreadLoadQueue() {
this->_mainThreadLoadQueue.clear();
}

void Tileset::_unloadPendingChildren(Tile& tile) noexcept {
for (Tile& childTile : tile.getChildren()) {
this->_externalTilesPendingClear.remove(childTile);
childTile.setState(TileLoadState::Unloaded);
this->_unloadPendingChildren(childTile);
}
}

void Tileset::_unloadCachedTiles(double timeBudget) noexcept {
// Clear children of external tilesets unloaded last frame
Tile* pPendingExternalTile;
while ((pPendingExternalTile = this->_externalTilesPendingClear.head()) !=
nullptr) {
this->_externalTilesPendingClear.remove(*pPendingExternalTile);
// 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);
pPendingExternalTile->clearChildren();
}

// Clear list of pending external tiles
this->_externalTilesPendingClear = Tile::LoadedLinkedList();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't do what you want. In fact, we should probably disable the assignment operator on this class. Take a look at how this LoadedLinkedList works. It's an "intrusive" linked list, where the links between nodes are actually stored in the data (Tiles) themselves. This means that a tile can only be in one list at a time. And clearing the tiles from a list requires traversing the entire list and setting the previous and next pointers in each tile to nullptr.


const int64_t maxBytes = this->getOptions().maximumCachedBytes;

const Tile* pRootTile = this->_pTilesetContentManager->getRootTile();
Expand Down Expand Up @@ -1492,10 +1517,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.insertAtTail(*pTile);
}
}

pTile = pNext;
Expand Down
111 changes: 73 additions & 38 deletions Cesium3DTilesSelection/src/TilesetContentManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -599,6 +590,7 @@ TilesetContentManager::TilesetContentManager(
const TilesetExternals& externals,
const TilesetOptions& tilesetOptions,
RasterOverlayCollection&& overlayCollection,
Tile::LoadedLinkedList& loadedTiles,
std::vector<CesiumAsync::IAssetAccessor::THeader>&& requestHeaders,
std::unique_ptr<TilesetContentLoader>&& pLoader,
std::unique_ptr<Tile>&& pRootTile)
Expand All @@ -616,6 +608,7 @@ TilesetContentManager::TilesetContentManager(
_overlayCollection{std::move(overlayCollection)},
_tileLoadsInProgress{0},
_loadedTilesCount{0},
_loadedTiles(loadedTiles),
_tilesDataUsed{0},
_destructionCompletePromise{externals.asyncSystem.createPromise<void>()},
_destructionCompleteFuture{
Expand All @@ -630,6 +623,7 @@ TilesetContentManager::TilesetContentManager(
const TilesetExternals& externals,
const TilesetOptions& tilesetOptions,
RasterOverlayCollection&& overlayCollection,
Tile::LoadedLinkedList& loadedTiles,
const std::string& url)
: _externals{externals},
_requestHeaders{},
Expand All @@ -645,6 +639,7 @@ TilesetContentManager::TilesetContentManager(
_overlayCollection{std::move(overlayCollection)},
_tileLoadsInProgress{0},
_loadedTilesCount{0},
_loadedTiles(loadedTiles),
_tilesDataUsed{0},
_destructionCompletePromise{externals.asyncSystem.createPromise<void>()},
_destructionCompleteFuture{
Expand Down Expand Up @@ -767,6 +762,7 @@ TilesetContentManager::TilesetContentManager(
const TilesetExternals& externals,
const TilesetOptions& tilesetOptions,
RasterOverlayCollection&& overlayCollection,
Tile::LoadedLinkedList& loadedTiles,
int64_t ionAssetID,
const std::string& ionAccessToken,
const std::string& ionAssetEndpointUrl)
Expand All @@ -784,6 +780,7 @@ TilesetContentManager::TilesetContentManager(
_overlayCollection{std::move(overlayCollection)},
_tileLoadsInProgress{0},
_loadedTilesCount{0},
_loadedTiles(loadedTiles),
_tilesDataUsed{0},
_destructionCompletePromise{externals.asyncSystem.createPromise<void>()},
_destructionCompleteFuture{
Expand Down Expand Up @@ -1026,6 +1023,27 @@ void TilesetContentManager::updateTileContent(
}
}

bool TilesetContentManager::handleUpsampledTileChildren(Tile& tile) {
for (Tile& child : tile.getChildren()) {
if (child.getState() == TileLoadState::ContentLoading &&
std::holds_alternative<CesiumGeometry::UpsampledQuadtreeNode>(
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) {
Expand All @@ -1037,9 +1055,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;
}

Expand All @@ -1050,46 +1075,50 @@ 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;
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;
}
}

// Are any children currently being upsampled from this tile?
for (const Tile& child : tile.getChildren()) {
if (child.getState() == TileLoadState::ContentLoading &&
std::holds_alternative<CesiumGeometry::UpsampledQuadtreeNode>(
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 (isReadyToUnload) {
// Make sure we unload all children
gsl::span<Cesium3DTilesSelection::Tile> children = tile.getChildren();
for (Tile& child : children) {
this->unloadTileContent(child);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This raises some performance red flags, though I don't know exactly how to tell you to fix it. When we decide to unload a tile, we'll traverse all of its descendants in isReadyToUnload to see if it can unloaded. And if it can, we'll then unload its children (here), which will involve calling isReadyToUnload again on each child, which will again traverse the entire tree. A tile subtree can have thousands of tiles, and this is an n^2 operation in the number of tiles, so 😱.

}
}

// If we make it this far, the tile's content will be fully unloaded.
notifyTileUnloading(&tile);
content.setContentKind(TileUnknownContent{});
tile.setState(TileLoadState::Unloaded);
if (isExternalContent) {
// We don't want to set external tilesets as Unloaded quite yet, because
// then they might get reloaded before we've had a chance to clear their
// children and cause an error. They'll get their children cleared and their
// state set to Unloaded before next clean up
tile.setState(TileLoadState::Done);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is safe. Setting it to Done will give the selection algorithm free reign to render this tile. Which will leads to holes. Perhaps Unloading?

} else {
tile.setState(TileLoadState::Unloaded);
}
return true;
}

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);
}
}

Expand Down Expand Up @@ -1424,7 +1453,10 @@ void TilesetContentManager::updateDoneState(
void TilesetContentManager::unloadContentLoadedState(Tile& tile) {
TileContent& content = tile.getContent();
TileRenderContent* pRenderContent = content.getRenderContent();
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(
Expand All @@ -1437,7 +1469,10 @@ void TilesetContentManager::unloadContentLoadedState(Tile& tile) {
void TilesetContentManager::unloadDoneState(Tile& tile) {
TileContent& content = tile.getContent();
TileRenderContent* pRenderContent = content.getRenderContent();
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(
Expand Down
Loading
Loading