Skip to content
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

Draft for handling shared textures in statistics #12331

Merged
merged 10 commits into from
Nov 27, 2024

Conversation

javagl
Copy link
Contributor

@javagl javagl commented Nov 23, 2024

Addresses #11897

Description

Issue summary

An attempt of a tl;dr of the issue #11897 :

Cesium keeps track of the total size of resources that are currently kept in memory when loading a tileset, including the size of textures that are loaded. When the total memory consumption exceeds a certain threshold, then the "maximum screen space error" is increased, causing some tiles to be unloaded, to free up memory. This is indicated by a console message

The tiles needed to meet maximumScreenSpaceError would use more memory than allocated for this tileset.
The tileset will be rendered with a larger screen space error (see memoryAdjustedScreenSpaceError).
Consider using larger values for cacheBytes and maximumCacheOverflowBytes.

These statistics do not properly handle the case where a tileset contains GLB files that refer to the same textures (PNG or JPG) via URIs. In this case, the memory requirements for the textures have been tracked for each GLB that referred to the texture, even though the texture was only loaded once.

Approach

Some options for resolving this have already been mentioned in a comment to the issue. And this PR is a DRAFT that shows how this could be resolved.
("DRAFT means: Don't look at the code yet... it's littered with debug logs...)

The main changes are the following:

  • Originally, each Texture object had a (random) UUID. Now it can receive a custom ID. When the texture is loaded with a GltfTextureLoader, then it receives its "cacheKey" as the ID. This is as unique as the texture loader, and may be a string like texture:http://localhost:8003/texture1.png-sampler-10497-10497-9729-9729-context-a4d9a28d-3340-4e39-8cbf-76f8423e1643
  • The ModelStatistics originally contained a set of all texture IDs of the model. Now, this set is actually a mapping from the texture ID to the byte length of that texture
  • The Model3DTileContent offers two functions: One for obtaining the IDs of all textures that are contained in the model (via its statistics), and one for looking up the byte length of that texture
  • The Cesium3DTilesetStatistics examines the Model3DTileContent, to properly update the global statistics...

The last point is the most important one:

The Cesium3DTilesetStatistics has a function updatePointAndFeatureCounts. When a tile is loaded or unloaded, this function receives a Cesium3DTileContent, and updates the global statistics based on the size information from this tile content.

Now, this function dedicatedly checks whether the given content is a Model3DTileContent. If it is, then it examines the texture IDs that are stored in this content. It internally maintains a "reference counter" for each texture ID, and updates its total texturesByteLength accordingly:

  • When a tile content is loaded ...
    • it increases the reference counter for all its texture IDs by 1
    • for each newly found texture ID, the texture byte size is added to its total texturesByteLength
  • When a tile content is unloaded...
    • it decreases the reference counter for all its texture IDs by 1
    • for each texture ID that is no longer used, the texture byte size is subtracted from its total texturesByteLength

Results

I inserted some debug logs, and excerpts of the relevant ones are shown here:


For the "ExternalTextureSharing-2024-03-25.zip" example that was attached in the issue:

The old output was this:

After load   statistics.texturesByteLength now 16777216
After load   statistics.texturesByteLength now 33554432
After load   statistics.texturesByteLength now 50331648
After load   statistics.texturesByteLength now 67108864
...
After load   statistics.texturesByteLength now 1056964608
After load   statistics.texturesByteLength now 1073741824
The tiles needed to meet maximumScreenSpaceError would use more memory than allocated for this tileset.
    The tileset will be rendered with a larger screen space error (see memoryAdjustedScreenSpaceError).
    Consider using larger values for cacheBytes and maximumCacheOverflowBytes.
After unload statistics.texturesByteLength now 1056964608
After load   statistics.texturesByteLength now 1073741824
After unload statistics.texturesByteLength now 1056964608
After unload statistics.texturesByteLength now 1040187392
...

It loaded the textures, and at some point, exceeded the memory, printing the warning message, and falling into a ping-pong of loading and unloading tiles.

The new output is this:

After load   statistics.texturesByteLength now 0
After load   statistics.texturesByteLength now 0
After load   statistics.texturesByteLength now 16777216
After load   statistics.texturesByteLength now 33554432
After load   statistics.texturesByteLength now 33554432
...
After load   statistics.texturesByteLength now 33554432
After load   statistics.texturesByteLength now 33554432

It loads the two textures, and ... that's it.


For another tileset, the old output was this:

After load   statistics.texturesByteLength now 11184810
After load   statistics.texturesByteLength now 36350633
After load   statistics.texturesByteLength now 36350633
After load   statistics.texturesByteLength now 36350633
After load   statistics.texturesByteLength now 47535443
...
After load   statistics.texturesByteLength now 926242113
After load   statistics.texturesByteLength now 993350976
The tiles needed to meet maximumScreenSpaceError would use more memory than allocated for this tileset.
    The tileset will be rendered with a larger screen space error (see memoryAdjustedScreenSpaceError).
    Consider using larger values for cacheBytes and maximumCacheOverflowBytes.
After unload statistics.texturesByteLength now 993350976
After unload statistics.texturesByteLength now 982166166
...
After unload statistics.texturesByteLength now 959796545
After load   statistics.texturesByteLength now 1082829460
After unload statistics.texturesByteLength now 976573761
After unload statistics.texturesByteLength now 897930563
After unload statistics.texturesByteLength now 819287365
After load   statistics.texturesByteLength now 919950659
After load   statistics.texturesByteLength now 964689901
After load   statistics.texturesByteLength now 964689901
After load   statistics.texturesByteLength now 1020613953
After unload statistics.texturesByteLength now 964689901
After load   statistics.texturesByteLength now 1020613953
After unload statistics.texturesByteLength now 964689901
After load   statistics.texturesByteLength now 975874711
After unload statistics.texturesByteLength now 964689901
After load   statistics.texturesByteLength now 975874711
After load   statistics.texturesByteLength now 975874711
...

The same pattern: It loads the textures, prints the message, and then helplessly loads and unloads data...

The new output:

After load   statistics.texturesByteLength now 0
...
After load   statistics.texturesByteLength now 11184810
After load   statistics.texturesByteLength now 55924052
After load   statistics.texturesByteLength now 78643198
After load   statistics.texturesByteLength now 101012819
...
After load   statistics.texturesByteLength now 509607925
After load   statistics.texturesByteLength now 509607925
The tiles needed to meet maximumScreenSpaceError would use more memory than allocated for this tileset.
    The tileset will be rendered with a larger screen space error (see memoryAdjustedScreenSpaceError).
    Consider using larger values for cacheBytes and maximumCacheOverflowBytes.
After unload statistics.texturesByteLength now 509607925
After unload statistics.texturesByteLength now 509607925
After unload statistics.texturesByteLength now 509607925
...

It still does print the message (because the textures just are too large...). But it prints it much later, and after unloading some data, it remains stable.


An example of dumping out the reference counters for the textures in this tileset after an update (shortened IDs here) is

After load   statistics.texturesByteLength now 302339406
Details:
  referenceCounter 2 for texture:https://[omitted]stones_simC_color_brown_mod.jpg...
  referenceCounter 2 for texture:https://[omitted]hickory_%20natural.jpg...
  referenceCounter 3 for texture:https://[omitted]metals.ornamental%20metals.plate.mesh.1.bump.jpg...
  referenceCounter 8 for texture:https://[omitted]concrete_cast-in-place_color.jpg...
  referenceCounter 16 for texture:https://[omitted]wood_birch_color.jpg...
  referenceCounter 4 for texture:https://[omitted]Woods%20&%20Plastics.Finish%20Carpentry.Wood.Red%20Birch.jpg...
  referenceCounter 8 for texture:https://[omitted]quartz_snow_color.jpg...
  referenceCounter 8 for texture:https://[omitted]polished_concrete_color_brown.jpg...
  referenceCounter 8 for texture:https://[omitted]plywood_maple_color.jpg...
  referenceCounter 2 for texture:https://[omitted]polystyrene3_color.jpg...
  referenceCounter 4 for texture:https://[omitted]metal_panels_color_brown.jpg...
  referenceCounter 8 for texture:https://[omitted]metal_panels_color_white.jpg...
  referenceCounter 2 for texture:https://[omitted]concrete_aggregate_polished_color.jpg...
  referenceCounter 6 for texture:https://[omitted]concrete_precast-smooth-ext_color.jpg...
  referenceCounter 3 for texture:https://[omitted]red_oak_espresso_floor_color.jpg...
  referenceCounter 2 for texture:https://[omitted]EPDM_color.jpg...
  referenceCounter 2 for texture:https://[omitted]flooring_slate_color_dark_green.jpg...
  referenceCounter 2 for texture:https://[omitted]surface_concrete_color.jpg...
  referenceCounter 2 for texture:https://[omitted]grass_raw_field_color.jpg...
  referenceCounter 1 for texture:https://[omitted]metal_panels_color_light-grey.jpg...
  referenceCounter 1 for texture:https://[omitted]asphalt_uniform_color_light.jpg...

Showing that there are, for example, 16 GLB files that use the wood_birch_color.jpg, but it is only counted once in the statistics.


TODO:

The way of how this information is currently handled in the Cesium3DTilesetStatistics is not so nice. What bugs me most is that if (content instanceof Model3DTileContent) there. But there are basically two options:

  • Add some "dummy" methods for obtaining texture IDs and sizes to each implementation of Cesium3DTileContent
  • Use that instanceof

Both are not nice. The latter is "less 'not-nice'".

More generally, about the updatePointAndFeatureCounts: I did insert some JSDoc there. But when it's necessary to write such a long comment, then it's clear that the method should be refactored. I'd do this when wrapping up this PR (together with all the cleanups and comments that have to be done at other places that are changed here)

Issue number and link

#11897

Testing plan

Load the example tileset that was posted in the issue, and notice that all tiles are loaded, it does not print the "The tiles ... would use more memory" message.

Do the same with any other tileset that has the structure of many GLBs that refer to the same textures.

Author checklist

  • I have submitted a Contributor License Agreement
  • I have added my name to CONTRIBUTORS.md
  • I have updated CHANGES.md with a short summary of my change
  • I have added or updated unit tests to ensure consistent code coverage
  • I have updated the inline documentation, and included code examples where relevant
  • I have performed a self-review of my code

Copy link

Thank you for the pull request, @javagl!

✅ We can confirm we have a CLA on file for you.

@lilleyse
Copy link
Contributor

@javagl the approach here seems reasonable. I briefly looked at the code but will wait until it's ready for a deeper review.

I agree that if (content instanceof Model3DTileContent) is unfortunate but probably the best option.

@javagl
Copy link
Contributor Author

javagl commented Nov 25, 2024

I did a cleanup pass.

And wanted to add some specs.

For that, I created a smaller/simpler version of the example that was posted in the issue. And I wanted to insert a test that resembles an existing one. This test should do some basic santity checks for the underlying reference counting...

  • Look nowhere, and confirm that there is nothing loaded
  • Look at the first tile, and confirm that there is one texture loaded
  • Look at the first two tiles, and confirm that there are two textures loaded
  • Look at all tiles, and confirm that still only two textures are loaded
  • Look at the last tile (and call some "prune/cleanup") and confirm that there is only one texture loaded

I'm setting the camera to a fixed, hard-wired position that (in a Sandcastle) definitely shows the tiles....

Cesium Texture Sharing Spec 001

But in the specs, it simply doesn't load anything (i.e. Cesium3DTilesTester.waitForTilesLoaded always finishes with 0 tiles loaded). That may be some reallyTryToLoadThings = true that is missing somewhere - but it's nearly impossible to figure out what exactly is wrong there...

@javagl
Copy link
Contributor Author

javagl commented Nov 25, 2024

However, I pushed the stub of the spec. Maybe someone knows why no tiles are rendered at 32dd865#diff-d583bed57d63fe55e0948b41083f208cf988cca081e41845659480a3ccbfefc8R997 . Otherwise, there will be a lot of trial-and-error here...

@javagl
Copy link
Contributor Author

javagl commented Nov 26, 2024

The cleanup had a pending commit with a fix for a wrong increment/decrement and a log output that was supposed to be removed. This commit was added now.

About the specs... well, that was painful 🤕

It took quite a while until I figured out why the specs ~"just didn't work". Eventually, this was resolved by adding a
camera.lookAtTransform(Matrix4.IDENTITY);
in the specs. Apparently, calling lookAt brings the camera into an ~"inconsistent state". And the camera can not be "made consistent" by assigning all the properties that should define it. So without this line, the camera was oriented properly, according to all its public properties. But some of its private properties (like _actualTransform and such) just had "wrong values" that didn't match these properties. This is ... suprising, at the very least, but I'd consider it as a bug, and will try to carve that out into a dedicated issue with a clear description of the actual-vs-expected behavior.

That said, the tests are passing now. Screenshots from the spec "debug canvas" are here for illustration:

It starts with a view where nothing is visible, resulting in 0 bytes for textures and geometry.

Then it moves into this view:

Cesium Texture Stats Spec 0001

This causes one geometry and one texture to be loaded.

Then it moves to this view:

Cesium Texture Stats Spec 0002

This causes four geometries but only two(!) textures to be loaded.

Then it moves back to the previous (single-tile) view, trims all loaded tiles, and verifies that it now again has only one geometry and one texture.

@javagl javagl marked this pull request as ready for review November 26, 2024 20:06
@lilleyse
Copy link
Contributor

It still does print the message (because the textures just are too large...). But it prints it much later, and after unloading some data, it remains stable.

I see this as well, but I also have a KTX2 version of the model where memory usage stays within more reasonable limits and doesn't print the message. So seems like things are working as expected.

I also confirmed that statistics and caching are working correctly for tilesets that doesn't have shared texture references.

Nothing else to add, the approach looks good to me.

@lilleyse lilleyse added this pull request to the merge queue Nov 27, 2024
Merged via the queue into main with commit c403b50 Nov 27, 2024
@lilleyse lilleyse deleted the fix-resource-statistics-draft branch November 27, 2024 01:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants