-
Notifications
You must be signed in to change notification settings - Fork 242
Add support for selecting tiles for multiple independent views #1125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
So it can be done outside of cesium-native itself.
…ews-one-reference-count
There was a problem hiding this 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!
…nt' into multiple-views-more
…ount Multiple views: Only store one reference count on `Tile`
Multiple views: Some final improvements from review
Thanks @kring ! Merging once CI passes |
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 aTileset
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 byTileset
and can be obtained by callinggetDefaultViewGroup()
.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 themaximumSimultaneousTileLoads
property inTilesetOptions
.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 whyloadTiles
is a separate function onTileset
now, and must be called explicitly by the user when utilizingupdateViewGroup
. For backward compatibility,updateView
callsloadTiles
automatically after updating the default view group.LoadedLinkedList
➡️UnusedLinkedList
Previously, Tiles that were traversed during
updateView
were added to an intrusiveLoadedLinkedList
on theTileset
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 anIntrusivePointer<const Tile>
for every tile that it visited during the lastupdateViewGroup
. This intrusive pointer increments/decrements the_referenceCount
field on theTile
. This is separate from and in addition to_doNotUnloadSubtreeCount
that was added in #1107. It differs in two ways:_referenceCount
keeps tile content from unloading._doNotUnloadSubtreeCount
only keeps subtrees from unloading andTile
instances from being deleted, but tile content is allowed to unload._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, theTile
is added to the tail of the new, intrusiveUnusedLinkedList
. If the_referenceCount
goes back above zero, it is removed from theUnusedLinkedList
. 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 byLoadedTileEnumerator
. 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 newTileLoadRequester
abstract base class.Anything that wants to influence which tiles are loaded should inherit from
TileLoadRequester
. Currently that means two types:TilesetViewGroup
andTilesetHeightRequest
(which is created by a call tosampleHeightMostDetailed
).A
TileLoadRequester
may be registered with theTileset
by callingTileset::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 hasgetWeight
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 thegetWeight
method: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 theTile
itself.A simple approach, like storing a map of "Tile -> Selection State" on the
TilesetViewGroup
, or a map of "View Group -> Selection State" on theTile
, would work just fine. Unfortunately, it would also be slow. I saw about a 20% increase in the time spent inupdateView
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 toCreditSystem
)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 theCreditSystem
. 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. TheCreditSystem
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 aTile
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 theTilesetViewGroup
while actually manipulating the retained reference counts that are now owned byCreditSystem
.TilesetViewGroup
has twoCreditReferencer
instances:_previousFrameCredits
and_currentFrameCredits
.In
TilesetViewGroup::finishFrame
, which happens at the end ofupdateViewGroup
, all of the active credits added to the_currentFrameCredits
, which indirectly enables them on theCreditSystem
. Then, all of the credits in_previousFrameCredits
are efficiently disabled. Finally, the twoCreditReferencer
instances are swapped.Fixes #928
To do:
updateViewGroup
shouldn't return the sameViewUpdateResult
from a field on theTileset
. Instead, theViewUpdateResult
could be stored in theTilesetViewGroup
. Or perhaps these don't even need to be separate classes?std::unordered_map
based tile selection state tracking. My feeling is that this is going to be too slow.DebugTileStateDatabase
.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.updateViewGroupOffline
still works, and add a view group version of it.Tile::_doNotUnloadSubtreeCount
and_referenceCount
can be combined into one. (Multiple views: Only store one reference count onTile
#1156)LoadedTileEnumerator
should continue to enumerate the root tile when no tiles at all are loaded.updateView
should be deprecated, and users told to useupdateViewGroup
andloadTiles
instead.