-
Couldn't load subscription status.
- Fork 244
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.
TilesetViewGroupRepresents a set of views that select tiles together. The actual views (
ViewStateinstances) 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?
TilesetViewGroupcan 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➡️updateViewGroupPreviously, users would call the
updateViewmethod on aTilesetand 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 byTilesetand can be obtained by callinggetDefaultViewGroup().However, when using view groups, it is recommended to instead use the new
updateViewGroupfunction. 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
Tilesetthen 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 themaximumSimultaneousTileLoadsproperty 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 whyloadTilesis a separate function onTilesetnow, and must be called explicitly by the user when utilizingupdateViewGroup. For backward compatibility,updateViewcallsloadTilesautomatically after updating the default view group.LoadedLinkedList➡️UnusedLinkedListPreviously, Tiles that were traversed during
updateViewwere added to an intrusiveLoadedLinkedListon theTilesetcalled_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
LoadedLinkedListis gone in this PR.Instead,
TilesetViewGroupnow holds anIntrusivePointer<const Tile>for every tile that it visited during the lastupdateViewGroup. This intrusive pointer increments/decrements the_referenceCountfield on theTile. This is separate from and in addition to_doNotUnloadSubtreeCountthat was added in #1107. It differs in two ways:_referenceCountkeeps tile content from unloading._doNotUnloadSubtreeCountonly keeps subtrees from unloading andTileinstances from being deleted, but tile content is allowed to unload._referenceCountis 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
_referenceCountgoes to zero, theTileis added to the tail of the new, intrusiveUnusedLinkedList. If the_referenceCountgoes 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.
LoadedTileEnumeratorThe
LoadedLinkedListwas 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
LoadedLinkedListnow 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_doNotUnloadSubtreeCountgreater 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.
TileLoadRequesterI mentioned above that
Tilesetuses a weight round-robin algorithm to give multiple view groups a fair chance to load tiles. This happens via the newTileLoadRequesterabstract base class.Anything that wants to influence which tiles are loaded should inherit from
TileLoadRequester. Currently that means two types:TilesetViewGroupandTilesetHeightRequest(which is created by a call tosampleHeightMostDetailed).A
TileLoadRequestermay be registered with theTilesetby 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 hasgetWeightmethod 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 thegetWeightmethod:TreeTraversalStateOne 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,
updateViewneeds 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 theTileitself.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 inupdateViewwith this approach!Instead, this PR uses a new class called
TreeTraversalStateto 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,
CreditSystemworked in a sort of "immediate mode". Each frame (updateView), the credits applicable for the current update would be added to theCreditSystem. For convenience,CreditSystememployed 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
CreditSystemhas 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. TheCreditSystemcan 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
updateViewtricky. 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 aTilecan 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.CreditReferencerexposes the old "immediate mode" pattern to theTilesetViewGroupwhile actually manipulating the retained reference counts that are now owned byCreditSystem.TilesetViewGrouphas twoCreditReferencerinstances:_previousFrameCreditsand_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_previousFrameCreditsare efficiently disabled. Finally, the twoCreditReferencerinstances are swapped.Fixes #928
To do:
updateViewGroupshouldn't return the sameViewUpdateResultfrom a field on theTileset. Instead, theViewUpdateResultcould be stored in theTilesetViewGroup. Or perhaps these don't even need to be separate classes?std::unordered_mapbased tile selection state tracking. My feeling is that this is going to be too slow.DebugTileStateDatabase.computeLoadProgressneeds 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.updateViewGroupOfflinestill works, and add a view group version of it.Tile::_doNotUnloadSubtreeCountand_referenceCountcan be combined into one. (Multiple views: Only store one reference count onTile#1156)LoadedTileEnumeratorshould continue to enumerate the root tile when no tiles at all are loaded.updateViewshould be deprecated, and users told to useupdateViewGroupandloadTilesinstead.