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 ++++++++++++++++++++- indra/llrender/lluiimage.h | 163 ++++++++++++++++++- indra/llrender/lluiimage.inl | 80 ++++++--- indra/newview/app_settings/settings.xml | 11 ++ indra/newview/llappviewer.cpp | 1 + indra/newview/llstatusbar.cpp | 1 + indra/newview/llviewertexturelist.cpp | 1 + indra/newview/pipeline.cpp | 12 ++ indra/newview/skins/default/xui/en/menu_viewer.xml | 10 ++ 9 files changed, 436 insertions(+), 22 deletions(-) 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 diff --git a/indra/llrender/lluiimage.h b/indra/llrender/lluiimage.h index 7dde84f295..9e481b6615 100644 --- a/indra/llrender/lluiimage.h +++ b/indra/llrender/lluiimage.h @@ -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 @@ -34,6 +34,7 @@ #include "llinitparam.h" #include "lltexture.h" #include "llrender2dutils.h" +#include "llvertexbuffer.h" #include @@ -58,21 +59,28 @@ public: LL_FORCE_INLINE void setClipRegion(const LLRectf& region) { mClipRegion = region; + // This happens when image becomes loaded + invalidateDisplayLists(); } LL_FORCE_INLINE void setScaleRegion(const LLRectf& region) { mScaleRegion = region; + // This happens when image becomes loaded + invalidateDisplayLists(); } LL_FORCE_INLINE void setScaleStyle(EScaleStyle style) { mScaleStyle = style; + // This happens when image becomes loaded + invalidateDisplayLists(); } LL_FORCE_INLINE LLPointer getImage() { return mImage; } LL_FORCE_INLINE const LLPointer& getImage() const { return mImage; } + LL_FORCE_INLINE void draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const; LL_FORCE_INLINE void draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color = UI_VERTEX_COLOR) const; LL_FORCE_INLINE void draw(S32 x, S32 y, const LLColor4& color = UI_VERTEX_COLOR) const; LL_FORCE_INLINE void draw(const LLRect& rect, const LLColor4& color = UI_VERTEX_COLOR) const { draw(rect.mLeft, rect.mBottom, rect.getWidth(), rect.getHeight(), color); } @@ -85,6 +93,10 @@ public: LL_FORCE_INLINE void drawBorder(const LLRect& rect, const LLColor4& color, S32 border_width) const { drawBorder(rect.mLeft, rect.mBottom, rect.getWidth(), rect.getHeight(), color, border_width); } LL_FORCE_INLINE void drawBorder(S32 x, S32 y, const LLColor4& color, S32 border_width) const { drawBorder(x, y, getWidth(), getHeight(), color, border_width); } + // Note: draw3D is not cached with display lists because it uses world-space rendering + // with dynamic transforms (gl_segmented_rect_3d_tex). These calls are infrequent and + // highly dynamic, making caching ineffective. The 2D UI methods benefit from caching + // because they're called many times per frame with the same dimensions. void draw3D(const LLVector3& origin_agent, const LLVector3& x_axis, const LLVector3& y_axis, const LLRect& rect, const LLColor4& color); LL_FORCE_INLINE const std::string& getName() const { return mName; } @@ -100,7 +112,145 @@ public: void onImageLoaded(); + // Global cleanup of unused display lists across all LLUIImage instances + // Should be called periodically (e.g., once per frame or when memory pressure is detected) + static void updateClass(); + static void cleanupClass(); + + static void enableDisplayListsCollection(bool enable) { sEnableDisplayListsCollection = enable; } + protected: + // Key for identifying unique display list configurations + struct DisplayListKey + { + 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 + // 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 + { + return position == other.position && + color_flags == other.color_flags && + dimensions == other.dimensions && + translate == other.translate && + scale == other.scale; + } + }; + + constexpr PackedKey pack() const + { + // 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); + }; + + uint8_t r = float_to_u8(color.mV[VRED]); + uint8_t g = float_to_u8(color.mV[VGREEN]); + uint8_t b = float_to_u8(color.mV[VBLUE]); + uint8_t a = float_to_u8(color.mV[VALPHA]); + + uint64_t pos = (static_cast(static_cast(x)) << 32) | + static_cast(static_cast(y)); + + uint64_t col = (static_cast(r) << 56) | + (static_cast(g) << 48) | + (static_cast(b) << 40) | + (static_cast(a) << 32) | + (solid_color ? 1ULL : 0ULL); + + uint64_t dim = (static_cast(static_cast(width)) << 32) | + static_cast(static_cast(height)); + + uint64_t trns = (static_cast(float_to_bits(translate.mV[VX])) << 32) | + static_cast(float_to_bits(translate.mV[VY])); + + 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 }; + } + + 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 + struct CachedDisplayList + { + buffer_data_list_t list; + std::chrono::steady_clock::time_point last_used; + }; + + // Get a display list for the given configuration + buffer_data_list_t* findDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const; + // Generate a new display list for the given configuration, draws immediately. + buffer_data_list_t* genDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const; + + // Invalidate all cached display lists (called when image properties change) + void invalidateDisplayLists(); + + // Clean up old display lists for this image (called by updateClass) + void cleanupDisplayLists(); + void unregisterFromGlobalCleanup(); + image_loaded_signal_t* mImageLoaded; std::string mName; @@ -110,6 +260,17 @@ protected: EScaleStyle mScaleStyle; 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; + + // Track all LLUIImage cache instances for global cleanup + static std::vector > sImageList; + static size_t sCleanupIndex; // Round-robin cleanup position + static bool sEnableDisplayListsCollection; }; #include "lluiimage.inl" diff --git a/indra/llrender/lluiimage.inl b/indra/llrender/lluiimage.inl index dff1fcdfcc..1915d0e7ce 100644 --- a/indra/llrender/lluiimage.inl +++ b/indra/llrender/lluiimage.inl @@ -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 @@ -29,30 +29,72 @@ void LLUIImage::draw(S32 x, S32 y, const LLColor4& color) const draw(x, y, getWidth(), getHeight(), color); } +void LLUIImage::draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const +{ + if (sEnableDisplayListsCollection) + { + // Get display list for this configuration + buffer_data_list_t* display_list = findDisplayList(x, y, width, height, color, solid_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 + gGL.flush(); + LL_PROFILE_ZONE_SCOPED; + gGL.getTexUnit(0)->enable(LLTexUnit::TT_TEXTURE); + + //gGL.pushUIMatrix(); + + if (solid_color) + { + gSolidColorProgram.bind(); + } + + gGL.color4fv(color.mV); // for the shader + + // Replay the cached display list + for (LLVertexBufferData& buffer : *display_list) + { + buffer.draw(); + } + + if (solid_color) + { + gUIProgram.bind(); + } + //gGL.popUIMatrix(); + } + else + { + // Create, draw and capture display list. + // Basically a wrapper around gl_draw_scaled_image_with_border + // that records the output into a list. + genDisplayList(x, y, width, height, color, solid_color); + } + } + else + { + gl_draw_scaled_image_with_border( + x, y, + width, height, + mImage, + color, + solid_color, + mClipRegion, + mScaleRegion, + mScaleStyle == SCALE_INNER); + } +} + void LLUIImage::draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color) const { - gl_draw_scaled_image_with_border( - x, y, - width, height, - mImage, - color, - false, - mClipRegion, - mScaleRegion, - mScaleStyle == SCALE_INNER); + draw(x, y, width, height, color, false); } void LLUIImage::drawSolid(S32 x, S32 y, S32 width, S32 height, const LLColor4& color) const { - gl_draw_scaled_image_with_border( - x, y, - width, height, - mImage, - color, - true, - mClipRegion, - mScaleRegion, - mScaleStyle == SCALE_INNER); + draw(x, y, width, height, color, true); } void LLUIImage::drawBorder(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, S32 border_width) const diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index b1e161182e..43b3fdf1b0 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -9893,6 +9893,17 @@ Value 1 + 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. + Persist + 0 + Type + Boolean + Value + 1 + ShowMyComplexityChanges Comment diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index b0c13d9818..e2bd8798f4 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -5179,6 +5179,7 @@ void LLAppViewer::idle() static LLCachedControl downscale_method(gSavedSettings, "RenderDownScaleMethod"); gGLManager.mDownScaleMethod = downscale_method; LLImageGL::updateClass(); + LLUIImage::updateClass(); // Service the WorkQueue we use for replies from worker threads. // Use function statics for the timeslice setting so we only have to fetch diff --git a/indra/newview/llstatusbar.cpp b/indra/newview/llstatusbar.cpp index 44bada13c2..0df3c27f3e 100644 --- a/indra/newview/llstatusbar.cpp +++ b/indra/newview/llstatusbar.cpp @@ -143,6 +143,7 @@ LLStatusBar::~LLStatusBar() // virtual void LLStatusBar::draw() { + LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; refresh(); LLPanel::draw(); } diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 7dd32074cf..4bee11b6f7 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -1641,6 +1641,7 @@ void LLViewerTextureList::processImageNotInDatabase(LLMessageSystem *msg,void ** // guaranteed. void LLUIImageList::cleanUp() { + LLUIImage::cleanupClass(); mUIImages.clear(); mUITextureList.clear() ; } diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 4ef88f5deb..54fa7a2c09 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -614,6 +614,16 @@ void LLPipeline::init() LLFontWidthBuffer::enableBufferCollection(enable_buffers); }); } + + cntrl_ptr = gSavedSettings.getControl("CollectUIImageVertexBuffers"); + if (cntrl_ptr.notNull()) + { + cntrl_ptr->getCommitSignal()->connect([](LLControlVariable* control, const LLSD& value, const LLSD& previous) + { + bool enable_buffers = control->getValue().asBoolean(); + LLUIImage::enableDisplayListsCollection(enable_buffers); + }); + } } LLPipeline::~LLPipeline() @@ -1151,6 +1161,8 @@ void LLPipeline::refreshCachedSettings() bool enable_buffers = gSavedSettings.getBOOL("CollectFontVertexBuffers"); LLFontVertexBuffer::enableBufferCollection(enable_buffers); LLFontWidthBuffer::enableBufferCollection(enable_buffers); + enable_buffers = gSavedSettings.getBOOL("CollectUIImageVertexBuffers"); + LLUIImage::enableDisplayListsCollection(enable_buffers); } void LLPipeline::releaseGLBuffers() diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index 3364b831b2..a4b9ab61df 100644 --- a/indra/newview/skins/default/xui/en/menu_viewer.xml +++ b/indra/newview/skins/default/xui/en/menu_viewer.xml @@ -3529,6 +3529,16 @@ function="World.EnvPreset" function="ToggleControl" parameter="CollectFontVertexBuffers" /> + + + +