CesiumGS / cesium-native

Apache License 2.0
416 stars 210 forks source link

Tileset and Tile Content Loader Refactor Overview #481

Closed baothientran closed 2 years ago

baothientran commented 2 years ago

Why we need to refactor our tile and tileset content loader?

What is the goal of the refactor?

Proposed architecture change

Another Benefits of the Changes

That's it for now. I'm still thinking about the design as we go on, so things will change though. I also know it requires massive change in the library itself, so it may not be planned soon or thing may not work as expected

kring commented 2 years ago

Thanks for writing this up Bao! I need to read it in detail, but I noticed one problem in quick read:

Tile::requestContent() does upsampling for quantized mesh, but the code seems to never be used for normal tileset, implicit quadtree, and octree

Upsampling is only needed when raster overlays are attached. If you attach a raster overlay to a regular tileset.json-driven tileset, you'll see that upsampling happens for it, too. It's not specific to quantized-mesh at all.

kring commented 2 years ago

We should handle other similar types of tileset in the future as well like S2 implicit tree

S2 implicit trees are already supported in the current code.

baothientran commented 2 years ago

Oh I didn't know upsampling happens for all tileset. It makes sense since raster overlay makes tile create children with 4 UpsamplesQuadtree id

kring commented 2 years ago

@baothientran I think this is really sensible, and a great improvement over what we're doing now. It also doesn't sound too drastic. A lot of code will change, but mostly to move around. So as far as refactorings go, it's not terribly scary. I'm curious if you have a rough idea how long you might need to implement this. And what help from others on the team (if any) you would need. @nithinp7 I'm also interested in your take if you have a little time to review this.

requestTileContent(Tile &tile) is for ... loading the content of a tile

What about calling it loadTileContent? It may or may not request the content, e.g. if the content is procedural. Plus symmetry with unloadTileContent.

A requirement for PostProcessor is that we should never allow it to modify the Tile's State. Only Tileset Loader itself is the only one does that.

I'm curious about your thinking behind having the loader manage the state transition. It seems like the loaders should be focused on loading, i.e. turning whatever the wire format is into tiles and glTFs. It would be nice if an MSL loader didn't need to concern itself with any details other than creating an MSL glTF when asked.

So in that scenario, I guess the Tile or Tileset would manage the state transitions, and just notify the loader along the way. What do you think?

One thing that strikes me is the similarity in the PostProcessor and TileContentLoader interfaces. The fundamental difference is that Tile Content Loader creates a glTF, while a Post Processor just operates on a previously created one. But that isn't really reflected in the interface. So maybe it's worth considering if the TileContentLoader could just be the first postprocessor, rather than something different? It'd need a different name at that point, though.

baothientran commented 2 years ago

What about calling it loadTileContent? It may or may not request the content, e.g. if the content is procedural. Plus symmetry with unloadTileContent.

👍

I'm curious about your thinking behind having the loader manage the state transition. It seems like the loaders should be focused on loading, i.e. turning whatever the wire format is into tiles and glTFs. It would be nice if an MSL loader didn't need to concern itself with any details other than creating an MSL glTF when asked.

My initial thought was that since the loader requests the tile content, it probably knows when the tile is done as well. But as you mention, it's will be very bad to let derived loaders manage those state changes due to inconsistency in behavior and code duplications in the derived class

So in that scenario, I guess the Tile or Tileset would manage the state transitions, and just notify the loader along the way. What do you think?

That certainly works. I think Tileset should be the one to drive the transition in this case. The reason is that TilesetContentLoader uses the Tile in its interface, so the Tile itself shouldn't use the loader internally to prevent cyclic dependency. That also makes the Tile lightweight as well

void Tileset::requestTile(Tile &tile) {
    tile.setState(LoadState::Loading);
    tilesetLoader.loadTileContent(tile).thenInMainThread([&tile](auto content){
          tile.setState(LoadState::ContentLoaded);
    });
}

void Tileset::updateTile(Tile &tile) {
    if (tile.getState() == LoadState::ContentLoaded) {
         tilesetLoader.processLoadedContent(tile);
         tile.setState(LoadState::Done);
    }
    else {
         // tilesetLoader may own raster overlay and the overlay tile needs to be updated
        // per frame
         tilesetLoader.updateTileContent(tile);
    }
}

