Skip to content

chore(ww3d2): Implement texture atlas - #3562

Open
stephanmeesters wants to merge 13 commits into
TheSuperHackers:mainfrom
stephanmeesters:chore/ww3d-texture-atlas
Open

stephanmeesters wants to merge 13 commits into
TheSuperHackers:mainfrom
stephanmeesters:chore/ww3d-texture-atlas

Conversation

@stephanmeesters

@stephanmeesters stephanmeesters commented Oct 19, 2026 •

Copy link
Copy Markdown

  • Implement TextureAtlasClass in WW3D2. (around 350 LoC)
  • Implement premultiply_alpha in Dependencies. Multiple versions: portable, SSE2, VC6 compat.
  • Adds unit tests for both.

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 using Reset. 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) atlas-particles-full-generals

@coderabbitai

This comment was marked as spam.

@stephanmeesters stephanmeesters added the System Is Systems related label Oct 19, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 292e8ca8-2b91-4248-b7a7-465257875006
📥 Commits

Reviewing files that changed from the base of the PR and between 2bdb1be and 85076bb.

📒 Files selected for processing: 15
  • Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt
  • Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp
  • Core/Libraries/Source/WWVegas/WW3D2/textureatlas.h
  • Dependencies/Utility/CMakeLists.txt
  • Dependencies/Utility/Utility/premultiply_alpha.h
  • Dependencies/Utility/Utility/premultiply_alpha.inl
  • Dependencies/Utility/Utility/premultiply_alpha_sse2.inl
  • Dependencies/Utility/Utility/premultiply_alpha_x86asm.inl
  • Tests/Google/Core/CMakeLists.txt
  • Tests/Google/Core/Libraries/WWVegas/WW3D2/TextureAtlasTest.cpp
  • Tests/Google/Dependencies/CMakeLists.txt
  • Tests/Google/Dependencies/Utility/premultiply_alpha_common.h
  • Tests/Google/Dependencies/Utility/premultiply_alpha_portable_test.cpp
  • Tests/Google/Dependencies/Utility/premultiply_alpha_sse2_test.cpp
  • Tests/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.

Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp
Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp
Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp
Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp
@stephanmeesters stephanmeesters added the Rendering Is Rendering related label Oct 19, 2026
@stephanmeesters
stephanmeesters marked this pull request as ready for review October 19, 2026 19:44
Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp
@stephanmeesters stephanmeesters changed the title chore(textureatlas): Implement texture atlas chore(ww3d2): Implement texture atlas Oct 19, 2026
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Low impact] The latest changes appear safe to merge; no new blocking issue was found.

Summary

This PR adds texture atlas pages, optional premultiplied colors, and helper tests.

  • WW3D2 packs named textures into shared pages and returns UV bounds.
  • Pixel colors can be premultiplied with portable or optimized code paths.

Reviews (11) · Last reviewed commit: "Process feedback" · Reviewed by Greptile

Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp Outdated
Comment thread Tests/Google/Core/Libraries/WWVegas/WW3D2/TextureAtlasTest.cpp
Comment thread Dependencies/Utility/Utility/premultiply_alpha_sse2.inl
Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp Outdated
@Skyaero42

Copy link
Copy Markdown

Is this a framework, or already applied in the game?
If the latter, what is the performance gain?

@stephanmeesters

Copy link
Copy Markdown
Author

Is this a framework, or already applied in the game? If the latter, what is the performance gain?

Not yet applied, this is planned in #3557 for which this atlas is a prerequisite. 77% performance gain for the scene I tested.

@bobtista bobtista left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More perf gains comin 🚀 LGTM

@xezon

xezon commented Oct 25, 2026

Copy link
Copy Markdown

Is this meant to be titled as chore?

@stephanmeesters

Copy link
Copy Markdown
Author

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 perf PR, but if you feel it should be feat we can go with that.

Comment thread Dependencies/Utility/Utility/premultiply_alpha_x86asm.inl Outdated
Comment thread Tests/Google/Dependencies/Utility/premultiply_alpha_common.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp
Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.cpp Outdated
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@stephanmeesters stephanmeesters Oct 27, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@xezon xezon Oct 26, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@stephanmeesters stephanmeesters Oct 27, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Core/Libraries/Source/WWVegas/WW3D2/textureatlas.h
stephanmeesters and others added 11 commits October 27, 2026 20:34
@stephanmeesters
stephanmeesters force-pushed the chore/ww3d-texture-atlas branch from 091bb67 to 0b74010 Compare October 27, 2026 19:35
{
const unsigned int alpha = line[x] >> 24;
unsigned int result = line[x] & 0xFF000000u;
for (unsigned int shift = 0; shift < 24; shift += 8)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.


namespace TextureAtlas
{
constexpr const int PAGE_SIZE = 2304; // Size takes into account padding for more efficient packing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Can sort by byte size.

TextureLocations[PREMULTIPLY_OFF].clear();
TextureLocations[PREMULTIPLY_ON].clear();
Pages.clear();
OutOfSpace = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reset OutOfSpaceFrame and PacksThisFrame ?

return TextureLocation::Create_Invalid();
}

Evict_If_Out_Of_Space();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this call to early? It is long before Pack_Texture

@stephanmeesters stephanmeesters Oct 28, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the frame relevant for eviction?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reason as the comment above.

{
if (it->second.page == evicted || it->second.page == nullptr)
{
TextureLocations[i].erase(it++);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this it++ legal? Looks as if it is deleted and then advanced.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be it = TextureLocations[i].erase(it) instead.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks as if it keeps the old texture here. Does it mean it keep its old drawn contents and then violates future paddings?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@stephanmeesters stephanmeesters Oct 28, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes textures of the same height go on the same row, there is an image in the PR description, in a collapsed section.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Rendering Is Rendering related System Is Systems related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants