Skip to content

Conversation

kring
Copy link
Member

@kring kring commented Mar 11, 2025

This is a big change, so I'm going to try to break it down here to hopefully make review more approachable.

TilesetViewGroup

Represents a set of views that select tiles together. The actual views (ViewState instances) can change from frame to frame, and even the number of views in the group need not be consistent from one frame to the next.

However, the view group is "stateful" with respect to the selection algorithm. Which tiles were selected last frame in a view group affects which tiles will be selected this frame. This allows the algorithm to ensure that tiles don't blink out of existence from one frame to the next, and that unnecessary holes (missing tiles) are minimized.

The tiles selected for a view group are completely independent from other view groups. Two different view groups might cover a particular area of the model with different levels-of-detail, for example. When rendering a particular viewport, it's essential that only tiles from a single view group are rendered. View groups are useful when you have multiple viewports.

They might also be useful in some weirder circumstances. Rendering shadows using tiles selected by a view group with the perspective of a light source, perhaps? Or a view group at the destination of a camera flight to force those tiles to load before the camera arrives?

TilesetViewGroup can be default-constructed, and is owned by the user. Users are free to manage its lifetime however they see fit. It can be both moved and copied.

updateView ➡️ updateViewGroup

Previously, users would call the updateView method on a Tileset and pass a list of frustums to use to select tiles. This will still work after this PR, and it will update the "default" view group. The default view group is owned by Tileset and can be obtained by calling getDefaultViewGroup().

However, when using view groups, it is recommended to instead use the new updateViewGroup function. In addition to allowing a non-default view group to be updated, this function also does not load tiles. That needs a little explanation.

View groups are not required to all be updated at the same rate. It's perfectly valid to update one view group at 60 FPS and another only rarely. Each time a view group is updated, it determines which tiles it would ideally like to load. The Tileset then uses a weighted round robin algorithm to give all view groups a fair chance to load tiles. The total number of tiles that may be in flight at any given time is, as before, limited by the maximumSimultaneousTileLoads property in TilesetOptions.

If we initiated tile loads after every updateViewGroup, then the first view group updated would likely use up all the tile load slots, because no other view groups would have any tiles to load yet. That's why loadTiles is a separate function on Tileset now, and must be called explicitly by the user when utilizing updateViewGroup. For backward compatibility, updateView calls loadTiles automatically after updating the default view group.

LoadedLinkedList ➡️ UnusedLinkedList

Previously, Tiles that were traversed during updateView were added to an intrusive LoadedLinkedList on the Tileset called _loadedTiles. Each time a tile was visited, it was moved to the tail of the linked list. Since traversal always starts at the root, the root tile in the linked list marked the start of the tiles that were used in the most recent frame. Any tiles before this were not traversed last frame and so could be unloaded, with the least recently used closest to the head.

This is unworkable with multiple views, because we traverse the tile hierarchy multiple times. So the LoadedLinkedList is gone in this PR.

Instead, TilesetViewGroup now holds an IntrusivePointer<const Tile> for every tile that it visited during the last updateViewGroup. This intrusive pointer increments/decrements the _referenceCount field on the Tile. This is separate from and in addition to _doNotUnloadSubtreeCount that was added in #1107. It differs in two ways:

  1. A non-zero _referenceCount keeps tile content from unloading. _doNotUnloadSubtreeCount only keeps subtrees from unloading and Tile instances from being deleted, but tile content is allowed to unload.
  2. _referenceCount is not incremented on parent tiles when it is incremented on child tiles. The depth-first traversal will ensure that all ancestors of referenced children are also referenced.

When the _referenceCount goes to zero, the Tile is added to the tail of the new, intrusive UnusedLinkedList. If the _referenceCount goes back above zero, it is removed from the UnusedLinkedList. All tiles in this list can have their content unloaded, and the ones closer to the head are the least recently used.

This new mechanism for tracking unloadable tiles works well across view groups.

LoadedTileEnumerator

The LoadedLinkedList was previously used to determine which tiles were unloadable and in what order they should be unloaded, as described above, but it had another purpose as well. It also allowed the complete set of "loaded" tiles to be enumerated. This was used for a few different things, notably adding or removing raster overlays to/from existing tiles.

So with the LoadedLinkedList now gone, we need a new way to enumerate the set of loaded tiles. In this PR, this job is handled by LoadedTileEnumerator. It works by traversing the tile hierarchy itself. Rather than traversing the entire tile hierarchy, though - which would be very slow - it only traverses tiles that either have loaded content or that have a _doNotUnloadSubtreeCount greater than zero (indicating that a descendant tile has loaded content).

This is probably a bit less efficient than the old linked list traversal, but it's still plenty fast for the types of use-cases where we use it.

TileLoadRequester

I mentioned above that Tileset uses a weight round-robin algorithm to give multiple view groups a fair chance to load tiles. This happens via the new TileLoadRequester abstract base class.

Anything that wants to influence which tiles are loaded should inherit from TileLoadRequester. Currently that means two types: TilesetViewGroup and TilesetHeightRequest (which is created by a call to sampleHeightMostDetailed).

A TileLoadRequester may be registered with the Tileset by calling Tileset::registerLoadRequester. The requester offers two queues of tiles it wants to load, one that needs worker-thread loading and the other that needs main-thread loading. The two queues are exposed using a simple "has more?" / "get next!" interface, so the underlying mechanism for the storing the queue is up to the implementation. The requester also has getWeight method that determines the load priority of this requester versus the others. Here's the explanation of how the weight works, copied from the doxygen on the getWeight method:

