summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan "Geenz" Goodman <geenz@geenzo.com>2026-07-22 00:38:54 -0400
committerJonathan "Geenz" Goodman <geenz@geenzo.com>2026-07-22 00:38:54 -0400
commit6b9f0f0bf43250f984631032a3f3b41d245907a9 (patch)
tree6cede3cddc362ff84cb46e16057c262919da28d9
parent91c11c8d87c963f2b17e45b525396bf8c8386c26 (diff)
Revert "Merge pull request #5982 from secondlife/geenz/texture-streaming-tweaks"
This reverts commit 3d465f6c966f0a2fcfd5d3c9effb6ce20c2754c8, reversing changes made to f2c356fc7130ce5e38b994dcfa1e210609b458b9.
-rw-r--r--indra/llprimitive/lltextureentry.cpp2
-rw-r--r--indra/llrender/llimagegl.cpp64
-rw-r--r--indra/llrender/llimagegl.h33
-rw-r--r--indra/llrender/llrender.cpp7
-rw-r--r--indra/newview/app_settings/settings.xml300
-rw-r--r--indra/newview/featuretable.txt1
-rw-r--r--indra/newview/featuretable_linux.txt2
-rw-r--r--indra/newview/featuretable_mac.txt1
-rw-r--r--indra/newview/llfetchedgltfmaterial.cpp29
-rw-r--r--indra/newview/lltextureview.cpp13
-rw-r--r--indra/newview/llviewercontrol.cpp88
-rw-r--r--indra/newview/llviewermessage.cpp7
-rw-r--r--indra/newview/llviewerobject.cpp18
-rw-r--r--indra/newview/llviewershadermgr.cpp6
-rw-r--r--indra/newview/llviewertexture.cpp763
-rw-r--r--indra/newview/llviewertexture.h98
-rw-r--r--indra/newview/llviewertexturelist.cpp489
-rw-r--r--indra/newview/llviewertexturelist.h4
-rw-r--r--indra/newview/llviewerwindow.cpp8
-rw-r--r--indra/newview/llvovolume.cpp63
-rw-r--r--indra/newview/pipeline.cpp6
21 files changed, 1154 insertions, 848 deletions
diff --git a/indra/llprimitive/lltextureentry.cpp b/indra/llprimitive/lltextureentry.cpp
index 2b0f989701..ac482ffbf9 100644
--- a/indra/llprimitive/lltextureentry.cpp
+++ b/indra/llprimitive/lltextureentry.cpp
@@ -600,7 +600,7 @@ LLGLTFMaterial* LLTextureEntry::getGLTFRenderMaterial() const
return mGLTFRenderMaterial;
}
- //llassert(getGLTFMaterialOverride() == nullptr || getGLTFMaterialOverride()->isClearedForBaseMaterial());
+ llassert(getGLTFMaterialOverride() == nullptr || getGLTFMaterialOverride()->isClearedForBaseMaterial());
return getGLTFMaterial();
}
diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp
index c8a23d873e..6bcc34938c 100644
--- a/indra/llrender/llimagegl.cpp
+++ b/indra/llrender/llimagegl.cpp
@@ -166,8 +166,6 @@ U64 LLImageGL::getTextureBytesAllocated()
//statics
U32 LLImageGL::sUniqueCount = 0;
-std::atomic<U32> LLImageGL::sOOMErrorCount(0);
-thread_local bool LLImageGL::sStampBindFrame = true;
U32 LLImageGL::sBindCount = 0;
S32 LLImageGL::sCount = 0;
@@ -493,7 +491,7 @@ bool LLImageGL::create(LLPointer<LLImageGL>& dest, const LLImageRaw* imageraw, b
//----------------------------------------------------------------------------
LLImageGL::LLImageGL(bool usemipmaps/* = true*/, bool allow_compression/* = true*/)
-: mExternalTexture(false)
+: mSaveData(0), mExternalTexture(false)
{
init(usemipmaps, allow_compression);
setSize(0, 0, 0);
@@ -502,7 +500,7 @@ LLImageGL::LLImageGL(bool usemipmaps/* = true*/, bool allow_compression/* = true
}
LLImageGL::LLImageGL(U32 width, U32 height, U8 components, bool usemipmaps/* = true*/, bool allow_compression/* = true*/)
-: mExternalTexture(false)
+: mSaveData(0), mExternalTexture(false)
{
llassert( components <= 4 );
init(usemipmaps, allow_compression);
@@ -512,7 +510,7 @@ LLImageGL::LLImageGL(U32 width, U32 height, U8 components, bool usemipmaps/* = t
}
LLImageGL::LLImageGL(const LLImageRaw* imageraw, bool usemipmaps/* = true*/, bool allow_compression/* = true*/)
-: mExternalTexture(false)
+: mSaveData(0), mExternalTexture(false)
{
init(usemipmaps, allow_compression);
setSize(0, 0, 0);
@@ -621,6 +619,8 @@ void LLImageGL::cleanup()
destroyGLTexture();
}
freePickMask();
+
+ mSaveData = NULL; // deletes data
}
//----------------------------------------------------------------------------
@@ -777,7 +777,6 @@ void LLImageGL::setImage(const LLImageRaw* imageraw)
bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 usename /* = 0 */)
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
- LLImageGLStampBypass no_stamp; // upload binds are not visibility
const bool is_compressed = isCompressed();
@@ -1503,12 +1502,6 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt
{
free_cur_tex_image();
}
-
- // Drain stale GL errors so an OOM detected below belongs to this alloc.
- // Otherwise a failed glTexImage2D is swallowed in release while
- // alloc_tex_image still counts the bytes, inflating the used-VRAM figure.
- while (glGetError() != GL_NO_ERROR) {}
-
const bool use_sub_image = should_stagger_image_set(compress);
if (!use_sub_image)
{
@@ -1518,30 +1511,19 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt
else
{
// break up calls to a manageable size for the GL command buffer
- LL_PROFILE_ZONE_NAMED("glTexImage2D alloc");
- glTexImage2D(target, miplevel, intformat, width, height, 0, pixformat, pixtype, nullptr);
- }
+ {
+ LL_PROFILE_ZONE_NAMED("glTexImage2D alloc");
+ glTexImage2D(target, miplevel, intformat, width, height, 0, pixformat, pixtype, nullptr);
+ }
- if (glGetError() == GL_OUT_OF_MEMORY)
- {
- ++sOOMErrorCount;
- LL_WARNS_ONCE("Texture") << "glTexImage2D failed with GL_OUT_OF_MEMORY ("
- << width << "x" << height << " mip " << miplevel
- << ") - not counting bytes" << LL_ENDL;
- }
- else
- {
- if (use_sub_image)
+ U8* src = (U8*)(pixels);
+ if (src)
{
- U8* src = (U8*)(pixels);
- if (src)
- {
- LL_PROFILE_ZONE_NAMED("glTexImage2D copy");
- sub_image_lines(target, miplevel, 0, 0, width, height, pixformat, pixtype, src, width);
- }
+ LL_PROFILE_ZONE_NAMED("glTexImage2D copy");
+ sub_image_lines(target, miplevel, 0, 0, width, height, pixformat, pixtype, src, width);
}
- alloc_tex_image(width, height, intformat, 1);
}
+ alloc_tex_image(width, height, intformat, 1);
}
stop_glerror();
}
@@ -1686,7 +1668,6 @@ bool LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, bool data_
LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
LL_PROFILE_GPU_ZONE("createGLTexture");
checkActiveThread();
- LLImageGLStampBypass no_stamp; // creation binds are not visibility
bool main_thread = on_main_thread();
@@ -2109,21 +2090,12 @@ S32 LLImageGL::dimDerivedMaxDiscard(S32 width, S32 height)
void LLImageGL::stampBound() const
{
- // Both stamps skip same-frame re-binds (bindFast runs per draw). They dedupe
- // separately, so a non-camera pass touching the time stamp first doesn't stop
- // a real camera bind from setting the frame stamp later the same frame.
+ // 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;
}
- if (sStampBindFrame)
- {
- const U32 frame = LLFrameTimer::getFrameCount();
- if (mLastBindFrame != frame)
- {
- mLastBindFrame = frame;
- }
- }
}
S64 LLImageGL::getBytes(S32 discard_level) const
@@ -2548,10 +2520,6 @@ bool LLImageGL::scaleDown(S32 desired_discard)
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
- // Don't let eviction re-arm visibility: the glGenerateMipmap re-bind below
- // would otherwise stamp mLastBindFrame and keep the texture fetch-eligible.
- LLImageGLStampBypass no_stamp;
-
if (mTarget != GL_TEXTURE_2D
|| mFormatInternal == -1 // not initialized
)
diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h
index ca75b543c0..0c85446b84 100644
--- a/indra/llrender/llimagegl.h
+++ b/indra/llrender/llimagegl.h
@@ -39,7 +39,6 @@
#include "llrender.h"
#include "threadpool.h"
#include "workqueue.h"
-#include <atomic>
#include <unordered_set>
#define LL_IMAGEGL_THREAD_CHECK 0 //set to 1 to enable thread debugging for ImageGL
@@ -239,23 +238,15 @@ public:
public:
// Various GL/Rendering options
S64Bytes mTextureMemory;
- mutable F32 mLastBindTime = 0.f; // wall-clock time at last stampBound (bind or bind-attempt)
- mutable U32 mLastBindFrame = 0; // frame index (LLFrameTimer::getFrameCount) at last CAMERA-pass
- // stampBound; 0 = never. Drives visibility GC + fetch gating.
- F32 mGLCreateTime = 0.f; // wall-clock time the GL texture was created
-
- // When false, stampBound skips the mLastBindFrame stamp (mLastBindTime still
- // updates). Set false around non-camera passes (probes, shadows, impostors)
- // and administrative binds (upload, scaleDown - via LLImageGLStampBypass) so
- // those binds don't count as camera visibility. Thread-local so a GL upload
- // thread can't flip it on the render thread mid-frame.
- static thread_local bool sStampBindFrame;
+ 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);
void freePickMask();
bool isCompressed();
+ LLPointer<LLImageRaw> mSaveData; // used for destroyGL/restoreGL
LL::WorkQueue::weak_t mMainQueue;
U8* mPickMask; //downsampled bitmap approximation of alpha channel. NULL if no alpha channel
U16 mPickMaskWidth;
@@ -309,11 +300,6 @@ public:
// Global memory statistics
static U32 sBindCount; // Tracks number of texture binds for current frame
static U32 sUniqueCount; // Tracks number of unique texture binds for current frame
- // glTexImage2D GL_OUT_OF_MEMORY failures detected (bytes NOT counted for
- // these). Written from whichever thread runs texture creation; read by
- // the streaming 1Hz pressure log. Nonzero = the driver is refusing
- // allocations and the VRAM budget is unreliable.
- static std::atomic<U32> sOOMErrorCount;
static bool sGlobalUseAnisotropic;
static LLImageGL* sDefaultGLTexture ;
static bool sAutomatedTest;
@@ -369,19 +355,6 @@ public:
};
-// RAII: suppress the mLastBindFrame stamp for the current scope. Use around
-// administrative binds (upload, create, scaleDown) so they don't count as
-// camera visibility - otherwise the GC's own scaleDown re-stamps what it just
-// aged out and oscillates. Saves/restores, so it nests correctly.
-class LLImageGLStampBypass
-{
-public:
- LLImageGLStampBypass() : mPrev(LLImageGL::sStampBindFrame) { LLImageGL::sStampBindFrame = false; }
- ~LLImageGLStampBypass() { LLImageGL::sStampBindFrame = mPrev; }
-private:
- bool mPrev;
-};
-
class LLImageGLThread : public LLSimpleton<LLImageGLThread>, LL::ThreadPool
{
public:
diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp
index f0a1c44507..5e845fbcce 100644
--- a/indra/llrender/llrender.cpp
+++ b/indra/llrender/llrender.cpp
@@ -245,11 +245,6 @@ bool LLTexUnit::bind(LLTexture* texture, bool for_rendering, bool forceBind)
texture->setActive() ;
texture->updateBindStatsForTester() ;
}
- // updateBindStats only stamps time; the GC and fetch gate use
- // the frame stamp, so stamp it here too or bind()-drawn faces
- // (bump/material/media) oscillate. Admin/non-camera binds are
- // already suppressed via LLImageGLStampBypass / sStampBindFrame.
- gl_tex->stampBound();
mHasMipMaps = gl_tex->mHasMipMaps;
if (gl_tex->mTexOptionsDirty)
{
@@ -330,8 +325,6 @@ bool LLTexUnit::bind(LLImageGL* texture, bool for_rendering, bool forceBind, S32
glBindTexture(sGLTextureType[texture->getTarget()], mCurrTexture);
stop_glerror();
texture->updateBindStats();
- // Frame-stamp fresh binds too - see bind(LLTexture*) above.
- texture->stampBound();
mHasMipMaps = texture->mHasMipMaps;
if (texture->mTexOptionsDirty)
{
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 45a7dced5c..81299a5b71 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -8002,6 +8002,17 @@
<key>Value</key>
<integer>1</integer>
</map>
+ <key>RenderMinFreeMainMemoryThreshold</key>
+ <map>
+ <key>Comment</key>
+ <string>If available free physical memory is below this value textures get agresively scaled down</string>
+ <key>Persist</key>
+ <integer>0</integer>
+ <key>Type</key>
+ <string>U32</string>
+ <key>Value</key>
+ <integer>512</integer>
+ </map>
<key>RenderLowMemMinDiscardIncrement</key>
<map>
<key>Comment</key>
@@ -8024,6 +8035,17 @@
<key>Value</key>
<real>0.1</real>
</map>
+ <key>RenderMaxTextureIndex</key>
+ <map>
+ <key>Comment</key>
+ <string>Maximum texture index to use for indexed texture rendering.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>U32</string>
+ <key>Value</key>
+ <integer>16</integer>
+ </map>
<key>RenderMaxTextureResolution</key>
<map>
<key>Comment</key>
@@ -8038,7 +8060,7 @@
<key>RenderTextureQuality</key>
<map>
<key>Comment</key>
- <string>Texture quality preset: 0=Low, 1=Medium, 2=High, 3=Ultra. Drives RenderMaxTextureResolution, TexturePixelToTexelRatio, TexturePressureTightenRate, TexturePressureRelaxRate, and TextureChannelRatio* (Normal/BaseColor/Specular/Emissive). The pressure water marks (TexturePressureHighWater/LowWater) are constant across tiers.</string>
+ <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>
@@ -8057,6 +8079,17 @@
<key>Value</key>
<integer>0</integer>
</map>
+ <key>RenderDebugTextureBind</key>
+ <map>
+ <key>Comment</key>
+ <string>Enable texture bind performance test.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>Boolean</string>
+ <key>Value</key>
+ <integer>0</integer>
+ </map>
<key>RenderDelayCreation</key>
<map>
<key>Comment</key>
@@ -9373,6 +9406,17 @@
<key>Value</key>
<integer>64</integer>
</map>
+ <key>RenderReservedTextureIndices</key>
+ <map>
+ <key>Comment</key>
+ <string>Count of texture indices to reserve for shadow and reflection maps when using indexed texture rendering. Probably only want to set from the login screen.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>S32</string>
+ <key>Value</key>
+ <integer>14</integer>
+ </map>
<key>RenderResolutionDivisor</key>
<map>
<key>Comment</key>
@@ -11827,10 +11871,10 @@
<key>Value</key>
<real>20.0</real>
</map>
- <key>TextureChannelRatioNormal</key>
+ <key>TextureChannelNormal</key>
<map>
<key>Comment</key>
- <string>Per-channel pixel:texel ratio multiplier for normal maps (texels per pixel, applied on top of the global TexturePixelToTexelRatio). 1.0 = full quality; lower = coarser. Driven by the RenderTextureQuality preset.</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>
@@ -11838,21 +11882,21 @@
<key>Value</key>
<real>1.0</real>
</map>
- <key>TextureChannelRatioBaseColor</key>
+ <key>TextureChannelBaseColor</key>
<map>
<key>Comment</key>
- <string>Per-channel pixel:texel ratio multiplier for base color / diffuse (texels per pixel, applied on top of the global TexturePixelToTexelRatio). 1.0 = full quality. Driven by the RenderTextureQuality preset.</string>
+ <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>1.0</real>
+ <real>0.75</real>
</map>
- <key>TextureChannelRatioSpecular</key>
+ <key>TextureChannelSpecular</key>
<map>
<key>Comment</key>
- <string>Per-channel pixel:texel ratio multiplier for specular / metallic-roughness (texels per pixel, applied on top of the global TexturePixelToTexelRatio). 0.5 = half resolution (specular detail is usually less perceptible than diffuse). Driven by the RenderTextureQuality preset.</string>
+ <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>
@@ -11860,16 +11904,16 @@
<key>Value</key>
<real>0.5</real>
</map>
- <key>TextureChannelRatioEmissive</key>
+ <key>TextureChannelEmissive</key>
<map>
<key>Comment</key>
- <string>Per-channel pixel:texel ratio multiplier for emissive (texels per pixel, applied on top of the global TexturePixelToTexelRatio). 0.5 = half resolution. Driven by the RenderTextureQuality preset.</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.5</real>
+ <real>0.75</real>
</map>
<key>TextureMaxDiscardOverride</key>
<map>
@@ -11882,87 +11926,165 @@
<key>Value</key>
<integer>0</integer>
</map>
- <key>TexturePixelToTexelRatio</key>
+ <key>TextureMemoryHighWaterMark</key>
<map>
<key>Comment</key>
- <string>Max texels per screen pixel the streamer will allocate (the "R" in a 1:R pixel:texel ratio). 1.0 = one texel per pixel. VRAM pressure walks the effective ratio down from here.</string>
+ <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>1.0</real>
+ <real>0.8</real>
</map>
- <key>TextureBackgroundMinRatio</key>
+ <key>TextureMemoryPressureBackoffStart</key>
<map>
<key>Comment</key>
- <string>Lowest pixel:texel ratio the streamer decays to while the viewer is backgrounded, to free VRAM for other apps. Lower frees more but re-rezzes slower on return. Restored when focus comes back.</string>
+ <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.001</real>
+ <real>0.85</real>
</map>
- <key>TexturePressureHighWater</key>
+ <key>TextureMemoryPressureMaxMultiplier</key>
<map>
<key>Comment</key>
- <string>High water mark as a fraction of the VRAM budget. When used VRAM crosses this, the global pixel:texel ratio tightens (backs off detail) at TexturePressureTightenRate. Held at 0.90 - it's a physical "crossed the budget" threshold, not a tier preference.</string>
+ <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>0.90</real>
+ <real>64.0</real>
</map>
- <key>TexturePressureLowWater</key>
+ <key>TextureLastDitchEngageProgress</key>
<map>
<key>Comment</key>
- <string>Low water mark as a fraction of the VRAM budget. When used VRAM drops below this, the global pixel:texel ratio relaxes (restores detail) at TexturePressureRelaxRate. The band between low and high is the hysteresis zone where the ratio holds steady - wide enough to prevent sawtooth.</string>
+ <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.70</real>
+ <real>0.95</real>
</map>
- <key>TexturePressureTightenRate</key>
+ <key>TextureLastDitchRampRate</key>
<map>
<key>Comment</key>
- <string>Rate (ratio units/sec) at which the global pixel:texel ratio drops while used VRAM is above the high watermark. Deliberately slow so eviction (scaleDown draining) frees bytes before the next step - prevents over-shoot / thrash.</string>
+ <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.30</real>
+ <real>0.5</real>
</map>
- <key>TexturePressureRelaxRate</key>
+ <key>TextureLastDitchDecayRate</key>
<map>
<key>Comment</key>
- <string>Rate (ratio units/sec) at which the global pixel:texel ratio climbs back toward TexturePixelToTexelRatio while used VRAM is below the low watermark. Typically slower than the tighten rate so detail returns gradually as headroom appears.</string>
+ <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.10</real>
+ <real>0.5</real>
</map>
- <key>TextureUpRezMargin</key>
+ <key>TextureLastDitchMinDiscardMax</key>
<map>
<key>Comment</key>
- <string>Hysteresis dead-band (in mip levels) around a texture's current discard. A texture only fetches a finer mip when its ideal discard falls more than this below the current level, and only evicts when it rises more than this above - prevents fetch/scaleDown thrash at mip boundaries when an object slowly recedes.</string>
+ <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>0.2</real>
+ <real>13.0</real>
</map>
- <key>TextureCooldownStepSeconds</key>
+ <key>TextureMemoryPressurePredictionGain</key>
<map>
<key>Comment</key>
- <string>While backgrounded, the pixel:texel ratio drops one mip toward TextureBackgroundMinRatio every this many seconds.</string>
+ <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>
@@ -11970,39 +12092,95 @@
<key>Value</key>
<real>5.0</real>
</map>
- <key>TextureGCStepFrames</key>
+ <key>TextureCloseBubbleMinMeters</key>
<map>
<key>Comment</key>
- <string>Foreground GC cooldown, in rendered frames. For every this many frames a texture goes without being drawn, its mip drops by TextureGCStepMips. Resets when the texture is drawn again.</string>
+ <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>U32</string>
+ <string>F32</string>
<key>Value</key>
- <integer>5</integer>
+ <real>3.0</real>
</map>
- <key>TextureGCStepMips</key>
+ <key>TextureCloseBubbleShrinkThreshold</key>
<map>
<key>Comment</key>
- <string>Mip levels dropped each time a TextureGCStepFrames cooldown elapses. 1 is gentlest; higher sheds VRAM faster in coarser jumps.</string>
+ <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>U32</string>
+ <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>TextureFetchVisibilityFrames</key>
+ <key>TextureDistanceDiscardPower</key>
<map>
<key>Comment</key>
- <string>Only fetch a texture if it was drawn within this many rendered frames; out-of-view content isn't fetched. Minimum 1 (0 is clamped up). Boosted/UI textures, avatar bakes, and callback textures are exempt.</string>
+ <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>U32</string>
+ <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>
@@ -12036,6 +12214,28 @@
<key>Value</key>
<integer>0</integer>
</map>
+ <key>TextureDiscardBackgroundedTime</key>
+ <map>
+ <key>Comment</key>
+ <string>Specify how long to wait before discarding texture data after viewer is backgrounded. (zero or negative to disable)</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>60.0</real>
+ </map>
+ <key>TextureDiscardMinimizedTime</key>
+ <map>
+ <key>Comment</key>
+ <string>Specify how long to wait before discarding texture data after viewer is minimized. (zero or negative to disable)</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>1.0</real>
+ </map>
<key>TextureFetchConcurrency</key>
<map>
<key>Comment</key>
@@ -12124,27 +12324,27 @@
<key>Value</key>
<string />
</map>
- <key>TextureAvatarBoost</key>
+ <key>TextureScaleMinAreaFactor</key>
<map>
<key>Comment</key>
- <string>Coverage multiplier for avatar textures: worn attachments (rigged extents make their measurement unreliable) and baked system-avatar textures. 4.0 = one mip finer than measured. A bonus, not a pin - nearby avatars gain headroom while distant avatars still downrez with their measured coverage. 1.0 disables.</string>
+ <string>Limits how texture scale affects area calculation.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>4.0</real>
+ <real>0.0095</real>
</map>
- <key>TextureDownrezCoverageBias</key>
+ <key>TextureScaleMaxAreaFactor</key>
<map>
<key>Comment</key>
- <string>Which end of a texture's texels-per-pixel spread sizes it. Each texture tracks the screen coverage of its most demanding use (lowest texels per pixel) and least demanding use (highest texels per pixel, most oversampled). 0 = size to the most demanding use (best quality); 1 = size to the least demanding use (frees the most memory). Interpolation is geometric (log-space), so the resulting discard level moves linearly with this value - 0.5 sits halfway between the two ends in mip levels. Default 0.25 is the best universal balance.</string>
+ <string>Limits how texture scale affects area calculation.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>0.25</real>
+ <real>25.0</real>
</map>
<key>ThreadPoolSizes</key>
<map>
diff --git a/indra/newview/featuretable.txt b/indra/newview/featuretable.txt
index 72e0f43c0e..f05f77c222 100644
--- a/indra/newview/featuretable.txt
+++ b/indra/newview/featuretable.txt
@@ -68,6 +68,7 @@ RenderShadowDetail 1 2
RenderUseStreamVBO 1 1
RenderFSAAType 1 2
RenderFSAASamples 1 3
+RenderMaxTextureIndex 1 16
RenderGLContextCoreProfile 1 1
RenderGLMultiThreadedTextures 1 0
RenderGLMultiThreadedMedia 1 1
diff --git a/indra/newview/featuretable_linux.txt b/indra/newview/featuretable_linux.txt
index a39c64510f..d8d4f08429 100644
--- a/indra/newview/featuretable_linux.txt
+++ b/indra/newview/featuretable_linux.txt
@@ -66,6 +66,7 @@ RenderDeferredSSAO 1 1
RenderUseAdvancedAtmospherics 1 0
RenderShadowDetail 1 2
RenderFSAASamples 1 16
+RenderMaxTextureIndex 1 16
RenderMirrors 1 1
//
@@ -490,6 +491,7 @@ RenderVBOEnable 1 0
list OpenGLPre30
RenderDeferred 0 0
+RenderMaxTextureIndex 1 1
list Intel
RenderAnisotropic 1 0
diff --git a/indra/newview/featuretable_mac.txt b/indra/newview/featuretable_mac.txt
index ebfe45d9ea..b11fa28c48 100644
--- a/indra/newview/featuretable_mac.txt
+++ b/indra/newview/featuretable_mac.txt
@@ -65,6 +65,7 @@ RenderShadowDetail 1 2
RenderUseStreamVBO 1 1
RenderFSAAType 1 2
RenderFSAASamples 1 3
+RenderMaxTextureIndex 1 16
RenderGLContextCoreProfile 1 1
RenderGLMultiThreadedTextures 1 1
RenderGLMultiThreadedMedia 1 1
diff --git a/indra/newview/llfetchedgltfmaterial.cpp b/indra/newview/llfetchedgltfmaterial.cpp
index d71f0c1bd4..a05f725673 100644
--- a/indra/newview/llfetchedgltfmaterial.cpp
+++ b/indra/newview/llfetchedgltfmaterial.cpp
@@ -73,20 +73,6 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex)
LLViewerTexture* baseColorTex = media_tex ? media_tex : mBaseColorTexture;
LLViewerTexture* emissiveTex = media_tex ? media_tex : mEmissiveTexture;
- if (media_tex)
- {
- // Media hides these but they stay registered for coverage. Stamp them so
- // the GC doesn't coarsen/refetch them while the media is playing.
- if (mBaseColorTexture.notNull())
- {
- if (LLImageGL* gl_tex = mBaseColorTexture->getGLTexture()) { gl_tex->stampBound(); }
- }
- if (mEmissiveTexture.notNull())
- {
- if (LLImageGL* gl_tex = mEmissiveTexture->getGLTexture()) { gl_tex->stampBound(); }
- }
- }
-
if (!LLPipeline::sShadowRender || (mAlphaMode == LLGLTFMaterial::ALPHA_MODE_MASK))
{
if (mAlphaMode == LLGLTFMaterial::ALPHA_MODE_MASK)
@@ -111,26 +97,13 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex)
if (!LLPipeline::sShadowRender)
{
- // Bind the normal map at whatever resolution is resident, like Blinn-Phong
- // (soft, never absent). Only fall back to the flat normal when it's not
- // loaded yet. (The old discard<=4 gate dropped distant/tiled PBR normals
- // entirely, making PBR look flatter than equivalent Blinn content.)
- if (mNormalTexture.notNull() && mNormalTexture->hasGLTexture())
+ if (mNormalTexture.notNull() && mNormalTexture->getDiscardLevel() <= 4)
{
shader->bindTexture(LLShaderMgr::BUMP_MAP, mNormalTexture);
}
else
{
shader->bindTexture(LLShaderMgr::BUMP_MAP, LLViewerFetchedTexture::sFlatNormalImagep);
- if (mNormalTexture.notNull())
- {
- // In use, just not loaded yet - stamp it so the GC doesn't treat
- // it as unseen and pin it deep before its first real bind.
- if (LLImageGL* gl_tex = mNormalTexture->getGLTexture())
- {
- gl_tex->stampBound();
- }
- }
}
if (mMetallicRoughnessTexture.notNull())
diff --git a/indra/newview/lltextureview.cpp b/indra/newview/lltextureview.cpp
index 10b57f47da..470a133fa7 100644
--- a/indra/newview/lltextureview.cpp
+++ b/indra/newview/lltextureview.cpp
@@ -476,7 +476,7 @@ private:
void LLGLTexMemBar::draw()
{
- F32 pixel_to_texel_ratio = LLViewerTexture::sPixelToTexelRatio;
+ F32 discard_bias = LLViewerTexture::sDesiredDiscardBias;
F32 cache_usage = (F32)LLAppViewer::getTextureCache()->getUsage().valueInUnits<LLUnits::Megabytes>();
F32 cache_max_usage = (F32)LLAppViewer::getTextureCache()->getMaxUsage().valueInUnits<LLUnits::Megabytes>();
S32 line_height = LLFontGL::getFontMonospace()->getLineHeight();
@@ -560,22 +560,25 @@ void LLGLTexMemBar::draw()
gGL.color4f(0.f, 0.f, 0.f, 0.25f);
gl_rect_2d(-10, getRect().getHeight() + line_height*2 + 1, getRect().getWidth()+2, getRect().getHeight()+2);
- text = llformat("Est. Free: %d MB Sys Free: %d MB FBO: %d MB Probe#: %d Probe Mem: %d MB Px:Texel 1:%.2f Cache: %.1f/%.1f MB",
+ text = llformat("Est. Free: %d MB Sys Free: %d MB FBO: %d MB Probe#: %d Probe Mem: %d MB Bias: %.2f Cache: %.1f/%.1f MB",
(S32)LLViewerTexture::sFreeVRAMMegabytes,
LLMemory::getAvailableMemKB()/1024,
LLRenderTarget::sBytesAllocated/(1024*1024),
gPipeline.mReflectionMapManager.probeCount(),
gPipeline.mReflectionMapManager.probeMemory(),
- pixel_to_texel_ratio,
+ discard_bias,
cache_usage,
cache_max_usage);
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)",
+ 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 dd2357ae8f..a4a7f1fd20 100644
--- a/indra/newview/llviewercontrol.cpp
+++ b/indra/newview/llviewercontrol.cpp
@@ -112,60 +112,45 @@ static bool handleRenderAvatarMouselookChanged(const LLSD& newvalue)
return true;
}
-// Per-tier texture quality preset. Data-driven so adding a setting is
-// "add a column", and the tier values are visible side-by-side. Index is
-// RenderTextureQuality: 0=Low, 1=Medium, 2=High, 3=Ultra.
-namespace
-{
- struct TexturePreset
- {
- const char* name;
- U32 max_resolution;
- F32 pixel_to_texel_ratio; // TexturePixelToTexelRatio (R_max, texels per pixel)
- F32 pressure_tighten_rate; // TexturePressureTightenRate (ratio units/sec)
- F32 pressure_relax_rate; // TexturePressureRelaxRate (ratio units/sec)
- F32 channel_ratio_normal; // TextureChannelRatioNormal
- F32 channel_ratio_basecolor; // TextureChannelRatioBaseColor
- F32 channel_ratio_specular; // TextureChannelRatioSpecular
- F32 channel_ratio_emissive; // TextureChannelRatioEmissive
- };
-
- // Tier values: Low (2-4GB), Medium (4-8GB), High (8-16GB), Ultra (16+GB).
- // Quality ladder, expressed in pixel:texel (texels per pixel):
- // - pixel_to_texel_ratio is the baseline quality: how many texels per
- // screen pixel the tier allocates when VRAM is comfortable (1.0 = 1:1).
- // Under pressure the runtime drives the global ratio below this with no
- // floor (down to 0 = deepest mips), so there is no per-tier minimum.
- // - the channel ratios coarsen specular/emissive/normal relative to base
- // color (each is a multiplier on the global ratio).
- // The pressure water marks (TexturePressureHighWater/LowWater) are NOT tiered - they're a
- // physical "crossed the budget" threshold (0.90 / 0.70), constant across
- // tiers. Lower tiers start blurrier (lower R_max) and tighten faster.
- // max_res Rmax tight relax N BC S E
- static constexpr TexturePreset TEXTURE_PRESETS[4] = {
- /* 0 Low */ { "Low", 1024, 0.10f, 0.50f, 0.05f, 0.50f, 1.00f, 0.25f, 0.25f },
- /* 1 Medium */ { "Medium", 2048, 0.40f, 0.35f, 0.08f, 1.00f, 1.00f, 0.50f, 0.50f },
- /* 2 High */ { "High", 2048, 0.80f, 0.25f, 0.10f, 1.00f, 1.00f, 0.50f, 1.00f },
- /* 3 Ultra */ { "Ultra", 2048, 1.00f, 0.15f, 0.12f, 1.00f, 1.00f, 1.00f, 1.00f },
- };
-}
-
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();
- if (quality > 3) quality = 3;
- const TexturePreset& p = TEXTURE_PRESETS[quality];
-
- gSavedSettings.setU32("RenderMaxTextureResolution", p.max_resolution);
- gSavedSettings.setF32("TexturePixelToTexelRatio", p.pixel_to_texel_ratio);
- gSavedSettings.setF32("TexturePressureTightenRate", p.pressure_tighten_rate);
- gSavedSettings.setF32("TexturePressureRelaxRate", p.pressure_relax_rate);
- gSavedSettings.setF32("TextureChannelRatioNormal", p.channel_ratio_normal);
- gSavedSettings.setF32("TextureChannelRatioBaseColor", p.channel_ratio_basecolor);
- gSavedSettings.setF32("TextureChannelRatioSpecular", p.channel_ratio_specular);
- gSavedSettings.setF32("TextureChannelRatioEmissive", p.channel_ratio_emissive);
-
- LL_INFOS("TextureStream") << "Applied texture quality preset: " << p.name << LL_ENDL;
+ 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;
}
@@ -884,6 +869,7 @@ void settings_setup_listeners()
setting_setup_signal_listener(gSavedSettings, "OctreeMaxNodeCapacity", handleRepartition);
setting_setup_signal_listener(gSavedSettings, "OctreeAlphaDistanceFactor", handleRepartition);
setting_setup_signal_listener(gSavedSettings, "OctreeAttachmentSizeFactor", handleRepartition);
+ setting_setup_signal_listener(gSavedSettings, "RenderMaxTextureIndex", handleSetShaderChanged);
setting_setup_signal_listener(gSavedSettings, "RenderUIBuffer", handleWindowResized);
setting_setup_signal_listener(gSavedSettings, "RenderDepthOfField", handleReleaseGLBufferChanged);
setting_setup_signal_listener(gSavedSettings, "RenderFSAAType", handleReleaseGLBufferChanged);
diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp
index 8863dfb501..7e671ef9a2 100644
--- a/indra/newview/llviewermessage.cpp
+++ b/indra/newview/llviewermessage.cpp
@@ -3380,6 +3380,13 @@ void send_agent_update(bool force_send, bool send_reliable)
memory_limited_draw_distance = llmax(gAgentCamera.mDrawDistance / mem_factor, gAgentCamera.mDrawDistance / 2.f);
}
+ if (LLViewerTexture::getSystemMemoryBudgetFactor() > 1.f)
+ {
+ // We are critcally low on memory or recovering,
+ // limit requested draw distance
+ memory_limited_draw_distance = llmax(gAgentCamera.mDrawDistance / LLViewerTexture::getSystemMemoryBudgetFactor(), gAgentCamera.mDrawDistance / 2.f);
+ }
+
if (tp_state == LLAgent::TELEPORT_ARRIVING || LLStartUp::getStartupState() < STATE_MISC)
{
// Inform interest list, prioritize closer area.
diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp
index 4e4282a8ca..7c26cb3c9f 100644
--- a/indra/newview/llviewerobject.cpp
+++ b/indra/newview/llviewerobject.cpp
@@ -5229,20 +5229,7 @@ void LLViewerObject::setTE(const U8 te, const LLTextureEntry& texture_entry)
const LLUUID& image_id = getTE(te)->getID();
LLViewerTexture* bakedTexture = getBakedTextureForMagicId(image_id);
- if (bakedTexture)
- {
- mTEImages[te] = bakedTexture;
- }
- else
- {
- LLViewerFetchedTexture* img = LLViewerTextureManager::getFetchedTexture(image_id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
- // Same creation seed the PBR path applies in updateTEMaterialTextures'
- // fetch_texture - without it a Blinn texture has decode_priority 0 and
- // cannot even fetch headers until its first coverage measurement,
- // while the equivalent PBR texture starts fetching immediately.
- img->addTextureStats(64.f * 64.f, true);
- mTEImages[te] = img;
- }
+ mTEImages[te] = bakedTexture ? bakedTexture : LLViewerTextureManager::getFetchedTexture(image_id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
updateAvatarMeshVisibility(image_id, old_image_id);
@@ -5253,14 +5240,11 @@ void LLViewerObject::updateTEMaterialTextures(U8 te)
{
if (getTE(te)->getMaterialParams().notNull())
{
- // Same creation seed as the PBR fetch_texture below - see setTE.
const LLUUID& norm_id = getTE(te)->getMaterialParams()->getNormalID();
mTENormalMaps[te] = LLViewerTextureManager::getFetchedTexture(norm_id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
- mTENormalMaps[te]->addTextureStats(64.f * 64.f, true);
const LLUUID& spec_id = getTE(te)->getMaterialParams()->getSpecularID();
mTESpecularMaps[te] = LLViewerTextureManager::getFetchedTexture(spec_id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
- mTESpecularMaps[te]->addTextureStats(64.f * 64.f, true);
}
LLFetchedGLTFMaterial* mat = (LLFetchedGLTFMaterial*) getTE(te)->getGLTFRenderMaterial();
diff --git a/indra/newview/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp
index 255317ce80..27865f7598 100644
--- a/indra/newview/llviewershadermgr.cpp
+++ b/indra/newview/llviewershadermgr.cpp
@@ -561,7 +561,13 @@ void LLViewerShaderMgr::setShaders()
LLAppViewer::instance()->isSecondInstance());
}
+ static LLCachedControl<U32> max_texture_index(gSavedSettings, "RenderMaxTextureIndex", 16);
+
+ // when using indexed texture rendering, leave some texture units available for shadow and reflection maps
+ static LLCachedControl<S32> reserved_texture_units(gSavedSettings, "RenderReservedTextureIndices", 14);
+
LLGLSLShader::sIndexedTextureChannels = 4;
+ //llclamp<S32>(max_texture_index, 1, gGLManager.mNumTextureImageUnits-reserved_texture_units);
reentrance = true;
diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp
index 80daffdbf3..ac8bb1d0c5 100644
--- a/indra/newview/llviewertexture.cpp
+++ b/indra/newview/llviewertexture.cpp
@@ -60,7 +60,6 @@
#include "llmediaentry.h"
#include "llvovolume.h"
#include "llviewermedia.h"
-#include "lldrawable.h"
#include "lltexturecache.h"
#include "llviewerwindow.h"
#include "llwindow.h"
@@ -87,8 +86,19 @@ S32 LLViewerTexture::sImageCount = 0;
S32 LLViewerTexture::sRawCount = 0;
S32 LLViewerTexture::sAuxCount = 0;
LLFrameTimer LLViewerTexture::sEvaluationTimer;
-F32 LLViewerTexture::sPixelToTexelRatio = 1.f;
-U32 LLViewerTexture::sGCSuspendedFrame = 0;
+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
constexpr S32 MAX_CACHED_RAW_IMAGE_AREA = 64 * 64;
@@ -100,9 +110,11 @@ U32 LLViewerTexture::sMinLargeImageSize = 65536; //256 * 256.
U32 LLViewerTexture::sMaxSmallImageSize = MAX_CACHED_RAW_IMAGE_AREA;
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;
+F32 LLViewerTexture::sSysMemoryFactor = 1.f;
LLViewerTexture::EDebugTexels LLViewerTexture::sDebugTexelsMode = LLViewerTexture::DEBUG_TEXELS_OFF;
@@ -479,6 +491,13 @@ void LLViewerTexture::initClass()
LLImageGL::sDefaultGLTexture = LLViewerFetchedTexture::sDefaultImagep->getGLTexture();
}
+S32Megabytes get_render_free_main_memory_treshold()
+{
+ static LLCachedControl<U32> min_free_main_memory(gSavedSettings, "RenderMinFreeMainMemoryThreshold", 512);
+ const U32Megabytes MIN_FREE_MAIN_MEMORY(min_free_main_memory);
+ return MIN_FREE_MAIN_MEMORY;
+}
+
//static
void LLViewerTexture::updateClass()
{
@@ -527,60 +546,375 @@ void LLViewerTexture::updateClass()
// 'bias' calculation to kick in.
F32 vram_target = llmax(llmin(vram_budget - 512.f, vram_budget * 0.8f), MIN_VRAM_BUDGET);
sFreeVRAMMegabytes = vram_target - vram_used;
+ const S32Megabytes free_sys_mem = getFreeSystemMemory();
- // VRAM pressure controller for the global pixel:texel ratio. Tightens above
- // the high watermark, relaxes below the low one, holds in the band (the band
- // stops sawtooth). Rates are slow so eviction frees bytes before the next step.
+ F32 over_pct = (vram_used - vram_target) / vram_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> ratio_max(gSavedSettings, "TexturePixelToTexelRatio", 1.0f);
- static LLCachedControl<F32> bg_min_ratio(gSavedSettings, "TextureBackgroundMinRatio", 0.001f);
- static LLCachedControl<F32> wm_high(gSavedSettings, "TexturePressureHighWater", 0.90f);
- static LLCachedControl<F32> wm_low(gSavedSettings, "TexturePressureLowWater", 0.70f);
- static LLCachedControl<F32> tighten_rate(gSavedSettings, "TexturePressureTightenRate", 0.30f);
- static LLCachedControl<F32> relax_rate(gSavedSettings, "TexturePressureRelaxRate", 0.10f);
- static LLCachedControl<F32> cooldown_step(gSavedSettings, "TextureCooldownStepSeconds", 5.f);
+ 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);
- // No lower bound: pressure can drive the ratio to 0 (deepest mip for
- // everything). Only the top is capped, by the configured max.
- F32 r_max = llmax((F32)ratio_max, 0.f);
- F32 high_frac = llclamp((F32)wm_high, 0.1f, 1.f);
- F32 high = vram_budget * high_frac;
- F32 low = vram_budget * llclamp((F32)wm_low, 0.05f, high_frac);
- F32 dt = (F32)gFrameIntervalSeconds;
+ F32 backoff_target = vram_target * llclamp((F32)backoff_start, 0.05f, 1.f);
+ F32 cap = llmax((F32)max_mult, 1.0001f);
+ F32 dt = (F32)gFrameIntervalSeconds;
- bool in_background = (gViewerWindow && !gViewerWindow->getWindow()->getVisible()) || !gFocusMgr.getAppHasFocus();
+ // 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
+ || vram_used > backoff_target * 0.5f;
- if (in_background)
+ 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 = vram_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 = vram_used >= vram_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 = vram_used / llmax(backoff_target, 1.f);
+ LL_INFOS("TextureStream") << "pressure"
+ << " mult=" << sMemoryPressureMultiplier
+ << " target_mult=" << target_mult
+ << " progress=" << progress
+ << " used=" << vram_used
+ << " predicted=" << predicted_used
+ << " target=" << vram_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_sys_critically_low = isSystemMemoryCritical();
+ bool is_low = is_sys_low || over_pct > 0.f;
+
+ static bool was_low = false;
+ static bool sys_was_low = false;
+
+ // System memory factor
+ // sSysMemoryFactor affects draw distance
+ //
+ // We only decrement when more than 406MB is free, but increment
+ // when below 256MB free. This should provide a stable value
+ // in the 256-406MB range to avoid draw range fluctuations.
+ //
+ // Draw range reduction is a last resort, texture bias is supposed
+ // to free at least some memory before we get here.
+ // Note: textures were mostly moved to vram, we might want to
+ // detach texture bias from system memory.
+ if (is_sys_critically_low)
+ {
+ const S32Megabytes MIN_FREE_MAIN_MEMORY(get_render_free_main_memory_treshold() / 2);
+ // debt is a negative value since MIN_FREE_MAIN_MEMORY > free memory.
+ S32 sys_budget_debt = free_sys_mem - MIN_FREE_MAIN_MEMORY;
+
+ // Leave some padding, otherwise we will crash out of memory before hitting factor 2.
+ const S32Megabytes PAD_BUFFER(32);
+ S32Megabytes budget_target = MIN_FREE_MAIN_MEMORY - PAD_BUFFER;
+ if (!sys_was_low)
+ {
+ // Result should range from 1 at 0 debt to 2 at -224 debt, 2.14 at -256MB
+ F32 new_factor = 1.f - (F32)sys_budget_debt / (F32)budget_target;
+ sSysMemoryFactor = llmax(sSysMemoryFactor, new_factor);
+ }
+ else
+ {
+ // Slowly ramp up factor to free memory (increasing factor decreases draw range)
+ constexpr F32 MAX_INCREMENT = 0.05f;
+ F32 increment = MAX_INCREMENT * llmax(-(F32)sys_budget_debt / (F32)budget_target, 0.f);
+ sSysMemoryFactor += increment * gFrameIntervalSeconds;
+ }
+ sSysMemoryFactor = llclamp(sSysMemoryFactor, 1.f, 2.f);
+ }
+ else
+ {
+ const S32Megabytes MIN_FREE_MAIN_MEMORY(get_render_free_main_memory_treshold() / 2);
+ // Only start ramping down when we have breathing room.
+ // This should be under the value of isSystemMemoryLow to not throw texture
+ // bias into 1.5+ territory each time we fluctuate around isSystemMemoryLow's
+ // treshold.
+ const S32Megabytes MEM_THRESHOLD = MIN_FREE_MAIN_MEMORY + S32Megabytes(150);
+ if (free_sys_mem > MEM_THRESHOLD && sSysMemoryFactor > 1.f)
{
- // Backgrounded: decay toward bg_min to free VRAM for other apps, one
- // mip per cooldown_step seconds (multiplicative). Don't relax back up
- // until we're focused again. Pressure can still push below bg_min.
- F32 bg_min = llclamp((F32)bg_min_ratio, 0.f, r_max);
- if (sPixelToTexelRatio > bg_min)
+ // Ramp down factor over time.
+ constexpr F32 DECREMENT = 0.02f;
+ sSysMemoryFactor -= DECREMENT * gFrameIntervalSeconds;
+ sSysMemoryFactor = llclamp(sSysMemoryFactor, 1.f, 2.f);
+ }
+ }
+ sys_was_low = is_sys_critically_low;
+
+ // VRAM memory bias
+ if (is_low && !was_low)
+ {
+ if (is_sys_low)
+ {
+ // Not having system memory is more serious, so discard harder
+ sDesiredDiscardBias = llmax(sDesiredDiscardBias, 1.5f * getSystemMemoryBudgetFactor());
+ }
+ else
+ {
+ // Slam to 1.5 bias the moment we hit low memory (discards off screen textures immediately)
+ sDesiredDiscardBias = llmax(sDesiredDiscardBias, 1.5f);
+ }
+
+ if (is_sys_low || over_pct > 2.f)
+ { // if we're low on system memory, emergency purge off screen textures to avoid a death spiral
+ LL_WARNS() << "Low system memory detected, emergency downrezzing off screen textures" << LL_ENDL;
+ for (auto& image : gTextureList)
{
- const F32 step = llmax((F32)cooldown_step, 0.01f);
- sPixelToTexelRatio = llmax(sPixelToTexelRatio * powf(0.25f, dt / step), bg_min);
+ gTextureList.updateImageDecodePriority(image, false /*will modify gTextureList otherwise!*/);
}
}
- else if (vram_used > high)
+ }
+
+ was_low = is_low;
+
+ if (is_low)
+ {
+ // ramp up discard bias over time to free memory
+ if (sEvaluationTimer.getElapsedTimeF32() > MEMORY_CHECK_WAIT_TIME)
{
- sPixelToTexelRatio -= llmax((F32)tighten_rate, 0.f) * dt;
+ static LLCachedControl<F32> low_mem_min_discard_increment(gSavedSettings, "RenderLowMemMinDiscardIncrement", .1f);
+
+ F32 increment = low_mem_min_discard_increment + llmax(over_pct, 0.f);
+ sDesiredDiscardBias += increment * gFrameIntervalSeconds;
}
- else if (vram_used < low)
+ }
+ else
+ {
+ // 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_THRESHOLD = 100; // 100MB more than isSystemMemoryLow to avoid fluctuations.
+ const S32Megabytes MIN_FREE_MAIN_MEMORY(get_render_free_main_memory_treshold() + S32Megabytes(FREE_SYS_MEM_THRESHOLD));
+ if (sDesiredDiscardBias > 1.f
+ && over_pct < FREE_PERCENTAGE_TRESHOLD
+ && free_sys_mem > MIN_FREE_MAIN_MEMORY
+ && !eviction_in_flight)
{
- sPixelToTexelRatio += llmax((F32)relax_rate, 0.f) * dt;
+ static LLCachedControl<F32> high_mem_discard_decrement(gSavedSettings, "RenderHighMemMinDiscardDecrement", .1f);
+
+ F32 decrement = high_mem_discard_decrement - llmin(over_pct - FREE_PERCENTAGE_TRESHOLD, 0.f);
+ sDesiredDiscardBias -= decrement * gFrameIntervalSeconds;
}
- // else: hold in the hysteresis band.
- sPixelToTexelRatio = llclamp(sPixelToTexelRatio, 0.f, r_max);
+ }
+
+ // set to max discard bias if the window has been backgrounded for a while
+ static F32 last_desired_discard_bias = 1.f;
+ static F32 last_texture_update_count_bias = 1.f;
+ static bool was_backgrounded = false;
+ static LLFrameTimer backgrounded_timer;
+ static LLCachedControl<F32> minimized_discard_time(gSavedSettings, "TextureDiscardMinimizedTime", 1.f);
+ static LLCachedControl<F32> backgrounded_discard_time(gSavedSettings, "TextureDiscardBackgroundedTime", 60.f);
- // Keep the GC-suspend frame current while backgrounded. This suppresses
- // the foreground GC now, and gives it a grace window after we come back so
- // visible content can re-stamp its bind frames before anything is collected.
+ bool in_background = (gViewerWindow && !gViewerWindow->getWindow()->getVisible()) || !gFocusMgr.getAppHasFocus();
+ bool is_minimized = gViewerWindow && gViewerWindow->getWindow()->getMinimized() && in_background;
+ if (in_background)
+ {
+ F32 discard_time = is_minimized ? minimized_discard_time : backgrounded_discard_time;
+ if (discard_time > 0.f && backgrounded_timer.getElapsedTimeF32() > discard_time)
+ {
+ if (!was_backgrounded)
+ {
+ LL_INFOS() << "Viewer was " << (is_minimized ? "minimized" : "backgrounded") << " for " << discard_time
+ << "s, freeing up video memory." << LL_ENDL;
+
+ last_desired_discard_bias = sDesiredDiscardBias;
+ was_backgrounded = true;
+ }
+ sDesiredDiscardBias = 5.f;
+ }
+ }
+ else
+ {
+ backgrounded_timer.reset();
+ if (was_backgrounded)
+ { // if the viewer was backgrounded
+ LL_INFOS() << "Viewer is no longer backgrounded or minimized, resuming normal texture usage." << LL_ENDL;
+ was_backgrounded = false;
+ sDesiredDiscardBias = last_desired_discard_bias;
+ }
+ }
+
+ // 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)
{
- sGCSuspendedFrame = LLFrameTimer::getFrameCount();
+ 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)
+ {
+ // bias increased, reset texture update counter to
+ // let updates happen at an increased rate.
+ last_texture_update_count_bias = sDesiredDiscardBias;
+ sBiasTexturesUpdated = 0;
+ }
+ else if (last_texture_update_count_bias > sDesiredDiscardBias + 0.1f)
+ {
+ // bias decreased, 0.1f is there to filter out small fluctuations
+ // and not reset sBiasTexturesUpdated too often.
+ // Bias jumps to 1.5 at low memory, so getting stuck at 1.1 is not
+ // a problem.
+ last_texture_update_count_bias = sDesiredDiscardBias;
+ }
+}
+
+//static
+U32Megabytes LLViewerTexture::getFreeSystemMemory()
+{
+ static LLFrameTimer timer;
+ static U32Megabytes physical_res = U32Megabytes(U32_MAX);
+
+ if (timer.getElapsedTimeF32() < MEMORY_CHECK_WAIT_TIME) //call this once per second.
+ {
+ return physical_res;
+ }
+
+ timer.reset();
+
+ LLMemory::updateMemoryInfo();
+ physical_res = LLMemory::getAvailableMemKB();
+ return physical_res;
+}
+
+//static
+bool LLViewerTexture::isSystemMemoryLow()
+{
+ return getFreeSystemMemory() < get_render_free_main_memory_treshold();
+}
+
+//static
+bool LLViewerTexture::isSystemMemoryCritical()
+{
+ return getFreeSystemMemory() < get_render_free_main_memory_treshold() / 2;
+}
+
+// static
+F32 LLViewerTexture::getSystemMemoryBudgetFactor()
+{
+ return sSysMemoryFactor;
}
//end of static functions
@@ -2028,30 +2362,6 @@ bool LLViewerFetchedTexture::updateFetch()
LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - current < min");
make_request = false;
}
- else
- {
- // Only fetch streamed world textures the renderer is actually drawing
- // (mLastBindFrame is stamped per drawn frame). Out-of-view content gets
- // no residency. Exempt: boosted/UI, avatar bakes, textures with loaded
- // callbacks, and bake uploads.
- static LLCachedControl<U32> vis_frames(gSavedSettings, "TextureFetchVisibilityFrames", 5);
- const bool visibility_gated = mBoostLevel < LLGLTexture::BOOST_HIGH
- && mUseMipMaps
- && !mDontDiscard
- && !isAgentAvatarBoost(mBoostLevel)
- && !mForceToSaveRawImage
- && mLoadedCallbackList.empty();
- if (visibility_gated && mGLTexturep.notNull())
- {
- const U32 last = mGLTexturep->mLastBindFrame;
- const U32 now = LLFrameTimer::getFrameCount();
- if (last == 0 || now - last > llmax((U32)vis_frames, 1u))
- {
- LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - not visible");
- make_request = false;
- }
- }
- }
if (make_request)
{
@@ -2107,7 +2417,6 @@ bool LLViewerFetchedTexture::updateFetch()
// in some cases createRequest can modify discard, as an example
// bake textures are always at discard 0
mRequestedDiscardLevel = llmin(desired_discard, fetch_request_response);
-
mFetchState = LLAppViewer::getTextureFetch()->getFetchState(mID, mDownloadProgress, mRequestedDownloadPriority,
mFetchPriority, mFetchDeltaTime, mRequestDeltaTime, mCanUseHTTP);
}
@@ -2949,151 +3258,6 @@ S8 LLViewerLODTexture::getType() const
return LLViewerTexture::LOD_TEXTURE;
}
-// Desired discard from the pixel:texel ratio - this is the entire streaming
-// policy. For each channel bucket the texture is used in, the most-demanding
-// (largest screen coverage) face sets that bucket's requirement at
-// floor(log4(texels / (R_global * channelRatio[b] * coverage))); the sharpest
-// requirement across buckets wins, since one GL image serves every channel it
-// is used in. floor (not ceil) keeps content native up close - "1:1" is a
-// target, not a hard cap that downrezzes anything slightly oversampled. A
-// per-mip hysteresis dead-band against the current discard level prevents
-// fetch/scaleDown thrash as an object slowly crosses a mip boundary.
-S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) const
-{
- static const F64 log_4 = log(4.0);
-
- // UI-pinned (icons / thumbnails): size against the known draw size, with no
- // ratio or pressure - these want exact native-for-their-slot resolution.
- if (mKnownDrawWidth && mKnownDrawHeight)
- {
- S32 draw_texels = llclamp(mKnownDrawWidth * mKnownDrawHeight, MIN_IMAGE_AREA, MAX_IMAGE_AREA);
- S32 d = (draw_texels >= (S32)mTexelsPerImage)
- ? 0
- : (S32)floor(log((F64)mTexelsPerImage / (F64)draw_texels) / log_4);
- return llclamp(d, 0, dim_max_i);
- }
-
- // Avatar bakes ignore the global pressure ramp (a blurred bake reads as a
- // cloud avatar); they always size against the configured max ratio.
- static LLCachedControl<F32> ratio_max(gSavedSettings, "TexturePixelToTexelRatio", 1.0f);
- const F32 r_global = avatar_bake ? llmax((F32)ratio_max, 0.01f) : sPixelToTexelRatio;
-
- static LLCachedControl<F32> ch_normal (gSavedSettings, "TextureChannelRatioNormal", 1.0f);
- static LLCachedControl<F32> ch_basecolor(gSavedSettings, "TextureChannelRatioBaseColor", 1.0f);
- static LLCachedControl<F32> ch_specular (gSavedSettings, "TextureChannelRatioSpecular", 0.5f);
- static LLCachedControl<F32> ch_emissive (gSavedSettings, "TextureChannelRatioEmissive", 0.5f);
- const F32 channel_ratio[4] = { (F32)ch_normal, (F32)ch_basecolor, (F32)ch_specular, (F32)ch_emissive };
-
- // Downrez bias: 0 sizes each bucket to its most demanding use (the lowest
- // texels-per-pixel variant - the quality bound); 1 sizes to its least
- // demanding use (the most oversampled variant - frees the most memory).
- // Values between lerp across the texture's measured coverage spread.
- static LLCachedControl<F32> downrez_bias(gSavedSettings, "TextureDownrezCoverageBias", 0.25f);
- const F32 cov_bias = llclampf((F32)downrez_bias);
-
- // Continuous ideal discard = sharpest (smallest) requirement across the
- // channels this texture is actually used in.
- F32 ideal = (F32)dim_max_i; // default: coarsest, until a measurement arrives
- bool measured = false;
- for (S32 b = 0; b < 4; ++b)
- {
- F32 coverage = mChannelCoverage[b];
- if (coverage <= 0.f)
- {
- continue;
- }
- if (cov_bias > 0.f && mChannelCoverageMin[b] > 0.f && mChannelCoverageMin[b] < coverage)
- {
- // Geometric (log-space) lerp between the coverage bounds. The
- // consumer below is log4(coverage), and min/max are routinely
- // orders of magnitude apart - a linear pixel-area lerp barely
- // moves the resulting discard until bias approaches 1, then
- // plunges (reads as binary). Interpolating the RATIO instead
- // moves the discard linearly with bias: 0.5 = halfway between
- // the two ends in mip levels.
- coverage *= powf(mChannelCoverageMin[b] / coverage, cov_bias);
- }
- measured = true;
- F32 allowed_texels = coverage * r_global * llmax(channel_ratio[b], 0.01f);
- F32 d;
- if (allowed_texels <= 0.f) // ratio driven to 0 -> deepest mip
- d = (F32)dim_max_i;
- else if (allowed_texels >= (F32)mTexelsPerImage)
- d = 0.f;
- else
- d = (F32)(log((F64)mTexelsPerImage / (F64)allowed_texels) / log_4);
- ideal = llmin(ideal, d);
- }
- if (!measured)
- {
- return dim_max_i; // off-screen / never measured -> coarsest mip
- }
-
- // Round toward sharper (floor): a texture stays at a mip level until its
- // ideal is a full level past the boundary. This is what makes "1:1" mean
- // "native up close" - ceil would downrez anything even slightly oversampled
- // (a 2048 map can't hit its own resolution on a 1080p screen), which reads
- // as everything being blurry. Pressure still evicts by lowering R_global.
- ideal = llmax(ideal, 0.f);
- const S32 target = (S32)floor(ideal);
-
- // Hysteresis: a texture at discard C is "happy" while floor(ideal) == C,
- // i.e. ideal in [C, C+1). Only leave that band once ideal is past it by the
- // margin, so coverage jitter at a boundary doesn't ping-pong fetch<->scaleDown.
- static LLCachedControl<F32> uprez_margin(gSavedSettings, "TextureUpRezMargin", 0.2f);
- const F32 margin = llclamp((F32)uprez_margin, 0.f, 0.9f);
- const S32 current = getDiscardLevel();
- S32 desired;
- if (current < 0)
- {
- desired = target; // nothing loaded yet
- }
- else if (ideal >= (F32)current + 1.f + margin)
- {
- desired = target; // clearly coarser -> evict
- }
- else if (ideal <= (F32)current - margin)
- {
- desired = target; // clearly finer -> uprez
- }
- else
- {
- desired = current; // inside the dead-band -> hold
- }
-
- // Foreground visibility GC (avatar bakes exempt). Background degradation is
- // handled by the ratio decay in updateClass, and the GC self-suppresses while
- // backgrounded via the sGCSuspendedFrame check below, so the two don't fight.
- //
- // For every gc_cooldown frames a texture goes without a camera bind, drop its
- // mip by gc_step, walking gradually toward the deepest mip instead of slamming.
- // Content drawn within the last cooldown stays full-res, so a fast camera pan
- // finds it only a step or two coarse on the way back. Resets when drawn again.
- if (!avatar_bake)
- {
- if (LLImageGL* gli = getGLTexture())
- {
- static LLCachedControl<U32> gc_cooldown_frames(gSavedSettings, "TextureGCStepFrames", 5);
- static LLCachedControl<U32> gc_step_mips(gSavedSettings, "TextureGCStepMips", 1);
- constexpr U32 GC_RESUME_GRACE_FRAMES = 10;
- const U32 now = LLFrameTimer::getFrameCount();
- if (gli->mLastBindFrame > 0 // drawn at least once
- && now - sGCSuspendedFrame > GC_RESUME_GRACE_FRAMES) // not just back from background
- {
- const U32 cooldown = llmax((U32)gc_cooldown_frames, 1u);
- const S32 periods = (S32)((now - gli->mLastBindFrame) / cooldown);
- if (periods > 0)
- {
- const S32 step_mips = (S32)llmax((U32)gc_step_mips, 1u);
- desired = llclamp(desired + periods * step_mips, desired, dim_max_i);
- }
- }
- }
- }
-
- return llclamp(desired, 0, dim_max_i);
-}
-
// This is gauranteed to get called periodically for every texture
//virtual
void LLViewerLODTexture::processTextureStats()
@@ -3101,13 +3265,7 @@ void LLViewerLODTexture::processTextureStats()
LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
updateVirtualSize();
- // Hoisted once: avatar bake textures are exempt from several pressure
- // mechanisms below (anti-cloud-bug protection). Reads of `avatar_bake`
- // replace inline isAgentAvatarBoost(mBoostLevel) calls; each site keeps
- // its own intent comment explaining *why* the exemption applies. See
- // also LLViewerFetchedTexture::isAgentAvatarBoost() in the header for
- // the canonical list of exemption sites.
- const bool avatar_bake = isAgentAvatarBoost(mBoostLevel);
+ bool did_downscale = false;
static LLCachedControl<bool> textures_fullres(gSavedSettings,"TextureLoadFullRes", false);
@@ -3126,7 +3284,7 @@ void LLViewerLODTexture::processTextureStats()
mDesiredDiscardLevel = 0;
}
// HUD/UI/preview and mDontDiscard textures bypass streaming - no
- // coverage signal applies, they need native resolution.
+ // face_distance signal applies, they need native resolution.
else if (mBoostLevel >= LLGLTexture::BOOST_HIGH
|| mDontDiscard
|| !mUseMipMaps)
@@ -3142,37 +3300,165 @@ void LLViewerLODTexture::processTextureStats()
}
else
{
- // Per-texture max discard (smallest meaningful mip): floor(log2(max(w,h))).
+ F32 discard_level = 0.f;
+
+ // 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;
- // The whole policy: pixel:texel ratio at the most-demanding face.
- S32 discard = computeDesiredDiscard(dim_max_for_image_i, avatar_bake);
+ 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);
+ discard_level = (F32)(log(mTexelsPerImage / draw_texels) / log_4);
+ }
+ else
+ {
+ // 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);
- // Per-texture caps: force >=1 for sources over the resolution cap;
- // bound by the debug override or the dim-derived max.
- S32 min_discard = 0;
+ 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);
+
+ F32 min_discard = 0.f;
if (mFullWidth > max_tex_res || mFullHeight > max_tex_res)
- min_discard = 1;
+ min_discard = 1.f;
+ // 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);
- const S32 effective_cap = (max_discard_override > 0) ? (S32)max_discard_override : dim_max_for_image_i;
- discard = llclamp(discard, min_discard, effective_cap);
+ 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;
+ }
+ }
- // Caller-set min-discard ceiling (terrain / avatar-self / thumbnails):
- // never coarser than the caller explicitly asked for.
- discard = llmin(discard, (S32)mMinDesiredDiscardLevel);
- mDesiredDiscardLevel = (S8)discard;
+ //
+ // At this point we've calculated the quality level that we want,
+ // if possible. Now we check to see if we have it, and take the
+ // proper action if we don't.
+ //
- // If the GPU already holds finer data than we now want, evict it.
- // Avatar bakes exempt: shrinking mid-bake can leave the avatar stuck
- // as a cloud until the next bake completes.
S32 current_discard = getDiscardLevel();
- if (!avatar_bake && current_discard >= 0 && current_discard < mDesiredDiscardLevel && !mForceToSaveRawImage)
+ // Avatar bakes exempt: shrinking mid-bake can leave the avatar
+ // stuck as a cloud until the next bake completes.
+ if (!isAgentAvatarBoost(mBoostLevel))
{
- scaleDown();
+ if (current_discard < mDesiredDiscardLevel && !mForceToSaveRawImage)
+ { // should scale down
+ scaleDown();
+ }
}
mDesiredDiscardLevel = llmin(mDesiredDiscardLevel, (S32)mLoadedCallbackDesiredDiscardLevel);
@@ -3200,7 +3486,12 @@ bool LLViewerLODTexture::scaleDown()
return false;
}
- // Structural blocks only; per-texture policy lives in processTextureStats.
+ // 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;
diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h
index cc9fbe5e48..8c5c48bafd 100644
--- a/indra/newview/llviewertexture.h
+++ b/indra/newview/llviewertexture.h
@@ -114,6 +114,11 @@ protected:
public:
static void initClass();
static void updateClass();
+ static bool isSystemMemoryLow();
+ static bool isSystemMemoryCritical();
+
+ // Ranges from 1 (no RAM deficit) to 2 (RAM deficit)
+ static F32 getSystemMemoryBudgetFactor();
LLViewerTexture(bool usemipmaps = true);
LLViewerTexture(const LLUUID& id, bool usemipmaps) ;
@@ -145,11 +150,6 @@ public:
virtual F32 getMaxVirtualSize() ;
- // Read-only debug access to the per-bucket coverage bounds (see
- // mChannelCoverage) - used by the RENDER_DEBUG_TEXTURE_PRIORITY overlay.
- F32 getChannelCoverage(S32 bucket) const { return (bucket >= 0 && bucket < 4) ? mChannelCoverage[bucket] : 0.f; }
- F32 getChannelCoverageMin(S32 bucket) const { return (bucket >= 0 && bucket < 4) ? mChannelCoverageMin[bucket] : 0.f; }
-
LLFrameTimer* getLastReferencedTimer() { return &mLastReferencedTimer; }
S32 getFullWidth() const { return mFullWidth; }
@@ -193,6 +193,8 @@ private:
friend class LLBumpImageList;
friend class LLUIImageList;
+ static U32Megabytes getFreeSystemMemory();
+
protected:
friend class LLViewerTextureList;
LLUUID mID;
@@ -203,17 +205,24 @@ protected:
mutable S32 mMaxVirtualSizeResetInterval;
LLFrameTimer mLastReferencedTimer;
- // Screen-space pixel coverage bounds among the texture's faces, per
- // priority bucket (0=Normal, 1=BaseColor, 2=Specular, 3=Emissive). 0 = the
- // texture is not used in that channel (or hasn't been measured yet).
- // Populated by LLViewerTextureList::updateImageDecodePriority; consumed by
- // LLViewerLODTexture::computeDesiredDiscard. This is the only view-dependent
- // streaming signal - distance, size, tiling, and channel role all collapse
- // into it. Max = the most demanding use (lowest texels-per-pixel variant,
- // the quality bound); Min = the least demanding positive use (highest
- // texels-per-pixel, most oversampled - the downrez-bias end).
- F32 mChannelCoverage[4] = { 0.f, 0.f, 0.f, 0.f };
- F32 mChannelCoverageMin[4] = { 0.f, 0.f, 0.f, 0.f };
+ // 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];
@@ -235,17 +244,24 @@ public:
static S32 sRawCount;
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;
- // The global max pixel:texel ratio (texels per screen pixel, the "R" in 1:R).
- // The watermark controller in updateClass walks it between TexturePixelToTexelRatio
- // and 0 as VRAM pressure changes; lower means coarser desired discards.
- static F32 sPixelToTexelRatio;
-
- // Frame index the GC was last suspended (kept current while backgrounded).
- // The foreground GC only runs once getFrameCount() is a grace window past
- // this, so content can re-stamp after an alt-tab before anything is collected.
- static U32 sGCSuspendedFrame;
+ // 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 ;
@@ -254,6 +270,7 @@ public:
// estimated free memory for textures, by bias calculation
static F32 sFreeVRAMMegabytes;
+ static F32 sSysMemoryFactor;
// Viewport pixel area, refreshed once per frame. Hoisted to keep the
// per-texture hot path out of gViewerWindow.
static F32 sWindowPixelArea;
@@ -314,6 +331,27 @@ public:
}
public:
+
+ struct Compare
+ {
+ // lhs < rhs
+ bool operator()(const LLPointer<LLViewerFetchedTexture> &lhs, const LLPointer<LLViewerFetchedTexture> &rhs) const
+ {
+ const LLViewerFetchedTexture* lhsp = (const LLViewerFetchedTexture*)lhs;
+ const LLViewerFetchedTexture* rhsp = (const LLViewerFetchedTexture*)rhs;
+
+ // greater priority is "less"
+ const F32 lpriority = lhsp->mMaxVirtualSize;
+ const F32 rpriority = rhsp->mMaxVirtualSize;
+ if (lpriority > rpriority) // higher priority
+ return true;
+ if (lpriority < rpriority)
+ return false;
+ return lhsp < rhsp;
+ }
+ };
+
+public:
/*virtual*/ S8 getType() const override;
FTType getFTType() const;
/*virtual*/ void forceImmediateUpdate() override;
@@ -564,16 +602,6 @@ public:
private:
void init(bool firstinit) ;
-
- // The whole streaming pipeline: desired discard from the pixel:texel ratio.
- // For each channel bucket the texture is used in, the most-demanding
- // (largest coverage) face sets that bucket's requirement at
- // floor(log4(texels / (R_global * channelRatio[b] * coverage))); the sharpest
- // requirement across buckets wins (one GL image serves all its channels).
- // A per-mip hysteresis dead-band against the current discard level prevents
- // fetch/scaleDown thrash. avatar_bake textures use the configured max ratio
- // instead of the pressure-driven sPixelToTexelRatio (anti cloud-bug).
- S32 computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) const;
};
//
diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp
index 3e1481f8b4..4d09eff74e 100644
--- a/indra/newview/llviewertexturelist.cpp
+++ b/indra/newview/llviewertexturelist.cpp
@@ -64,9 +64,6 @@
#include "llviewerwindow.h"
#include "llsurface.h"
#include "llvoavatarself.h"
-#include "lldrawable.h"
-#include "llvovolume.h"
-#include "llviewertextureanim.h"
#include "llprogressview.h"
////////////////////////////////////////////////////////////////////////////
@@ -74,6 +71,7 @@
void (*LLViewerTextureList::sUUIDCallback)(void **, const LLUUID&) = NULL;
S32 LLViewerTextureList::sNumImages = 0;
+F32 LLViewerTextureList::sCurrentBubbleMeters = 0.f;
LLViewerTextureList gTextureList;
@@ -919,86 +917,100 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
{
llassert(!gCubeSnapshot);
- // Refresh spotlight priorities first: light projector textures register as
- // LIGHT_TEX volumes (no faces), and both their fetch priority
- // (addTextureStats inside updateSpotLightPriority) and their coverage
- // (folded into the block below) derive from mSpotLightPriority.
- for (S32 vi = 0; vi < imagep->getNumVolumes(LLRender::LIGHT_TEX); ++vi)
- {
- LLVOVolume* volume = (*imagep->getVolumeList(LLRender::LIGHT_TEX))[vi];
- volume->updateSpotLightPriority();
- }
+ 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
{
- // Bounds on the per-face UV repeat-area divisor (mined from the old
- // getTextureVirtualSize texel_area clamp [1/64, 128]): atlas/crop boost
- // capped at 64x (3 mips finer), tiling penalty at 128x (3.5 mips
- // coarser) so pathological UV scales can't explode either direction.
- constexpr F32 MIN_REPEAT_AREA = 1.f / 64.f;
- constexpr F32 MAX_REPEAT_AREA = 128.f;
+ static LLCachedControl<F32> texture_scale_min(gSavedSettings, "TextureScaleMinAreaFactor", 0.0095f);
+ static LLCachedControl<F32> texture_scale_max(gSavedSettings, "TextureScaleMaxAreaFactor", 25.f);
- // Per priority bucket (0=Normal, 1=BaseColor, 2=Specular, 3=Emissive):
- // the HIGHEST per-face effective coverage (= the lowest texels-per-pixel
- // use, the most demanding variant - drives desired discard) and the
- // LOWEST positive coverage (= the highest texels-per-pixel use, the most
- // oversampled variant - available for downrez-bias policy). The overall
- // max is the fetch priority (raw - no bias). A bucket with faces but
- // zero coverage (all off-screen) publishes 0, which
- // computeDesiredDiscard reads as "coarsest mip".
- F32 channel_coverage[4] = { 0.f, 0.f, 0.f, 0.f };
- F32 channel_coverage_min[4] = { FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX };
- bool bucket_used[4] = { false, false, false, false };
- F32 max_coverage = 0.f;
+ 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;
- const U32 max_faces_to_check = 1024;
+ U32 max_faces_to_check = 1024;
- // Cheap first pass: which buckets is this texture used in, and how many
- // faces total. No per-face geometry work.
+ // 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)
{
- U32 n = imagep->getNumFaces(i);
- face_count += n;
- if (n > 0)
+ if (imagep->getNumFaces(i) > 0)
{
- S32 b = sChannelToPriority[i];
- if (b >= 0 && b < 4) bucket_used[b] = true;
+ S32 mapped = sChannelToPriority[i];
+ priority_channel = (priority_channel < 0) ? mapped : llmin(priority_channel, mapped);
}
}
-
- if (face_count > max_faces_to_check)
+ if (priority_channel < 0)
{
- // Used in so many places that scanning the face list isn't worth it
- // (and isn't time-sliced) - treat as full-screen so it loads sharp.
- for (S32 b = 0; b < 4; ++b)
- {
- if (bucket_used[b])
- {
- channel_coverage[b] = (F32)MAX_IMAGE_AREA;
- channel_coverage_min[b] = (F32)MAX_IMAGE_AREA;
- }
- }
- max_coverage = (F32)MAX_IMAGE_AREA;
+ priority_channel = 1; // no faces - default to diffuse
}
- else
+ 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);
+ F32 bias = llclamp(max_discard - 2.f, 1.f, LLViewerTexture::sDesiredDiscardBias);
+
+ // convert bias into a vsize scaler
+ bias = (F32) llroundf(powf(4, bias - 1.f));
+
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
+ for (U32 i = 0; i < LLRender::NUM_TEXTURE_CHANNELS; ++i)
{
- LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
- for (U32 i = 0; i < LLRender::NUM_TEXTURE_CHANNELS; ++i)
+ face_count += imagep->getNumFaces(i);
+ S32 faces_to_check = (face_count > max_faces_to_check) ? 0 : imagep->getNumFaces(i);
+
+ for (S32 fi = 0; fi < faces_to_check; ++fi)
{
- const S32 bucket = sChannelToPriority[i];
- const U32 num_faces = imagep->getNumFaces(i);
- for (U32 fi = 0; fi < num_faces; ++fi)
- {
- LLFace* face = (*(imagep->getFaceList(i)))[fi];
- if (!face || !face->getViewerObject())
- {
- continue;
- }
+ LLFace* face = (*(imagep->getFaceList(i)))[fi];
+ if (face && face->getViewerObject())
+ {
F32 radius;
F32 cos_angle_to_view_dir;
+
if ((gFrameCount - face->mLastTextureUpdate) > 10)
{ // only call calcPixelArea at most once every 10 frames for a given face
// this helps eliminate redundant calls to calcPixelArea for faces that have multiple textures
@@ -1007,251 +1019,171 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
face->mLastTextureUpdate = gFrameCount;
}
- // Most-demanding-point measurement: the spec is that the
- // LOWEST pixel:texel ratio governs, so pixel density is
- // evaluated at the face's NEAREST point and applied to the
- // face's true world area. The previous whole-face average
- // (bounding-disc pixel area) under-resolved perspective
- // surfaces: on a floor, the tile at your feet covers far
- // more screen than the average tile, and the GPU samples
- // fine mips right there - tiled (PBR-heavy) content went
- // soft while untiled content looked fine.
- const LLVector4a* ext = face->isState(LLFace::RIGGED) ? face->mRiggedExtents : face->mExtents;
- LLVector4a diag;
- diag.setSub(ext[1], ext[0]);
- // World area of the face ~ product of the two largest AABB
- // dims (max pairwise product; robust for flat faces).
- F32 dx = diag[0], dy = diag[1], dz = diag[2];
- F32 area_world = llmax(dx * dy, llmax(dx * dz, dy * dz));
- // Pixels per meter at the nearest point. Distance floored:
- // nearer than this the screen clamp below governs anyway.
- F32 dist = llmax(face->mDistanceToCamera, 0.5f);
- F32 ppm = LLDrawable::sCurPixelAngle / dist;
- F32 face_px = area_world * ppm * ppm;
- if (face_px <= 0.f)
+ F32 vsize = face->getPixelArea();
+
+ 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)
{
- // Degenerate extents: the face hasn't been through a
- // geometry build yet (or a rigged face has no rigged
- // extents) - it isn't renderable, so it must not be
- // measured. Skipping matters especially for the
- // per-bucket MIN bound: any invented placeholder
- // value (the old fallback hit LLFace::init's 16px
- // default) becomes the texture's least-demanding
- // "use" and, under TextureDownrezCoverageBias, drags
- // the whole texture to its deepest mip - and it
- // poisoned BP and PBR asymmetrically since the two
- // systems register faces at different points in the
- // geometry lifecycle.
- continue;
+ on_agent_avatar = true;
}
- // Effective UV repeat AREA across this face: the tiling
- // term of texels-drawn-per-screen-pixel. More tiling =>
- // each tile is smaller on screen => coarser mips suffice
- // (penalty). Repeats < 1 (atlas/crop) => only a sub-rect
- // of the image is shown, but discard levels are whole-
- // image, so the full image must be resident at 1/repeats
- // times the crop's pixel count (boost).
+ // Scale desired texture resolution higher or lower depending on texture scale
+ //
+ // Minimum usage examples: a 1024x1024 texture with aplhabet (texture atlas),
+ // runing string shows one letter at a time. If texture has ten 100px symbols
+ // per side, minimal scale is (100/1024)^2 = 0.0095
+ //
+ // Maximum usage examples: huge chunk of terrain repeats texture
+ // TODO: make this work with the GLTF texture transforms
S32 te_offset = face->getTEOffset(); // offset is -1 if not inited
LLViewerObject* objp = face->getViewerObject();
const LLTextureEntry* te = (te_offset < 0 || te_offset >= objp->getNumTEs()) ? nullptr : objp->getTE(te_offset);
+ F32 min_scale = te ? llmin(fabsf(te->getScaleS()), fabsf(te->getScaleT())) : 1.f;
+ min_scale = llclamp(min_scale * min_scale, texture_scale_min(), texture_scale_max());
+ vsize /= min_scale;
- F32 repeats = 1.f;
- if (te)
- {
- // UV scale source: every channel reads the repeat
- // values ITS renderer actually applies, then flows
- // through the identical pipeline below. Sources:
- // diffuse -> TE scale
- // Blinn normal/spec -> LLMaterial per-map repeats
- // PBR channels -> KHR texture_transform scale
- // Fallback for any missing material is the TE scale -
- // never a silent hardcoded 1.
- F32 scale_s = te->getScaleS();
- F32 scale_t = te->getScaleT();
- if (i >= LLRender::BASECOLOR_MAP)
- {
- // LLRender channel -> LLGLTFMaterial::TextureInfo
- static const S32 gltf_info[4] = {
- LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR, // BASECOLOR_MAP (3)
- LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, // METALLIC_ROUGHNESS_MAP (4)
- LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL, // GLTF_NORMAL_MAP (5)
- LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE, // EMISSIVE_MAP (6)
- };
- if (const LLGLTFMaterial* gltf_mat = te->getGLTFRenderMaterial())
- {
- const LLVector2& s = gltf_mat->mTextureTransform[gltf_info[i - LLRender::BASECOLOR_MAP]].mScale;
- scale_s = s.mV[0];
- scale_t = s.mV[1];
- }
- }
- else if (i == LLRender::NORMAL_MAP || i == LLRender::SPECULAR_MAP)
- {
- // Blinn-Phong normal/specular maps carry their own
- // repeats in LLMaterial - the renderer builds their
- // texture matrices from these, NOT from the TE's
- // diffuse scale. Reading the diffuse scale here made
- // Blinn normals scale differently than PBR normals
- // (whose per-channel transform IS read above).
- if (const LLMaterial* mat = te->getMaterialParams().get())
- {
- if (i == LLRender::NORMAL_MAP)
- {
- mat->getNormalRepeat(scale_s, scale_t);
- }
- else
- {
- mat->getSpecularRepeat(scale_s, scale_t);
- }
- }
- }
-
- // Continuously-animated scale (llSetTextureAnim SCALE)
- // bypasses both static sources via mTextureMatrix -
- // the live animated values win on either path.
- if (LLVOVolume* vvo = face->getDrawable() ? face->getDrawable()->getVOVolume() : nullptr)
- {
- LLViewerTextureAnim* anim = vvo->mTextureAnimp;
- if (anim && (anim->mMode & LLTextureAnim::ON) && (anim->mMode & LLTextureAnim::SCALE)
- && (anim->mFace < 0 || anim->mFace == te_offset))
- {
- scale_s = anim->mScaleS;
- scale_t = anim->mScaleT;
- }
- }
-
- repeats = fabsf(scale_s * scale_t);
-
- // Mesh atlas sub-rect: a face whose intrinsic UVs span
- // only part of [0,1]^2 shows that fraction of the
- // image. Applies identically to both paths - the
- // transforms above stack on the raw face UVs.
- if (LLVolume* vol = objp->getVolume())
- {
- if (te_offset >= 0 && te_offset < vol->getNumVolumeFaces())
- {
- const LLVolumeFace& vf = vol->getVolumeFace(te_offset);
- F32 span = fabsf((vf.mTexCoordExtents[1].mV[0] - vf.mTexCoordExtents[0].mV[0])
- * (vf.mTexCoordExtents[1].mV[1] - vf.mTexCoordExtents[0].mV[1]));
- if (span > 0.f)
- {
- repeats *= span;
- }
- }
- }
- }
-
- repeats = llclamp(repeats, MIN_REPEAT_AREA, MAX_REPEAT_AREA);
+ // 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 the two sides of the repeat term in the right
- // order relative to the screen clamp:
- // - tiling (repeats > 1): the per-tile footprint at the
- // nearest point, THEN clamped - one tile can't draw
- // more pixels than the screen. (Clamping the whole
- // face first and then dividing crushed near tiles.)
- // - atlas/crop (repeats < 1): boost AFTER the clamp -
- // whole-image residency for a crop legitimately
- // demands more than its screen coverage.
- F32 tiling = llmax(repeats, 1.f);
- F32 crop = llmin(repeats, 1.f);
- F32 vsize = llmin(face_px / tiling, LLViewerTexture::sWindowPixelArea) / crop;
-
- // Avatar bonus: worn attachments get a coverage
- // multiplier - avatars are what people look at, and
- // rigged extents make attachment coverage measurement
- // unreliable anyway. Multiplicative, not a slam: a
- // nearby avatar gains ~a mip of headroom while a distant
- // one still downrezzes naturally with its coverage.
- // (System-avatar bakes get the same bonus in the
- // no-faces branch below.)
- if (objp->isAttachment())
+ // 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)
{
- static LLCachedControl<F32> avatar_boost(gSavedSettings, "TextureAvatarBoost", 4.f);
- vsize *= llmax((F32)avatar_boost, 1.f);
+ vsize /= bias;
}
- if (bucket >= 0 && bucket < 4)
+ max_vsize = llmax(max_vsize, vsize);
+
+ // addTextureStats limits size to sMaxVirtualSize
+ if (max_vsize >= LLViewerFetchedTexture::sMaxVirtualSize
+ && (on_screen || LLViewerTexture::sDesiredDiscardBias <= BIAS_TRS_ON_SCREEN))
{
- channel_coverage[bucket] = llmax(channel_coverage[bucket], vsize);
- if (vsize > 0.f)
- {
- channel_coverage_min[bucket] = llmin(channel_coverage_min[bucket], vsize);
- }
+ break;
}
- max_coverage = llmax(max_coverage, vsize);
}
}
+
+ if (max_vsize >= LLViewerFetchedTexture::sMaxVirtualSize
+ && (on_screen || LLViewerTexture::sDesiredDiscardBias <= BIAS_TRS_ON_SCREEN))
+ {
+ break;
+ }
}
- // Terrain detail textures register no faces (LLVOSurfacePatch
- // addFace(NULL)); synthesize coverage from the nearest visible patch so
- // they degrade with distance like everything else.
- if (face_count == 0 && imagep->getBoostLevel() == LLGLTexture::BOOST_TERRAIN)
+ 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;
+ }
+
+ imagep->addTextureStats(max_vsize);
+
+ // 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 draw_distance = llmax(gAgentCamera.mDrawDistance, 1.f);
- F32 near_frac = (nearest < FLT_MAX) ? llclampf(1.f - nearest / draw_distance) : 0.f;
- // Floor so terrain never collapses to nothing at draw distance.
- F32 cov = LLViewerTexture::sWindowPixelArea * llmax(near_frac, 0.05f);
- channel_coverage[1] = cov; // terrain detail maps are diffuse / base color
- channel_coverage_min[1] = cov;
- max_coverage = cov;
+ 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);
}
- // Baked avatar textures render on system-avatar body meshes whose
- // joint meshes never register faces with the texture list
- // (LLAvatarJointMesh::setTexture just stores the pointer).
- // LLVOAvatar::updateTextures feeds their on-screen pixel area through
- // addTextureStats every frame - use that as BaseColor coverage, with
- // the same avatar bonus attachments get above.
- else if (face_count == 0
- && (imagep->getFTType() == FTT_SERVER_BAKE || imagep->getFTType() == FTT_HOST_BAKE))
+ else
{
- static LLCachedControl<F32> avatar_boost(gSavedSettings, "TextureAvatarBoost", 4.f);
- F32 cov = llmin(imagep->getMaxVirtualSize(), LLViewerTexture::sWindowPixelArea)
- * llmax((F32)avatar_boost, 1.f);
- if (cov > 0.f)
- {
- channel_coverage[1] = cov;
- channel_coverage_min[1] = cov;
- max_coverage = cov;
- }
+ imagep->mMinDistanceFactor = min_distance_factor;
+ imagep->mMaxOnScreenSize = max_on_screen_size;
}
+ imagep->mOnAgentAvatar = on_agent_avatar;
- // Light projector textures register as LIGHT_TEX volumes, not faces.
- // mSpotLightPriority (refreshed above) is already a screen-pixel-area
- // estimate of the lit radius - fold it in as BaseColor coverage so
- // projectors stream view-dependently like everything else.
- for (S32 vi = 0; vi < imagep->getNumVolumes(LLRender::LIGHT_TEX); ++vi)
+ // 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()))
{
- LLVOVolume* volume = (*imagep->getVolumeList(LLRender::LIGHT_TEX))[vi];
- F32 cov = llmin(volume->getSpotLightPriority(), LLViewerTexture::sWindowPixelArea);
- if (cov > 0.f)
- {
- channel_coverage[1] = llmax(channel_coverage[1], cov);
- channel_coverage_min[1] = llmin(channel_coverage_min[1], cov);
- max_coverage = llmax(max_coverage, cov);
- }
+ 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);
- imagep->addTextureStats(max_coverage);
+ // 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;
- // Publish per-bucket coverage bounds for
- // LLViewerLODTexture::computeDesiredDiscard.
- for (S32 b = 0; b < 4; ++b)
- {
- imagep->mChannelCoverage[b] = channel_coverage[b];
- imagep->mChannelCoverageMin[b] = (channel_coverage_min[b] == FLT_MAX) ? 0.f : channel_coverage_min[b];
+ if (!has_clock || time_since <= grace)
+ {
+ imagep->mStalenessFactor = 0.f;
+ }
+ else
+ {
+ 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
- imagep->setDebugText(llformat("%d/%d -- %d/%d",
+ imagep->setDebugText(llformat("%d/%d - %d/%d -- %d/%d",
+ (S32)sqrtf(max_vsize),
+ (S32)sqrtf(imagep->mMaxVirtualSize),
imagep->getDiscardLevel(),
imagep->getDesiredDiscardLevel(),
imagep->getWidth(),
imagep->getFullWidth()));
#endif
+ // make sure to addTextureStats for any spotlights that are using this texture
+ for (S32 vi = 0; vi < imagep->getNumVolumes(LLRender::LIGHT_TEX); ++vi)
+ {
+ LLVOVolume* volume = (*imagep->getVolumeList(LLRender::LIGHT_TEX))[vi];
+ volume->updateSpotLightPriority();
+ }
+
F32 max_inactive_time = 20.f; // inactive time before deleting saved raw image
S32 min_refs = 3; // 1 for mImageList, 1 for mUUIDMap, and 1 for "entries" in updateImagesFetchTextures
@@ -1477,6 +1409,21 @@ 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);
+ // 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())
+ {
+ 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
+ // at bias = 4 with 4 times the rate permanently.
+ LLViewerTexture::sBiasTexturesUpdated += update_count;
+ }
update_count = llmin(update_count, (U32) mUUIDMap.size());
{ // copy entries out of UUID map to avoid iterator invalidation from deletion inside updateImageDecodeProiroty or updateFetch below
diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h
index 931f2ed50e..dbed8b5c2f 100644
--- a/indra/newview/llviewertexturelist.h
+++ b/indra/newview/llviewertexturelist.h
@@ -244,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/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp
index e2601e6465..dea96e2012 100644
--- a/indra/newview/llviewerwindow.cpp
+++ b/indra/newview/llviewerwindow.cpp
@@ -5597,13 +5597,7 @@ bool LLViewerWindow::cubeSnapshot(const LLVector3& origin, LLCubeMapArray* cubea
// actually render the scene
gCubeSnapshot = true;
- {
- // Probe binds aren't visibility - otherwise every probe slice re-stamps
- // behind-camera textures and cycles them evict->refetch. RAII so the
- // nested shadow pass in display_cube_face doesn't re-enable stamping.
- LLImageGLStampBypass stamp_bypass;
- display_cube_face();
- }
+ display_cube_face();
gCubeSnapshot = false;
}
diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp
index d57456aa5a..3b41ccb6fc 100644
--- a/indra/newview/llvovolume.cpp
+++ b/indra/newview/llvovolume.cpp
@@ -878,15 +878,11 @@ void LLVOVolume::updateTextureVirtualSize(bool forced)
LLViewerFetchedTexture* img = LLViewerTextureManager::staticCastToFetchedTexture(imagep) ;
if(img)
{
- // cur:desired:width then per-bucket coverage bounds
- // (N/BC/S/E, sqrt so values read as pixel dimensions,
- // max~min) - the exact inputs computeDesiredDiscard sees.
- debug_text << img->getDiscardLevel() << ":" << img->getDesiredDiscardLevel() << ":" << img->getWidth()
- << " N" << (S32)sqrtf(img->getChannelCoverage(0)) << "~" << (S32)sqrtf(img->getChannelCoverageMin(0))
- << " BC" << (S32)sqrtf(img->getChannelCoverage(1)) << "~" << (S32)sqrtf(img->getChannelCoverageMin(1))
- << " S" << (S32)sqrtf(img->getChannelCoverage(2)) << "~" << (S32)sqrtf(img->getChannelCoverageMin(2))
- << " E" << (S32)sqrtf(img->getChannelCoverage(3)) << "~" << (S32)sqrtf(img->getChannelCoverageMin(3))
- << "\n";
+ debug_text << img->getDiscardLevel() << ":" << img->getDesiredDiscardLevel() << ":" << img->getWidth() << ":" << (S32) sqrtf(vsize) << ":" << (S32) sqrtf(img->getMaxVirtualSize()) << "\n";
+ /*F32 pri = img->getDecodePriority();
+ pri = llmax(pri, 0.0f);
+ if (pri < min_vsize) min_vsize = pri;
+ if (pri > max_vsize) max_vsize = pri;*/
}
}
else if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_FACE_AREA))
@@ -932,7 +928,7 @@ void LLVOVolume::updateTextureVirtualSize(bool forced)
{
LLLightImageParams* params = getLightImageParams();
LLUUID id = params->getLightTexture();
- mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
+ mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE);
if (mLightTexture.notNull())
{
F32 rad = getLightRadius();
@@ -1801,41 +1797,6 @@ void LLVOVolume::regenFaces()
facep->setNormalMap(getTENormalMap(i));
facep->setSpecularMap(getTESpecularMap(i));
}
-
- // Register PBR channel textures HERE, at geometry build, exactly when
- // the Blinn textures above register - mirrored from rebuildGeom
- // (which still re-runs this when the material resolves later).
- // Without this, PBR textures had zero registered faces until the
- // spatial group's budget-throttled rebuildGeom ran, so the streaming
- // math read "not measured -> deepest mip" for PBR content while
- // identical Blinn content on the same geometry measured immediately.
- {
- const LLTextureEntry* te = facep->getTextureEntry();
- LLFetchedGLTFMaterial* gltf_mat = te ? (LLFetchedGLTFMaterial*)te->getGLTFRenderMaterial() : nullptr;
- if (gltf_mat)
- {
- if (!facep->hasMedia())
- {
- facep->setTexture(LLRender::DIFFUSE_MAP, nullptr);
- }
- facep->setTexture(LLRender::NORMAL_MAP, nullptr);
- facep->setTexture(LLRender::SPECULAR_MAP, nullptr);
- facep->setTexture(LLRender::BASECOLOR_MAP, gltf_mat->mBaseColorTexture);
- facep->setTexture(LLRender::GLTF_NORMAL_MAP, gltf_mat->mNormalTexture);
- facep->setTexture(LLRender::METALLIC_ROUGHNESS_MAP, gltf_mat->mMetallicRoughnessTexture);
- facep->setTexture(LLRender::EMISSIVE_MAP, gltf_mat->mEmissiveTexture);
- }
- else
- {
- // No (or no longer a) PBR material: clear any stale GLTF
- // channel registrations so a removed material's textures
- // stop accruing phantom coverage from this face.
- facep->setTexture(LLRender::BASECOLOR_MAP, nullptr);
- facep->setTexture(LLRender::GLTF_NORMAL_MAP, nullptr);
- facep->setTexture(LLRender::METALLIC_ROUGHNESS_MAP, nullptr);
- facep->setTexture(LLRender::EMISSIVE_MAP, nullptr);
- }
- }
facep->setViewerObject(this);
// If the face had media on it, this will have broken the link between the LLViewerMediaTexture and the face.
@@ -3406,7 +3367,7 @@ LLViewerTexture* LLVOVolume::getLightTexture()
{
if (mLightTexture.isNull() || id != mLightTexture->getID())
{
- mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
+ mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE);
}
}
else
@@ -5890,16 +5851,6 @@ void LLVolumeGeometryManager::rebuildGeom(LLSpatialGroup* group)
facep->setTexture(LLRender::METALLIC_ROUGHNESS_MAP, gltf_mat->mMetallicRoughnessTexture);
facep->setTexture(LLRender::EMISSIVE_MAP, gltf_mat->mEmissiveTexture);
}
- else
- {
- // Face is not (or no longer) PBR: clear any stale GLTF
- // channel registrations, or a removed material's textures
- // keep accruing phantom coverage from this face forever.
- facep->setTexture(LLRender::BASECOLOR_MAP, nullptr);
- facep->setTexture(LLRender::GLTF_NORMAL_MAP, nullptr);
- facep->setTexture(LLRender::METALLIC_ROUGHNESS_MAP, nullptr);
- facep->setTexture(LLRender::EMISSIVE_MAP, nullptr);
- }
//ALWAYS null out vertex buffer on rebuild -- if the face lands in a render
// batch, it will recover its vertex buffer reference from the spatial group
diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp
index f18ec727df..4ef88f5deb 100644
--- a/indra/newview/pipeline.cpp
+++ b/indra/newview/pipeline.cpp
@@ -9461,9 +9461,6 @@ void LLPipeline::renderShadow(const glm::mat4& view, const glm::mat4& proj, LLCa
LL_PROFILE_GPU_ZONE("renderShadow");
LLPipeline::sShadowRender = true;
- // Shadow binds aren't visibility. RAII (not a hardcoded restore) so a shadow
- // pass nested in a probe render doesn't re-enable stamping for the rest of it.
- LLImageGLStampBypass stamp_bypass;
// disable occlusion culling during shadow render
U32 saved_occlusion = sUseOcclusion;
@@ -10848,9 +10845,6 @@ void LLPipeline::generateImpostor(LLVOAvatar* avatar, bool preview_avatar, bool
sShadowRender = true;
sImpostorRender = true;
- // Impostor binds aren't visibility. RAII so the nested shadow pass can't
- // re-enable stamping mid-render.
- LLImageGLStampBypass stamp_bypass;
LLViewerCamera* viewer_camera = LLViewerCamera::getInstance();