summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan "Geenz" Goodman <geenz@lindenlab.com>2026-05-22 13:13:30 -0400
committerGitHub <noreply@github.com>2026-05-22 13:13:30 -0400
commit6b4e3f3288ec1e9917cecd862a7a52945e5b4db2 (patch)
treeeb7f6952eaa5e78326308882db39500a4f149271
parent99ab6316b4ae9058f22d9f57d21e795ca45797fd (diff)
parentdad44ba5d67d04a73708a9a25bbe1ddec29a6a9a (diff)
Merge pull request #5829 from secondlife/geenz/texture-quality
Texture streaming rework
-rw-r--r--indra/llimage/llimagej2c.cpp34
-rw-r--r--indra/llimage/llimagej2c.h5
-rw-r--r--indra/llimagej2coj/llimagej2coj.cpp29
-rw-r--r--indra/llimagej2coj/llimagej2coj.h19
-rw-r--r--indra/llkdu/llimagej2ckdu.cpp30
-rw-r--r--indra/llkdu/llimagej2ckdu.h2
-rw-r--r--indra/llkdu/tests/llimagej2ckdu_test.cpp2
-rw-r--r--indra/llrender/llimagegl.cpp96
-rw-r--r--indra/llrender/llimagegl.h17
-rw-r--r--indra/llrender/llrender.cpp29
-rw-r--r--indra/newview/app_settings/settings.xml326
-rw-r--r--indra/newview/featuretable.txt31
-rw-r--r--indra/newview/featuretable_mac.txt31
-rw-r--r--indra/newview/lldrawpoolwater.cpp8
-rw-r--r--indra/newview/llface.cpp30
-rw-r--r--indra/newview/llface.h7
-rw-r--r--indra/newview/llfeaturemanager.cpp24
-rw-r--r--indra/newview/llsurface.cpp14
-rw-r--r--indra/newview/llsurface.h6
-rw-r--r--indra/newview/lltexturefetch.cpp12
-rw-r--r--indra/newview/lltexturefetch.h3
-rw-r--r--indra/newview/lltextureview.cpp8
-rw-r--r--indra/newview/llviewercontrol.cpp43
-rw-r--r--indra/newview/llviewertexture.cpp437
-rw-r--r--indra/newview/llviewertexture.h59
-rw-r--r--indra/newview/llviewertexturelist.cpp236
-rw-r--r--indra/newview/llviewertexturelist.h9
-rw-r--r--indra/newview/llvlcomposition.cpp18
-rw-r--r--indra/newview/skins/default/xui/en/floater_preferences_graphics_advanced.xml32
29 files changed, 1333 insertions, 264 deletions
diff --git a/indra/llimage/llimagej2c.cpp b/indra/llimage/llimagej2c.cpp
index 5a941dc958..8099a18045 100644
--- a/indra/llimage/llimagej2c.cpp
+++ b/indra/llimage/llimagej2c.cpp
@@ -268,35 +268,11 @@ S32 LLImageJ2C::calcHeaderSizeJ2C()
//static
S32 LLImageJ2C::calcDataSizeJ2C(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate)
{
- // Note: This provides an estimation for the first to last quality layer of a given discard level
- // This is however an efficient approximation, as the true discard level boundary would be
- // in general too big for fast fetching.
- // For details about the equation used here, see https://wiki.lindenlab.com/wiki/THX1138_KDU_Improvements#Byte_Range_Study
-
- // Estimate the number of layers. This is consistent with what's done for j2c encoding in LLImageJ2CKDU::encodeImpl().
- constexpr S32 precision = 8; // assumed bitrate per component channel, might change in future for HDR support
- constexpr S32 max_components = 4; // assumed the file has four components; three color and alpha
- // Use MAX_IMAGE_SIZE_DEFAULT (currently 2048) if either dimension is unknown (zero)
- S32 width = (w > 0) ? w : 2048;
- S32 height = (h > 0) ? h : 2048;
- S32 max_dimension = llmax(width, height); // Find largest dimension
- S32 block_area = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE; // Calculated initial block area from established max block size (currently 64)
- S32 max_layers = (S32)llmax(llround(log2f((float)max_dimension) - log2f((float)MAX_BLOCK_SIZE)), 4); // Find number of powers of two between extents and block size to a minimum of 4
- block_area *= llmax(max_layers, 1); // Adjust initial block area by max number of layers
- S32 totalbytes = (S32) (MIN_LAYER_SIZE * max_components * precision); // Start estimation with a minimum reasonable size
- S32 block_layers = 0;
- while (block_layers <= max_layers) // Walk the layers
- {
- if (block_layers <= (5 - discard_level)) // Walk backwards from discard 5 to required discard layer.
- totalbytes += (S32) (block_area * max_components * precision * rate); // Add each block layer reduced by assumed compression rate
- block_layers++; // Move to next layer
- block_area *= 4; // Increase block area by power of four
- }
-
- totalbytes /= 8; // to bytes
- totalbytes += calcHeaderSizeJ2C(); // header
-
- return totalbytes;
+ // Dispatch to the linked impl so OpenJPEG (block-aligned, needs
+ // over-allocation) and KDU (packet-aligned, lean) each return what
+ // their decoder actually needs.
+ static std::unique_ptr<LLImageJ2CImpl> s_estimator(fallbackCreateLLImageJ2CImpl());
+ return s_estimator->estimateDataSize(w, h, comp, discard_level, rate);
}
S32 LLImageJ2C::calcHeaderSize()
diff --git a/indra/llimage/llimagej2c.h b/indra/llimage/llimagej2c.h
index 19744a7f87..81b24cc0b3 100644
--- a/indra/llimage/llimagej2c.h
+++ b/indra/llimage/llimagej2c.h
@@ -106,6 +106,11 @@ class LLImageJ2CImpl
{
public:
virtual ~LLImageJ2CImpl();
+
+ // Estimate the byte size of a J2C codestream sufficient to decode the
+ // given discard level. KDU uses a packet-by-packet impl; OpenJPEG
+ // overrides with a more conservative block-aligned estimate.
+ virtual S32 estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const = 0;
protected:
// Find out the image size and number of channels.
// Return value:
diff --git a/indra/llimagej2coj/llimagej2coj.cpp b/indra/llimagej2coj/llimagej2coj.cpp
index 7cfadb889d..488c07e1c9 100644
--- a/indra/llimagej2coj/llimagej2coj.cpp
+++ b/indra/llimagej2coj/llimagej2coj.cpp
@@ -919,3 +919,32 @@ bool LLImageJ2COJ::getMetadata(LLImageJ2C &base)
base.setSize(width, height, components);
return true;
}
+
+
+// OpenJPEG-tuned byte estimator. Conservative pyramid walk that accounts for
+// OJ's whole-code-block decode behavior (even with strict mode off). Larger
+// images get a per-resolution multiplier so the byte range lands inside the
+// last needed code-block boundary.
+S32 LLImageJ2COJ::estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const
+{
+ constexpr S32 precision = 8;
+ constexpr S32 max_components = 4;
+ S32 width = (w > 0) ? w : 2048;
+ S32 height = (h > 0) ? h : 2048;
+ S32 max_dimension = llmax(width, height);
+ S32 block_area = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE;
+ S32 max_layers = (S32)llmax(llround(log2f((float)max_dimension) - log2f((float)MAX_BLOCK_SIZE)), 4);
+ block_area *= llmax(max_layers, 1);
+ S32 totalbytes = (S32)(MIN_LAYER_SIZE * max_components * precision);
+ S32 block_layers = 0;
+ while (block_layers <= max_layers)
+ {
+ if (block_layers <= (5 - discard_level))
+ totalbytes += (S32)(block_area * max_components * precision * rate);
+ block_layers++;
+ block_area *= 4;
+ }
+ totalbytes /= 8;
+ totalbytes += LLImageJ2C::calcHeaderSizeJ2C();
+ return totalbytes;
+}
diff --git a/indra/llimagej2coj/llimagej2coj.h b/indra/llimagej2coj/llimagej2coj.h
index da49597302..0cc8d5c34c 100644
--- a/indra/llimagej2coj/llimagej2coj.h
+++ b/indra/llimagej2coj/llimagej2coj.h
@@ -35,15 +35,20 @@ class LLImageJ2COJ : public LLImageJ2CImpl
{
public:
LLImageJ2COJ();
- virtual ~LLImageJ2COJ();
+ virtual ~LLImageJ2COJ() override;
protected:
- virtual bool getMetadata(LLImageJ2C &base);
- virtual bool decodeImpl(LLImageJ2C &base, LLImageRaw &raw_image, F32 decode_time, S32 first_channel, S32 max_channel_count);
+ virtual bool getMetadata(LLImageJ2C &base) override;
+ virtual bool decodeImpl(LLImageJ2C &base, LLImageRaw &raw_image, F32 decode_time, S32 first_channel, S32 max_channel_count) override;
virtual bool encodeImpl(LLImageJ2C &base, const LLImageRaw &raw_image, const char* comment_text, F32 encode_time=0.0,
- bool reversible = false);
- virtual bool initDecode(LLImageJ2C &base, LLImageRaw &raw_image, int discard_level = -1, int* region = NULL);
- virtual bool initEncode(LLImageJ2C &base, LLImageRaw &raw_image, int blocks_size = -1, int precincts_size = -1, int levels = 0);
- virtual std::string getEngineInfo() const;
+ bool reversible = false) override;
+ virtual bool initDecode(LLImageJ2C &base, LLImageRaw &raw_image, int discard_level = -1, int* region = NULL) override;
+ virtual bool initEncode(LLImageJ2C &base, LLImageRaw &raw_image, int blocks_size = -1, int precincts_size = -1, int levels = 0) override;
+ virtual std::string getEngineInfo() const override;
+public:
+ // OpenJPEG decodes whole code-blocks even with strict mode off, so the
+ // lean packet-walk under-allocates and clips quality. Keep the older
+ // conservative pyramid-with-multiplier estimate here.
+ virtual S32 estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const override;
};
#endif
diff --git a/indra/llkdu/llimagej2ckdu.cpp b/indra/llkdu/llimagej2ckdu.cpp
index e7ac6bdb31..4330b1e5b1 100644
--- a/indra/llkdu/llimagej2ckdu.cpp
+++ b/indra/llkdu/llimagej2ckdu.cpp
@@ -1513,3 +1513,33 @@ void kdc_flow_control::process_components()
}
}
}
+
+// Layer-factored byte estimator. Walks the resolution pyramid to count
+// layers, weights by layer_factor, then picks between a sqrt-based "new"
+// estimate and a raw-dimensions "old" estimate per TextureNewByteRange.
+// Reference: https://wiki.lindenlab.com/wiki/THX1138_KDU_Improvements#Byte_Range_Study
+S32 LLImageJ2CKDU::estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const
+{
+ S32 width = (w > 0) ? w : 2048;
+ S32 height = (h > 0) ? h : 2048;
+ S32 nb_layers = 1;
+ S32 surface = width * height;
+ S32 s = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE;
+ while (surface > s)
+ {
+ nb_layers++;
+ s *= 4;
+ }
+ F32 layer_factor = 3.0f * (7 - llclamp(nb_layers, 1, 6));
+
+ width >>= discard_level;
+ height >>= discard_level;
+ width = llmax(width, 1);
+ height = llmax(height, 1);
+
+ S32 new_bytes = (S32)(sqrtf((F32)(width * height)) * (F32)comp * rate * 1000.f / layer_factor);
+ S32 old_bytes = (S32)((F32)(width * height * comp) * rate);
+ S32 bytes = (LLImage::useNewByteRange() && (new_bytes < old_bytes)) ? new_bytes : old_bytes;
+ bytes = llmax(bytes, LLImageJ2C::calcHeaderSizeJ2C());
+ return bytes;
+}
diff --git a/indra/llkdu/llimagej2ckdu.h b/indra/llkdu/llimagej2ckdu.h
index c9aa0c5250..6079585948 100644
--- a/indra/llkdu/llimagej2ckdu.h
+++ b/indra/llkdu/llimagej2ckdu.h
@@ -67,6 +67,8 @@ protected:
virtual bool initDecode(LLImageJ2C &base, LLImageRaw &raw_image, int discard_level = -1, int* region = NULL);
virtual bool initEncode(LLImageJ2C &base, LLImageRaw &raw_image, int blocks_size = -1, int precincts_size = -1, int levels = 0);
virtual std::string getEngineInfo() const;
+public:
+ virtual S32 estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const;
private:
bool initDecode(LLImageJ2C &base, LLImageRaw &raw_image, F32 decode_time, ECodeStreamMode mode, S32 first_channel, S32 max_channel_count, int discard_level = -1, int* region = NULL);
diff --git a/indra/llkdu/tests/llimagej2ckdu_test.cpp b/indra/llkdu/tests/llimagej2ckdu_test.cpp
index bc52a15c4a..6be2d3a078 100644
--- a/indra/llkdu/tests/llimagej2ckdu_test.cpp
+++ b/indra/llkdu/tests/llimagej2ckdu_test.cpp
@@ -83,6 +83,8 @@ void LLImageBase::setSize(S32 , S32 , S32 ) { }
bool LLImageBase::isBufferInvalid() const { return false; }
LLImageJ2CImpl::~LLImageJ2CImpl() { }
+bool LLImage::sUseNewByteRange = false;
+S32 LLImageJ2C::calcHeaderSizeJ2C() { return 0; }
LLImageFormatted::LLImageFormatted(S8 ) { }
LLImageFormatted::~LLImageFormatted() { }
diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp
index 4a3d32c7ff..6bcc34938c 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)
{
@@ -684,7 +715,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
@@ -838,7 +872,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 +898,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 +1496,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)
{
@@ -1613,7 +1653,6 @@ bool LLImageGL::createGLTexture(S32 discard_level, const LLImageRaw* imageraw, S
{
destroyGLTexture();
mCurrentDiscardLevel = discard_level;
- mLastBindTime = sLastFrameTime;
mGLTextureCreated = false;
return true ;
}
@@ -1729,9 +1768,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;
@@ -1858,7 +1895,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);
}
//-----------------------------------------------------------------------------------------------
@@ -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..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 57be8570af..5e845fbcce 100644
--- a/indra/llrender/llrender.cpp
+++ b/indra/llrender/llrender.cpp
@@ -197,6 +197,10 @@ void LLTexUnit::bindFast(LLTexture* texture)
glActiveTexture(GL_TEXTURE0 + mIndex);
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");
@@ -249,11 +253,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 +335,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();
@@ -1718,7 +1733,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());
@@ -1733,6 +1757,9 @@ LLVertexBuffer* LLRender::genBuffer(U32 attribute_mask, S32 count)
}
#if LL_DARWIN
+ // unmapBuffer creates the GL buffer, uploads, and leaves it bound,
+ // drawBuffer's later setBuffer() then runs setupVertexBuffer against
+ // a valid VBO.
vb->unmapBuffer();
#endif
vb->unbind();
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index c69f4a0c21..38de81a51f 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -8049,7 +8049,7 @@
<key>RenderMaxTextureResolution</key>
<map>
<key>Comment</key>
- <string>Maximum texture resolution to download for non-boosted textures.</string>
+ <string>Maximum texture resolution to download for non-boosted textures. Driven by RenderTextureQuality.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
@@ -8057,6 +8057,17 @@
<key>Value</key>
<integer>2048</integer>
</map>
+ <key>RenderTextureQuality</key>
+ <map>
+ <key>Comment</key>
+ <string>Texture quality preset: 0=Low, 1=Medium, 2=High, 3=Ultra. Drives RenderMaxTextureResolution, the four TextureChannel* exponents, and TextureDistanceDiscardPower.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>U32</string>
+ <key>Value</key>
+ <integer>2</integer>
+ </map>
<key>RenderDownScaleMethod</key>
<map>
<key>Comment</key>
@@ -9254,7 +9265,7 @@
<key>RenderReflectionDetail</key>
<map>
<key>Comment</key>
- <string>DEPRECATED -- use RenderTransparentWater and RenderReflectionProbeDetail -- Detail of reflection render pass.</string>
+ <string>DEPRECATED - use RenderTransparentWater and RenderReflectionProbeDetail - Detail of reflection render pass.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
@@ -11860,33 +11871,316 @@
<key>Value</key>
<real>20.0</real>
</map>
- <key>TextureChannelPriority</key>
+ <key>TextureChannelNormal</key>
<map>
<key>Comment</key>
- <string>Per-channel texture streaming aggressiveness. X=normals, Y=diffuse, Z=specular/metallic, W=emissive. 1.0=baseline, higher=more aggressive downrez.</string>
+ <string>Per-channel discard exponent for normal maps. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
- <string>Vector4</string>
+ <string>F32</string>
<key>Value</key>
- <array>
- <real>5</real>
- <real>7.5</real>
- <real>20</real>
- <real>7.5</real>
- </array>
+ <real>1.0</real>
+ </map>
+ <key>TextureChannelBaseColor</key>
+ <map>
+ <key>Comment</key>
+ <string>Per-channel discard exponent for base color / diffuse. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.75</real>
+ </map>
+ <key>TextureChannelSpecular</key>
+ <map>
+ <key>Comment</key>
+ <string>Per-channel discard exponent for specular / metallic-roughness. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.5</real>
</map>
- <key>TextureCameraBoost</key>
+ <key>TextureChannelEmissive</key>
<map>
<key>Comment</key>
- <string>Amount to boost resolution of textures that are important to the camera.</string>
+ <string>Per-channel discard exponent for emissive. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset.</string>
<key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.75</real>
+ </map>
+ <key>TextureMaxDiscardOverride</key>
+ <map>
+ <key>Comment</key>
+ <string>When non-zero, overrides the per-texture codec-derived max discard cap. 0 = use codec-reported levels. Higher lets the streaming math push past the codec ceiling; scaleDown handles the GL side.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>S32</string>
+ <key>Value</key>
<integer>0</integer>
+ </map>
+ <key>TextureMemoryHighWaterMark</key>
+ <map>
+ <key>Comment</key>
+ <string>Fraction of budget (0..1) at which the pressure controller bypasses smoothing and slams to cap. Last-ditch min-discard also creeps without waiting for mult_progress.</string>
+ <key>Persist</key>
+ <integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>8.0</real>
+ <real>0.8</real>
+ </map>
+ <key>TextureMemoryPressureBackoffStart</key>
+ <map>
+ <key>Comment</key>
+ <string>Fraction of the VRAM target at which the pressure ramp starts (0..1). Lower = earlier headroom-building; 1.0 disables backoff (ramp only above target).</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.85</real>
+ </map>
+ <key>TextureMemoryPressureMaxMultiplier</key>
+ <map>
+ <key>Comment</key>
+ <string>Upper bound on the VRAM-pressure distance multiplier (&gt;= 1). Mostly defensive -- at mult=64 the streaming ramp collapses to ~ramp_range/64, already extreme. Higher allows even more aggressive compression in tight-budget scenes.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>64.0</real>
+ </map>
+ <key>TextureLastDitchEngageProgress</key>
+ <map>
+ <key>Comment</key>
+ <string>mult_progress (0..1) at which the last-ditch floor starts creeping up. The floor only advances when mult is at or above this fraction of its cap AND prediction is still over budget. Decays back toward 0 whenever prediction is under budget.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.95</real>
+ </map>
+ <key>TextureLastDitchRampRate</key>
+ <map>
+ <key>Comment</key>
+ <string>Rate (discard levels/sec) at which sLastDitchMinDiscard creeps up while engaged. 0.5 = takes ~2 sec to add one discard level. Mirrors sDesiredDiscardBias ramp shape.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.5</real>
+ </map>
+ <key>TextureLastDitchDecayRate</key>
+ <map>
+ <key>Comment</key>
+ <string>Rate (discard levels/sec) at which sLastDitchMinDiscard decays back to 0 when prediction is under budget.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.5</real>
+ </map>
+ <key>TextureLastDitchMinDiscardMax</key>
+ <map>
+ <key>Comment</key>
+ <string>Hard ceiling on sLastDitchMinDiscard. At 13 the floor can climb all the way to the deepest meaningful mip; lower values cap how aggressive the last-ditch escalation can get before we are simply out of discards.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>13.0</real>
+ </map>
+ <key>TextureMemoryPressurePredictionGain</key>
+ <map>
+ <key>Comment</key>
+ <string>Power exponent mapping predicted-over-budget ratio to target multiplier. target_mult = pred_over^gain. Higher gain saturates faster.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>10.0</real>
+ </map>
+ <key>TextureMemoryPressureSmoothingRate</key>
+ <map>
+ <key>Comment</key>
+ <string>Lerp rate (1/sec) at which the pressure multiplier converges to its prediction-driven target. Higher = faster response, lower = smoother. Default 4 reaches ~63% in 0.25s.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>4.0</real>
+ </map>
+ <key>TextureTerrainDistanceFloor</key>
+ <map>
+ <key>Comment</key>
+ <string>Minimum distance factor for BOOST_TERRAIN textures. Keeps combined &gt; 0 so VRAM pressure can evict terrain. Lower = higher idle quality, less pressure response.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.01</real>
+ </map>
+ <key>TextureTerrainCoverageFraction</key>
+ <map>
+ <key>Comment</key>
+ <string>Synthetic on-screen coverage fraction for BOOST_TERRAIN textures (no faces are registered). Higher = higher idle quality, less pressure response.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.99</real>
+ </map>
+ <key>TextureAgentAvatarBoost</key>
+ <map>
+ <key>Comment</key>
+ <string>Quality boost (0..1) for textures on the agent's avatar (rigged mesh / animated objects). Lower = higher quality. Preference, not exemption - pressure can still evict.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.5</real>
+ </map>
+ <key>TextureBackgroundFactorRatePerSec</key>
+ <map>
+ <key>Comment</key>
+ <string>Per-second ramp rate of the background-window discard floor (0..1). Snaps to 0 in foreground. Default 0.011 ~ 90s to saturate.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.011</real>
+ </map>
+ <key>TextureBackgroundDiscardOffset</key>
+ <map>
+ <key>Comment</key>
+ <string>Backgrounded textures will only discard up to (dim_max - offset). e.g. a 2048 texture (dim_max 11) with offset 2 caps the background floor at discard 9. 0 disables the cap (background can drive to max discard).</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>S32</string>
+ <key>Value</key>
+ <integer>2</integer>
+ </map>
+
+ <key>TextureCloseBubbleMeters</key>
+ <map>
+ <key>Comment</key>
+ <string>Close-camera bubble (meters). Faces inside this distance get dist_factor = 0 (no discard contribution); the ramp to 1 spans (bubble, draw_distance]. Shrinks toward TextureCloseBubbleMinMeters as VRAM pressure ramps the multiplier toward its cap.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>5.0</real>
+ </map>
+ <key>TextureCloseBubbleMinMeters</key>
+ <map>
+ <key>Comment</key>
+ <string>Floor (meters) for the close-camera bubble under maximum VRAM pressure. At sMemoryPressureMultiplier = TextureMemoryPressureMaxMultiplier the bubble collapses to this value, allowing eviction of even close textures when nothing else fits.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>3.0</real>
+ </map>
+ <key>TextureCloseBubbleShrinkThreshold</key>
+ <map>
+ <key>Comment</key>
+ <string>Bubble stays at full size until mult_progress exceeds this fraction (0..1) of its range to the cap. Above that, bubble lerps from full to TextureCloseBubbleMinMeters. Keeps the bubble out of the normal feedback loop.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.8</real>
+ </map>
+ <key>TextureCloseBubbleTrackRate</key>
+ <map>
+ <key>Comment</key>
+ <string>Rate (1/sec) at which the actual bubble tracks its target. Lower = smoother, slower to react. Damps short-term multiplier swings so close textures don't yo-yo.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.5</real>
</map>
+ <key>TextureDistanceDiscardPower</key>
+ <map>
+ <key>Comment</key>
+ <string>Exponent on the distance factor (face_distance / draw_distance). 1.0 = linear; lower = textures hit max discard sooner with distance. Default 0.5 = sqrt.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.5</real>
+ </map>
+ <key>TextureSizeDiscardPower</key>
+ <map>
+ <key>Comment</key>
+ <string>Exponent on the on-screen size factor (1 - coverage). 1.0 = linear; lower = small-on-screen textures attenuate sooner.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>1.0</real>
+ </map>
+ <key>TextureStalenessIntervalSeconds</key>
+ <map>
+ <key>Comment</key>
+ <string>Seconds per staleness step. Per-interval increment is 1/max_discard so any texture saturates after interval * max_discard seconds idle.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>5.0</real>
+ </map>
+ <key>TextureBindDecaySeconds</key>
+ <map>
+ <key>Comment</key>
+ <string>Grace seconds after a bind during which the staleness factor stays at 0. Prevents intermittently-bound textures from ramping. 0 disables the grace period.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>5.0</real>
+ </map>
+ <key>TextureFetchPressureScale</key>
+ <map>
+ <key>Comment</key>
+ <string>Pending-fetch divisor for the bias floor: bias floor = 1 + clamp(pending / scale, 0, 3). Lower = bias rises sooner under load floods.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>1000.0</real>
+ </map>
+
<key>TextureDecodeDisabled</key>
<map>
<key>Comment</key>
@@ -16250,7 +16544,7 @@
<key>EmulateCoreCount</key>
<map>
<key>Comment</key>
- <string>For debugging -- number of cores to restrict the main process to, or 0 for no limit. Requires restart.</string>
+ <string>For debugging - number of cores to restrict the main process to, or 0 for no limit. Requires restart.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
@@ -16404,7 +16698,7 @@
<key>MultiModeDoubleClickFolder</key>
<map>
<key>Comment</key>
- <string>Sets the action for Double-click on folder in multi-folder view (0 - expands and collapses folder, 1 - opens a new window, 2 – stays in current floater but switches to SFV)</string>
+ <string>Sets the action for Double-click on folder in multi-folder view (0 - expands and collapses folder, 1 - opens a new window, 2 - stays in current floater but switches to SFV)</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
diff --git a/indra/newview/featuretable.txt b/indra/newview/featuretable.txt
index 1090dd8ffb..f05f77c222 100644
--- a/indra/newview/featuretable.txt
+++ b/indra/newview/featuretable.txt
@@ -1,4 +1,4 @@
-version 74
+version 76
// The version number above should be incremented IF AND ONLY IF some
// change has been made that is sufficiently important to justify
// resetting the graphics preferences of all users to the recommended
@@ -85,7 +85,7 @@ RenderExposure 1 4
RenderTonemapType 1 1
RenderTonemapMix 1 1
RenderDisableVintageMode 1 1
-RenderMaxTextureResolution 1 2048
+RenderTextureQuality 1 3
RenderReflectionProbeCount 1 256
//
@@ -128,7 +128,6 @@ RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
RenderDisableVintageMode 1 0
-RenderMaxTextureResolution 1 512
RenderReflectionProbeCount 1 1
//
@@ -171,7 +170,6 @@ RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
RenderDisableVintageMode 1 0
-RenderMaxTextureResolution 1 1024
RenderReflectionProbeCount 1 32
//
@@ -213,7 +211,6 @@ RenderCASSharpness 1 0
RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
-RenderMaxTextureResolution 1 2048
RenderReflectionProbeCount 1 64
//
@@ -255,7 +252,6 @@ RenderCASSharpness 1 0
RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
-RenderMaxTextureResolution 1 2048
RenderReflectionProbeCount 1 64
//
@@ -297,7 +293,6 @@ RenderCASSharpness 1 0.4
RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
-RenderMaxTextureResolution 1 2048
RenderReflectionProbeCount 1 128
//
@@ -339,7 +334,6 @@ RenderCASSharpness 1 0.4
RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
-RenderMaxTextureResolution 1 2048
RenderReflectionProbeCount 1 256
//
@@ -381,7 +375,6 @@ RenderCASSharpness 1 0.4
RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
-RenderMaxTextureResolution 1 2048
RenderReflectionProbeCount 1 256
//
@@ -400,6 +393,24 @@ list VRAMGT512
RenderCompressTextures 1 0
//
+// VRAM 4GB to 8GB
+//
+list VRAMLT8GB
+RenderTextureQuality 1 2
+
+//
+// VRAM 2GB to 4GB
+//
+list VRAMLT4GB
+RenderTextureQuality 1 1
+
+//
+// VRAM 2GB and under
+//
+list VRAMLT2GB
+RenderTextureQuality 1 0
+
+//
// "Default" setups for safe, low, medium, high
//
list safe
@@ -415,7 +426,7 @@ RenderShadowDetail 0 0
RenderReflectionProbeDetail 0 -1
RenderMirrors 0 0
RenderDisableVintageMode 1 0
-RenderMaxTextureResolution 1 2048
+RenderTextureQuality 1 2
RenderReflectionProbeCount 0 0
list Intel
diff --git a/indra/newview/featuretable_mac.txt b/indra/newview/featuretable_mac.txt
index c3e2dd0c41..b11fa28c48 100644
--- a/indra/newview/featuretable_mac.txt
+++ b/indra/newview/featuretable_mac.txt
@@ -1,4 +1,4 @@
-version 73
+version 75
// The version number above should be incremented IF AND ONLY IF some
// change has been made that is sufficiently important to justify
// resetting the graphics preferences of all users to the recommended
@@ -85,7 +85,7 @@ RenderTonemapType 1 1
RenderTonemapMix 1 1
RenderDisableVintageMode 1 1
RenderDownScaleMethod 1 0
-RenderMaxTextureResolution 1 2048
+RenderTextureQuality 1 3
RenderReflectionProbeCount 1 256
//
@@ -128,7 +128,6 @@ RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
RenderDisableVintageMode 1 0
-RenderMaxTextureResolution 1 512
RenderReflectionProbeCount 1 1
//
@@ -171,7 +170,6 @@ RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
RenderDisableVintageMode 1 0
-RenderMaxTextureResolution 1 1024
RenderReflectionProbeCount 1 32
//
@@ -213,7 +211,6 @@ RenderCASSharpness 1 0
RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
-RenderMaxTextureResolution 1 2048
RenderReflectionProbeCount 1 64
//
@@ -255,7 +252,6 @@ RenderCASSharpness 1 0
RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
-RenderMaxTextureResolution 1 2048
RenderReflectionProbeCount 1 64
//
@@ -297,7 +293,6 @@ RenderCASSharpness 1 0
RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
-RenderMaxTextureResolution 1 2048
RenderReflectionProbeCount 1 128
//
@@ -339,7 +334,6 @@ RenderCASSharpness 1 0.4
RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
-RenderMaxTextureResolution 1 2048
RenderReflectionProbeCount 1 256
//
@@ -381,7 +375,6 @@ RenderCASSharpness 1 0.4
RenderExposure 1 1
RenderTonemapType 1 1
RenderTonemapMix 1 0.7
-RenderMaxTextureResolution 1 2048
RenderReflectionProbeCount 1 256
//
@@ -401,6 +394,24 @@ list VRAMGT512
RenderCompressTextures 1 0
//
+// VRAM 4GB to 8GB
+//
+list VRAMLT8GB
+RenderTextureQuality 1 2
+
+//
+// VRAM 2GB to 4GB
+//
+list VRAMLT4GB
+RenderTextureQuality 1 1
+
+//
+// VRAM 2GB and under
+//
+list VRAMLT2GB
+RenderTextureQuality 1 0
+
+//
// "Default" setups for safe, low, medium, high
//
list safe
@@ -414,7 +425,7 @@ RenderDeferredSSAO 0 0
RenderShadowDetail 0 0
RenderMirrors 0 0
RenderDisableVintageMode 1 0
-RenderMaxTextureResolution 1 2048
+RenderTextureQuality 1 2
RenderReflectionProbeCount 0 0
list TexUnit8orLess
diff --git a/indra/newview/lldrawpoolwater.cpp b/indra/newview/lldrawpoolwater.cpp
index cdf3244389..fbecaa1583 100644
--- a/indra/newview/lldrawpoolwater.cpp
+++ b/indra/newview/lldrawpoolwater.cpp
@@ -204,22 +204,22 @@ void LLDrawPoolWater::renderPostDeferred(S32 pass)
if (tex_a && (!tex_b || (tex_a == tex_b)))
{
- shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a);
tex_a->setFilteringOption(filter_mode);
+ shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a);
blend_factor = 0; // only one tex provided, no blending
}
else if (tex_b && !tex_a)
{
- shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_b);
tex_b->setFilteringOption(filter_mode);
+ shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_b);
blend_factor = 0; // only one tex provided, no blending
}
else if (tex_b != tex_a)
{
- shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a);
tex_a->setFilteringOption(filter_mode);
- shader->bindTexture(LLViewerShaderMgr::BUMP_MAP2, tex_b);
tex_b->setFilteringOption(filter_mode);
+ shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a);
+ shader->bindTexture(LLViewerShaderMgr::BUMP_MAP2, tex_b);
}
shader->bindTexture(LLShaderMgr::WATER_EXCLUSIONTEX, &gPipeline.mWaterExclusionMask);
diff --git a/indra/newview/llface.cpp b/indra/newview/llface.cpp
index 018d4c4bba..e34bea63ef 100644
--- a/indra/newview/llface.cpp
+++ b/indra/newview/llface.cpp
@@ -1577,7 +1577,7 @@ bool LLFace::getGeometryVolume(const LLVolume& volume,
skin = mSkinInfo;
}
- //TODO -- cache this (check profile marker above)?
+ //TODO - cache this (check profile marker above)?
glm::mat4 m = glm::make_mat4((F32*)skin->mBindShapeMatrix.getF32ptr());
m = glm::transpose(glm::inverse(m));
mat_normal.loadu(glm::value_ptr(m));
@@ -2265,7 +2265,18 @@ F32 LLFace::getTextureVirtualSize()
face_area = mPixelArea / llclamp(texel_area, 0.015625f, 128.f);
}
- face_area = LLFace::adjustPixelArea(mImportanceToCamera, face_area);
+ // Diffuse source area as the dim-aware hint for adjustPixelArea.
+ S32 source_area = 0;
+ if (mTexture[LLRender::DIFFUSE_MAP].notNull())
+ {
+ S32 sw = mTexture[LLRender::DIFFUSE_MAP]->getFullWidth();
+ S32 sh = mTexture[LLRender::DIFFUSE_MAP]->getFullHeight();
+ if (sw > 0 && sh > 0)
+ {
+ source_area = sw * sh;
+ }
+ }
+ face_area = LLFace::adjustPixelArea(mImportanceToCamera, face_area, source_area);
if(face_area > LLViewerTexture::sMinLargeImageSize) //if is large image, shrink face_area by considering the partial overlapping.
{
if(mImportanceToCamera > LEAST_IMPORTANCE_FOR_LARGE_IMAGE && mTexture[LLRender::DIFFUSE_MAP].notNull() && mTexture[LLRender::DIFFUSE_MAP]->isLargeImage())
@@ -2389,6 +2400,7 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius)
F32 dist = lookAt.getLength3().getF32();
dist = llmax(dist-size.getLength3().getF32(), 0.001f);
+ mDistanceToCamera = dist;
lookAt.normalize3fast() ;
@@ -2513,8 +2525,16 @@ F32 LLFace::calcImportanceToCamera(F32 cos_angle_to_view_dir, F32 dist)
}
//static
-F32 LLFace::adjustPixelArea(F32 importance, F32 pixel_area)
+F32 LLFace::adjustPixelArea(F32 importance, F32 pixel_area, S32 source_area)
{
+ // Dim-aware floor: source_area/256 lowers the "large but unimportant"
+ // clamp proportionally so smaller sources can be pushed past discard 4.
+ F32 large_floor = (F32)LLViewerTexture::sMinLargeImageSize;
+ if (source_area > 0)
+ {
+ large_floor = llmin(large_floor, (F32)source_area / 256.f);
+ }
+
if(pixel_area > LLViewerTexture::sMaxSmallImageSize)
{
if(importance < LEAST_IMPORTANCE) //if the face is not important, do not load hi-res.
@@ -2522,11 +2542,11 @@ F32 LLFace::adjustPixelArea(F32 importance, F32 pixel_area)
static const F32 MAX_LEAST_IMPORTANCE_IMAGE_SIZE = 128.0f * 128.0f ;
pixel_area = llmin(pixel_area * 0.5f, MAX_LEAST_IMPORTANCE_IMAGE_SIZE) ;
}
- else if(pixel_area > LLViewerTexture::sMinLargeImageSize) //if is large image, shrink face_area by considering the partial overlapping.
+ else if(pixel_area > large_floor) //if is large image, shrink face_area by considering the partial overlapping.
{
if(importance < LEAST_IMPORTANCE_FOR_LARGE_IMAGE)//if the face is not important, do not load hi-res.
{
- pixel_area = (F32)LLViewerTexture::sMinLargeImageSize ;
+ pixel_area = large_floor ;
}
}
}
diff --git a/indra/newview/llface.h b/indra/newview/llface.h
index 6e9d23c3a2..71ba3d0f2f 100644
--- a/indra/newview/llface.h
+++ b/indra/newview/llface.h
@@ -242,7 +242,9 @@ private:
bool calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) ;
public:
static F32 calcImportanceToCamera(F32 to_view_dir, F32 dist);
- static F32 adjustPixelArea(F32 importance, F32 pixel_area) ;
+ // source_area > 0 lowers the "large but unimportant" floor for
+ // moderate sources; 0 keeps the legacy sMinLargeImageSize floor.
+ static F32 adjustPixelArea(F32 importance, F32 pixel_area, S32 source_area = 0) ;
public:
@@ -251,6 +253,9 @@ public:
LLVector2 mTexExtents[2];
F32 mDistance;
+ // Camera-to-face distance, cached by calcPixelArea; read by the
+ // streaming math's distance signal.
+ F32 mDistanceToCamera = 0.f;
F32 mLastUpdateTime;
F32 mLastSkinTime;
F32 mLastMoveTime;
diff --git a/indra/newview/llfeaturemanager.cpp b/indra/newview/llfeaturemanager.cpp
index c8692224f1..aab2865ef7 100644
--- a/indra/newview/llfeaturemanager.cpp
+++ b/indra/newview/llfeaturemanager.cpp
@@ -752,9 +752,29 @@ void LLFeatureManager::applyBaseMasks()
{
maskFeatures("VRAMGT512");
}
- if (gGLManager.mVRAM < 2048)
+
+ // Texture quality is driven by detected VRAM. Feature masks take the MIN
+ // of applied values, so cascading lower tiers downgrade RenderTextureQuality:
+ // <= 2GB -> Low(0), <= 4GB -> Medium(1), < 8GB -> High(2), >= 8GB -> Ultra(3).
+ // When VRAM cannot be detected (mVRAM == 0, common on Linux) fall back to Medium.
+ if (gGLManager.mVRAM == 0)
{
- maskFeatures("VRAMLT2GB");
+ maskFeatures("VRAMLT4GB");
+ }
+ else
+ {
+ if (gGLManager.mVRAM < 8192)
+ {
+ maskFeatures("VRAMLT8GB");
+ }
+ if (gGLManager.mVRAM <= 4096)
+ {
+ maskFeatures("VRAMLT4GB");
+ }
+ if (gGLManager.mVRAM <= 2048)
+ {
+ maskFeatures("VRAMLT2GB");
+ }
}
if (gGLManager.mGLVersion < 3.99f)
{
diff --git a/indra/newview/llsurface.cpp b/indra/newview/llsurface.cpp
index 64359b6cbe..fe77d585c8 100644
--- a/indra/newview/llsurface.cpp
+++ b/indra/newview/llsurface.cpp
@@ -57,6 +57,8 @@ namespace
LLColor4U MAX_WATER_COLOR(0, 48, 96, 240);
S32 LLSurface::sTextureSize = 256;
+F32 LLSurface::sNearestVisiblePatchDistance = FLT_MAX;
+U32 LLSurface::sNearestVisiblePatchFrame = 0;
// ---------------- LLSurface:: Public Members ---------------
@@ -122,7 +124,7 @@ LLSurface::~LLSurface()
else if (poolp->mReferences.empty())
{
gPipeline.removePool(poolp);
- // Don't enable this until we blitz the draw pool for it as well. -- djs
+ // Don't enable this until we blitz the draw pool for it as well. - djs
mSTexturep = nullptr;
}
else
@@ -583,6 +585,13 @@ void LLSurface::updatePatchVisibilities(LLAgent &agent)
LLSurfacePatch *patchp;
+ // Reset the cross-region accumulator at the start of each frame.
+ if (sNearestVisiblePatchFrame != gFrameCount)
+ {
+ sNearestVisiblePatchDistance = FLT_MAX;
+ sNearestVisiblePatchFrame = gFrameCount;
+ }
+
mVisiblePatchCount = 0;
for (S32 i=0; i<mNumberOfPatches; i++)
{
@@ -593,6 +602,7 @@ void LLSurface::updatePatchVisibilities(LLAgent &agent)
{
mVisiblePatchCount++;
patchp->updateCameraDistanceRegion(pos_region);
+ sNearestVisiblePatchDistance = llmin(sNearestVisiblePatchDistance, patchp->getDistance());
}
}
}
@@ -961,7 +971,7 @@ std::ostream& operator<<(std::ostream &s, const LLSurface &S)
void LLSurface::createPatchData()
{
// Assumes mGridsPerEdge, mGridsPerPatchEdge, and mPatchesPerEdge have been properly set
- // TODO -- check for create() called when surface is not empty
+ // TODO - check for create() called when surface is not empty
S32 i, j;
LLSurfacePatch *patchp;
diff --git a/indra/newview/llsurface.h b/indra/newview/llsurface.h
index a599019ca5..4bdb90b102 100644
--- a/indra/newview/llsurface.h
+++ b/indra/newview/llsurface.h
@@ -163,6 +163,12 @@ public:
F32 mDetailTextureScale; // Number of times to repeat detail texture across this surface
+ // Closest visible patch distance across all surfaces this frame
+ // (meters), or FLT_MAX if none visible. Drives BOOST_TERRAIN
+ // streaming - terrain has no faces registered with its texture.
+ static F32 sNearestVisiblePatchDistance;
+ static U32 sNearestVisiblePatchFrame;
+
private:
void createSTexture();
void initTextures();
diff --git a/indra/newview/lltexturefetch.cpp b/indra/newview/lltexturefetch.cpp
index 51ade60827..574c200eb4 100644
--- a/indra/newview/lltexturefetch.cpp
+++ b/indra/newview/lltexturefetch.cpp
@@ -542,6 +542,7 @@ private:
S32 mRequestedDiscard;
S32 mLoadedDiscard;
S32 mDecodedDiscard;
+ S32 mCodecLevels = 0;
LLFrameTimer mRequestedDeltaTimer;
LLFrameTimer mFetchDeltaTimer;
LLTimer mCacheReadTimer;
@@ -1843,6 +1844,10 @@ bool LLTextureFetchWorker::doWork(S32 param)
else
{
llassert_always(mRawImage.notNull());
+ if (mFormattedImage.notNull())
+ {
+ mCodecLevels = (S32)mFormattedImage->getLevels();
+ }
LL_DEBUGS(LOG_TXT) << mID << ": Decoded. Discard: " << mDecodedDiscard
<< " Raw Image: " << llformat("%dx%d",mRawImage->getWidth(),mRawImage->getHeight()) << LL_ENDL;
setState(WRITE_TO_CACHE);
@@ -2774,10 +2779,12 @@ LLTextureFetchWorker* LLTextureFetch::getWorker(const LLUUID& id)
// Threads: T*
bool LLTextureFetch::getRequestFinished(const LLUUID& id, S32& discard_level, S32& worker_state,
LLPointer<LLImageRaw>& raw, LLPointer<LLImageRaw>& aux,
- LLCore::HttpStatus& last_http_get_status)
+ LLCore::HttpStatus& last_http_get_status,
+ S32& codec_levels)
{
LL_PROFILE_ZONE_SCOPED;
bool res = false;
+ codec_levels = 0;
LLTextureFetchWorker* worker = getWorker(id);
if (worker)
{
@@ -2809,6 +2816,9 @@ bool LLTextureFetch::getRequestFinished(const LLUUID& id, S32& discard_level, S3
discard_level = worker->mDecodedDiscard;
raw = worker->mRawImage;
aux = worker->mAuxImage;
+ // Cached on the worker so the value survives mFormattedImage
+ // clears (cache-retry, decode-abort, write-to-cache complete).
+ codec_levels = worker->mCodecLevels;
decode_time = worker->mDecodeTime;
fetch_time = worker->mFetchTime;
diff --git a/indra/newview/lltexturefetch.h b/indra/newview/lltexturefetch.h
index 851d6c11a0..d75e16ab7c 100644
--- a/indra/newview/lltexturefetch.h
+++ b/indra/newview/lltexturefetch.h
@@ -106,7 +106,8 @@ public:
// keep in mind that if fetcher isn't done, it still might need original raw image
bool getRequestFinished(const LLUUID& id, S32& discard_level, S32& worker_state,
LLPointer<LLImageRaw>& raw, LLPointer<LLImageRaw>& aux,
- LLCore::HttpStatus& last_http_get_status);
+ LLCore::HttpStatus& last_http_get_status,
+ S32& codec_levels);
// Threads: T*
bool updateRequestPriority(const LLUUID& id, F32 priority);
diff --git a/indra/newview/lltextureview.cpp b/indra/newview/lltextureview.cpp
index 8cbede8303..4534db958f 100644
--- a/indra/newview/lltextureview.cpp
+++ b/indra/newview/lltextureview.cpp
@@ -572,9 +572,13 @@ void LLGLTexMemBar::draw()
LLFontGL::getFontMonospace()->renderUTF8(text, 0, 0, v_offset + line_height*8,
text_color, LLFontGL::LEFT, LLFontGL::TOP);
- text = llformat("Images: %d Raw: %d (%.2f MB) Saved: %d (%.2f MB) Aux: %d (%.2f MB)", image_count, raw_image_count, raw_image_bytes_MB,
+ text = llformat("Images: %d Raw: %d (%.2f MB) Saved: %d (%.2f MB) Aux: %d (%.2f MB) Bubble: %.1fm PressMult: %.1fx LDMin: %.1f",
+ image_count, raw_image_count, raw_image_bytes_MB,
saved_raw_image_count, saved_raw_image_bytes_MB,
- aux_raw_image_count, aux_raw_image_bytes_MB);
+ aux_raw_image_count, aux_raw_image_bytes_MB,
+ LLViewerTextureList::sCurrentBubbleMeters,
+ LLViewerTexture::sMemoryPressureMultiplier,
+ LLViewerTexture::sLastDitchMinDiscard);
LLFontGL::getFontMonospace()->renderUTF8(text, 0, 0, v_offset + line_height * 7,
text_color, LLFontGL::LEFT, LLFontGL::TOP);
diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp
index 0c93b24751..a4a7f1fd20 100644
--- a/indra/newview/llviewercontrol.cpp
+++ b/indra/newview/llviewercontrol.cpp
@@ -112,6 +112,48 @@ static bool handleRenderAvatarMouselookChanged(const LLSD& newvalue)
return true;
}
+static bool handleRenderTextureQualityChanged(const LLSD& newvalue)
+{
+ // 0=Low, 1=Medium, 2=High, 3=Ultra. Drives RenderMaxTextureResolution,
+ // the four TextureChannel* exponents (Normal/BaseColor/Spec/Emissive),
+ // and TextureDistanceDiscardPower.
+ U32 quality = (U32)newvalue.asInteger();
+ U32 max_res = 2048;
+ F32 ch_normal = 1.0f;
+ F32 ch_basecolor = 0.75f;
+ F32 ch_specular = 0.5f;
+ F32 ch_emissive = 0.75f;
+ F32 distance_power = 0.5f;
+ switch (quality)
+ {
+ case 0: // Low
+ max_res = 1024;
+ ch_normal = 0.5f; ch_basecolor = 0.75f; ch_specular = 0.1f; ch_emissive = 0.5f;
+ distance_power = 0.15f;
+ break;
+ case 1: // Medium
+ ch_normal = 0.75f; ch_basecolor = 0.75f; ch_specular = 0.3f; ch_emissive = 0.75f;
+ distance_power = 0.25f;
+ break;
+ case 2: // High
+ // channel defaults above
+ distance_power = 0.35f;
+ break;
+ case 3: // Ultra
+ default:
+ ch_normal = 1.f; ch_basecolor = 1.f; ch_specular = 1.f; ch_emissive = 1.f;
+ distance_power = 0.5f;
+ break;
+ }
+ gSavedSettings.setU32("RenderMaxTextureResolution", max_res);
+ gSavedSettings.setF32("TextureChannelNormal", ch_normal);
+ gSavedSettings.setF32("TextureChannelBaseColor", ch_basecolor);
+ gSavedSettings.setF32("TextureChannelSpecular", ch_specular);
+ gSavedSettings.setF32("TextureChannelEmissive", ch_emissive);
+ gSavedSettings.setF32("TextureDistanceDiscardPower", distance_power);
+ return true;
+}
+
static bool handleRenderFarClipChanged(const LLSD& newvalue)
{
if (LLStartUp::getStartupState() >= STATE_STARTED)
@@ -815,6 +857,7 @@ void settings_setup_listeners()
{
LL_PROFILE_ZONE_SCOPED;
setting_setup_signal_listener(gSavedSettings, "FirstPersonAvatarVisible", handleRenderAvatarMouselookChanged);
+ setting_setup_signal_listener(gSavedSettings, "RenderTextureQuality", handleRenderTextureQualityChanged);
setting_setup_signal_listener(gSavedSettings, "RenderFarClip", handleRenderFarClipChanged);
setting_setup_signal_listener(gSavedSettings, "RenderTerrainScale", handleTerrainScaleChanged);
setting_setup_signal_listener(gSavedSettings, "RenderTerrainPBRScale", handlePBRTerrainScaleChanged);
diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp
index 0f23596c9a..82f4cc8341 100644
--- a/indra/newview/llviewertexture.cpp
+++ b/indra/newview/llviewertexture.cpp
@@ -87,6 +87,17 @@ S32 LLViewerTexture::sRawCount = 0;
S32 LLViewerTexture::sAuxCount = 0;
LLFrameTimer LLViewerTexture::sEvaluationTimer;
F32 LLViewerTexture::sDesiredDiscardBias = 0.f;
+F32 LLViewerTexture::sBackgroundFactor = 0.f;
+F32 LLViewerTexture::sMemoryPressureMultiplier = 1.f;
+F32 LLViewerTexture::sLastDitchMinDiscard = 0.f;
+
+//static
+F32 LLViewerTexture::getMemoryPressureProgress()
+{
+ static LLCachedControl<F32> max_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f);
+ F32 cap = llmax((F32)max_mult, 1.0001f);
+ return llclampf((sMemoryPressureMultiplier - 1.f) / (cap - 1.f));
+}
U32 LLViewerTexture::sBiasTexturesUpdated = 0;
S32 LLViewerTexture::sMaxSculptRez = 128; //max sculpt image size
@@ -97,12 +108,12 @@ constexpr S32 DEFAULT_ICON_DIMENSIONS = 32;
constexpr S32 DEFAULT_THUMBNAIL_DIMENSIONS = 256;
U32 LLViewerTexture::sMinLargeImageSize = 65536; //256 * 256.
U32 LLViewerTexture::sMaxSmallImageSize = MAX_CACHED_RAW_IMAGE_AREA;
-bool LLViewerTexture::sFreezeImageUpdates = false;
F32 LLViewerTexture::sCurrentTime = 0.0f;
constexpr F32 MEMORY_CHECK_WAIT_TIME = 1.0f;
constexpr F32 MIN_VRAM_BUDGET = 768.f;
F32 LLViewerTexture::sFreeVRAMMegabytes = MIN_VRAM_BUDGET;
+F32 LLViewerTexture::sWindowPixelArea = 1.f;
LLViewerTexture::EDebugTexels LLViewerTexture::sDebugTexelsMode = LLViewerTexture::DEBUG_TEXELS_OFF;
@@ -120,7 +131,7 @@ LLLoadedCallbackEntry::LLLoadedCallbackEntry(loaded_callback_func cb,
LLViewerFetchedTexture* target,
bool pause)
: mCallback(cb),
- mLastUsedDiscard(MAX_DISCARD_LEVEL+1),
+ mLastUsedDiscard(S32_MAX),
mDesiredDiscard(discard_level),
mNeedsImageRaw(need_imageraw),
mUserData(userdata),
@@ -485,6 +496,13 @@ void LLViewerTexture::updateClass()
LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
sCurrentTime = gFrameTimeSeconds;
+ if (gViewerWindow)
+ {
+ F32 w = (F32)gViewerWindow->getWindowWidthRaw();
+ F32 h = (F32)gViewerWindow->getWindowHeightRaw();
+ sWindowPixelArea = llmax(w * h, 1.f);
+ }
+
LLTexturePipelineTester* tester = (LLTexturePipelineTester*)LLMetricPerformanceTesterBasic::getTester(sTesterName);
if (tester)
{
@@ -523,6 +541,133 @@ void LLViewerTexture::updateClass()
F32 over_pct = (used - target) / target;
+ // Predicted-VRAM pressure controller. Eviction is fast, refetch is slow,
+ // so feedback on instantaneous `used` sawtooths; feeding `used +
+ // in_flight_delta` lets mult converge to equilibrium instead of cycling.
+ {
+ static LLCachedControl<F32> backoff_start(gSavedSettings, "TextureMemoryPressureBackoffStart", 0.85f);
+ static LLCachedControl<F32> max_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f);
+ static LLCachedControl<F32> prediction_gain(gSavedSettings, "TextureMemoryPressurePredictionGain", 10.f);
+ static LLCachedControl<F32> smoothing_rate(gSavedSettings, "TextureMemoryPressureSmoothingRate", 4.f);
+
+ F32 backoff_target = target * llclamp((F32)backoff_start, 0.05f, 1.f);
+ F32 cap = llmax((F32)max_mult, 1.0001f);
+ F32 dt = (F32)gFrameIntervalSeconds;
+
+ // Skip the full-list iteration when there is no pressure to react to:
+ // mult already at baseline, last-ditch at zero, and used well clear of
+ // the backoff target. Worst case the controller picks up the spike one
+ // frame later, from `used` alone.
+ bool need_predict = sMemoryPressureMultiplier > 1.001f
+ || sLastDitchMinDiscard > 0.f
+ || used > backoff_target * 0.5f;
+
+ S64 pending_bytes_increase = 0;
+ S64 pending_bytes_decrease = 0;
+ if (need_predict)
+ {
+ LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vt - in-flight predict");
+ for (auto& imagep : gTextureList)
+ {
+ if (imagep.isNull()) continue;
+ // Cheap inline checks first so the virtual getDiscardLevel()
+ // call only fires when there is a real chance of contribution.
+ S32 desired = imagep->getDesiredDiscardLevel();
+ if (desired < 0) continue;
+ S32 fw = imagep->getFullWidth();
+ S32 fh = imagep->getFullHeight();
+ if (fw <= 0 || fh <= 0) continue;
+ S32 current = imagep->getDiscardLevel();
+ if (current < 0 || desired == current) continue;
+
+ S32 wd = llmax(1, fw >> desired);
+ S32 hd = llmax(1, fh >> desired);
+ S32 wc = llmax(1, fw >> current);
+ S32 hc = llmax(1, fh >> current);
+ // bpp=4, mip pyramid overhead 4/3
+ S64 size_d = (S64)wd * hd * 4 * 4 / 3;
+ S64 size_c = (S64)wc * hc * 4 * 4 / 3;
+
+ if (desired < current)
+ pending_bytes_increase += (size_d - size_c);
+ else
+ pending_bytes_decrease += (size_c - size_d);
+ }
+ }
+
+ // 1024 * 512 = 524288: matches the unit reduction at line 513.
+ constexpr F32 BYTES_TO_USED_UNITS = 1.f / 524288.f;
+ F32 predicted_used = used
+ + (F32)pending_bytes_increase * BYTES_TO_USED_UNITS
+ - (F32)pending_bytes_decrease * BYTES_TO_USED_UNITS;
+ F32 predicted_over = predicted_used / llmax(backoff_target, 1.f);
+
+ // High water mark: when used crosses budget * high_water, skip the
+ // smoothed convergence and slam the controller into hard-cap state.
+ // Recovers the historical 90% behavior - immediate aggressive
+ // response instead of waiting for the lerp to chase the target.
+ static LLCachedControl<F32> high_water(gSavedSettings, "TextureMemoryHighWaterMark", 0.8f);
+ bool above_high_water = used >= budget * llclamp((F32)high_water, 0.5f, 1.f);
+
+ F32 target_mult = llclamp(powf(llmax(predicted_over, 1.f), llmax((F32)prediction_gain, 0.0001f)), 1.f, cap);
+ if (above_high_water)
+ {
+ target_mult = cap;
+ sMemoryPressureMultiplier = cap;
+ }
+ else
+ {
+ // ~63% convergence in 1/smoothing_rate seconds (default 0.25s).
+ F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)smoothing_rate, 0.f));
+ sMemoryPressureMultiplier += (target_mult - sMemoryPressureMultiplier) * alpha;
+ }
+ sMemoryPressureMultiplier = llclamp(sMemoryPressureMultiplier, 1.f, cap);
+
+ F32 progress = getMemoryPressureProgress();
+
+ {
+ static LLCachedControl<F32> ld_engage(gSavedSettings, "TextureLastDitchEngageProgress", 0.95f);
+ static LLCachedControl<F32> ld_ramp(gSavedSettings, "TextureLastDitchRampRate", 0.5f);
+ static LLCachedControl<F32> ld_decay(gSavedSettings, "TextureLastDitchDecayRate", 0.5f);
+ static LLCachedControl<F32> ld_max(gSavedSettings, "TextureLastDitchMinDiscardMax", 13.f);
+ // Above the high water mark, last-ditch creeps regardless of
+ // mult_progress: by definition we are out of normal headroom.
+ bool engage = above_high_water || progress >= llclampf((F32)ld_engage);
+ if (engage && predicted_over > 1.f)
+ {
+ sLastDitchMinDiscard += llmax((F32)ld_ramp, 0.f) * dt;
+ }
+ else if (!above_high_water && predicted_over < 1.f)
+ {
+ sLastDitchMinDiscard -= llmax((F32)ld_decay, 0.f) * dt;
+ }
+ sLastDitchMinDiscard = llclamp(sLastDitchMinDiscard, 0.f, llmax((F32)ld_max, 0.f));
+ }
+
+ // 1 Hz pressure log.
+ static LLFrameTimer s_pressure_log_timer;
+ if (s_pressure_log_timer.getElapsedTimeF32() > 1.f)
+ {
+ s_pressure_log_timer.reset();
+ F32 over = used / llmax(backoff_target, 1.f);
+ LL_INFOS("TextureStream") << "pressure"
+ << " mult=" << sMemoryPressureMultiplier
+ << " target_mult=" << target_mult
+ << " progress=" << progress
+ << " used=" << used
+ << " predicted=" << predicted_used
+ << " target=" << target
+ << " over=" << over
+ << " pred_over=" << predicted_over
+ << " in+=" << (S32)(pending_bytes_increase / 1024 / 1024)
+ << "MB in-=" << (S32)(pending_bytes_decrease / 1024 / 1024)
+ << "MB bias=" << sDesiredDiscardBias
+ << " ldmin=" << sLastDitchMinDiscard
+ << " dsq=" << (S32)gTextureList.mDownScaleQueue.size()
+ << LL_ENDL;
+ }
+ }
+
bool is_sys_low = isSystemMemoryLow();
bool is_low = is_sys_low || over_pct > 0.f;
@@ -569,6 +714,10 @@ void LLViewerTexture::updateClass()
// don't execute above until the slam to 1.5 has a chance to take effect
sEvaluationTimer.reset();
+ // Don't decay bias while downscale is still draining - those bytes
+ // are about to free and the loop would oscillate.
+ bool eviction_in_flight = !gTextureList.mDownScaleQueue.empty();
+
// lower discard bias over time when at least 10% of budget is free
constexpr F32 FREE_PERCENTAGE_TRESHOLD = -0.1f;
constexpr U32 FREE_SYS_MEM_TRESHOLD = 100;
@@ -576,7 +725,8 @@ void LLViewerTexture::updateClass()
const S32Megabytes MIN_FREE_MAIN_MEMORY(min_free_main_memory() + FREE_SYS_MEM_TRESHOLD);
if (sDesiredDiscardBias > 1.f
&& over_pct < FREE_PERCENTAGE_TRESHOLD
- && getFreeSystemMemory() > MIN_FREE_MAIN_MEMORY)
+ && getFreeSystemMemory() > MIN_FREE_MAIN_MEMORY
+ && !eviction_in_flight)
{
static LLCachedControl<F32> high_mem_discard_decrement(gSavedSettings, "RenderHighMemMinDiscardDecrement", .1f);
@@ -622,6 +772,33 @@ void LLViewerTexture::updateClass()
}
}
+ // Background-window ramp: 0 -> 1 at rate per second while backgrounded,
+ // snaps to 0 in foreground. Default 0.011 ~ 90s to saturate.
+ {
+ static LLCachedControl<F32> bg_factor_rate(gSavedSettings, "TextureBackgroundFactorRatePerSec", 0.011f);
+ if (in_background)
+ {
+ sBackgroundFactor += (F32)bg_factor_rate * gFrameIntervalSeconds;
+ sBackgroundFactor = llclampf(sBackgroundFactor);
+ }
+ else
+ {
+ sBackgroundFactor = 0.f;
+ }
+ }
+
+ // Fetch-queue depth as a one-way bias floor (decay path still drops
+ // bias when the queue drains). Pushes bias up before VRAM overflows
+ // during teleport/scene-change floods.
+ if (LLTextureFetch* fetcher = LLAppViewer::getTextureFetch())
+ {
+ S32 pending = fetcher->getNumRequests();
+ static LLCachedControl<F32> fetch_pressure_scale(gSavedSettings, "TextureFetchPressureScale", 1000.f);
+ F32 scale = llmax((F32)fetch_pressure_scale, 1.f);
+ F32 fetch_pressure = llclamp((F32)pending / scale, 0.f, 3.f);
+ sDesiredDiscardBias = llmax(sDesiredDiscardBias, 1.f + fetch_pressure);
+ }
+
sDesiredDiscardBias = llclamp(sDesiredDiscardBias, 1.f, 4.f);
if (last_texture_update_count_bias < sDesiredDiscardBias)
{
@@ -638,8 +815,6 @@ void LLViewerTexture::updateClass()
// a problem.
last_texture_update_count_bias = sDesiredDiscardBias;
}
-
- LLViewerTexture::sFreezeImageUpdates = false;
}
//static
@@ -1156,8 +1331,10 @@ void LLViewerFetchedTexture::init(bool firstinit)
mRequestedDownloadPriority = 0.f;
mFullyLoaded = false;
mCanUseHTTP = true;
- mDesiredDiscardLevel = MAX_DISCARD_LEVEL + 1;
- mMinDesiredDiscardLevel = MAX_DISCARD_LEVEL + 1;
+ mDesiredDiscardLevel = S8_MAX;
+ // S8_MAX = no cap. setMinDiscardLevel takes min(current, new), so
+ // explicit caps from terrain / avatar self / thumbnails still apply.
+ mMinDesiredDiscardLevel = S8_MAX;
mDecodingAux = false;
@@ -1977,21 +2154,10 @@ bool LLViewerFetchedTexture::processFetchResults(S32& desired_discard, S32 curre
setIsMissingAsset();
desired_discard = -1;
}
- else
- {
- //LL_WARNS() << mID << ": Setting min discard to " << current_discard << LL_ENDL;
- if (current_discard >= 0)
- {
- mMinDiscardLevel = current_discard;
- //desired_discard = current_discard;
- }
- else
- {
- S32 dis_level = getDiscardLevel();
- mMinDiscardLevel = dis_level;
- //desired_discard = dis_level;
- }
- }
+ // Transient failure (decoder OOM, network blip): don't latch
+ // mMinDiscardLevel - that would block all future fetches via
+ // the make_request gate. Permanent failures are caught above
+ // (getDiscardLevel()<0 -> setIsMissingAsset).
destroyRawImage();
}
else if (mRawImage.notNull())
@@ -2072,8 +2238,18 @@ bool LLViewerFetchedTexture::updateFetch()
if (mRawImage.notNull()) sRawCount--;
if (mAuxRawImage.notNull()) sAuxCount--;
// keep in mind that fetcher still might need raw image, don't modify original
+ S32 codec_levels = 0;
bool finished = LLAppViewer::getTextureFetch()->getRequestFinished(getID(), fetch_discard, mFetchState, mRawImage, mAuxRawImage,
- mLastHttpGetStatus);
+ mLastHttpGetStatus, codec_levels);
+ if (codec_levels > 0)
+ {
+ mCodecMaxDiscardLevel = (S8)llmin(codec_levels, (S32)S8_MAX);
+ if (codec_levels > 5)
+ {
+ LL_DEBUGS("TextureStream") << "Texture " << mID << " codec-reported max discard "
+ << codec_levels << " (above the historical hardcoded cap of 5)" << LL_ENDL;
+ }
+ }
if (mRawImage.notNull()) sRawCount++;
if (mAuxRawImage.notNull())
{
@@ -2108,7 +2284,9 @@ bool LLViewerFetchedTexture::updateFetch()
}
}
- desired_discard = llmin(desired_discard, getMaxDiscardLevel());
+ // Clamp the fetch request to what the codestream encodes; deeper
+ // discards are served from the GL mip pyramid via scaleDown.
+ desired_discard = llmin(desired_discard, (S32)mCodecMaxDiscardLevel);
bool make_request = true;
if (decode_priority <= 0)
@@ -2116,9 +2294,13 @@ bool LLViewerFetchedTexture::updateFetch()
LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - priority <= 0");
make_request = false;
}
- else if (mDesiredDiscardLevel > getMaxDiscardLevel())
+ else if (mDesiredDiscardLevel > (S32)mCodecMaxDiscardLevel &&
+ current_discard >= 0)
{
- LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - desired > max");
+ // Desired is past codec_max. Only scaleDown can satisfy it.
+ // Applies even when current is also past codec_max (post-scaleDown);
+ // re-fetching at codec_max then scaleDown-ing again is pure thrash.
+ LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - desired > codec max");
make_request = false;
}
else if (mNeedsCreateTexture || mIsMissingAsset)
@@ -2570,20 +2752,21 @@ bool LLViewerFetchedTexture::doLoadedCallbacks()
S32 gl_discard = getDiscardLevel();
- // If we don't have a legit GL image, set it to be lower than the worst discard level
+ // S32_MAX is the "no data" sentinel; real discards can now exceed
+ // MAX_DISCARD_LEVEL via dimDerivedMaxDiscard.
if (gl_discard == -1)
{
- gl_discard = MAX_DISCARD_LEVEL + 1;
+ gl_discard = S32_MAX;
}
//
// Determine the quality levels of textures that we can provide to callbacks
// and whether we need to do decompression/readback to get it
//
- S32 current_raw_discard = MAX_DISCARD_LEVEL + 1; // We can always do a readback to get a raw discard
+ S32 current_raw_discard = S32_MAX; // We can always do a readback to get a raw discard
S32 best_raw_discard = gl_discard; // Current GL quality level
- S32 current_aux_discard = MAX_DISCARD_LEVEL + 1;
- S32 best_aux_discard = MAX_DISCARD_LEVEL + 1;
+ S32 current_aux_discard = S32_MAX;
+ S32 best_aux_discard = S32_MAX;
LLImageRaw *current_raw_image = nullptr;
if (mIsRawImageValid)
@@ -2683,7 +2866,7 @@ bool LLViewerFetchedTexture::doLoadedCallbacks()
//
// Run raw/auxiliary data callbacks
//
- if (run_raw_callbacks && current_raw_image != nullptr && (current_raw_discard <= getMaxDiscardLevel()))
+ if (run_raw_callbacks && current_raw_image != nullptr && current_raw_discard != S32_MAX)
{
// Do callbacks which require raw image data.
//LL_INFOS() << "doLoadedCallbacks raw for " << getID() << LL_ENDL;
@@ -2723,7 +2906,7 @@ bool LLViewerFetchedTexture::doLoadedCallbacks()
//
// Run GL callbacks
//
- if (run_gl_callbacks && (gl_discard <= getMaxDiscardLevel()))
+ if (run_gl_callbacks && gl_discard != S32_MAX)
{
//LL_INFOS() << "doLoadedCallbacks GL for " << getID() << LL_ENDL;
@@ -3026,11 +3209,6 @@ S8 LLViewerLODTexture::getType() const
return LLViewerTexture::LOD_TEXTURE;
}
-bool LLViewerLODTexture::isUpdateFrozen()
-{
- return LLViewerTexture::sFreezeImageUpdates;
-}
-
// This is gauranteed to get called periodically for every texture
//virtual
void LLViewerLODTexture::processTextureStats()
@@ -3056,18 +3234,15 @@ void LLViewerLODTexture::processTextureStats()
{
mDesiredDiscardLevel = 0;
}
- // Generate the request priority and render priority
- else if (mDontDiscard || !mUseMipMaps)
+ // HUD/UI/preview and mDontDiscard textures bypass streaming - no
+ // face_distance signal applies, they need native resolution.
+ else if (mBoostLevel >= LLGLTexture::BOOST_HIGH
+ || mDontDiscard
+ || !mUseMipMaps)
{
mDesiredDiscardLevel = 0;
if (mFullWidth > MAX_IMAGE_SIZE_DEFAULT || mFullHeight > MAX_IMAGE_SIZE_DEFAULT)
- mDesiredDiscardLevel = 1; // MAX_IMAGE_SIZE_DEFAULT = 2048 and max size ever is 4096
- }
- else if (mBoostLevel < LLGLTexture::BOOST_HIGH && mMaxVirtualSize <= 10.f)
- {
- // If the image has not been significantly visible in a while, we don't want it
- mDesiredDiscardLevel = llmin(mMinDesiredDiscardLevel, (S8)(MAX_DISCARD_LEVEL + 1));
- mDesiredDiscardLevel = llmin(mDesiredDiscardLevel, (S32)mLoadedCallbackDesiredDiscardLevel);
+ mDesiredDiscardLevel = 1; // 4096^2 source can't be loaded full res
}
else if (!mFullWidth || !mFullHeight)
{
@@ -3076,28 +3251,104 @@ void LLViewerLODTexture::processTextureStats()
}
else
{
- //static const F64 log_2 = log(2.0);
- static const F64 log_4 = log(4.0);
-
F32 discard_level = 0.f;
- // If we know the output width and height, we can force the discard
- // level to the correct value, and thus not decode more texture
- // data than we need to.
+ // floor(log2(max(w, h))) - both the multiplier on the normalized
+ // factor and the cap clamp at the bottom of this function.
+ S32 dim_max_for_image_i = (mFullWidth > 0 && mFullHeight > 0)
+ ? LLImageGL::dimDerivedMaxDiscard(mFullWidth, mFullHeight)
+ : (S32)mCodecMaxDiscardLevel;
+ F32 dim_max_for_image = (F32)dim_max_for_image_i;
+
if (mKnownDrawWidth && mKnownDrawHeight)
{
+ // UI-pinned target dimensions - use pixel-area math.
+ static const F64 log_4 = log(4.0);
S32 draw_texels = mKnownDrawWidth * mKnownDrawHeight;
draw_texels = llclamp(draw_texels, MIN_IMAGE_AREA, MAX_IMAGE_AREA);
-
- // Use log_4 because we're in square-pixel space, so an image
- // with twice the width and twice the height will have mTexelsPerImage
- // 4 * draw_size
discard_level = (F32)(log(mTexelsPerImage / draw_texels) / log_4);
}
else
{
- // Calculate the required scale factor of the image using pixels per texel
- discard_level = (F32)(log(mTexelsPerImage / mMaxVirtualSize) / log_4);
+ // Two 0..1 signals composed multiplicatively:
+ // discard = distance_factor * size_factor * max_discard
+ // distance_factor: face_dist / draw_dist, shaped by
+ // TextureDistanceDiscardPower (default 0.5 = sqrt).
+ // size_factor: 1 - (mMaxOnScreenSize / window_pixels), shaped
+ // by TextureSizeDiscardPower.
+ // Either factor near 0 keeps the result fine - both have to
+ // be high for the texture to go deep.
+ static LLCachedControl<F32> distance_power(gSavedSettings, "TextureDistanceDiscardPower", 0.5f);
+ F32 power = llmax((F32)distance_power, 0.0001f);
+ F32 distance_factor = (power == 1.f) ? mMinDistanceFactor : powf(mMinDistanceFactor, power);
+
+ static LLCachedControl<F32> size_power(gSavedSettings, "TextureSizeDiscardPower", 1.f);
+ F32 sz_power = llmax((F32)size_power, 0.0001f);
+ F32 coverage = llclampf(mMaxOnScreenSize / sWindowPixelArea);
+ F32 inv_cov = 1.f - coverage;
+ F32 size_factor = (sz_power == 1.f) ? inv_cov : powf(inv_cov, sz_power);
+
+ F32 combined = distance_factor * size_factor;
+
+ // VRAM pressure: multiply the combined signal and clamp to 0..1.
+ // Compresses the effective draw range and picks up close-coverage
+ // textures (small combined) too. Applied before the channel
+ // exponent so subsequent transforms see a normalized 0..1 value.
+ // Avatar bakes exempt.
+ if (!isAgentAvatarBoost(mBoostLevel) && sMemoryPressureMultiplier > 1.f)
+ {
+ combined = llmin(combined * sMemoryPressureMultiplier, 1.f);
+ }
+
+ // Per-channel exponent. 1.0 = baseline; <1.0 pushes combined
+ // toward 1 (max attenuation) faster. Edges are preserved:
+ // pow(0, p) = 0, pow(1, p) = 1.
+ // mPriorityChannel order: 0=Normal, 1=BaseColor, 2=Specular, 3=Emissive.
+ S32 priority_channel = (mPriorityChannel >= 0 && mPriorityChannel < 4) ? (S32)mPriorityChannel : 1;
+ static LLCachedControl<F32> channel_normal (gSavedSettings, "TextureChannelNormal", 1.0f);
+ static LLCachedControl<F32> channel_basecolor(gSavedSettings, "TextureChannelBaseColor", 0.75f);
+ static LLCachedControl<F32> channel_specular (gSavedSettings, "TextureChannelSpecular", 0.5f);
+ static LLCachedControl<F32> channel_emissive (gSavedSettings, "TextureChannelEmissive", 0.75f);
+ const F32 channels[4] = {
+ (F32)channel_normal,
+ (F32)channel_basecolor,
+ (F32)channel_specular,
+ (F32)channel_emissive,
+ };
+ F32 channel_power = llmax(channels[priority_channel], 0.0001f);
+ if (channel_power != 1.f)
+ {
+ combined = powf(combined, channel_power);
+ }
+
+ // Own-avatar boost: shave combined for rigged/animated faces
+ // on gAgentAvatarp. Preference, not exemption - applied
+ // before the staleness/background/pressure floors so heavy
+ // pressure can still evict.
+ if (mOnAgentAvatar)
+ {
+ static LLCachedControl<F32> agent_avatar_boost(gSavedSettings, "TextureAgentAvatarBoost", 0.5f);
+ combined *= llclampf((F32)agent_avatar_boost);
+ }
+
+ // Staleness / background floors. Avatar bakes exempt from
+ // background to avoid the universal-cloud bug when re-foregrounding.
+ combined = llmax(combined, mStalenessFactor);
+ if (!isAgentAvatarBoost(mBoostLevel))
+ {
+ // Background floor capped at (dim_max - offset) so we can
+ // keep some baseline quality while backgrounded.
+ static LLCachedControl<S32> bg_offset(gSavedSettings, "TextureBackgroundDiscardOffset", 2);
+ F32 bg = sBackgroundFactor;
+ if ((S32)bg_offset > 0 && dim_max_for_image > 0.f)
+ {
+ F32 cap = llmax(dim_max_for_image - (F32)(S32)bg_offset, 0.f) / dim_max_for_image;
+ bg = llmin(bg, cap);
+ }
+ combined = llmax(combined, bg);
+ }
+
+ discard_level = combined * dim_max_for_image;
}
discard_level = floorf(discard_level);
@@ -3106,12 +3357,43 @@ void LLViewerLODTexture::processTextureStats()
if (mFullWidth > max_tex_res || mFullHeight > max_tex_res)
min_discard = 1.f;
- discard_level = llclamp(discard_level, min_discard, (F32)MAX_DISCARD_LEVEL);
+ // dim_max_for_image_i is the per-texture cap. TextureMaxDiscardOverride
+ // raises it (debug). Codec_max applies only to fetches, not here.
+ static LLCachedControl<S32> max_discard_override(gSavedSettings, "TextureMaxDiscardOverride", 0);
+ S32 effective_cap = (max_discard_override > 0) ? (S32)max_discard_override : dim_max_for_image_i;
+ discard_level = llclamp(discard_level, min_discard, (F32)effective_cap);
+
+ mDesiredDiscardLevel = llmin(effective_cap, (S32)discard_level);
+
+ // Apply the setMinDiscardLevel cap, relaxed under VRAM pressure
+ // (cap_relax = 1 - 1/mult: 0 at mult=1, ~0.5 at mult=2, ~0.9 at
+ // mult=10). Caps of 0 (thumbnails) and avatar bakes are preserved.
+ S32 effective_min_cap = mMinDesiredDiscardLevel;
+ if (sMemoryPressureMultiplier > 1.f &&
+ mMinDesiredDiscardLevel > 0 && mMinDesiredDiscardLevel < S8_MAX &&
+ !isAgentAvatarBoost(mBoostLevel))
+ {
+ F32 cap_relax = 1.f - 1.f / sMemoryPressureMultiplier;
+ F32 room = (F32)dim_max_for_image_i - (F32)mMinDesiredDiscardLevel;
+ effective_min_cap += (S32)(cap_relax * room);
+ effective_min_cap = llmin(effective_min_cap, dim_max_for_image_i);
+ }
+ mDesiredDiscardLevel = llmin((S8)effective_min_cap, mDesiredDiscardLevel);
+
+ // Halve the floor for bubble-resident textures (mMinDistanceFactor == 0
+ // = at least one face inside the bubble) so the close-vs-far gradient
+ // is preserved at every pressure level.
+ if (!isAgentAvatarBoost(mBoostLevel))
+ {
+ S32 forced = (S32)floorf(sLastDitchMinDiscard);
+ if (mMinDistanceFactor <= 0.f) forced /= 2;
+ forced = llclamp(forced, 0, dim_max_for_image_i);
+ if (forced > mDesiredDiscardLevel)
+ {
+ mDesiredDiscardLevel = (S8)forced;
+ }
+ }
- // Can't go higher than the max discard level
- mDesiredDiscardLevel = llmin(getMaxDiscardLevel() + 1, (S32)discard_level);
- // Clamp to min desired discard
- mDesiredDiscardLevel = llmin(mMinDesiredDiscardLevel, mDesiredDiscardLevel);
//
// At this point we've calculated the quality level that we want,
@@ -3120,7 +3402,9 @@ void LLViewerLODTexture::processTextureStats()
//
S32 current_discard = getDiscardLevel();
- if (mBoostLevel < LLGLTexture::BOOST_AVATAR_BAKED)
+ // Avatar bakes exempt: shrinking mid-bake can leave the avatar
+ // stuck as a cloud until the next bake completes.
+ if (!isAgentAvatarBoost(mBoostLevel))
{
if (current_discard < mDesiredDiscardLevel && !mForceToSaveRawImage)
{ // should scale down
@@ -3128,13 +3412,6 @@ void LLViewerLODTexture::processTextureStats()
}
}
- if (isUpdateFrozen() // we are out of memory and nearing max allowed bias
- && mBoostLevel < LLGLTexture::BOOST_SCULPTED
- && mDesiredDiscardLevel < current_discard)
- {
- // stop requesting more
- mDesiredDiscardLevel = current_discard;
- }
mDesiredDiscardLevel = llmin(mDesiredDiscardLevel, (S32)mLoadedCallbackDesiredDiscardLevel);
}
@@ -3160,6 +3437,22 @@ bool LLViewerLODTexture::scaleDown()
return false;
}
+ // Hard structural blocks only. Per-texture policy (icons pinned to full
+ // res, etc.) lives in processTextureStats; if that policy is later
+ // relaxed (e.g. honor mKnownDrawWidth for icons rendered at 8x8 in a
+ // friend list) the scaleDown path stays open.
+ // BOOST_HIGH is the emergency-out for GLTF's "force full res" hack;
+ // the other two flags are structural.
+ if (!mUseMipMaps || mDontDiscard || mBoostLevel >= LLGLTexture::BOOST_HIGH)
+ {
+ return false;
+ }
+ // Avatar bakes are exempt from mid-bake eviction (cloud avatar risk).
+ if (isAgentAvatarBoost(mBoostLevel))
+ {
+ return false;
+ }
+
if (!mDownScalePending)
{
mDownScalePending = true;
diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h
index 2937651995..991bb638a1 100644
--- a/indra/newview/llviewertexture.h
+++ b/indra/newview/llviewertexture.h
@@ -203,6 +203,25 @@ protected:
mutable S32 mMaxVirtualSizeResetInterval;
LLFrameTimer mLastReferencedTimer;
+ // 0=Normal, 1=BaseColor, 2=Specular, 3=Emissive. -1 -> base color.
+ S8 mPriorityChannel = -1;
+
+ // Bind-staleness floor, 0..1. Per-interval increment is 1/max_discard
+ // so any texture saturates after interval x max_discard seconds idle.
+ F32 mStalenessFactor = 0.f;
+
+ // Closest face's face_distance / draw_distance, clamped 0..1.
+ // Defaults to 1 so never-measured textures resolve to deepest discard.
+ F32 mMinDistanceFactor = 1.f;
+
+ // Largest per-face screen-space coverage in pixels. Raw - no bias or
+ // channel-priority contamination.
+ F32 mMaxOnScreenSize = 0.f;
+
+ // Any face on the agent's avatar (rigged / animated). Drives the
+ // own-avatar quality boost in processTextureStats.
+ bool mOnAgentAvatar = false;
+
ll_face_list_t mFaceList[LLRender::NUM_TEXTURE_CHANNELS]; //reverse pointer pointing to the faces using this image as texture
U32 mNumFaces[LLRender::NUM_TEXTURE_CHANNELS];
LLFrameTimer mLastFaceListUpdateTimer ;
@@ -224,16 +243,35 @@ public:
static S32 sAuxCount;
static LLFrameTimer sEvaluationTimer;
static F32 sDesiredDiscardBias;
+ // Backgrounded-window discard floor, 0..1. Ramps while backgrounded,
+ // snaps to 0 in foreground. Avatar bakes exempt.
+ static F32 sBackgroundFactor;
+
+ // VRAM-pressure distance multiplier, >= 1. Compresses the distance
+ // signal: dist_factor = clamp(mMinDistanceFactor * mult, 0, 1).
+ // Grows geometrically while over budget; decays back to 1 when fitting.
+ static F32 sMemoryPressureMultiplier;
+ // Last-ditch global discard floor. Mirrors sDesiredDiscardBias once the
+ // multiplier is exhausted.
+ static F32 sLastDitchMinDiscard;
+
+ // 0..1 progress of the pressure multiplier from baseline (1) to its
+ // configured cap (TextureMemoryPressureMaxMultiplier). Used to gate
+ // bubble shrink and last-ditch engagement.
+ static F32 getMemoryPressureProgress();
static U32 sBiasTexturesUpdated;
static S32 sMaxSculptRez ;
static U32 sMinLargeImageSize ;
static U32 sMaxSmallImageSize ;
- static bool sFreezeImageUpdates;
static F32 sCurrentTime ;
// estimated free memory for textures, by bias calculation
static F32 sFreeVRAMMegabytes;
+ // Viewport pixel area, refreshed once per frame. Hoisted to keep the
+ // per-texture hot path out of gViewerWindow.
+ static F32 sWindowPixelArea;
+
enum EDebugTexels
{
DEBUG_TEXELS_OFF,
@@ -279,6 +317,16 @@ public:
LLViewerFetchedTexture(const LLImageRaw* raw, FTType f_type, bool usemipmaps);
LLViewerFetchedTexture(const std::string& url, FTType f_type, const LLUUID& id, bool usemipmaps = true);
+ // Avatar bake/skin textures - exempt from non-visibility-driven discard
+ // (staleness, background, pressure) to avoid the universal-cloud bug.
+ static bool isAgentAvatarBoost(S32 boost_level)
+ {
+ return boost_level == BOOST_AVATAR
+ || boost_level == BOOST_AVATAR_BAKED
+ || boost_level == BOOST_AVATAR_SELF
+ || boost_level == BOOST_AVATAR_BAKED_SELF;
+ }
+
public:
struct Compare
@@ -339,7 +387,7 @@ public:
void updateVirtualSize() ;
- S32 getDesiredDiscardLevel() { return mDesiredDiscardLevel; }
+ S32 getDesiredDiscardLevel() const { return mDesiredDiscardLevel; }
void setMinDiscardLevel(S32 discard) { mMinDesiredDiscardLevel = llmin(mMinDesiredDiscardLevel,(S8)discard); }
void setBoostLevel(S32 level) override;
@@ -461,6 +509,12 @@ protected:
S8 mDesiredDiscardLevel; // The discard level we'd LIKE to have - if we have it and there's space
S8 mMinDesiredDiscardLevel; // The minimum discard level we'd like to have
+ // Fetch-side discard cap from the J2C codestream's DWT level count
+ // (populated by the fetcher). Distinct from LLImageGL::mMaxDiscardLevel -
+ // scaleDown can still trim the GL pyramid past this.
+ static constexpr S8 sFallbackCodecMaxDiscardLevel = 5; // MIN_DECOMPOSITION_LEVELS
+ S8 mCodecMaxDiscardLevel = sFallbackCodecMaxDiscardLevel;
+
bool mNeedsAux; // We need to decode the auxiliary channels
bool mHasAux; // We have aux channels
bool mDecodingAux; // Are we decoding high components
@@ -540,7 +594,6 @@ public:
S8 getType() const override;
// Process image stats to determine priority/quality requirements.
void processTextureStats() override;
- bool isUpdateFrozen() ;
bool scaleDown() override;
diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp
index 7dd32074cf..4d09eff74e 100644
--- a/indra/newview/llviewertexturelist.cpp
+++ b/indra/newview/llviewertexturelist.cpp
@@ -47,6 +47,7 @@
#include "message.h"
#include "lldrawpoolbump.h" // to init bumpmap images
+#include "llagentcamera.h"
#include "lltexturecache.h"
#include "lltexturefetch.h"
#include "llviewercontrol.h"
@@ -61,6 +62,8 @@
#include "lltracerecording.h"
#include "llviewerdisplay.h"
#include "llviewerwindow.h"
+#include "llsurface.h"
+#include "llvoavatarself.h"
#include "llprogressview.h"
////////////////////////////////////////////////////////////////////////////
@@ -68,6 +71,7 @@
void (*LLViewerTextureList::sUUIDCallback)(void **, const LLUUID&) = NULL;
S32 LLViewerTextureList::sNumImages = 0;
+F32 LLViewerTextureList::sCurrentBubbleMeters = 0.f;
LLViewerTextureList gTextureList;
@@ -93,6 +97,20 @@ LLTextureKey::LLTextureKey(LLUUID id, ETexListType tex_type)
///////////////////////////////////////////////////////////////////////////////
+// eTexIndex -> TextureChannel* index (0=Normal, 1=BaseColor, 2=Specular,
+// 3=Emissive). Single source of truth - route all channel-priority lookups
+// through this table.
+const S32 LLViewerTextureList::sChannelToPriority[LLRender::NUM_TEXTURE_CHANNELS] =
+{
+ 1, // DIFFUSE_MAP (0) -> Y (diffuse)
+ 0, // NORMAL_MAP / ALT_DIFFUSE (1) -> X (normals)
+ 2, // SPECULAR_MAP (2) -> Z (specular/metallic)
+ 1, // BASECOLOR_MAP (3) -> Y (diffuse)
+ 2, // METALLIC_ROUGHNESS_MAP (4) -> Z (specular/metallic)
+ 0, // GLTF_NORMAL_MAP (5) -> X (normals)
+ 3, // EMISSIVE_MAP (6) -> W (emissive)
+};
+
LLViewerTextureList::LLViewerTextureList()
: mForceResetTextureStats(false),
mInitialized(false)
@@ -899,8 +917,7 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
{
llassert(!gCubeSnapshot);
- constexpr F32 BIAS_TRS_OUT_OF_SCREEN = 1.5f;
- constexpr F32 BIAS_TRS_ON_SCREEN = 1.f;
+ constexpr F32 BIAS_TRS_ON_SCREEN = 1.f; // perf gate for face-loop early exit
if (imagep->getBoostLevel() < LLViewerFetchedTexture::BOOST_HIGH) // don't bother checking face list for boosted textures
{
@@ -910,9 +927,67 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
F32 max_vsize = 0.f;
bool on_screen = false;
+ // Accumulators for the per-texture signals published below.
+ // Defaults map to "deepest discard wanted" until evidence updates them.
+ F32 min_distance_factor = 1.f;
+ F32 max_on_screen_size = 0.f;
+ bool on_agent_avatar = false;
+ F32 draw_distance = llmax(gAgentCamera.mDrawDistance, 0.001f);
+
+ // Close-camera bubble: faces inside `bubble` meters resolve to
+ // dist_factor = 0, so the distance ramp spans (bubble, draw_distance].
+ static LLCachedControl<F32> close_bubble(gSavedSettings, "TextureCloseBubbleMeters", 5.f);
+ static LLCachedControl<F32> close_bubble_min(gSavedSettings, "TextureCloseBubbleMinMeters", 0.1f);
+ static LLCachedControl<F32> bubble_shrink_threshold(gSavedSettings, "TextureCloseBubbleShrinkThreshold", 0.8f);
+ static LLCachedControl<F32> bubble_track_rate(gSavedSettings, "TextureCloseBubbleTrackRate", 0.5f);
+ F32 bubble_full = llmax((F32)close_bubble, 0.f);
+ F32 bubble_min = llclamp((F32)close_bubble_min, 0.f, bubble_full);
+ // Advance the slow-track once per frame, not per texture: this
+ // function runs once per texture so a naive per-call lerp converges
+ // in a single frame.
+ static F32 s_tracked_bubble = -1.f;
+ static U32 s_tracked_bubble_frame = 0;
+ if (s_tracked_bubble < 0.f) s_tracked_bubble = bubble_full;
+ if (s_tracked_bubble_frame != LLFrameTimer::getFrameCount())
+ {
+ s_tracked_bubble_frame = LLFrameTimer::getFrameCount();
+ F32 progress = LLViewerTexture::getMemoryPressureProgress();
+ F32 shrink_thresh = llclampf((F32)bubble_shrink_threshold);
+ F32 shrink_frac = (progress > shrink_thresh)
+ ? (progress - shrink_thresh) / llmax(1.f - shrink_thresh, 0.0001f)
+ : 0.f;
+ F32 target_bubble = bubble_full - (bubble_full - bubble_min) * shrink_frac;
+ F32 dt = (F32)gFrameIntervalSeconds;
+ F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)bubble_track_rate, 0.f));
+ s_tracked_bubble += (target_bubble - s_tracked_bubble) * alpha;
+ s_tracked_bubble = llclamp(s_tracked_bubble, bubble_min, bubble_full);
+ sCurrentBubbleMeters = s_tracked_bubble;
+ }
+ F32 bubble = llclamp(s_tracked_bubble, 0.f, draw_distance - 0.001f);
+ F32 ramp_range = llmax(draw_distance - bubble, 0.001f);
+
U32 face_count = 0;
U32 max_faces_to_check = 1024;
+ // Pick the least-aggressive channel across all uses, so a texture
+ // used as both diffuse and normal isn't penalized by its harshest
+ // role. -1 sentinel keeps emissive-only textures (W=3) from being
+ // clobbered by a smaller init value.
+ S32 priority_channel = -1;
+ for (U32 i = 0; i < LLRender::NUM_TEXTURE_CHANNELS; ++i)
+ {
+ if (imagep->getNumFaces(i) > 0)
+ {
+ S32 mapped = sChannelToPriority[i];
+ priority_channel = (priority_channel < 0) ? mapped : llmin(priority_channel, mapped);
+ }
+ }
+ if (priority_channel < 0)
+ {
+ priority_channel = 1; // no faces - default to diffuse
+ }
+ imagep->mPriorityChannel = (S8)priority_channel;
+
// get adjusted bias based on image resolution
LLImageGL* img = imagep->getGLTexture();
F32 max_discard = F32(img ? img->getMaxDiscardLevel() : MAX_DISCARD_LEVEL);
@@ -948,6 +1023,15 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
on_screen |= face->mInFrustum;
+ F32 dist_above_bubble = llmax(face->mDistanceToCamera - bubble, 0.f);
+ F32 dist_factor = llclampf(dist_above_bubble / ramp_range);
+ min_distance_factor = llmin(min_distance_factor, dist_factor);
+
+ if (face->mAvatar && face->mAvatar == gAgentAvatarp)
+ {
+ on_agent_avatar = true;
+ }
+
// Scale desired texture resolution higher or lower depending on texture scale
//
// Minimum usage examples: a 1024x1024 texture with aplhabet (texture atlas),
@@ -963,6 +1047,10 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
min_scale = llclamp(min_scale * min_scale, texture_scale_min(), texture_scale_max());
vsize /= min_scale;
+ // Raw screen-space coverage - taken before the bias /
+ // camera-boost mutations below so the size signal is clean.
+ max_on_screen_size = llmax(max_on_screen_size, vsize);
+
// apply bias to offscreen faces all the time, but only to onscreen faces when bias is large
// use mImportanceToCamera to make bias switch a bit more gradual
if (!face->mInFrustum || LLViewerTexture::sDesiredDiscardBias > 1.9f + face->mImportanceToCamera / 2.f)
@@ -970,13 +1058,6 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
vsize /= bias;
}
- // boost resolution of textures that are important to the camera
- if (face->mInFrustum)
- {
- static LLCachedControl<F32> texture_camera_boost(gSavedSettings, "TextureCameraBoost", 8.f);
- vsize *= llmax(face->mImportanceToCamera*texture_camera_boost, 1.f);
- }
-
max_vsize = llmax(max_vsize, vsize);
// addTextureStats limits size to sMaxVirtualSize
@@ -995,57 +1076,95 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
}
}
- if (face_count > max_faces_to_check)
+ bool used_face_fast_path = (face_count > max_faces_to_check);
+ if (used_face_fast_path)
{ // this texture is used in so many places we should just boost it and not bother checking its vsize
// this is especially important because the above is not time sliced and can hit multiple ms for a single texture
max_vsize = MAX_IMAGE_AREA;
}
- if (imagep->getType() == LLViewerTexture::LOD_TEXTURE && imagep->getBoostLevel() == LLViewerTexture::BOOST_NONE)
- { // conditionally reset max virtual size for unboosted LOD_TEXTURES
- // this is an alternative to decaying mMaxVirtualSize over time
- // that keeps textures from continously downrezzing and uprezzing in the background
+ imagep->addTextureStats(max_vsize);
- if (LLViewerTexture::sDesiredDiscardBias > BIAS_TRS_OUT_OF_SCREEN ||
- (!on_screen && LLViewerTexture::sDesiredDiscardBias > BIAS_TRS_ON_SCREEN))
- {
- imagep->mMaxVirtualSize = 0.f;
- }
+ // Publish per-texture signals for processTextureStats. Closest face
+ // wins for distance (min); biggest face wins for size (max). Default
+ // distance=1, size=0 maps to "deepest discard wanted" - never-
+ // measured textures stay coarse until distance/size evidence arrives.
+ if (used_face_fast_path)
+ {
+ // Fast path saw only a prefix of faces - force best-quality
+ // sentinels to match the MAX_IMAGE_AREA vsize boost above.
+ imagep->mMinDistanceFactor = 0.f;
+ imagep->mMaxOnScreenSize = (F32)MAX_IMAGE_AREA;
}
+ else if (face_count == 0 && imagep->getBoostLevel() == LLGLTexture::BOOST_TERRAIN)
+ {
+ // Terrain detail textures don't register faces with the texture
+ // (LLVOSurfacePatch addFace(NULL)). Drive distance from the LOD
+ // system; floor at a small nonzero value so pressure has
+ // something to bite into (pow(0, p) = 0).
+ static LLCachedControl<F32> terrain_distance_floor(gSavedSettings, "TextureTerrainDistanceFloor", 0.01f);
+ static LLCachedControl<F32> terrain_coverage(gSavedSettings, "TextureTerrainCoverageFraction", 0.99f);
+ F32 nearest = LLSurface::sNearestVisiblePatchDistance;
+ F32 nearest_above_bubble = (nearest < FLT_MAX) ? llmax(nearest - bubble, 0.f) : ramp_range;
+ F32 dist = llclampf(nearest_above_bubble / ramp_range);
+ imagep->mMinDistanceFactor = llmax(dist, llclampf((F32)terrain_distance_floor));
+ imagep->mMaxOnScreenSize = LLViewerTexture::sWindowPixelArea * llclampf((F32)terrain_coverage);
+ }
+ else
+ {
+ imagep->mMinDistanceFactor = min_distance_factor;
+ imagep->mMaxOnScreenSize = max_on_screen_size;
+ }
+ imagep->mOnAgentAvatar = on_agent_avatar;
- imagep->addTextureStats(max_vsize);
-
- // Derive stream priority channel from face lists.
- // Map render texture channels to priority channels:
- // 0 = normal, 1 = diffuse, 2 = specular, 3 = emissive
+ // Bind-staleness. Avatar bakes exempt (cloud-bug protection).
+ // Per-interval increment is 1/max_discard so saturation time is
+ // interval * max_discard seconds regardless of texture size.
+ // Never-bound textures defer to distance/size or initial fetch
+ // could never start.
+ if (LLViewerFetchedTexture::isAgentAvatarBoost(imagep->getBoostLevel()))
{
- static const S32 render_to_priority[] = {
- 1, // DIFFUSE_MAP (0)
- 0, // NORMAL_MAP / ALTERNATE_DIFFUSE_MAP (1)
- 2, // SPECULAR_MAP (2)
- 1, // BASECOLOR_MAP (3)
- 2, // METALLIC_ROUGHNESS_MAP (4)
- 0, // GLTF_NORMAL_MAP (5)
- 3, // EMISSIVE_MAP (6)
- };
+ imagep->mStalenessFactor = 0.f;
+ }
+ else if (LLImageGL* gli = imagep->getGLTexture())
+ {
+ static LLCachedControl<F32> bind_decay_seconds(gSavedSettings, "TextureBindDecaySeconds", 5.f);
+ static LLCachedControl<F32> staleness_interval(gSavedSettings, "TextureStalenessIntervalSeconds", 5.f);
+ F32 grace = llmax((F32)bind_decay_seconds, 0.f);
+ F32 interval = llmax((F32)staleness_interval, 0.0001f);
+
+ // Clock starts at whichever is later: the last real bind or
+ // the GL-create time. The latter is the fallback for textures
+ // decoded into GL but never actually rendered - without it,
+ // mLastBindTime stays 0 forever and staleness can't evict.
+ F32 clock_time = llmax(gli->mLastBindTime, gli->mGLCreateTime);
+ bool has_clock = (clock_time > 0.f);
+ F32 time_since = has_clock ? (LLImageGL::sLastFrameTime - clock_time) : 0.f;
- S32 priority_channel = 1; // default to diffuse
- for (U32 i = 0; i < LLRender::NUM_TEXTURE_CHANNELS; ++i)
+ if (!has_clock || time_since <= grace)
{
- if (imagep->getNumFaces(i) > 0)
- {
- priority_channel = llmin(priority_channel, render_to_priority[i]);
- }
+ imagep->mStalenessFactor = 0.f;
}
-
- static LLCachedControl<LLVector4> channel_priority(gSavedSettings, "TextureChannelPriority",
- LLVector4(10.0f, 20.0f, 40.0f, 20.0f));
- F32 factor = llmax(channel_priority().mV[priority_channel], 0.1f);
- if (factor != 1.0f)
+ else
{
- imagep->mMaxVirtualSize /= factor;
+ S32 full_w = imagep->getFullWidth();
+ S32 full_h = imagep->getFullHeight();
+ S32 max_discard = (full_w > 0 && full_h > 0)
+ ? LLImageGL::dimDerivedMaxDiscard(full_w, full_h)
+ : (S32)gli->getMaxDiscardLevel();
+ if (max_discard > 0)
+ {
+ F32 steps = (time_since - grace) / interval;
+ F32 step_size = 1.f / (F32)max_discard;
+ imagep->mStalenessFactor = llclampf(steps * step_size);
+ }
+ else
+ {
+ imagep->mStalenessFactor = 0.f;
+ }
}
}
+
}
#if 0
@@ -1154,8 +1273,7 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time)
imagep->postCreateTexture();
imagep->mCreatePending = false;
- if (imagep->hasGLTexture() && imagep->getDiscardLevel() < imagep->getDesiredDiscardLevel() &&
- (imagep->getDesiredDiscardLevel() <= MAX_DISCARD_LEVEL))
+ if (imagep->hasGLTexture() && imagep->getDiscardLevel() < imagep->getDesiredDiscardLevel())
{
// NOTE: this may happen if the desired discard reduces while a decode is in progress and does not
// necessarily indicate a problem, but if log occurrences excede that of dsiplay_stats: FPS,
@@ -1180,13 +1298,16 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time)
gCopyProgram.bind();
gPipeline.mScreenTriangleVB->setBuffer();
- // give time to downscaling first -- if mDownScaleQueue is not empty, we're running out of memory and need
+ // give time to downscaling first - if mDownScaleQueue is not empty, we're running out of memory and need
// to free up memory by discarding off screen textures quickly
- // do at least 5 and make sure we don't get too far behind even if it violates
- // the time limit. If we don't downscale quickly the viewer will hit swap and may
- // freeze.
- S32 min_count = (S32)mCreateTextureList.size() / 20 + 5;
+ // Drain rate scales with both pending creates and the downscale
+ // queue itself. Without the queue term, a backlog of evictions
+ // could only drain 5/frame regardless of size, and the system
+ // can't actually free VRAM fast enough under pressure.
+ S32 min_count = (S32)mCreateTextureList.size() / 20
+ + (S32)mDownScaleQueue.size() / 5
+ + 5;
create_timer.reset();
while (!mDownScaleQueue.empty())
@@ -1288,12 +1409,15 @@ F32 LLViewerTextureList::updateImagesFetchTextures(F32 max_time)
//update MIN_UPDATE_COUNT or 5% of other textures, whichever is greater
update_count = llmax((U32) MIN_UPDATE_COUNT, (U32) mUUIDMap.size()/20);
- if (LLViewerTexture::sDesiredDiscardBias > 1.f
+ // Scale up the per-frame update window under VRAM pressure so eviction
+ // candidates get re-evaluated quickly. Both the legacy bias and the
+ // new pressure multiplier widen the window.
+ F32 pressure_scale = llmax(LLViewerTexture::sDesiredDiscardBias,
+ LLViewerTexture::sMemoryPressureMultiplier);
+ if (pressure_scale > 1.f
&& LLViewerTexture::sBiasTexturesUpdated < (U32)mUUIDMap.size())
{
- // We are over memory target. Bias affects discard rates, so update
- // existing textures agresively to free memory faster.
- update_count = (S32)(update_count * LLViewerTexture::sDesiredDiscardBias);
+ update_count = (S32)(update_count * pressure_scale);
// This isn't particularly precise and can overshoot, but it doesn't need
// to be, just making sure it did a full circle and doesn't get stuck updating
diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h
index 7c7112f4cf..dbed8b5c2f 100644
--- a/indra/newview/llviewertexturelist.h
+++ b/indra/newview/llviewertexturelist.h
@@ -30,6 +30,7 @@
#include "lluuid.h"
//#include "message.h"
#include "llgl.h"
+#include "llrender.h"
#include "llviewertexture.h"
#include "llui.h"
#include <list>
@@ -92,6 +93,10 @@ class LLViewerTextureList
friend class LLLocalBitmap;
public:
+ // eTexIndex -> TextureChannel* index (0=Normal, 1=BaseColor,
+ // 2=Specular, 3=Emissive). Single source of truth.
+ static const S32 sChannelToPriority[LLRender::NUM_TEXTURE_CHANNELS];
+
static bool createUploadFile(LLPointer<LLImageRaw> raw_image,
const std::string& out_filename,
const S32 max_image_dimentions = LLViewerFetchedTexture::MAX_IMAGE_SIZE_DEFAULT,
@@ -239,6 +244,10 @@ private:
bool mInitialized ;
LLFrameTimer mForceDecodeTimer;
+public:
+ // Current close-camera bubble in meters (frame-coherent, slow-tracked).
+ static F32 sCurrentBubbleMeters;
+
private:
static S32 sNumImages;
static void (*sUUIDCallback)(void**, const LLUUID &);
diff --git a/indra/newview/llvlcomposition.cpp b/indra/newview/llvlcomposition.cpp
index 3441e25c6a..eb0261b5e5 100644
--- a/indra/newview/llvlcomposition.cpp
+++ b/indra/newview/llvlcomposition.cpp
@@ -173,7 +173,10 @@ LLPointer<LLViewerFetchedTexture> fetch_terrain_texture(const LLUUID& id)
return nullptr;
}
- LLPointer<LLViewerFetchedTexture> tex = LLViewerTextureManager::getFetchedTexture(id);
+ // LOD_TEXTURE so streaming math runs (the base-class processTextureStats
+ // pins mDesiredDiscardLevel at 0).
+ LLPointer<LLViewerFetchedTexture> tex = LLViewerTextureManager::getFetchedTexture(
+ id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
return tex;
}
@@ -343,18 +346,9 @@ bool LLTerrainMaterials::makeTextureReady(LLPointer<LLViewerFetchedTexture>& tex
{
if (boost)
{
+ // Quality is driven by the streaming math via synthetic signals
+ // for BOOST_TERRAIN textures in updateImageDecodePriority.
boost_minimap_texture(tex, BASE_SIZE*BASE_SIZE);
-
- S32 width = tex->getFullWidth();
- S32 height = tex->getFullHeight();
- S32 min_dim = llmin(width, height);
- S32 ddiscard = 0;
- while (min_dim > BASE_SIZE && ddiscard < MAX_DISCARD_LEVEL)
- {
- ddiscard++;
- min_dim /= 2;
- }
- tex->setMinDiscardLevel(ddiscard);
}
return false;
}
diff --git a/indra/newview/skins/default/xui/en/floater_preferences_graphics_advanced.xml b/indra/newview/skins/default/xui/en/floater_preferences_graphics_advanced.xml
index 99dca7b395..3043b17589 100644
--- a/indra/newview/skins/default/xui/en/floater_preferences_graphics_advanced.xml
+++ b/indra/newview/skins/default/xui/en/floater_preferences_graphics_advanced.xml
@@ -127,31 +127,35 @@
top_delta="16"
left="30"
width="160"
- name="MaxTextureResolutionLabel"
+ name="TextureQualityLabel"
text_readonly_color="LabelDisabledColor">
- Maximum LOD resolution:
+ Texture quality:
</text>
<combo_box
- control_name="RenderMaxTextureResolution"
+ control_name="RenderTextureQuality"
height="19"
layout="topleft"
left_pad="10"
top_delta="0"
- name="MaxTextureResolution"
- tool_tip="Maximum resolution for 'level of detail' textures"
+ name="TextureQuality"
+ tool_tip="Controls maximum texture resolution and how aggressively textures stream by channel. Higher quality keeps more detail in memory."
width="90">
<combo_box.item
- label="512"
- name="512"
- value="512"/>
+ label="Low"
+ name="Low"
+ value="0"/>
+ <combo_box.item
+ label="Medium"
+ name="Medium"
+ value="1"/>
<combo_box.item
- label="1024"
- name="1024"
- value="1024"/>
+ label="High"
+ name="High"
+ value="2"/>
<combo_box.item
- label="2048"
- name="2048"
- value="2048"/>
+ label="Ultra"
+ name="Ultra"
+ value="3"/>
</combo_box>
<check_box