-
Notifications
You must be signed in to change notification settings - Fork 196
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
tiles: Tile traversal optimizations #1183
Conversation
@Avnerus Really excited to see that you are making such in-depth use of the 3D Tiles loader. Regarding your changes, they make perfect sense though I have to admit that they do require quite a bit of effort and context switching for me to review as it has been a while since I worked on this module, and we currently do not really have an active maintainer of the 3D tiles traversal parts. Since you have done enough research to make changes of this caliber and you are maintaining your own fork of loaders.gl, how would you feel about becoming a maintainer (or even lead) of the 3d tiles module in loaders.gl? As a maintainer you'd get write access to loaders.gl repo, ability to approve PRs, influence direction etc. We also have public open governance meetings and also steering committee meetings etc that you could join at your option. Note that we do have active full-time maintainers on the I3SLoader and also the rest of loaders.gl, it is mainly the 3D tiles area that is poorly covered at the moment, so your scope would be limited. If you are interested in learning more about how this could work, we can chat on the deck.gl forum or contact me via email in my profile. |
Hi @ibgreen, that sounds really interesting. I'll send you an e-mail. |
Added to this PR fixes for empty traversal. The refinement check was not working correctly for nested |
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.
This looks good.
- One of the biggest problems with our traversal is lack of unit tests - which makes any refactor of the already complicated code tricky to review. Any test you could add, no matter how trivial, would help.
- We need to check that these changes work in I3S as well, as the
tiles
module is shared between both formats. You can start by running the I3S example, and @dryabinin-actionengine could also help you test.
@@ -145,6 +144,8 @@ export default class TilesetTraverser { | |||
!skipLevelOfDetail && tile.refine === TILE_REFINEMENT.REPLACE && tile.hasRenderContent; | |||
|
|||
let hasVisibleChild = false; | |||
let refines = true; |
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.
Should we make refinement calculation into a separate helper function? And ideally add a unit test case?
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.
Hi, about a helper function, at least for me it helps to try and mirror the CesiumJS code as much as possible so I can somewhat keep track of the differences.
Unit testing would definitely be good, but I will not have the time to add that soon.
while (stack.length > 0) { | ||
const tile = stack.pop(); | ||
|
||
/* TODO: This doesn't seem needed, should check further. |
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.
Would be great if we can sort this out.
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.
So far, at least in my test cases, it seems to work well. The tiles are updated in the main traversal and I don't see any glitches.
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.
These turned out to be needed after-all in a more complex model.
Regarding I3S, I can confirm at least that the examples here seem to work. but would be good if someone could make a performance comparison before/after the fixes. Also you could try adding |
@belom88 @dryabinin-actionengine Please test on I3S and land this PR asap. |
Looks like traverser requests much more tiles than before during refinement.
https://drive.google.com/file/d/1HJWoSHTf8cViES0AJntC075cpXpObyEY/view?usp=sharing Expected behavior (on master): https://drive.google.com/file/d/1ECQEQ7bhzhAZgwhwbnFAMOng5SdXIg0M/view?usp=sharing |
Thanks, |
4a7e3b7
to
74005c3
Compare
Hi @belom88! |
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.
@Avnerus Thanks for getting back to this. I think it would be great if you could split this into something like 3-4 PRs:
- one simple PR bumping the dependencies and making any necessary changes to the webpack setup etc that we can land quickly (so you can rebase the remaining PRs)
- one PR with the request throttling changes
- one PR with the worker farm changes
- one PR with the more delicate traversal changes
@@ -19,7 +19,6 @@ export default class WorkerFarm { | |||
/** Get the singleton instance of the global worker farm */ | |||
static getWorkerFarm(props: WorkerFarmProps): WorkerFarm; | |||
|
|||
readonly maxConcurrency: number; |
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.
Why is this deleted? This should be specifiable on the farm?
|
||
const maxConcurrency = isMobile ? this.props.maxMobileConcurrency : this.props.maxConcurrency; | ||
|
||
if (workerPool && workerPool.maxConcurrency !== maxConcurrency) { |
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.
Why?
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.
This is code that I still need to revisit, because it worked only before the worker logic was refactored. the goal here is to be able to change the maxConcurrency
in runtime. I found it useful to be able to quickly test different concurrency configurations using the gui.
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.
Perhaps we could make WorkerFarm.setProps({maxConcurrency})
call workerPool.setProps({maxConcurrency}) on all workerPools?
If so, WorkerPools may need some logic to destroy returned workers if the maxConcurrency has been reduced.
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.
Yes this might be the missing piece. I will work on that one later on. Thanks!
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.
Hi @ibgreen!
I revisited this code now and it does actually work. Earlier I just forgot to alias the new worker-utils module.
The getWorkerFarm
method already calls setProps
, so eventually maxConcurrency
propagates from the load options down to the worker pool. Essentially with this code, every time a loader is passed a maxConcurrency
that is different from the existing value, the worker threads are destroyed and then recreated with the updated number.
Do you think this solution can work or do you foresee an issue?
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.
I don't see it.
- The workerFarm just passes the props when it creates a new workerPool, not during workerFarm.setProps.
- I think it could work if workerFarm iterated over all its current workerPools in
setProps
.
Hi @ibgreen, sorry I didn't clarify this. This branch now contains a copy of my fork, which will be split and cleaned. I just wanted to quickly get something out there so @belom88 is able to test the performance on i3s which I now see as the main issue blocking the changes, while I continue working on the PRs. One note about splitting |
Super! |
@Avnerus This is great news. Since we are wrapping up 3.0 it would be fantastic if we can get these changes landed soon. I'll prioritize review of any PRs you open. |
Thank you! l believe by the end of next week I should have the final PRs ready. |
cfb6974
to
1a59877
Compare
@ibgreen and @belom88, this PR now contains only the tile traversal changes. Here is recap and summary of the changes:
Let me know your comments! Thank yo |
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.
Looks pretty clean, given that I3S testing checks out I am fine with this.
@@ -68,8 +68,9 @@ Parameters: | |||
|
|||
- `json`: loaded tileset json object, should follow the format [tiles format](https://loaders.gl/docs/specifications/category-3d-tiles) | |||
- `options`: | |||
- `options.ellipsoid`=`Ellipsoid.WGS84` (`Ellipsoid`) - The ellipsoid determining the size and shape of the globe. | |||
- `options.throttleRequests`=`true` (`Boolean`) - Determines whether or not to throttle tile fetching requests. | |||
- `options.ellipsoid`=`Ellipsoid.WGS84` Determines whether or not to throttle tile fetching requestDetermines whether or not to throttle tile fetching request(`Ellipsoid`) - The ellipsoid determining the size and shape of the globe. |
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.
This looks strange, copy paste?
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.
Yes, fixed!
modules/tiles/src/tileset/tile-3d.js
Outdated
@@ -170,16 +170,32 @@ export default class TileHeader { | |||
} | |||
} | |||
|
|||
// If skipLevelOfDetail is off try to load child tiles as soon as possible so that their parent can refine sooner. |
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.
JSDoc style /** */
shows the comment when hovering in vscode.
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.
Fixed!
modules/tiles/src/tileset/tile-3d.js
Outdated
@@ -461,7 +478,7 @@ export default class TileHeader { | |||
this._centerZDepth = 0; | |||
this._screenSpaceError = 0; | |||
this._visibilityPlaneMask = CullingVolume.MASK_INDETERMINATE; | |||
this._visible = false; | |||
this._visible = null; |
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.
What is the type of _visible? Can it be set to null
? We have a typescript PR about to land.
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.
Would it work as optional _visible?: boolean
?
I used this to indicate when the visibility is not at all updated (headless, tile-covnerter) so the scheduler will still process the request.
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.
If you need, fine. But I certainly wish we didn't need so many optional fields. The firmer we can type a field the better. More variations => more bugs.
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.
Well, another way would be to add a second boolean such as headless
which would be falsified when visibility is updated, but I am not sure if it is more readable.
How about using undefined
and defining in Typescript:
_visible: boolean | undefined
? I'm not sure what's the standard way to type variables that have an optional value (like this Rust equivalent) or this one).
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.
Sure. If it is only this field that needs three states, then _visible?: boolean
would work as opposed to having two fields just for this. However if we need to return this to the undefined
state at some point, I'd rather use null
(_visible: boolean | null
.
If there were multiple fields that could be controlled by one common field that indicates whether things are initialized, then I'd personally separate it out.
This is all moot before we actually land the typescript branch.
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.
OK. I think then we can stick with _visible?: boolean
for this one. Seems readable enough to me.
@@ -551,7 +555,7 @@ export default class Tileset3D { | |||
} | |||
|
|||
_unloadTile(tile) { | |||
this.gpuMemoryUsageInBytes -= tile.content.byteLength || 0; | |||
this.gpuMemoryUsageInBytes -= (tile.content && tile.content.byteLength) || 0; |
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.
It would be good to maintain a gpuMemoryUsageInBytes
on the tile so we didn't need to rake into its internals here. I am not sure content
would usually be a typed array.
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.
I am not sure what you mean? All I added here was a check that tile.content
exists (I experienced a race condition in which the content was already gone) .
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.
Sorry I see that I wasn't reviewing your change, I was reviewing the original code.
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.
Ah ok. we can then maybe handle this later on.
modules/tiles/src/tileset/tile-3d.js
Outdated
return -1; | ||
} | ||
if (this.contentState === TILE_CONTENT_STATE.UNLOADED) { | ||
return -1; | ||
} | ||
|
||
return Math.max(1e7 - this._priority, 0) || 0; | ||
const parent = this.parent; | ||
const useParentScreenSpaceError = |
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.
If you are able to add some more comments (given that you have spent time understanding the algorithm) that would be nice. Imagine the questions a new person would have when looking at this code.
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.
Added some comments.
@ibgreen Added another change which I understood should be attached to this PR. |
a8b6161
to
24fb694
Compare
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.
- Cannot build the website on this branch;
- If I try to do
git merge master
, there are conflicts
Hi! this is a long one.
As part of our work on Cesium 3d tiles with photogrammetry and three.js I ended up doing some changes to the loaders.gl tiles code which I detail in this PR:
Consolidate the tile
getPriority
function:When working with heavy street-view-style photogrammetry tilesets, I always had to use
requestThrottling
, or the browser could not handle the amount of tiles being loaded. However, the priority function used for the request scheduler was using onlylodMetricValue
, not taking into account the distance to the camera. A priority function which resembles more the CesiumJS function and takes both sse and camera distance into account was used in the traversal code when loading the tile, I therefore moved that code to the tile and use that one instead, both on tile load and for request scheduling.Regarding Skip VS Base traversal, we do not yet have a skip traversal implementation in three.js, which requires the use of the stencil buffer for tile depth (do you by the way have an implementation in deck.gl or other?). However, I have done some tests and noticed that there might be some lines in the traversal and priority functions that are specific for either skip or base traversal:
loaders.gl/modules/tiles/src/tileset/traversers/tileset-traverser.js
Lines 172 to 174 in 480b38a
This line breaks the iteration of a parent tile if at least one child is not yet refined. I think this is specific for base traversal because in skip traversal you might want to load a certain child even if siblings are not loaded. This break does not happen in the original CesiumJS code. I then changed it to:
And then here:
loaders.gl/modules/tiles/src/tileset/tile-3d.js
Lines 160 to 164 in 480b38a
This line aborts the loading of a tile if it is not visible, but in base traversal you might need to load invisible tiles or else the parent would not refine. I had to change it to:
So I could enable
requestThrottling
.contentState
process and decide myself when the tiles are definitely ready once all of the three.js objects are created (can be after an asynchronous gltf parsing operation). This intervening introduced some race conditions in which a tile's content was being accessed even after it was destroyed. Perhaps the proper way to fix this is to support a user definedPromise
which defers the tile'scontentState
setting ofREADY
orUNLOADED
, but in the meantime I think it is enough to have those race conditions checks.No urgency on this one as we are using our fork for now, but would appreciate your comments on these fixes.
Thank you!
/Avner