Another idea I have is to use templated methods pattern. That is we can move those codes into the TilesetContentLoader base class, and derived class is required to implement private virtual functions:

I don't have strong preferences between them though. What do you think?

One thing that strikes me is the similarity in the PostProcessor and TileContentLoader interfaces. The fundamental difference is that Tile Content Loader creates a glTF, while a Post Processor just operates on a previously created one.

I didn't notice this, but that's true. I agree that merging them together into one single interface like you suggest is a better option, and less interface for client to remember

nithinp7 commented 2 years ago

I think this is a good idea, the refactor is well-motivated and the proposed architecture looks promising. A few notes / questions:

baothientran commented 2 years ago

What is the DefaultTilesetLoader, is it for loading tileset.json?

Yup it's for normal tileset.json

How does TileContext fit into this diagram? Will it also be virtualized into loader-specific contexts?

it will be removed as it contains the info of tileset a tile belongs to, and is used to load the tile accordinly. So it's the TilesetContentLoader all along

I don't know if this affects anything, but currently there is a coupling between Tile and raster tile load states, since isRenderable requires all rasters to be done loading for the tile. I wonder if that will matter when trying to pull raster overlay handling into a decoupled PostProcessor?

Good question, I haven't thought much about it, but will give more thought on it

Just for my understanding regarding the PostProcessor interface, what are some potential uses of the PostProcessor other than raster overlays? What motivates the abstraction there instead of the current raster overlay-specific code?

I can imagine raster overlay is not the only one that can be used to modify the tileset content. Later on, we can add more modifier into the tileset (glTF batching, meshopt, etc), and it's the goal that we/clients won't have to modify the internal code to do it. So planning it early to have designated customization point seems to be reasonable to me

lilleyse commented 2 years ago

@baothientran out of curiosity are you planning on supporting multiple contents in the tile loading refactor?

baothientran commented 2 years ago

It's probably after the refactoring since I want to make sure existing features work first

javagl commented 2 years ago

It seems like some of the points from the "Discussion about the traversal process" are going to be addressed here. Particularly, the statement from the initial post here...

The state machine for keeping track of the tile loading process is leaked out to the Tileset class. This should be encapsulated within the tile itself, and never exposed to the client. Otherwise, we will never know who change the state of whom, and if not careful, will leak resources

seems to be a summary of some points from the other issue:

  • Get rid of global state accesses and global state changes during traversal, i.e. the this->options and the three load queues....
  • Make the state clearer. When a Tile is in a certain Tile::LoadState (or a RasterOverlayTile is in a certain RasterOverlayTile::LoadState...), or a tile is in a certain TileSelectionState, it must be clear
    • How did the tile get into this state?
    • What does that state say about the properties/fields of the tile?
    • What are the conditions for a tile going into another state?
    • What is the (in the best case: single) method that changes that state?
    • Which thread is causing the state change?

The last point is a crucial one. I might have missed it in the description above - I'm not really up to date with the codebase and plans - but: Will the refactor include a documentation about the threads on which state changes do take place or may take place? "Knowing" the code base or looking at a call stack and seeing that certain functions are called via thenInSomeThread is not sufficient here. I think that it is crucial to document the thread constraints for state transitions, on the method-level as well as in higher-level documentation like diagrams, to reduce the headaches of debugging race conditions...

baothientran commented 2 years ago

@javagl The only thread that does the state transition is the main thread and that main thread task has to be resided within TilesetContentManager. The only class that can does the transition is the TilesetContentManager which manages all aspect of tile content loading. Other classes including Tileset cannot do that transition.

In the TilesetContentManager, the state machine of a tile begins by calling loadTileContent(). Calling updateTileContent() will transition the tile to the next state, and calling unloadTileContent() will finish the state machine of a tile. That is the only three methods that do the transition for the tile.

I think that it is crucial to document the thread constraints for state transitions, on the method-level as well as in higher-level documentation like diagrams, to reduce the headaches of debugging race conditions...

I agree. I will try to document all of it as much as possible

baothientran commented 2 years ago

This is now merged to main. I'm closing now