From 1b8fc8f43d373fcad2fe71ba5fb617dc448f6bc5 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Thu, 9 Apr 2026 13:59:48 -0700 Subject: [PATCH 01/28] call .get() to fix test --- Cesium3DTilesSelection/test/MockTilesetContentManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp b/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp index 84294f6eb7..3a9b67b230 100644 --- a/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp @@ -19,7 +19,7 @@ void MockTilesetContentManagerTestFixture::setTileShouldContinueUpdating( void MockTilesetContentManagerTestFixture::setTileContent( Cesium3DTilesSelection::Tile& tile, Cesium3DTilesSelection::TileContent&& content) { - tile._content = std::move(content); + tile._content.get() = std::move(content); }; } // namespace Cesium3DTilesSelection From 5eece01f951308f5065770948075e024e29045ac Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Thu, 9 Apr 2026 14:01:30 -0700 Subject: [PATCH 02/28] create MainThreadOnly and TileObserver --- .../Cesium3DTilesSelection/ThreadSafety.h | 120 +++++++++++++++++ .../include/Cesium3DTilesSelection/Tile.h | 34 +++-- .../Cesium3DTilesSelection/TileObserver.h | 126 ++++++++++++++++++ 3 files changed, 267 insertions(+), 13 deletions(-) create mode 100644 Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h create mode 100644 Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h new file mode 100644 index 0000000000..ac319d1d4c --- /dev/null +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h @@ -0,0 +1,120 @@ +#pragma once + +// ThreadSafety.h — Ownership-discipline types for Cesium3DTilesSelection. +// +// These types encode threading contracts at the type level so that violations +// are caught in debug builds (assertion failures) rather than silently causing +// data races. +// +// Annotation convention for functions (comment-based; not enforced by tooling): +// [main-thread] — must only be called from the main thread. +// [any-thread] — may be called from any thread (the type itself or an +// atomic/future enforces safety internally). +// [worker-thread] — must only be called from within a worker-thread dispatch. + +#include + +#include +#include +#include + +namespace Cesium3DTilesSelection { + +/** + * @brief Wraps a value that must only be accessed from the main thread. + * + * In debug builds, every access through `get()` (mutable or const) asserts + * that `std::this_thread::get_id()` matches the thread on which this wrapper + * was constructed. This catches accidental cross-thread access at runtime + * without requiring a lock. + * + * In release builds the wrapper compiles away to zero overhead — `get()` + * is a direct member reference with no branching or memory barrier. + * + * ### Usage + * + * ```cpp + * // In a class declaration: + * MainThreadOnly _content; + * MainThreadOnly _loadState; + * + * // Access (debug: asserts main thread; release: direct access): + * TileContent& c = _content.get(); + * ``` + * + * @tparam T The wrapped value type. + */ +template class MainThreadOnly { +public: + /** @brief Default-construct the wrapped value on the calling (main) thread. + */ + MainThreadOnly() +#ifndef NDEBUG + : _ownerThread(std::this_thread::get_id()) +#endif + { + } + + /** @brief Construct with an initial value on the calling (main) thread. */ + template < + typename... Args, + typename = std::enable_if_t>> + explicit MainThreadOnly(Args&&... args) + : _value(std::forward(args)...) +#ifndef NDEBUG + , + _ownerThread(std::this_thread::get_id()) +#endif + { + } + + // Allow default copy/move so that TileContent and TileLoadState (both + // trivially moveable) can be moved during tile tree construction, which + // happens on the main thread. Thread-id on copy/move keeps the owner as the + // constructing thread (the main thread). + MainThreadOnly(const MainThreadOnly&) = default; + MainThreadOnly(MainThreadOnly&&) noexcept = default; + MainThreadOnly& operator=(const MainThreadOnly&) = default; + MainThreadOnly& operator=(MainThreadOnly&&) noexcept = default; + ~MainThreadOnly() = default; + + /** + * @brief Returns a mutable reference to the wrapped value. + * + * Debug-asserts that the caller is on the owner (main) thread. + */ + T& get() noexcept { + assertMainThread(); + return _value; + } + + /** + * @brief Returns a const reference to the wrapped value. + * + * Debug-asserts that the caller is on the owner (main) thread. + * + * Note: even reads are main-thread-only because the writer is also the main + * thread and there is no synchronisation — an unsynchronised read from + * another thread would still be a data race. + */ + const T& get() const noexcept { + assertMainThread(); + return _value; + } + +private: + void assertMainThread() const noexcept { +#ifndef NDEBUG + CESIUM_ASSERT( + std::this_thread::get_id() == _ownerThread && + "MainThreadOnly accessed from outside the main thread"); +#endif + } + + T _value{}; +#ifndef NDEBUG + std::thread::id _ownerThread; +#endif +}; + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h index 88a9847a3c..21d88a2eff 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -234,14 +236,15 @@ class CESIUM3DTILESSELECTION_API Tile final { /** * @brief Returns the parent of this tile in the tile hierarchy. * - * This will be the `nullptr` if this is the root tile. + * This will be `nullptr` if this is the root tile. The returned pointer is + * non-owning — the caller must not store it beyond the tile's lifetime. * - * @return The parent. + * @return The parent, or `nullptr`. */ - Tile* getParent() noexcept { return this->_pParent; } + Tile* getParent() noexcept { return this->_pParent.get(); } /** @copydoc Tile::getParent() */ - const Tile* getParent() const noexcept { return this->_pParent; } + const Tile* getParent() const noexcept { return this->_pParent.get(); } /** * @brief Returns a *view* on the children of this tile. @@ -499,12 +502,12 @@ class CESIUM3DTILESSELECTION_API Tile final { } /** - * @brief Get the content of the tile. + * @brief Get the content of the tile. [main-thread] */ - const TileContent& getContent() const noexcept { return _content; } + const TileContent& getContent() const noexcept { return _content.get(); } - /** @copydoc Tile::getContent() const */ - TileContent& getContent() noexcept { return _content; } + /** @copydoc Tile::getContent() const [main-thread] */ + TileContent& getContent() noexcept { return _content.get(); } /** * @brief Determines if this tile is currently renderable. @@ -532,7 +535,7 @@ class CESIUM3DTILESSELECTION_API Tile final { TilesetContentLoader* getLoader() const noexcept; /** - * @brief Returns the {@link TileLoadState} of this tile. + * @brief Returns the {@link TileLoadState} of this tile. [main-thread] */ TileLoadState getState() const noexcept; @@ -684,7 +687,10 @@ class CESIUM3DTILESSELECTION_API Tile final { void setMightHaveLatentChildren(bool mightHaveLatentChildren) noexcept; // Position in bounding-volume hierarchy. - Tile* _pParent; + // Non-owning: the child observes the parent but does not extend its lifetime. + // Reference-count bookkeeping (addReference/releaseReference) is performed + // explicitly in Tile.cpp whenever the child's own reference count changes. + TileObserver _pParent; std::vector _children; // Properties from tileset.json. @@ -697,11 +703,13 @@ class CESIUM3DTILESSELECTION_API Tile final { TileRefine _refine; glm::dmat4x4 _transform; - // tile content + // tile content — both fields are [main-thread-only]; access via + // getContent() / getState() / setState() which enforce the threading + // contract in debug builds via MainThreadOnly. CesiumUtility::DoublyLinkedListPointers _unusedTilesLinks; - TileContent _content; + MainThreadOnly _content; TilesetContentLoader* _pLoader; - TileLoadState _loadState; + MainThreadOnly _loadState; bool _mightHaveLatentChildren; // mapped raster overlay diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h new file mode 100644 index 0000000000..50497dd8fc --- /dev/null +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h @@ -0,0 +1,126 @@ +#pragma once + +#include + +#include + +namespace Cesium3DTilesSelection { + +/** + * @brief A non-owning, non-reference-counted observer pointer. + * + * This type documents that the holder **does not** manage the lifetime of the + * pointed-to object. It provides the same null-check and dereference interface + * as a raw pointer, but makes the ownership intent explicit at the type level. + * + * Unlike `std::shared_ptr` or `CesiumUtility::IntrusivePointer`, constructing + * or destroying a `TileObserver` never increments or decrements any reference + * count. The pointed-to object must stay alive by some other means for the + * duration of the observer's use. + * + * Intended use: parent-back-pointer in the `Tile` hierarchy, where children + * observe (but do not own) their parent. The parent's lifetime is guaranteed + * by the child holding an *intrusive reference count* on the parent — that + * reference counting is done explicitly via `addReference` / `releaseReference` + * in `Tile.cpp`, and is a separate mechanism from this type. + * + * @tparam T The pointed-to type. + */ +template class TileObserver { +public: + /** @brief Constructs a null observer. */ + constexpr TileObserver() noexcept : _ptr(nullptr) {} + + /** + * @brief Constructs an observer pointing at `ptr`. + * + * Accepts `nullptr` explicitly because the field is frequently initialized + * to `nullptr` before a parent is set. + */ + constexpr explicit TileObserver(T* ptr) noexcept : _ptr(ptr) {} + + /** @brief Constructs a null observer from a null pointer literal. */ + constexpr TileObserver(std::nullptr_t) noexcept : _ptr(nullptr) {} + + // Non-owning — no special copy/move/destructor semantics needed. + TileObserver(const TileObserver&) noexcept = default; + TileObserver(TileObserver&&) noexcept = default; + TileObserver& operator=(const TileObserver&) noexcept = default; + TileObserver& operator=(TileObserver&&) noexcept = default; + ~TileObserver() noexcept = default; + + /** + * @brief Returns the observed pointer. May be `nullptr`. + */ + constexpr T* get() const noexcept { return _ptr; } + + /** + * @brief Arrow operator. Debug-asserts that the pointer is not null. + */ + constexpr T* operator->() const noexcept { + CESIUM_ASSERT(_ptr != nullptr); + return _ptr; + } + + /** + * @brief Dereference operator. Debug-asserts that the pointer is not null. + */ + constexpr T& operator*() const noexcept { + CESIUM_ASSERT(_ptr != nullptr); + return *_ptr; + } + + /** @brief Returns `true` if the observed pointer is non-null. */ + constexpr explicit operator bool() const noexcept { return _ptr != nullptr; } + + /** @brief Sets the observed pointer, or resets to null. */ + void reset(T* ptr = nullptr) noexcept { _ptr = ptr; } + + /** @copydoc TileObserver::reset() */ + TileObserver& operator=(std::nullptr_t) noexcept { + _ptr = nullptr; + return *this; + } + + /** @brief Equality comparison. */ + friend constexpr bool + operator==(const TileObserver& lhs, const TileObserver& rhs) noexcept { + return lhs._ptr == rhs._ptr; + } + friend constexpr bool + operator!=(const TileObserver& lhs, const TileObserver& rhs) noexcept { + return lhs._ptr != rhs._ptr; + } + + friend constexpr bool + operator==(const TileObserver& lhs, std::nullptr_t) noexcept { + return lhs._ptr == nullptr; + } + friend constexpr bool + operator!=(const TileObserver& lhs, std::nullptr_t) noexcept { + return lhs._ptr != nullptr; + } + friend constexpr bool + operator==(std::nullptr_t, const TileObserver& rhs) noexcept { + return rhs._ptr == nullptr; + } + friend constexpr bool + operator!=(std::nullptr_t, const TileObserver& rhs) noexcept { + return rhs._ptr != nullptr; + } + + /** @brief Raw-pointer equality (useful for CESIUM_ASSERT comparisons). */ + friend constexpr bool + operator==(const TileObserver& lhs, const T* rhs) noexcept { + return lhs._ptr == rhs; + } + friend constexpr bool + operator!=(const TileObserver& lhs, const T* rhs) noexcept { + return lhs._ptr != rhs; + } + +private: + T* _ptr; +}; + +} // namespace Cesium3DTilesSelection From 89bc16d060f6622bace04a1abd1076951c28813f Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Thu, 9 Apr 2026 14:02:46 -0700 Subject: [PATCH 03/28] create TileHiearchy and TileOverlaySystem --- .../Cesium3DTilesSelection/TileHierarchy.h | 45 +++++++++++++ .../src/TileOverlaySystem.cpp | 11 ++++ .../src/TileOverlaySystem.h | 64 +++++++++++++++++++ .../src/TilesetContentManager.cpp | 60 ++++++++--------- .../src/TilesetContentManager.h | 17 +++-- 5 files changed, 160 insertions(+), 37 deletions(-) create mode 100644 Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h create mode 100644 Cesium3DTilesSelection/src/TileOverlaySystem.cpp create mode 100644 Cesium3DTilesSelection/src/TileOverlaySystem.h diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h new file mode 100644 index 0000000000..9d4f5e7893 --- /dev/null +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include + +namespace Cesium3DTilesSelection { + +/** + * @brief Owns the root of the tile tree. + * + * All Tile instances are either held here (as the root) or owned + * by their parent via `Tile::_children`. This is the single point of + * tree ownership — it is non-copyable and non-shared. + * + * Access must occur on the main thread. [main-thread-only] + */ +class TileHierarchy { +public: + TileHierarchy() noexcept = default; + ~TileHierarchy() noexcept = default; + + TileHierarchy(const TileHierarchy&) = delete; + TileHierarchy& operator=(const TileHierarchy&) = delete; + TileHierarchy(TileHierarchy&&) noexcept = default; + TileHierarchy& operator=(TileHierarchy&&) noexcept = default; + + /** @brief Returns the root tile, or nullptr when not yet loaded. + * [main-thread] */ + const Tile* getRoot() const noexcept { return _pRoot.get(); } + /** @brief Returns the root tile, or nullptr when not yet loaded. + * [main-thread] */ + Tile* getRoot() noexcept { return _pRoot.get(); } + + /** @brief Transfers ownership of a new root tile into this hierarchy. + * [main-thread] */ + void setRoot(std::unique_ptr&& pRoot) noexcept { + _pRoot = std::move(pRoot); + } + +private: + std::unique_ptr _pRoot; +}; + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TileOverlaySystem.cpp b/Cesium3DTilesSelection/src/TileOverlaySystem.cpp new file mode 100644 index 0000000000..c08fd5fe9e --- /dev/null +++ b/Cesium3DTilesSelection/src/TileOverlaySystem.cpp @@ -0,0 +1,11 @@ +#include "TileOverlaySystem.h" + +namespace Cesium3DTilesSelection { + +TileOverlaySystem::TileOverlaySystem( + const LoadedTileEnumerator& loadedTiles, + const TilesetExternals& externals, + const CesiumGeospatial::Ellipsoid& ellipsoid) noexcept + : _collection(loadedTiles, externals, ellipsoid), _upsampler() {} + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TileOverlaySystem.h b/Cesium3DTilesSelection/src/TileOverlaySystem.h new file mode 100644 index 0000000000..9f1f54a82c --- /dev/null +++ b/Cesium3DTilesSelection/src/TileOverlaySystem.h @@ -0,0 +1,64 @@ +#pragma once + +// TileOverlaySystem.h lives in src/ (internal) because RasterOverlayUpsampler +// is also internal. If RasterOverlayUpsampler is ever promoted to a public +// header this file can move to include/. + +#include "RasterOverlayUpsampler.h" + +#include +#include +#include +#include + +namespace Cesium3DTilesSelection { + +/** + * @brief Owns the raster overlay collection and the upsampler. + * + * This is the single point of ownership for all overlay-related resources. + * In the current design tiles hold references into data owned here; Phase 4 + * will make those non-owning references explicit at the type level. + * + * The upsampler is a `TilesetContentLoader` specialisation whose `setOwner()` + * must be called by the containing `TilesetContentManager` after construction, + * because the owner back-reference is to the manager, not the system. + * + * [main-thread-only] + */ +class TileOverlaySystem { +public: + TileOverlaySystem( + const LoadedTileEnumerator& loadedTiles, + const TilesetExternals& externals, + const CesiumGeospatial::Ellipsoid& ellipsoid) noexcept; + + ~TileOverlaySystem() noexcept = default; + + TileOverlaySystem(const TileOverlaySystem&) = delete; + TileOverlaySystem& operator=(const TileOverlaySystem&) = delete; + TileOverlaySystem(TileOverlaySystem&&) noexcept = default; + TileOverlaySystem& operator=(TileOverlaySystem&&) noexcept = default; + + /** @brief The raster overlay collection. [main-thread] */ + const RasterOverlayCollection& getCollection() const noexcept { + return _collection; + } + RasterOverlayCollection& getCollection() noexcept { return _collection; } + + /** + * @brief The quadtree-upsampling loader. + * Caller is responsible for calling `setOwner()` after construction. + * [main-thread] + */ + const RasterOverlayUpsampler& getUpsampler() const noexcept { + return _upsampler; + } + RasterOverlayUpsampler& getUpsampler() noexcept { return _upsampler; } + +private: + RasterOverlayCollection _collection; + RasterOverlayUpsampler _upsampler; +}; + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 41835d3b09..622e88f6e9 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -656,10 +656,9 @@ TilesetContentManager::TilesetContentManager( : _externals{externals}, _requestHeaders{tilesetOptions.requestHeaders}, _pLoader{nullptr}, - _pRootTile{nullptr}, _userCredit(), _tilesetCredits{}, - _overlayCollection( + _overlays( LoadedTileEnumerator(nullptr), externals, tilesetOptions.ellipsoid), @@ -674,7 +673,6 @@ TilesetContentManager::TilesetContentManager( _rootTileAvailablePromise{externals.asyncSystem.createPromise()}, _rootTileAvailableFuture{ this->_rootTileAvailablePromise.getFuture().share()}, - _tilesEligibleForContentUnloading(), _requesters(), _roundRobinValueWorker(0.0), _roundRobinValueMain(0.0), @@ -686,7 +684,7 @@ TilesetContentManager::TilesetContentManager( externals.pCreditSystem, this->_creditSource); - this->_upsampler.setOwner(*this); + this->_overlays.getUpsampler().setOwner(*this); } /* static */ CesiumUtility::IntrusivePointer @@ -1149,7 +1147,7 @@ void TilesetContentManager::loadTileContent( Tile& tile, const TilesetOptions& tilesetOptions) { CESIUM_ASSERT(this->_pLoader != nullptr); - CESIUM_ASSERT(this->_pRootTile != nullptr); + CESIUM_ASSERT(this->_hierarchy.getRoot() != nullptr); CESIUM_TRACE("TilesetContentManager::loadTileContent"); @@ -1219,7 +1217,7 @@ void TilesetContentManager::loadTileContent( // map raster overlay to tile std::vector projections = - this->_overlayCollection.addTileOverlays(tile, tilesetOptions); + this->_overlays.getCollection().addTileOverlays(tile, tilesetOptions); // begin loading tile notifyTileStartLoading(&tile); @@ -1235,8 +1233,8 @@ void TilesetContentManager::loadTileContent( tile}; TilesetContentLoader* pLoader; - if (tile.getLoader() == &this->_upsampler) { - pLoader = &this->_upsampler; + if (tile.getLoader() == &this->_overlays.getUpsampler()) { + pLoader = &this->_overlays.getUpsampler(); } else { pLoader = this->_pLoader.get(); } @@ -1477,8 +1475,8 @@ UnloadTileContentResult TilesetContentManager::unloadTileContent(Tile& tile) { 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); + if (this->_hierarchy.getRoot()) { + unloadTileRecursively(*this->_hierarchy.getRoot(), *this); } } @@ -1511,7 +1509,7 @@ bool TilesetContentManager::waitUntilIdle( rasterOverlayTilesLoading = 0; for (const auto& pActivated : - this->_overlayCollection.getActivatedOverlays()) { + this->_overlays.getCollection().getActivatedOverlays()) { rasterOverlayTilesLoading += pActivated->getNumberOfTilesLoading(); } @@ -1525,11 +1523,11 @@ bool TilesetContentManager::waitUntilIdle( } const Tile* TilesetContentManager::getRootTile() const noexcept { - return this->_pRootTile.get(); + return this->_hierarchy.getRoot(); } Tile* TilesetContentManager::getRootTile() noexcept { - return this->_pRootTile.get(); + return this->_hierarchy.getRoot(); } const std::vector& @@ -1544,12 +1542,12 @@ TilesetContentManager::getRequestHeaders() noexcept { const RasterOverlayCollection& TilesetContentManager::getRasterOverlayCollection() const noexcept { - return this->_overlayCollection; + return this->_overlays.getCollection(); } RasterOverlayCollection& TilesetContentManager::getRasterOverlayCollection() noexcept { - return this->_overlayCollection; + return this->_overlays.getCollection(); } const Credit* TilesetContentManager::getUserCredit() const noexcept { @@ -1581,7 +1579,7 @@ int32_t TilesetContentManager::getNumberOfTilesLoaded() const noexcept { int64_t TilesetContentManager::getTotalDataUsed() const noexcept { int64_t bytes = this->_tilesDataUsed; for (const auto& pActivated : - this->_overlayCollection.getActivatedOverlays()) { + this->_overlays.getCollection().getActivatedOverlays()) { bytes += pActivated->getTileDataBytes(); } @@ -1669,14 +1667,11 @@ void TilesetContentManager::finishLoading( } void TilesetContentManager::markTileIneligibleForContentUnloading(Tile& tile) { - this->_tilesEligibleForContentUnloading.remove(tile); + this->_unloadQueue.markIneligible(tile); } void TilesetContentManager::markTileEligibleForContentUnloading(Tile& tile) { - // If the tile is not yet in the list, add it to the end (most recently used). - if (!this->_tilesEligibleForContentUnloading.contains(tile)) { - this->_tilesEligibleForContentUnloading.insertAtTail(tile); - } + this->_unloadQueue.markEligible(tile); // If the Tileset has already been destroyed, unload this unused Tile // immediately to allow the TilesetContentManager destruction process to @@ -1689,7 +1684,7 @@ void TilesetContentManager::markTileEligibleForContentUnloading(Tile& tile) { void TilesetContentManager::unloadCachedBytes( int64_t maximumCachedBytes, double timeBudgetMilliseconds) { - Tile* pTile = this->_tilesEligibleForContentUnloading.head(); + Tile* pTile = this->_unloadQueue.head(); // A time budget of 0.0 indicates we shouldn't throttle cache unloads. So set // the end time to the max time_point in that case. @@ -1707,11 +1702,11 @@ void TilesetContentManager::unloadCachedBytes( break; } - Tile* pNext = this->_tilesEligibleForContentUnloading.next(*pTile); + Tile* pNext = this->_unloadQueue.next(*pTile); const UnloadTileContentResult removed = this->unloadTileContent(*pTile); if (removed != UnloadTileContentResult::Keep) { - this->_tilesEligibleForContentUnloading.remove(*pTile); + this->_unloadQueue.remove(*pTile); } if (removed == UnloadTileContentResult::RemoveAndClearChildren) { @@ -1743,7 +1738,7 @@ void TilesetContentManager::clearChildrenRecursively(Tile* pTile) noexcept { child.getState() == TileLoadState::Unloaded); CESIUM_ASSERT(child.getReferenceCount() == 0); CESIUM_ASSERT(!child.hasReferencingContent()); - this->_tilesEligibleForContentUnloading.remove(child); + this->_unloadQueue.remove(child); this->clearChildrenRecursively(&child); child.setParent(nullptr); } @@ -2125,7 +2120,9 @@ void TilesetContentManager::updateDoneState( const TileRenderContent* pRenderContent = content.getRenderContent(); if (pRenderContent) { TileRasterOverlayStatus status = - this->_overlayCollection.updateTileOverlays(tile, tilesetOptions); + this->_overlays.getCollection().updateTileOverlays( + tile, + tilesetOptions); if (status.firstIndexWithMissingProjection) { // The mesh doesn't have the right texture coordinates for this @@ -2150,7 +2147,10 @@ void TilesetContentManager::updateDoneState( // have raster tiles that are not the most detailed available, create fake // children to hang more detailed rasters on by subdividing this tile. if (doSubdivide && tile.getChildren().empty()) { - createQuadtreeSubdividedChildren(ellipsoid, tile, this->_upsampler); + createQuadtreeSubdividedChildren( + ellipsoid, + tile, + this->_overlays.getUpsampler()); } } else { // We can't hang raster images on a tile without geometry, and their @@ -2255,10 +2255,10 @@ void TilesetContentManager::propagateTilesetContentLoaderResult( this->_requestHeaders = std::move(result.requestHeaders); this->_pLoader = std::move(result.pLoader); - this->_pRootTile = std::move(result.pRootTile); + this->_hierarchy.setRoot(std::move(result.pRootTile)); - this->_overlayCollection.setLoadedTileEnumerator( - LoadedTileEnumerator(this->_pRootTile.get())); + this->_overlays.getCollection().setLoadedTileEnumerator( + LoadedTileEnumerator(this->_hierarchy.getRoot())); this->_pLoader->setOwner(*this); } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index 9d14ae76f2..1c10bf89c0 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -1,11 +1,14 @@ #pragma once #include "RasterOverlayUpsampler.h" +#include "TileOverlaySystem.h" #include #include #include #include +#include +#include #include #include #include @@ -230,11 +233,13 @@ class TilesetContentManager TilesetExternals _externals; std::vector _requestHeaders; std::unique_ptr _pLoader; - std::unique_ptr _pRootTile; + /// @brief Owns the tile tree root. Single owner; children are owned by their + /// parent tile. Access on main thread only. + TileHierarchy _hierarchy; std::optional _userCredit; std::vector _tilesetCredits; - RasterOverlayUpsampler _upsampler; - RasterOverlayCollection _overlayCollection; + /// @brief Owns the raster overlay collection and upsampler. + TileOverlaySystem _overlays; int32_t _tileLoadsInProgress; int32_t _loadedTilesCount; int64_t _tilesDataUsed; @@ -249,10 +254,8 @@ class TilesetContentManager CesiumAsync::Promise _rootTileAvailablePromise; CesiumAsync::SharedFuture _rootTileAvailableFuture; - // These tiles are not currently used, so their content may be unloaded. The - // tiles at the head of the list are the least recently used, and the ones at - // the tail are the most recently used. - Tile::UnusedLinkedList _tilesEligibleForContentUnloading; + /// @brief Tracks tiles eligible for content eviction (LRU order). + TileUnloadQueue _unloadQueue; std::vector _requesters; double _roundRobinValueWorker; From c282c89565a07e1a3db273be29272860075b2082 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Thu, 9 Apr 2026 14:03:18 -0700 Subject: [PATCH 04/28] create TileUnloadQueue --- .../Cesium3DTilesSelection/TileUnloadQueue.h | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h new file mode 100644 index 0000000000..d009e0ffcf --- /dev/null +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +namespace Cesium3DTilesSelection { + +/** + * @brief Tracks tiles whose content may be evicted when memory is low. + * + * Tiles at the head are least-recently-used; tiles at the tail are + * most-recently-used. All methods must be called from the main thread. + * [main-thread-only] + * + * This type makes the ownership intent explicit: it manages a LRU linked-list + * of non-owning Tile references. A tile's presence here does NOT imply + * ownership — the tile hierarchy (TileHierarchy) is the sole owner. + */ +class TileUnloadQueue { +public: + TileUnloadQueue() noexcept = default; + ~TileUnloadQueue() noexcept = default; + + TileUnloadQueue(const TileUnloadQueue&) = delete; + TileUnloadQueue& operator=(const TileUnloadQueue&) = delete; + TileUnloadQueue(TileUnloadQueue&&) noexcept = default; + TileUnloadQueue& operator=(TileUnloadQueue&&) noexcept = default; + + /** + * @brief Adds `tile` to the tail (most-recently-used end) of the queue. + * No-op if `tile` is already tracked. [main-thread] + */ + void markEligible(Tile& tile) noexcept { + if (!_queue.contains(tile)) { + _queue.insertAtTail(tile); + } + } + + /** + * @brief Removes `tile` from the queue. No-op if not present. [main-thread] + */ + void markIneligible(Tile& tile) noexcept { _queue.remove(tile); } + + /** @brief Returns true when `tile` is currently in the candidate list. + * [main-thread] */ + bool contains(const Tile& tile) const noexcept { + return _queue.contains(tile); + } + + /** + * @brief Directly removes `tile` from the list. + * Prefer markIneligible; this exists for sites that call remove() + * unconditionally. [main-thread] + */ + void remove(Tile& tile) noexcept { _queue.remove(tile); } + + /** @brief Returns the least-recently-used tile, or nullptr. [main-thread] */ + Tile* head() noexcept { return _queue.head(); } + + /** @brief Returns the tile following `tile` in LRU order. [main-thread] */ + Tile* next(Tile& tile) noexcept { return _queue.next(tile); } + +private: + Tile::UnusedLinkedList _queue; +}; + +} // namespace Cesium3DTilesSelection From 8b64f4b87b9aef1cbd2262ee25246e50aa54629e Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Thu, 9 Apr 2026 14:03:50 -0700 Subject: [PATCH 05/28] make tile state update a function callback --- .../Cesium3DTilesSelection/TilesetFrameState.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h index 38f7d13a9d..914bd1e458 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h @@ -3,10 +3,12 @@ #include #include +#include #include namespace Cesium3DTilesSelection { +class Tile; class TilesetViewGroup; /** @@ -29,6 +31,19 @@ class TilesetFrameState { * @brief The computed fog density for each frustum. */ std::vector fogDensities; + + /** + * @brief [main-thread] Callback invoked once per visited tile to advance its + * content state machine (unloading, content-loaded finalization, latent + * children creation). Populated by Tileset::updateViewGroup before the + * traversal begins. + * + * Keeping this as a callback rather than a direct TilesetContentManager + * reference means the traversal functions (_visitTileIfNeeded, _visitTile, + * etc.) have no compile-time dependency on TilesetContentManager, making + * them independently testable. + */ + std::function tileStateUpdater; }; } // namespace Cesium3DTilesSelection From fa17459dcb7b6a025853d8ef15421ff8a301afe2 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Thu, 9 Apr 2026 14:04:23 -0700 Subject: [PATCH 06/28] create TileSelection --- .../Cesium3DTilesSelection/TilesetSelection.h | 52 + .../src/TilesetSelection.cpp | 895 ++++++++++++++++++ 2 files changed, 947 insertions(+) create mode 100644 Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h create mode 100644 Cesium3DTilesSelection/src/TilesetSelection.cpp diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h new file mode 100644 index 0000000000..c044fbf83e --- /dev/null +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include + +namespace Cesium3DTilesSelection { + +class Tile; +class TilesetFrameState; +class TilesetExternals; +struct TilesetOptions; +class TileOcclusionRendererProxy; + +/** + * @brief Bundles the immutable per-frame inputs to the tile selection + * algorithm. Holds only references — no ownership. + */ +struct TileSelectionContext { + const TilesetOptions& options; + const TilesetExternals& externals; +}; + +/** + * @brief Runs the tile selection / LOD traversal algorithm. + * + * This is a free function with no dependency on `Tileset`, enabling + * standalone unit testing against an in-memory tile tree. + * + * Side effects are limited to explicit reference parameters: + * - `frameState.viewGroup` receives traversal state updates and load queue + * entries. + * - `scratchDistances` and `scratchOcclusionProxies` are reused across + * frames to avoid per-frame heap allocation; their contents on entry are + * unspecified. + * + * @param ctx Configuration and external dependencies. + * @param frameState Per-frame view parameters and the view group to update. + * @param rootTile Root of the tile hierarchy to traverse. + * @param scratchDistances Caller-owned scratch buffer. + * @param scratchOcclusionProxies Caller-owned scratch buffer. + * @return The frame's render result and statistics. + */ +CESIUM3DTILESSELECTION_API ViewUpdateResult selectTiles( + const TileSelectionContext& ctx, + const TilesetFrameState& frameState, + Tile& rootTile, + std::vector& scratchDistances, + std::vector& scratchOcclusionProxies); + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TilesetSelection.cpp b/Cesium3DTilesSelection/src/TilesetSelection.cpp new file mode 100644 index 0000000000..bf9d58fb34 --- /dev/null +++ b/Cesium3DTilesSelection/src/TilesetSelection.cpp @@ -0,0 +1,895 @@ +// TilesetSelection.cpp +// +// Implements the tile LOD selection / traversal algorithm as pure free +// functions. No dependency on the Tileset class — all context is passed +// through explicit parameters. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace CesiumGeometry; +using namespace CesiumGeospatial; +using namespace CesiumRasterOverlays; +using namespace CesiumUtility; + +namespace Cesium3DTilesSelection { + +namespace { + +// Internal context — bundles all inputs for the recursive traversal helpers. +// References only; no ownership. +struct TraversalContext { + const TilesetOptions& options; + const TilesetExternals& externals; + const TilesetFrameState& frameState; + std::vector& distances; + std::vector& childOcclusionProxies; +}; + +// Internal per-subtree summary returned by each visit function. +struct TraversalDetails { + bool allAreRenderable = true; + bool anyWereRenderedLastFrame = false; + uint32_t notYetRenderableCount = 0; +}; + +struct CullResult { + bool shouldVisit = true; + bool culled = false; +}; + +enum class VisitTileAction { Render, Refine }; + +// Small helpers (previously in anonymous namespace in Tileset.cpp) + +TileSelectionState getPreviousState( + const TilesetViewGroup& viewGroup, + [[maybe_unused]] const Tile& tile) { + const TilesetViewGroup::TraversalState& traversalState = + viewGroup.getTraversalState(); + CESIUM_ASSERT(traversalState.getCurrentNode() == &tile); + const TileSelectionState* pState = traversalState.previousState(); + return pState == nullptr ? TileSelectionState() : *pState; +} + +void addToTilesFadingOutIfPreviouslyRendered( + TileSelectionState::Result lastResult, + Tile& tile, + ViewUpdateResult& result) { + if (lastResult == TileSelectionState::Result::Rendered || + (lastResult == TileSelectionState::Result::Refined && + tile.getRefine() == TileRefine::Add)) { + result.tilesFadingOut.insert(&tile); + TileRenderContent* pRenderContent = tile.getContent().getRenderContent(); + if (pRenderContent) { + pRenderContent->setLodTransitionFadePercentage(0.0f); + } + } +} + +void addCurrentTileToTilesFadingOutIfPreviouslyRendered( + TilesetViewGroup& viewGroup, + Tile& tile, + ViewUpdateResult& result) { + TileSelectionState::Result lastResult = + getPreviousState(viewGroup, tile).getResult(); + addToTilesFadingOutIfPreviouslyRendered(lastResult, tile, result); +} + +void addCurrentTileDescendantsToTilesFadingOutIfPreviouslyRendered( + TilesetViewGroup& viewGroup, + [[maybe_unused]] Tile& tile, + ViewUpdateResult& result) { + if (getPreviousState(viewGroup, tile).getResult() == + TileSelectionState::Result::Refined) { + viewGroup.getTraversalState().forEachPreviousDescendant( + [&](const Tile::Pointer& pTile, const TileSelectionState& state) { + addToTilesFadingOutIfPreviouslyRendered( + state.getResult(), + *pTile, + result); + }); + } +} + +void addCurrentTileAndDescendantsToTilesFadingOutIfPreviouslyRendered( + TilesetViewGroup& viewGroup, + Tile& tile, + ViewUpdateResult& result) { + addCurrentTileToTilesFadingOutIfPreviouslyRendered(viewGroup, tile, result); + addCurrentTileDescendantsToTilesFadingOutIfPreviouslyRendered( + viewGroup, + tile, + result); +} + +bool isVisibleFromCamera( + const ViewState& viewState, + const BoundingVolume& boundingVolume, + const Ellipsoid& ellipsoid, + bool forceRenderTilesUnderCamera) { + if (viewState.isBoundingVolumeVisible(boundingVolume)) { + return true; + } + if (!forceRenderTilesUnderCamera) { + return false; + } + const std::optional& position = + viewState.getPositionCartographic(); + std::optional maybeRectangle = + estimateGlobeRectangle(boundingVolume, ellipsoid); + if (position && maybeRectangle) { + return maybeRectangle->contains(position.value()); + } + return false; +} + +bool isVisibleInFog(double distance, double fogDensity) noexcept { + if (fogDensity <= 0.0) { + return true; + } + const double fogScalar = distance * fogDensity; + return glm::exp(-(fogScalar * fogScalar)) > 0.0; +} + +double computeTilePriority( + const Tile& tile, + const std::vector& frustums, + const std::vector& distances) { + double highestLoadPriority = std::numeric_limits::max(); + const glm::dvec3 boundingVolumeCenter = + getBoundingVolumeCenter(tile.getBoundingVolume()); + + for (size_t i = 0; i < frustums.size() && i < distances.size(); ++i) { + const ViewState& frustum = frustums[i]; + const double distance = distances[i]; + + glm::dvec3 tileDirection = boundingVolumeCenter - frustum.getPosition(); + const double magnitude = glm::length(tileDirection); + + if (magnitude >= CesiumUtility::Math::Epsilon5) { + tileDirection /= magnitude; + const double loadPriority = + (1.0 - glm::dot(tileDirection, frustum.getDirection())) * distance; + if (loadPriority < highestLoadPriority) { + highestLoadPriority = loadPriority; + } + } + } + return highestLoadPriority; +} + +void computeDistances( + const Tile& tile, + const std::vector& frustums, + std::vector& distances) { + const BoundingVolume& boundingVolume = tile.getBoundingVolume(); + distances.clear(); + distances.resize(frustums.size()); + std::transform( + frustums.begin(), + frustums.end(), + distances.begin(), + [boundingVolume](const ViewState& frustum) -> double { + return glm::sqrt(glm::max( + frustum.computeDistanceSquaredToBoundingVolume(boundingVolume), + 0.0)); + }); +} + +void addTileToLoadQueue( + const TraversalContext& ctx, + Tile& tile, + TileLoadPriorityGroup priorityGroup, + double priority) { + ctx.frameState.viewGroup.addToLoadQueue( + TileLoadTask{&tile, priorityGroup, priority}, + ctx.externals.pGltfModifier); +} + +void addTileToRender(ViewUpdateResult& result, Tile& tile, double sse) { + result.tilesToRenderThisFrame.emplace_back(&tile); + result.tileScreenSpaceErrorThisFrame.emplace_back(sse); +} + +double computeSse(const TraversalContext& ctx, const Tile& tile) noexcept { + double largestSse = 0.0; + const auto& frustums = ctx.frameState.frustums; + const auto& distances = ctx.distances; + CESIUM_ASSERT(frustums.size() == distances.size()); + for (size_t i = 0; i < frustums.size(); ++i) { + const double sse = frustums[i].computeScreenSpaceError( + tile.getGeometricError(), + distances[i]); + if (sse > largestSse) { + largestSse = sse; + } + } + return largestSse; +} + +bool meetsSseThreshold( + const TraversalContext& ctx, + double sse, + bool culled) noexcept { + return culled ? !ctx.options.enforceCulledScreenSpaceError || + sse < ctx.options.culledScreenSpaceError + : sse < ctx.options.maximumScreenSpaceError; +} + +bool isLeaf(const Tile& tile) noexcept { return tile.getChildren().empty(); } + +bool mustContinueRefiningToDeeperTiles( + const Tile& tile, + const TileSelectionState& lastFrameSelectionState) noexcept { + const TileSelectionState::Result originalResult = + lastFrameSelectionState.getOriginalResult(); + return originalResult == TileSelectionState::Result::Refined && + !tile.isRenderable(); +} + +// Forward declarations of recursive traversal helpers +TraversalDetails visitTileIfNeeded( + const TraversalContext& ctx, + uint32_t depth, + bool ancestorMeetsSse, + Tile& tile, + ViewUpdateResult& result); + +TraversalDetails visitTile( + const TraversalContext& ctx, + uint32_t depth, + bool meetsSse, + bool ancestorMeetsSse, + Tile& tile, + double tilePriority, + double tileSse, + ViewUpdateResult& result); + +TraversalDetails visitVisibleChildrenNearToFar( + const TraversalContext& ctx, + uint32_t depth, + bool ancestorMeetsSse, + Tile& tile, + ViewUpdateResult& result); + +// Frustum and fog culling + +void frustumCull( + const TraversalContext& ctx, + const Tile& tile, + bool cullWithChildrenBounds, + CullResult& cullResult) { + + if (!cullResult.shouldVisit || cullResult.culled) { + return; + } + + const Ellipsoid& ellipsoid = ctx.options.ellipsoid; + const std::vector& frustums = ctx.frameState.frustums; + + if (cullWithChildrenBounds) { + if (std::any_of( + frustums.begin(), + frustums.end(), + [&ellipsoid, + children = tile.getChildren(), + renderTilesUnderCamera = + ctx.options.renderTilesUnderCamera](const ViewState& frustum) { + for (const Tile& child : children) { + if (isVisibleFromCamera( + frustum, + child.getBoundingVolume(), + ellipsoid, + renderTilesUnderCamera)) { + return true; + } + } + return false; + })) { + return; + } + } else if (std::any_of( + frustums.begin(), + frustums.end(), + [&ellipsoid, + &boundingVolume = tile.getBoundingVolume(), + renderTilesUnderCamera = ctx.options.renderTilesUnderCamera]( + const ViewState& frustum) { + return isVisibleFromCamera( + frustum, + boundingVolume, + ellipsoid, + renderTilesUnderCamera); + })) { + return; + } + + cullResult.culled = true; + if (ctx.options.enableFrustumCulling) { + cullResult.shouldVisit = false; + } +} + +void fogCull(const TraversalContext& ctx, CullResult& cullResult) { + + if (!cullResult.shouldVisit || cullResult.culled) { + return; + } + + const auto& frustums = ctx.frameState.frustums; + const auto& fogDensities = ctx.frameState.fogDensities; + const auto& distances = ctx.distances; + + bool isFogCulled = true; + for (size_t i = 0; i < frustums.size(); ++i) { + if (isVisibleInFog(distances[i], fogDensities[i])) { + isFogCulled = false; + break; + } + } + + if (isFogCulled) { + cullResult.culled = true; + if (ctx.options.enableFogCulling) { + cullResult.shouldVisit = false; + } + } +} + +TileOcclusionState +checkOcclusion(const TraversalContext& ctx, const Tile& tile) { + const std::shared_ptr& pOcclusionPool = + ctx.externals.pTileOcclusionProxyPool; + if (!pOcclusionPool) { + return TileOcclusionState::NotOccluded; + } + + const TileOcclusionRendererProxy* pOcclusion = + pOcclusionPool->fetchOcclusionProxyForTile(tile); + if (!pOcclusion) { + return TileOcclusionState::NotOccluded; + } + + switch (static_cast(pOcclusion->getOcclusionState())) { + case TileOcclusionState::OcclusionUnavailable: + return TileOcclusionState::OcclusionUnavailable; + case TileOcclusionState::Occluded: + return TileOcclusionState::Occluded; + case TileOcclusionState::NotOccluded: + if (tile.getChildren().empty()) { + return TileOcclusionState::NotOccluded; + } + break; + } + + for (const Tile& child : tile.getChildren()) { + if (child.getUnconditionallyRefine()) { + return TileOcclusionState::NotOccluded; + } + } + + auto& childOcclusionProxies = + const_cast&>( + ctx.childOcclusionProxies); + childOcclusionProxies.clear(); + childOcclusionProxies.reserve(tile.getChildren().size()); + for (const Tile& child : tile.getChildren()) { + const TileOcclusionRendererProxy* pChildProxy = + pOcclusionPool->fetchOcclusionProxyForTile(child); + if (!pChildProxy) { + return TileOcclusionState::NotOccluded; + } + childOcclusionProxies.push_back(pChildProxy); + } + + for (const TileOcclusionRendererProxy* pChildProxy : childOcclusionProxies) { + if (pChildProxy->getOcclusionState() == TileOcclusionState::NotOccluded) { + return TileOcclusionState::NotOccluded; + } + } + for (const TileOcclusionRendererProxy* pChildProxy : childOcclusionProxies) { + if (pChildProxy->getOcclusionState() == + TileOcclusionState::OcclusionUnavailable) { + return TileOcclusionState::OcclusionUnavailable; + } + } + + return TileOcclusionState::Occluded; +} + +TraversalDetails createTraversalDetailsForSingleTile( + const TraversalContext& ctx, + const Tile& tile) { + TileSelectionState::Result lastFrameResult = + getPreviousState(ctx.frameState.viewGroup, tile).getResult(); + + bool isRenderable = tile.isRenderable(); + bool wasRenderedLastFrame = + lastFrameResult == TileSelectionState::Result::Rendered; + + if (!wasRenderedLastFrame && + lastFrameResult == TileSelectionState::Result::Refined) { + if (tile.getRefine() == TileRefine::Add) { + wasRenderedLastFrame = true; + } else { + ctx.frameState.viewGroup.getTraversalState().forEachPreviousDescendant( + [&](const Tile::Pointer& /* pTile */, + const TileSelectionState& state) { + if (state.getResult() == TileSelectionState::Result::Rendered) { + wasRenderedLastFrame = true; + } + }); + } + } + + TraversalDetails details; + details.allAreRenderable = isRenderable; + details.anyWereRenderedLastFrame = isRenderable && wasRenderedLastFrame; + details.notYetRenderableCount = isRenderable ? 0 : 1; + return details; +} + +TraversalDetails renderLeaf( + const TraversalContext& ctx, + Tile& tile, + double tilePriority, + double tileSse, + ViewUpdateResult& result) { + ctx.frameState.viewGroup.getTraversalState().currentState() = + TileSelectionState(TileSelectionState::Result::Rendered); + addTileToRender(result, tile, tileSse); + addTileToLoadQueue(ctx, tile, TileLoadPriorityGroup::Normal, tilePriority); + return createTraversalDetailsForSingleTile(ctx, tile); +} + +TraversalDetails renderInnerTile( + const TraversalContext& ctx, + Tile& tile, + double tileSse, + ViewUpdateResult& result) { + addCurrentTileDescendantsToTilesFadingOutIfPreviouslyRendered( + ctx.frameState.viewGroup, + tile, + result); + ctx.frameState.viewGroup.getTraversalState().currentState() = + TileSelectionState(TileSelectionState::Result::Rendered); + addTileToRender(result, tile, tileSse); + return createTraversalDetailsForSingleTile(ctx, tile); +} + +bool loadAndRenderAdditiveRefinedTile( + const TraversalContext& ctx, + Tile& tile, + ViewUpdateResult& result, + double tilePriority, + double tileSse, + bool queuedForLoad) { + if (tile.getRefine() == TileRefine::Add) { + addTileToRender(result, tile, tileSse); + if (!queuedForLoad) { + addTileToLoadQueue( + ctx, + tile, + TileLoadPriorityGroup::Normal, + tilePriority); + } + return true; + } + return false; +} + +bool kickDescendantsAndRenderTile( + const TraversalContext& ctx, + Tile& tile, + ViewUpdateResult& result, + TraversalDetails& traversalDetails, + size_t firstRenderedDescendantIndex, + const TilesetViewGroup::LoadQueueCheckpoint& loadQueueBeforeChildren, + bool queuedForLoad, + double tilePriority, + double tileSse) { + + TilesetViewGroup::TraversalState& traversalState = + ctx.frameState.viewGroup.getTraversalState(); + + traversalState.forEachCurrentDescendant( + [](const Tile::Pointer& /*pTile*/, TileSelectionState& selectionState) { + selectionState.kick(); + }); + + traversalState.forEachPreviousDescendant( + [&result]( + const Tile::Pointer& pTile, + const TileSelectionState& previousState) { + addToTilesFadingOutIfPreviouslyRendered( + previousState.getResult(), + *pTile, + result); + }); + + std::vector& renderList = result.tilesToRenderThisFrame; + std::vector& sseList = result.tileScreenSpaceErrorThisFrame; + renderList.erase( + renderList.begin() + + static_cast::iterator::difference_type>( + firstRenderedDescendantIndex), + renderList.end()); + sseList.erase( + sseList.begin() + + static_cast::iterator::difference_type>( + firstRenderedDescendantIndex), + sseList.end()); + + if (tile.getRefine() != TileRefine::Add) { + addTileToRender(result, tile, tileSse); + } + + traversalState.currentState() = + TileSelectionState(TileSelectionState::Result::Rendered); + + TileSelectionState::Result lastFrameSelectionState = + getPreviousState(ctx.frameState.viewGroup, tile).getResult(); + const bool wasRenderedLastFrame = + lastFrameSelectionState == TileSelectionState::Result::Rendered; + const bool isRenderable = tile.isRenderable(); + const bool wasReallyRenderedLastFrame = wasRenderedLastFrame && isRenderable; + + if (!wasReallyRenderedLastFrame && + traversalDetails.notYetRenderableCount > + ctx.options.loadingDescendantLimit && + !tile.isExternalContent() && !tile.getUnconditionallyRefine()) { + + result.tilesKicked += static_cast( + ctx.frameState.viewGroup.restoreTileLoadQueueCheckpoint( + loadQueueBeforeChildren)); + + if (!queuedForLoad) { + addTileToLoadQueue( + ctx, + tile, + TileLoadPriorityGroup::Normal, + tilePriority); + } + + traversalDetails.notYetRenderableCount = tile.isRenderable() ? 0 : 1; + queuedForLoad = true; + } + + traversalDetails.allAreRenderable = isRenderable; + traversalDetails.anyWereRenderedLastFrame = wasReallyRenderedLastFrame; + return queuedForLoad; +} + +TraversalDetails visitVisibleChildrenNearToFar( + const TraversalContext& ctx, + uint32_t depth, + bool ancestorMeetsSse, + Tile& tile, + ViewUpdateResult& result) { + TraversalDetails traversalDetails; + for (Tile& child : tile.getChildren()) { + const TraversalDetails childTraversal = + visitTileIfNeeded(ctx, depth + 1, ancestorMeetsSse, child, result); + traversalDetails.allAreRenderable &= childTraversal.allAreRenderable; + traversalDetails.anyWereRenderedLastFrame |= + childTraversal.anyWereRenderedLastFrame; + traversalDetails.notYetRenderableCount += + childTraversal.notYetRenderableCount; + } + return traversalDetails; +} + +TraversalDetails visitTile( + const TraversalContext& ctx, + uint32_t depth, + bool meetsSse, + bool ancestorMeetsSse, + Tile& tile, + double tilePriority, + double tileSse, + ViewUpdateResult& result) { + + TilesetViewGroup::TraversalState& traversalState = + ctx.frameState.viewGroup.getTraversalState(); + + ++result.tilesVisited; + result.maxDepthVisited = glm::max(result.maxDepthVisited, depth); + + if (isLeaf(tile)) { + return renderLeaf(ctx, tile, tilePriority, tileSse, result); + } + + const bool unconditionallyRefine = tile.getUnconditionallyRefine(); + const bool refineForSse = !meetsSse && !ancestorMeetsSse; + + VisitTileAction action = (unconditionallyRefine || refineForSse) + ? VisitTileAction::Refine + : VisitTileAction::Render; + + TileSelectionState lastFrameSelectionState = + getPreviousState(ctx.frameState.viewGroup, tile); + TileSelectionState::Result lastFrameSelectionResult = + lastFrameSelectionState.getResult(); + + bool tileLastRefined = + lastFrameSelectionResult == TileSelectionState::Result::Refined; + bool childLastRefined = false; + traversalState.forEachPreviousChild( + [&](const Tile::Pointer& /*pTile*/, const TileSelectionState& state) { + if (state.getResult() == TileSelectionState::Result::Refined) { + childLastRefined = true; + } + }); + + bool shouldCheckOcclusion = + ctx.options.enableOcclusionCulling && action == VisitTileAction::Refine && + !unconditionallyRefine && (!tileLastRefined || !childLastRefined); + + if (shouldCheckOcclusion) { + TileOcclusionState occlusion = checkOcclusion(ctx, tile); + if (occlusion == TileOcclusionState::Occluded) { + ++result.tilesOccluded; + action = VisitTileAction::Render; + meetsSse = true; + } else if ( + occlusion == TileOcclusionState::OcclusionUnavailable && + ctx.options.delayRefinementForOcclusion && + lastFrameSelectionState.getOriginalResult() != + TileSelectionState::Result::Refined) { + ++result.tilesWaitingForOcclusionResults; + action = VisitTileAction::Render; + meetsSse = true; + } + } + + bool queuedForLoad = false; + + if (action == VisitTileAction::Render) { + bool mustRefine = + mustContinueRefiningToDeeperTiles(tile, lastFrameSelectionState); + if (mustRefine) { + action = VisitTileAction::Refine; + if (!ancestorMeetsSse) { + addTileToLoadQueue( + ctx, + tile, + TileLoadPriorityGroup::Urgent, + tilePriority); + queuedForLoad = true; + } + ancestorMeetsSse = true; + } else { + if (!ancestorMeetsSse) { + addTileToLoadQueue( + ctx, + tile, + TileLoadPriorityGroup::Normal, + tilePriority); + } + return renderInnerTile(ctx, tile, tileSse, result); + } + } + + // Refine! + queuedForLoad = loadAndRenderAdditiveRefinedTile( + ctx, + tile, + result, + tilePriority, + tileSse, + queuedForLoad) || + queuedForLoad; + + const size_t firstRenderedDescendantIndex = + result.tilesToRenderThisFrame.size(); + TilesetViewGroup::LoadQueueCheckpoint loadQueueBeforeChildren = + ctx.frameState.viewGroup.saveTileLoadQueueCheckpoint(); + + TraversalDetails traversalDetails = + visitVisibleChildrenNearToFar(ctx, depth, ancestorMeetsSse, tile, result); + + const TileRenderContent* pRenderContent = + tile.getContent().getRenderContent(); + bool kickDueToNonReadyDescendant = !traversalDetails.allAreRenderable && + !traversalDetails.anyWereRenderedLastFrame; + bool kickDueToTileFadingIn = + ctx.options.enableLodTransitionPeriod && + ctx.options.kickDescendantsWhileFadingIn && + lastFrameSelectionResult == TileSelectionState::Result::Rendered && + pRenderContent && pRenderContent->getLodTransitionFadePercentage() < 1.0f; + + bool wantToKick = kickDueToNonReadyDescendant || kickDueToTileFadingIn; + bool willKick = wantToKick && (traversalDetails.notYetRenderableCount > + ctx.options.loadingDescendantLimit || + tile.isRenderable()); + + if (willKick) { + queuedForLoad = kickDescendantsAndRenderTile( + ctx, + tile, + result, + traversalDetails, + firstRenderedDescendantIndex, + loadQueueBeforeChildren, + queuedForLoad, + tilePriority, + tileSse); + } else { + if (tile.getRefine() != TileRefine::Add) { + addCurrentTileToTilesFadingOutIfPreviouslyRendered( + ctx.frameState.viewGroup, + tile, + result); + } + traversalState.currentState() = + TileSelectionState(TileSelectionState::Result::Refined); + } + + if (ctx.options.preloadAncestors && !queuedForLoad) { + addTileToLoadQueue(ctx, tile, TileLoadPriorityGroup::Preload, tilePriority); + } + + return traversalDetails; +} + +// visitTileIfNeeded (the outermost recursive entry point) +TraversalDetails visitTileIfNeeded( + const TraversalContext& ctx, + uint32_t depth, + bool ancestorMeetsSse, + Tile& tile, + ViewUpdateResult& result) { + + TilesetViewGroup::TraversalState& traversalState = + ctx.frameState.viewGroup.getTraversalState(); + traversalState.beginNode(&tile); + + computeDistances(tile, ctx.frameState.frustums, ctx.distances); + double tilePriority = + computeTilePriority(tile, ctx.frameState.frustums, ctx.distances); + + if (ctx.frameState.tileStateUpdater) { + ctx.frameState.tileStateUpdater(tile); + } + + CullResult cullResult{}; + + bool cullWithChildrenBounds = + tile.getRefine() == TileRefine::Replace && !tile.getChildren().empty(); + for (Tile& child : tile.getChildren()) { + if (child.getUnconditionallyRefine()) { + cullWithChildrenBounds = false; + break; + } + } + + for (const std::shared_ptr& pExcluder : + ctx.options.excluders) { + if (pExcluder->shouldExclude(tile)) { + cullResult.culled = true; + cullResult.shouldVisit = false; + break; + } + } + + frustumCull(ctx, tile, cullWithChildrenBounds, cullResult); + fogCull(ctx, cullResult); + + if (!cullResult.shouldVisit && tile.getUnconditionallyRefine()) { + if ((ctx.options.forbidHoles && tile.getRefine() == TileRefine::Replace) || + tile.getParent() == nullptr) { + cullResult.shouldVisit = true; + } + } + + if (!cullResult.shouldVisit) { + addCurrentTileAndDescendantsToTilesFadingOutIfPreviouslyRendered( + ctx.frameState.viewGroup, + tile, + result); + + ctx.frameState.viewGroup.getTraversalState().currentState() = + TileSelectionState(TileSelectionState::Result::Culled); + ++result.tilesCulled; + + TraversalDetails traversalDetails{}; + + if (ctx.options.forbidHoles && tile.getRefine() == TileRefine::Replace) { + addTileToLoadQueue( + ctx, + tile, + TileLoadPriorityGroup::Normal, + tilePriority); + traversalDetails = createTraversalDetailsForSingleTile(ctx, tile); + } else if (ctx.options.preloadSiblings) { + addTileToLoadQueue( + ctx, + tile, + TileLoadPriorityGroup::Preload, + tilePriority); + } + + traversalState.finishNode(&tile); + return traversalDetails; + } + + if (cullResult.culled) { + ++result.culledTilesVisited; + } + + double tileSse = computeSse(ctx, tile); + bool meetsSse = meetsSseThreshold(ctx, tileSse, cullResult.culled); + + TraversalDetails details = visitTile( + ctx, + depth, + meetsSse, + ancestorMeetsSse, + tile, + tilePriority, + tileSse, + result); + + traversalState.finishNode(&tile); + return details; +} + +} // anonymous namespace + +// Public entry point +ViewUpdateResult selectTiles( + const TileSelectionContext& ctx, + const TilesetFrameState& frameState, + Tile& rootTile, + std::vector& scratchDistances, + std::vector& scratchOcclusionProxies) { + + TraversalContext tctx{ + ctx.options, + ctx.externals, + frameState, + scratchDistances, + scratchOcclusionProxies}; + + ViewUpdateResult result; + visitTileIfNeeded(tctx, 0, false, rootTile, result); + return result; +} + +} // namespace Cesium3DTilesSelection From aafcd229f24f2b9451bc0594201bb41356236c25 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Thu, 9 Apr 2026 14:05:23 -0700 Subject: [PATCH 07/28] tile selection delegates to free functions --- .../include/Cesium3DTilesSelection/Tileset.h | 143 +-- Cesium3DTilesSelection/src/Tileset.cpp | 1024 +---------------- 2 files changed, 19 insertions(+), 1148 deletions(-) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index 36c34466e1..5a87758736 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -468,125 +469,6 @@ class CESIUM3DTILESSELECTION_API Tileset final { Tileset& operator=(const Tileset& rhs) = delete; private: - /** - * @brief The result of traversing one branch of the tile hierarchy. - * - * Instances of this structure are created by the `_visit...` functions, - * and summarize the information that was gathered during the traversal - * of the respective branch, so that this information can be used by - * the parent to decide on the further traversal process. - */ - struct TraversalDetails { - /** - * @brief Whether all selected tiles in this tile's subtree are renderable. - * - * This is `true` if all selected (i.e. not culled or refined) tiles in this - * tile's subtree are renderable. If the subtree is renderable, we'll render - * it; no drama. - */ - bool allAreRenderable = true; - - /** - * @brief Whether any tile in this tile's subtree was rendered in the last - * frame. - * - * This is `true` if any tiles in this tile's subtree were rendered last - * frame. If any were, we must render the subtree rather than this tile, - * because rendering this tile would cause detail to vanish that was visible - * last frame, and that's no good. - */ - bool anyWereRenderedLastFrame = false; - - /** - * @brief The number of selected tiles in this tile's subtree that are not - * yet renderable. - * - * Counts the number of selected tiles in this tile's subtree that are - * not yet ready to be rendered because they need more loading. Note that - * this value will _not_ necessarily be zero when - * `allAreRenderable` is `true`, for subtle reasons. - * When `allAreRenderable` and `anyWereRenderedLastFrame` are both `false`, - * we will render this tile instead of any tiles in its subtree and the - * `allAreRenderable` value for this tile will reflect only whether _this_ - * tile is renderable. The `notYetRenderableCount` value, however, will - * still reflect the total number of tiles that we are waiting on, including - * the ones that we're not rendering. `notYetRenderableCount` is only reset - * when a subtree is removed from the render queue because the - * `notYetRenderableCount` exceeds the - * {@link TilesetOptions::loadingDescendantLimit}. - */ - uint32_t notYetRenderableCount = 0; - }; - - TraversalDetails _renderLeaf( - const TilesetFrameState& frameState, - Tile& tile, - double tilePriority, - double tileSse, - ViewUpdateResult& result); - TraversalDetails _renderInnerTile( - const TilesetFrameState& frameState, - Tile& tile, - double tileSse, - ViewUpdateResult& result); - bool _kickDescendantsAndRenderTile( - const TilesetFrameState& frameState, - Tile& tile, - ViewUpdateResult& result, - TraversalDetails& traversalDetails, - size_t firstRenderedDescendantIndex, - const TilesetViewGroup::LoadQueueCheckpoint& loadQueueBeforeChildren, - bool queuedForLoad, - double tilePriority, - double tileSse); - TileOcclusionState _checkOcclusion(const Tile& tile); - - TraversalDetails _visitTile( - const TilesetFrameState& frameState, - uint32_t depth, - bool meetsSse, - bool ancestorMeetsSse, - Tile& tile, - double tilePriority, - double tileSse, - ViewUpdateResult& result); - - struct CullResult { - // whether we should visit this tile - bool shouldVisit = true; - // whether this tile was culled (Note: we might still want to visit it) - bool culled = false; - }; - - // TODO: abstract these into a composable culling interface. - void _frustumCull( - const Tile& tile, - const TilesetFrameState& frameState, - bool cullWithChildrenBounds, - CullResult& cullResult); - void _fogCull( - const TilesetFrameState& frameState, - const std::vector& distances, - CullResult& cullResult); - double _computeSse( - const std::vector& frustums, - const Tile& tile, - const std::vector& distances) const noexcept; - bool _meetsSseThreshold(double sse, bool culled) const noexcept; - - TraversalDetails _visitTileIfNeeded( - const TilesetFrameState& frameState, - uint32_t depth, - bool ancestorMeetsSse, - Tile& tile, - ViewUpdateResult& result); - TraversalDetails _visitVisibleChildrenNearToFar( - const TilesetFrameState& frameState, - uint32_t depth, - bool ancestorMeetsSse, - Tile& tile, - ViewUpdateResult& result); - /** * @brief When called on an additive-refined tile, queues it for load and adds * it to the render list. @@ -603,13 +485,6 @@ class CESIUM3DTILESSELECTION_API Tileset final { * render list. * @return false The non-additive-refined tile was ignored. */ - bool _loadAndRenderAdditiveRefinedTile( - const TilesetFrameState& frameState, - Tile& tile, - ViewUpdateResult& result, - double tilePriority, - double tileSse, - bool queuedForLoad); void _unloadCachedTiles(double timeBudget) noexcept; @@ -624,11 +499,11 @@ class CESIUM3DTILESSELECTION_API Tileset final { TilesetOptions _options; // Holds computed distances, to avoid allocating them on the heap during tile - // selection. + // selection. Passed by reference into selectTiles(). std::vector _distances; - // Holds the occlusion proxies of the children of a tile. Store them in this - // scratch variable so that it can allocate only when growing bigger. + // Holds the occlusion proxies of the children of a tile. Passed by reference + // into selectTiles() to avoid per-frame allocation. std::vector _childOcclusionProxies; CesiumUtility::IntrusivePointer @@ -637,16 +512,6 @@ class CESIUM3DTILESSELECTION_API Tileset final { std::list _heightRequests; TilesetViewGroup _defaultViewGroup; - - void addTileToLoadQueue( - const TilesetFrameState& frameState, - Tile& tile, - TileLoadPriorityGroup priorityGroup, - double priority); - - static TraversalDetails createTraversalDetailsForSingleTile( - const TilesetFrameState& frameState, - const Tile& tile); }; } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index 4afc47a0f4..6a589a8792 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -424,11 +424,24 @@ const ViewUpdateResult& Tileset::updateViewGroup( return computeFogDensity(fogDensityTable, frustum); }); - TilesetFrameState frameState{viewGroup, frustums, std::move(fogDensities)}; + TilesetFrameState frameState{ + viewGroup, + frustums, + std::move(fogDensities), + [this](Tile& tile) { + this->_pTilesetContentManager->updateTileContent(tile, this->_options); + }}; if (!frustums.empty()) { viewGroup.startNewFrame(*this, frameState); - this->_visitTileIfNeeded(frameState, 0, false, *pRootTile, result); + TileSelectionContext ctx{this->_options, this->_externals}; + ViewUpdateResult selectionResult = selectTiles( + ctx, + frameState, + *pRootTile, + this->_distances, + this->_childOcclusionProxies); + result = std::move(selectionResult); viewGroup.finishFrame(*this, frameState); } else { result = ViewUpdateResult(); @@ -593,1011 +606,4 @@ const TilesetViewGroup& Tileset::getDefaultViewGroup() const { return this->_defaultViewGroup; } -namespace { - -TileSelectionState getPreviousState( - const TilesetViewGroup& viewGroup, - [[maybe_unused]] const Tile& tile) { - const TilesetViewGroup::TraversalState& traversalState = - viewGroup.getTraversalState(); - CESIUM_ASSERT(traversalState.getCurrentNode() == &tile); - const TileSelectionState* pState = traversalState.previousState(); - return pState == nullptr ? TileSelectionState() : *pState; -} - -void addToTilesFadingOutIfPreviouslyRendered( - TileSelectionState::Result lastResult, - Tile& tile, - ViewUpdateResult& result) { - if (lastResult == TileSelectionState::Result::Rendered || - (lastResult == TileSelectionState::Result::Refined && - tile.getRefine() == TileRefine::Add)) { - result.tilesFadingOut.insert(&tile); - TileRenderContent* pRenderContent = tile.getContent().getRenderContent(); - if (pRenderContent) { - pRenderContent->setLodTransitionFadePercentage(0.0f); - } - } -} - -void addCurrentTileToTilesFadingOutIfPreviouslyRendered( - TilesetViewGroup& viewGroup, - Tile& tile, - ViewUpdateResult& result) { - TileSelectionState::Result lastResult = - getPreviousState(viewGroup, tile).getResult(); - addToTilesFadingOutIfPreviouslyRendered(lastResult, tile, result); -} - -void addCurrentTileDescendantsToTilesFadingOutIfPreviouslyRendered( - TilesetViewGroup& viewGroup, - [[maybe_unused]] Tile& tile, - ViewUpdateResult& result) { - if (getPreviousState(viewGroup, tile).getResult() == - TileSelectionState::Result::Refined) { - viewGroup.getTraversalState().forEachPreviousDescendant( - [&](const Tile::Pointer& pTile, const TileSelectionState& state) { - addToTilesFadingOutIfPreviouslyRendered( - state.getResult(), - *pTile, - result); - }); - } -} - -void addCurrentTileAndDescendantsToTilesFadingOutIfPreviouslyRendered( - TilesetViewGroup& viewGroup, - Tile& tile, - ViewUpdateResult& result) { - addCurrentTileToTilesFadingOutIfPreviouslyRendered(viewGroup, tile, result); - addCurrentTileDescendantsToTilesFadingOutIfPreviouslyRendered( - viewGroup, - tile, - result); -} - -/** - * @brief Returns whether a tile with the given bounding volume is visible for - * the camera. - * - * @param viewState The {@link ViewState} - * @param boundingVolume The bounding volume of the tile - * @param forceRenderTilesUnderCamera Whether tiles under the camera should - * always be considered visible and rendered (see - * {@link Cesium3DTilesSelection::TilesetOptions}). - * @return Whether the tile is visible according to the current camera - * configuration - */ -bool isVisibleFromCamera( - const ViewState& viewState, - const BoundingVolume& boundingVolume, - const Ellipsoid& ellipsoid, - bool forceRenderTilesUnderCamera) { - if (viewState.isBoundingVolumeVisible(boundingVolume)) { - return true; - } - if (!forceRenderTilesUnderCamera) { - return false; - } - - const std::optional& position = - viewState.getPositionCartographic(); - - // TODO: it would be better to test a line pointing down (and up?) from the - // camera against the bounding volume itself, rather than transforming the - // bounding volume to a region. - std::optional maybeRectangle = - estimateGlobeRectangle(boundingVolume, ellipsoid); - if (position && maybeRectangle) { - return maybeRectangle->contains(position.value()); - } - return false; -} - -/** - * @brief Returns whether a tile at the given distance is visible in the fog. - * - * @param distance The distance of the tile bounding volume to the camera - * @param fogDensity The fog density - * @return Whether the tile is visible in the fog - */ -bool isVisibleInFog(double distance, double fogDensity) noexcept { - if (fogDensity <= 0.0) { - return true; - } - - const double fogScalar = distance * fogDensity; - return glm::exp(-(fogScalar * fogScalar)) > 0.0; -} -} // namespace - -void Tileset::_frustumCull( - const Tile& tile, - const TilesetFrameState& frameState, - bool cullWithChildrenBounds, - CullResult& cullResult) { - - if (!cullResult.shouldVisit || cullResult.culled) { - return; - } - - const CesiumGeospatial::Ellipsoid& ellipsoid = this->getEllipsoid(); - - const std::vector& frustums = frameState.frustums; - // Frustum cull using the children's bounds. - if (cullWithChildrenBounds) { - if (std::any_of( - frustums.begin(), - frustums.end(), - [&ellipsoid, - children = tile.getChildren(), - renderTilesUnderCamera = this->_options.renderTilesUnderCamera]( - const ViewState& frustum) { - for (const Tile& child : children) { - if (isVisibleFromCamera( - frustum, - child.getBoundingVolume(), - ellipsoid, - renderTilesUnderCamera)) { - return true; - } - } - - return false; - })) { - // At least one child is visible in at least one frustum, so don't cull. - return; - } - // Frustum cull based on the actual tile's bounds. - } else if (std::any_of( - frustums.begin(), - frustums.end(), - [&ellipsoid, - &boundingVolume = tile.getBoundingVolume(), - renderTilesUnderCamera = - this->_options.renderTilesUnderCamera]( - const ViewState& frustum) { - return isVisibleFromCamera( - frustum, - boundingVolume, - ellipsoid, - renderTilesUnderCamera); - })) { - // The tile is visible in at least one frustum, so don't cull. - return; - } - - // If we haven't returned yet, this tile is frustum culled. - cullResult.culled = true; - - if (this->_options.enableFrustumCulling) { - // frustum culling is enabled so we shouldn't visit this off-screen tile - cullResult.shouldVisit = false; - } -} - -void Tileset::_fogCull( - const TilesetFrameState& frameState, - const std::vector& distances, - CullResult& cullResult) { - - if (!cullResult.shouldVisit || cullResult.culled) { - return; - } - - const std::vector& frustums = frameState.frustums; - const std::vector& fogDensities = frameState.fogDensities; - - bool isFogCulled = true; - - for (size_t i = 0; i < frustums.size(); ++i) { - const double distance = distances[i]; - const double fogDensity = fogDensities[i]; - - if (isVisibleInFog(distance, fogDensity)) { - isFogCulled = false; - break; - } - } - - if (isFogCulled) { - // this tile is occluded by fog so it is a culled tile - cullResult.culled = true; - if (this->_options.enableFogCulling) { - // fog culling is enabled so we shouldn't visit this tile - cullResult.shouldVisit = false; - } - } -} - -namespace { - -double computeTilePriority( - const Tile& tile, - const std::vector& frustums, - const std::vector& distances) { - double highestLoadPriority = std::numeric_limits::max(); - const glm::dvec3 boundingVolumeCenter = - getBoundingVolumeCenter(tile.getBoundingVolume()); - - for (size_t i = 0; i < frustums.size() && i < distances.size(); ++i) { - const ViewState& frustum = frustums[i]; - const double distance = distances[i]; - - glm::dvec3 tileDirection = boundingVolumeCenter - frustum.getPosition(); - const double magnitude = glm::length(tileDirection); - - if (magnitude >= CesiumUtility::Math::Epsilon5) { - tileDirection /= magnitude; - const double loadPriority = - (1.0 - glm::dot(tileDirection, frustum.getDirection())) * distance; - if (loadPriority < highestLoadPriority) { - highestLoadPriority = loadPriority; - } - } - } - - return highestLoadPriority; -} - -void computeDistances( - const Tile& tile, - const std::vector& frustums, - std::vector& distances) { - const BoundingVolume& boundingVolume = tile.getBoundingVolume(); - - distances.clear(); - distances.resize(frustums.size()); - - std::transform( - frustums.begin(), - frustums.end(), - distances.begin(), - [boundingVolume](const ViewState& frustum) -> double { - return glm::sqrt(glm::max( - frustum.computeDistanceSquaredToBoundingVolume(boundingVolume), - 0.0)); - }); -} - -} // namespace - -double Tileset::_computeSse( - const std::vector& frustums, - const Tile& tile, - const std::vector& distances) const noexcept { - double largestSse = 0.0; - - CESIUM_ASSERT(frustums.size() == distances.size()); - for (size_t i = 0; i < frustums.size(); ++i) { - const ViewState& frustum = frustums[i]; - const double distance = distances[i]; - - // Does this tile meet the screen-space error? - const double sse = - frustum.computeScreenSpaceError(tile.getGeometricError(), distance); - if (sse > largestSse) { - largestSse = sse; - } - } - return largestSse; -} - -bool Tileset::_meetsSseThreshold(double sse, bool culled) const noexcept { - return culled ? !this->_options.enforceCulledScreenSpaceError || - sse < this->_options.culledScreenSpaceError - : sse < this->_options.maximumScreenSpaceError; -} - -namespace { -void addTileToRender(ViewUpdateResult& result, Tile& tile, double sse) { - result.tilesToRenderThisFrame.emplace_back(&tile); - result.tileScreenSpaceErrorThisFrame.emplace_back(sse); -} -} // namespace - -// Visits a tile for possible rendering. When we call this function with a tile: -// * It is not yet known whether the tile is visible. -// * Its parent tile does _not_ meet the SSE (unless ancestorMeetsSse=true, -// see comments below). -// * The tile may or may not be renderable. -// * The tile has not yet been added to a load queue. -Tileset::TraversalDetails Tileset::_visitTileIfNeeded( - const TilesetFrameState& frameState, - uint32_t depth, - bool ancestorMeetsSse, - Tile& tile, - ViewUpdateResult& result) { - TilesetViewGroup::TraversalState& traversalState = - frameState.viewGroup.getTraversalState(); - - traversalState.beginNode(&tile); - - std::vector& distances = this->_distances; - computeDistances(tile, frameState.frustums, distances); - double tilePriority = - computeTilePriority(tile, frameState.frustums, distances); - - this->_pTilesetContentManager->updateTileContent(tile, _options); - - CullResult cullResult{}; - - // Culling with children bounds will give us incorrect results with Add - // refinement, but is a useful optimization for Replace refinement. - bool cullWithChildrenBounds = - tile.getRefine() == TileRefine::Replace && !tile.getChildren().empty(); - for (Tile& child : tile.getChildren()) { - if (child.getUnconditionallyRefine()) { - cullWithChildrenBounds = false; - break; - } - } - - // TODO: add cullWithChildrenBounds to the tile excluder interface? - for (const std::shared_ptr& pExcluder : - this->_options.excluders) { - if (pExcluder->shouldExclude(tile)) { - cullResult.culled = true; - cullResult.shouldVisit = false; - break; - } - } - - // TODO: abstract culling stages into composable interface? - this->_frustumCull(tile, frameState, cullWithChildrenBounds, cullResult); - this->_fogCull(frameState, distances, cullResult); - - if (!cullResult.shouldVisit && tile.getUnconditionallyRefine()) { - // Unconditionally refined tiles must always be visited in forbidHoles - // mode, because we need to load this tile's descendants before we can - // render any of its siblings. An unconditionally refined root tile must be - // visited as well, otherwise we won't load anything at all. - if ((this->_options.forbidHoles && - tile.getRefine() == TileRefine::Replace) || - tile.getParent() == nullptr) { - cullResult.shouldVisit = true; - } - } - - if (!cullResult.shouldVisit) { - addCurrentTileAndDescendantsToTilesFadingOutIfPreviouslyRendered( - frameState.viewGroup, - tile, - result); - - frameState.viewGroup.getTraversalState().currentState() = - TileSelectionState(TileSelectionState::Result::Culled); - - ++result.tilesCulled; - - TraversalDetails traversalDetails{}; - - if (this->_options.forbidHoles && tile.getRefine() == TileRefine::Replace) { - // In order to prevent holes, we need to load this tile and also not - // render any siblings until it is ready. We don't actually need to - // render it, though. - addTileToLoadQueue( - frameState, - tile, - TileLoadPriorityGroup::Normal, - tilePriority); - - traversalDetails = - Tileset::createTraversalDetailsForSingleTile(frameState, tile); - } else if (this->_options.preloadSiblings) { - // Preload this culled sibling as requested. - addTileToLoadQueue( - frameState, - tile, - TileLoadPriorityGroup::Preload, - tilePriority); - } - - traversalState.finishNode(&tile); - - return traversalDetails; - } - - if (cullResult.culled) { - ++result.culledTilesVisited; - } - - double tileSse = this->_computeSse(frameState.frustums, tile, distances); - bool meetsSse = this->_meetsSseThreshold(tileSse, cullResult.culled); - - TraversalDetails details = this->_visitTile( - frameState, - depth, - meetsSse, - ancestorMeetsSse, - tile, - tilePriority, - tileSse, - result); - - traversalState.finishNode(&tile); - - return details; -} - -namespace { -bool isLeaf(const Tile& tile) noexcept { return tile.getChildren().empty(); } -} // namespace - -Tileset::TraversalDetails Tileset::_renderLeaf( - const TilesetFrameState& frameState, - Tile& tile, - double tilePriority, - double tileSse, - ViewUpdateResult& result) { - frameState.viewGroup.getTraversalState().currentState() = - TileSelectionState(TileSelectionState::Result::Rendered); - addTileToRender(result, tile, tileSse); - - addTileToLoadQueue( - frameState, - tile, - TileLoadPriorityGroup::Normal, - tilePriority); - - return Tileset::createTraversalDetailsForSingleTile(frameState, tile); -} - -namespace { - -/** - * @brief Determines if we must refine this tile so that we can continue - * rendering the deeper descendant tiles of this tile. - * - * If this tile was refined last frame, and is not yet renderable, then we - * should REFINE past this tile in order to continue rendering the deeper tiles - * that we rendered last frame, until such time as this tile is loaded and we - * can render it instead. This is necessary to avoid detail vanishing when - * the camera zooms out and lower-detail tiles are not yet loaded. - * - * @param tile The tile to check, which is assumed to meet the SSE for - * rendering. - * @param lastFrameSelectionState The selection state of this tile last frame. - * @return True if this tile must be refined instead of rendered, so that we can - * continue rendering deeper tiles. - */ -bool mustContinueRefiningToDeeperTiles( - const Tile& tile, - const TileSelectionState& lastFrameSelectionState) noexcept { - const TileSelectionState::Result originalResult = - lastFrameSelectionState.getOriginalResult(); - - return originalResult == TileSelectionState::Result::Refined && - !tile.isRenderable(); -} - -} // namespace - -Tileset::TraversalDetails Tileset::_renderInnerTile( - const TilesetFrameState& frameState, - Tile& tile, - double tileSse, - ViewUpdateResult& result) { - addCurrentTileDescendantsToTilesFadingOutIfPreviouslyRendered( - frameState.viewGroup, - tile, - result); - frameState.viewGroup.getTraversalState().currentState() = - TileSelectionState(TileSelectionState::Result::Rendered); - addTileToRender(result, tile, tileSse); - - return Tileset::createTraversalDetailsForSingleTile(frameState, tile); -} - -bool Tileset::_loadAndRenderAdditiveRefinedTile( - const TilesetFrameState& frameState, - Tile& tile, - ViewUpdateResult& result, - double tilePriority, - double tileSse, - bool queuedForLoad) { - // If this tile uses additive refinement, we need to render this tile in - // addition to its children. - if (tile.getRefine() == TileRefine::Add) { - addTileToRender(result, tile, tileSse); - if (!queuedForLoad) - addTileToLoadQueue( - frameState, - tile, - TileLoadPriorityGroup::Normal, - tilePriority); - return true; - } - - return false; -} - -bool Tileset::_kickDescendantsAndRenderTile( - const TilesetFrameState& frameState, - Tile& tile, - ViewUpdateResult& result, - TraversalDetails& traversalDetails, - size_t firstRenderedDescendantIndex, - const TilesetViewGroup::LoadQueueCheckpoint& loadQueueBeforeChildren, - bool queuedForLoad, - double tilePriority, - double tileSse) { - // Mark all visited descendants of this tile as kicked. - TilesetViewGroup::TraversalState& traversalState = - frameState.viewGroup.getTraversalState(); - traversalState.forEachCurrentDescendant( - [](const Tile::Pointer& /*pTile*/, TileSelectionState& selectionState) { - selectionState.kick(); - }); - - // If any kicked tiles were rendered last frame, add them to the - // tilesFadingOut. This is unlikely! It would imply that a tile rendered last - // frame has suddenly become unrenderable, and therefore eligible for kicking. - // - // In general, it's possible that a Tile previously traversed has been deleted - // completely, so we have to be careful about dereferencing the Tile pointers - // given to the callback below. However, we can be certain that a Tile that - // was rendered last frame has _not_ been deleted yet. - traversalState.forEachPreviousDescendant( - [&result]( - const Tile::Pointer& pTile, - const TileSelectionState& previousState) { - addToTilesFadingOutIfPreviouslyRendered( - previousState.getResult(), - *pTile, - result); - }); - - // Remove all descendants from the render list and add this tile. - std::vector& renderList = result.tilesToRenderThisFrame; - std::vector& sseList = result.tileScreenSpaceErrorThisFrame; - renderList.erase( - renderList.begin() + - static_cast::iterator::difference_type>( - firstRenderedDescendantIndex), - renderList.end()); - sseList.erase( - sseList.begin() + - static_cast::iterator::difference_type>( - firstRenderedDescendantIndex), - sseList.end()); - - if (tile.getRefine() != Cesium3DTilesSelection::TileRefine::Add) { - addTileToRender(result, tile, tileSse); - } - - traversalState.currentState() = - TileSelectionState(TileSelectionState::Result::Rendered); - - // If we're waiting on heaps of descendants, the above will take too long. So - // in that case, load this tile INSTEAD of loading any of the descendants, and - // tell the up-level we're only waiting on this tile. Keep doing this until we - // actually manage to render this tile. - // Make sure we don't end up waiting on a tile that will _never_ be - // renderable. - TileSelectionState::Result lastFrameSelectionState = - getPreviousState(frameState.viewGroup, tile).getResult(); - const bool wasRenderedLastFrame = - lastFrameSelectionState == TileSelectionState::Result::Rendered; - const bool isRenderable = tile.isRenderable(); - const bool wasReallyRenderedLastFrame = wasRenderedLastFrame && isRenderable; - - if (!wasReallyRenderedLastFrame && - traversalDetails.notYetRenderableCount > - this->_options.loadingDescendantLimit && - !tile.isExternalContent() && !tile.getUnconditionallyRefine()) { - - // Remove all descendants from the load queues. - result.tilesKicked += static_cast( - frameState.viewGroup.restoreTileLoadQueueCheckpoint( - loadQueueBeforeChildren)); - - if (!queuedForLoad) { - addTileToLoadQueue( - frameState, - tile, - TileLoadPriorityGroup::Normal, - tilePriority); - } - - traversalDetails.notYetRenderableCount = tile.isRenderable() ? 0 : 1; - queuedForLoad = true; - } - - traversalDetails.allAreRenderable = isRenderable; - traversalDetails.anyWereRenderedLastFrame = wasReallyRenderedLastFrame; - - return queuedForLoad; -} - -TileOcclusionState Tileset::_checkOcclusion(const Tile& tile) { - const std::shared_ptr& pOcclusionPool = - this->_externals.pTileOcclusionProxyPool; - if (pOcclusionPool) { - // First check if this tile's bounding volume has occlusion info and is - // known to be occluded. - const TileOcclusionRendererProxy* pOcclusion = - pOcclusionPool->fetchOcclusionProxyForTile(tile); - if (!pOcclusion) { - // This indicates we ran out of occlusion proxies. We don't want to wait - // on occlusion info here since it might not ever arrive, so treat this - // tile as if it is _known_ to be unoccluded. - return TileOcclusionState::NotOccluded; - } else - switch ( - static_cast(pOcclusion->getOcclusionState())) { - case TileOcclusionState::OcclusionUnavailable: - // We have an occlusion proxy, but it does not have valid occlusion - // info yet, wait for it. - return TileOcclusionState::OcclusionUnavailable; - break; - case TileOcclusionState::Occluded: - return TileOcclusionState::Occluded; - break; - case TileOcclusionState::NotOccluded: - if (tile.getChildren().size() == 0) { - // This is a leaf tile, so we can't use children bounding volumes. - return TileOcclusionState::NotOccluded; - } - } - - // The tile's bounding volume is known to be unoccluded, but check the - // union of the children bounding volumes since it is tighter fitting. - - // If any children are to be unconditionally refined, we can't rely on - // their bounding volumes. We also don't want to recurse indefinitely to - // find a valid descendant bounding volumes union. - for (const Tile& child : tile.getChildren()) { - if (child.getUnconditionallyRefine()) { - return TileOcclusionState::NotOccluded; - } - } - - this->_childOcclusionProxies.clear(); - this->_childOcclusionProxies.reserve(tile.getChildren().size()); - for (const Tile& child : tile.getChildren()) { - const TileOcclusionRendererProxy* pChildProxy = - pOcclusionPool->fetchOcclusionProxyForTile(child); - - if (!pChildProxy) { - // We ran out of occlusion proxies, treat this as if it is _known_ to - // be unoccluded so we don't wait for it. - return TileOcclusionState::NotOccluded; - } - - this->_childOcclusionProxies.push_back(pChildProxy); - } - - // Check if any of the proxies are known to be unoccluded - for (const TileOcclusionRendererProxy* pChildProxy : - this->_childOcclusionProxies) { - if (pChildProxy->getOcclusionState() == TileOcclusionState::NotOccluded) { - return TileOcclusionState::NotOccluded; - } - } - - // Check if any of the proxies are waiting for valid occlusion info. - for (const TileOcclusionRendererProxy* pChildProxy : - this->_childOcclusionProxies) { - if (pChildProxy->getOcclusionState() == - TileOcclusionState::OcclusionUnavailable) { - // We have an occlusion proxy, but it does not have valid occlusion - // info yet, wait for it. - return TileOcclusionState::OcclusionUnavailable; - } - } - - // If we know the occlusion state of all children, and none are unoccluded, - // we can treat this tile as occluded. - return TileOcclusionState::Occluded; - } - - // We don't have an occlusion pool to query occlusion with, treat everything - // as unoccluded. - return TileOcclusionState::NotOccluded; -} - -namespace { - -enum class VisitTileAction { Render, Refine }; - -} - -// Visits a tile for possible rendering. When we call this function with a tile: -// * The tile has previously been determined to be visible. -// * Its parent tile does _not_ meet the SSE (unless ancestorMeetsSse=true, -// see comments below). -// * The tile may or may not be renderable. -// * The tile has not yet been added to a load queue. -Tileset::TraversalDetails Tileset::_visitTile( - const TilesetFrameState& frameState, - uint32_t depth, - bool meetsSse, - bool ancestorMeetsSse, // Careful: May be modified before being passed to - // children! - Tile& tile, - double tilePriority, - double tileSse, - ViewUpdateResult& result) { - TilesetViewGroup::TraversalState& traversalState = - frameState.viewGroup.getTraversalState(); - - ++result.tilesVisited; - result.maxDepthVisited = glm::max(result.maxDepthVisited, depth); - - // If this is a leaf tile, just render it (it's already been deemed visible). - if (isLeaf(tile)) { - return this->_renderLeaf(frameState, tile, tilePriority, tileSse, result); - } - - const bool unconditionallyRefine = tile.getUnconditionallyRefine(); - const bool refineForSse = !meetsSse && !ancestorMeetsSse; - - // Determine whether to REFINE or RENDER. Note that even if this tile is - // initially marked for RENDER here, it may later switch to REFINE as a - // result of `mustContinueRefiningToDeeperTiles`. - VisitTileAction action; - if (unconditionallyRefine || refineForSse) - action = VisitTileAction::Refine; - else - action = VisitTileAction::Render; - - TileSelectionState lastFrameSelectionState = - getPreviousState(frameState.viewGroup, tile); - TileSelectionState::Result lastFrameSelectionResult = - lastFrameSelectionState.getResult(); - - // If occlusion culling is enabled, we may not want to refine for two - // reasons: - // - The tile is known to be occluded, so don't refine further. - // - The tile was not previously refined and the occlusion state for this - // tile is not known yet, but will be known in the next several frames. If - // delayRefinementForOcclusion is enabled, we will wait until the tile has - // valid occlusion info to decide to refine. This might save us from - // kicking off descendant loads that we later find to be unnecessary. - bool tileLastRefined = - lastFrameSelectionResult == TileSelectionState::Result::Refined; - - bool childLastRefined = false; - traversalState.forEachPreviousChild( - [&](const Tile::Pointer& /*pTile*/, const TileSelectionState& state) { - if (state.getResult() == TileSelectionState::Result::Refined) { - childLastRefined = true; - } - }); - - // If this tile and a child were both refined last frame, this tile does not - // need occlusion results. - bool shouldCheckOcclusion = this->_options.enableOcclusionCulling && - action == VisitTileAction::Refine && - !unconditionallyRefine && - (!tileLastRefined || !childLastRefined); - - if (shouldCheckOcclusion) { - TileOcclusionState occlusion = this->_checkOcclusion(tile); - if (occlusion == TileOcclusionState::Occluded) { - ++result.tilesOccluded; - action = VisitTileAction::Render; - meetsSse = true; - } else if ( - occlusion == TileOcclusionState::OcclusionUnavailable && - this->_options.delayRefinementForOcclusion && - lastFrameSelectionState.getOriginalResult() != - TileSelectionState::Result::Refined) { - ++result.tilesWaitingForOcclusionResults; - action = VisitTileAction::Render; - meetsSse = true; - } - } - - bool queuedForLoad = false; - - if (action == VisitTileAction::Render) { - // This tile meets the screen-space error requirement, so we'd like to - // render it, if we can. - bool mustRefine = - mustContinueRefiningToDeeperTiles(tile, lastFrameSelectionState); - if (mustRefine) { - // // We must refine even though this tile meets the SSE. - action = VisitTileAction::Refine; - - // Loading this tile is very important, because a number of deeper, - // higher-detail tiles are being rendered in its stead, so we want to load - // it with high priority. However, if `ancestorMeetsSse` is set, then our - // parent tile is in the exact same situation, and loading this tile with - // high priority would compete with that one. We should prefer the parent - // because it is closest to the actual desired LOD and because up the tree - // there can only be fewer tiles that need loading. - if (!ancestorMeetsSse) { - addTileToLoadQueue( - frameState, - tile, - TileLoadPriorityGroup::Urgent, - tilePriority); - queuedForLoad = true; - } - - // Fall through to REFINE, but mark this tile as already meeting the - // required SSE. - ancestorMeetsSse = true; - } else { - // Render this tile and return without visiting children. - // Only load this tile if it (not just an ancestor) meets the SSE. - if (!ancestorMeetsSse) { - addTileToLoadQueue( - frameState, - tile, - TileLoadPriorityGroup::Normal, - tilePriority); - } - - return this->_renderInnerTile(frameState, tile, tileSse, result); - } - } - - // Refine! - - queuedForLoad = _loadAndRenderAdditiveRefinedTile( - frameState, - tile, - result, - tilePriority, - tileSse, - queuedForLoad) || - queuedForLoad; - - const size_t firstRenderedDescendantIndex = - result.tilesToRenderThisFrame.size(); - TilesetViewGroup::LoadQueueCheckpoint loadQueueBeforeChildren = - frameState.viewGroup.saveTileLoadQueueCheckpoint(); - - TraversalDetails traversalDetails = this->_visitVisibleChildrenNearToFar( - frameState, - depth, - ancestorMeetsSse, - tile, - result); - - // Zero or more descendant tiles were added to the render list. - // The traversalDetails tell us what happened while visiting the children. - - // Descendants will be kicked if any are not ready to render yet and none - // were rendered last frame. - bool kickDueToNonReadyDescendant = !traversalDetails.allAreRenderable && - !traversalDetails.anyWereRenderedLastFrame; - - // Descendants may also be kicked if this tile was rendered last frame and - // has not finished fading in yet. - const TileRenderContent* pRenderContent = - tile.getContent().getRenderContent(); - bool kickDueToTileFadingIn = - _options.enableLodTransitionPeriod && - _options.kickDescendantsWhileFadingIn && - lastFrameSelectionResult == TileSelectionState::Result::Rendered && - pRenderContent && pRenderContent->getLodTransitionFadePercentage() < 1.0f; - - // Only kick the descendants of this tile if it is renderable, or if we've - // exceeded the loadingDescendantLimit. It's pointless to kick the descendants - // of a tile that is not yet loaded, because it means we will still have a - // hole, and quite possibly a bigger one. - bool wantToKick = kickDueToNonReadyDescendant || kickDueToTileFadingIn; - bool willKick = wantToKick && (traversalDetails.notYetRenderableCount > - this->_options.loadingDescendantLimit || - tile.isRenderable()); - - if (willKick) { - // Kick all descendants out of the render list and render this tile instead - // Continue to load them though! - queuedForLoad = _kickDescendantsAndRenderTile( - frameState, - tile, - result, - traversalDetails, - firstRenderedDescendantIndex, - loadQueueBeforeChildren, - queuedForLoad, - tilePriority, - tileSse); - } else { - if (tile.getRefine() != TileRefine::Add) { - addCurrentTileToTilesFadingOutIfPreviouslyRendered( - frameState.viewGroup, - tile, - result); - } - - traversalState.currentState() = - TileSelectionState(TileSelectionState::Result::Refined); - } - - if (this->_options.preloadAncestors && !queuedForLoad) { - addTileToLoadQueue( - frameState, - tile, - TileLoadPriorityGroup::Preload, - tilePriority); - } - - return traversalDetails; -} - -Tileset::TraversalDetails Tileset::_visitVisibleChildrenNearToFar( - const TilesetFrameState& frameState, - uint32_t depth, - bool ancestorMeetsSse, - Tile& tile, - ViewUpdateResult& result) { - TraversalDetails traversalDetails; - - // TODO: actually visit near-to-far, rather than in order of occurrence. - std::span children = tile.getChildren(); - for (Tile& child : children) { - const TraversalDetails childTraversal = this->_visitTileIfNeeded( - frameState, - depth + 1, - ancestorMeetsSse, - child, - result); - - traversalDetails.allAreRenderable &= childTraversal.allAreRenderable; - traversalDetails.anyWereRenderedLastFrame |= - childTraversal.anyWereRenderedLastFrame; - traversalDetails.notYetRenderableCount += - childTraversal.notYetRenderableCount; - } - - return traversalDetails; -} - -void Tileset::addTileToLoadQueue( - const TilesetFrameState& frameState, - Tile& tile, - TileLoadPriorityGroup priorityGroup, - double priority) { - frameState.viewGroup.addToLoadQueue( - TileLoadTask{&tile, priorityGroup, priority}, - this->_externals.pGltfModifier); -} - -Tileset::TraversalDetails Tileset::createTraversalDetailsForSingleTile( - const TilesetFrameState& frameState, - const Tile& tile) { - TileSelectionState::Result lastFrameResult = - getPreviousState(frameState.viewGroup, tile).getResult(); - - bool isRenderable = tile.isRenderable(); - - bool wasRenderedLastFrame = - lastFrameResult == TileSelectionState::Result::Rendered; - if (!wasRenderedLastFrame && - lastFrameResult == TileSelectionState::Result::Refined) { - if (tile.getRefine() == TileRefine::Add) { - // An additive-refined tile that was refined was also rendered. - wasRenderedLastFrame = true; - } else { - // With replace-refinement, if any of this refined tile's children were - // rendered last frame, but are no longer rendered because this tile is - // loaded and has sufficient detail, we must treat this tile as rendered - // last frame, too. This is necessary to prevent this tile from being - // kicked just because _it_ wasn't rendered last frame (which could cause - // a new hole to appear). - frameState.viewGroup.getTraversalState().forEachPreviousDescendant( - [&](const Tile::Pointer& /* pTile */, - const TileSelectionState& state) { - if (state.getResult() == TileSelectionState::Result::Rendered) { - wasRenderedLastFrame = true; - } - }); - } - } - - TraversalDetails traversalDetails; - traversalDetails.allAreRenderable = isRenderable; - traversalDetails.anyWereRenderedLastFrame = - isRenderable && wasRenderedLastFrame; - traversalDetails.notYetRenderableCount = isRenderable ? 0 : 1; - - return traversalDetails; -} - } // namespace Cesium3DTilesSelection From f4c06d92b3d8caac681056f32a6c7851066e4228 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Thu, 9 Apr 2026 14:05:43 -0700 Subject: [PATCH 08/28] update content calls to use .get() --- Cesium3DTilesSelection/src/Tile.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Cesium3DTilesSelection/src/Tile.cpp b/Cesium3DTilesSelection/src/Tile.cpp index 36a05b34b3..664b590ee0 100644 --- a/Cesium3DTilesSelection/src/Tile.cpp +++ b/Cesium3DTilesSelection/src/Tile.cpp @@ -239,22 +239,22 @@ bool Tile::isRenderable() const noexcept { } bool Tile::isRenderContent() const noexcept { - return this->_content.isRenderContent(); + return this->_content.get().isRenderContent(); } bool Tile::isExternalContent() const noexcept { - return this->_content.isExternalContent(); + return this->_content.get().isExternalContent(); } bool Tile::isEmptyContent() const noexcept { - return this->_content.isEmptyContent(); + return this->_content.get().isEmptyContent(); } TilesetContentLoader* Tile::getLoader() const noexcept { return this->_pLoader; } -TileLoadState Tile::getState() const noexcept { return this->_loadState; } +TileLoadState Tile::getState() const noexcept { return this->_loadState.get(); } namespace { @@ -315,7 +315,7 @@ void Tile::setParent(Tile* pParent) noexcept { } } - this->_pParent = pParent; + this->_pParent.reset(pParent); if (this->getReferenceCount() > 0) { // Add a reference to the new parent, or to the @@ -328,7 +328,9 @@ void Tile::setParent(Tile* pParent) noexcept { } } -void Tile::setState(TileLoadState state) noexcept { this->_loadState = state; } +void Tile::setState(TileLoadState state) noexcept { + this->_loadState.get() = state; +} bool Tile::getMightHaveLatentChildren() const noexcept { return this->_mightHaveLatentChildren; @@ -476,7 +478,7 @@ int32_t Tile::getReferenceCount() const noexcept { } bool Tile::hasReferencingContent() const noexcept { - return !this->_content.isUnknownContent() && + return !this->_content.get().isUnknownContent() && TileIdUtilities::isLoadable(this->_id); } From 836f7093b864d508150c07ab60ee0f0f3fae6bc6 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Thu, 9 Apr 2026 14:11:56 -0700 Subject: [PATCH 09/28] create free selectTiles test --- .../test/TestTilesetSelection.cpp | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 Cesium3DTilesSelection/test/TestTilesetSelection.cpp diff --git a/Cesium3DTilesSelection/test/TestTilesetSelection.cpp b/Cesium3DTilesSelection/test/TestTilesetSelection.cpp new file mode 100644 index 0000000000..637d096f5f --- /dev/null +++ b/Cesium3DTilesSelection/test/TestTilesetSelection.cpp @@ -0,0 +1,227 @@ +// TestTilesetSelection.cpp +// +// Unit tests for the selectTiles() free function. These tests construct +// minimal in-memory tile trees and invoke the selection algorithm directly, +// without file I/O or async machinery beyond what TilesetViewGroup requires. + +#include "SimplePrepareRendererResource.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace Cesium3DTilesSelection; +using namespace CesiumAsync; +using namespace CesiumGeospatial; +using namespace CesiumNativeTests; +using namespace CesiumUtility; + +namespace { + +// Minimal in-memory loader — always reports tiles as empty, no children. +class EmptyLoader : public TilesetContentLoader { +public: + Future loadTileContent(const TileLoadInput& input) override { + return input.asyncSystem.createResolvedFuture(TileLoadResult{ + .contentKind = TileEmptyContent(), + .glTFUpAxis = CesiumGeometry::Axis::Z, + .updatedBoundingVolume = std::nullopt, + .updatedContentBoundingVolume = std::nullopt, + .rasterOverlayDetails = std::nullopt, + .pAssetAccessor = input.pAssetAccessor, + .pCompletedRequest = nullptr, + .tileInitializer = {}, + .state = TileLoadResultState::Success, + .ellipsoid = CesiumGeospatial::Ellipsoid::WGS84}); + } + + TileChildrenResult createTileChildren( + const Tile& /* tile */, + const Ellipsoid& /* ellipsoid */) override { + return TileChildrenResult{{}, TileLoadResultState::Success}; + } +}; + +// Build a minimal TilesetExternals with no asset accessor. +TilesetExternals makeExternals() { + return TilesetExternals{ + .pAssetAccessor = std::make_shared( + std::map>{}), + .pPrepareRendererResources = + std::make_shared(), + .asyncSystem = AsyncSystem(std::make_shared()), + .pCreditSystem = std::make_shared(), + }; +} + +// Build a ViewState looking at the root of a tileset from far away so SSE +// is low and the root tile meets the threshold. +ViewState makeFarViewState() { + const Ellipsoid& ellipsoid = Ellipsoid::WGS84; + // Camera above 0N 0E at 15 000 km — far enough that a global-scale tile + // easily meets any reasonable SSE threshold. + Cartographic camCarto{0.0, 0.0, 15'000'000.0}; + glm::dvec3 camPos = ellipsoid.cartographicToCartesian(camCarto); + glm::dvec3 camDir = glm::normalize(-camPos); + glm::dvec3 camUp{0.0, 0.0, 1.0}; + glm::dvec2 viewport{1280.0, 720.0}; + double hFov = Math::degreesToRadians(60.0); + double vFov = 2.0 * std::atan(std::tan(hFov * 0.5) / (1280.0 / 720.0)); + return ViewState(camPos, camDir, camUp, viewport, hFov, vFov, ellipsoid); +} + +// Build a ViewState from very close in so the root does NOT meet SSE and +// children would be needed. Works with EllipsoidTilesetLoader. +ViewState makeCloseViewState() { + const Ellipsoid& ellipsoid = Ellipsoid::WGS84; + Cartographic camCarto{0.0, 0.0, 100.0}; + glm::dvec3 camPos = ellipsoid.cartographicToCartesian(camCarto); + glm::dvec3 camDir = glm::normalize(-camPos); + glm::dvec3 camUp{0.0, 0.0, 1.0}; + glm::dvec2 viewport{1280.0, 720.0}; + double hFov = Math::degreesToRadians(60.0); + double vFov = 2.0 * std::atan(std::tan(hFov * 0.5) / (1280.0 / 720.0)); + return ViewState(camPos, camDir, camUp, viewport, hFov, vFov, ellipsoid); +} + +} // namespace + +TEST_CASE("selectTiles is callable as a free function") { + // Verify that selectTiles() can be invoked directly without going through + // Tileset::updateViewGroup. This is the isolation guarantee introduced by + // the Phase B refactoring. + + TilesetExternals externals = makeExternals(); + TilesetOptions options; + options.maximumScreenSpaceError = 16.0; + + // Use EllipsoidTilesetLoader so we get a loaded root tile without I/O. + auto pTileset = EllipsoidTilesetLoader::createTileset(externals, options); + REQUIRE(pTileset != nullptr); + + // Let the tileset initialise (root tile creation, etc.) + externals.asyncSystem.dispatchMainThreadTasks(); + pTileset->loadTiles(); + externals.asyncSystem.dispatchMainThreadTasks(); + pTileset->loadTiles(); + + Tile* pRoot = const_cast(pTileset->getRootTile()); + REQUIRE(pRoot != nullptr); + + ViewState viewState = makeFarViewState(); + std::vector frustums{viewState}; + + std::vector fogDensities(1, 0.0); + TilesetViewGroup& viewGroup = pTileset->getDefaultViewGroup(); + + TilesetFrameState frameState{ + viewGroup, + frustums, + std::move(fogDensities), + // No tileStateUpdater needed — tiles are already loaded. + {}}; + + std::vector scratchDistances; + std::vector scratchOcclusion; + + TileSelectionContext ctx{options, externals}; + + viewGroup.startNewFrame(*pTileset, frameState); + ViewUpdateResult result = + selectTiles(ctx, frameState, *pRoot, scratchDistances, scratchOcclusion); + viewGroup.finishFrame(*pTileset, frameState); + + // From far away the root tile (or its immediate children) should be + // selected; there must be at least one tile to render. + CHECK(result.tilesToRenderThisFrame.size() >= 1); + // No tiles should have been kicked. + CHECK(result.tilesKicked == 0); +} + +TEST_CASE("selectTiles result matches updateViewGroup result") { + // Run both the selectTiles() free function and Tileset::updateViewGroup on + // the same tileset in the same frame configuration and verify they produce + // identical render lists. + + TilesetExternals externals = makeExternals(); + TilesetOptions options; + options.maximumScreenSpaceError = 16.0; + + auto pTileset = EllipsoidTilesetLoader::createTileset(externals, options); + REQUIRE(pTileset != nullptr); + + // Warm up the tileset. + ViewState viewState = makeFarViewState(); + for (int i = 0; i < 3; ++i) { + externals.asyncSystem.dispatchMainThreadTasks(); + pTileset->updateViewGroup(pTileset->getDefaultViewGroup(), {viewState}); + externals.asyncSystem.dispatchMainThreadTasks(); + pTileset->loadTiles(); + } + + Tile* pRoot = const_cast(pTileset->getRootTile()); + REQUIRE(pRoot != nullptr); + + // Run via updateViewGroup (the established path) + ViewUpdateResult referenceResult = + pTileset->updateViewGroup(pTileset->getDefaultViewGroup(), {viewState}); + + size_t referenceRenderCount = referenceResult.tilesToRenderThisFrame.size(); + uint32_t referenceVisited = referenceResult.tilesVisited; + + // Run via selectTiles() directly (the new path) + std::vector fogDensities(1, 0.0); + TilesetViewGroup& viewGroup = pTileset->getDefaultViewGroup(); + + std::vector frustums{viewState}; + TilesetFrameState frameState{ + viewGroup, + frustums, + std::move(fogDensities), + // Mirror what updateViewGroup does — call updateTileContent via the + // tileStateUpdater so tile states advance identically. + {}}; + + std::vector scratchDistances; + std::vector scratchOcclusion; + + TileSelectionContext ctx{options, externals}; + + viewGroup.startNewFrame(*pTileset, frameState); + ViewUpdateResult freeResult = + selectTiles(ctx, frameState, *pRoot, scratchDistances, scratchOcclusion); + viewGroup.finishFrame(*pTileset, frameState); + + // Tile counts must agree between the two paths. + CHECK(freeResult.tilesToRenderThisFrame.size() == referenceRenderCount); + CHECK(freeResult.tilesVisited == referenceVisited); +} From 32abaaa181f29e96ad286e52c63886a59b59e6db Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Fri, 10 Apr 2026 06:37:07 -0700 Subject: [PATCH 10/28] remov unused makeCloseViewstate function to fix -Wunused-function ci error --- .../test/TestTilesetSelection.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Cesium3DTilesSelection/test/TestTilesetSelection.cpp b/Cesium3DTilesSelection/test/TestTilesetSelection.cpp index 637d096f5f..a09a513fff 100644 --- a/Cesium3DTilesSelection/test/TestTilesetSelection.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetSelection.cpp @@ -99,20 +99,6 @@ ViewState makeFarViewState() { return ViewState(camPos, camDir, camUp, viewport, hFov, vFov, ellipsoid); } -// Build a ViewState from very close in so the root does NOT meet SSE and -// children would be needed. Works with EllipsoidTilesetLoader. -ViewState makeCloseViewState() { - const Ellipsoid& ellipsoid = Ellipsoid::WGS84; - Cartographic camCarto{0.0, 0.0, 100.0}; - glm::dvec3 camPos = ellipsoid.cartographicToCartesian(camCarto); - glm::dvec3 camDir = glm::normalize(-camPos); - glm::dvec3 camUp{0.0, 0.0, 1.0}; - glm::dvec2 viewport{1280.0, 720.0}; - double hFov = Math::degreesToRadians(60.0); - double vFov = 2.0 * std::atan(std::tan(hFov * 0.5) / (1280.0 / 720.0)); - return ViewState(camPos, camDir, camUp, viewport, hFov, vFov, ellipsoid); -} - } // namespace TEST_CASE("selectTiles is callable as a free function") { From acb9a7d7d3a03edafa5249fcf73bc93fdf517b90 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Fri, 10 Apr 2026 06:41:51 -0700 Subject: [PATCH 11/28] document undocumented public members --- .../include/Cesium3DTilesSelection/ThreadSafety.h | 4 ++++ .../include/Cesium3DTilesSelection/TileHierarchy.h | 2 ++ .../include/Cesium3DTilesSelection/TileObserver.h | 10 ++++++++++ .../include/Cesium3DTilesSelection/TileUnloadQueue.h | 2 ++ .../include/Cesium3DTilesSelection/TilesetSelection.h | 2 ++ 5 files changed, 20 insertions(+) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h index ac319d1d4c..9a504b4d97 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h @@ -72,9 +72,13 @@ template class MainThreadOnly { // trivially moveable) can be moved during tile tree construction, which // happens on the main thread. Thread-id on copy/move keeps the owner as the // constructing thread (the main thread). + /** @brief Copy constructor. Retains the owner thread of the source. */ MainThreadOnly(const MainThreadOnly&) = default; + /** @brief Move constructor. Retains the owner thread of the source. */ MainThreadOnly(MainThreadOnly&&) noexcept = default; + /** @brief Copy assignment. Retains the owner thread. */ MainThreadOnly& operator=(const MainThreadOnly&) = default; + /** @brief Move assignment. Retains the owner thread. */ MainThreadOnly& operator=(MainThreadOnly&&) noexcept = default; ~MainThreadOnly() = default; diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h index 9d4f5e7893..84a4f1f3ec 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h @@ -22,7 +22,9 @@ class TileHierarchy { TileHierarchy(const TileHierarchy&) = delete; TileHierarchy& operator=(const TileHierarchy&) = delete; + /** @brief Move constructor. */ TileHierarchy(TileHierarchy&&) noexcept = default; + /** @brief Move assignment. */ TileHierarchy& operator=(TileHierarchy&&) noexcept = default; /** @brief Returns the root tile, or nullptr when not yet loaded. diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h index 50497dd8fc..aa7278359f 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h @@ -43,9 +43,13 @@ template class TileObserver { constexpr TileObserver(std::nullptr_t) noexcept : _ptr(nullptr) {} // Non-owning — no special copy/move/destructor semantics needed. + /** @brief Copy constructor. */ TileObserver(const TileObserver&) noexcept = default; + /** @brief Move constructor. */ TileObserver(TileObserver&&) noexcept = default; + /** @brief Copy assignment. */ TileObserver& operator=(const TileObserver&) noexcept = default; + /** @brief Move assignment. */ TileObserver& operator=(TileObserver&&) noexcept = default; ~TileObserver() noexcept = default; @@ -87,23 +91,28 @@ template class TileObserver { operator==(const TileObserver& lhs, const TileObserver& rhs) noexcept { return lhs._ptr == rhs._ptr; } + /** @brief Inequality comparison. */ friend constexpr bool operator!=(const TileObserver& lhs, const TileObserver& rhs) noexcept { return lhs._ptr != rhs._ptr; } + /** @brief Equality comparison with nullptr. */ friend constexpr bool operator==(const TileObserver& lhs, std::nullptr_t) noexcept { return lhs._ptr == nullptr; } + /** @brief Inequality comparison with nullptr. */ friend constexpr bool operator!=(const TileObserver& lhs, std::nullptr_t) noexcept { return lhs._ptr != nullptr; } + /** @brief Equality comparison with nullptr (reversed order). */ friend constexpr bool operator==(std::nullptr_t, const TileObserver& rhs) noexcept { return rhs._ptr == nullptr; } + /** @brief Inequality comparison with nullptr (reversed order). */ friend constexpr bool operator!=(std::nullptr_t, const TileObserver& rhs) noexcept { return rhs._ptr != nullptr; @@ -114,6 +123,7 @@ template class TileObserver { operator==(const TileObserver& lhs, const T* rhs) noexcept { return lhs._ptr == rhs; } + /** @brief Raw-pointer inequality. */ friend constexpr bool operator!=(const TileObserver& lhs, const T* rhs) noexcept { return lhs._ptr != rhs; diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h index d009e0ffcf..9d39f26c80 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h @@ -22,7 +22,9 @@ class TileUnloadQueue { TileUnloadQueue(const TileUnloadQueue&) = delete; TileUnloadQueue& operator=(const TileUnloadQueue&) = delete; + /** @brief Move constructor. */ TileUnloadQueue(TileUnloadQueue&&) noexcept = default; + /** @brief Move assignment. */ TileUnloadQueue& operator=(TileUnloadQueue&&) noexcept = default; /** diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h index c044fbf83e..1ceb3bf403 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h @@ -18,7 +18,9 @@ class TileOcclusionRendererProxy; * algorithm. Holds only references — no ownership. */ struct TileSelectionContext { + /** @brief The tileset configuration options. */ const TilesetOptions& options; + /** @brief The external interfaces (asset accessor, task processor, etc.). */ const TilesetExternals& externals; }; From 9abf20922882c64297ac5760a04f32d88d48eca8 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Fri, 10 Apr 2026 07:27:39 -0700 Subject: [PATCH 12/28] satisfy clang-tidy includes --- Cesium3DTilesSelection/src/TileOverlaySystem.cpp | 4 ++++ Cesium3DTilesSelection/src/Tileset.cpp | 13 +------------ Cesium3DTilesSelection/src/TilesetSelection.cpp | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Cesium3DTilesSelection/src/TileOverlaySystem.cpp b/Cesium3DTilesSelection/src/TileOverlaySystem.cpp index c08fd5fe9e..b6013cdbc5 100644 --- a/Cesium3DTilesSelection/src/TileOverlaySystem.cpp +++ b/Cesium3DTilesSelection/src/TileOverlaySystem.cpp @@ -1,5 +1,9 @@ #include "TileOverlaySystem.h" +#include +#include +#include + namespace Cesium3DTilesSelection { TileOverlaySystem::TileOverlaySystem( diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index 6a589a8792..c4a3a71006 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -1,7 +1,6 @@ #include "TilesetContentManager.h" #include "TilesetHeightQuery.h" -#include #include #include #include @@ -10,8 +9,6 @@ #include #include #include -#include -#include #include #include #include @@ -19,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -26,28 +24,19 @@ #include #include #include -#include -#include #include #include #include #include -#include #include #include -#include -#include -#include #include -#include #include #include -#include #include #include -#include #include #include #include diff --git a/Cesium3DTilesSelection/src/TilesetSelection.cpp b/Cesium3DTilesSelection/src/TilesetSelection.cpp index bf9d58fb34..88363dd8aa 100644 --- a/Cesium3DTilesSelection/src/TilesetSelection.cpp +++ b/Cesium3DTilesSelection/src/TilesetSelection.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -39,6 +38,7 @@ #include #include #include +#include #include #include From cb0aac9e40f22457e0b13bdbd6d656c023a9cd5d Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Sat, 11 Apr 2026 10:32:35 -0700 Subject: [PATCH 13/28] Update comment TileOverlaySystem.h --- Cesium3DTilesSelection/src/TileOverlaySystem.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cesium3DTilesSelection/src/TileOverlaySystem.h b/Cesium3DTilesSelection/src/TileOverlaySystem.h index 9f1f54a82c..0636bcbfd0 100644 --- a/Cesium3DTilesSelection/src/TileOverlaySystem.h +++ b/Cesium3DTilesSelection/src/TileOverlaySystem.h @@ -17,8 +17,7 @@ namespace Cesium3DTilesSelection { * @brief Owns the raster overlay collection and the upsampler. * * This is the single point of ownership for all overlay-related resources. - * In the current design tiles hold references into data owned here; Phase 4 - * will make those non-owning references explicit at the type level. + * In the current design tiles hold references into data owned here. * * The upsampler is a `TilesetContentLoader` specialisation whose `setOwner()` * must be called by the containing `TilesetContentManager` after construction, From 1a576f86b3f2987cc93c04cc88c68bbfef6422bb Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Mon, 13 Apr 2026 06:57:12 -0700 Subject: [PATCH 14/28] move selectTiles to top --- .../src/TilesetSelection.cpp | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/Cesium3DTilesSelection/src/TilesetSelection.cpp b/Cesium3DTilesSelection/src/TilesetSelection.cpp index 88363dd8aa..bbd84a5e0b 100644 --- a/Cesium3DTilesSelection/src/TilesetSelection.cpp +++ b/Cesium3DTilesSelection/src/TilesetSelection.cpp @@ -1,9 +1,3 @@ -// TilesetSelection.cpp -// -// Implements the tile LOD selection / traversal algorithm as pure free -// functions. No dependency on the Tileset class — all context is passed -// through explicit parameters. - #include #include #include @@ -49,10 +43,9 @@ using namespace CesiumUtility; namespace Cesium3DTilesSelection { +// Internal types used by selectTiles and traversal helpers. namespace { -// Internal context — bundles all inputs for the recursive traversal helpers. -// References only; no ownership. struct TraversalContext { const TilesetOptions& options; const TilesetExternals& externals; @@ -61,7 +54,6 @@ struct TraversalContext { std::vector& childOcclusionProxies; }; -// Internal per-subtree summary returned by each visit function. struct TraversalDetails { bool allAreRenderable = true; bool anyWereRenderedLastFrame = false; @@ -75,7 +67,33 @@ struct CullResult { enum class VisitTileAction { Render, Refine }; -// Small helpers (previously in anonymous namespace in Tileset.cpp) +// Forward declaration of the top-level recursive entry point. +TraversalDetails visitTileIfNeeded( + const TraversalContext& ctx, + uint32_t depth, + bool ancestorMeetsSse, + Tile& tile, + ViewUpdateResult& result); + +} // namespace + +ViewUpdateResult selectTiles( + const TileSelectionContext& ctx, + const TilesetFrameState& frameState, + Tile& rootTile) { + TraversalContext tctx{ + ctx.options, + ctx.externals, + frameState, + ctx.scratchDistances, + ctx.scratchOcclusionProxies}; + + ViewUpdateResult result; + visitTileIfNeeded(tctx, 0, false, rootTile, result); + return result; +} + +namespace { TileSelectionState getPreviousState( const TilesetViewGroup& viewGroup, @@ -872,24 +890,4 @@ TraversalDetails visitTileIfNeeded( } // anonymous namespace -// Public entry point -ViewUpdateResult selectTiles( - const TileSelectionContext& ctx, - const TilesetFrameState& frameState, - Tile& rootTile, - std::vector& scratchDistances, - std::vector& scratchOcclusionProxies) { - - TraversalContext tctx{ - ctx.options, - ctx.externals, - frameState, - scratchDistances, - scratchOcclusionProxies}; - - ViewUpdateResult result; - visitTileIfNeeded(tctx, 0, false, rootTile, result); - return result; -} - } // namespace Cesium3DTilesSelection From 051f3ae745f79eba512e240719c4954215d34eef Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Mon, 13 Apr 2026 06:58:18 -0700 Subject: [PATCH 15/28] revert unnecessary changes based on feedback --- .../Cesium3DTilesSelection/ThreadSafety.h | 124 ---------------- .../include/Cesium3DTilesSelection/Tile.h | 34 ++--- .../Cesium3DTilesSelection/TileHierarchy.h | 47 ------ .../Cesium3DTilesSelection/TileObserver.h | 136 ------------------ .../Cesium3DTilesSelection/TileUnloadQueue.h | 22 ++- .../Cesium3DTilesSelection/TilesetSelection.h | 23 +-- Cesium3DTilesSelection/src/Tile.cpp | 16 +-- .../src/TileOverlaySystem.cpp | 15 -- .../src/TileOverlaySystem.h | 64 --------- Cesium3DTilesSelection/src/Tileset.cpp | 11 +- .../src/TilesetContentManager.cpp | 45 +++--- .../src/TilesetContentManager.h | 10 +- .../test/MockTilesetContentManager.cpp | 2 +- .../test/TestTilesetSelection.cpp | 18 ++- 14 files changed, 84 insertions(+), 483 deletions(-) delete mode 100644 Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h delete mode 100644 Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h delete mode 100644 Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h delete mode 100644 Cesium3DTilesSelection/src/TileOverlaySystem.cpp delete mode 100644 Cesium3DTilesSelection/src/TileOverlaySystem.h diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h deleted file mode 100644 index 9a504b4d97..0000000000 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h +++ /dev/null @@ -1,124 +0,0 @@ -#pragma once - -// ThreadSafety.h — Ownership-discipline types for Cesium3DTilesSelection. -// -// These types encode threading contracts at the type level so that violations -// are caught in debug builds (assertion failures) rather than silently causing -// data races. -// -// Annotation convention for functions (comment-based; not enforced by tooling): -// [main-thread] — must only be called from the main thread. -// [any-thread] — may be called from any thread (the type itself or an -// atomic/future enforces safety internally). -// [worker-thread] — must only be called from within a worker-thread dispatch. - -#include - -#include -#include -#include - -namespace Cesium3DTilesSelection { - -/** - * @brief Wraps a value that must only be accessed from the main thread. - * - * In debug builds, every access through `get()` (mutable or const) asserts - * that `std::this_thread::get_id()` matches the thread on which this wrapper - * was constructed. This catches accidental cross-thread access at runtime - * without requiring a lock. - * - * In release builds the wrapper compiles away to zero overhead — `get()` - * is a direct member reference with no branching or memory barrier. - * - * ### Usage - * - * ```cpp - * // In a class declaration: - * MainThreadOnly _content; - * MainThreadOnly _loadState; - * - * // Access (debug: asserts main thread; release: direct access): - * TileContent& c = _content.get(); - * ``` - * - * @tparam T The wrapped value type. - */ -template class MainThreadOnly { -public: - /** @brief Default-construct the wrapped value on the calling (main) thread. - */ - MainThreadOnly() -#ifndef NDEBUG - : _ownerThread(std::this_thread::get_id()) -#endif - { - } - - /** @brief Construct with an initial value on the calling (main) thread. */ - template < - typename... Args, - typename = std::enable_if_t>> - explicit MainThreadOnly(Args&&... args) - : _value(std::forward(args)...) -#ifndef NDEBUG - , - _ownerThread(std::this_thread::get_id()) -#endif - { - } - - // Allow default copy/move so that TileContent and TileLoadState (both - // trivially moveable) can be moved during tile tree construction, which - // happens on the main thread. Thread-id on copy/move keeps the owner as the - // constructing thread (the main thread). - /** @brief Copy constructor. Retains the owner thread of the source. */ - MainThreadOnly(const MainThreadOnly&) = default; - /** @brief Move constructor. Retains the owner thread of the source. */ - MainThreadOnly(MainThreadOnly&&) noexcept = default; - /** @brief Copy assignment. Retains the owner thread. */ - MainThreadOnly& operator=(const MainThreadOnly&) = default; - /** @brief Move assignment. Retains the owner thread. */ - MainThreadOnly& operator=(MainThreadOnly&&) noexcept = default; - ~MainThreadOnly() = default; - - /** - * @brief Returns a mutable reference to the wrapped value. - * - * Debug-asserts that the caller is on the owner (main) thread. - */ - T& get() noexcept { - assertMainThread(); - return _value; - } - - /** - * @brief Returns a const reference to the wrapped value. - * - * Debug-asserts that the caller is on the owner (main) thread. - * - * Note: even reads are main-thread-only because the writer is also the main - * thread and there is no synchronisation — an unsynchronised read from - * another thread would still be a data race. - */ - const T& get() const noexcept { - assertMainThread(); - return _value; - } - -private: - void assertMainThread() const noexcept { -#ifndef NDEBUG - CESIUM_ASSERT( - std::this_thread::get_id() == _ownerThread && - "MainThreadOnly accessed from outside the main thread"); -#endif - } - - T _value{}; -#ifndef NDEBUG - std::thread::id _ownerThread; -#endif -}; - -} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h index 21d88a2eff..e4c007d0fe 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h @@ -3,10 +3,8 @@ #include #include #include -#include #include #include -#include #include #include #include @@ -236,15 +234,14 @@ class CESIUM3DTILESSELECTION_API Tile final { /** * @brief Returns the parent of this tile in the tile hierarchy. * - * This will be `nullptr` if this is the root tile. The returned pointer is - * non-owning — the caller must not store it beyond the tile's lifetime. + * This will be `nullptr` if this is the root tile. * - * @return The parent, or `nullptr`. + * @return The parent. */ - Tile* getParent() noexcept { return this->_pParent.get(); } + Tile* getParent() noexcept { return this->_pParent; } /** @copydoc Tile::getParent() */ - const Tile* getParent() const noexcept { return this->_pParent.get(); } + const Tile* getParent() const noexcept { return this->_pParent; } /** * @brief Returns a *view* on the children of this tile. @@ -502,12 +499,12 @@ class CESIUM3DTILESSELECTION_API Tile final { } /** - * @brief Get the content of the tile. [main-thread] + * @brief Get the content of the tile. */ - const TileContent& getContent() const noexcept { return _content.get(); } + const TileContent& getContent() const noexcept { return _content; } - /** @copydoc Tile::getContent() const [main-thread] */ - TileContent& getContent() noexcept { return _content.get(); } + /** @copydoc Tile::getContent() const */ + TileContent& getContent() noexcept { return _content; } /** * @brief Determines if this tile is currently renderable. @@ -535,7 +532,7 @@ class CESIUM3DTILESSELECTION_API Tile final { TilesetContentLoader* getLoader() const noexcept; /** - * @brief Returns the {@link TileLoadState} of this tile. [main-thread] + * @brief Returns the {@link TileLoadState} of this tile. */ TileLoadState getState() const noexcept; @@ -687,10 +684,7 @@ class CESIUM3DTILESSELECTION_API Tile final { void setMightHaveLatentChildren(bool mightHaveLatentChildren) noexcept; // Position in bounding-volume hierarchy. - // Non-owning: the child observes the parent but does not extend its lifetime. - // Reference-count bookkeeping (addReference/releaseReference) is performed - // explicitly in Tile.cpp whenever the child's own reference count changes. - TileObserver _pParent; + Tile* _pParent; std::vector _children; // Properties from tileset.json. @@ -703,13 +697,11 @@ class CESIUM3DTILESSELECTION_API Tile final { TileRefine _refine; glm::dmat4x4 _transform; - // tile content — both fields are [main-thread-only]; access via - // getContent() / getState() / setState() which enforce the threading - // contract in debug builds via MainThreadOnly. + // tile content CesiumUtility::DoublyLinkedListPointers _unusedTilesLinks; - MainThreadOnly _content; + TileContent _content; TilesetContentLoader* _pLoader; - MainThreadOnly _loadState; + TileLoadState _loadState; bool _mightHaveLatentChildren; // mapped raster overlay diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h deleted file mode 100644 index 84a4f1f3ec..0000000000 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include - -#include - -namespace Cesium3DTilesSelection { - -/** - * @brief Owns the root of the tile tree. - * - * All Tile instances are either held here (as the root) or owned - * by their parent via `Tile::_children`. This is the single point of - * tree ownership — it is non-copyable and non-shared. - * - * Access must occur on the main thread. [main-thread-only] - */ -class TileHierarchy { -public: - TileHierarchy() noexcept = default; - ~TileHierarchy() noexcept = default; - - TileHierarchy(const TileHierarchy&) = delete; - TileHierarchy& operator=(const TileHierarchy&) = delete; - /** @brief Move constructor. */ - TileHierarchy(TileHierarchy&&) noexcept = default; - /** @brief Move assignment. */ - TileHierarchy& operator=(TileHierarchy&&) noexcept = default; - - /** @brief Returns the root tile, or nullptr when not yet loaded. - * [main-thread] */ - const Tile* getRoot() const noexcept { return _pRoot.get(); } - /** @brief Returns the root tile, or nullptr when not yet loaded. - * [main-thread] */ - Tile* getRoot() noexcept { return _pRoot.get(); } - - /** @brief Transfers ownership of a new root tile into this hierarchy. - * [main-thread] */ - void setRoot(std::unique_ptr&& pRoot) noexcept { - _pRoot = std::move(pRoot); - } - -private: - std::unique_ptr _pRoot; -}; - -} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h deleted file mode 100644 index aa7278359f..0000000000 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileObserver.h +++ /dev/null @@ -1,136 +0,0 @@ -#pragma once - -#include - -#include - -namespace Cesium3DTilesSelection { - -/** - * @brief A non-owning, non-reference-counted observer pointer. - * - * This type documents that the holder **does not** manage the lifetime of the - * pointed-to object. It provides the same null-check and dereference interface - * as a raw pointer, but makes the ownership intent explicit at the type level. - * - * Unlike `std::shared_ptr` or `CesiumUtility::IntrusivePointer`, constructing - * or destroying a `TileObserver` never increments or decrements any reference - * count. The pointed-to object must stay alive by some other means for the - * duration of the observer's use. - * - * Intended use: parent-back-pointer in the `Tile` hierarchy, where children - * observe (but do not own) their parent. The parent's lifetime is guaranteed - * by the child holding an *intrusive reference count* on the parent — that - * reference counting is done explicitly via `addReference` / `releaseReference` - * in `Tile.cpp`, and is a separate mechanism from this type. - * - * @tparam T The pointed-to type. - */ -template class TileObserver { -public: - /** @brief Constructs a null observer. */ - constexpr TileObserver() noexcept : _ptr(nullptr) {} - - /** - * @brief Constructs an observer pointing at `ptr`. - * - * Accepts `nullptr` explicitly because the field is frequently initialized - * to `nullptr` before a parent is set. - */ - constexpr explicit TileObserver(T* ptr) noexcept : _ptr(ptr) {} - - /** @brief Constructs a null observer from a null pointer literal. */ - constexpr TileObserver(std::nullptr_t) noexcept : _ptr(nullptr) {} - - // Non-owning — no special copy/move/destructor semantics needed. - /** @brief Copy constructor. */ - TileObserver(const TileObserver&) noexcept = default; - /** @brief Move constructor. */ - TileObserver(TileObserver&&) noexcept = default; - /** @brief Copy assignment. */ - TileObserver& operator=(const TileObserver&) noexcept = default; - /** @brief Move assignment. */ - TileObserver& operator=(TileObserver&&) noexcept = default; - ~TileObserver() noexcept = default; - - /** - * @brief Returns the observed pointer. May be `nullptr`. - */ - constexpr T* get() const noexcept { return _ptr; } - - /** - * @brief Arrow operator. Debug-asserts that the pointer is not null. - */ - constexpr T* operator->() const noexcept { - CESIUM_ASSERT(_ptr != nullptr); - return _ptr; - } - - /** - * @brief Dereference operator. Debug-asserts that the pointer is not null. - */ - constexpr T& operator*() const noexcept { - CESIUM_ASSERT(_ptr != nullptr); - return *_ptr; - } - - /** @brief Returns `true` if the observed pointer is non-null. */ - constexpr explicit operator bool() const noexcept { return _ptr != nullptr; } - - /** @brief Sets the observed pointer, or resets to null. */ - void reset(T* ptr = nullptr) noexcept { _ptr = ptr; } - - /** @copydoc TileObserver::reset() */ - TileObserver& operator=(std::nullptr_t) noexcept { - _ptr = nullptr; - return *this; - } - - /** @brief Equality comparison. */ - friend constexpr bool - operator==(const TileObserver& lhs, const TileObserver& rhs) noexcept { - return lhs._ptr == rhs._ptr; - } - /** @brief Inequality comparison. */ - friend constexpr bool - operator!=(const TileObserver& lhs, const TileObserver& rhs) noexcept { - return lhs._ptr != rhs._ptr; - } - - /** @brief Equality comparison with nullptr. */ - friend constexpr bool - operator==(const TileObserver& lhs, std::nullptr_t) noexcept { - return lhs._ptr == nullptr; - } - /** @brief Inequality comparison with nullptr. */ - friend constexpr bool - operator!=(const TileObserver& lhs, std::nullptr_t) noexcept { - return lhs._ptr != nullptr; - } - /** @brief Equality comparison with nullptr (reversed order). */ - friend constexpr bool - operator==(std::nullptr_t, const TileObserver& rhs) noexcept { - return rhs._ptr == nullptr; - } - /** @brief Inequality comparison with nullptr (reversed order). */ - friend constexpr bool - operator!=(std::nullptr_t, const TileObserver& rhs) noexcept { - return rhs._ptr != nullptr; - } - - /** @brief Raw-pointer equality (useful for CESIUM_ASSERT comparisons). */ - friend constexpr bool - operator==(const TileObserver& lhs, const T* rhs) noexcept { - return lhs._ptr == rhs; - } - /** @brief Raw-pointer inequality. */ - friend constexpr bool - operator!=(const TileObserver& lhs, const T* rhs) noexcept { - return lhs._ptr != rhs; - } - -private: - T* _ptr; -}; - -} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h index 9d39f26c80..fd4bf801da 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h @@ -8,12 +8,9 @@ namespace Cesium3DTilesSelection { * @brief Tracks tiles whose content may be evicted when memory is low. * * Tiles at the head are least-recently-used; tiles at the tail are - * most-recently-used. All methods must be called from the main thread. - * [main-thread-only] + * most-recently-used. * - * This type makes the ownership intent explicit: it manages a LRU linked-list - * of non-owning Tile references. A tile's presence here does NOT imply - * ownership — the tile hierarchy (TileHierarchy) is the sole owner. + * Manages a LRU linked-list of non-owning Tile references. */ class TileUnloadQueue { public: @@ -29,7 +26,7 @@ class TileUnloadQueue { /** * @brief Adds `tile` to the tail (most-recently-used end) of the queue. - * No-op if `tile` is already tracked. [main-thread] + * No-op if `tile` is already tracked. */ void markEligible(Tile& tile) noexcept { if (!_queue.contains(tile)) { @@ -38,27 +35,24 @@ class TileUnloadQueue { } /** - * @brief Removes `tile` from the queue. No-op if not present. [main-thread] + * @brief Removes `tile` from the queue. No-op if not present. */ void markIneligible(Tile& tile) noexcept { _queue.remove(tile); } - /** @brief Returns true when `tile` is currently in the candidate list. - * [main-thread] */ + /** @brief Returns true when `tile` is currently in the candidate list. */ bool contains(const Tile& tile) const noexcept { return _queue.contains(tile); } /** - * @brief Directly removes `tile` from the list. - * Prefer markIneligible; this exists for sites that call remove() - * unconditionally. [main-thread] + * @brief Removes `tile` from the list. */ void remove(Tile& tile) noexcept { _queue.remove(tile); } - /** @brief Returns the least-recently-used tile, or nullptr. [main-thread] */ + /** @brief Returns the least-recently-used tile, or nullptr. */ Tile* head() noexcept { return _queue.head(); } - /** @brief Returns the tile following `tile` in LRU order. [main-thread] */ + /** @brief Returns the tile following `tile` in LRU order. */ Tile* next(Tile& tile) noexcept { return _queue.next(tile); } private: diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h index 1ceb3bf403..95563fc60e 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h @@ -22,6 +22,16 @@ struct TileSelectionContext { const TilesetOptions& options; /** @brief The external interfaces (asset accessor, task processor, etc.). */ const TilesetExternals& externals; + /** + * @brief Caller-owned scratch buffer reused across frames to avoid + * per-frame heap allocation. Contents on entry are unspecified. + */ + std::vector& scratchDistances; + /** + * @brief Caller-owned scratch buffer reused across frames to avoid + * per-frame heap allocation. Contents on entry are unspecified. + */ + std::vector& scratchOcclusionProxies; }; /** @@ -33,22 +43,17 @@ struct TileSelectionContext { * Side effects are limited to explicit reference parameters: * - `frameState.viewGroup` receives traversal state updates and load queue * entries. - * - `scratchDistances` and `scratchOcclusionProxies` are reused across - * frames to avoid per-frame heap allocation; their contents on entry are - * unspecified. + * - `ctx.scratchDistances` and `ctx.scratchOcclusionProxies` are reused + * across frames to avoid per-frame heap allocation. * - * @param ctx Configuration and external dependencies. + * @param ctx Configuration, external dependencies, and scratch buffers. * @param frameState Per-frame view parameters and the view group to update. * @param rootTile Root of the tile hierarchy to traverse. - * @param scratchDistances Caller-owned scratch buffer. - * @param scratchOcclusionProxies Caller-owned scratch buffer. * @return The frame's render result and statistics. */ CESIUM3DTILESSELECTION_API ViewUpdateResult selectTiles( const TileSelectionContext& ctx, const TilesetFrameState& frameState, - Tile& rootTile, - std::vector& scratchDistances, - std::vector& scratchOcclusionProxies); + Tile& rootTile); } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/Tile.cpp b/Cesium3DTilesSelection/src/Tile.cpp index 664b590ee0..36a05b34b3 100644 --- a/Cesium3DTilesSelection/src/Tile.cpp +++ b/Cesium3DTilesSelection/src/Tile.cpp @@ -239,22 +239,22 @@ bool Tile::isRenderable() const noexcept { } bool Tile::isRenderContent() const noexcept { - return this->_content.get().isRenderContent(); + return this->_content.isRenderContent(); } bool Tile::isExternalContent() const noexcept { - return this->_content.get().isExternalContent(); + return this->_content.isExternalContent(); } bool Tile::isEmptyContent() const noexcept { - return this->_content.get().isEmptyContent(); + return this->_content.isEmptyContent(); } TilesetContentLoader* Tile::getLoader() const noexcept { return this->_pLoader; } -TileLoadState Tile::getState() const noexcept { return this->_loadState.get(); } +TileLoadState Tile::getState() const noexcept { return this->_loadState; } namespace { @@ -315,7 +315,7 @@ void Tile::setParent(Tile* pParent) noexcept { } } - this->_pParent.reset(pParent); + this->_pParent = pParent; if (this->getReferenceCount() > 0) { // Add a reference to the new parent, or to the @@ -328,9 +328,7 @@ void Tile::setParent(Tile* pParent) noexcept { } } -void Tile::setState(TileLoadState state) noexcept { - this->_loadState.get() = state; -} +void Tile::setState(TileLoadState state) noexcept { this->_loadState = state; } bool Tile::getMightHaveLatentChildren() const noexcept { return this->_mightHaveLatentChildren; @@ -478,7 +476,7 @@ int32_t Tile::getReferenceCount() const noexcept { } bool Tile::hasReferencingContent() const noexcept { - return !this->_content.get().isUnknownContent() && + return !this->_content.isUnknownContent() && TileIdUtilities::isLoadable(this->_id); } diff --git a/Cesium3DTilesSelection/src/TileOverlaySystem.cpp b/Cesium3DTilesSelection/src/TileOverlaySystem.cpp deleted file mode 100644 index b6013cdbc5..0000000000 --- a/Cesium3DTilesSelection/src/TileOverlaySystem.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "TileOverlaySystem.h" - -#include -#include -#include - -namespace Cesium3DTilesSelection { - -TileOverlaySystem::TileOverlaySystem( - const LoadedTileEnumerator& loadedTiles, - const TilesetExternals& externals, - const CesiumGeospatial::Ellipsoid& ellipsoid) noexcept - : _collection(loadedTiles, externals, ellipsoid), _upsampler() {} - -} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TileOverlaySystem.h b/Cesium3DTilesSelection/src/TileOverlaySystem.h deleted file mode 100644 index 9f1f54a82c..0000000000 --- a/Cesium3DTilesSelection/src/TileOverlaySystem.h +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -// TileOverlaySystem.h lives in src/ (internal) because RasterOverlayUpsampler -// is also internal. If RasterOverlayUpsampler is ever promoted to a public -// header this file can move to include/. - -#include "RasterOverlayUpsampler.h" - -#include -#include -#include -#include - -namespace Cesium3DTilesSelection { - -/** - * @brief Owns the raster overlay collection and the upsampler. - * - * This is the single point of ownership for all overlay-related resources. - * In the current design tiles hold references into data owned here; Phase 4 - * will make those non-owning references explicit at the type level. - * - * The upsampler is a `TilesetContentLoader` specialisation whose `setOwner()` - * must be called by the containing `TilesetContentManager` after construction, - * because the owner back-reference is to the manager, not the system. - * - * [main-thread-only] - */ -class TileOverlaySystem { -public: - TileOverlaySystem( - const LoadedTileEnumerator& loadedTiles, - const TilesetExternals& externals, - const CesiumGeospatial::Ellipsoid& ellipsoid) noexcept; - - ~TileOverlaySystem() noexcept = default; - - TileOverlaySystem(const TileOverlaySystem&) = delete; - TileOverlaySystem& operator=(const TileOverlaySystem&) = delete; - TileOverlaySystem(TileOverlaySystem&&) noexcept = default; - TileOverlaySystem& operator=(TileOverlaySystem&&) noexcept = default; - - /** @brief The raster overlay collection. [main-thread] */ - const RasterOverlayCollection& getCollection() const noexcept { - return _collection; - } - RasterOverlayCollection& getCollection() noexcept { return _collection; } - - /** - * @brief The quadtree-upsampling loader. - * Caller is responsible for calling `setOwner()` after construction. - * [main-thread] - */ - const RasterOverlayUpsampler& getUpsampler() const noexcept { - return _upsampler; - } - RasterOverlayUpsampler& getUpsampler() noexcept { return _upsampler; } - -private: - RasterOverlayCollection _collection; - RasterOverlayUpsampler _upsampler; -}; - -} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index c4a3a71006..10a6333b56 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -423,13 +423,12 @@ const ViewUpdateResult& Tileset::updateViewGroup( if (!frustums.empty()) { viewGroup.startNewFrame(*this, frameState); - TileSelectionContext ctx{this->_options, this->_externals}; - ViewUpdateResult selectionResult = selectTiles( - ctx, - frameState, - *pRootTile, + TileSelectionContext ctx{ + this->_options, + this->_externals, this->_distances, - this->_childOcclusionProxies); + this->_childOcclusionProxies}; + ViewUpdateResult selectionResult = selectTiles(ctx, frameState, *pRootTile); result = std::move(selectionResult); viewGroup.finishFrame(*this, frameState); } else { diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 622e88f6e9..7bfae38047 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -656,12 +656,14 @@ TilesetContentManager::TilesetContentManager( : _externals{externals}, _requestHeaders{tilesetOptions.requestHeaders}, _pLoader{nullptr}, + _pRootTile{nullptr}, _userCredit(), _tilesetCredits{}, - _overlays( + _rasterOverlayCollection( LoadedTileEnumerator(nullptr), externals, tilesetOptions.ellipsoid), + _upsampler(), _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, @@ -684,7 +686,7 @@ TilesetContentManager::TilesetContentManager( externals.pCreditSystem, this->_creditSource); - this->_overlays.getUpsampler().setOwner(*this); + this->_upsampler.setOwner(*this); } /* static */ CesiumUtility::IntrusivePointer @@ -1147,7 +1149,7 @@ void TilesetContentManager::loadTileContent( Tile& tile, const TilesetOptions& tilesetOptions) { CESIUM_ASSERT(this->_pLoader != nullptr); - CESIUM_ASSERT(this->_hierarchy.getRoot() != nullptr); + CESIUM_ASSERT(this->_pRootTile != nullptr); CESIUM_TRACE("TilesetContentManager::loadTileContent"); @@ -1217,7 +1219,7 @@ void TilesetContentManager::loadTileContent( // map raster overlay to tile std::vector projections = - this->_overlays.getCollection().addTileOverlays(tile, tilesetOptions); + this->_rasterOverlayCollection.addTileOverlays(tile, tilesetOptions); // begin loading tile notifyTileStartLoading(&tile); @@ -1233,8 +1235,8 @@ void TilesetContentManager::loadTileContent( tile}; TilesetContentLoader* pLoader; - if (tile.getLoader() == &this->_overlays.getUpsampler()) { - pLoader = &this->_overlays.getUpsampler(); + if (tile.getLoader() == &this->_upsampler) { + pLoader = &this->_upsampler; } else { pLoader = this->_pLoader.get(); } @@ -1475,8 +1477,8 @@ UnloadTileContentResult TilesetContentManager::unloadTileContent(Tile& tile) { void TilesetContentManager::unloadAll() { // TODO: use the linked-list of loaded tiles instead of walking the entire // tile tree. - if (this->_hierarchy.getRoot()) { - unloadTileRecursively(*this->_hierarchy.getRoot(), *this); + if (this->_pRootTile) { + unloadTileRecursively(*this->_pRootTile, *this); } } @@ -1509,7 +1511,7 @@ bool TilesetContentManager::waitUntilIdle( rasterOverlayTilesLoading = 0; for (const auto& pActivated : - this->_overlays.getCollection().getActivatedOverlays()) { + this->_rasterOverlayCollection.getActivatedOverlays()) { rasterOverlayTilesLoading += pActivated->getNumberOfTilesLoading(); } @@ -1523,11 +1525,11 @@ bool TilesetContentManager::waitUntilIdle( } const Tile* TilesetContentManager::getRootTile() const noexcept { - return this->_hierarchy.getRoot(); + return this->_pRootTile.get(); } Tile* TilesetContentManager::getRootTile() noexcept { - return this->_hierarchy.getRoot(); + return this->_pRootTile.get(); } const std::vector& @@ -1542,12 +1544,12 @@ TilesetContentManager::getRequestHeaders() noexcept { const RasterOverlayCollection& TilesetContentManager::getRasterOverlayCollection() const noexcept { - return this->_overlays.getCollection(); + return this->_rasterOverlayCollection; } RasterOverlayCollection& TilesetContentManager::getRasterOverlayCollection() noexcept { - return this->_overlays.getCollection(); + return this->_rasterOverlayCollection; } const Credit* TilesetContentManager::getUserCredit() const noexcept { @@ -1579,7 +1581,7 @@ int32_t TilesetContentManager::getNumberOfTilesLoaded() const noexcept { int64_t TilesetContentManager::getTotalDataUsed() const noexcept { int64_t bytes = this->_tilesDataUsed; for (const auto& pActivated : - this->_overlays.getCollection().getActivatedOverlays()) { + this->_rasterOverlayCollection.getActivatedOverlays()) { bytes += pActivated->getTileDataBytes(); } @@ -2120,9 +2122,7 @@ void TilesetContentManager::updateDoneState( const TileRenderContent* pRenderContent = content.getRenderContent(); if (pRenderContent) { TileRasterOverlayStatus status = - this->_overlays.getCollection().updateTileOverlays( - tile, - tilesetOptions); + this->_rasterOverlayCollection.updateTileOverlays(tile, tilesetOptions); if (status.firstIndexWithMissingProjection) { // The mesh doesn't have the right texture coordinates for this @@ -2147,10 +2147,7 @@ void TilesetContentManager::updateDoneState( // have raster tiles that are not the most detailed available, create fake // children to hang more detailed rasters on by subdividing this tile. if (doSubdivide && tile.getChildren().empty()) { - createQuadtreeSubdividedChildren( - ellipsoid, - tile, - this->_overlays.getUpsampler()); + createQuadtreeSubdividedChildren(ellipsoid, tile, this->_upsampler); } } else { // We can't hang raster images on a tile without geometry, and their @@ -2255,10 +2252,10 @@ void TilesetContentManager::propagateTilesetContentLoaderResult( this->_requestHeaders = std::move(result.requestHeaders); this->_pLoader = std::move(result.pLoader); - this->_hierarchy.setRoot(std::move(result.pRootTile)); + this->_pRootTile = std::move(result.pRootTile); - this->_overlays.getCollection().setLoadedTileEnumerator( - LoadedTileEnumerator(this->_hierarchy.getRoot())); + this->_rasterOverlayCollection.setLoadedTileEnumerator( + LoadedTileEnumerator(this->_pRootTile.get())); this->_pLoader->setOwner(*this); } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index 1c10bf89c0..6ac2cc0d9d 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -1,13 +1,11 @@ #pragma once #include "RasterOverlayUpsampler.h" -#include "TileOverlaySystem.h" #include #include #include #include -#include #include #include #include @@ -233,13 +231,11 @@ class TilesetContentManager TilesetExternals _externals; std::vector _requestHeaders; std::unique_ptr _pLoader; - /// @brief Owns the tile tree root. Single owner; children are owned by their - /// parent tile. Access on main thread only. - TileHierarchy _hierarchy; + std::unique_ptr _pRootTile; std::optional _userCredit; std::vector _tilesetCredits; - /// @brief Owns the raster overlay collection and upsampler. - TileOverlaySystem _overlays; + RasterOverlayCollection _rasterOverlayCollection; + RasterOverlayUpsampler _upsampler; int32_t _tileLoadsInProgress; int32_t _loadedTilesCount; int64_t _tilesDataUsed; diff --git a/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp b/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp index 3a9b67b230..84294f6eb7 100644 --- a/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp @@ -19,7 +19,7 @@ void MockTilesetContentManagerTestFixture::setTileShouldContinueUpdating( void MockTilesetContentManagerTestFixture::setTileContent( Cesium3DTilesSelection::Tile& tile, Cesium3DTilesSelection::TileContent&& content) { - tile._content.get() = std::move(content); + tile._content = std::move(content); }; } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/test/TestTilesetSelection.cpp b/Cesium3DTilesSelection/test/TestTilesetSelection.cpp index a09a513fff..93974727fd 100644 --- a/Cesium3DTilesSelection/test/TestTilesetSelection.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetSelection.cpp @@ -139,11 +139,14 @@ TEST_CASE("selectTiles is callable as a free function") { std::vector scratchDistances; std::vector scratchOcclusion; - TileSelectionContext ctx{options, externals}; + TileSelectionContext ctx{ + options, + externals, + scratchDistances, + scratchOcclusion}; viewGroup.startNewFrame(*pTileset, frameState); - ViewUpdateResult result = - selectTiles(ctx, frameState, *pRoot, scratchDistances, scratchOcclusion); + ViewUpdateResult result = selectTiles(ctx, frameState, *pRoot); viewGroup.finishFrame(*pTileset, frameState); // From far away the root tile (or its immediate children) should be @@ -200,11 +203,14 @@ TEST_CASE("selectTiles result matches updateViewGroup result") { std::vector scratchDistances; std::vector scratchOcclusion; - TileSelectionContext ctx{options, externals}; + TileSelectionContext ctx{ + options, + externals, + scratchDistances, + scratchOcclusion}; viewGroup.startNewFrame(*pTileset, frameState); - ViewUpdateResult freeResult = - selectTiles(ctx, frameState, *pRoot, scratchDistances, scratchOcclusion); + ViewUpdateResult freeResult = selectTiles(ctx, frameState, *pRoot); viewGroup.finishFrame(*pTileset, frameState); // Tile counts must agree between the two paths. From d51f93004e0fe0fa9c576305e53efcd8b2b32f8f Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Mon, 13 Apr 2026 07:15:19 -0700 Subject: [PATCH 16/28] remove [main-thread-only] comment --- .../include/Cesium3DTilesSelection/TilesetFrameState.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h index 914bd1e458..1698f50617 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h @@ -33,7 +33,7 @@ class TilesetFrameState { std::vector fogDensities; /** - * @brief [main-thread] Callback invoked once per visited tile to advance its + * @brief Callback invoked once per visited tile to advance its * content state machine (unloading, content-loaded finalization, latent * children creation). Populated by Tileset::updateViewGroup before the * traversal begins. From 1a9ddf5f4c0e3b3007634528cc4e35d34a02954e Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Mon, 13 Apr 2026 07:17:48 -0700 Subject: [PATCH 17/28] revert _overlayCollection rename --- .../src/TilesetContentManager.cpp | 16 ++++++++-------- .../src/TilesetContentManager.h | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 7bfae38047..0f0ce71c2a 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -659,7 +659,7 @@ TilesetContentManager::TilesetContentManager( _pRootTile{nullptr}, _userCredit(), _tilesetCredits{}, - _rasterOverlayCollection( + _overlayCollection( LoadedTileEnumerator(nullptr), externals, tilesetOptions.ellipsoid), @@ -1219,7 +1219,7 @@ void TilesetContentManager::loadTileContent( // map raster overlay to tile std::vector projections = - this->_rasterOverlayCollection.addTileOverlays(tile, tilesetOptions); + this->_overlayCollection.addTileOverlays(tile, tilesetOptions); // begin loading tile notifyTileStartLoading(&tile); @@ -1511,7 +1511,7 @@ bool TilesetContentManager::waitUntilIdle( rasterOverlayTilesLoading = 0; for (const auto& pActivated : - this->_rasterOverlayCollection.getActivatedOverlays()) { + this->_overlayCollection.getActivatedOverlays()) { rasterOverlayTilesLoading += pActivated->getNumberOfTilesLoading(); } @@ -1544,12 +1544,12 @@ TilesetContentManager::getRequestHeaders() noexcept { const RasterOverlayCollection& TilesetContentManager::getRasterOverlayCollection() const noexcept { - return this->_rasterOverlayCollection; + return this->_overlayCollection; } RasterOverlayCollection& TilesetContentManager::getRasterOverlayCollection() noexcept { - return this->_rasterOverlayCollection; + return this->_overlayCollection; } const Credit* TilesetContentManager::getUserCredit() const noexcept { @@ -1581,7 +1581,7 @@ int32_t TilesetContentManager::getNumberOfTilesLoaded() const noexcept { int64_t TilesetContentManager::getTotalDataUsed() const noexcept { int64_t bytes = this->_tilesDataUsed; for (const auto& pActivated : - this->_rasterOverlayCollection.getActivatedOverlays()) { + this->_overlayCollection.getActivatedOverlays()) { bytes += pActivated->getTileDataBytes(); } @@ -2122,7 +2122,7 @@ void TilesetContentManager::updateDoneState( const TileRenderContent* pRenderContent = content.getRenderContent(); if (pRenderContent) { TileRasterOverlayStatus status = - this->_rasterOverlayCollection.updateTileOverlays(tile, tilesetOptions); + this->_overlayCollection.updateTileOverlays(tile, tilesetOptions); if (status.firstIndexWithMissingProjection) { // The mesh doesn't have the right texture coordinates for this @@ -2254,7 +2254,7 @@ void TilesetContentManager::propagateTilesetContentLoaderResult( this->_pLoader = std::move(result.pLoader); this->_pRootTile = std::move(result.pRootTile); - this->_rasterOverlayCollection.setLoadedTileEnumerator( + this->_overlayCollection.setLoadedTileEnumerator( LoadedTileEnumerator(this->_pRootTile.get())); this->_pLoader->setOwner(*this); } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index 6ac2cc0d9d..0c7a1159fd 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -234,7 +234,7 @@ class TilesetContentManager std::unique_ptr _pRootTile; std::optional _userCredit; std::vector _tilesetCredits; - RasterOverlayCollection _rasterOverlayCollection; + RasterOverlayCollection _overlayCollection; RasterOverlayUpsampler _upsampler; int32_t _tileLoadsInProgress; int32_t _loadedTilesCount; From 792878b3e51edbc67aefca2e21dec265de69bbc5 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Fri, 24 Apr 2026 14:50:36 -0500 Subject: [PATCH 18/28] fix comments --- .../Cesium3DTilesSelection/TileUnloadQueue.h | 24 ++++--------- .../TilesetFrameState.h | 12 ++----- .../Cesium3DTilesSelection/TilesetSelection.h | 34 ++++++------------- 3 files changed, 20 insertions(+), 50 deletions(-) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h index fd4bf801da..2a4c405b94 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h @@ -5,12 +5,9 @@ namespace Cesium3DTilesSelection { /** - * @brief Tracks tiles whose content may be evicted when memory is low. + * @brief LRU list of tiles eligible for content eviction. * - * Tiles at the head are least-recently-used; tiles at the tail are - * most-recently-used. - * - * Manages a LRU linked-list of non-owning Tile references. + * Head is least-recently-used, tail is most-recently-used. */ class TileUnloadQueue { public: @@ -19,14 +16,12 @@ class TileUnloadQueue { TileUnloadQueue(const TileUnloadQueue&) = delete; TileUnloadQueue& operator=(const TileUnloadQueue&) = delete; - /** @brief Move constructor. */ TileUnloadQueue(TileUnloadQueue&&) noexcept = default; - /** @brief Move assignment. */ TileUnloadQueue& operator=(TileUnloadQueue&&) noexcept = default; /** - * @brief Adds `tile` to the tail (most-recently-used end) of the queue. - * No-op if `tile` is already tracked. + * @brief Adds `tile` to the tail (most-recently-used end). No-op if already + * tracked. */ void markEligible(Tile& tile) noexcept { if (!_queue.contains(tile)) { @@ -34,21 +29,14 @@ class TileUnloadQueue { } } - /** - * @brief Removes `tile` from the queue. No-op if not present. - */ + /** @brief Removes `tile` from the queue. No-op if not present. */ void markIneligible(Tile& tile) noexcept { _queue.remove(tile); } - /** @brief Returns true when `tile` is currently in the candidate list. */ + /** @brief Returns true if `tile` is currently in the queue. */ bool contains(const Tile& tile) const noexcept { return _queue.contains(tile); } - /** - * @brief Removes `tile` from the list. - */ - void remove(Tile& tile) noexcept { _queue.remove(tile); } - /** @brief Returns the least-recently-used tile, or nullptr. */ Tile* head() noexcept { return _queue.head(); } diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h index 1698f50617..a7f699ce82 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h @@ -33,15 +33,9 @@ class TilesetFrameState { std::vector fogDensities; /** - * @brief Callback invoked once per visited tile to advance its - * content state machine (unloading, content-loaded finalization, latent - * children creation). Populated by Tileset::updateViewGroup before the - * traversal begins. - * - * Keeping this as a callback rather than a direct TilesetContentManager - * reference means the traversal functions (_visitTileIfNeeded, _visitTile, - * etc.) have no compile-time dependency on TilesetContentManager, making - * them independently testable. + * @brief Called once per visited tile to advance its content state (loading, + * finalization, child creation). Set by Tileset::updateViewGroup; may be + * null when testing selectTiles directly. */ std::function tileStateUpdater; }; diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h index 95563fc60e..3f37791786 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h @@ -14,46 +14,34 @@ struct TilesetOptions; class TileOcclusionRendererProxy; /** - * @brief Bundles the immutable per-frame inputs to the tile selection - * algorithm. Holds only references — no ownership. + * @brief Per-frame inputs for the tile selection algorithm. */ struct TileSelectionContext { /** @brief The tileset configuration options. */ const TilesetOptions& options; - /** @brief The external interfaces (asset accessor, task processor, etc.). */ + /** @brief External interfaces (asset accessor, task processor, etc.). */ const TilesetExternals& externals; - /** - * @brief Caller-owned scratch buffer reused across frames to avoid - * per-frame heap allocation. Contents on entry are unspecified. - */ + /** @brief Scratch buffer reused across frames. Contents on entry are unspecified. */ std::vector& scratchDistances; - /** - * @brief Caller-owned scratch buffer reused across frames to avoid - * per-frame heap allocation. Contents on entry are unspecified. - */ + /** @brief Scratch buffer reused across frames. Contents on entry are unspecified. */ std::vector& scratchOcclusionProxies; }; /** * @brief Runs the tile selection / LOD traversal algorithm. * - * This is a free function with no dependency on `Tileset`, enabling - * standalone unit testing against an in-memory tile tree. + * Populates `result` with the tiles to render this frame. The view group in + * `frameState` also receives load queue updates. * - * Side effects are limited to explicit reference parameters: - * - `frameState.viewGroup` receives traversal state updates and load queue - * entries. - * - `ctx.scratchDistances` and `ctx.scratchOcclusionProxies` are reused - * across frames to avoid per-frame heap allocation. - * - * @param ctx Configuration, external dependencies, and scratch buffers. + * @param ctx Configuration, external dependencies, and scratch buffers. * @param frameState Per-frame view parameters and the view group to update. * @param rootTile Root of the tile hierarchy to traverse. - * @return The frame's render result and statistics. + * @param result Filled with the selected tiles and statistics for this frame. */ -CESIUM3DTILESSELECTION_API ViewUpdateResult selectTiles( +CESIUM3DTILESSELECTION_API void selectTiles( const TileSelectionContext& ctx, const TilesetFrameState& frameState, - Tile& rootTile); + Tile& rootTile, + ViewUpdateResult& result); } // namespace Cesium3DTilesSelection From e33a898dc628974630eddf5850cb06c5cb3ded60 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Fri, 24 Apr 2026 14:51:05 -0500 Subject: [PATCH 19/28] update markIneligble call --- Cesium3DTilesSelection/src/TilesetContentManager.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 0f0ce71c2a..52ed00bc3d 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -1708,7 +1708,7 @@ void TilesetContentManager::unloadCachedBytes( const UnloadTileContentResult removed = this->unloadTileContent(*pTile); if (removed != UnloadTileContentResult::Keep) { - this->_unloadQueue.remove(*pTile); + this->_unloadQueue.markIneligible(*pTile); } if (removed == UnloadTileContentResult::RemoveAndClearChildren) { @@ -1732,15 +1732,14 @@ void TilesetContentManager::unloadCachedBytes( } void TilesetContentManager::clearChildrenRecursively(Tile* pTile) noexcept { - // Iterate through all children, calling this method recursively to make sure - // children are all removed from _tilesEligibleForContentUnloading. + // Recursively remove all children from the unload queue before clearing them. for (Tile& child : pTile->getChildren()) { CESIUM_ASSERT( !TileIdUtilities::isLoadable(child.getTileID()) || child.getState() == TileLoadState::Unloaded); CESIUM_ASSERT(child.getReferenceCount() == 0); CESIUM_ASSERT(!child.hasReferencingContent()); - this->_unloadQueue.remove(child); + this->_unloadQueue.markIneligible(child); this->clearChildrenRecursively(&child); child.setParent(nullptr); } From 9eaca5cd67c4b59cc3e37f55c6eab0246d5ee682 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Fri, 24 Apr 2026 14:51:37 -0500 Subject: [PATCH 20/28] pass ViewUpdateResult instead of recreating in selectTiles --- Cesium3DTilesSelection/src/Tileset.cpp | 3 +-- .../src/TilesetSelection.cpp | 10 +++------- .../test/TestTilesetSelection.cpp | 18 +++++++----------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index 10a6333b56..e8050118a0 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -428,8 +428,7 @@ const ViewUpdateResult& Tileset::updateViewGroup( this->_externals, this->_distances, this->_childOcclusionProxies}; - ViewUpdateResult selectionResult = selectTiles(ctx, frameState, *pRootTile); - result = std::move(selectionResult); + selectTiles(ctx, frameState, *pRootTile, result); viewGroup.finishFrame(*this, frameState); } else { result = ViewUpdateResult(); diff --git a/Cesium3DTilesSelection/src/TilesetSelection.cpp b/Cesium3DTilesSelection/src/TilesetSelection.cpp index bbd84a5e0b..c4ab97393a 100644 --- a/Cesium3DTilesSelection/src/TilesetSelection.cpp +++ b/Cesium3DTilesSelection/src/TilesetSelection.cpp @@ -67,7 +67,6 @@ struct CullResult { enum class VisitTileAction { Render, Refine }; -// Forward declaration of the top-level recursive entry point. TraversalDetails visitTileIfNeeded( const TraversalContext& ctx, uint32_t depth, @@ -77,10 +76,11 @@ TraversalDetails visitTileIfNeeded( } // namespace -ViewUpdateResult selectTiles( +void selectTiles( const TileSelectionContext& ctx, const TilesetFrameState& frameState, - Tile& rootTile) { + Tile& rootTile, + ViewUpdateResult& result) { TraversalContext tctx{ ctx.options, ctx.externals, @@ -88,9 +88,7 @@ ViewUpdateResult selectTiles( ctx.scratchDistances, ctx.scratchOcclusionProxies}; - ViewUpdateResult result; visitTileIfNeeded(tctx, 0, false, rootTile, result); - return result; } namespace { @@ -281,7 +279,6 @@ bool mustContinueRefiningToDeeperTiles( !tile.isRenderable(); } -// Forward declarations of recursive traversal helpers TraversalDetails visitTileIfNeeded( const TraversalContext& ctx, uint32_t depth, @@ -786,7 +783,6 @@ TraversalDetails visitTile( return traversalDetails; } -// visitTileIfNeeded (the outermost recursive entry point) TraversalDetails visitTileIfNeeded( const TraversalContext& ctx, uint32_t depth, diff --git a/Cesium3DTilesSelection/test/TestTilesetSelection.cpp b/Cesium3DTilesSelection/test/TestTilesetSelection.cpp index 93974727fd..e81ca0398d 100644 --- a/Cesium3DTilesSelection/test/TestTilesetSelection.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetSelection.cpp @@ -1,8 +1,4 @@ -// TestTilesetSelection.cpp -// -// Unit tests for the selectTiles() free function. These tests construct -// minimal in-memory tile trees and invoke the selection algorithm directly, -// without file I/O or async machinery beyond what TilesetViewGroup requires. +// Unit tests for the selectTiles() free function. #include "SimplePrepareRendererResource.h" @@ -103,8 +99,7 @@ ViewState makeFarViewState() { TEST_CASE("selectTiles is callable as a free function") { // Verify that selectTiles() can be invoked directly without going through - // Tileset::updateViewGroup. This is the isolation guarantee introduced by - // the Phase B refactoring. + // Tileset::updateViewGroup. TilesetExternals externals = makeExternals(); TilesetOptions options; @@ -146,7 +141,8 @@ TEST_CASE("selectTiles is callable as a free function") { scratchOcclusion}; viewGroup.startNewFrame(*pTileset, frameState); - ViewUpdateResult result = selectTiles(ctx, frameState, *pRoot); + ViewUpdateResult result; + selectTiles(ctx, frameState, *pRoot, result); viewGroup.finishFrame(*pTileset, frameState); // From far away the root tile (or its immediate children) should be @@ -196,8 +192,7 @@ TEST_CASE("selectTiles result matches updateViewGroup result") { viewGroup, frustums, std::move(fogDensities), - // Mirror what updateViewGroup does — call updateTileContent via the - // tileStateUpdater so tile states advance identically. + // No tileStateUpdater needed — tiles are already loaded. {}}; std::vector scratchDistances; @@ -210,7 +205,8 @@ TEST_CASE("selectTiles result matches updateViewGroup result") { scratchOcclusion}; viewGroup.startNewFrame(*pTileset, frameState); - ViewUpdateResult freeResult = selectTiles(ctx, frameState, *pRoot); + ViewUpdateResult freeResult; + selectTiles(ctx, frameState, *pRoot, freeResult); viewGroup.finishFrame(*pTileset, frameState); // Tile counts must agree between the two paths. From d7a8b8927df0c416dc1b00890da9c6f5a406b213 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Fri, 24 Apr 2026 14:53:00 -0500 Subject: [PATCH 21/28] format --- .../include/Cesium3DTilesSelection/TilesetSelection.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h index 3f37791786..c77b3ad323 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h @@ -21,9 +21,11 @@ struct TileSelectionContext { const TilesetOptions& options; /** @brief External interfaces (asset accessor, task processor, etc.). */ const TilesetExternals& externals; - /** @brief Scratch buffer reused across frames. Contents on entry are unspecified. */ + /** @brief Scratch buffer reused across frames. Contents on entry are + * unspecified. */ std::vector& scratchDistances; - /** @brief Scratch buffer reused across frames. Contents on entry are unspecified. */ + /** @brief Scratch buffer reused across frames. Contents on entry are + * unspecified. */ std::vector& scratchOcclusionProxies; }; From 7d1de8eb6c16c0c5263e5f77a82532cfca598e7d Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Fri, 24 Apr 2026 14:55:50 -0500 Subject: [PATCH 22/28] fix const cast --- Cesium3DTilesSelection/src/TilesetSelection.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Cesium3DTilesSelection/src/TilesetSelection.cpp b/Cesium3DTilesSelection/src/TilesetSelection.cpp index c4ab97393a..9f5c783eec 100644 --- a/Cesium3DTilesSelection/src/TilesetSelection.cpp +++ b/Cesium3DTilesSelection/src/TilesetSelection.cpp @@ -419,9 +419,8 @@ checkOcclusion(const TraversalContext& ctx, const Tile& tile) { } } - auto& childOcclusionProxies = - const_cast&>( - ctx.childOcclusionProxies); + std::vector& childOcclusionProxies = + ctx.childOcclusionProxies; childOcclusionProxies.clear(); childOcclusionProxies.reserve(tile.getChildren().size()); for (const Tile& child : tile.getChildren()) { From 3e6c4ebb28db2a7f4717649ce84f24706d523d79 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Fri, 24 Apr 2026 14:56:16 -0500 Subject: [PATCH 23/28] fix orphaned comment --- .../include/Cesium3DTilesSelection/Tileset.h | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index 5a87758736..4f91d43395 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -469,23 +469,6 @@ class CESIUM3DTILESSELECTION_API Tileset final { Tileset& operator=(const Tileset& rhs) = delete; private: - /** - * @brief When called on an additive-refined tile, queues it for load and adds - * it to the render list. - * - * For replacement-refined tiles, this method does nothing and returns false. - * - * @param tile The tile to potentially load and render. - * @param result The current view update result. - * @param tilePriority The load priority of this tile. - * priority. - * @param tileSse The screen space error of this tile. - * @param queuedForLoad True if this tile has already been queued for loading. - * @return true The additive-refined tile was queued for load and added to the - * render list. - * @return false The non-additive-refined tile was ignored. - */ - void _unloadCachedTiles(double timeBudget) noexcept; void _updateLodTransitions( From 488eb2d7dac293398d2bfce87d8c182e0e7bb239 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Fri, 24 Apr 2026 14:59:18 -0500 Subject: [PATCH 24/28] fix remaining issues --- .../include/Cesium3DTilesSelection/Tileset.h | 6 +++--- Cesium3DTilesSelection/src/TilesetContentManager.h | 2 +- Cesium3DTilesSelection/src/TilesetSelection.cpp | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index 4f91d43395..3e85b90037 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -482,11 +482,11 @@ class CESIUM3DTILESSELECTION_API Tileset final { TilesetOptions _options; // Holds computed distances, to avoid allocating them on the heap during tile - // selection. Passed by reference into selectTiles(). + // selection. std::vector _distances; - // Holds the occlusion proxies of the children of a tile. Passed by reference - // into selectTiles() to avoid per-frame allocation. + // Holds the occlusion proxies of the children of a tile, to avoid + // per-frame allocation. std::vector _childOcclusionProxies; CesiumUtility::IntrusivePointer diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index 0c7a1159fd..31b1f2d2a6 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -250,7 +250,7 @@ class TilesetContentManager CesiumAsync::Promise _rootTileAvailablePromise; CesiumAsync::SharedFuture _rootTileAvailableFuture; - /// @brief Tracks tiles eligible for content eviction (LRU order). + // Tracks tiles eligible for content eviction in LRU order. TileUnloadQueue _unloadQueue; std::vector _requesters; diff --git a/Cesium3DTilesSelection/src/TilesetSelection.cpp b/Cesium3DTilesSelection/src/TilesetSelection.cpp index 9f5c783eec..6b43dbc947 100644 --- a/Cesium3DTilesSelection/src/TilesetSelection.cpp +++ b/Cesium3DTilesSelection/src/TilesetSelection.cpp @@ -43,7 +43,6 @@ using namespace CesiumUtility; namespace Cesium3DTilesSelection { -// Internal types used by selectTiles and traversal helpers. namespace { struct TraversalContext { From 4112199269fc0f78b91ad0a4bff91473b904686c Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Wed, 13 May 2026 09:49:11 -0500 Subject: [PATCH 25/28] remove TraversalContext --- .../src/TilesetSelection.cpp | 280 ++++++++++-------- 1 file changed, 163 insertions(+), 117 deletions(-) diff --git a/Cesium3DTilesSelection/src/TilesetSelection.cpp b/Cesium3DTilesSelection/src/TilesetSelection.cpp index 6b43dbc947..cb6ba5bcdc 100644 --- a/Cesium3DTilesSelection/src/TilesetSelection.cpp +++ b/Cesium3DTilesSelection/src/TilesetSelection.cpp @@ -45,14 +45,16 @@ namespace Cesium3DTilesSelection { namespace { -struct TraversalContext { - const TilesetOptions& options; - const TilesetExternals& externals; - const TilesetFrameState& frameState; - std::vector& distances; - std::vector& childOcclusionProxies; -}; - +/** + * @brief The result of traversing one branch of the tile hierarchy. + * + * Instances of this structure are created by the `_visit...` functions, + * and summarize the information that was gathered during the traversal + * of the respective branch, so that this information can be used by + * the parent to decide on the further traversal process. + * + * @private + */ struct TraversalDetails { bool allAreRenderable = true; bool anyWereRenderedLastFrame = false; @@ -67,7 +69,8 @@ struct CullResult { enum class VisitTileAction { Render, Refine }; TraversalDetails visitTileIfNeeded( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, uint32_t depth, bool ancestorMeetsSse, Tile& tile, @@ -76,18 +79,12 @@ TraversalDetails visitTileIfNeeded( } // namespace void selectTiles( - const TileSelectionContext& ctx, + const TileSelectionContext& context, const TilesetFrameState& frameState, Tile& rootTile, ViewUpdateResult& result) { - TraversalContext tctx{ - ctx.options, - ctx.externals, - frameState, - ctx.scratchDistances, - ctx.scratchOcclusionProxies}; - visitTileIfNeeded(tctx, 0, false, rootTile, result); + visitTileIfNeeded(context, frameState, 0, false, rootTile, result); } namespace { @@ -228,13 +225,14 @@ void computeDistances( } void addTileToLoadQueue( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, Tile& tile, TileLoadPriorityGroup priorityGroup, double priority) { - ctx.frameState.viewGroup.addToLoadQueue( + frameState.viewGroup.addToLoadQueue( TileLoadTask{&tile, priorityGroup, priority}, - ctx.externals.pGltfModifier); + context.externals.pGltfModifier); } void addTileToRender(ViewUpdateResult& result, Tile& tile, double sse) { @@ -242,10 +240,13 @@ void addTileToRender(ViewUpdateResult& result, Tile& tile, double sse) { result.tileScreenSpaceErrorThisFrame.emplace_back(sse); } -double computeSse(const TraversalContext& ctx, const Tile& tile) noexcept { +double computeSse( + const TileSelectionContext& context, + const TilesetFrameState& frameState, + const Tile& tile) noexcept { double largestSse = 0.0; - const auto& frustums = ctx.frameState.frustums; - const auto& distances = ctx.distances; + const auto& frustums = frameState.frustums; + const auto& distances = context.scratchDistances; CESIUM_ASSERT(frustums.size() == distances.size()); for (size_t i = 0; i < frustums.size(); ++i) { const double sse = frustums[i].computeScreenSpaceError( @@ -259,12 +260,13 @@ double computeSse(const TraversalContext& ctx, const Tile& tile) noexcept { } bool meetsSseThreshold( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, double sse, bool culled) noexcept { - return culled ? !ctx.options.enforceCulledScreenSpaceError || - sse < ctx.options.culledScreenSpaceError - : sse < ctx.options.maximumScreenSpaceError; + return culled ? !context.options.enforceCulledScreenSpaceError || + sse < context.options.culledScreenSpaceError + : sse < context.options.maximumScreenSpaceError; } bool isLeaf(const Tile& tile) noexcept { return tile.getChildren().empty(); } @@ -278,15 +280,9 @@ bool mustContinueRefiningToDeeperTiles( !tile.isRenderable(); } -TraversalDetails visitTileIfNeeded( - const TraversalContext& ctx, - uint32_t depth, - bool ancestorMeetsSse, - Tile& tile, - ViewUpdateResult& result); - TraversalDetails visitTile( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, uint32_t depth, bool meetsSse, bool ancestorMeetsSse, @@ -296,7 +292,8 @@ TraversalDetails visitTile( ViewUpdateResult& result); TraversalDetails visitVisibleChildrenNearToFar( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, uint32_t depth, bool ancestorMeetsSse, Tile& tile, @@ -305,7 +302,8 @@ TraversalDetails visitVisibleChildrenNearToFar( // Frustum and fog culling void frustumCull( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, const Tile& tile, bool cullWithChildrenBounds, CullResult& cullResult) { @@ -314,8 +312,8 @@ void frustumCull( return; } - const Ellipsoid& ellipsoid = ctx.options.ellipsoid; - const std::vector& frustums = ctx.frameState.frustums; + const Ellipsoid& ellipsoid = context.options.ellipsoid; + const std::vector& frustums = frameState.frustums; if (cullWithChildrenBounds) { if (std::any_of( @@ -323,8 +321,8 @@ void frustumCull( frustums.end(), [&ellipsoid, children = tile.getChildren(), - renderTilesUnderCamera = - ctx.options.renderTilesUnderCamera](const ViewState& frustum) { + renderTilesUnderCamera = context.options.renderTilesUnderCamera]( + const ViewState& frustum) { for (const Tile& child : children) { if (isVisibleFromCamera( frustum, @@ -343,7 +341,8 @@ void frustumCull( frustums.end(), [&ellipsoid, &boundingVolume = tile.getBoundingVolume(), - renderTilesUnderCamera = ctx.options.renderTilesUnderCamera]( + renderTilesUnderCamera = + context.options.renderTilesUnderCamera]( const ViewState& frustum) { return isVisibleFromCamera( frustum, @@ -355,20 +354,23 @@ void frustumCull( } cullResult.culled = true; - if (ctx.options.enableFrustumCulling) { + if (context.options.enableFrustumCulling) { cullResult.shouldVisit = false; } } -void fogCull(const TraversalContext& ctx, CullResult& cullResult) { +void fogCull( + const TileSelectionContext& context, + const TilesetFrameState& frameState, + CullResult& cullResult) { if (!cullResult.shouldVisit || cullResult.culled) { return; } - const auto& frustums = ctx.frameState.frustums; - const auto& fogDensities = ctx.frameState.fogDensities; - const auto& distances = ctx.distances; + const auto& frustums = frameState.frustums; + const auto& fogDensities = frameState.fogDensities; + const auto& distances = context.scratchDistances; bool isFogCulled = true; for (size_t i = 0; i < frustums.size(); ++i) { @@ -380,16 +382,18 @@ void fogCull(const TraversalContext& ctx, CullResult& cullResult) { if (isFogCulled) { cullResult.culled = true; - if (ctx.options.enableFogCulling) { + if (context.options.enableFogCulling) { cullResult.shouldVisit = false; } } } -TileOcclusionState -checkOcclusion(const TraversalContext& ctx, const Tile& tile) { +TileOcclusionState checkOcclusion( + const TileSelectionContext& context, + const TilesetFrameState& frameState, + const Tile& tile) { const std::shared_ptr& pOcclusionPool = - ctx.externals.pTileOcclusionProxyPool; + context.externals.pTileOcclusionProxyPool; if (!pOcclusionPool) { return TileOcclusionState::NotOccluded; } @@ -419,7 +423,7 @@ checkOcclusion(const TraversalContext& ctx, const Tile& tile) { } std::vector& childOcclusionProxies = - ctx.childOcclusionProxies; + context.scratchOcclusionProxies; childOcclusionProxies.clear(); childOcclusionProxies.reserve(tile.getChildren().size()); for (const Tile& child : tile.getChildren()) { @@ -447,10 +451,11 @@ checkOcclusion(const TraversalContext& ctx, const Tile& tile) { } TraversalDetails createTraversalDetailsForSingleTile( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, const Tile& tile) { TileSelectionState::Result lastFrameResult = - getPreviousState(ctx.frameState.viewGroup, tile).getResult(); + getPreviousState(frameState.viewGroup, tile).getResult(); bool isRenderable = tile.isRenderable(); bool wasRenderedLastFrame = @@ -461,7 +466,7 @@ TraversalDetails createTraversalDetailsForSingleTile( if (tile.getRefine() == TileRefine::Add) { wasRenderedLastFrame = true; } else { - ctx.frameState.viewGroup.getTraversalState().forEachPreviousDescendant( + frameState.viewGroup.getTraversalState().forEachPreviousDescendant( [&](const Tile::Pointer& /* pTile */, const TileSelectionState& state) { if (state.getResult() == TileSelectionState::Result::Rendered) { @@ -479,35 +484,43 @@ TraversalDetails createTraversalDetailsForSingleTile( } TraversalDetails renderLeaf( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, Tile& tile, double tilePriority, double tileSse, ViewUpdateResult& result) { - ctx.frameState.viewGroup.getTraversalState().currentState() = + frameState.viewGroup.getTraversalState().currentState() = TileSelectionState(TileSelectionState::Result::Rendered); addTileToRender(result, tile, tileSse); - addTileToLoadQueue(ctx, tile, TileLoadPriorityGroup::Normal, tilePriority); - return createTraversalDetailsForSingleTile(ctx, tile); + addTileToLoadQueue( + context, + frameState, + tile, + TileLoadPriorityGroup::Normal, + tilePriority); + return createTraversalDetailsForSingleTile(context, frameState, tile); } TraversalDetails renderInnerTile( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, Tile& tile, double tileSse, ViewUpdateResult& result) { addCurrentTileDescendantsToTilesFadingOutIfPreviouslyRendered( - ctx.frameState.viewGroup, + frameState.viewGroup, tile, result); - ctx.frameState.viewGroup.getTraversalState().currentState() = + frameState.viewGroup.getTraversalState().currentState() = TileSelectionState(TileSelectionState::Result::Rendered); addTileToRender(result, tile, tileSse); - return createTraversalDetailsForSingleTile(ctx, tile); + return createTraversalDetailsForSingleTile(context, frameState, tile); } bool loadAndRenderAdditiveRefinedTile( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, Tile& tile, ViewUpdateResult& result, double tilePriority, @@ -517,7 +530,8 @@ bool loadAndRenderAdditiveRefinedTile( addTileToRender(result, tile, tileSse); if (!queuedForLoad) { addTileToLoadQueue( - ctx, + context, + frameState, tile, TileLoadPriorityGroup::Normal, tilePriority); @@ -528,7 +542,8 @@ bool loadAndRenderAdditiveRefinedTile( } bool kickDescendantsAndRenderTile( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, Tile& tile, ViewUpdateResult& result, TraversalDetails& traversalDetails, @@ -539,7 +554,7 @@ bool kickDescendantsAndRenderTile( double tileSse) { TilesetViewGroup::TraversalState& traversalState = - ctx.frameState.viewGroup.getTraversalState(); + frameState.viewGroup.getTraversalState(); traversalState.forEachCurrentDescendant( [](const Tile::Pointer& /*pTile*/, TileSelectionState& selectionState) { @@ -577,7 +592,7 @@ bool kickDescendantsAndRenderTile( TileSelectionState(TileSelectionState::Result::Rendered); TileSelectionState::Result lastFrameSelectionState = - getPreviousState(ctx.frameState.viewGroup, tile).getResult(); + getPreviousState(frameState.viewGroup, tile).getResult(); const bool wasRenderedLastFrame = lastFrameSelectionState == TileSelectionState::Result::Rendered; const bool isRenderable = tile.isRenderable(); @@ -585,16 +600,17 @@ bool kickDescendantsAndRenderTile( if (!wasReallyRenderedLastFrame && traversalDetails.notYetRenderableCount > - ctx.options.loadingDescendantLimit && + context.options.loadingDescendantLimit && !tile.isExternalContent() && !tile.getUnconditionallyRefine()) { result.tilesKicked += static_cast( - ctx.frameState.viewGroup.restoreTileLoadQueueCheckpoint( + frameState.viewGroup.restoreTileLoadQueueCheckpoint( loadQueueBeforeChildren)); if (!queuedForLoad) { addTileToLoadQueue( - ctx, + context, + frameState, tile, TileLoadPriorityGroup::Normal, tilePriority); @@ -610,15 +626,21 @@ bool kickDescendantsAndRenderTile( } TraversalDetails visitVisibleChildrenNearToFar( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, uint32_t depth, bool ancestorMeetsSse, Tile& tile, ViewUpdateResult& result) { TraversalDetails traversalDetails; for (Tile& child : tile.getChildren()) { - const TraversalDetails childTraversal = - visitTileIfNeeded(ctx, depth + 1, ancestorMeetsSse, child, result); + const TraversalDetails childTraversal = visitTileIfNeeded( + context, + frameState, + depth + 1, + ancestorMeetsSse, + child, + result); traversalDetails.allAreRenderable &= childTraversal.allAreRenderable; traversalDetails.anyWereRenderedLastFrame |= childTraversal.anyWereRenderedLastFrame; @@ -629,7 +651,8 @@ TraversalDetails visitVisibleChildrenNearToFar( } TraversalDetails visitTile( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, uint32_t depth, bool meetsSse, bool ancestorMeetsSse, @@ -639,13 +662,13 @@ TraversalDetails visitTile( ViewUpdateResult& result) { TilesetViewGroup::TraversalState& traversalState = - ctx.frameState.viewGroup.getTraversalState(); + frameState.viewGroup.getTraversalState(); ++result.tilesVisited; result.maxDepthVisited = glm::max(result.maxDepthVisited, depth); if (isLeaf(tile)) { - return renderLeaf(ctx, tile, tilePriority, tileSse, result); + return renderLeaf(context, frameState, tile, tilePriority, tileSse, result); } const bool unconditionallyRefine = tile.getUnconditionallyRefine(); @@ -656,7 +679,7 @@ TraversalDetails visitTile( : VisitTileAction::Render; TileSelectionState lastFrameSelectionState = - getPreviousState(ctx.frameState.viewGroup, tile); + getPreviousState(frameState.viewGroup, tile); TileSelectionState::Result lastFrameSelectionResult = lastFrameSelectionState.getResult(); @@ -670,19 +693,20 @@ TraversalDetails visitTile( } }); - bool shouldCheckOcclusion = - ctx.options.enableOcclusionCulling && action == VisitTileAction::Refine && - !unconditionallyRefine && (!tileLastRefined || !childLastRefined); + bool shouldCheckOcclusion = context.options.enableOcclusionCulling && + action == VisitTileAction::Refine && + !unconditionallyRefine && + (!tileLastRefined || !childLastRefined); if (shouldCheckOcclusion) { - TileOcclusionState occlusion = checkOcclusion(ctx, tile); + TileOcclusionState occlusion = checkOcclusion(context, frameState, tile); if (occlusion == TileOcclusionState::Occluded) { ++result.tilesOccluded; action = VisitTileAction::Render; meetsSse = true; } else if ( occlusion == TileOcclusionState::OcclusionUnavailable && - ctx.options.delayRefinementForOcclusion && + context.options.delayRefinementForOcclusion && lastFrameSelectionState.getOriginalResult() != TileSelectionState::Result::Refined) { ++result.tilesWaitingForOcclusionResults; @@ -700,7 +724,8 @@ TraversalDetails visitTile( action = VisitTileAction::Refine; if (!ancestorMeetsSse) { addTileToLoadQueue( - ctx, + context, + frameState, tile, TileLoadPriorityGroup::Urgent, tilePriority); @@ -710,18 +735,20 @@ TraversalDetails visitTile( } else { if (!ancestorMeetsSse) { addTileToLoadQueue( - ctx, + context, + frameState, tile, TileLoadPriorityGroup::Normal, tilePriority); } - return renderInnerTile(ctx, tile, tileSse, result); + return renderInnerTile(context, frameState, tile, tileSse, result); } } // Refine! queuedForLoad = loadAndRenderAdditiveRefinedTile( - ctx, + context, + frameState, tile, result, tilePriority, @@ -732,29 +759,35 @@ TraversalDetails visitTile( const size_t firstRenderedDescendantIndex = result.tilesToRenderThisFrame.size(); TilesetViewGroup::LoadQueueCheckpoint loadQueueBeforeChildren = - ctx.frameState.viewGroup.saveTileLoadQueueCheckpoint(); + frameState.viewGroup.saveTileLoadQueueCheckpoint(); - TraversalDetails traversalDetails = - visitVisibleChildrenNearToFar(ctx, depth, ancestorMeetsSse, tile, result); + TraversalDetails traversalDetails = visitVisibleChildrenNearToFar( + context, + frameState, + depth, + ancestorMeetsSse, + tile, + result); const TileRenderContent* pRenderContent = tile.getContent().getRenderContent(); bool kickDueToNonReadyDescendant = !traversalDetails.allAreRenderable && !traversalDetails.anyWereRenderedLastFrame; bool kickDueToTileFadingIn = - ctx.options.enableLodTransitionPeriod && - ctx.options.kickDescendantsWhileFadingIn && + context.options.enableLodTransitionPeriod && + context.options.kickDescendantsWhileFadingIn && lastFrameSelectionResult == TileSelectionState::Result::Rendered && pRenderContent && pRenderContent->getLodTransitionFadePercentage() < 1.0f; bool wantToKick = kickDueToNonReadyDescendant || kickDueToTileFadingIn; bool willKick = wantToKick && (traversalDetails.notYetRenderableCount > - ctx.options.loadingDescendantLimit || + context.options.loadingDescendantLimit || tile.isRenderable()); if (willKick) { queuedForLoad = kickDescendantsAndRenderTile( - ctx, + context, + frameState, tile, result, traversalDetails, @@ -766,7 +799,7 @@ TraversalDetails visitTile( } else { if (tile.getRefine() != TileRefine::Add) { addCurrentTileToTilesFadingOutIfPreviouslyRendered( - ctx.frameState.viewGroup, + frameState.viewGroup, tile, result); } @@ -774,30 +807,36 @@ TraversalDetails visitTile( TileSelectionState(TileSelectionState::Result::Refined); } - if (ctx.options.preloadAncestors && !queuedForLoad) { - addTileToLoadQueue(ctx, tile, TileLoadPriorityGroup::Preload, tilePriority); + if (context.options.preloadAncestors && !queuedForLoad) { + addTileToLoadQueue( + context, + frameState, + tile, + TileLoadPriorityGroup::Preload, + tilePriority); } return traversalDetails; } TraversalDetails visitTileIfNeeded( - const TraversalContext& ctx, + const TileSelectionContext& context, + const TilesetFrameState& frameState, uint32_t depth, bool ancestorMeetsSse, Tile& tile, ViewUpdateResult& result) { TilesetViewGroup::TraversalState& traversalState = - ctx.frameState.viewGroup.getTraversalState(); + frameState.viewGroup.getTraversalState(); traversalState.beginNode(&tile); - computeDistances(tile, ctx.frameState.frustums, ctx.distances); + computeDistances(tile, frameState.frustums, context.scratchDistances); double tilePriority = - computeTilePriority(tile, ctx.frameState.frustums, ctx.distances); + computeTilePriority(tile, frameState.frustums, context.scratchDistances); - if (ctx.frameState.tileStateUpdater) { - ctx.frameState.tileStateUpdater(tile); + if (frameState.tileStateUpdater) { + frameState.tileStateUpdater(tile); } CullResult cullResult{}; @@ -812,7 +851,7 @@ TraversalDetails visitTileIfNeeded( } for (const std::shared_ptr& pExcluder : - ctx.options.excluders) { + context.options.excluders) { if (pExcluder->shouldExclude(tile)) { cullResult.culled = true; cullResult.shouldVisit = false; @@ -820,11 +859,12 @@ TraversalDetails visitTileIfNeeded( } } - frustumCull(ctx, tile, cullWithChildrenBounds, cullResult); - fogCull(ctx, cullResult); + frustumCull(context, frameState, tile, cullWithChildrenBounds, cullResult); + fogCull(context, frameState, cullResult); if (!cullResult.shouldVisit && tile.getUnconditionallyRefine()) { - if ((ctx.options.forbidHoles && tile.getRefine() == TileRefine::Replace) || + if ((context.options.forbidHoles && + tile.getRefine() == TileRefine::Replace) || tile.getParent() == nullptr) { cullResult.shouldVisit = true; } @@ -832,26 +872,30 @@ TraversalDetails visitTileIfNeeded( if (!cullResult.shouldVisit) { addCurrentTileAndDescendantsToTilesFadingOutIfPreviouslyRendered( - ctx.frameState.viewGroup, + frameState.viewGroup, tile, result); - ctx.frameState.viewGroup.getTraversalState().currentState() = + frameState.viewGroup.getTraversalState().currentState() = TileSelectionState(TileSelectionState::Result::Culled); ++result.tilesCulled; TraversalDetails traversalDetails{}; - if (ctx.options.forbidHoles && tile.getRefine() == TileRefine::Replace) { + if (context.options.forbidHoles && + tile.getRefine() == TileRefine::Replace) { addTileToLoadQueue( - ctx, + context, + frameState, tile, TileLoadPriorityGroup::Normal, tilePriority); - traversalDetails = createTraversalDetailsForSingleTile(ctx, tile); - } else if (ctx.options.preloadSiblings) { + traversalDetails = + createTraversalDetailsForSingleTile(context, frameState, tile); + } else if (context.options.preloadSiblings) { addTileToLoadQueue( - ctx, + context, + frameState, tile, TileLoadPriorityGroup::Preload, tilePriority); @@ -865,11 +909,13 @@ TraversalDetails visitTileIfNeeded( ++result.culledTilesVisited; } - double tileSse = computeSse(ctx, tile); - bool meetsSse = meetsSseThreshold(ctx, tileSse, cullResult.culled); + double tileSse = computeSse(context, frameState, tile); + bool meetsSse = + meetsSseThreshold(context, frameState, tileSse, cullResult.culled); TraversalDetails details = visitTile( - ctx, + context, + frameState, depth, meetsSse, ancestorMeetsSse, From 44aec1530aeb62527c5d5a304d8a40101dba77e9 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Wed, 13 May 2026 10:10:47 -0500 Subject: [PATCH 26/28] add this->_queue --- .../Cesium3DTilesSelection/TileUnloadQueue.h | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h index 2a4c405b94..dcc368b684 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h @@ -8,6 +8,8 @@ namespace Cesium3DTilesSelection { * @brief LRU list of tiles eligible for content eviction. * * Head is least-recently-used, tail is most-recently-used. + * + * @private */ class TileUnloadQueue { public: @@ -24,24 +26,24 @@ class TileUnloadQueue { * tracked. */ void markEligible(Tile& tile) noexcept { - if (!_queue.contains(tile)) { - _queue.insertAtTail(tile); + if (!this->_queue.contains(tile)) { + this->_queue.insertAtTail(tile); } } /** @brief Removes `tile` from the queue. No-op if not present. */ - void markIneligible(Tile& tile) noexcept { _queue.remove(tile); } + void markIneligible(Tile& tile) noexcept { this->_queue.remove(tile); } /** @brief Returns true if `tile` is currently in the queue. */ bool contains(const Tile& tile) const noexcept { - return _queue.contains(tile); + return this->_queue.contains(tile); } /** @brief Returns the least-recently-used tile, or nullptr. */ - Tile* head() noexcept { return _queue.head(); } + Tile* head() noexcept { return this->_queue.head(); } /** @brief Returns the tile following `tile` in LRU order. */ - Tile* next(Tile& tile) noexcept { return _queue.next(tile); } + Tile* next(Tile& tile) noexcept { return this->_queue.next(tile); } private: Tile::UnusedLinkedList _queue; From 2c8f6bf69dfcb119e10dac1ca587063bdac83e66 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Wed, 13 May 2026 10:29:24 -0500 Subject: [PATCH 27/28] fix docs/comments --- .../Cesium3DTilesSelection/TilesetSelection.h | 15 +- .../src/TilesetSelection.cpp | 255 ++++++++++++++++-- 2 files changed, 247 insertions(+), 23 deletions(-) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h index c77b3ad323..cea2cd3650 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h @@ -21,11 +21,12 @@ struct TileSelectionContext { const TilesetOptions& options; /** @brief External interfaces (asset accessor, task processor, etc.). */ const TilesetExternals& externals; - /** @brief Scratch buffer reused across frames. Contents on entry are - * unspecified. */ + /** @brief Scratch buffer reused across frames to avoid allocating them on the + * heap during selection. */ std::vector& scratchDistances; - /** @brief Scratch buffer reused across frames. Contents on entry are - * unspecified. */ + /** @brief Scratch buffer reused across frames to avoid allocating them on the + * heap during selection. Holds the occlusion proxies of the children of a + * tile. */ std::vector& scratchOcclusionProxies; }; @@ -35,13 +36,15 @@ struct TileSelectionContext { * Populates `result` with the tiles to render this frame. The view group in * `frameState` also receives load queue updates. * - * @param ctx Configuration, external dependencies, and scratch buffers. + * @param context Configuration, external dependencies, and scratch buffers. * @param frameState Per-frame view parameters and the view group to update. * @param rootTile Root of the tile hierarchy to traverse. * @param result Filled with the selected tiles and statistics for this frame. + * + * @private */ CESIUM3DTILESSELECTION_API void selectTiles( - const TileSelectionContext& ctx, + const TileSelectionContext& context, const TilesetFrameState& frameState, Tile& rootTile, ViewUpdateResult& result); diff --git a/Cesium3DTilesSelection/src/TilesetSelection.cpp b/Cesium3DTilesSelection/src/TilesetSelection.cpp index cb6ba5bcdc..2cd9f648e5 100644 --- a/Cesium3DTilesSelection/src/TilesetSelection.cpp +++ b/Cesium3DTilesSelection/src/TilesetSelection.cpp @@ -56,18 +56,62 @@ namespace { * @private */ struct TraversalDetails { + /** + * @brief Whether all selected tiles in this tile's subtree are renderable. + * + * This is `true` if all selected (i.e. not culled or refined) tiles in this + * tile's subtree are renderable. If the subtree is renderable, we'll render + * it; no drama. + */ bool allAreRenderable = true; + + /** + * @brief Whether any tile in this tile's subtree was rendered in the last + * frame. + * + * This is `true` if any tiles in this tile's subtree were rendered last + * frame. If any were, we must render the subtree rather than this tile, + * because rendering this tile would cause detail to vanish that was visible + * last frame, and that's no good. + */ bool anyWereRenderedLastFrame = false; + + /** + * @brief The number of selected tiles in this tile's subtree that are not + * yet renderable. + * + * Counts the number of selected tiles in this tile's subtree that are + * not yet ready to be rendered because they need more loading. Note that + * this value will _not_ necessarily be zero when + * `allAreRenderable` is `true`, for subtle reasons. + * When `allAreRenderable` and `anyWereRenderedLastFrame` are both `false`, + * we will render this tile instead of any tiles in its subtree and the + * `allAreRenderable` value for this tile will reflect only whether _this_ + * tile is renderable. The `notYetRenderableCount` value, however, will + * still reflect the total number of tiles that we are waiting on, including + * the ones that we're not rendering. `notYetRenderableCount` is only reset + * when a subtree is removed from the render queue because the + * `notYetRenderableCount` exceeds the + * {@link TilesetOptions::loadingDescendantLimit}. + */ uint32_t notYetRenderableCount = 0; }; struct CullResult { + // whether we should visit this tile bool shouldVisit = true; + // whether this tile was culled (Note: we might still want to visit it) bool culled = false; }; enum class VisitTileAction { Render, Refine }; +// Visits a tile for possible rendering. When we call this function with a tile: +// * It is not yet known whether the tile is visible. +// * Its parent tile does _not_ meet the SSE (unless ancestorMeetsSse=true, +// see comments below). +// * The tile may or may not be renderable. +// * The tile has not yet been added to a load queue. TraversalDetails visitTileIfNeeded( const TileSelectionContext& context, const TilesetFrameState& frameState, @@ -150,6 +194,18 @@ void addCurrentTileAndDescendantsToTilesFadingOutIfPreviouslyRendered( result); } +/** + * @brief Returns whether a tile with the given bounding volume is visible for + * the camera. + * + * @param viewState The {@link ViewState} + * @param boundingVolume The bounding volume of the tile + * @param forceRenderTilesUnderCamera Whether tiles under the camera should + * always be considered visible and rendered (see + * {@link Cesium3DTilesSelection::TilesetOptions}). + * @return Whether the tile is visible according to the current camera + * configuration + */ bool isVisibleFromCamera( const ViewState& viewState, const BoundingVolume& boundingVolume, @@ -163,6 +219,10 @@ bool isVisibleFromCamera( } const std::optional& position = viewState.getPositionCartographic(); + + // TODO: it would be better to test a line pointing down (and up?) from the + // camera against the bounding volume itself, rather than transforming the + // bounding volume to a region. std::optional maybeRectangle = estimateGlobeRectangle(boundingVolume, ellipsoid); if (position && maybeRectangle) { @@ -171,6 +231,13 @@ bool isVisibleFromCamera( return false; } +/** + * @brief Returns whether a tile at the given distance is visible in the fog. + * + * @param distance The distance of the tile bounding volume to the camera + * @param fogDensity The fog density + * @return Whether the tile is visible in the fog + */ bool isVisibleInFog(double distance, double fogDensity) noexcept { if (fogDensity <= 0.0) { return true; @@ -271,6 +338,22 @@ bool meetsSseThreshold( bool isLeaf(const Tile& tile) noexcept { return tile.getChildren().empty(); } +/** + * @brief Determines if we must refine this tile so that we can continue + * rendering the deeper descendant tiles of this tile. + * + * If this tile was refined last frame, and is not yet renderable, then we + * should REFINE past this tile in order to continue rendering the deeper tiles + * that we rendered last frame, until such time as this tile is loaded and we + * can render it instead. This is necessary to avoid detail vanishing when + * the camera zooms out and lower-detail tiles are not yet loaded. + * + * @param tile The tile to check, which is assumed to meet the SSE for + * rendering. + * @param lastFrameSelectionState The selection state of this tile last frame. + * @return True if this tile must be refined instead of rendered, so that we can + * continue rendering deeper tiles. + */ bool mustContinueRefiningToDeeperTiles( const Tile& tile, const TileSelectionState& lastFrameSelectionState) noexcept { @@ -280,17 +363,6 @@ bool mustContinueRefiningToDeeperTiles( !tile.isRenderable(); } -TraversalDetails visitTile( - const TileSelectionContext& context, - const TilesetFrameState& frameState, - uint32_t depth, - bool meetsSse, - bool ancestorMeetsSse, - Tile& tile, - double tilePriority, - double tileSse, - ViewUpdateResult& result); - TraversalDetails visitVisibleChildrenNearToFar( const TileSelectionContext& context, const TilesetFrameState& frameState, @@ -299,8 +371,7 @@ TraversalDetails visitVisibleChildrenNearToFar( Tile& tile, ViewUpdateResult& result); -// Frustum and fog culling - +// TODO: abstract thse into a composable culling interface. void frustumCull( const TileSelectionContext& context, const TilesetFrameState& frameState, @@ -316,6 +387,7 @@ void frustumCull( const std::vector& frustums = frameState.frustums; if (cullWithChildrenBounds) { + // Frustum cull using the children's bounds. if (std::any_of( frustums.begin(), frustums.end(), @@ -334,8 +406,10 @@ void frustumCull( } return false; })) { + // At least one child is visible in at least one frustum, so don't cull. return; } + // Frustum cull based on the actual tile's bounds. } else if (std::any_of( frustums.begin(), frustums.end(), @@ -350,11 +424,15 @@ void frustumCull( ellipsoid, renderTilesUnderCamera); })) { + // The tile is visible in at least one frustum, so don't cull. return; } + // If we haven't returned yet, this tile is frustum culled. cullResult.culled = true; + if (context.options.enableFrustumCulling) { + // frustum culling is enabled so we shouldn't visit this off-screen tile cullResult.shouldVisit = false; } } @@ -372,6 +450,14 @@ void fogCull( const auto& fogDensities = frameState.fogDensities; const auto& distances = context.scratchDistances; + // prevent out-of-bounds access in the loops below. + CESIUM_ASSERT(fogDensities.size() == frustums.size()); + + // distances is always resized to frustums.size() by computeDistances which is + // called just before fogCull in visitTileIfNeeded so distances.size() should + // always be the same as frustums.size() here, but we'll assert just in case + // this is ever called independently. + CESIUM_ASSERT(distances.size() == frustums.size()); bool isFogCulled = true; for (size_t i = 0; i < frustums.size(); ++i) { if (isVisibleInFog(distances[i], fogDensities[i])) { @@ -381,8 +467,10 @@ void fogCull( } if (isFogCulled) { + // this tile is occluded by fog so it is a culled tile cullResult.culled = true; if (context.options.enableFogCulling) { + // fog culling is enabled so we shouldn't visit this tile cullResult.shouldVisit = false; } } @@ -395,27 +483,43 @@ TileOcclusionState checkOcclusion( const std::shared_ptr& pOcclusionPool = context.externals.pTileOcclusionProxyPool; if (!pOcclusionPool) { + // We don't have an occlusion pool to query occlusion with, treat everything + // as unoccluded. return TileOcclusionState::NotOccluded; } + // First check if this tile's bounding volume has occlusion info and is + // known to be occluded. const TileOcclusionRendererProxy* pOcclusion = pOcclusionPool->fetchOcclusionProxyForTile(tile); if (!pOcclusion) { + // This indicates we ran out of occlusion proxies. We don't want to wait + // on occlusion info here since it might not ever arrive, so treat this + // tile as if it is _known_ to be unoccluded. return TileOcclusionState::NotOccluded; } switch (static_cast(pOcclusion->getOcclusionState())) { case TileOcclusionState::OcclusionUnavailable: + // We have an occlusion proxy, but it does not have valid occlusion + // info yet, wait for it. return TileOcclusionState::OcclusionUnavailable; case TileOcclusionState::Occluded: return TileOcclusionState::Occluded; case TileOcclusionState::NotOccluded: if (tile.getChildren().empty()) { + // This is a leaf tile, so we can't use children bounding volumes. return TileOcclusionState::NotOccluded; } break; } + // The tile's bounding volume is known to be unoccluded, but check the + // union of the children bounding volumes since it is tighter fitting. + + // If any children are to be unconditionally refined, we can't rely on + // their bounding volumes. We also don't want to recurse indefinitely to + // find a valid descendant bounding volumes union. for (const Tile& child : tile.getChildren()) { if (child.getUnconditionallyRefine()) { return TileOcclusionState::NotOccluded; @@ -430,23 +534,32 @@ TileOcclusionState checkOcclusion( const TileOcclusionRendererProxy* pChildProxy = pOcclusionPool->fetchOcclusionProxyForTile(child); if (!pChildProxy) { + // We ran out of occlusion proxies, treat this as if it is _known_ to + // be unoccluded so we don't wait for it. return TileOcclusionState::NotOccluded; } childOcclusionProxies.push_back(pChildProxy); } + // Check if any of the proxies are known to be unoccluded. for (const TileOcclusionRendererProxy* pChildProxy : childOcclusionProxies) { if (pChildProxy->getOcclusionState() == TileOcclusionState::NotOccluded) { return TileOcclusionState::NotOccluded; } } + + // Check if any of the proxies are waiting for valid occlusion info. for (const TileOcclusionRendererProxy* pChildProxy : childOcclusionProxies) { if (pChildProxy->getOcclusionState() == TileOcclusionState::OcclusionUnavailable) { + // We have an occlusion proxy, but it does not have valid occlusion + // info yet, wait for it. return TileOcclusionState::OcclusionUnavailable; } } + // If we know the occlusion state of all children, and none are unoccluded, + // we can treat this tile as occluded. return TileOcclusionState::Occluded; } @@ -466,6 +579,12 @@ TraversalDetails createTraversalDetailsForSingleTile( if (tile.getRefine() == TileRefine::Add) { wasRenderedLastFrame = true; } else { + // With replace-refinement, if any of this refined tile's children were + // rendered last frame, but are no longer rendered because this tile is + // loaded and has sufficient detail, we must treat this tile as rendered + // last frame, too. This is necessary to prevent this tile from being + // kicked just because _it_ wasn't rendered last frame (which could cause + // a new hole to appear). frameState.viewGroup.getTraversalState().forEachPreviousDescendant( [&](const Tile::Pointer& /* pTile */, const TileSelectionState& state) { @@ -518,6 +637,24 @@ TraversalDetails renderInnerTile( return createTraversalDetailsForSingleTile(context, frameState, tile); } +/** + * @brief When called on an additive-refined tile, queues it for load and adds + * it to the render list. + * + * For replacement-refined tiles, this method does nothing and returns false. + * + * @param context The tile selection context. + * @param frameState The current frame state. + * @param tile The tile to potentially load and render. + * @param result The current view update result. + * @param tilePriority The load priority of this tile. + * priority. + * @param tileSse The screen space error of this tile. + * @param queuedForLoad True if this tile has already been queued for loading. + * @return true The additive-refined tile was queued for load and added to the + * render list. + * @return false The non-additive-refined tile was ignored. + */ bool loadAndRenderAdditiveRefinedTile( const TileSelectionContext& context, const TilesetFrameState& frameState, @@ -526,6 +663,8 @@ bool loadAndRenderAdditiveRefinedTile( double tilePriority, double tileSse, bool queuedForLoad) { + // If this tile uses additive refinement, we need to render this tile in + // addition to its children. if (tile.getRefine() == TileRefine::Add) { addTileToRender(result, tile, tileSse); if (!queuedForLoad) { @@ -552,7 +691,7 @@ bool kickDescendantsAndRenderTile( bool queuedForLoad, double tilePriority, double tileSse) { - + // Mark all visited descendants of this tile as kicked. TilesetViewGroup::TraversalState& traversalState = frameState.viewGroup.getTraversalState(); @@ -561,6 +700,14 @@ bool kickDescendantsAndRenderTile( selectionState.kick(); }); + // If any kicked tiles were rendered last frame, add them to the + // tilesFadingOut. This is unlikely! It would imply that a tile rendered last + // frame has suddenly become unrenderable, and therefore eligible for kicking. + // + // In general, it's possible that a Tile previously traversed has been deleted + // completely, so we have to be careful about dereferencing the Tile pointers + // given to the callback below. However, we can be certain that a Tile that + // was rendered last frame has _not_ been deleted yet. traversalState.forEachPreviousDescendant( [&result]( const Tile::Pointer& pTile, @@ -571,6 +718,7 @@ bool kickDescendantsAndRenderTile( result); }); + // Remove all descendants from the render list and add this tile. std::vector& renderList = result.tilesToRenderThisFrame; std::vector& sseList = result.tileScreenSpaceErrorThisFrame; renderList.erase( @@ -591,6 +739,12 @@ bool kickDescendantsAndRenderTile( traversalState.currentState() = TileSelectionState(TileSelectionState::Result::Rendered); + // If we're waiting on heaps of descendants, the above will take too long. So + // in that case, load this tile INSTEAD of loading any of the descendants, and + // tell the up-level we're only waiting on this tile. Keep doing this until we + // actually manage to render this tile. + // Make sure we don't end up waiting on a tile that will _never_ be + // renderable. TileSelectionState::Result lastFrameSelectionState = getPreviousState(frameState.viewGroup, tile).getResult(); const bool wasRenderedLastFrame = @@ -603,6 +757,7 @@ bool kickDescendantsAndRenderTile( context.options.loadingDescendantLimit && !tile.isExternalContent() && !tile.getUnconditionallyRefine()) { + // Remove all descendants from the load queues. result.tilesKicked += static_cast( frameState.viewGroup.restoreTileLoadQueueCheckpoint( loadQueueBeforeChildren)); @@ -633,6 +788,7 @@ TraversalDetails visitVisibleChildrenNearToFar( Tile& tile, ViewUpdateResult& result) { TraversalDetails traversalDetails; + // TODO: actually visit near-to-far, rather than in order of occurrence. for (Tile& child : tile.getChildren()) { const TraversalDetails childTraversal = visitTileIfNeeded( context, @@ -650,12 +806,19 @@ TraversalDetails visitVisibleChildrenNearToFar( return traversalDetails; } +// Visits a tile for possible rendering. When we call this function with a tile: +// * The tile has previously been determined to be visible. +// * Its parent tile does _not_ meet the SSE (unless ancestorMeetsSse=true, +// see comments below). +// * The tile may or may not be renderable. +// * The tile has not yet been added to a load queue. TraversalDetails visitTile( const TileSelectionContext& context, const TilesetFrameState& frameState, uint32_t depth, bool meetsSse, - bool ancestorMeetsSse, + bool ancestorMeetsSse, // Careful: May be modified before being passed to + // children! Tile& tile, double tilePriority, double tileSse, @@ -667,6 +830,7 @@ TraversalDetails visitTile( ++result.tilesVisited; result.maxDepthVisited = glm::max(result.maxDepthVisited, depth); + // If this is a leaf tile, just render it (it's already been deemed visible). if (isLeaf(tile)) { return renderLeaf(context, frameState, tile, tilePriority, tileSse, result); } @@ -674,6 +838,9 @@ TraversalDetails visitTile( const bool unconditionallyRefine = tile.getUnconditionallyRefine(); const bool refineForSse = !meetsSse && !ancestorMeetsSse; + // Determine whether to REFINE or RENDER. Note that even if this tile is + // initially marked for RENDER here, it may later switch to REFINE as a + // result of `mustContinueRefiningToDeeperTiles`. VisitTileAction action = (unconditionallyRefine || refineForSse) ? VisitTileAction::Refine : VisitTileAction::Render; @@ -683,6 +850,14 @@ TraversalDetails visitTile( TileSelectionState::Result lastFrameSelectionResult = lastFrameSelectionState.getResult(); + // If occlusion culling is enabled, we may not want to refine for two + // reasons: + // - The tile is known to be occluded, so don't refine further. + // - The tile was not previously refined and the occlusion state for this + // tile is not known yet, but will be known in the next several frames. If + // delayRefinementForOcclusion is enabled, we will wait until the tile has + // valid occlusion info to decide to refine. This might save us from + // kicking off descendant loads that we later find to be unnecessary. bool tileLastRefined = lastFrameSelectionResult == TileSelectionState::Result::Refined; bool childLastRefined = false; @@ -693,6 +868,8 @@ TraversalDetails visitTile( } }); + // If this tile and a child were both refined last frame, this tile does not + // need occlusion results. bool shouldCheckOcclusion = context.options.enableOcclusionCulling && action == VisitTileAction::Refine && !unconditionallyRefine && @@ -718,10 +895,19 @@ TraversalDetails visitTile( bool queuedForLoad = false; if (action == VisitTileAction::Render) { + // This tile meets the screen-space error requirement, so we'd like to + // render it, if we can. bool mustRefine = mustContinueRefiningToDeeperTiles(tile, lastFrameSelectionState); if (mustRefine) { action = VisitTileAction::Refine; + // Loading this tile is very important, because a number of deeper, + // higher-detail tiles are being rendered in its stead, so we want to load + // it with high priority. However, if `ancestorMeetsSse` is set, then our + // parent tile is in the exact same situation, and loading this tile with + // high priority would compete with that one. We should prefer the parent + // because it is closest to the actual desired LOD and because up the tree + // there can only be fewer tiles that need loading. if (!ancestorMeetsSse) { addTileToLoadQueue( context, @@ -731,8 +917,12 @@ TraversalDetails visitTile( tilePriority); queuedForLoad = true; } + // Fall through to REFINE, but mark this tile as already meeting the + // required SSE. ancestorMeetsSse = true; } else { + // Render this tile and return without visiting children. + // Only load this tile if it (not just an ancestor) meets the SSE. if (!ancestorMeetsSse) { addTileToLoadQueue( context, @@ -769,22 +959,36 @@ TraversalDetails visitTile( tile, result); - const TileRenderContent* pRenderContent = - tile.getContent().getRenderContent(); + // Zero or more descendant tiles were added to the render list. + // The traversalDetails tell us what happened while visiting the children. + + // Descendants will be kicked if any are not ready to render yet and none + // were rendered last frame. bool kickDueToNonReadyDescendant = !traversalDetails.allAreRenderable && !traversalDetails.anyWereRenderedLastFrame; + + // Descendants may also be kicked if this tile was rendered last frame and + // has not finished fading in yet. + const TileRenderContent* pRenderContent = + tile.getContent().getRenderContent(); bool kickDueToTileFadingIn = context.options.enableLodTransitionPeriod && context.options.kickDescendantsWhileFadingIn && lastFrameSelectionResult == TileSelectionState::Result::Rendered && pRenderContent && pRenderContent->getLodTransitionFadePercentage() < 1.0f; + // Only kick the descendants of this tile if it is renderable, or if we've + // exceeded the loadingDescendantLimit. It's pointless to kick the descendants + // of a tile that is not yet loaded, because it means we will still have a + // hole, and quite possibly a bigger one. bool wantToKick = kickDueToNonReadyDescendant || kickDueToTileFadingIn; bool willKick = wantToKick && (traversalDetails.notYetRenderableCount > context.options.loadingDescendantLimit || tile.isRenderable()); if (willKick) { + // Kick all descendants out of the render list and render this tile instead + // Continue to load them though! queuedForLoad = kickDescendantsAndRenderTile( context, frameState, @@ -819,6 +1023,12 @@ TraversalDetails visitTile( return traversalDetails; } +// Visits a tile for possible rendering. When we call this function with a tile: +// * It is not yet known whether the tile is visible. +// * Its parent tile does _not_ meet the SSE (unless ancestorMeetsSse=true, +// see comments below). +// * The tile may or may not be renderable. +// * The tile has not yet been added to a load queue. TraversalDetails visitTileIfNeeded( const TileSelectionContext& context, const TilesetFrameState& frameState, @@ -841,6 +1051,8 @@ TraversalDetails visitTileIfNeeded( CullResult cullResult{}; + // Culling with children bounds will give us incorrect results with Add + // refinement, but is a useful optimization for Replace refinement. bool cullWithChildrenBounds = tile.getRefine() == TileRefine::Replace && !tile.getChildren().empty(); for (Tile& child : tile.getChildren()) { @@ -850,6 +1062,7 @@ TraversalDetails visitTileIfNeeded( } } + // TODO: add cullWithChildrenBounds to the tile excluder interface? for (const std::shared_ptr& pExcluder : context.options.excluders) { if (pExcluder->shouldExclude(tile)) { @@ -859,10 +1072,15 @@ TraversalDetails visitTileIfNeeded( } } + // TODO: abstract culling stages into composable interface? frustumCull(context, frameState, tile, cullWithChildrenBounds, cullResult); fogCull(context, frameState, cullResult); if (!cullResult.shouldVisit && tile.getUnconditionallyRefine()) { + // Unconditionally refined tiles must always be visited in forbidHoles + // mode, because we need to load this tile's descendants before we can + // render any of its siblings. An unconditionally refined root tile must be + // visited as well, otherwise we won't load anything at all. if ((context.options.forbidHoles && tile.getRefine() == TileRefine::Replace) || tile.getParent() == nullptr) { @@ -884,6 +1102,9 @@ TraversalDetails visitTileIfNeeded( if (context.options.forbidHoles && tile.getRefine() == TileRefine::Replace) { + // In order to prevent holes, we need to load this tile and also not + // render any siblings until it is ready. We don't actually need to + // render it, though. addTileToLoadQueue( context, frameState, From 7d9efb3baa8ec49c73cfa6ce18b7785f5ffdae18 Mon Sep 17 00:00:00 2001 From: Caleb Buffa Date: Wed, 13 May 2026 10:36:16 -0500 Subject: [PATCH 28/28] remove unused parameters --- .../src/TilesetSelection.cpp | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Cesium3DTilesSelection/src/TilesetSelection.cpp b/Cesium3DTilesSelection/src/TilesetSelection.cpp index 2cd9f648e5..2ace7d0fc5 100644 --- a/Cesium3DTilesSelection/src/TilesetSelection.cpp +++ b/Cesium3DTilesSelection/src/TilesetSelection.cpp @@ -328,7 +328,6 @@ double computeSse( bool meetsSseThreshold( const TileSelectionContext& context, - const TilesetFrameState& frameState, double sse, bool culled) noexcept { return culled ? !context.options.enforceCulledScreenSpaceError || @@ -476,10 +475,8 @@ void fogCull( } } -TileOcclusionState checkOcclusion( - const TileSelectionContext& context, - const TilesetFrameState& frameState, - const Tile& tile) { +TileOcclusionState +checkOcclusion(const TileSelectionContext& context, const Tile& tile) { const std::shared_ptr& pOcclusionPool = context.externals.pTileOcclusionProxyPool; if (!pOcclusionPool) { @@ -564,7 +561,6 @@ TileOcclusionState checkOcclusion( } TraversalDetails createTraversalDetailsForSingleTile( - const TileSelectionContext& context, const TilesetFrameState& frameState, const Tile& tile) { TileSelectionState::Result lastFrameResult = @@ -618,11 +614,10 @@ TraversalDetails renderLeaf( tile, TileLoadPriorityGroup::Normal, tilePriority); - return createTraversalDetailsForSingleTile(context, frameState, tile); + return createTraversalDetailsForSingleTile(frameState, tile); } TraversalDetails renderInnerTile( - const TileSelectionContext& context, const TilesetFrameState& frameState, Tile& tile, double tileSse, @@ -634,7 +629,7 @@ TraversalDetails renderInnerTile( frameState.viewGroup.getTraversalState().currentState() = TileSelectionState(TileSelectionState::Result::Rendered); addTileToRender(result, tile, tileSse); - return createTraversalDetailsForSingleTile(context, frameState, tile); + return createTraversalDetailsForSingleTile(frameState, tile); } /** @@ -876,7 +871,7 @@ TraversalDetails visitTile( (!tileLastRefined || !childLastRefined); if (shouldCheckOcclusion) { - TileOcclusionState occlusion = checkOcclusion(context, frameState, tile); + TileOcclusionState occlusion = checkOcclusion(context, tile); if (occlusion == TileOcclusionState::Occluded) { ++result.tilesOccluded; action = VisitTileAction::Render; @@ -931,7 +926,7 @@ TraversalDetails visitTile( TileLoadPriorityGroup::Normal, tilePriority); } - return renderInnerTile(context, frameState, tile, tileSse, result); + return renderInnerTile(frameState, tile, tileSse, result); } } @@ -1111,8 +1106,7 @@ TraversalDetails visitTileIfNeeded( tile, TileLoadPriorityGroup::Normal, tilePriority); - traversalDetails = - createTraversalDetailsForSingleTile(context, frameState, tile); + traversalDetails = createTraversalDetailsForSingleTile(frameState, tile); } else if (context.options.preloadSiblings) { addTileToLoadQueue( context, @@ -1131,8 +1125,7 @@ TraversalDetails visitTileIfNeeded( } double tileSse = computeSse(context, frameState, tile); - bool meetsSse = - meetsSseThreshold(context, frameState, tileSse, cullResult.culled); + bool meetsSse = meetsSseThreshold(context, tileSse, cullResult.culled); TraversalDetails details = visitTile( context,