Extract Tile selection algorithms from Tileset into free functions#1342
Extract Tile selection algorithms from Tileset into free functions#1342calebbuffa wants to merge 26 commits intoCesiumGS:mainfrom
Tile selection algorithms from Tileset into free functions#1342Conversation
kring
left a comment
There was a problem hiding this comment.
Thanks for the PR @calebbuffa!
I love the way you've broken the tileset selection algorithm into a separate, testable function.
Some of the other changes seem mostly unrelated to this important core, though, and I have some quibbles with them. See below.
| * | ||
| * 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] |
There was a problem hiding this comment.
I think this class plays by our thread safety rules just fine. There's no need to further restrict it to "main thread only".
https://cesium.com/learn/cesium-native/ref-doc/thread-safety-rules.html
|
@kring thank you for the feedback! I got a little too carried away. I stripped the changes just to |
Tile selection algorithms from Tileset into free functions
|
I asked Copilot to review this PR, specifically looking for any changes that were inadvertently introduced when I reviewed the code moved from Two issues: 1. Orphaned doc comment in When private:
/**
* @brief When called on an additive-refined tile, queues it for load and adds
* it to the render list.
* ...
* @return false The non-additive-refined tile was ignored.
*/
// ← _loadAndRenderAdditiveRefinedTile declaration was deleted, but this comment wasn't
void _unloadCachedTiles(double timeBudget) noexcept;This happened because the deletion straddled a diff hunk boundary — the Fix: delete the orphaned 2. Unnecessary In auto& childOcclusionProxies =
const_cast<std::vector<const TileOcclusionRendererProxy*>&>(
ctx.childOcclusionProxies);
Fix: remove the |
|
Hi @calebbuffa! We're interested in getting this into the next release, are you able to address Kevin's comments above within the next few days? |
|
@j9liu yes! Sorry got a little sidetracked the last few days. I'll fix the PR by end of week! |
|
Thanks for the updates @calebbuffa! Just for transparency, I think I'll actually defer merging this to after our May 1 release if that's alright. We'll still review this soon, but it would help to have additional time to stress-test the changes, just in case there are any surprises in our runtimes. |
There was a problem hiding this comment.
Pull request overview
This PR refactors the tileset LOD / tile-selection traversal by extracting it from Tileset private methods into a standalone selectTiles() free function, aiming to reduce Tileset surface area and make selection logic more directly testable.
Changes:
- Introduces
selectTiles()andTileSelectionContextas a public entry point for the selection traversal. - Adds
TilesetFrameState::tileStateUpdaterto decouple traversal fromTilesetContentManager. - Refactors tile content eviction tracking behind a new
TileUnloadQueuewrapper and adds new unit tests for the free-function selection path.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| Cesium3DTilesSelection/test/TestTilesetSelection.cpp | Adds unit tests that call selectTiles() directly and compare results to Tileset::updateViewGroup. |
| Cesium3DTilesSelection/src/TilesetSelection.cpp | New implementation of the extracted tile-selection / traversal algorithm. |
| Cesium3DTilesSelection/src/Tileset.cpp | Makes updateViewGroup a coordinator that builds frame state/context and calls selectTiles(). |
| Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h | New public API header defining TileSelectionContext and selectTiles(). |
| Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetFrameState.h | Adds tileStateUpdater callback for per-visited-tile state advancement. |
| Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h | Removes private traversal helpers and includes the new selection header. |
| Cesium3DTilesSelection/src/TilesetContentManager.h | Replaces raw unused-tile linked list member with TileUnloadQueue. |
| Cesium3DTilesSelection/src/TilesetContentManager.cpp | Switches eviction bookkeeping to TileUnloadQueue and adjusts member initialization. |
| Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h | Adds a small wrapper around Tile::UnusedLinkedList for LRU eviction tracking. |
| Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h | Minor doc wording tweak for getParent() comment. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| TraversalDetails visitTileIfNeeded( | ||
| const TraversalContext& ctx, | ||
| uint32_t depth, | ||
| bool ancestorMeetsSse, | ||
| Tile& tile, | ||
| ViewUpdateResult& result); | ||
|
|
There was a problem hiding this comment.
visitTileIfNeeded is forward-declared here and then forward-declared again later in the file. Keeping only one forward declaration block (or reordering definitions) would reduce duplication and make the file easier to follow.
There was a problem hiding this comment.
Not the end of the world, but I guess just remove the duplicate one that appears later 🤷
| CESIUM3DTILESSELECTION_API void selectTiles( | ||
| const TileSelectionContext& ctx, | ||
| const TilesetFrameState& frameState, | ||
| Tile& rootTile, | ||
| ViewUpdateResult& result); |
There was a problem hiding this comment.
PR description shows selectTiles returning a ViewUpdateResult (and taking no result out-parameter), but the implemented public API is void selectTiles(..., ViewUpdateResult& result). Please update the PR description (or the API) so they match, to avoid confusing reviewers and downstream users reading the change log / docs.
There was a problem hiding this comment.
Probably enough to leave an "OUTDATED" comment on the PR description, not the end of the world 🤷
| * @brief Runs the tile selection / LOD traversal algorithm. | ||
| * | ||
| * 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 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. |
There was a problem hiding this comment.
Because selectTiles() mutates frameState.viewGroup traversal/load-queue state, the API doc should state the required call pattern/preconditions for external callers (e.g., TilesetViewGroup::startNewFrame must be called before and finishFrame after; and frameState.fogDensities must correspond 1:1 with frameState.frustums). Without this, it’s easy to call selectTiles() directly and hit assertions or UB.
| 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; |
There was a problem hiding this comment.
fogCull indexes fogDensities[i] assuming frameState.fogDensities.size() == frameState.frustums.size(). Since selectTiles() is a public free function, callers can accidentally provide mismatched vectors, leading to out-of-bounds access in release builds. Add a CESIUM_ASSERT (or iterate up to min(frustums.size(), fogDensities.size(), distances.size())) to make the precondition explicit and prevent UB.
There was a problem hiding this comment.
CESIUM_ASSERT isn't a bad idea here, since that internal assumption is no longer guaranteed outside of Tileset.cpp
j9liu
left a comment
There was a problem hiding this comment.
Thank you for the work on this PR @calebbuffa! It might look like there's a lot of comments but I promise they're all minor.
The biggest note is that we lost a lot of helpful internal comments that we should preserve for future knowledge. Otherwise, it's just a matter of style / minor tweaks.
| struct TraversalDetails { | ||
| bool allAreRenderable = true; | ||
| bool anyWereRenderedLastFrame = false; | ||
| uint32_t notYetRenderableCount = 0; | ||
| }; |
There was a problem hiding this comment.
I think it would still be valuable to preserve comments that were here when this was defined in Tileset.h (minus allusions to outdated implementation details).
| * @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 result Filled with the selected tiles and statistics for this frame. | ||
| */ | ||
| CESIUM3DTILESSELECTION_API void selectTiles( | ||
| const TileSelectionContext& ctx, |
There was a problem hiding this comment.
Hm, I can't find where we mention it in the style guide, but in general we avoid abbreviations for better code readability. Since this is public API, it's better to be overly explicit and write out everything unless truly cumbersome.
| * @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 result Filled with the selected tiles and statistics for this frame. | |
| */ | |
| CESIUM3DTILESSELECTION_API void selectTiles( | |
| const TileSelectionContext& ctx, | |
| * @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. | |
| */ | |
| CESIUM3DTILESSELECTION_API void selectTiles( | |
| const TileSelectionContext& context, |
| 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); } |
There was a problem hiding this comment.
Per our style guide:
Use
this->when accessing member variables and functions. This helps distinguish member variables from local variables and member functions from anonymous namespace functions.
If we could this->_queue throughout the file that'd be great!
| if (!frustums.empty()) { | ||
| viewGroup.startNewFrame(*this, frameState); | ||
| this->_visitTileIfNeeded(frameState, 0, false, *pRootTile, result); | ||
| TileSelectionContext ctx{ |
There was a problem hiding this comment.
Avoid abbreviations unless the full word(s) are truly cumbersome.
| TileSelectionContext ctx{ | |
| TileSelectionContext context{ |
| result); | ||
| } | ||
|
|
||
| bool isVisibleFromCamera( |
There was a problem hiding this comment.
IMO it's valuable to keep the previous comments around, even for the more straightforward private functions.
| bool isVisibleFromCamera( | |
| /** | |
| * @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( |
| return TileOcclusionState::NotOccluded; | ||
| } | ||
|
|
||
| const TileOcclusionRendererProxy* pOcclusion = |
There was a problem hiding this comment.
The internal comments in this function got lost in the move; could you restore them?
| 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; |
There was a problem hiding this comment.
CESIUM_ASSERT isn't a bad idea here, since that internal assumption is no longer guaranteed outside of Tileset.cpp
|
|
||
| // Frustum and fog culling | ||
|
|
||
| void frustumCull( |
There was a problem hiding this comment.
This function has several internal comments that got lost in the move; could you restore them?
| * @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. | ||
| */ |
There was a problem hiding this comment.
Maybe worth marking as @private API for now; not sure if access to this function might confuse users
| */ | |
| * @private | |
| */ |
| * Head is least-recently-used, tail is most-recently-used. | ||
| */ |
There was a problem hiding this comment.
Maybe worth marking as @private API for now, might not have public API value for users.
| * Head is least-recently-used, tail is most-recently-used. | |
| */ | |
| * Head is least-recently-used, tail is most-recently-used. | |
| * | |
| * @private | |
| */ |
Description
This PR breaks tile selection algorithms out of private methods in the
Tilesetclass and into free functions. The purpose is to reduce the surface area ofTilesetand make the selection algorithms independently testable.Changes
1) —
tileStateUpdatercallback: decouple traversal from loadingThe traversal helpers (
_visitTileIfNeeded, etc.) previously called_pTilesetContentManager->updateTileContent()directly, creating a compile-time dependency onTilesetContentManagerinside the selection algorithm. Astd::function<void(Tile&)> tileStateUpdaterfield was added toTilesetFrameState.Tileset::updateViewGroupinjects the implementation via a lambda; traversal calls the callback. The traversal has no#includedependency onTilesetContentManager.Files:
TilesetFrameState.h,Tileset.cpp2) —
selectTiles(): pure free function for tile LOD selectionThe entire traversal algorithm (
_visitTileIfNeeded,_visitTile,_renderLeaf,_renderInnerTile,_visitVisibleChildrenNearToFar,_kickDescendantsAndRenderTile,_loadAndRenderAdditiveRefinedTile, frustum/fog cull, occlusion check, SSE computation) was extracted fromTilesetprivate methods into free functions in a new translation unitTilesetSelection.cpp. The public entry point is:Tileset::updateViewGroupis now a thin coordinator: it buildsframeState, callsselectTiles(), and the result is returned directly. The private traversal method declarations andTraversalDetails/CullResultstructs are gone fromTileset.h.Files:
TilesetSelection.h(new),TilesetSelection.cpp(new),Tileset.h,Tileset.cppIssue number or link
N/A.
Author checklist
CHANGES.mdwith a short summary of my change (for user-facing changes).Testing plan
Regression: All existing tests in
TestTilesetSelectionAlgorithm.cppexercise the full selection pipeline viaTileset::updateViewGroup, which now delegates toselectTiles(). These cover: replace/additive refinement, SSE thresholds, frustum/fog culling, multi-frustum,forbidHoles, LOD transition fade-out,unconditionallyRefine.New isolation test (
TestTilesetSelection.cpp):selectTiles is callable as a free function— constructs a minimalEllipsoidTilesetLoader-backed tileset, callsselectTiles()directly (bypassingTileset::updateViewGroup), and asserts at least one tile is selected with no tiles kicked.selectTiles result matches updateViewGroup result— runs bothupdateViewGroupandselectTiles()on the same warmed-up tileset and asserts tile render count and visit count are identical between the two paths.Build verification:
cmake --build build --target Cesium3DTilesSelectionsucceeds with zero errors.Remaining Tasks
Things that could be done:
TilesetViewGroup::startNewFrame/finishFramestill takeconst Tileset&, which prevents fully async-free unit tests ofselectTiles. Decoupling that is a natural follow-up once this PR lands.