summaryrefslogtreecommitdiff
path: root/indra/llrender
diff options
context:
space:
mode:
authorAndrey Lihatskiy <alihatskiy@productengine.com>2026-08-13 04:19:15 +0300
committerAndrey Lihatskiy <alihatskiy@productengine.com>2026-08-13 04:19:15 +0300
commitf376c52ee2a22b3b58263d86566394e80626fc1a (patch)
tree012d0585b39ef5cd1eac25f4f783d930ff50dff6 /indra/llrender
parent21bb60fc65e7fa6c3cf472a29cda220f653af1d8 (diff)
parente3a92a3f8e7d6df3772adcecb037149e6fcf0f3e (diff)
Merge branch 'geenz/linux-to-develop' into marchcat/lua-linux-develop
# Conflicts: # .github/workflows/build.yaml # indra/llinventory/llinventory.cpp # indra/newview/CMakeLists.txt # indra/newview/llappviewer.cpp # indra/newview/llappviewerwin32.cpp # indra/newview/llviewermenu.cpp # indra/newview/llvoiceclient.cpp # indra/newview/skins/default/xui/en/floater_preferences_graphics_advanced.xml
Diffstat (limited to 'indra/llrender')
-rw-r--r--indra/llrender/llfontfreetype.cpp93
-rw-r--r--indra/llrender/llfontfreetype.h6
-rw-r--r--indra/llrender/llfontgl.cpp14
-rw-r--r--indra/llrender/llgl.cpp11
-rw-r--r--indra/llrender/llimagegl.cpp150
-rw-r--r--indra/llrender/llimagegl.h2
-rw-r--r--indra/llrender/llrender.cpp6
-rw-r--r--indra/llrender/llshadermgr.cpp90
-rw-r--r--indra/llrender/lluiimage.cpp211
-rw-r--r--indra/llrender/lluiimage.h126
-rw-r--r--indra/llrender/lluiimage.inl80
-rw-r--r--indra/llrender/llvertexbuffer.cpp13
12 files changed, 717 insertions, 85 deletions
diff --git a/indra/llrender/llfontfreetype.cpp b/indra/llrender/llfontfreetype.cpp
index cc4d6ad5ef..5af6b61513 100644
--- a/indra/llrender/llfontfreetype.cpp
+++ b/indra/llrender/llfontfreetype.cpp
@@ -110,6 +110,7 @@ LLFontManager::~LLFontManager()
LLFontGlyphInfo::LLFontGlyphInfo(U32 index, EFontGlyphType glyph_type)
: mGlyphIndex(index),
mGlyphType(glyph_type),
+ mChar(0),
mWidth(0), // In pixels
mHeight(0), // In pixels
mXAdvance(0.f), // In pixels
@@ -127,6 +128,7 @@ LLFontGlyphInfo::LLFontGlyphInfo(U32 index, EFontGlyphType glyph_type)
LLFontGlyphInfo::LLFontGlyphInfo(const LLFontGlyphInfo& fgi)
: mGlyphIndex(fgi.mGlyphIndex)
, mGlyphType(fgi.mGlyphType)
+ , mChar(fgi.mChar)
, mWidth(fgi.mWidth)
, mHeight(fgi.mHeight)
, mXAdvance(fgi.mXAdvance)
@@ -151,7 +153,8 @@ LLFontFreetype::LLFontFreetype()
mFTFace(nullptr),
mRenderGlyphCount(0),
mStyle(0),
- mPointSize(0)
+ mPointSize(0),
+ mMaxDigitWidth(0.0f)
{
}
@@ -347,6 +350,10 @@ F32 LLFontFreetype::getXAdvance(llwchar wch) const
LLFontGlyphInfo* gi = getGlyphInfo(wch, EFontGlyphType::Unspecified);
if (gi)
{
+ if (wch >= '0' && wch <= '9' && mMaxDigitWidth > 0.0f)
+ {
+ return mMaxDigitWidth;
+ }
return gi->mXAdvance;
}
else
@@ -367,6 +374,12 @@ F32 LLFontFreetype::getXAdvance(const LLFontGlyphInfo* glyph) const
if (mFTFace == nullptr)
return 0.0;
+ // Use max digit width for tabular numbers
+ if (mWeight > 0 && glyph->mChar >= '0' && glyph->mChar <= '9' && mMaxDigitWidth > 0.0f)
+ {
+ return mMaxDigitWidth;
+ }
+
return glyph->mXAdvance;
}
@@ -388,8 +401,27 @@ F32 LLFontFreetype::getXKerning(const LLFontGlyphInfo* left_glyph_info, const LL
if (mFTFace == nullptr)
return 0.0;
- U32 left_glyph = left_glyph_info ? left_glyph_info->mGlyphIndex : 0;
- U32 right_glyph = right_glyph_info ? right_glyph_info->mGlyphIndex : 0;
+ U32 left_glyph = 0;
+ U32 right_glyph = 0;
+
+ if (left_glyph_info)
+ {
+ if (mWeight > 0 && left_glyph_info->mChar >= '0' && left_glyph_info->mChar <= '9')
+ {
+ // Disable kerning for digits when using tabular numbers
+ return 0.0;
+ }
+ left_glyph = left_glyph_info->mGlyphIndex;
+ }
+ if (right_glyph_info)
+ {
+ if (mWeight > 0 && right_glyph_info->mChar >= '0' && right_glyph_info->mChar <= '9')
+ {
+ // Disable kerning for digits when using tabular numbers
+ return 0.0;
+ }
+ right_glyph = right_glyph_info->mGlyphIndex;
+ }
FT_Vector delta;
@@ -589,6 +621,7 @@ LLFontGlyphInfo* LLFontFreetype::addGlyphFromFont(const LLFontFreetype *fontp, l
mFontBitmapCachep->nextOpenPos(width, pos_x, pos_y, bitmap_glyph_type, bitmap_num);
LLFontGlyphInfo* gi = new LLFontGlyphInfo(glyph_index, requested_glyph_type);
+ gi->mChar = wch;
gi->mXBitmapOffset = pos_x;
gi->mYBitmapOffset = pos_y;
gi->mBitmapEntry = std::make_pair(bitmap_glyph_type, bitmap_num);
@@ -605,6 +638,14 @@ LLFontGlyphInfo* LLFontFreetype::addGlyphFromFont(const LLFontFreetype *fontp, l
gi->mXAdvance = fontp->mFTFace->glyph->advance.x / 64.f;
gi->mYAdvance = fontp->mFTFace->glyph->advance.y / 64.f;
+ if (mWeight > 0 && wch >= '0' && wch <= '9')
+ {
+ // Digits are supposed to be preloaded, and buffers
+ // refresh when new chars get added, so this lazy load
+ // should not cause any issues.
+ mMaxDigitWidth = llmax(mMaxDigitWidth, gi->mXAdvance);
+ }
+
insertGlyphInfo(wch, gi);
if (requested_glyph_type != bitmap_glyph_type)
@@ -761,7 +802,50 @@ void LLFontFreetype::renderGlyph(EFontGlyphType bitmap_type, U32 glyph_index, ll
llassert_always_msg(FT_Err_Ok == error, message.c_str());
}
- llassert_always(! FT_Render_Glyph(mFTFace->glyph, gFontRenderMode) );
+ // TODO: Make this more sturdy, make asserts/ll_errs conditional
+ // to non-critical characters.
+ // Temporarily leaving them for data gathering, but unicode chars
+ // like emojis should not cause the app to crash and should either
+ // fallback to some predetermined bitmap or simply return.
+
+ // Verify glyph slot is valid
+ if (!mFTFace->glyph)
+ {
+ LL_ERRS() << "FT_Load_Glyph succeeded but glyph slot is null for wchar " << llformat("U+%xu", U32(wch)) << LL_ENDL;
+ return;
+ }
+
+ // Check if bitmap buffer is already allocated
+ // It can potentially be preallocated for:
+ // 1. SVG/color glyphs rendered by FreeType's SVG_RendererHooks
+ // 2. Embedded bitmap fonts
+ // 3. Some Color emoji that use FT_LOAD_COLOR
+ if (!mFTFace->glyph->bitmap.buffer)
+ {
+ error = FT_Render_Glyph(mFTFace->glyph, gFontRenderMode);
+ if (error != FT_Err_Ok)
+ {
+ std::string render_message = llformat(
+ "Error %d (%s) rendering wchar %u glyph %u: format=%lu, pixel_mode=%d, render_mode=%d",
+ error, FT_Error_String(error), wch, glyph_index,
+ (unsigned long)mFTFace->glyph->format, mFTFace->glyph->bitmap.pixel_mode, gFontRenderMode);
+
+ // Try with FT_RENDER_MODE_NORMAL as fallback
+ if (gFontRenderMode != FT_RENDER_MODE_NORMAL)
+ {
+ LL_WARNS_ONCE() << render_message << LL_ENDL;
+ error = FT_Render_Glyph(mFTFace->glyph, FT_RENDER_MODE_NORMAL);
+ if (error != FT_Err_Ok)
+ {
+ LL_ERRS() << "Fallback to FT_RENDER_MODE_NORMAL failed. " << render_message << LL_ENDL;
+ }
+ }
+ else
+ {
+ LL_ERRS() << render_message << LL_ENDL;
+ }
+ }
+ }
mRenderGlyphCount++;
}
@@ -797,6 +881,7 @@ void LLFontFreetype::resetBitmapCache()
}
mCharGlyphInfoMap.clear();
mFontBitmapCachep->reset();
+ mMaxDigitWidth = 0.0f;
// Adding default glyph is skipped for fallback fonts here as well as in loadFace().
// This if was added as fix for EXT-4971.
diff --git a/indra/llrender/llfontfreetype.h b/indra/llrender/llfontfreetype.h
index 12a4052cae..3191ca8df7 100644
--- a/indra/llrender/llfontfreetype.h
+++ b/indra/llrender/llfontfreetype.h
@@ -77,6 +77,8 @@ struct LLFontGlyphInfo
U32 mGlyphIndex;
EFontGlyphType mGlyphType;
+ llwchar mChar;
+
// Metrics
S32 mWidth; // In pixels
@@ -148,6 +150,9 @@ public:
F32 getXKerning(llwchar char_left, llwchar char_right) const; // Get the kerning between the two characters
F32 getXKerning(const LLFontGlyphInfo* left_glyph_info, const LLFontGlyphInfo* right_glyph_info) const; // Get the kerning between the two characters
+ F32 getMaxDigitWidth() const { return mMaxDigitWidth; }
+ S32 getFontWeight() const { return mWeight; }
+
LLFontGlyphInfo* getGlyphInfo(llwchar wch, EFontGlyphType glyph_type) const;
void reset(F32 vert_dpi, F32 horz_dpi);
@@ -186,6 +191,7 @@ private:
F32 mAscender;
F32 mDescender;
F32 mLineHeight;
+ mutable F32 mMaxDigitWidth;
LLFT_Face mFTFace;
diff --git a/indra/llrender/llfontgl.cpp b/indra/llrender/llfontgl.cpp
index 5d99c35047..bb5a12375e 100644
--- a/indra/llrender/llfontgl.cpp
+++ b/indra/llrender/llfontgl.cpp
@@ -345,6 +345,14 @@ S32 LLFontGL::render(const LLWString &wstr, S32 begin_offset, F32 x, F32 y, cons
break;
}
+ // Calculate horizontal offset for tabular numbers (center narrow digits)
+ F32 x_offset = 0.0f;
+ if (mFontFreetype->getFontWeight() > 0 && fgi->mChar >= '0' && fgi->mChar <= '9' && mFontFreetype->getMaxDigitWidth() > 0.0f)
+ {
+ // use mXAdvance directly here, since we don't want to get max width instead.
+ x_offset = (mFontFreetype->getMaxDigitWidth() - fgi->mXAdvance) * 0.5f;
+ }
+
// Draw the text at the appropriate location
//Specify vertices and texture coordinates
LLRectf uv_rect((fgi->mXBitmapOffset) * inv_width,
@@ -352,9 +360,9 @@ S32 LLFontGL::render(const LLWString &wstr, S32 begin_offset, F32 x, F32 y, cons
(fgi->mXBitmapOffset + fgi->mWidth) * inv_width,
(fgi->mYBitmapOffset - PAD_UVY) * inv_height);
// snap glyph origin to whole screen pixel
- LLRectf screen_rect((F32)ll_round(cur_render_x + (F32)fgi->mXBearing),
+ LLRectf screen_rect((F32)ll_round(cur_render_x + (F32)fgi->mXBearing + x_offset),
(F32)ll_round(cur_render_y + (F32)fgi->mYBearing),
- (F32)ll_round(cur_render_x + (F32)fgi->mXBearing) + (F32)fgi->mWidth,
+ (F32)ll_round(cur_render_x + (F32)fgi->mXBearing + x_offset) + (F32)fgi->mWidth,
(F32)ll_round(cur_render_y + (F32)fgi->mYBearing) - (F32)fgi->mHeight);
if (glyph_count >= GLYPH_BATCH_SIZE)
@@ -375,7 +383,7 @@ S32 LLFontGL::render(const LLWString &wstr, S32 begin_offset, F32 x, F32 y, cons
col, style_to_add, shadow, drop_shadow_strength);
chars_drawn++;
- cur_x += fgi->mXAdvance;
+ cur_x += mFontFreetype->getXAdvance(fgi);
cur_y += fgi->mYAdvance;
llwchar next_char = wstr[i+1];
diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp
index 5357a79bae..8545f634a3 100644
--- a/indra/llrender/llgl.cpp
+++ b/indra/llrender/llgl.cpp
@@ -1042,11 +1042,6 @@ void LLGLManager::initWGL()
{
LL_WARNS("RenderInit") << "No ARB WGL PBuffer extensions" << LL_ENDL;
}
-
- if(!mGLExtensions.contains("WGL_ARB_render_texture"))
- {
- LL_WARNS("RenderInit") << "No ARB WGL render texture extensions" << LL_ENDL;
- }
#endif
}
@@ -1264,7 +1259,7 @@ bool LLGLManager::initGL()
}
if (mVRAM != 0)
{
- LL_WARNS("RenderInit") << "VRAM Detected (AMDAssociations):" << mVRAM << LL_ENDL;
+ LL_INFOS("RenderInit") << "VRAM Detected (AMDAssociations):" << mVRAM << LL_ENDL;
}
} else
#endif
@@ -1279,7 +1274,7 @@ bool LLGLManager::initGL()
if (mVRAM != 0)
{
- LL_WARNS("RenderInit") << "VRAM Detected (GLXMesaQueryRenderer):" << mVRAM << LL_ENDL;
+ LL_INFOS("RenderInit") << "VRAM Detected (GLXMesaQueryRenderer):" << mVRAM << LL_ENDL;
}
}
}
@@ -1295,7 +1290,7 @@ bool LLGLManager::initGL()
if (mVRAM != 0)
{
- LL_WARNS("RenderInit") << "VRAM Detected (NVXGpuMemoryInfo):" << mVRAM << LL_ENDL;
+ LL_INFOS("RenderInit") << "VRAM Detected (NVXGpuMemoryInfo):" << mVRAM << LL_ENDL;
}
}
diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp
index 868284031d..fafc92b5d0 100644
--- a/indra/llrender/llimagegl.cpp
+++ b/indra/llrender/llimagegl.cpp
@@ -540,6 +540,7 @@ void LLImageGL::init(bool usemipmaps, bool allow_compression)
mIsMask = false;
mNeedsAlphaAndPickMask = true ;
+ mAlphaAnalysisSerial = 0;
mAlphaStride = 0 ;
mAlphaOffset = 0 ;
@@ -1945,6 +1946,9 @@ void LLImageGL::destroyGLTexture()
mTexName = 0;
mGLTextureCreated = false ;
}
+
+ // Invalidate pending jobs
+ ++mAlphaAnalysisSerial;
}
//force to invalidate the gl texture, most likely a sculpty texture
@@ -2104,6 +2108,9 @@ void LLImageGL::setNeedsAlphaAndPickMask(bool need_mask)
{
mAlphaOffset = INVALID_OFFSET ;
mIsMask = false;
+
+ // Invalidate pending jobs
+ ++mAlphaAnalysisSerial;
}
}
}
@@ -2181,11 +2188,16 @@ void LLImageGL::calcAlphaChannelOffsetAndStride()
}
}
-void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h)
+bool LLImageGL::analyzeAlphaData(
+ const void* data_in,
+ U32 w,
+ U32 h,
+ S8 alpha_offset,
+ S8 alpha_stride)
{
- if(!data_in || sSkipAnalyzeAlpha || !mNeedsAlphaAndPickMask)
+ if (!data_in)
{
- return ;
+ return false;
}
LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
@@ -2194,7 +2206,7 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h)
U32 alphatotal = 0;
U32 sample[16];
- memset(sample, 0, sizeof(U32)*16);
+ memset(sample, 0, sizeof(U32) * 16);
// generate histogram of quantized alpha.
// also add-in the histogram of a 2x2 box-sampled version. The idea is
@@ -2205,46 +2217,46 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h)
{
llassert(w % 2 == 0);
llassert(h % 2 == 0);
- const GLubyte* rowstart = ((const GLubyte*) data_in) + mAlphaOffset;
+ const GLubyte* rowstart = ((const GLubyte*)data_in) + alpha_offset;
for (U32 y = 0; y < h; y += 2)
{
- const GLubyte* current = rowstart;
+ const unsigned char* current = rowstart;
for (U32 x = 0; x < w; x += 2)
{
const U32 s1 = current[0];
alphatotal += s1;
- const U32 s2 = current[w * mAlphaStride];
+ const U32 s2 = current[w * alpha_stride];
alphatotal += s2;
- current += mAlphaStride;
+ current += alpha_stride;
const U32 s3 = current[0];
alphatotal += s3;
- const U32 s4 = current[w * mAlphaStride];
+ const U32 s4 = current[w * alpha_stride];
alphatotal += s4;
- current += mAlphaStride;
+ current += alpha_stride;
- ++sample[s1/16];
- ++sample[s2/16];
- ++sample[s3/16];
- ++sample[s4/16];
+ ++sample[s1 / 16];
+ ++sample[s2 / 16];
+ ++sample[s3 / 16];
+ ++sample[s4 / 16];
- const U32 asum = (s1+s2+s3+s4);
+ const U32 asum = (s1 + s2 + s3 + s4);
alphatotal += asum;
- sample[asum/(16*4)] += 4;
+ sample[asum / (16 * 4)] += 4;
}
- rowstart += 2 * w * mAlphaStride;
+ rowstart += 2 * w * alpha_stride;
}
length *= 2; // we sampled everything twice, essentially
}
else
{
- const GLubyte* current = ((const GLubyte*) data_in) + mAlphaOffset;
+ const unsigned char* current = ((const unsigned char*)data_in) + alpha_offset;
for (U32 i = 0; i < length; i++)
{
const U32 s1 = *current;
alphatotal += s1;
- ++sample[s1/16];
- current += mAlphaStride;
+ ++sample[s1 / 16];
+ current += alpha_stride;
}
}
@@ -2271,15 +2283,103 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h)
upperhalftotal += sample[i];
}
- if (midrangetotal > length/48 || // lots of midrange, or
- (lowerhalftotal == length && alphatotal != 0) || // all close to transparent but not all totally transparent, or
- (upperhalftotal == length && alphatotal != 255*length)) // all close to opaque but not all totally opaque
+ if (midrangetotal > length / 48 ||
+ (lowerhalftotal == length && alphatotal != 0) ||
+ (upperhalftotal == length && alphatotal != 255 * length))
{
- mIsMask = false; // not suitable for masking
+ return false; // not suitable for masking
}
else
{
- mIsMask = true;
+ return true; // is a mask
+ }
+}
+
+void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h)
+{
+ // if mNeedsAlphaAndPickMask is true, then offset and stride are supposed to be valid.
+ if (!data_in || sSkipAnalyzeAlpha || !mNeedsAlphaAndPickMask)
+ return;
+
+ // Already on a worker thread or a small image - analyze immediately
+ if (!on_main_thread() || (w < 64 && h < 64))
+ {
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
+ // Should have been incremented by destroyGLTexture,
+ // but increment either way, for extra safety.
+ ++mAlphaAnalysisSerial;
+
+ mIsMask = analyzeAlphaData(data_in, w, h, mAlphaOffset, mAlphaStride);
+ return;
+ }
+
+ // On main thread - defer to worker thread
+
+ // Capture context
+ const S8 alpha_offset = mAlphaOffset;
+ const S8 alpha_stride = mAlphaStride;
+ const U32 request_serial = ++mAlphaAnalysisSerial;
+
+ // Copy data for worker thread
+ const size_t data_size = size_t(w) * size_t(h) * size_t(alpha_stride);
+ U8* data_copy = new (std::nothrow) U8[data_size];
+
+ if (!data_copy)
+ {
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
+ mIsMask = analyzeAlphaData(data_in, w, h, alpha_offset, alpha_stride);
+ return;
+ }
+ memcpy(data_copy, static_cast<const U8*>(data_in), data_size);
+
+ // Viewer can rapidly switch between lods, which would invalidate
+ // previous analysis results.
+ // Use a serial number to filter out obsolete analysis results.
+
+ ref(); // Keep texture alive
+
+ auto mainq = mMainQueue.lock();
+
+ // Get the GL thread queue
+ auto workerq = LLImageGLThread::sEnabledTextures ?
+ LL::WorkQueue::getInstance("LLImageGL") : // Use the image processing queue if available
+ LL::WorkQueue::getInstance("General"); // Fallback to general
+
+ bool posted_job = false;
+ if (mainq && workerq)
+ {
+ posted_job = mainq->postTo(
+ workerq,
+ // Worker thread: analyze alpha
+ [data_copy, w, h, alpha_offset, alpha_stride]() -> bool
+ {
+ LL_PROFILE_ZONE_NAMED("Deffered alpha mask analysis");
+ bool is_mask = LLImageGL::analyzeAlphaData(data_copy, w, h, alpha_offset, alpha_stride);
+ delete[] data_copy;
+ return is_mask;
+ },
+ // Main thread: apply result
+ [this, request_serial](bool is_mask)
+ {
+ // Only apply if no newer analysis has been requested
+ if (mAlphaAnalysisSerial == request_serial)
+ {
+ mIsMask = is_mask;
+ }
+ unref();
+ }
+ );
+
+ // Conservative default until analysis completes
+ mIsMask = false;
+ }
+ if (!posted_job)
+ {
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
+ // Queues not available - fall back to synchronous analysis
+ delete[] data_copy;
+ mIsMask = analyzeAlphaData(data_in, w, h, mAlphaOffset, mAlphaStride);
+ unref();
}
}
diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h
index 773b647d69..0e186ca833 100644
--- a/indra/llrender/llimagegl.h
+++ b/indra/llrender/llimagegl.h
@@ -111,6 +111,7 @@ public:
protected:
virtual ~LLImageGL();
+ static bool analyzeAlphaData(const void* data_in, U32 w, U32 h, S8 alpha_offset, S8 alpha_stride);
void analyzeAlpha(const void* data_in, U32 w, U32 h);
void calcAlphaChannelOffsetAndStride();
@@ -242,6 +243,7 @@ private:
bool mIsMask;
bool mNeedsAlphaAndPickMask;
+ LLAtomicU32 mAlphaAnalysisSerial; // for request tracking.
S8 mAlphaStride ;
S8 mAlphaOffset ;
diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp
index 8bbc9a8176..6a39a34c4e 100644
--- a/indra/llrender/llrender.cpp
+++ b/indra/llrender/llrender.cpp
@@ -968,7 +968,7 @@ void LLRender::syncMatrices()
shader->uniformMatrix3fv(LLShaderMgr::NORMAL_MATRIX, 1, GL_FALSE, norm_mat);
}
- if (shader->getUniformLocation(LLShaderMgr::INVERSE_MODELVIEW_MATRIX))
+ if (shader->getUniformLocation(LLShaderMgr::INVERSE_MODELVIEW_MATRIX) >= 0)
{
shader->uniformMatrix4fv(LLShaderMgr::INVERSE_MODELVIEW_MATRIX, 1, GL_FALSE, glm::value_ptr(cached_inv_mdv));
}
@@ -1000,14 +1000,14 @@ void LLRender::syncMatrices()
// GZ: This was previously disabled seemingly due to a bug involving the deferred renderer's regular pushing and popping of mats.
// We're reenabling this and cleaning up the code around that - that would've been the appropriate course initially.
// Anything beyond the standard proj and inv proj mats are special cases. Please setup special uniforms accordingly in the future.
- if (shader->getUniformLocation(LLShaderMgr::INVERSE_PROJECTION_MATRIX))
+ if (shader->getUniformLocation(LLShaderMgr::INVERSE_PROJECTION_MATRIX) >= 0)
{
glm::mat4 inv_proj = glm::inverse(mat);
shader->uniformMatrix4fv(LLShaderMgr::INVERSE_PROJECTION_MATRIX, 1, false, glm::value_ptr(inv_proj));
}
// Used by some full screen effects - such as full screen lights, glow, etc.
- if (shader->getUniformLocation(LLShaderMgr::IDENTITY_MATRIX))
+ if (shader->getUniformLocation(LLShaderMgr::IDENTITY_MATRIX) >= 0)
{
shader->uniformMatrix4fv(LLShaderMgr::IDENTITY_MATRIX, 1, GL_FALSE, glm::value_ptr(glm::identity<glm::mat4>()));
}
diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp
index d8a620cf5b..9f7c244fe9 100644
--- a/indra/llrender/llshadermgr.cpp
+++ b/indra/llrender/llshadermgr.cpp
@@ -1085,7 +1085,14 @@ void LLShaderMgr::persistShaderCacheMetadata()
return;
}
- LL_INFOS("ShaderMgr") << "Persisting shader cache metadata to disk" << LL_ENDL;
+ if (mShaderCacheDir.empty() || !LLFile::isdir(mShaderCacheDir))
+ {
+ LL_WARNS("ShaderMgr") << "Invalid shader cache directory: " << mShaderCacheDir << LL_ENDL;
+ return;
+ }
+
+ size_t total_entries = mShaderBinaryCache.size();
+ LL_INFOS("ShaderMgr") << "Persisting shader " << (S32)total_entries << " cache metadata entries to disk" << LL_ENDL;
LLSD out;
// Settings and shader cache get saved at different time, thus making
@@ -1096,6 +1103,8 @@ void LLShaderMgr::persistShaderCacheMetadata()
out["shaders"] = LLSD::emptyMap();
LLSD &shaders = out["shaders"];
+ size_t removed = 0;
+
static const F32 LRU_TIME = (60.f * 60.f) * 24.f * 7.f; // 14 days
const F32 current_time = (F32)LLTimer::getTotalSeconds();
for (auto it = mShaderBinaryCache.begin(); it != mShaderBinaryCache.end();)
@@ -1104,8 +1113,9 @@ void LLShaderMgr::persistShaderCacheMetadata()
if ((shader_metadata.mLastUsedTime + LRU_TIME) < current_time)
{
std::string shader_path = gDirUtilp->add(mShaderCacheDir, it->first.asString() + ".shaderbin");
- LLFile::remove(shader_path);
+ LLFile::remove(shader_path, ENOENT);
it = mShaderBinaryCache.erase(it);
+ removed++;
}
else
{
@@ -1119,14 +1129,32 @@ void LLShaderMgr::persistShaderCacheMetadata()
}
std::string meta_out_path = gDirUtilp->add(mShaderCacheDir, "shaderdata.llsd");
+ if (shaders.size() == 0)
+ {
+ LL_WARNS("ShaderMgr") << "No shader cache entries to persist, removing cache metadata file" << LL_ENDL;
+ LLFile::remove(meta_out_path);
+ return;
+ }
+
llofstream outstream(meta_out_path, std::ios_base::out | std::ios_base::binary);
if (!outstream.is_open())
{
LL_WARNS("ShaderMgr") << "Failed to open file. Unable to save shader cache to: " << mShaderCacheDir << LL_ENDL;
return;
}
+
LLSDSerialize::toBinary(out, outstream);
+ if (outstream.fail())
+ {
+ LL_WARNS("ShaderMgr") << "Failed to serialize shader cache metadata" << LL_ENDL;
+ outstream.close();
+ LLFile::remove(meta_out_path); // Clean up partial write
+ return;
+ }
outstream.close();
+
+ LL_INFOS("ShaderMgr") << "Persisted " << (S32)shaders.size()
+ << " entries. Removed " << (S32)removed << " entries." << LL_ENDL;
}
bool LLShaderMgr::loadCachedProgramBinary(LLGLSLShader* shader)
@@ -1140,34 +1168,56 @@ bool LLShaderMgr::loadCachedProgramBinary(LLGLSLShader* shader)
{
std::string in_path = gDirUtilp->add(mShaderCacheDir, shader->mShaderHash.asString() + ".shaderbin");
auto& shader_info = binary_iter->second;
- if (shader_info.mBinaryLength > 0)
+
+ try
{
- std::vector<U8> in_data;
- in_data.resize(shader_info.mBinaryLength);
- std::error_code ec;
- LLFile filep = LLFile(in_path, LLFile::in | LLFile::binary, ec);
- if (!ec && (bool)filep)
+ constexpr GLsizei MAX_SHADER_BINARY_SIZE = 1024 * 1024; // 1 MB, normally around 10KB
+ if (shader_info.mBinaryLength > 0 && shader_info.mBinaryLength <= MAX_SHADER_BINARY_SIZE)
{
- size_t result = filep.read(in_data.data(), in_data.size(), ec);
- filep.close();
+ std::vector<U8> in_data;
+ in_data.resize(shader_info.mBinaryLength);
- if (result == in_data.size())
+ std::error_code ec;
+ LLFile filep = LLFile(in_path, LLFile::in | LLFile::binary, ec);
+ if (!ec && (bool)filep)
{
- GLenum error = glGetError(); // Clear current error
- glProgramBinary(shader->mProgramObject, shader_info.mBinaryFormat, in_data.data(), shader_info.mBinaryLength);
+ size_t result = filep.read(in_data.data(), in_data.size(), ec);
+ filep.close();
+
+ if (result == in_data.size())
+ {
+ GLenum error = glGetError(); // Clear current error
+ glProgramBinary(shader->mProgramObject, shader_info.mBinaryFormat, in_data.data(), shader_info.mBinaryLength);
- error = glGetError();
- GLint success = GL_TRUE;
- glGetProgramiv(shader->mProgramObject, GL_LINK_STATUS, &success);
- if (error == GL_NO_ERROR && success == GL_TRUE)
+ error = glGetError();
+ GLint success = GL_TRUE;
+ glGetProgramiv(shader->mProgramObject, GL_LINK_STATUS, &success);
+ if (error == GL_NO_ERROR && success == GL_TRUE)
+ {
+ binary_iter->second.mLastUsedTime = (F32)LLTimer::getTotalSeconds();
+ LL_INFOS() << "Loaded cached binary for shader: " << shader->mName << LL_ENDL;
+ return true;
+ }
+ }
+ else
{
- binary_iter->second.mLastUsedTime = (F32)LLTimer::getTotalSeconds();
- LL_INFOS() << "Loaded cached binary for shader: " << shader->mName << LL_ENDL;
- return true;
+ LL_WARNS("ShaderMgr") << "Incomplete read of shader binary. Expected: "
+ << in_data.size() << ", read: " << result << LL_ENDL;
}
}
}
}
+ catch (const std::bad_alloc&)
+ {
+ LL_WARNS("ShaderMgr") << "Failed to allocate memory for shader binary ("
+ << shader_info.mBinaryLength << " bytes) for: "
+ << shader->mName << LL_ENDL;
+ }
+ catch (const std::exception& err)
+ {
+ LL_WARNS("ShaderMgr") << "Caught exception " << err.what() << " while loading shader binary for: " << shader->mName << LL_ENDL;
+ }
+
//an error occured, normally we would print log but in this case it means the shader needs recompiling.
LL_INFOS() << "Failed to load cached binary for shader: " << shader->mName << " falling back to compilation" << LL_ENDL;
LLFile::remove(in_path);
diff --git a/indra/llrender/lluiimage.cpp b/indra/llrender/lluiimage.cpp
index dc18bf16bf..d0b6bcbf66 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,12 @@
// Project includes
#include "lluiimage.h"
+#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 +55,12 @@ LLUIImage::LLUIImage(const std::string& name, LLPointer<LLTexture> image)
LLUIImage::~LLUIImage()
{
delete mImageLoaded;
+
+ // 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();
}
S32 LLUIImage::getWidth() const
@@ -63,6 +75,199 @@ 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
+{
+ 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, tex_name);
+
+ auto it = mDisplayLists.find(key);
+ if (it != mDisplayLists.end())
+ {
+ 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;
+
+ 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();
+
+ // 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();
+
+ // 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
+ // 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
+ 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.
+ if (sCleanupIndex > 0)
+ {
+ sCleanupIndex--;
+ }
+ else if (sImageList.empty())
+ {
+ sCleanupIndex = 0;
+ }
+ else
+ {
+ sCleanupIndex = sImageList.size() - 1;
+ }
+ 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.
+ if (sCleanupIndex > 0)
+ {
+ sCleanupIndex--;
+ }
+ else if (sImageList.empty())
+ {
+ sCleanupIndex = 0;
+ }
+ else
+ {
+ sCleanupIndex = sImageList.size() - 1;
+ }
+ }
+}
+
+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 +282,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 +329,8 @@ void LLUIImage::onImageLoaded()
{
(*mImageLoaded)();
}
+
+ invalidateDisplayLists();
}
namespace LLInitParam
diff --git a/indra/llrender/lluiimage.h b/indra/llrender/lluiimage.h
index 7dde84f295..0998ac17b4 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,10 +34,15 @@
#include "llinitparam.h"
#include "lltexture.h"
#include "llrender2dutils.h"
+#include "llvertexbuffer.h"
#include <boost/signals2.hpp>
+#include <bit>
+#include <chrono>
#include <type_traits>
+#include <unordered_map>
+#include <vector>
extern const LLColor4 UI_VERTEX_COLOR;
@@ -58,21 +63,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 +97,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 +116,107 @@ 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:
+ // Packed key for identifying unique display list configurations
+ struct PackedKey
+ {
+ 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
+ {
+ return position == other.position &&
+ color_flags == other.color_flags &&
+ dimensions == other.dimensions &&
+ translate == other.translate &&
+ scale == other.scale &&
+ tex_name == other.tex_name;
+ }
+
+ struct Hash
+ {
+ std::size_t operator()(const PackedKey& key) const
+ {
+ return static_cast<std::size_t>(key.position ^ key.color_flags ^
+ 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,
+ LLGLuint texture_name)
+ {
+ auto float_to_u8 = [](F32 f) -> uint8_t {
+ return static_cast<uint8_t>(llclamp(f * 255.0f, 0.0f, 255.0f));
+ };
+
+ 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]));
+
+ // Store full 32-bit texture name in lower 32 bits (upper 32 bits unused/zero)
+ uint64_t tex = static_cast<uint64_t>(texture_name);
+
+ return PackedKey{ pos, col, dim, trns, scl, tex };
+ }
+ };
+
+ // 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 +226,14 @@ protected:
EScaleStyle mScaleStyle;
mutable S32 mCachedW;
mutable S32 mCachedH;
+
+ // Display list cache - now using PackedKey directly
+ mutable std::unordered_map<PackedKey, CachedDisplayList, PackedKey::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..d40835a0f4 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 image, so don't register them under draw zone
+ 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/llrender/llvertexbuffer.cpp b/indra/llrender/llvertexbuffer.cpp
index d59ddd0fec..9b839350ae 100644
--- a/indra/llrender/llvertexbuffer.cpp
+++ b/indra/llrender/llvertexbuffer.cpp
@@ -939,12 +939,14 @@ void LLVertexBuffer::initClass(LLWindow* window)
{
llassert(sVBOPool == nullptr);
+#if LL_DARWIN || LL_ARM64
if (gGLManager.mIsApple)
{
LL_INFOS() << "VBO Pooling Disabled" << LL_ENDL;
sVBOPool = new LLAppleVBOPool();
}
else
+#endif
{
LL_INFOS() << "VBO Pooling Enabled" << LL_ENDL;
sVBOPool = new LLDefaultVBOPool();
@@ -1282,7 +1284,12 @@ U8* LLVertexBuffer::mapVertexBuffer(LLVertexBuffer::AttributeType type, U32 inde
count = mNumVerts - index;
}
+#if LL_DARWIN || LL_ARM64
+ // Region tracking not needed on apple silicon - it recreates entire buffer
+ // While mIsApple can be encountered under windows, this is a
+ // macOS OpenGL behavior workaround. LL_ARM64 check might be not needed
if (!gGLManager.mIsApple)
+#endif
{
U32 start = mOffsets[type] + sTypeSize[type] * index;
U32 end = start + sTypeSize[type] * count-1;
@@ -1319,7 +1326,9 @@ U8* LLVertexBuffer::mapIndexBuffer(U32 index, S32 count)
count = mNumIndices-index;
}
+#if LL_DARWIN || LL_ARM64
if (!gGLManager.mIsApple)
+#endif
{
U32 start = sizeof(U16) * index;
U32 end = start + sizeof(U16) * count-1;
@@ -1354,6 +1363,7 @@ U8* LLVertexBuffer::mapIndexBuffer(U32 index, S32 count)
// dst -- mMappedData or mMappedIndexData
void LLVertexBuffer::flush_vbo(GLenum target, U32 start, U32 end, void* data, U8* dst)
{
+#if LL_DARWIN || LL_ARM64
if (gGLManager.mIsApple)
{
// on OS X, flush_vbo doesn't actually write to the GL buffer, so be sure to call
@@ -1365,6 +1375,7 @@ void LLVertexBuffer::flush_vbo(GLenum target, U32 start, U32 end, void* data, U8
memcpy(dst+start, data, end-start+1);
}
else
+#endif
{
llassert(target == GL_ARRAY_BUFFER ? sGLRenderBuffer == mGLBuffer : sGLRenderIndices == mGLIndices);
@@ -1420,6 +1431,7 @@ void LLVertexBuffer::_unmapBuffer()
}
};
+#if LL_DARWIN || LL_ARM64
if (gGLManager.mIsApple)
{
STOP_GLERROR;
@@ -1462,6 +1474,7 @@ void LLVertexBuffer::_unmapBuffer()
STOP_GLERROR;
}
else
+#endif // LL_DARWIN || LL_ARM64
{
if (!mMappedVertexRegions.empty())
{