summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrey Kleshchev <117672381+akleshchev@users.noreply.github.com>2026-07-02 00:13:47 +0300
committerAndrey Kleshchev <117672381+akleshchev@users.noreply.github.com>2026-07-10 23:06:07 +0300
commit07c58edf61c8eb1e3daaa17759206bbd4edd1af8 (patch)
tree9dabdcca9db74308a6a78b5eeedd1e5ce09865af
parent97b0c99e29a60e858f7328094de7e59e5ad02278 (diff)
#5972 LLUIImage based buffer cache
-rw-r--r--indra/llrender/lluiimage.cpp179
-rw-r--r--indra/llrender/lluiimage.h163
-rw-r--r--indra/llrender/lluiimage.inl80
-rw-r--r--indra/newview/app_settings/settings.xml11
-rw-r--r--indra/newview/llappviewer.cpp1
-rw-r--r--indra/newview/llstatusbar.cpp1
-rw-r--r--indra/newview/llviewertexturelist.cpp1
-rw-r--r--indra/newview/pipeline.cpp12
-rw-r--r--indra/newview/skins/default/xui/en/menu_viewer.xml10
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 <chrono>
+#include <algorithm>
+
+// Static member initialization
+std::vector<LLPointer<LLUIImage> > LLUIImage::sImageList;
+size_t LLUIImage::sCleanupIndex = 0;
+bool LLUIImage::sEnableDisplayListsCollection = true;
LLUIImage::LLUIImage(const std::string& name, LLPointer<LLTexture> image)
: mName(name),
@@ -49,6 +56,23 @@ LLUIImage::LLUIImage(const std::string& name, LLPointer<LLTexture> 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<LLUIImage*>(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<LLPointer<LLUIImage> > 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 <boost/signals2.hpp>
@@ -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<LLTexture> getImage() { return mImage; }
LL_FORCE_INLINE const LLPointer<LLTexture>& 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<uint8_t>(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<uint32_t>(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<uint64_t>(static_cast<uint32_t>(x)) << 32) |
+ static_cast<uint64_t>(static_cast<uint32_t>(y));
+
+ uint64_t col = (static_cast<uint64_t>(r) << 56) |
+ (static_cast<uint64_t>(g) << 48) |
+ (static_cast<uint64_t>(b) << 40) |
+ (static_cast<uint64_t>(a) << 32) |
+ (solid_color ? 1ULL : 0ULL);
+
+ uint64_t dim = (static_cast<uint64_t>(static_cast<uint32_t>(width)) << 32) |
+ static_cast<uint64_t>(static_cast<uint32_t>(height));
+
+ uint64_t trns = (static_cast<uint64_t>(float_to_bits(translate.mV[VX])) << 32) |
+ static_cast<uint64_t>(float_to_bits(translate.mV[VY]));
+
+ uint64_t scl = (static_cast<uint64_t>(float_to_bits(scale.mV[VX])) << 32) |
+ static_cast<uint64_t>(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<std::size_t>(packed.position ^ packed.color_flags ^ packed.dimensions ^ packed.translate ^ packed.scale);
+ }
+
+ std::size_t operator()(const PackedKey& packed) const
+ {
+ return static_cast<std::size_t>(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<DisplayListKey, CachedDisplayList, DisplayListKey::Hash> mDisplayLists;
+
+ // Track all LLUIImage cache instances for global cleanup
+ static std::vector<LLPointer<LLUIImage> > 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 @@
<key>Value</key>
<real>1</real>
</map>
+ <key>CollectUIImageVertexBuffers</key>
+ <map>
+ <key>Comment</key>
+ <string>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.</string>
+ <key>Persist</key>
+ <integer>0</integer>
+ <key>Type</key>
+ <string>Boolean</string>
+ <key>Value</key>
+ <real>1</real>
+ </map>
<key>ShowMyComplexityChanges</key>
<map>
<key>Comment</key>
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<U32> 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" />
</menu_item_check>
+ <menu_item_check
+ label="Collect UI Image Vertex Buffers"
+ name="Collect UI Image Vertex Buffers">
+ <menu_item_check.on_check
+ function="CheckControl"
+ parameter="CollectUIImageVertexBuffers" />
+ <menu_item_check.on_click
+ function="ToggleControl"
+ parameter="CollectUIImageVertexBuffers" />
+ </menu_item_check>
<menu_item_separator />
<menu_item_check
label="Enable Shader Cache"