Most requesters should return a weight of 1.0. When all requesters have the same weight, they will all have an equal opportunity to load tiles. If one requester's weight is 2.0 and the rest are 1.0, that requester will have twice as many opportunities to load tiles as the others.

A very high weight will prevent all other requesters from loading tiles until this requester has none left to load. A very low weight (but above 0.0!) will allow all other requesters to finish loading tiles before this one starts.

TreeTraversalState

One of the challenges in moving to multiple view groups is where to put the per-group "selection state". That is, whether a given tile was rendered, refined, culled, or kicked in the last frame. In order to do its job, updateView needs to know what decision was made last frame for each tile that it visits. Previously, when we had only one view group, this was stored on the Tile itself.

A simple approach, like storing a map of "Tile -> Selection State" on the TilesetViewGroup, or a map of "View Group -> Selection State" on the Tile, would work just fine. Unfortunately, it would also be slow. I saw about a 20% increase in the time spent in updateView with this approach!

Instead, this PR uses a new class called TreeTraversalState to track the per-tile states during one traversal and access it during the next one. See #1129 for details about how this works.

CreditReferencer (and changes to CreditSystem)

Credit / attribution tracking gets more complicated with the move to multiple view groups. Previously, CreditSystem worked in a sort of "immediate mode". Each frame (updateView), the credits applicable for the current update would be added to the CreditSystem. For convenience, CreditSystem employed a bit of bookkeeping so that it could report a) the credits active in the current frame, and b) the credits active last frame that are not active anymore.

This whole idea is unfortunately broken when we move to multiple viewports. View groups are not required to tick together, so there's no useful notion of a "frame" for credit purposes.

So CreditSystem has now moved to a "retained mode" pattern, where credits are explicitly enabled and disabled. A reference count tracks the number of times a credit has been enabled, and it won't disappear entirely until it has been disabled an equal number of time. The CreditSystem can produce a "snapshot" on request, which identifies the credits that are currently shown as well as the credits that have been hidden since the last snapshot.

Unfortunately, this makes credit management in updateView tricky. Conceptually, it's not too hard: when a tile is shown, enable its credits; when a tile is hidden, disable its credits. But it's more difficult in practice, because the set of credits associated with a Tile can change over time, such as when it's loaded or a new raster overlay is added. Ensuring that credit enable/disable calls are perfectly paired is error prone, and when it goes wrong it's difficult to debug.

That's why this PR introduces a new type, CreditReferencer. CreditReferencer exposes the old "immediate mode" pattern to the TilesetViewGroup while actually manipulating the retained reference counts that are now owned by CreditSystem.

TilesetViewGroup has two CreditReferencer instances: _previousFrameCredits and _currentFrameCredits.
In TilesetViewGroup::finishFrame, which happens at the end of updateViewGroup, all of the active credits added to the _currentFrameCredits, which indirectly enables them on the CreditSystem. Then, all of the credits in _previousFrameCredits are efficiently disabled. Finally, the two CreditReferencer instances are swapped.

Fixes #928

To do:

  • Every call to updateViewGroup shouldn't return the same ViewUpdateResult from a field on the Tileset. Instead, the ViewUpdateResult could be stored in the TilesetViewGroup. Or perhaps these don't even need to be separate classes?
  • Check performance of the std::unordered_map based tile selection state tracking. My feeling is that this is going to be too slow.
  • Fix and reactivate some code that was temporarily disabled, such as DebugTileStateDatabase.
  • Write unit tests
  • computeLoadProgress needs to take view groups into account. It should be possible to provide an overload that reports the progress for a particular view group, and one that reports overall progress of all view groups.
  • Update tile selection algorithm docs based on these changes.
  • Credit handling
  • Check that updateViewGroupOffline still works, and add a view group version of it.
  • Consider whether Tile::_doNotUnloadSubtreeCount and _referenceCount can be combined into one. (Multiple views: Only store one reference count on Tile #1156)
  • Consider whether LoadedTileEnumerator should continue to enumerate the root tile when no tiles at all are loaded.
  • Consider whether updateView should be deprecated, and users told to use updateViewGroup and loadTiles instead.
  • Check whether tile fade in/out is working. It probably needs to switch to a fade value per view instead of per tile.

kring and others added 30 commits February 12, 2025 16:14
So it can be done outside of cesium-native itself.
@kring
Copy link
Member Author

kring commented Apr 23, 2025

I believe that this PR, plus #1156 and #1160, address all the review feedback and this is ready to go.

Copy link
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.

Thanks @kring for bearing with all my feedback 😄 There might be one outstanding comment in TilesetHeightQuery.cpp, but it's not a big deal, so don't sweat to address it.

I'm approving this PR, though I'm holding off the merge since it's a big change, and we only have a week before the next release. If we feel more confident to include it, let me know!

@j9liu
Copy link
Contributor

j9liu commented Apr 29, 2025

Thanks @kring ! Merging once CI passes

@j9liu j9liu self-assigned this Apr 29, 2025
@j9liu j9liu merged commit e9d767c into main Apr 29, 2025
22 checks passed
@j9liu j9liu deleted the multiple-views branch April 29, 2025 15:24
@j9liu j9liu restored the multiple-views branch May 21, 2025 15:19
@j9liu j9liu deleted the multiple-views branch May 21, 2025 15:32
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.

Support individual viewport settings / LOD selection when using multiple views
2 participants