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 13 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;
std::list<Tile*> _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
43 changes: 42 additions & 1 deletion Cesium3DTilesSelection/src/Tileset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ Tileset::Tileset(
ionAssetEndpointUrl)} {}

Tileset::~Tileset() noexcept {
this->_pTilesetContentManager->unloadAll();
if (this->_externals.pTileOcclusionProxyPool) {
this->_externals.pTileOcclusionProxyPool->destroyPool();
}
Expand Down Expand Up @@ -1461,7 +1460,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();
Expand Down Expand Up @@ -1492,10 +1516,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;
Expand All @@ -1509,6 +1541,15 @@ void Tileset::_unloadCachedTiles(double timeBudget) noexcept {

void Tileset::_markTileVisited(Tile& tile) noexcept {
this->_loadedTiles.insertAtTail(tile);
// Don't clear the children of this tile next frame
this->_externalTilesPendingClear.remove(&tile);
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);
}
Copy link
Member

Choose a reason for hiding this comment

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

This is the cause of the missing tiles. tile.getState() == TileLoadState::Unloaded && !tile.getChildren().empty() will be true for a very large number of tiles before the first time they're loaded. Setting them to Done will ensure they never get loaded. Do you need to check that it's also in _externalTilesPendingClear maybe?

}

void Tileset::addTileToLoadQueue(
Expand Down
91 changes: 52 additions & 39 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 @@ -1026,6 +1017,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 +1049,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,31 +1069,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<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 (!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;
}
}

Expand All @@ -1089,7 +1096,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);
}
}

Expand Down Expand Up @@ -1424,7 +1431,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 +1447,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
1 change: 1 addition & 0 deletions Cesium3DTilesSelection/src/TilesetContentManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class TilesetContentManager
void updateTileContent(Tile& tile, const TilesetOptions& tilesetOptions);

bool unloadTileContent(Tile& tile);
bool handleUpsampledTileChildren(Tile& tile);
Copy link
Member

Choose a reason for hiding this comment

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

Please use more descriptive names. "Handle" how?


void waitUntilIdle();

Expand Down
Loading