Repository navigation
chore(ww3d2): Implement texture atlas - #3562
stephanmeesters wants to merge 13 commits into
Conversation
This comment was marked as spam.
This comment was marked as spam.
There was a problem hiding this comment.
Actionable comments posted: 4
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
292e8ca8-2b91-4248-b7a7-465257875006
📒 Files selected for processing: 15
Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txtCore/Libraries/Source/WWVegas/WW3D2/textureatlas.cppCore/Libraries/Source/WWVegas/WW3D2/textureatlas.hDependencies/Utility/CMakeLists.txtDependencies/Utility/Utility/premultiply_alpha.hDependencies/Utility/Utility/premultiply_alpha.inlDependencies/Utility/Utility/premultiply_alpha_sse2.inlDependencies/Utility/Utility/premultiply_alpha_x86asm.inlTests/Google/Core/CMakeLists.txtTests/Google/Core/Libraries/WWVegas/WW3D2/TextureAtlasTest.cppTests/Google/Dependencies/CMakeLists.txtTests/Google/Dependencies/Utility/premultiply_alpha_common.hTests/Google/Dependencies/Utility/premultiply_alpha_portable_test.cppTests/Google/Dependencies/Utility/premultiply_alpha_sse2_test.cppTests/Google/Dependencies/Utility/premultiply_alpha_x86asm_test.cpp
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.
[Low impact] The latest changes appear safe to merge; no new blocking issue was found.
|
|
Is this a framework, or already applied in the game? |
Not yet applied, this is planned in #3557 for which this atlas is a prerequisite. 77% performance gain for the scene I tested. |
|
Is this meant to be titled as chore? |
Yes, in the end I went with chore because it adds the infrastructure needed for a follow-up |
| TextureLocationMap::iterator it = locations.find(source->Get_Texture_Name()); | ||
| if (it == locations.end()) | ||
| { | ||
| // To prevent lag spikes, limit the number of textures that can be inserted (packed) per frame. |
There was a problem hiding this comment.
What is the visual consequence on the runtime of this limit? Does it mean the caller will fall back to not using the texture atlas?
There was a problem hiding this comment.
Correct in that case it would fall back to using the normal texture, and in the case of particle rendering just render a bit more inefficiently for a short moment because it can't batch as efficiently.
There was a problem hiding this comment.
Ok good. Maybe explain this mechanism somewhere. Also maybe MAX_PACKS_PER_FRAME should be configurable per Atlas? Maybe an Atlas user wants to pack more per frame?
There was a problem hiding this comment.
Packs per frame now configurable and can be disabled. Comment added
| , PacksThisFrame(0) | ||
| {} | ||
|
|
||
| TextureClass* Find_Or_Insert(TextureClass* source, bool premultiply, Vector4& rect); // Gets the page holding the texture and its uv rectangle, packing the texture first if needed. |
There was a problem hiding this comment.
Do we also need a function for removing a texture class or do we only ever need to add? Do we throw away the atlas and rebuild it if one texture is no longer needed?
There was a problem hiding this comment.
For the use case of rendering particles the focus is on lazily packing all of the textures. All of these particle textures (84 textures according to a .big scan) combined can fit on a single texture atlas page, which is great for performance. One 2048x2048 page with mips is only 21.25 MiB vram so we could also bump up the resolution when needed.
We could do something like a LRU cache when the atlas is full, but practically the multiple pages should be able to fit anything, perhaps this can be done when there is ever a need for it.
There was a problem hiding this comment.
When will the Atlas be purged? On level unload or also when particles with some textures are no longer drawn? If the Atlas is never purging during level runtime, then Mods with many new particles textures can accumulate into the Atlas until it is full? What happens after it is full? And how can new textures kick out old ones?
There was a problem hiding this comment.
When will the Atlas be purged? On level unload or also when particles with some textures are no longer drawn?
This will be controlled by the owner of the atlas. For the particles I am using W3DParticleSystemManager::reset() to purge the atlas, so this happens on engine reset after each level, through GameEngine::resetSubsystems().
If the Atlas is never purging during level runtime, then Mods with many new particles textures can accumulate into the Atlas until it is full? What happens after it is full?
Correct, but in the worse case you would only miss out on the performance benefits for some of the particles. And we do have some leeway with the multiple pages that can be used.
And how can new textures kick out old ones?
Not possible right now, I suppose it could be situation-dependent so we could let the owner of the atlas control it, by purging the atlas when required?
I can take a stab at implementing some system for kicking out old textures. Right now, the code can be simple because textures can only be added at the end of a row, or added to a new row, or a new page. In this new situation it would have to look for holes on the page, and if a big texture comes in may need to purge multiple textures, and perhaps we then also need a defragmentation feature.
There was a problem hiding this comment.
My main concern here is that the accumulation of big amount of particles would leave them permanently in the Atlas during the level runtime. It is cool that all retail particles fit in one Atlas page, but we should at least anticipate and handle extreme excess so it causes no unexpected degradations in Mods that use higher numbers of particle textures.
There was a problem hiding this comment.
OK worked with Claude on this. The solution that was landed on: purge the oldest page in the atlas when it reaches the limit. This keeps the code simple (no looking for gaps, or defragmentation) and addresses the concern that some textures remain forever.
…age size for efficient packing
When a texture does not fit, the first lookup of the next frame empties the oldest page and reuses it as the newest one. Textures still drawn from it are packed again. Failed textures get another try. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
091bb67 to
0b74010
Compare
| { | ||
| const unsigned int alpha = line[x] >> 24; | ||
| unsigned int result = line[x] & 0xFF000000u; | ||
| for (unsigned int shift = 0; shift < 24; shift += 8) |
There was a problem hiding this comment.
Instead of doing this loop I suggest to just unroll it. It does just 3 lines.
| { | ||
| const unsigned int alpha = line[x] >> 24; | ||
| unsigned int result = line[x] & 0xFF000000u; | ||
| for (unsigned int shift = 0; shift < 24; shift += 8) |
|
|
||
| namespace TextureAtlas | ||
| { | ||
| constexpr const int PAGE_SIZE = 2304; // Size takes into account padding for more efficient packing. |
There was a problem hiding this comment.
The comment hints at it but the value looks too magical. Can we express this more understandably, like 2048 + PADDING * N ...
| unsigned PackFrame; | ||
| int PacksThisFrame; | ||
| int MaxPacksPerFrame; | ||
| bool OutOfSpace; |
| TextureLocations[PREMULTIPLY_OFF].clear(); | ||
| TextureLocations[PREMULTIPLY_ON].clear(); | ||
| Pages.clear(); | ||
| OutOfSpace = false; |
There was a problem hiding this comment.
Reset OutOfSpaceFrame and PacksThisFrame ?
| return TextureLocation::Create_Invalid(); | ||
| } | ||
|
|
||
| Evict_If_Out_Of_Space(); |
There was a problem hiding this comment.
Is this call to early? It is long before Pack_Texture
There was a problem hiding this comment.
Page eviction should happen as early in the frame as possible to prevent invalidating any in-use packed textures in the middle of a render batch.
Will add a clarifying comment for that.
|
|
||
| void TextureAtlasClass::Evict_If_Out_Of_Space() | ||
| { | ||
| // Evict only after a texture did not fit in an earlier frame. |
There was a problem hiding this comment.
Same reason as the comment above.
| { | ||
| if (it->second.page == evicted || it->second.page == nullptr) | ||
| { | ||
| TextureLocations[i].erase(it++); |
There was a problem hiding this comment.
Is this it++ legal? Looks as if it is deleted and then advanced.
There was a problem hiding this comment.
Should be it = TextureLocations[i].erase(it) instead.
There was a problem hiding this comment.
it = TextureLocations[i].erase(it) does not work for VC6 because it returns void.
TextureLocations[i].erase(it++); works OK for std::map.
|
|
||
| // Reuse the oldest page as the newest one. | ||
| std::rotate(Pages.begin(), Pages.begin() + 1, Pages.end()); | ||
| Pages.back().rows.clear(); |
There was a problem hiding this comment.
It looks as if it keeps the old texture here. Does it mean it keep its old drawn contents and then violates future paddings?
There was a problem hiding this comment.
Correct it rotates the page and does not bother to clear it, it will know which parts of the page are filled and are free by the list of Row objects
|
|
||
| bool TextureAtlasClass::Allocate_Cell(const int width, const int height, int& pageIndex, int& x, int& y) | ||
| { | ||
| // Use the first row of exactly this height that still has room. |
There was a problem hiding this comment.
How does the Atlas look like when there are Small and Large textures added to an Atlas? Is it smart and packs equal sized textures in the same row or does it put a Small one next to a Large one?
There was a problem hiding this comment.
Yes textures of the same height go on the same row, there is an image in the PR description, in a collapsed section.
TextureAtlasClassin WW3D2. (around 350 LoC)premultiply_alphain Dependencies. Multiple versions: portable, SSE2, VC6 compat.A texture atlas is implemented for speeding up rendering. It can be used with variably sized (but power of two) textures and supports mip-mapping. New textures are added using
Find_Or_Insert. Owners of the atlas are responsible for cleaning it up usingReset. Atlas pages are 2048x2048 and multiple pages can be created on demand.A special feature is to store the textures in premultiplied form which is necessary for rendering particles efficiently using this atlas. This will be done in #3557.
To keep stutters to a minimum, the number of textures that can be added per frame has a limiter, and the premultiply alpha step was optimized using SSE2. Cost measured using Tracy came out to around a maximum of 0.5ms per frame. It was investigated whether we can run the texture adding in a background thread but that does not appear to be feasible, the expensive D3DX copy calls (for DXT textures) are required to run on the render thread.
Unit tests for testing texture packing were created but these require a DX device and won't run on the CI so are omitted here. The tests added are for the helper functions.
AI was used during implementing and testing.
Example of filled atlas page (with premultiplied alpha variants of each for illustration)