From 07c58edf61c8eb1e3daaa17759206bbd4edd1af8 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:13:47 +0300 Subject: #5972 LLUIImage based buffer cache --- indra/llrender/lluiimage.cpp | 179 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 2 deletions(-) (limited to 'indra/llrender/lluiimage.cpp') diff --git a/indra/llrender/lluiimage.cpp b/indra/llrender/lluiimage.cpp index dc18bf16bf..b1394a07bc 100644 --- a/indra/llrender/lluiimage.cpp +++ b/indra/llrender/lluiimage.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -31,6 +31,13 @@ // Project includes #include "lluiimage.h" +#include +#include + +// Static member initialization +std::vector > LLUIImage::sImageList; +size_t LLUIImage::sCleanupIndex = 0; +bool LLUIImage::sEnableDisplayListsCollection = true; LLUIImage::LLUIImage(const std::string& name, LLPointer image) : mName(name), @@ -49,6 +56,23 @@ LLUIImage::LLUIImage(const std::string& name, LLPointer image) LLUIImage::~LLUIImage() { delete mImageLoaded; + + if (!mDisplayLists.empty()) + { + llassert(false); + // Unregister from global cleanup list (sanity check) + // But it's supposed to be cleared already, else we wouldn't + // be destructing this opbject. + auto it = std::find(sImageList.begin(), sImageList.end(), this); + if (it != sImageList.end()) + { + // Swap with last element and pop (O(1) removal) + *it = sImageList.back(); + sImageList.pop_back(); + } + } + + mDisplayLists.clear(); } S32 LLUIImage::getWidth() const @@ -63,6 +87,155 @@ S32 LLUIImage::getHeight() const return ll_round((F32)mImage->getHeight(0) * mClipRegion.getHeight()); } +buffer_data_list_t* LLUIImage::findDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const +{ + LLVector3 ui_translation = gGL.getUITranslation(); + LLVector3 ui_scale = gGL.getUIScale(); + DisplayListKey key{ x, y, width, height, color, solid_color, ui_translation, ui_scale }; + + auto it = mDisplayLists.find(key); + if (it != mDisplayLists.end()) + { + // Found cached display list, update last used time + it->second.last_used = std::chrono::steady_clock::now(); + return &it->second.list; + } + return nullptr; +} + +buffer_data_list_t* LLUIImage::genDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const +{ + LL_PROFILE_ZONE_SCOPED; + LLVector3 ui_translation = gGL.getUITranslation(); + LLVector3 ui_scale = gGL.getUIScale(); + DisplayListKey key{ x, y, width, height, color, solid_color, ui_translation, ui_scale }; + + CachedDisplayList cached; + cached.last_used = std::chrono::steady_clock::now(); + + // Generate the display list by capturing the draw commands + gGL.beginList(&cached.list); + + gl_draw_scaled_image_with_border( + x, y, + width, height, + mImage, + color, + solid_color, + mClipRegion, + mScaleRegion, + mScaleStyle == SCALE_INNER); + + gGL.endList(); + + // Insert into cache + auto result = mDisplayLists.emplace(key, std::move(cached)); + + // Register for cleanup on first buffer creation + if (mDisplayLists.size() == 1) + { + sImageList.push_back(const_cast(this)); + } + + return &result.first->second.list; +} + +void LLUIImage::invalidateDisplayLists() +{ + mDisplayLists.clear(); + + unregisterFromGlobalCleanup(); +} + +void LLUIImage::cleanupDisplayLists() +{ + if (mDisplayLists.empty()) + { + llassert(false); //it shouldn't be in this list + unregisterFromGlobalCleanup(); + // marks current position for a recheck. Increments after cleanupDisplayLists. + sCleanupIndex--; + return; + } + + // Time threshold for cleaning up unused display lists (global cleanup) + constexpr std::chrono::seconds DISPLAY_LIST_TIMEOUT{ 2 }; + auto now = std::chrono::steady_clock::now(); + + // Remove display lists that haven't been used recently + for (auto it = mDisplayLists.begin(); it != mDisplayLists.end(); ) + { + if (now - it->second.last_used > DISPLAY_LIST_TIMEOUT) + { + it = mDisplayLists.erase(it); + } + else + { + ++it; + } + } + + // Unregister from cleanup list if all display lists were removed + if (mDisplayLists.empty()) + { + unregisterFromGlobalCleanup(); + // marks current position for a recheck. Increments after cleanupDisplayLists. + sCleanupIndex--; + } +} + +void LLUIImage::unregisterFromGlobalCleanup() +{ + auto list_it = std::find(sImageList.begin(), sImageList.end(), this); + if (list_it != sImageList.end()) + { + // Swap with last element and pop (O(1) removal) + *list_it = sImageList.back(); + sImageList.pop_back(); + } +} + +// static +void LLUIImage::updateClass() +{ + if (sImageList.empty()) + { + return; + } + + // Clean up a batch of images each frame to amortize the cost + // ensuring all images are checked regularly + // Note: buffers often get obsolete in batches, perhaps + // increase rate of cleanup after a buffer was removed? + // and decrease rate if no buffer were removed and creates + // for a while? + constexpr size_t BATCH_SIZE = 8; + size_t images_to_process = std::min(BATCH_SIZE, sImageList.size()); + + for (size_t i = 0; i < images_to_process; ++i) + { + if (sCleanupIndex >= sImageList.size()) + { + sCleanupIndex = 0; + } + + sImageList[sCleanupIndex]->cleanupDisplayLists(); + ++sCleanupIndex; + } +} + +void LLUIImage::cleanupClass() +{ + std::vector > list_copy(sImageList); + sImageList.clear(); + for (LLUIImage* image : list_copy) + { + // invalidateDisplayLists will attempt to clear sImageList + image->invalidateDisplayLists(); + } + sCleanupIndex = 0; +} + void LLUIImage::draw3D(const LLVector3& origin_agent, const LLVector3& x_axis, const LLVector3& y_axis, const LLRect& rect, const LLColor4& color) { @@ -77,7 +250,7 @@ void LLUIImage::draw3D(const LLVector3& origin_agent, const LLVector3& x_axis, c } else { - border_scale = (F32)rect.getWidth() / border_width; + border_scale = (F32)rect.getWidth() / border_width; } } @@ -124,6 +297,8 @@ void LLUIImage::onImageLoaded() { (*mImageLoaded)(); } + + invalidateDisplayLists(); } namespace LLInitParam -- cgit v1.3 From fa38b81c80e41280fb79c0fb0613a408548a27ac Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:22:21 +0300 Subject: #5972 LLUIImage based buffer cache #2 Simplify key a bit. --- indra/llrender/lluiimage.cpp | 53 +++++++++------- indra/llrender/lluiimage.h | 106 ++++++++++---------------------- indra/llrender/lluiimage.inl | 2 +- indra/newview/app_settings/settings.xml | 2 +- 4 files changed, 65 insertions(+), 98 deletions(-) (limited to 'indra/llrender/lluiimage.cpp') diff --git a/indra/llrender/lluiimage.cpp b/indra/llrender/lluiimage.cpp index b1394a07bc..85754209a4 100644 --- a/indra/llrender/lluiimage.cpp +++ b/indra/llrender/lluiimage.cpp @@ -31,7 +31,6 @@ // Project includes #include "lluiimage.h" -#include #include // Static member initialization @@ -57,21 +56,10 @@ LLUIImage::~LLUIImage() { delete mImageLoaded; - if (!mDisplayLists.empty()) - { - llassert(false); - // Unregister from global cleanup list (sanity check) - // But it's supposed to be cleared already, else we wouldn't - // be destructing this opbject. - auto it = std::find(sImageList.begin(), sImageList.end(), this); - if (it != sImageList.end()) - { - // Swap with last element and pop (O(1) removal) - *it = sImageList.back(); - sImageList.pop_back(); - } - } - + // Unregister from global cleanup list (sanity check) + // But it's supposed to be cleared already, else we wouldn't + // be destructing this object. + unregisterFromGlobalCleanup(); mDisplayLists.clear(); } @@ -91,12 +79,12 @@ buffer_data_list_t* LLUIImage::findDisplayList(S32 x, S32 y, S32 width, S32 heig { LLVector3 ui_translation = gGL.getUITranslation(); LLVector3 ui_scale = gGL.getUIScale(); - DisplayListKey key{ x, y, width, height, color, solid_color, ui_translation, ui_scale }; + + auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale); auto it = mDisplayLists.find(key); if (it != mDisplayLists.end()) { - // Found cached display list, update last used time it->second.last_used = std::chrono::steady_clock::now(); return &it->second.list; } @@ -108,7 +96,7 @@ buffer_data_list_t* LLUIImage::genDisplayList(S32 x, S32 y, S32 width, S32 heigh LL_PROFILE_ZONE_SCOPED; LLVector3 ui_translation = gGL.getUITranslation(); LLVector3 ui_scale = gGL.getUIScale(); - DisplayListKey key{ x, y, width, height, color, solid_color, ui_translation, ui_scale }; + auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale); CachedDisplayList cached; cached.last_used = std::chrono::steady_clock::now(); @@ -129,6 +117,7 @@ buffer_data_list_t* LLUIImage::genDisplayList(S32 x, S32 y, S32 width, S32 heigh gGL.endList(); // Insert into cache + // emplace, since we only call genDisplayList if key was not found. auto result = mDisplayLists.emplace(key, std::move(cached)); // Register for cleanup on first buffer creation @@ -154,7 +143,18 @@ void LLUIImage::cleanupDisplayLists() llassert(false); //it shouldn't be in this list unregisterFromGlobalCleanup(); // marks current position for a recheck. Increments after cleanupDisplayLists. - sCleanupIndex--; + if (sCleanupIndex > 0) + { + sCleanupIndex--; + } + else if (sImageList.empty()) + { + sCleanupIndex = 0; + } + else + { + sCleanupIndex = sImageList.size() - 1; + } return; } @@ -180,7 +180,18 @@ void LLUIImage::cleanupDisplayLists() { unregisterFromGlobalCleanup(); // marks current position for a recheck. Increments after cleanupDisplayLists. - sCleanupIndex--; + if (sCleanupIndex > 0) + { + sCleanupIndex--; + } + else if (sImageList.empty()) + { + sCleanupIndex = 0; + } + else + { + sCleanupIndex = sImageList.size() - 1; + } } } diff --git a/indra/llrender/lluiimage.h b/indra/llrender/lluiimage.h index 9e481b6615..6f33c31bfe 100644 --- a/indra/llrender/lluiimage.h +++ b/indra/llrender/lluiimage.h @@ -38,7 +38,11 @@ #include +#include +#include #include +#include +#include extern const LLColor4 UI_VERTEX_COLOR; @@ -120,46 +124,42 @@ public: static void enableDisplayListsCollection(bool enable) { sEnableDisplayListsCollection = enable; } protected: - // Key for identifying unique display list configurations - struct DisplayListKey + // Packed key for identifying unique display list configurations + struct PackedKey { - S32 x; - S32 y; - S32 width; - S32 height; - LLColor4 color; - bool solid_color; - LLVector3 translate; - LLVector3 scale; - - // Pack for hashing - struct PackedKey + uint64_t position; // x and y coordinates + uint64_t color_flags; // RGBA color + solid_color flag + uint64_t dimensions; // width and height + uint64_t translate; // UI offset + uint64_t scale; // UI scale + + constexpr bool operator==(const PackedKey& other) const + { + return position == other.position && + color_flags == other.color_flags && + dimensions == other.dimensions && + translate == other.translate && + scale == other.scale; + } + + struct Hash { - uint64_t position; // x and y coordinates - uint64_t color_flags; // RGBA color + solid_color flag - uint64_t dimensions; // width and height - // todo: fix, translation, scale, position, shouldn't be needed - uint64_t translate; // UI offset - uint64_t scale; // UI scale - - constexpr bool operator==(const PackedKey& other) const + std::size_t operator()(const PackedKey& key) const { - return position == other.position && - color_flags == other.color_flags && - dimensions == other.dimensions && - translate == other.translate && - scale == other.scale; + return static_cast(key.position ^ key.color_flags ^ + key.dimensions ^ key.translate ^ key.scale); } }; - constexpr PackedKey pack() const + // Static factory function to create PackedKey from parameters + static constexpr PackedKey create(S32 x, S32 y, S32 width, S32 height, + const LLColor4& color, bool solid_color, + const LLVector3& translate, const LLVector3& scale) { - // Convert floats to 8-bit values for packing (0.0-1.0 -> 0-255) auto float_to_u8 = [](F32 f) -> uint8_t { return static_cast(llclamp(f * 255.0f, 0.0f, 255.0f)); }; - // Helper to convert float to uint32 preserving bit pattern auto float_to_bits = [](F32 f) -> uint32_t { return std::bit_cast(f); }; @@ -189,47 +189,6 @@ protected: return PackedKey{ pos, col, dim, trns, scl }; } - - constexpr bool operator==(const DisplayListKey& other) const - { - return pack() == other.pack(); - } - - struct Hash - { - using is_transparent = void; // Enable transparent lookup - - std::size_t operator()(const DisplayListKey& key) const - { - auto packed = key.pack(); - return static_cast(packed.position ^ packed.color_flags ^ packed.dimensions ^ packed.translate ^ packed.scale); - } - - std::size_t operator()(const PackedKey& packed) const - { - return static_cast(packed.position ^ packed.color_flags ^ packed.dimensions ^ packed.translate ^ packed.scale); - } - }; - - struct KeyEqual - { - using is_transparent = void; // Enable transparent lookup - - bool operator()(const DisplayListKey& lhs, const DisplayListKey& rhs) const - { - return lhs == rhs; - } - - bool operator()(const DisplayListKey& lhs, const PackedKey& rhs) const - { - return lhs.pack() == rhs; - } - - bool operator()(const PackedKey& lhs, const DisplayListKey& rhs) const - { - return lhs == rhs.pack(); - } - }; }; // Cached display list for a specific configuration @@ -261,11 +220,8 @@ protected: mutable S32 mCachedW; mutable S32 mCachedH; - // Display list cache - // const member functions promise not to modify the object's logical state, but - // cache does not modify logical state, mutabale to permit const correctness - // (standard C++ pattern for transparent caching). - mutable std::unordered_map mDisplayLists; + // Display list cache - now using PackedKey directly + mutable std::unordered_map mDisplayLists; // Track all LLUIImage cache instances for global cleanup static std::vector > sImageList; diff --git a/indra/llrender/lluiimage.inl b/indra/llrender/lluiimage.inl index 1915d0e7ce..d40835a0f4 100644 --- a/indra/llrender/lluiimage.inl +++ b/indra/llrender/lluiimage.inl @@ -39,7 +39,7 @@ void LLUIImage::draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, if (display_list && !display_list->empty()) { // Deliberately empty pending verts. - // They aren't related to the iamge, so don't register them under draw + // They aren't related to the image, so don't register them under draw zone gGL.flush(); LL_PROFILE_ZONE_SCOPED; gGL.getTexUnit(0)->enable(LLTexUnit::TT_TEXTURE); diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 43b3fdf1b0..75258cfa79 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -9896,7 +9896,7 @@ CollectUIImageVertexBuffers Comment - When enabled images will cache buffers and reuse them. When disabled general cahce will be used with a significant overhead for hash, but it regenerates vertices each frame so it's always up to date. + When enabled, images will cache vertex buffers and reuse them. When disabled, the general cache will be used with significant hash-lookup overhead, but vertices are regenerated each frame so they are always up to date. Persist 0 Type -- cgit v1.3 From b35e537b5140c2a1d38fe979c2c551f735a6d21e Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:56:54 +0300 Subject: #5972 Track UI texture changes --- indra/llrender/lluiimage.cpp | 25 +++++++++++++++++++++++-- indra/llrender/lluiimage.h | 25 ++++++++++++++++--------- 2 files changed, 39 insertions(+), 11 deletions(-) (limited to 'indra/llrender/lluiimage.cpp') diff --git a/indra/llrender/lluiimage.cpp b/indra/llrender/lluiimage.cpp index 85754209a4..d0b6bcbf66 100644 --- a/indra/llrender/lluiimage.cpp +++ b/indra/llrender/lluiimage.cpp @@ -77,10 +77,17 @@ S32 LLUIImage::getHeight() const buffer_data_list_t* LLUIImage::findDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const { + LLImageGL* gl_image = mImage->getGLTexture(); + if (!gl_image) + { + return nullptr; + } + LLVector3 ui_translation = gGL.getUITranslation(); LLVector3 ui_scale = gGL.getUIScale(); + LLGLuint tex_name = gl_image->getTexName(); - auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale); + auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale, tex_name); auto it = mDisplayLists.find(key); if (it != mDisplayLists.end()) @@ -94,9 +101,23 @@ buffer_data_list_t* LLUIImage::findDisplayList(S32 x, S32 y, S32 width, S32 heig buffer_data_list_t* LLUIImage::genDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const { LL_PROFILE_ZONE_SCOPED; + + LLImageGL* gl_image = mImage->getGLTexture(); + if (!gl_image) + { + // Don't cache when texture hasn't been created yet + // draw just aborts in this case, so don't draw either. + return nullptr; + } + LLVector3 ui_translation = gGL.getUITranslation(); LLVector3 ui_scale = gGL.getUIScale(); - auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale); + + // Get the GL texture name - this uniquely identifies the current texture state + // including discard level changes, texture recreation, etc. + LLGLuint tex_name = gl_image->getTexName(); + + auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale, tex_name); CachedDisplayList cached; cached.last_used = std::chrono::steady_clock::now(); diff --git a/indra/llrender/lluiimage.h b/indra/llrender/lluiimage.h index 6f33c31bfe..0998ac17b4 100644 --- a/indra/llrender/lluiimage.h +++ b/indra/llrender/lluiimage.h @@ -127,11 +127,12 @@ protected: // Packed key for identifying unique display list configurations struct PackedKey { - uint64_t position; // x and y coordinates - uint64_t color_flags; // RGBA color + solid_color flag - uint64_t dimensions; // width and height - uint64_t translate; // UI offset - uint64_t scale; // UI scale + uint64_t position; // x and y coordinates (32 bits each) + uint64_t color_flags; // RGBA color (8 bits each) + solid_color flag (1 bit) + uint64_t dimensions; // width and height (32 bits each) + uint64_t translate; // UI translation (32 bits each for X and Y) + uint64_t scale; // UI scale (32 bits each for X and Y) + uint64_t tex_name; // OpenGL texture name (32 bits) + padding constexpr bool operator==(const PackedKey& other) const { @@ -139,7 +140,8 @@ protected: color_flags == other.color_flags && dimensions == other.dimensions && translate == other.translate && - scale == other.scale; + scale == other.scale && + tex_name == other.tex_name; } struct Hash @@ -147,14 +149,16 @@ protected: std::size_t operator()(const PackedKey& key) const { return static_cast(key.position ^ key.color_flags ^ - key.dimensions ^ key.translate ^ key.scale); + key.dimensions ^ key.translate ^ + key.scale ^ key.tex_name); } }; // Static factory function to create PackedKey from parameters static constexpr PackedKey create(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color, - const LLVector3& translate, const LLVector3& scale) + const LLVector3& translate, const LLVector3& scale, + LLGLuint texture_name) { auto float_to_u8 = [](F32 f) -> uint8_t { return static_cast(llclamp(f * 255.0f, 0.0f, 255.0f)); @@ -187,7 +191,10 @@ protected: uint64_t scl = (static_cast(float_to_bits(scale.mV[VX])) << 32) | static_cast(float_to_bits(scale.mV[VY])); - return PackedKey{ pos, col, dim, trns, scl }; + // Store full 32-bit texture name in lower 32 bits (upper 32 bits unused/zero) + uint64_t tex = static_cast(texture_name); + + return PackedKey{ pos, col, dim, trns, scl, tex }; } }; -- cgit v1.3