Skip to content

Extract Tile selection algorithms from Tileset into free functions#1342

Open
calebbuffa wants to merge 26 commits intoCesiumGS:mainfrom
calebbuffa:ownership
Open

Extract Tile selection algorithms from Tileset into free functions#1342
calebbuffa wants to merge 26 commits intoCesiumGS:mainfrom
calebbuffa:ownership

Conversation

@calebbuffa
Copy link
Copy Markdown
Contributor

@calebbuffa calebbuffa commented Apr 9, 2026

Description

This PR breaks tile selection algorithms out of private methods in the Tileset class and into free functions. The purpose is to reduce the surface area of Tileset and make the selection algorithms independently testable.

Changes

1) — tileStateUpdater callback: decouple traversal from loading

The traversal helpers (_visitTileIfNeeded, etc.) previously called _pTilesetContentManager->updateTileContent() directly, creating a compile-time dependency on TilesetContentManager inside the selection algorithm. A std::function<void(Tile&)> tileStateUpdater field was added to TilesetFrameState. Tileset::updateViewGroup injects the implementation via a lambda; traversal calls the callback. The traversal has no #include dependency on TilesetContentManager.

Files: TilesetFrameState.h, Tileset.cpp

2) — selectTiles(): pure free function for tile LOD selection

The entire traversal algorithm (_visitTileIfNeeded, _visitTile, _renderLeaf, _renderInnerTile, _visitVisibleChildrenNearToFar, _kickDescendantsAndRenderTile, _loadAndRenderAdditiveRefinedTile, frustum/fog cull, occlusion check, SSE computation) was extracted from Tileset private methods into free functions in a new translation unit TilesetSelection.cpp. The public entry point is:

ViewUpdateResult selectTiles(
    const TileSelectionContext& ctx,   // options + externals
    const TilesetFrameState& frameState,
    Tile& rootTile);

Tileset::updateViewGroup is now a thin coordinator: it builds frameState, calls selectTiles(), and the result is returned directly. The private traversal method declarations and TraversalDetails/CullResult structs are gone from Tileset.h.

Files: TilesetSelection.h (new), TilesetSelection.cpp (new), Tileset.h, Tileset.cpp


Issue number or link

N/A.


Author checklist

  • I have submitted a Contributor License Agreement (only needed once).
  • I have done a full self-review of my code.
  • I have updated CHANGES.md with a short summary of my change (for user-facing changes).
  • I have added or updated unit tests to ensure consistent code coverage as necessary.
  • I have updated the documentation as necessary.

Testing plan

Regression: All existing tests in TestTilesetSelectionAlgorithm.cpp exercise the full selection pipeline via Tileset::updateViewGroup, which now delegates to selectTiles(). 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 minimal EllipsoidTilesetLoader-backed tileset, calls selectTiles() directly (bypassing Tileset::updateViewGroup), and asserts at least one tile is selected with no tiles kicked.
  • selectTiles result matches updateViewGroup result — runs both updateViewGroup and selectTiles() 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 Cesium3DTilesSelection succeeds with zero errors.


Remaining Tasks

Things that could be done:

  • TilesetViewGroup::startNewFrame/finishFrame still take const Tileset&, which prevents fully async-free unit tests of selectTiles. Decoupling that is a natural follow-up once this PR lands.

@calebbuffa calebbuffa marked this pull request as ready for review April 9, 2026 21:18
@calebbuffa calebbuffa changed the title Ownership Improve Ownership Semantics Apr 9, 2026
@calebbuffa calebbuffa changed the title Improve Ownership Semantics Improve Ownership Semantics in Cesium3DTilesSelection Apr 10, 2026
Copy link
Copy Markdown
Member

@kring kring left a comment

Choose a reason for hiding this comment

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

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.

Comment thread Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h Outdated
Comment thread Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h Outdated
Comment thread Cesium3DTilesSelection/include/Cesium3DTilesSelection/ThreadSafety.h Outdated
Comment thread Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileHierarchy.h Outdated
Comment thread Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSelection.h Outdated
*
* 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]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Comment thread Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileUnloadQueue.h Outdated
Comment thread Cesium3DTilesSelection/src/TileOverlaySystem.h Outdated
Comment thread Cesium3DTilesSelection/src/TilesetSelection.cpp Outdated
Comment thread Cesium3DTilesSelection/src/TilesetSelection.cpp
@calebbuffa
Copy link
Copy Markdown
Contributor Author

calebbuffa commented Apr 13, 2026

@kring thank you for the feedback! I got a little too carried away. I stripped the changes just to selectTiles and the TilesUnloadQueue. Thanks for refocusing!

@calebbuffa calebbuffa changed the title Improve Ownership Semantics in Cesium3DTilesSelection Extract Tile selection algorithms from Tileset into free functions Apr 13, 2026
@j9liu j9liu requested a review from kring April 16, 2026 19:23
@j9liu j9liu added this to the May 2026 Release milestone Apr 17, 2026
@kring
Copy link
Copy Markdown
Member

kring commented Apr 20, 2026

I asked Copilot to review this PR, specifically looking for any changes that were inadvertently introduced when
moving code from Tileset.cpp to TilesetSelection.cpp. Both observations look correct to me. However, I suggest moving the orphaned comment into the new file instead of deleting it.


I reviewed the code moved from Tileset.cpp to TilesetSelection.cpp for inadvertent changes. The core traversal logic appears to be a faithful move — no inadvertent behavioral differences found.

Two issues:

1. Orphaned doc comment in Tileset.h

When _loadAndRenderAdditiveRefinedTile was removed, its full /** ... */ doc comment was accidentally left behind. It now sits immediately before _unloadCachedTiles, making it appear to document that function:

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 /** and @brief lines fell in the first hunk, the closing @param/@return/*/ lines in the second. The function declaration was correctly deleted but the entire comment body survived as context lines in both hunks.

Fix: delete the orphaned /** ... */ block from Tileset.h.

2. Unnecessary const_cast introduced in checkOcclusion

In TilesetSelection.cpp, the new checkOcclusion accesses ctx.childOcclusionProxies via a spurious const_cast:

auto& childOcclusionProxies =
    const_cast<std::vector<const TileOcclusionRendererProxy*>&>(
        ctx.childOcclusionProxies);

childOcclusionProxies is declared as std::vector<const TileOcclusionRendererProxy*>& (a non-const reference member) in TraversalContext. Reference members retain their mutability when accessed through a const struct reference in C++, so the cast is unnecessary. The original _checkOcclusion in Tileset.cpp modified this->_childOcclusionProxies directly without any cast.

Fix: remove the const_cast and use ctx.childOcclusionProxies directly.

Copy link
Copy Markdown
Member

@kring kring left a comment

Choose a reason for hiding this comment

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

A few more comments. Other than this, this PR looks good to me! I'll let @azrogers or @j9liu merge it, though.

Comment thread Cesium3DTilesSelection/src/Tileset.cpp Outdated
Comment thread Cesium3DTilesSelection/src/Tileset.cpp Outdated
Comment thread Cesium3DTilesSelection/src/Tileset.cpp Outdated
@j9liu
Copy link
Copy Markdown
Contributor

j9liu commented Apr 22, 2026

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?

@calebbuffa
Copy link
Copy Markdown
Contributor Author

@j9liu yes! Sorry got a little sidetracked the last few days. I'll fix the PR by end of week!

@j9liu j9liu removed this from the May 2026 Release milestone Apr 27, 2026
@j9liu j9liu requested review from Copilot and j9liu April 27, 2026 15:07
@j9liu
Copy link
Copy Markdown
Contributor

j9liu commented Apr 27, 2026

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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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() and TileSelectionContext as a public entry point for the selection traversal.
  • Adds TilesetFrameState::tileStateUpdater to decouple traversal from TilesetContentManager.
  • Refactors tile content eviction tracking behind a new TileUnloadQueue wrapper 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.

Comment on lines +69 to +75
TraversalDetails visitTileIfNeeded(
const TraversalContext& ctx,
uint32_t depth,
bool ancestorMeetsSse,
Tile& tile,
ViewUpdateResult& result);

Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not the end of the world, but I guess just remove the duplicate one that appears later 🤷

Comment on lines +43 to +47
CESIUM3DTILESSELECTION_API void selectTiles(
const TileSelectionContext& ctx,
const TilesetFrameState& frameState,
Tile& rootTile,
ViewUpdateResult& result);
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably enough to leave an "OUTDATED" comment on the PR description, not the end of the world 🤷

Comment on lines +33 to +41
* @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.
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +369 to +376
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;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CESIUM_ASSERT isn't a bad idea here, since that internal assumption is no longer guaranteed outside of Tileset.cpp

Comment thread Cesium3DTilesSelection/src/TilesetSelection.cpp
Copy link
Copy Markdown
Contributor

@j9liu j9liu left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +56 to +60
struct TraversalDetails {
bool allAreRenderable = true;
bool anyWereRenderedLastFrame = false;
uint32_t notYetRenderableCount = 0;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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).

Comment on lines +38 to +44
* @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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Suggested change
* @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,

Comment on lines +34 to +62
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); }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Avoid abbreviations unless the full word(s) are truly cumbersome.

Suggested change
TileSelectionContext ctx{
TileSelectionContext context{

result);
}

bool isVisibleFromCamera(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IMO it's valuable to keep the previous comments around, even for the more straightforward private functions.

Suggested change
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 =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The internal comments in this function got lost in the move; could you restore them?

Comment on lines +369 to +376
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe worth marking as @private API for now; not sure if access to this function might confuse users

Suggested change
*/
* @private
*/

Comment on lines +10 to +11
* Head is least-recently-used, tail is most-recently-used.
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe worth marking as @private API for now, might not have public API value for users.

Suggested change
* Head is least-recently-used, tail is most-recently-used.
*/
* Head is least-recently-used, tail is most-recently-used.
*
* @private
*/

@j9liu j9liu added this to the June 2026 Release milestone Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants