summaryrefslogtreecommitdiff
path: root/indra/llrender
diff options
context:
space:
mode:
authorErik Kundiman <erik@megapahit.org>2026-06-13 14:20:22 +0800
committerErik Kundiman <erik@megapahit.org>2026-06-13 17:44:11 +0800
commit191669d38d232d2bd61a0dd2252c7a3543a2b467 (patch)
treec1783f307fd217f38b31cebbe830ba0481925f8a /indra/llrender
parent47b583e9caa3388cd41f70e8de0a5a950082979d (diff)
parent663bf4d3eba16e1d0a781ac5261541e7e2d6b4f2 (diff)
Merge tag 'Second_Life_Release#663bf4d3-26.3' into 26.3
Diffstat (limited to 'indra/llrender')
-rw-r--r--indra/llrender/llfontfreetype.cpp43
-rw-r--r--indra/llrender/llfontvertexbuffer.cpp77
-rw-r--r--indra/llrender/llfontvertexbuffer.h46
-rw-r--r--indra/llrender/llimagegl.cpp96
-rw-r--r--indra/llrender/llimagegl.h17
-rw-r--r--indra/llrender/llrender.cpp28
-rw-r--r--indra/llrender/llshadermgr.cpp19
7 files changed, 290 insertions, 36 deletions
diff --git a/indra/llrender/llfontfreetype.cpp b/indra/llrender/llfontfreetype.cpp
index 91898e6de1..357dfc8ea8 100644
--- a/indra/llrender/llfontfreetype.cpp
+++ b/indra/llrender/llfontfreetype.cpp
@@ -990,30 +990,39 @@ namespace ll
U8 const* LLFontManager::loadFont( std::string const &aFilename, long &a_Size)
{
- a_Size = 0;
- std::map< std::string, std::shared_ptr<ll::fonts::LoadedFont> >::iterator itr = m_LoadedFonts.find( aFilename );
- if( itr != m_LoadedFonts.end() )
+ try
{
- ++itr->second->mRefs;
- // A possible overflow cannot happen here, as it is asserted that the size is less than std::numeric_limits<long>::max() a few lines below.
- a_Size = static_cast<long>(itr->second->mSize);
- return reinterpret_cast<U8 const*>(itr->second->mAddress.c_str());
- }
+ a_Size = 0;
+ std::map< std::string, std::shared_ptr<ll::fonts::LoadedFont> >::iterator itr = m_LoadedFonts.find(aFilename);
+ if (itr != m_LoadedFonts.end())
+ {
+ ++itr->second->mRefs;
+ // A possible overflow cannot happen here, as it is asserted that the size is less than std::numeric_limits<long>::max() a few lines below.
+ a_Size = static_cast<long>(itr->second->mSize);
+ return reinterpret_cast<U8 const*>(itr->second->mAddress.c_str());
+ }
- auto strContent = LLFile::getContents(aFilename);
+ auto strContent = LLFile::getContents(aFilename);
- if( strContent.empty() )
- return nullptr;
+ if (strContent.empty())
+ return nullptr;
- // For fontconfig a type of long is required, std::string::size() returns size_t. I think it is safe to limit this to 2GiB and not support fonts that huge (can that even be a thing?)
- llassert_always( strContent.size() < std::numeric_limits<long>::max() );
+ // For fontconfig a type of long is required, std::string::size() returns size_t. I think it is safe to limit this to 2GiB and not support fonts that huge (can that even be a thing?)
+ llassert_always(strContent.size() < std::numeric_limits<long>::max());
- a_Size = static_cast<long>(strContent.size());
+ a_Size = static_cast<long>(strContent.size());
- auto pCache = std::make_shared<ll::fonts::LoadedFont>( aFilename, strContent, a_Size );
- itr = m_LoadedFonts.insert( std::make_pair( aFilename, pCache ) ).first;
+ auto pCache = std::make_shared<ll::fonts::LoadedFont>(aFilename, strContent, a_Size);
+ itr = m_LoadedFonts.insert(std::make_pair(aFilename, pCache)).first;
- return reinterpret_cast<U8 const*>(itr->second->mAddress.c_str());
+ return reinterpret_cast<U8 const*>(itr->second->mAddress.c_str());
+ }
+ catch (const std::bad_alloc&)
+ {
+ LLError::LLUserWarningMsg::showOutOfMemory();
+ LL_ERRS() << "Failed to load font. Out of memory." << LL_ENDL;
+ }
+ return nullptr;
}
void LLFontManager::unloadAllFonts()
diff --git a/indra/llrender/llfontvertexbuffer.cpp b/indra/llrender/llfontvertexbuffer.cpp
index a223509d30..2a0115265f 100644
--- a/indra/llrender/llfontvertexbuffer.cpp
+++ b/indra/llrender/llfontvertexbuffer.cpp
@@ -237,3 +237,80 @@ void LLFontVertexBuffer::renderBuffers()
gGL.popUIMatrix();
}
+// LLFontWidthBuffer
+bool LLFontWidthBuffer::sEnableBufferCollection = true;
+
+LLFontWidthBuffer::LLFontWidthBuffer()
+{
+}
+
+LLFontWidthBuffer::~LLFontWidthBuffer()
+{
+}
+
+void LLFontWidthBuffer::reset()
+{
+ mLastFont = nullptr;
+ mLastOffset = 0;
+ mLastMaxChars = 0;
+ mLastNoPadding = false;
+ mWidth = -1.f;
+ mLastScaleX = 1.f;
+ mLastScaleY = 1.f;
+ mLastVertDPI = 0.f;
+ mLastHorizDPI = 0.f;
+ mLastResGeneration = 0;
+ mLastFontCacheGen = 0;
+}
+
+F32 LLFontWidthBuffer::getWidth(
+ const LLFontGL* fontp,
+ const llwchar* wchars,
+ S32 begin_offset,
+ S32 max_chars,
+ bool no_padding)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_UI;
+ if (!fontp || !wchars)
+ {
+ return 0.f;
+ }
+
+ if (!sEnableBufferCollection)
+ {
+ return fontp->getWidthF32(wchars, begin_offset, max_chars, no_padding);
+ }
+
+ // Check if we can use cached width
+ bool needs_recalc = (mWidth < 0.f)
+ || (mLastFont != fontp)
+ || (mLastOffset != begin_offset)
+ || (mLastMaxChars != max_chars)
+ || (mLastNoPadding != no_padding)
+ || (mLastScaleX != LLFontGL::sScaleX)
+ || (mLastScaleY != LLFontGL::sScaleY)
+ || (mLastVertDPI != LLFontGL::sVertDPI)
+ || (mLastHorizDPI != LLFontGL::sHorizDPI)
+ || (mLastResGeneration != LLFontGL::sResolutionGeneration)
+ || (mLastFontCacheGen != fontp->getCacheGeneration());
+
+ if (needs_recalc)
+ {
+ // Calculate width using the font
+ mWidth = fontp->getWidthF32(wchars, begin_offset, max_chars, no_padding);
+
+ // Cache the parameters
+ mLastFont = fontp;
+ mLastOffset = begin_offset;
+ mLastMaxChars = max_chars;
+ mLastNoPadding = no_padding;
+ mLastScaleX = LLFontGL::sScaleX;
+ mLastScaleY = LLFontGL::sScaleY;
+ mLastVertDPI = LLFontGL::sVertDPI;
+ mLastHorizDPI = LLFontGL::sHorizDPI;
+ mLastResGeneration = LLFontGL::sResolutionGeneration;
+ mLastFontCacheGen = fontp->getCacheGeneration();
+ }
+
+ return mWidth;
+}
diff --git a/indra/llrender/llfontvertexbuffer.h b/indra/llrender/llfontvertexbuffer.h
index a9e1e2337c..94b833d227 100644
--- a/indra/llrender/llfontvertexbuffer.h
+++ b/indra/llrender/llfontvertexbuffer.h
@@ -32,6 +32,11 @@
class LLVertexBufferData;
+// Rendering fonts is expensive, this class is intended to store
+// vertex buffers for rendered text, so that they can be reused.
+// LLFontVertexBuffer tracks font and rendering parameters, but
+// expects caller to track text changes and call reset() when
+// text changes.
class LLFontVertexBuffer
{
public:
@@ -127,4 +132,45 @@ private:
static bool sEnableBufferCollection;
};
+// Extracting width from a font is expensive, and due to
+// mechanics of font rendering, we need width separately
+// and usually before rendering.
+// LLFontWidthBuffer tracks font and rendering parameters,
+// but expects caller to track text changes and call reset()
+// when text changes.
+class LLFontWidthBuffer
+{
+public:
+ LLFontWidthBuffer();
+ ~LLFontWidthBuffer();
+
+ void reset();
+
+ F32 getWidth(const LLFontGL* fontp,
+ const llwchar* wchars,
+ S32 begin_offset,
+ S32 max_chars,
+ bool no_padding);
+
+ static void enableBufferCollection(bool enable) { sEnableBufferCollection = enable; }
+private:
+ const LLFontGL* mLastFont = nullptr;
+ S32 mLastOffset = 0;
+ S32 mLastMaxChars = 0;
+ bool mLastNoPadding = false;
+ F32 mWidth = -1.f;
+
+ // LLFontGL's values that affect width calculation
+ F32 mLastScaleX = 1.f;
+ F32 mLastScaleY = 1.f;
+ F32 mLastVertDPI = 0.f;
+ F32 mLastHorizDPI = 0.f;
+ S32 mLastResGeneration = 0;
+
+ // Cache generation tracking
+ S32 mLastFontCacheGen = 0;
+
+ static bool sEnableBufferCollection;
+};
+
#endif
diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp
index 4901cb423b..a40cb14f17 100644
--- a/indra/llrender/llimagegl.cpp
+++ b/indra/llrender/llimagegl.cpp
@@ -66,8 +66,8 @@ static LLMutex sTexMemMutex;
static std::unordered_map<U32, U64> sTextureAllocs;
static U64 sTextureBytes = 0;
-// track a texture alloc on the currently bound texture.
-// asserts that no currently tracked alloc exists
+// Per-mip upload paths call this once per level; only free_tex_image
+// removes a texture's accounting entirely.
void LLImageGLMemory::alloc_tex_image(U32 width, U32 height, U32 intformat, U32 count)
{
U32 texUnit = gGL.getCurrentTexUnitIndex();
@@ -80,15 +80,46 @@ void LLImageGLMemory::alloc_tex_image(U32 width, U32 height, U32 intformat, U32
sTexMemMutex.lock();
- // it is a precondition that no existing allocation exists for this texture
- llassert(sTextureAllocs.find(texName) == sTextureAllocs.end());
-
- sTextureAllocs[texName] = size;
+ auto iter = sTextureAllocs.find(texName);
+ if (iter != sTextureAllocs.end())
+ {
+ iter->second += size;
+ }
+ else
+ {
+ sTextureAllocs[texName] = size;
+ }
sTextureBytes += size;
sTexMemMutex.unlock();
}
+// Add mip 1..N bytes to existing accounting. Use after glGenerateMipmap.
+void LLImageGLMemory::account_extra_mip_bytes(U32 base_width, U32 base_height, U32 intformat)
+{
+ U64 extra = 0;
+ U32 w = base_width;
+ U32 h = base_height;
+ while (w > 1 || h > 1)
+ {
+ w = w > 1 ? w >> 1 : 1;
+ h = h > 1 ? h >> 1 : 1;
+ extra += LLImageGL::dataFormatBytes(intformat, w, h);
+ }
+
+ U32 texUnit = gGL.getCurrentTexUnitIndex();
+ U32 texName = gGL.getTexUnit(texUnit)->getCurrTexture();
+
+ sTexMemMutex.lock();
+ auto iter = sTextureAllocs.find(texName);
+ if (iter != sTextureAllocs.end())
+ {
+ iter->second += extra;
+ sTextureBytes += extra;
+ }
+ sTexMemMutex.unlock();
+}
+
// track texture free on given texName
void LLImageGLMemory::free_tex_image(U32 texName)
{
@@ -719,7 +750,10 @@ void LLImageGL::dump()
//----------------------------------------------------------------------------
void LLImageGL::forceUpdateBindStats(void) const
{
- mLastBindTime = sLastFrameTime;
+ // Intentionally a no-op: mLastBindTime is written only by real bind
+ // paths so the staleness signal reflects actual GPU use. Callers that
+ // still invoke this (avatar "keep alive" sites, deleted-texture
+ // fallback) no longer falsely refresh staleness.
}
bool LLImageGL::updateBindStats() const
@@ -879,7 +913,7 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32
mMipLevels = wpo2(llmax(w, h));
//use legacy mipmap generation mode (note: making this condional can cause rendering issues)
- // -- but making it not conditional triggers deprecation warnings when core profile is enabled
+ // - but making it not conditional triggers deprecation warnings when core profile is enabled
// (some rendering issues while core profile is enabled are acceptable at this point in time)
#if GL_VERSION_1_4
if (!LLRender::sGLCoreProfile)
@@ -909,6 +943,7 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32
{
LL_PROFILE_GPU_ZONE("generate mip map");
glGenerateMipmap(mTarget);
+ account_extra_mip_bytes(w, h, mFormatInternal);
}
stop_glerror();
}
@@ -1550,7 +1585,12 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt
LL_PROFILE_ZONE_NUM(width);
LL_PROFILE_ZONE_NUM(height);
- free_cur_tex_image();
+ // Release prior accounting only on the base mip; per-mip iteration
+ // accumulates the rest via the additive alloc_tex_image.
+ if (miplevel == 0)
+ {
+ free_cur_tex_image();
+ }
const bool use_sub_image = should_stagger_image_set(compress);
if (!use_sub_image)
{
@@ -1710,7 +1750,6 @@ bool LLImageGL::createGLTexture(S32 discard_level, const LLImageRaw* imageraw, S
{
destroyGLTexture();
mCurrentDiscardLevel = discard_level;
- mLastBindTime = sLastFrameTime;
mGLTextureCreated = false;
return true ;
}
@@ -1826,9 +1865,7 @@ bool LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, bool data_
mTextureMemory = (S64Bytes)getMipBytes(mCurrentDiscardLevel);
-
- // mark this as bound at this point, so we don't throw it out immediately
- mLastBindTime = sLastFrameTime;
+ mGLCreateTime = sLastFrameTime;
checkActiveThread();
return true;
@@ -1955,7 +1992,7 @@ bool LLImageGL::readBackRaw(S32 discard_level, LLImageRaw* imageraw, bool compre
LLGLint is_compressed = 0;
if (compressed_ok)
{
- glGetTexLevelParameteriv(mTarget, is_compressed, GL_TEXTURE_COMPRESSED, (GLint*)&is_compressed);
+ glGetTexLevelParameteriv(mTarget, gl_discard, GL_TEXTURE_COMPRESSED, (GLint*)&is_compressed);
}
//-----------------------------------------------------------------------------------------------
@@ -2140,6 +2177,28 @@ S32 LLImageGL::getWidth(S32 discard_level) const
return width;
}
+// static
+S32 LLImageGL::dimDerivedMaxDiscard(S32 width, S32 height)
+{
+ if (width <= 0 || height <= 0)
+ {
+ return 0;
+ }
+ // max(w,h) - min() caps short on rectangular textures
+ // (1024x512 reaches 1x1 at discard 10, not 9).
+ return (S32)floorf(log2f((F32)llmax(width, height)));
+}
+
+void LLImageGL::stampBound() const
+{
+ // Skip the store on same-frame re-binds - bindFast is per-draw and
+ // would dirty this cache line per bind per texture otherwise.
+ if (mLastBindTime != sLastFrameTime)
+ {
+ mLastBindTime = sLastFrameTime;
+ }
+}
+
S64 LLImageGL::getBytes(S32 discard_level) const
{
if (discard_level < 0)
@@ -2585,7 +2644,12 @@ bool LLImageGL::scaleDown(S32 desired_discard)
return false;
}
- desired_discard = llmin(desired_discard, mMaxDiscardLevel);
+ // GL pyramid reaches 1x1 regardless of codec levels;
+ // mMaxDiscardLevel is hardcapped at MAX_DISCARD_LEVEL.
+ S32 dim_max_discard = (mWidth > 0 && mHeight > 0)
+ ? dimDerivedMaxDiscard(mWidth, mHeight)
+ : (S32)mMaxDiscardLevel;
+ desired_discard = llmin(desired_discard, dim_max_discard);
if (desired_discard <= mCurrentDiscardLevel)
{
@@ -2622,6 +2686,7 @@ bool LLImageGL::scaleDown(S32 desired_discard)
gGL.getTexUnit(0)->bind(this);
glGenerateMipmap(mTarget);
LOG_GLERROR("LLImageGL::scaleDown() - glGenerateMipmap");
+ account_extra_mip_bytes(desired_width, desired_height, mFormatInternal);
gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);
}
}
@@ -2669,6 +2734,7 @@ bool LLImageGL::scaleDown(S32 desired_discard)
{
LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("scaleDown - glGenerateMipmap");
glGenerateMipmap(mTarget);
+ account_extra_mip_bytes(desired_width, desired_height, mFormatInternal);
}
gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);
diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h
index 6b4492c09e..0c85446b84 100644
--- a/indra/llrender/llimagegl.h
+++ b/indra/llrender/llimagegl.h
@@ -51,6 +51,11 @@ class LLWindow;
namespace LLImageGLMemory
{
void alloc_tex_image(U32 width, U32 height, U32 intformat, U32 count);
+
+ // Add mip 1..N bytes to existing accounting. Call after glGenerateMipmap
+ // when only the base mip was accounted; without this the bytes counter
+ // undercounts mipmap-generated textures by ~25%.
+ void account_extra_mip_bytes(U32 base_width, U32 base_height, U32 intformat);
void free_tex_image(U32 texName);
void free_tex_images(U32 count, const U32* texNames);
void free_cur_tex_image();
@@ -151,6 +156,15 @@ public:
S32 getDiscardLevel() const { return mCurrentDiscardLevel; }
S32 getMaxDiscardLevel() const { return mMaxDiscardLevel; }
+ // floor(log2(max(w, h))) - deepest GL pyramid level (down to 1x1).
+ // Returns 0 for non-positive inputs.
+ static S32 dimDerivedMaxDiscard(S32 width, S32 height);
+
+ // Record the wall-clock bind time - every bind path that touches a
+ // streaming-managed texture must call this, or the staleness signal
+ // sees the texture as never-bound and ramps it toward eviction.
+ void stampBound() const;
+
// override the current discard level
// should only be used for local textures where you know exactly what you're doing
void setDiscardLevel(S32 level) { mCurrentDiscardLevel = level; }
@@ -224,7 +238,8 @@ public:
public:
// Various GL/Rendering options
S64Bytes mTextureMemory;
- mutable F32 mLastBindTime; // last time this was bound, by discard level
+ mutable F32 mLastBindTime = 0.f; // wall-clock time at last stampBound; drives streaming staleness
+ F32 mGLCreateTime = 0.f; // wall-clock time the GL texture was created; staleness fallback for never-bound textures
private:
U32 createPickMask(S32 pWidth, S32 pHeight);
diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp
index 658947d531..6df5a80748 100644
--- a/indra/llrender/llrender.cpp
+++ b/indra/llrender/llrender.cpp
@@ -204,6 +204,9 @@ void LLTexUnit::bindFast(LLTexture* texture)
gGL.mCurrTextureUnitIndex = mIndex;
mCurrTexture = gl_tex->getTexName();
mCurrTexType = gl_tex->getTarget();
+ // bindFast bypasses updateBindStats(); stamp directly so the staleness
+ // signal sees per-frame use of batched textures.
+ gl_tex->stampBound();
if (!mCurrTexture)
{
LL_PROFILE_ZONE_NAMED("MISSING TEXTURE");
@@ -258,11 +261,17 @@ bool LLTexUnit::bind(LLTexture* texture, bool for_rendering, bool forceBind)
setTextureFilteringOption(gl_tex->mFilterOption);
}
}
+ else
+ {
+ // Already current - still being used, keep it fresh.
+ gl_tex->stampBound();
+ }
}
else
{
//if deleted, will re-generate it immediately
texture->forceImmediateUpdate() ;
+ gl_tex->stampBound();
gl_tex->forceUpdateBindStats() ;
return texture->bindDefaultImage(mIndex);
@@ -334,6 +343,11 @@ bool LLTexUnit::bind(LLImageGL* texture, bool for_rendering, bool forceBind, S32
stop_glerror();
}
}
+ else
+ {
+ // Already current - still being used, keep it fresh.
+ texture->stampBound();
+ }
stop_glerror();
@@ -1788,7 +1802,16 @@ LLVertexBuffer* LLRender::genBuffer(U32 attribute_mask, S32 count)
LLVertexBuffer * vb = new LLVertexBuffer(attribute_mask);
vb->allocateBuffer(count, 0);
- vb->setBuffer();
+ // Non-Apple path uses glBufferSubData inside setXxxData, so the VBO
+ // must already be bound. On Apple, the VBO is lazily created in
+ // _unmapBuffer (LLAppleVBOPool); calling setBuffer() here would bind
+ // mGLBuffer == 0 and then setupVertexBuffer would issue
+ // glVertexAttribIPointer with a non-null offset against no bound
+ // GL_ARRAY_BUFFER -> GL_INVALID_OPERATION in core profile.
+ if (!gGLManager.mIsApple)
+ {
+ vb->setBuffer();
+ }
vb->setPositionData(mVerticesp.get());
@@ -1804,6 +1827,9 @@ LLVertexBuffer* LLRender::genBuffer(U32 attribute_mask, S32 count)
if(gGLManager.mIsApple && LLVertexBuffer::getVertexBufferMode() == 0)
{
+ // unmapBuffer creates the GL buffer, uploads, and leaves it bound,
+ // drawBuffer's later setBuffer() then runs setupVertexBuffer against
+ // a valid VBO.
vb->unmapBuffer();
}
diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp
index f50ce26793..fae2bd5034 100644
--- a/indra/llrender/llshadermgr.cpp
+++ b/indra/llrender/llshadermgr.cpp
@@ -1026,8 +1026,23 @@ void LLShaderMgr::initShaderCache(bool enabled, const LLUUID& old_cache_version,
llifstream instream(meta_out_path, std::ifstream::in | std::ifstream::binary);
LLSD in_data;
- // todo: this is likely very expensive to parse, should use binary
- LLSDSerialize::fromBinary(in_data, instream, LLSDSerialize::SIZE_UNLIMITED);
+ try
+ {
+ LLSDSerialize::fromBinary(in_data, instream, LLSDSerialize::SIZE_UNLIMITED);
+ }
+ catch( std::bad_alloc& )
+ {
+ // Try to get a bit more memory back before we try to clear the cache.
+ in_data.clear();
+ // Just in case it was somehow the cause, clear cache.
+ clearShaderCache();
+ // If user run out of memory this early in init,
+ // we don't want to keep going just to crash again.
+ // Notify user and close.
+ LLError::LLUserWarningMsg::showOutOfMemory();
+ LL_ERRS("ShaderMgr") << "Failed to parse shader cache metadata, potentially due to size. Purged cache." << LL_ENDL;
+ return;
+ }
instream.close();
if (old_cache_version == current_cache_version