CesiumGS / cesium-unreal

Bringing the 3D geospatial ecosystem to Unreal Engine
https://cesium.com/platform/cesium-for-unreal/
Apache License 2.0
902 stars 287 forks source link

Further reduce texture memory usage by clearing pixelData in glTF images after creating Unreal textures. #1330

Closed kring closed 5 months ago

kring commented 7 months ago

This depends on CesiumGS/cesium-native#804 and CesiumGS/cesium-native#806 so merge them first.

After we create an Unreal texture from a glTF ImageCesium, we "mostly" don't need the pixelData anymore. Clearing it saves a lot of memory - an entire copy of the raw, uncompressed pixel data. This is an improvement comparable to #1329 again, and it applies even to Windows (unlike that PR).

So that's what this PR set out to do, but it kind of got away from me and ended up being a pretty substantial overall of how textures get created in Unreal. There are a bunch of benefits:

  1. In all cases except metadata textures, we now free the CPU-side copy of the texture data once we've handed it off to Unreal. This was the original goal, and is an enormous reduction in texture memory usage. It would be easy to do this for metadata textures, too, but the assumption is that we (sometimes? always?) need access to that data on the CPU.
  2. Metadata-related textures are now created using the same code path as all the other textures. Previously they used a separate, slower path. (note: I don't expect any major surprises here, but I'm not sure I was able to test every variation of a metadata texture. Bugs are possible, so some attention on this in review would be much appreciated!)
  3. We avoid an extra copy of the texture data in a worker thread on non-Windows platforms. Previously, every time we created a texture on non-D3D platforms, we made one copy of the pixel data in a worker thread, and then copied it again in the renderer thread. Now we only copy it once, in the renderer thread. On D3D, we also make one copy, but in that case that one copy is made only in the worker thread, which is better (this didn't change in this PR).
  4. When multiple glTF texture instances reference a single image, we now only create one copy of the texture data on the GPU. Previously we'd upload the image multiple times. This isn't super common, but it's a big win when it happens. Probably the most notable place it happens is when creating upsampled tiles for raster overlays. Previously the upsampled children would each get a full copy of the parent's textures. Now, they all share the parent's textures.

Some other notes:

  1. Metadata textures are now created in TEXTUREGROUP_8BitData instead of TEXTUREGROUP_World. This is a little bit arbitrary, but I think it's an improvement.
  2. Previously, the size of the pixelData was used to determine the size of texture data for caching purposes. Now that's pretty much always going to be zero, so the new ImageCesium::sizeBytes field (see CesiumGS/cesium-native#804) is used to track the size instead.
kring commented 7 months ago

I'm using TextureSettingsTest to test support for multiple textures with different sampler settings referencing a single image. I'm using this tileset.json to display it:

{
  "asset": { "version": "1.0" },
  "root": {
    "boundingVolume": {
      "sphere": [
        0,0,0,10
      ]
    },
    "children": [],
    "content": { "uri": "TextureSettingsTest.glb" },
    "geometricError": 0.0,
    "refine": "REPLACE"
  }
}

This is as close as we get to working, because double-sided materials are not supported in Cesium for Unreal:

image

It's doing a GPU copy of the texture data. There are edge cases, though. What if the first texture didn't have mipmaps, but a later one does? Ugh.

csciguy8 commented 6 months ago

@j9liu , if you have the time, can you help with a sanity check / test on anything that changed related to metadata?

I've already reviewed the dependent cesium-native changes, tested the unreal samples, and am looking at rest of the code the now.

csciguy8 commented 6 months ago

@kring any thoughts on how to test this properly?

I see all the unit tests passed, which is good, but I'm not seeing any unit tests that target texture loading / texture memory specifically.

I've also run through all the Unreal samples, in Windows, but don't have a non-windows device to check against.

As for code review, looks like a lot of "overhaul" type code, like you stated. Can definitely look for obvious issues, but there's a chance that anything subtle and non-obvious can slip through here.

csciguy8 commented 6 months ago

@kring One more thing, do you have any numbers of showing how much texture memory was saved?

Ex. In cesium-unreal-samples, map 02_CesiumMelbourne, I saw memory usage go from X to X, a X% reduction.

kring commented 6 months ago

I see all the unit tests passed, which is good, but I'm not seeing any unit tests that target texture loading / texture memory specifically.

It'll be extremely difficult to write tests for. We'd have somehow confirm that the GPU texture has been populated correctly. I'm open to ideas, but it doesn't seem worth the effort when it's very obvious very quickly just by looking if textures aren't being created successfully.

One thing to watch for is memory leaks. I've put a lot of effort into convincing myself there are none, but that's probably the biggest risk. Well, that and the risk that I've broken something related to metadata textures, because breaking those wouldn't be so obvious at a glance.

One more thing, do you have any numbers of showing how much texture memory was saved?

It eliminates a complete copy of the texture in CPU memory. After this PR, there should be none left (as far as I know!), so the CPU texture memory usage goes from X to 0, for an infinite improvement! šŸ˜

Counting CPU and GPU memory together, this will cut the texture memory usage in half. This should be true universally across all tilesets and raster overlays.

kring commented 6 months ago

Well I looked into it, and it's actually not too difficult to read back a texture in Unreal. So I added some tests. I also cleaned up some TODOs. Sadly this PR no longer removes more code than it adds, but it's probably worth it. šŸ˜

kring commented 6 months ago

I unfortunately had to mark the new tests NonNullRHI, meaning they effectively won't run on CI, because the null RHI doesn't allow to create a texture and then read back its pixels, so all the new tests fail. It's also not possible to run with a non-null RHI on the non-D3D11-capable GH Actions runners.

j9liu commented 6 months ago

I ran into a crash earlier that I can't reproduce šŸ¤” It was because my 06_CesiumMetadata had been changed for #1360, and when I opened it, it crashed some class that was "SharedThread____" ? But after I git restore'd the level, I didn't see it again, and I have no idea what I could have done for my PR to induce the crash!

Unfortunately, I took a screenshot and pasted it here to post later, but I accidentally navigated to a different tab and lost the image. And now I can't reproduce it. šŸ™ƒ I'm documenting this anyway in case I run into it again.

csciguy8 commented 5 months ago

@kring back to you. There's a few comments left here, and also some comments in cesium-native 806.

Appreciate the new texture unit tests, thanks. I think one of your big concerns was memory leaks, and indeed that is a general concern for the plugin right now, so I wrote a new issue to create memory leak tests.

I finished code review and didn't find anything major. It's a lot of code that's changed though, so it's very easy for subtle bugs to slip through. Hopefully unit testing will help. I'll also manual test a bit more through next week.

kring commented 5 months ago

Thanks for the reviews @csciguy8 and @j9liu. I believe I've addressed all your comments.

j9liu commented 5 months ago

Thanks @kring !