summaryrefslogtreecommitdiff
path: root/indra/llrender
diff options
context:
space:
mode:
authorJonathan "Geenz" Goodman <geenz@geenzo.com>2026-05-12 20:23:44 -0400
committerJonathan "Geenz" Goodman <geenz@geenzo.com>2026-05-12 20:23:44 -0400
commit82db4943e011fc65e9a0b87990abb49b898a7782 (patch)
treec7a16b81c6d681db2277cc95b875968206cb0509 /indra/llrender
parent5d5c74c27f7354dba499acb3534c64a9a7fa3dc7 (diff)
Rework texture streaming and tracking.
This is a big one: - Reworks the discard signal almost entirely. Now has a normalized 0..1 discard signal: distance x size x channel exponent, floored by staleness and background app state. Shaped by VRAM pressure. - Textures can now scale down to the smallest GPU mip (1×1), independent of the codec's encoded mip count. - Terrain texture LOD now works. Useful for 2K textures and PBR on terrain. Based upon camera distance to nearest terrain patch. - New texture quality setting. Low/Medium/High/Ultra - Caps texture resolution on Low to 1024, and otherwise shifts the discard signal around. Makes distance based texture LOD work a lot more predictably. - We now track last bind state for textures, and discard accordingly. We progressively discard based upon last bind time. - Avatar textures get a residency boost to stay loaded in VRAM longer under pressure.
Diffstat (limited to 'indra/llrender')
-rw-r--r--indra/llrender/llimagegl.cpp84
-rw-r--r--indra/llrender/llimagegl.h16
-rw-r--r--indra/llrender/llrender.cpp14
3 files changed, 104 insertions, 10 deletions
diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp
index 4a3d32c7ff..d79d13dc8b 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)
{
@@ -838,7 +869,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 (!LLRender::sGLCoreProfile)
{
@@ -864,6 +895,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();
}
@@ -1461,7 +1493,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)
{
@@ -2039,6 +2076,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)
@@ -2468,7 +2527,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)
{
@@ -2501,6 +2565,7 @@ bool LLImageGL::scaleDown(S32 desired_discard)
LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("scaleDown - glGenerateMipmap");
gGL.getTexUnit(0)->bind(this);
glGenerateMipmap(mTarget);
+ account_extra_mip_bytes(desired_width, desired_height, mFormatInternal);
gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);
}
}
@@ -2546,6 +2611,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..a02c320738 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,7 @@ 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
private:
U32 createPickMask(S32 pWidth, S32 pHeight);
diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp
index 1a3a499b20..696a0d145f 100644
--- a/indra/llrender/llrender.cpp
+++ b/indra/llrender/llrender.cpp
@@ -197,6 +197,9 @@ void LLTexUnit::bindFast(LLTexture* texture)
glActiveTexture(GL_TEXTURE0 + mIndex);
gGL.mCurrTextureUnitIndex = mIndex;
mCurrTexture = gl_tex->getTexName();
+ // 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");
@@ -249,11 +252,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);
@@ -325,6 +334,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();