summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorErik Kundiman <erik@megapahit.org>2026-07-07 20:49:46 +0800
committerErik Kundiman <erik@megapahit.org>2026-07-07 20:49:46 +0800
commitc659a085f05f5a9f62abf8a4c2e394d910af4ae9 (patch)
tree4e18706c29a8d0d95078c438b67b2e7a81f24005
parent2121d3b20269baeb8bb88cef534307628f1adb2e (diff)
parent3d465f6c966f0a2fcfd5d3c9effb6ce20c2754c8 (diff)
Merge tag 'Second_Life_Release#3d465f6c-26.3' into 26.3
-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/llfloatersimplesnapshot.cpp197
-rw-r--r--indra/newview/llpanelprofile.cpp158
-rw-r--r--indra/newview/llsidepanelinventory.cpp2
-rw-r--r--indra/newview/lltextureview.cpp13
-rw-r--r--indra/newview/llviewerassetupload.cpp95
-rw-r--r--indra/newview/llviewercontrol.cpp88
-rw-r--r--indra/newview/llviewerdisplay.cpp5
-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/llvocache.cpp6
-rw-r--r--indra/newview/llvovolume.cpp63
-rw-r--r--indra/newview/pipeline.cpp6
27 files changed, 1151 insertions, 1314 deletions
diff --git a/indra/llprimitive/lltextureentry.cpp b/indra/llprimitive/lltextureentry.cpp
index ac482ffbf9..2b0f989701 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 a40cb14f17..eb86f5bae2 100644
--- a/indra/llrender/llimagegl.cpp
+++ b/indra/llrender/llimagegl.cpp
@@ -166,6 +166,8 @@ 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;
@@ -526,7 +528,7 @@ bool LLImageGL::create(LLPointer<LLImageGL>& dest, const LLImageRaw* imageraw, b
//----------------------------------------------------------------------------
LLImageGL::LLImageGL(bool usemipmaps/* = true*/, bool allow_compression/* = true*/)
-: mSaveData(0), mExternalTexture(false)
+: mExternalTexture(false)
{
init(usemipmaps, allow_compression);
setSize(0, 0, 0);
@@ -535,7 +537,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*/)
-: mSaveData(0), mExternalTexture(false)
+: mExternalTexture(false)
{
llassert( components <= 4 );
init(usemipmaps, allow_compression);
@@ -545,7 +547,7 @@ LLImageGL::LLImageGL(U32 width, U32 height, U8 components, bool usemipmaps/* = t
}
LLImageGL::LLImageGL(const LLImageRaw* imageraw, bool usemipmaps/* = true*/, bool allow_compression/* = true*/)
-: mSaveData(0), mExternalTexture(false)
+: mExternalTexture(false)
{
init(usemipmaps, allow_compression);
setSize(0, 0, 0);
@@ -654,8 +656,6 @@ void LLImageGL::cleanup()
destroyGLTexture();
}
freePickMask();
-
- mSaveData = NULL; // deletes data
}
//----------------------------------------------------------------------------
@@ -812,6 +812,7 @@ 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();
@@ -1591,6 +1592,12 @@ 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)
{
@@ -1600,19 +1607,30 @@ 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);
+ }
- U8* src = (U8*)(pixels);
- if (src)
+ 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)
{
- LL_PROFILE_ZONE_NAMED("glTexImage2D copy");
- sub_image_lines(target, miplevel, 0, 0, width, height, pixformat, pixtype, src, width);
+ 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);
+ }
}
+ alloc_tex_image(width, height, intformat, 1);
}
- alloc_tex_image(width, height, intformat, 1);
}
stop_glerror();
}
@@ -1765,6 +1783,7 @@ 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();
@@ -2191,12 +2210,21 @@ S32 LLImageGL::dimDerivedMaxDiscard(S32 width, S32 height)
void LLImageGL::stampBound() const
{
- // Skip the store on same-frame re-binds - bindFast is per-draw and
- // would dirty this cache line per bind per texture otherwise.
+ // 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.
if (mLastBindTime != sLastFrameTime)
{
mLastBindTime = sLastFrameTime;
}
+ if (sStampBindFrame)
+ {
+ const U32 frame = LLFrameTimer::getFrameCount();
+ if (mLastBindFrame != frame)
+ {
+ mLastBindFrame = frame;
+ }
+ }
}
S64 LLImageGL::getBytes(S32 discard_level) const
@@ -2637,6 +2665,10 @@ 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 0c85446b84..ca75b543c0 100644
--- a/indra/llrender/llimagegl.h
+++ b/indra/llrender/llimagegl.h
@@ -39,6 +39,7 @@
#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
@@ -238,15 +239,23 @@ public:
public:
// Various GL/Rendering options
S64Bytes mTextureMemory;
- 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
+ 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;
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;
@@ -300,6 +309,11 @@ 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;
@@ -355,6 +369,19 @@ 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 6df5a80748..348cb867dd 100644
--- a/indra/llrender/llrender.cpp
+++ b/indra/llrender/llrender.cpp
@@ -253,6 +253,11 @@ 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)
{
@@ -333,6 +338,8 @@ 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 b9b48544ff..6dfcc10243 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -8175,17 +8175,6 @@
<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>1</integer>
- <key>Type</key>
- <string>U32</string>
- <key>Value</key>
- <integer>256</integer>
- </map>
<key>RenderLowMemMinDiscardIncrement</key>
<map>
<key>Comment</key>
@@ -8208,17 +8197,6 @@
<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>
@@ -8233,7 +8211,7 @@
<key>RenderTextureQuality</key>
<map>
<key>Comment</key>
- <string>Texture quality preset: 0=Low, 1=Medium, 2=High, 3=Ultra. Drives RenderMaxTextureResolution, the four TextureChannel* exponents, and TextureDistanceDiscardPower.</string>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
@@ -8252,17 +8230,6 @@
<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>
@@ -9579,17 +9546,6 @@
<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>
@@ -12132,10 +12088,10 @@
<key>Value</key>
<real>20.0</real>
</map>
- <key>TextureChannelNormal</key>
+ <key>TextureChannelRatioNormal</key>
<map>
<key>Comment</key>
- <string>Per-channel discard exponent for normal maps. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset.</string>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
@@ -12143,21 +12099,21 @@
<key>Value</key>
<real>1.0</real>
</map>
- <key>TextureChannelBaseColor</key>
+ <key>TextureChannelRatioBaseColor</key>
<map>
<key>Comment</key>
- <string>Per-channel discard exponent for base color / diffuse. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset.</string>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>0.75</real>
+ <real>1.0</real>
</map>
- <key>TextureChannelSpecular</key>
+ <key>TextureChannelRatioSpecular</key>
<map>
<key>Comment</key>
- <string>Per-channel discard exponent for specular / metallic-roughness. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset.</string>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
@@ -12165,16 +12121,16 @@
<key>Value</key>
<real>0.5</real>
</map>
- <key>TextureChannelEmissive</key>
+ <key>TextureChannelRatioEmissive</key>
<map>
<key>Comment</key>
- <string>Per-channel discard exponent for emissive. 1.0 = baseline; lower = more aggressive. Driven by the RenderTextureQuality preset.</string>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>0.75</real>
+ <real>0.5</real>
</map>
<key>TextureMaxDiscardOverride</key>
<map>
@@ -12187,165 +12143,87 @@
<key>Value</key>
<integer>0</integer>
</map>
- <key>TextureMemoryHighWaterMark</key>
- <map>
- <key>Comment</key>
- <string>Fraction of budget (0..1) at which the pressure controller bypasses smoothing and slams to cap. Last-ditch min-discard also creeps without waiting for mult_progress.</string>
- <key>Persist</key>
- <integer>1</integer>
- <key>Type</key>
- <string>F32</string>
- <key>Value</key>
- <real>0.8</real>
- </map>
- <key>TextureMemoryPressureBackoffStart</key>
- <map>
- <key>Comment</key>
- <string>Fraction of the VRAM target at which the pressure ramp starts (0..1). Lower = earlier headroom-building; 1.0 disables backoff (ramp only above target).</string>
- <key>Persist</key>
- <integer>1</integer>
- <key>Type</key>
- <string>F32</string>
- <key>Value</key>
- <real>0.85</real>
- </map>
- <key>TextureMemoryPressureMaxMultiplier</key>
- <map>
- <key>Comment</key>
- <string>Upper bound on the VRAM-pressure distance multiplier (&gt;= 1). Mostly defensive -- at mult=64 the streaming ramp collapses to ~ramp_range/64, already extreme. Higher allows even more aggressive compression in tight-budget scenes.</string>
- <key>Persist</key>
- <integer>1</integer>
- <key>Type</key>
- <string>F32</string>
- <key>Value</key>
- <real>64.0</real>
- </map>
- <key>TextureLastDitchEngageProgress</key>
- <map>
- <key>Comment</key>
- <string>mult_progress (0..1) at which the last-ditch floor starts creeping up. The floor only advances when mult is at or above this fraction of its cap AND prediction is still over budget. Decays back toward 0 whenever prediction is under budget.</string>
- <key>Persist</key>
- <integer>1</integer>
- <key>Type</key>
- <string>F32</string>
- <key>Value</key>
- <real>0.95</real>
- </map>
- <key>TextureLastDitchRampRate</key>
- <map>
- <key>Comment</key>
- <string>Rate (discard levels/sec) at which sLastDitchMinDiscard creeps up while engaged. 0.5 = takes ~2 sec to add one discard level. Mirrors sDesiredDiscardBias ramp shape.</string>
- <key>Persist</key>
- <integer>1</integer>
- <key>Type</key>
- <string>F32</string>
- <key>Value</key>
- <real>0.5</real>
- </map>
- <key>TextureLastDitchDecayRate</key>
- <map>
- <key>Comment</key>
- <string>Rate (discard levels/sec) at which sLastDitchMinDiscard decays back to 0 when prediction is under budget.</string>
- <key>Persist</key>
- <integer>1</integer>
- <key>Type</key>
- <string>F32</string>
- <key>Value</key>
- <real>0.5</real>
- </map>
- <key>TextureLastDitchMinDiscardMax</key>
+ <key>TexturePixelToTexelRatio</key>
<map>
<key>Comment</key>
- <string>Hard ceiling on sLastDitchMinDiscard. At 13 the floor can climb all the way to the deepest meaningful mip; lower values cap how aggressive the last-ditch escalation can get before we are simply out of discards.</string>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>13.0</real>
+ <real>1.0</real>
</map>
- <key>TextureMemoryPressurePredictionGain</key>
+ <key>TextureBackgroundMinRatio</key>
<map>
<key>Comment</key>
- <string>Power exponent mapping predicted-over-budget ratio to target multiplier. target_mult = pred_over^gain. Higher gain saturates faster.</string>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>10.0</real>
+ <real>0.001</real>
</map>
- <key>TextureMemoryPressureSmoothingRate</key>
+ <key>TexturePressureHighWater</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>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>4.0</real>
+ <real>0.90</real>
</map>
- <key>TextureTerrainDistanceFloor</key>
+ <key>TexturePressureLowWater</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>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>0.01</real>
+ <real>0.70</real>
</map>
- <key>TextureTerrainCoverageFraction</key>
+ <key>TexturePressureTightenRate</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>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>0.99</real>
+ <real>0.30</real>
</map>
- <key>TextureAgentAvatarBoost</key>
+ <key>TexturePressureRelaxRate</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>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>0.5</real>
+ <real>0.10</real>
</map>
- <key>TextureBackgroundFactorRatePerSec</key>
+ <key>TextureUpRezMargin</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>
+ <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>
<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>
+ <real>0.2</real>
</map>
-
- <key>TextureCloseBubbleMeters</key>
+ <key>TextureCooldownStepSeconds</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>
+ <string>While backgrounded, the pixel:texel ratio drops one mip toward TextureBackgroundMinRatio every this many seconds.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
@@ -12353,95 +12231,39 @@
<key>Value</key>
<real>5.0</real>
</map>
- <key>TextureCloseBubbleMinMeters</key>
+ <key>TextureGCStepFrames</key>
<map>
<key>Comment</key>
- <string>Floor (meters) for the close-camera bubble under maximum VRAM pressure. At sMemoryPressureMultiplier = TextureMemoryPressureMaxMultiplier the bubble collapses to this value, allowing eviction of even close textures when nothing else fits.</string>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
- <string>F32</string>
- <key>Value</key>
- <real>3.0</real>
- </map>
- <key>TextureCloseBubbleShrinkThreshold</key>
- <map>
- <key>Comment</key>
- <string>Bubble stays at full size until mult_progress exceeds this fraction (0..1) of its range to the cap. Above that, bubble lerps from full to TextureCloseBubbleMinMeters. Keeps the bubble out of the normal feedback loop.</string>
- <key>Persist</key>
- <integer>1</integer>
- <key>Type</key>
- <string>F32</string>
- <key>Value</key>
- <real>0.8</real>
- </map>
- <key>TextureCloseBubbleTrackRate</key>
- <map>
- <key>Comment</key>
- <string>Rate (1/sec) at which the actual bubble tracks its target. Lower = smoother, slower to react. Damps short-term multiplier swings so close textures don't yo-yo.</string>
- <key>Persist</key>
- <integer>1</integer>
- <key>Type</key>
- <string>F32</string>
- <key>Value</key>
- <real>0.5</real>
- </map>
- <key>TextureDistanceDiscardPower</key>
- <map>
- <key>Comment</key>
- <string>Exponent on the distance factor (face_distance / draw_distance). 1.0 = linear; lower = textures hit max discard sooner with distance. Default 0.5 = sqrt.</string>
- <key>Persist</key>
- <integer>1</integer>
- <key>Type</key>
- <string>F32</string>
+ <string>U32</string>
<key>Value</key>
- <real>0.5</real>
+ <integer>5</integer>
</map>
- <key>TextureSizeDiscardPower</key>
+ <key>TextureGCStepMips</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>
+ <string>Mip levels dropped each time a TextureGCStepFrames cooldown elapses. 1 is gentlest; higher sheds VRAM faster in coarser jumps.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
- <string>F32</string>
+ <string>U32</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>
+ <key>TextureFetchVisibilityFrames</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>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
- <string>F32</string>
+ <string>U32</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>
@@ -12475,28 +12297,6 @@
<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>
@@ -12585,27 +12385,27 @@
<key>Value</key>
<string />
</map>
- <key>TextureScaleMinAreaFactor</key>
+ <key>TextureAvatarBoost</key>
<map>
<key>Comment</key>
- <string>Limits how texture scale affects area calculation.</string>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>0.0095</real>
+ <real>4.0</real>
</map>
- <key>TextureScaleMaxAreaFactor</key>
+ <key>TextureDownrezCoverageBias</key>
<map>
<key>Comment</key>
- <string>Limits how texture scale affects area calculation.</string>
+ <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>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>25.0</real>
+ <real>0.25</real>
</map>
<key>ThreadPoolSizes</key>
<map>
diff --git a/indra/newview/featuretable.txt b/indra/newview/featuretable.txt
index f05f77c222..72e0f43c0e 100644
--- a/indra/newview/featuretable.txt
+++ b/indra/newview/featuretable.txt
@@ -68,7 +68,6 @@ 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 f97ff28062..7807f23db3 100644
--- a/indra/newview/featuretable_linux.txt
+++ b/indra/newview/featuretable_linux.txt
@@ -67,7 +67,6 @@ RenderDeferredSSAO 1 1
RenderUseAdvancedAtmospherics 1 0
RenderShadowDetail 1 2
RenderFSAASamples 1 16
-RenderMaxTextureIndex 1 16
RenderMirrors 1 1
//
@@ -500,7 +499,6 @@ 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 4e84f24533..f78fa36c6e 100644
--- a/indra/newview/featuretable_mac.txt
+++ b/indra/newview/featuretable_mac.txt
@@ -65,7 +65,6 @@ 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 a05f725673..d71f0c1bd4 100644
--- a/indra/newview/llfetchedgltfmaterial.cpp
+++ b/indra/newview/llfetchedgltfmaterial.cpp
@@ -73,6 +73,20 @@ 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)
@@ -97,13 +111,26 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex)
if (!LLPipeline::sShadowRender)
{
- if (mNormalTexture.notNull() && mNormalTexture->getDiscardLevel() <= 4)
+ // 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())
{
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/llfloatersimplesnapshot.cpp b/indra/newview/llfloatersimplesnapshot.cpp
index 55b39d9193..4671f74d6d 100644
--- a/indra/newview/llfloatersimplesnapshot.cpp
+++ b/indra/newview/llfloatersimplesnapshot.cpp
@@ -54,87 +54,162 @@ void post_thumbnail_image_coro(std::string cap_url, std::string path_to_image, L
{
LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t
- httpAdapter = std::make_shared<LLCoreHttpUtil::HttpCoroutineAdapter>("post_profile_image_coro", httpPolicy);
+ httpAdapter = std::make_shared<LLCoreHttpUtil::HttpCoroutineAdapter>("post_thumbnail_image_coro", httpPolicy);
LLCore::HttpRequest::ptr_t httpRequest = std::make_shared<LLCore::HttpRequest>();
LLCore::HttpHeaders::ptr_t httpHeaders;
LLCore::HttpOptions::ptr_t httpOpts = std::make_shared<LLCore::HttpOptions>();
httpOpts->setFollowRedirects(true);
- LLSD result = httpAdapter->postAndSuspend(httpRequest, cap_url, first_data, httpOpts, httpHeaders);
+ // Retry stage-2 upload by re-requesting a fresh one-time uploader capability (up to 3 attempts total)
+ const S32 MAX_UPLOAD_RETRIES = 2;
+ S32 upload_retry_count = 0;
+ LLUUID result_uuid;
- LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
- LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
-
- if (!status)
- {
- // todo: notification?
- LL_WARNS("AvatarProperties") << "Failed to get uploader cap " << status.toString() << LL_ENDL;
- return;
- }
- if (!result.has("uploader"))
+ while (upload_retry_count <= MAX_UPLOAD_RETRIES)
{
- // todo: notification?
- LL_WARNS("AvatarProperties") << "Failed to get uploader cap, response contains no data." << LL_ENDL;
- return;
- }
- std::string uploader_cap = result["uploader"].asString();
- if (uploader_cap.empty())
- {
- LL_WARNS("AvatarProperties") << "Failed to get uploader cap, cap invalid." << LL_ENDL;
- return;
- }
+ // Stage 1: Request uploader URL
+ LLSD result = httpAdapter->postAndSuspend(httpRequest, cap_url, first_data, httpOpts, httpHeaders);
- // Upload the image
+ LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
- LLCore::HttpRequest::ptr_t uploaderhttpRequest = std::make_shared<LLCore::HttpRequest>();
- LLCore::HttpHeaders::ptr_t uploaderhttpHeaders = std::make_shared<LLCore::HttpHeaders>();
- LLCore::HttpOptions::ptr_t uploaderhttpOpts = std::make_shared<LLCore::HttpOptions>();
- S64 length;
+ if (!status)
+ {
+ LL_WARNS("Thumbnail") << "Failed to get uploader cap " << status.toString() << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID());
+ }
+ LLFile::remove(path_to_image);
+ return;
+ }
- {
- llifstream instream(path_to_image.c_str(), std::iostream::binary | std::iostream::ate);
- if (!instream.is_open())
+ if (!result.has("uploader"))
{
- LL_WARNS("AvatarProperties") << "Failed to open file " << path_to_image << LL_ENDL;
+ LL_WARNS("Thumbnail") << "Failed to get uploader cap, response contains no data." << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID());
+ }
+ LLFile::remove(path_to_image);
return;
}
- length = instream.tellg();
- }
- uploaderhttpHeaders->append(HTTP_OUT_HEADER_CONTENT_TYPE, "application/jp2"); // optional
- uploaderhttpHeaders->append(HTTP_OUT_HEADER_CONTENT_LENGTH, llformat("%d", length)); // required!
- uploaderhttpOpts->setFollowRedirects(true);
+ std::string uploader_cap = result["uploader"].asString();
+ if (uploader_cap.empty())
+ {
+ LL_WARNS("Thumbnail") << "Failed to get uploader cap, cap invalid." << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID());
+ }
+ LLFile::remove(path_to_image);
+ return;
+ }
- result = httpAdapter->postFileAndSuspend(uploaderhttpRequest, uploader_cap, path_to_image, uploaderhttpOpts, uploaderhttpHeaders);
+ // Stage 2: Upload the image
+ LLCore::HttpRequest::ptr_t uploaderhttpRequest = std::make_shared<LLCore::HttpRequest>();
+ LLCore::HttpHeaders::ptr_t uploaderhttpHeaders = std::make_shared<LLCore::HttpHeaders>();
+ LLCore::HttpOptions::ptr_t uploaderhttpOpts = std::make_shared<LLCore::HttpOptions>();
+ S64 length;
- httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
- status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+ {
+ llifstream instream(path_to_image.c_str(), std::iostream::binary | std::iostream::ate);
+ if (!instream.is_open())
+ {
+ LL_WARNS("Thumbnail") << "Failed to open file " << path_to_image << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID());
+ }
+ LLFile::remove(path_to_image);
+ return;
+ }
+ length = instream.tellg();
+ }
- LL_DEBUGS("Thumbnail") << result << LL_ENDL;
+ uploaderhttpHeaders->append(HTTP_OUT_HEADER_CONTENT_TYPE, "application/jp2");
+ uploaderhttpHeaders->append(HTTP_OUT_HEADER_CONTENT_LENGTH, std::to_string(length));
+ uploaderhttpOpts->setFollowRedirects(true);
- if (!status)
- {
- LL_WARNS("Thumbnail") << "Failed to upload image " << status.toString() << LL_ENDL;
- return;
- }
+ result = httpAdapter->postFileAndSuspend(uploaderhttpRequest, uploader_cap, path_to_image, uploaderhttpOpts, uploaderhttpHeaders);
- if (result["state"].asString() != "complete")
- {
- if (result.has("message"))
- {
- LL_WARNS("Thumbnail") << "Failed to upload image, state " << result["state"] << " message: " << result["message"] << LL_ENDL;
- }
- else
+ httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+
+ LL_DEBUGS("Thumbnail") << result << LL_ENDL;
+
+ if (!status)
{
- LL_WARNS("Thumbnail") << "Failed to upload image " << result << LL_ENDL;
+ if (upload_retry_count < MAX_UPLOAD_RETRIES)
+ {
+ upload_retry_count++;
+ LL_WARNS("Thumbnail") << "Failed to upload image (attempt " << upload_retry_count
+ << " of " << (MAX_UPLOAD_RETRIES + 1) << "): " << status.toString()
+ << ", re-requesting uploader..." << LL_ENDL;
+ llcoro::suspendUntilTimeout(1.0f);
+ continue;
+ }
+ else
+ {
+ LL_WARNS("Thumbnail") << "Failed to upload image after " << (MAX_UPLOAD_RETRIES + 1)
+ << " attempts: " << status.toString() << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID());
+ }
+ LLFile::remove(path_to_image);
+ return;
+ }
}
- if (callback)
+ if (result["state"].asString() != "complete")
{
- callback(LLUUID());
+ if (upload_retry_count < MAX_UPLOAD_RETRIES)
+ {
+ upload_retry_count++;
+ if (result.has("message"))
+ {
+ LL_WARNS("Thumbnail") << "Failed to upload image, state " << result["state"]
+ << " message: " << result["message"] << " (attempt "
+ << upload_retry_count << " of " << (MAX_UPLOAD_RETRIES + 1)
+ << "), re-requesting uploader..." << LL_ENDL;
+ }
+ else
+ {
+ LL_WARNS("Thumbnail") << "Failed to upload image (attempt " << upload_retry_count
+ << " of " << (MAX_UPLOAD_RETRIES + 1)
+ << "), re-requesting uploader..." << LL_ENDL;
+ }
+ llcoro::suspendUntilTimeout(1.0f);
+ continue;
+ }
+ else
+ {
+ if (result.has("message"))
+ {
+ LL_WARNS("Thumbnail") << "Failed to upload image after " << (MAX_UPLOAD_RETRIES + 1)
+ << " attempts, state " << result["state"]
+ << " message: " << result["message"] << LL_ENDL;
+ }
+ else
+ {
+ LL_WARNS("Thumbnail") << "Failed to upload image after " << (MAX_UPLOAD_RETRIES + 1)
+ << " attempts" << LL_ENDL;
+ }
+ if (callback)
+ {
+ callback(LLUUID());
+ }
+ LLFile::remove(path_to_image);
+ return;
+ }
}
- return;
+
+ // Success!
+ result_uuid = result["new_asset"].asUUID();
+ break;
}
if (first_data.has("category_id"))
@@ -143,7 +218,7 @@ void post_thumbnail_image_coro(std::string cap_url, std::string path_to_image, L
LLViewerInventoryCategory* cat = gInventory.getCategory(cat_id);
if (cat)
{
- cat->setThumbnailUUID(result["new_asset"].asUUID());
+ cat->setThumbnailUUID(result_uuid);
}
gInventory.addChangedMask(LLInventoryObserver::INTERNAL, cat_id);
}
@@ -153,16 +228,18 @@ void post_thumbnail_image_coro(std::string cap_url, std::string path_to_image, L
LLViewerInventoryItem* item = gInventory.getItem(item_id);
if (item)
{
- item->setThumbnailUUID(result["new_asset"].asUUID());
+ item->setThumbnailUUID(result_uuid);
}
- // Are we supposed to get BulkUpdateInventory?
gInventory.addChangedMask(LLInventoryObserver::INTERNAL, item_id);
}
if (callback)
{
- callback(result["new_asset"].asUUID());
+ callback(result_uuid);
}
+
+ // Cleanup
+ LLFile::remove(path_to_image);
}
///----------------------------------------------------------------------------
diff --git a/indra/newview/llpanelprofile.cpp b/indra/newview/llpanelprofile.cpp
index 758ca0b0fa..3d8e5575ae 100644
--- a/indra/newview/llpanelprofile.cpp
+++ b/indra/newview/llpanelprofile.cpp
@@ -113,77 +113,131 @@ LLUUID post_profile_image(std::string cap_url, const LLSD &first_data, std::stri
LLCore::HttpOptions::ptr_t httpOpts = std::make_shared<LLCore::HttpOptions>();
httpOpts->setFollowRedirects(true);
- LLSD result = httpAdapter->postAndSuspend(httpRequest, cap_url, first_data, httpOpts, httpHeaders);
+ // Retry stage-2 upload by re-requesting a fresh one-time uploader capability (up to 3 attempts total)
+ const S32 MAX_UPLOAD_RETRIES = 2;
+ S32 upload_retry_count = 0;
+ LLUUID result_uuid;
- LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
- LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
-
- if (!status)
- {
- // todo: notification?
- LL_WARNS("AvatarProperties") << "Failed to get uploader cap " << status.toString() << LL_ENDL;
- return LLUUID::null;
- }
- if (!result.has("uploader"))
+ while (upload_retry_count <= MAX_UPLOAD_RETRIES)
{
- // todo: notification?
- LL_WARNS("AvatarProperties") << "Failed to get uploader cap, response contains no data." << LL_ENDL;
- return LLUUID::null;
- }
- std::string uploader_cap = result["uploader"].asString();
- if (uploader_cap.empty())
- {
- LL_WARNS("AvatarProperties") << "Failed to get uploader cap, cap invalid." << LL_ENDL;
- return LLUUID::null;
- }
+ // Stage 1: Request uploader URL
+ LLSD result = httpAdapter->postAndSuspend(httpRequest, cap_url, first_data, httpOpts, httpHeaders);
- // Upload the image
- LLCore::HttpRequest::ptr_t uploaderhttpRequest = std::make_shared<LLCore::HttpRequest>();
- LLCore::HttpHeaders::ptr_t uploaderhttpHeaders = std::make_shared<LLCore::HttpHeaders>();
- LLCore::HttpOptions::ptr_t uploaderhttpOpts = std::make_shared<LLCore::HttpOptions>();
- S64 length;
+ LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
- {
- llifstream instream(path_to_image.c_str(), std::iostream::binary | std::iostream::ate);
- if (!instream.is_open())
+ if (!status)
{
- LL_WARNS("AvatarProperties") << "Failed to open file " << path_to_image << LL_ENDL;
+ // todo: notification?
+ LL_WARNS("AvatarProperties") << "Failed to get uploader cap " << status.toString() << LL_ENDL;
+ return LLUUID::null;
+ }
+
+ if (!result.has("uploader"))
+ {
+ // todo: notification?
+ LL_WARNS("AvatarProperties") << "Failed to get uploader cap, response contains no data." << LL_ENDL;
return LLUUID::null;
}
- length = instream.tellg();
- }
- uploaderhttpHeaders->append(HTTP_OUT_HEADER_CONTENT_TYPE, "application/jp2"); // optional
- uploaderhttpHeaders->append(HTTP_OUT_HEADER_CONTENT_LENGTH, llformat("%d", length)); // required!
- uploaderhttpOpts->setFollowRedirects(true);
+ std::string uploader_cap = result["uploader"].asString();
+ if (uploader_cap.empty())
+ {
+ LL_WARNS("AvatarProperties") << "Failed to get uploader cap, cap invalid." << LL_ENDL;
+ return LLUUID::null;
+ }
+
+ // Stage 2: Upload the image
+ LLCore::HttpRequest::ptr_t uploaderhttpRequest = std::make_shared<LLCore::HttpRequest>();
+ LLCore::HttpHeaders::ptr_t uploaderhttpHeaders = std::make_shared<LLCore::HttpHeaders>();
+ LLCore::HttpOptions::ptr_t uploaderhttpOpts = std::make_shared<LLCore::HttpOptions>();
+ S64 length;
+
+ {
+ llifstream instream(path_to_image.c_str(), std::iostream::binary | std::iostream::ate);
+ if (!instream.is_open())
+ {
+ LL_WARNS("AvatarProperties") << "Failed to open file " << path_to_image << LL_ENDL;
+ return LLUUID::null;
+ }
+ length = instream.tellg();
+ }
- result = httpAdapter->postFileAndSuspend(uploaderhttpRequest, uploader_cap, path_to_image, uploaderhttpOpts, uploaderhttpHeaders);
+ uploaderhttpHeaders->append(HTTP_OUT_HEADER_CONTENT_TYPE, "application/jp2");
+ uploaderhttpHeaders->append(HTTP_OUT_HEADER_CONTENT_LENGTH, std::to_string(length));
+ uploaderhttpOpts->setFollowRedirects(true);
- httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
- status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+ result = httpAdapter->postFileAndSuspend(uploaderhttpRequest, uploader_cap, path_to_image, uploaderhttpOpts, uploaderhttpHeaders);
- LL_DEBUGS("AvatarProperties") << result << LL_ENDL;
+ httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
- if (!status)
- {
- LL_WARNS("AvatarProperties") << "Failed to upload image " << status.toString() << LL_ENDL;
- return LLUUID::null;
- }
+ LL_DEBUGS("AvatarProperties") << result << LL_ENDL;
- if (result["state"].asString() != "complete")
- {
- if (result.has("message"))
+ if (!status)
{
- LL_WARNS("AvatarProperties") << "Failed to upload image, state " << result["state"] << " message: " << result["message"] << LL_ENDL;
+ if (upload_retry_count < MAX_UPLOAD_RETRIES)
+ {
+ upload_retry_count++;
+ LL_WARNS("AvatarProperties") << "Failed to upload image (attempt " << upload_retry_count
+ << " of " << (MAX_UPLOAD_RETRIES + 1) << "): " << status.toString()
+ << ", re-requesting uploader..." << LL_ENDL;
+ llcoro::suspendUntilTimeout(1.0f);
+ continue;
+ }
+ else
+ {
+ LL_WARNS("AvatarProperties") << "Failed to upload image after " << (MAX_UPLOAD_RETRIES + 1)
+ << " attempts: " << status.toString() << LL_ENDL;
+ return LLUUID::null;
+ }
}
- else
+
+ // Todo: should we really repeat if 'complete' not set?
+ if (result["state"].asString() != "complete")
{
- LL_WARNS("AvatarProperties") << "Failed to upload image " << result << LL_ENDL;
+ if (upload_retry_count < MAX_UPLOAD_RETRIES)
+ {
+ upload_retry_count++;
+ if (result.has("message"))
+ {
+ LL_WARNS("AvatarProperties") << "Failed to upload image, state " << result["state"]
+ << " message: " << result["message"] << " (attempt "
+ << upload_retry_count << " of " << (MAX_UPLOAD_RETRIES + 1)
+ << "), re-requesting uploader..." << LL_ENDL;
+ }
+ else
+ {
+ LL_WARNS("AvatarProperties") << "Failed to upload image (attempt " << upload_retry_count
+ << " of " << (MAX_UPLOAD_RETRIES + 1)
+ << "), re-requesting uploader..." << LL_ENDL;
+ }
+ llcoro::suspendUntilTimeout(1.0f);
+ continue;
+ }
+ else
+ {
+ if (result.has("message"))
+ {
+ LL_WARNS("AvatarProperties") << "Failed to upload image after " << (MAX_UPLOAD_RETRIES + 1)
+ << " attempts, state " << result["state"]
+ << " message: " << result["message"] << LL_ENDL;
+ }
+ else
+ {
+ LL_WARNS("AvatarProperties") << "Failed to upload image after " << (MAX_UPLOAD_RETRIES + 1)
+ << " attempts" << LL_ENDL;
+ }
+ return LLUUID::null;
+ }
}
- return LLUUID::null;
+
+ // Success!
+ result_uuid = result["new_asset"].asUUID();
+ break;
}
- return result["new_asset"].asUUID();
+ return result_uuid;
}
enum EProfileImageType
diff --git a/indra/newview/llsidepanelinventory.cpp b/indra/newview/llsidepanelinventory.cpp
index b48417bd71..623db82939 100644
--- a/indra/newview/llsidepanelinventory.cpp
+++ b/indra/newview/llsidepanelinventory.cpp
@@ -402,8 +402,6 @@ void LLSidepanelInventory::onOpen(const LLSD& key)
gSavedPerAccountSettings.setU32("LastInventoryInboxActivity", (U32)time_corrected());
}
#endif
-
- gAgent.showLatestFeatureNotification("inventory");
}
void LLSidepanelInventory::performActionOnSelection(const std::string &action)
diff --git a/indra/newview/lltextureview.cpp b/indra/newview/lltextureview.cpp
index 470a133fa7..10b57f47da 100644
--- a/indra/newview/lltextureview.cpp
+++ b/indra/newview/lltextureview.cpp
@@ -476,7 +476,7 @@ private:
void LLGLTexMemBar::draw()
{
- F32 discard_bias = LLViewerTexture::sDesiredDiscardBias;
+ F32 pixel_to_texel_ratio = LLViewerTexture::sPixelToTexelRatio;
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,25 +560,22 @@ 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 Bias: %.2f Cache: %.1f/%.1f MB",
+ 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",
(S32)LLViewerTexture::sFreeVRAMMegabytes,
LLMemory::getAvailableMemKB()/1024,
LLRenderTarget::sBytesAllocated/(1024*1024),
gPipeline.mReflectionMapManager.probeCount(),
gPipeline.mReflectionMapManager.probeMemory(),
- discard_bias,
+ pixel_to_texel_ratio,
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) Bubble: %.1fm PressMult: %.1fx LDMin: %.1f",
+ text = llformat("Images: %d Raw: %d (%.2f MB) Saved: %d (%.2f MB) Aux: %d (%.2f MB)",
image_count, raw_image_count, raw_image_bytes_MB,
saved_raw_image_count, saved_raw_image_bytes_MB,
- aux_raw_image_count, aux_raw_image_bytes_MB,
- LLViewerTextureList::sCurrentBubbleMeters,
- LLViewerTexture::sMemoryPressureMultiplier,
- LLViewerTexture::sLastDitchMinDiscard);
+ aux_raw_image_count, aux_raw_image_bytes_MB);
LLFontGL::getFontMonospace()->renderUTF8(text, 0, 0, v_offset + line_height * 7,
text_color, LLFontGL::LEFT, LLFontGL::TOP);
diff --git a/indra/newview/llviewerassetupload.cpp b/indra/newview/llviewerassetupload.cpp
index 65a69acc88..46c76e2953 100644
--- a/indra/newview/llviewerassetupload.cpp
+++ b/indra/newview/llviewerassetupload.cpp
@@ -881,6 +881,7 @@ void LLViewerAssetUpload::AssetInventoryUploadCoproc(LLCoreHttpUtil::HttpCorouti
if (uploadInfo->showUploadDialog())
{
+ // todo: localize this string
std::string uploadMessage = "Uploading...\n\n";
uploadMessage.append(uploadInfo->getDisplayName());
LLUploadDialog::modalUploadDialog(uploadMessage);
@@ -888,66 +889,90 @@ void LLViewerAssetUpload::AssetInventoryUploadCoproc(LLCoreHttpUtil::HttpCorouti
LLSD body = uploadInfo->generatePostBody();
- result = httpAdapter->postAndSuspend(httpRequest, url, body, httpOptions);
+ // Retry stage-2 upload by re-requesting a fresh one-time uploader capability (up to 3 attempts total)
+ const S32 MAX_UPLOAD_RETRIES = 2;
+ S32 upload_retry_count = 0;
+ LLCore::HttpStatus status;
- LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
- LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
-
- if ((!status) || (result.has("error")))
+ while (upload_retry_count <= MAX_UPLOAD_RETRIES)
{
- HandleUploadError(status, result, uploadInfo);
- if (uploadInfo->showUploadDialog())
- LLUploadDialog::modalUploadFinished();
- return;
- }
+ // Stage 1: Request uploader URL
+ result = httpAdapter->postAndSuspend(httpRequest, url, body, httpOptions);
- std::string uploader = result["uploader"].asString();
-
- bool success = false;
- if (!uploader.empty() && uploadInfo->getAssetId().notNull())
- {
- result = httpAdapter->postFileAndSuspend(httpRequest, uploader, uploadInfo->getAssetId(), uploadInfo->getAssetType(), httpOptions);
- httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
- std::string ulstate = result["state"].asString();
-
- if ((!status) || (ulstate != "complete"))
+ if ((!status) || (result.has("error")))
{
HandleUploadError(status, result, uploadInfo);
if (uploadInfo->showUploadDialog())
LLUploadDialog::modalUploadFinished();
return;
}
- if (!result.has("success"))
+
+ std::string uploader = result["uploader"].asString();
+ if (uploader.empty() || uploadInfo->getAssetId().isNull())
{
- result["success"] = LLSD::Boolean((ulstate == "complete") && status);
+ LL_WARNS() << "No upload url provided. Nothing uploaded, responding with previous result." << LL_ENDL;
+ break;
}
- S32 uploadPrice = result["upload_price"].asInteger();
+ // Stage 2: Upload to the uploader URL
+ result = httpAdapter->postFileAndSuspend(httpRequest, uploader, uploadInfo->getAssetId(), uploadInfo->getAssetType(), httpOptions);
+ httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
- if (uploadPrice > 0)
- {
- // this upload costed us L$, update our balance
- // and display something saying that it cost L$
- LLStatusBar::sendMoneyBalanceRequest();
+ std::string ulstate = result["state"].asString();
- LLSD args;
- args["AMOUNT"] = llformat("%d", uploadPrice);
- LLNotificationsUtil::add("UploadPayment", args);
+ if ((!status) || (ulstate != "complete"))
+ {
+ if (upload_retry_count < MAX_UPLOAD_RETRIES)
+ {
+ upload_retry_count++;
+ LL_WARNS() << "Upload to uploader failed (attempt " << upload_retry_count
+ << " of " << (MAX_UPLOAD_RETRIES + 1) << "), re-requesting uploader..." << LL_ENDL;
+ llcoro::suspendUntilTimeout(1.0f);
+ continue;
+ }
+ else
+ {
+ HandleUploadError(status, result, uploadInfo);
+ if (uploadInfo->showUploadDialog())
+ LLUploadDialog::modalUploadFinished();
+ return;
+ }
}
+
+ // Success!
+ break;
}
- else
+
+ if (!result.has("success"))
+ {
+ result["success"] = LLSD::Boolean((result["state"].asString() == "complete") && status);
+ }
+
+ S32 uploadPrice = result["upload_price"].asInteger();
+
+ if (uploadPrice > 0)
{
- LL_WARNS() << "No upload url provided. Nothing uploaded, responding with previous result." << LL_ENDL;
+ // this upload costed us L$, update our balance
+ // and display something saying that it cost L$
+ LLStatusBar::sendMoneyBalanceRequest();
+
+ LLSD args;
+ args["AMOUNT"] = llformat("%d", uploadPrice);
+ LLNotificationsUtil::add("UploadPayment", args);
}
+
LLUUID serverInventoryItem = uploadInfo->finishUpload(result);
+ bool succeeded = false;
if (uploadInfo->showInventoryPanel())
{
if (serverInventoryItem.notNull())
{
- success = true;
+ succeeded = true;
LLFocusableElement* focus = gFocusMgr.getKeyboardFocus();
@@ -973,7 +998,7 @@ void LLViewerAssetUpload::AssetInventoryUploadCoproc(LLCoreHttpUtil::HttpCorouti
LLFloater* floater_snapshot = LLFloaterReg::findInstance("snapshot");
if (uploadInfo->getAssetType() == LLAssetType::AT_TEXTURE && floater_snapshot && floater_snapshot->isShown())
{
- floater_snapshot->notify(LLSD().with("set-finished", LLSD().with("ok", success).with("msg", "inventory")));
+ floater_snapshot->notify(LLSD().with("set-finished", LLSD().with("ok", succeeded).with("msg", "inventory")));
}
}
diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp
index f8bc8d9504..8a1c0cf476 100644
--- a/indra/newview/llviewercontrol.cpp
+++ b/indra/newview/llviewercontrol.cpp
@@ -113,45 +113,60 @@ 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();
- 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);
+ 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;
return true;
}
@@ -870,7 +885,6 @@ 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/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp
index 36432265eb..1276ec5263 100644
--- a/indra/newview/llviewerdisplay.cpp
+++ b/indra/newview/llviewerdisplay.cpp
@@ -217,11 +217,6 @@ void display_update_camera()
{
final_far *= 0.5f;
}
- // When system memory is critically low or recovering, shrink draw distance.
- else if (LLViewerTexture::getSystemMemoryBudgetFactor() > 1.f)
- {
- final_far = llmax(32.f, final_far / LLViewerTexture::getSystemMemoryBudgetFactor());
- }
LLViewerCamera::getInstance()->setFar(final_far);
LLVOAvatar::sRenderDistance = llclamp(final_far, 16.f, 256.f);
gViewerWindow->setup3DRender();
diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp
index d63e0dc836..9071f43ba7 100644
--- a/indra/newview/llviewermessage.cpp
+++ b/indra/newview/llviewermessage.cpp
@@ -3392,13 +3392,6 @@ void send_agent_update(bool force_send, bool send_reliable)
static F32 last_draw_disatance_step = 1024;
F32 memory_limited_draw_distance = gAgentCamera.mDrawDistance;
- 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 7f51ad1065..4fbb83f51e 100644
--- a/indra/newview/llviewerobject.cpp
+++ b/indra/newview/llviewerobject.cpp
@@ -5230,7 +5230,20 @@ void LLViewerObject::setTE(const U8 te, const LLTextureEntry& texture_entry)
const LLUUID& image_id = getTE(te)->getID();
LLViewerTexture* bakedTexture = getBakedTextureForMagicId(image_id);
- mTEImages[te] = bakedTexture ? bakedTexture : LLViewerTextureManager::getFetchedTexture(image_id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
+ 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;
+ }
updateAvatarMeshVisibility(image_id, old_image_id);
@@ -5241,11 +5254,14 @@ 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 eea0b37c43..3e03875d03 100644
--- a/indra/newview/llviewershadermgr.cpp
+++ b/indra/newview/llviewershadermgr.cpp
@@ -567,13 +567,7 @@ 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 a6191a0a5b..d50b9ea0a9 100644
--- a/indra/newview/llviewertexture.cpp
+++ b/indra/newview/llviewertexture.cpp
@@ -60,6 +60,7 @@
#include "llmediaentry.h"
#include "llvovolume.h"
#include "llviewermedia.h"
+#include "lldrawable.h"
#include "lltexturecache.h"
#include "llviewerwindow.h"
#include "llwindow.h"
@@ -86,19 +87,8 @@ S32 LLViewerTexture::sImageCount = 0;
S32 LLViewerTexture::sRawCount = 0;
S32 LLViewerTexture::sAuxCount = 0;
LLFrameTimer LLViewerTexture::sEvaluationTimer;
-F32 LLViewerTexture::sDesiredDiscardBias = 0.f;
-F32 LLViewerTexture::sBackgroundFactor = 0.f;
-F32 LLViewerTexture::sMemoryPressureMultiplier = 1.f;
-F32 LLViewerTexture::sLastDitchMinDiscard = 0.f;
-
-//static
-F32 LLViewerTexture::getMemoryPressureProgress()
-{
- static LLCachedControl<F32> max_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f);
- F32 cap = llmax((F32)max_mult, 1.0001f);
- return llclampf((sMemoryPressureMultiplier - 1.f) / (cap - 1.f));
-}
-U32 LLViewerTexture::sBiasTexturesUpdated = 0;
+F32 LLViewerTexture::sPixelToTexelRatio = 1.f;
+U32 LLViewerTexture::sGCSuspendedFrame = 0;
S32 LLViewerTexture::sMaxSculptRez = 128; //max sculpt image size
constexpr S32 MAX_CACHED_RAW_IMAGE_AREA = 64 * 64;
@@ -110,11 +100,9 @@ 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;
@@ -491,13 +479,6 @@ 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()
{
@@ -546,375 +527,60 @@ 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();
- 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.
+ // 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.
{
- static LLCachedControl<F32> backoff_start(gSavedSettings, "TextureMemoryPressureBackoffStart", 0.85f);
- static LLCachedControl<F32> max_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f);
- static LLCachedControl<F32> prediction_gain(gSavedSettings, "TextureMemoryPressurePredictionGain", 10.f);
- static LLCachedControl<F32> smoothing_rate(gSavedSettings, "TextureMemoryPressureSmoothingRate", 4.f);
-
- F32 backoff_target = vram_target * llclamp((F32)backoff_start, 0.05f, 1.f);
- F32 cap = llmax((F32)max_mult, 1.0001f);
- F32 dt = (F32)gFrameIntervalSeconds;
-
- // Skip the full-list iteration when there is no pressure to react to:
- // mult already at baseline, last-ditch at zero, and used well clear of
- // the backoff target. Worst case the controller picks up the spike one
- // frame later, from `used` alone.
- bool need_predict = sMemoryPressureMultiplier > 1.001f
- || sLastDitchMinDiscard > 0.f
- || vram_used > backoff_target * 0.5f;
-
- S64 pending_bytes_increase = 0;
- S64 pending_bytes_decrease = 0;
- if (need_predict)
- {
- LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vt - in-flight predict");
- for (auto& imagep : gTextureList)
- {
- if (imagep.isNull()) continue;
- // Cheap inline checks first so the virtual getDiscardLevel()
- // call only fires when there is a real chance of contribution.
- S32 desired = imagep->getDesiredDiscardLevel();
- if (desired < 0) continue;
- S32 fw = imagep->getFullWidth();
- S32 fh = imagep->getFullHeight();
- if (fw <= 0 || fh <= 0) continue;
- S32 current = imagep->getDiscardLevel();
- if (current < 0 || desired == current) continue;
-
- S32 wd = llmax(1, fw >> desired);
- S32 hd = llmax(1, fh >> desired);
- S32 wc = llmax(1, fw >> current);
- S32 hc = llmax(1, fh >> current);
- // bpp=4, mip pyramid overhead 4/3
- S64 size_d = (S64)wd * hd * 4 * 4 / 3;
- S64 size_c = (S64)wc * hc * 4 * 4 / 3;
-
- if (desired < current)
- pending_bytes_increase += (size_d - size_c);
- else
- pending_bytes_decrease += (size_c - size_d);
- }
- }
-
- // 1024 * 512 = 524288: matches the unit reduction at line 513.
- constexpr F32 BYTES_TO_USED_UNITS = 1.f / 524288.f;
- F32 predicted_used = 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);
+ 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);
- F32 progress = getMemoryPressureProgress();
+ // 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;
- {
- 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)
- {
- // 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;
+ bool in_background = (gViewerWindow && !gViewerWindow->getWindow()->getVisible()) || !gFocusMgr.getAppHasFocus();
- // 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
+ if (in_background)
{
- // 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)
+ // 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)
{
- gTextureList.updateImageDecodePriority(image, false /*will modify gTextureList otherwise!*/);
+ const F32 step = llmax((F32)cooldown_step, 0.01f);
+ sPixelToTexelRatio = llmax(sPixelToTexelRatio * powf(0.25f, dt / step), bg_min);
}
}
- }
-
- was_low = is_low;
-
- if (is_low)
- {
- // ramp up discard bias over time to free memory
- if (sEvaluationTimer.getElapsedTimeF32() > MEMORY_CHECK_WAIT_TIME)
+ else if (vram_used > high)
{
- 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;
+ sPixelToTexelRatio -= llmax((F32)tighten_rate, 0.f) * dt;
}
- }
- 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)
+ else if (vram_used < low)
{
- 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;
+ sPixelToTexelRatio += llmax((F32)relax_rate, 0.f) * dt;
}
- }
-
- // 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);
+ // else: hold in the hysteresis band.
+ sPixelToTexelRatio = llclamp(sPixelToTexelRatio, 0.f, r_max);
- 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);
+ // 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.
if (in_background)
{
- sBackgroundFactor += (F32)bg_factor_rate * gFrameIntervalSeconds;
- sBackgroundFactor = llclampf(sBackgroundFactor);
- }
- else
- {
- sBackgroundFactor = 0.f;
+ sGCSuspendedFrame = LLFrameTimer::getFrameCount();
}
}
-
- // 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
@@ -2362,6 +2028,30 @@ 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)
{
@@ -2417,6 +2107,7 @@ 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);
}
@@ -3258,6 +2949,151 @@ 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()
@@ -3265,7 +3101,13 @@ void LLViewerLODTexture::processTextureStats()
LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
updateVirtualSize();
- bool did_downscale = false;
+ // 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);
static LLCachedControl<bool> textures_fullres(gSavedSettings,"TextureLoadFullRes", false);
@@ -3284,7 +3126,7 @@ void LLViewerLODTexture::processTextureStats()
mDesiredDiscardLevel = 0;
}
// HUD/UI/preview and mDontDiscard textures bypass streaming - no
- // face_distance signal applies, they need native resolution.
+ // coverage signal applies, they need native resolution.
else if (mBoostLevel >= LLGLTexture::BOOST_HIGH
|| mDontDiscard
|| !mUseMipMaps)
@@ -3300,165 +3142,37 @@ void LLViewerLODTexture::processTextureStats()
}
else
{
- 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.
+ // Per-texture max discard (smallest meaningful mip): floor(log2(max(w,h))).
S32 dim_max_for_image_i = (mFullWidth > 0 && mFullHeight > 0)
? LLImageGL::dimDerivedMaxDiscard(mFullWidth, mFullHeight)
: (S32)mCodecMaxDiscardLevel;
- F32 dim_max_for_image = (F32)dim_max_for_image_i;
- if (mKnownDrawWidth && mKnownDrawHeight)
- {
- // UI-pinned target dimensions - use pixel-area math.
- static const F64 log_4 = log(4.0);
- S32 draw_texels = mKnownDrawWidth * mKnownDrawHeight;
- draw_texels = llclamp(draw_texels, MIN_IMAGE_AREA, MAX_IMAGE_AREA);
- 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);
+ // The whole policy: pixel:texel ratio at the most-demanding face.
+ S32 discard = computeDesiredDiscard(dim_max_for_image_i, avatar_bake);
- 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;
+ // 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;
if (mFullWidth > max_tex_res || mFullHeight > max_tex_res)
- min_discard = 1.f;
+ min_discard = 1;
- // dim_max_for_image_i is the per-texture cap. TextureMaxDiscardOverride
- // raises it (debug). Codec_max applies only to fetches, not here.
static LLCachedControl<S32> max_discard_override(gSavedSettings, "TextureMaxDiscardOverride", 0);
- S32 effective_cap = (max_discard_override > 0) ? (S32)max_discard_override : dim_max_for_image_i;
- discard_level = llclamp(discard_level, min_discard, (F32)effective_cap);
-
- mDesiredDiscardLevel = llmin(effective_cap, (S32)discard_level);
-
- // Apply the setMinDiscardLevel cap, relaxed under VRAM pressure
- // (cap_relax = 1 - 1/mult: 0 at mult=1, ~0.5 at mult=2, ~0.9 at
- // mult=10). Caps of 0 (thumbnails) and avatar bakes are preserved.
- S32 effective_min_cap = mMinDesiredDiscardLevel;
- if (sMemoryPressureMultiplier > 1.f &&
- mMinDesiredDiscardLevel > 0 && mMinDesiredDiscardLevel < S8_MAX &&
- !isAgentAvatarBoost(mBoostLevel))
- {
- F32 cap_relax = 1.f - 1.f / sMemoryPressureMultiplier;
- F32 room = (F32)dim_max_for_image_i - (F32)mMinDesiredDiscardLevel;
- effective_min_cap += (S32)(cap_relax * room);
- effective_min_cap = llmin(effective_min_cap, dim_max_for_image_i);
- }
- mDesiredDiscardLevel = llmin((S8)effective_min_cap, mDesiredDiscardLevel);
-
- // Halve the floor for bubble-resident textures (mMinDistanceFactor == 0
- // = at least one face inside the bubble) so the close-vs-far gradient
- // is preserved at every pressure level.
- if (!isAgentAvatarBoost(mBoostLevel))
- {
- S32 forced = (S32)floorf(sLastDitchMinDiscard);
- if (mMinDistanceFactor <= 0.f) forced /= 2;
- forced = llclamp(forced, 0, dim_max_for_image_i);
- if (forced > mDesiredDiscardLevel)
- {
- mDesiredDiscardLevel = (S8)forced;
- }
- }
+ const S32 effective_cap = (max_discard_override > 0) ? (S32)max_discard_override : dim_max_for_image_i;
+ discard = llclamp(discard, min_discard, effective_cap);
+ // Caller-set min-discard ceiling (terrain / avatar-self / thumbnails):
+ // never coarser than the caller explicitly asked for.
+ discard = llmin(discard, (S32)mMinDesiredDiscardLevel);
- //
- // 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.
- //
+ mDesiredDiscardLevel = (S8)discard;
+ // 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();
- // Avatar bakes exempt: shrinking mid-bake can leave the avatar
- // stuck as a cloud until the next bake completes.
- if (!isAgentAvatarBoost(mBoostLevel))
+ if (!avatar_bake && current_discard >= 0 && current_discard < mDesiredDiscardLevel && !mForceToSaveRawImage)
{
- if (current_discard < mDesiredDiscardLevel && !mForceToSaveRawImage)
- { // should scale down
- scaleDown();
- }
+ scaleDown();
}
mDesiredDiscardLevel = llmin(mDesiredDiscardLevel, (S32)mLoadedCallbackDesiredDiscardLevel);
@@ -3486,12 +3200,7 @@ bool LLViewerLODTexture::scaleDown()
return false;
}
- // Hard structural blocks only. Per-texture policy (icons pinned to full
- // res, etc.) lives in processTextureStats; if that policy is later
- // relaxed (e.g. honor mKnownDrawWidth for icons rendered at 8x8 in a
- // friend list) the scaleDown path stays open.
- // BOOST_HIGH is the emergency-out for GLTF's "force full res" hack;
- // the other two flags are structural.
+ // Structural blocks only; per-texture policy lives in processTextureStats.
if (!mUseMipMaps || mDontDiscard || mBoostLevel >= LLGLTexture::BOOST_HIGH)
{
return false;
diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h
index 8c5c48bafd..cc9fbe5e48 100644
--- a/indra/newview/llviewertexture.h
+++ b/indra/newview/llviewertexture.h
@@ -114,11 +114,6 @@ 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) ;
@@ -150,6 +145,11 @@ 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,8 +193,6 @@ private:
friend class LLBumpImageList;
friend class LLUIImageList;
- static U32Megabytes getFreeSystemMemory();
-
protected:
friend class LLViewerTextureList;
LLUUID mID;
@@ -205,24 +203,17 @@ protected:
mutable S32 mMaxVirtualSizeResetInterval;
LLFrameTimer mLastReferencedTimer;
- // 0=Normal, 1=BaseColor, 2=Specular, 3=Emissive. -1 -> base color.
- S8 mPriorityChannel = -1;
-
- // Bind-staleness floor, 0..1. Per-interval increment is 1/max_discard
- // so any texture saturates after interval x max_discard seconds idle.
- F32 mStalenessFactor = 0.f;
-
- // Closest face's face_distance / draw_distance, clamped 0..1.
- // Defaults to 1 so never-measured textures resolve to deepest discard.
- F32 mMinDistanceFactor = 1.f;
-
- // Largest per-face screen-space coverage in pixels. Raw - no bias or
- // channel-priority contamination.
- F32 mMaxOnScreenSize = 0.f;
-
- // Any face on the agent's avatar (rigged / animated). Drives the
- // own-avatar quality boost in processTextureStats.
- bool mOnAgentAvatar = false;
+ // 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 };
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];
@@ -244,24 +235,17 @@ 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;
- // 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;
+ // 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;
- // 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 ;
@@ -270,7 +254,6 @@ 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;
@@ -331,27 +314,6 @@ 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;
@@ -602,6 +564,16 @@ 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 992d84f6d3..981e3dd933 100644
--- a/indra/newview/llviewertexturelist.cpp
+++ b/indra/newview/llviewertexturelist.cpp
@@ -64,6 +64,9 @@
#include "llviewerwindow.h"
#include "llsurface.h"
#include "llvoavatarself.h"
+#include "lldrawable.h"
+#include "llvovolume.h"
+#include "llviewertextureanim.h"
#include "llprogressview.h"
////////////////////////////////////////////////////////////////////////////
@@ -71,7 +74,6 @@
void (*LLViewerTextureList::sUUIDCallback)(void **, const LLUUID&) = NULL;
S32 LLViewerTextureList::sNumImages = 0;
-F32 LLViewerTextureList::sCurrentBubbleMeters = 0.f;
LLViewerTextureList gTextureList;
@@ -927,100 +929,86 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
{
llassert(!gCubeSnapshot);
- constexpr F32 BIAS_TRS_ON_SCREEN = 1.f; // perf gate for face-loop early exit
+ // 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();
+ }
if (imagep->getBoostLevel() < LLViewerFetchedTexture::BOOST_HIGH) // don't bother checking face list for boosted textures
{
- static LLCachedControl<F32> texture_scale_min(gSavedSettings, "TextureScaleMinAreaFactor", 0.0095f);
- static LLCachedControl<F32> texture_scale_max(gSavedSettings, "TextureScaleMaxAreaFactor", 25.f);
+ // 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;
- F32 max_vsize = 0.f;
- bool on_screen = false;
+ // 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;
- // Accumulators for the per-texture signals published below.
- // Defaults map to "deepest discard wanted" until evidence updates them.
- F32 min_distance_factor = 1.f;
- F32 max_on_screen_size = 0.f;
- bool on_agent_avatar = false;
- F32 draw_distance = llmax(gAgentCamera.mDrawDistance, 0.001f);
-
- // Close-camera bubble: faces inside `bubble` meters resolve to
- // dist_factor = 0, so the distance ramp spans (bubble, draw_distance].
- static LLCachedControl<F32> close_bubble(gSavedSettings, "TextureCloseBubbleMeters", 5.f);
- static LLCachedControl<F32> close_bubble_min(gSavedSettings, "TextureCloseBubbleMinMeters", 0.1f);
- static LLCachedControl<F32> bubble_shrink_threshold(gSavedSettings, "TextureCloseBubbleShrinkThreshold", 0.8f);
- static LLCachedControl<F32> bubble_track_rate(gSavedSettings, "TextureCloseBubbleTrackRate", 0.5f);
- F32 bubble_full = llmax((F32)close_bubble, 0.f);
- F32 bubble_min = llclamp((F32)close_bubble_min, 0.f, bubble_full);
- // Advance the slow-track once per frame, not per texture: this
- // function runs once per texture so a naive per-call lerp converges
- // in a single frame.
- static F32 s_tracked_bubble = -1.f;
- static U32 s_tracked_bubble_frame = 0;
- if (s_tracked_bubble < 0.f) s_tracked_bubble = bubble_full;
- if (s_tracked_bubble_frame != LLFrameTimer::getFrameCount())
- {
- s_tracked_bubble_frame = LLFrameTimer::getFrameCount();
- F32 progress = LLViewerTexture::getMemoryPressureProgress();
- F32 shrink_thresh = llclampf((F32)bubble_shrink_threshold);
- F32 shrink_frac = (progress > shrink_thresh)
- ? (progress - shrink_thresh) / llmax(1.f - shrink_thresh, 0.0001f)
- : 0.f;
- F32 target_bubble = bubble_full - (bubble_full - bubble_min) * shrink_frac;
- F32 dt = (F32)gFrameIntervalSeconds;
- F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)bubble_track_rate, 0.f));
- s_tracked_bubble += (target_bubble - s_tracked_bubble) * alpha;
- s_tracked_bubble = llclamp(s_tracked_bubble, bubble_min, bubble_full);
- sCurrentBubbleMeters = s_tracked_bubble;
- }
- F32 bubble = llclamp(s_tracked_bubble, 0.f, draw_distance - 0.001f);
- F32 ramp_range = llmax(draw_distance - bubble, 0.001f);
U32 face_count = 0;
- U32 max_faces_to_check = 1024;
+ const U32 max_faces_to_check = 1024;
- // Pick the least-aggressive channel across all uses, so a texture
- // used as both diffuse and normal isn't penalized by its harshest
- // role. -1 sentinel keeps emissive-only textures (W=3) from being
- // clobbered by a smaller init value.
- S32 priority_channel = -1;
+ // Cheap first pass: which buckets is this texture used in, and how many
+ // faces total. No per-face geometry work.
for (U32 i = 0; i < LLRender::NUM_TEXTURE_CHANNELS; ++i)
{
- if (imagep->getNumFaces(i) > 0)
+ U32 n = imagep->getNumFaces(i);
+ face_count += n;
+ if (n > 0)
{
- S32 mapped = sChannelToPriority[i];
- priority_channel = (priority_channel < 0) ? mapped : llmin(priority_channel, mapped);
+ S32 b = sChannelToPriority[i];
+ if (b >= 0 && b < 4) bucket_used[b] = true;
}
}
- if (priority_channel < 0)
+
+ if (face_count > max_faces_to_check)
{
- priority_channel = 1; // no faces - default to diffuse
+ // 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;
}
- 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)
+ else
{
- 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)
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
+ for (U32 i = 0; i < LLRender::NUM_TEXTURE_CHANNELS; ++i)
{
- LLFace* face = (*(imagep->getFaceList(i)))[fi];
-
- if (face && face->getViewerObject())
+ 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;
+ }
+
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
@@ -1029,171 +1017,251 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
face->mLastTextureUpdate = gFrameCount;
}
- 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)
+ // 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)
{
- on_agent_avatar = true;
+ // 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;
}
- // 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
+ // 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).
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;
- // Raw screen-space coverage - taken before the bias /
- // camera-boost mutations below so the size signal is clean.
- max_on_screen_size = llmax(max_on_screen_size, vsize);
-
- // apply bias to offscreen faces all the time, but only to onscreen faces when bias is large
- // use mImportanceToCamera to make bias switch a bit more gradual
- if (!face->mInFrustum || LLViewerTexture::sDesiredDiscardBias > 1.9f + face->mImportanceToCamera / 2.f)
+ F32 repeats = 1.f;
+ if (te)
{
- vsize /= bias;
+ // 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;
+ }
+ }
+ }
}
- max_vsize = llmax(max_vsize, vsize);
+ repeats = llclamp(repeats, MIN_REPEAT_AREA, MAX_REPEAT_AREA);
- // addTextureStats limits size to sMaxVirtualSize
- if (max_vsize >= LLViewerFetchedTexture::sMaxVirtualSize
- && (on_screen || LLViewerTexture::sDesiredDiscardBias <= BIAS_TRS_ON_SCREEN))
+ // 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())
{
- break;
+ static LLCachedControl<F32> avatar_boost(gSavedSettings, "TextureAvatarBoost", 4.f);
+ vsize *= llmax((F32)avatar_boost, 1.f);
}
- }
- }
- if (max_vsize >= LLViewerFetchedTexture::sMaxVirtualSize
- && (on_screen || LLViewerTexture::sDesiredDiscardBias <= BIAS_TRS_ON_SCREEN))
- {
- break;
+ if (bucket >= 0 && bucket < 4)
+ {
+ channel_coverage[bucket] = llmax(channel_coverage[bucket], vsize);
+ if (vsize > 0.f)
+ {
+ channel_coverage_min[bucket] = llmin(channel_coverage_min[bucket], vsize);
+ }
+ }
+ max_coverage = llmax(max_coverage, vsize);
+ }
}
}
- 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 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)
{
- // Terrain detail textures don't register faces with the texture
- // (LLVOSurfacePatch addFace(NULL)). Drive distance from the LOD
- // system; floor at a small nonzero value so pressure has
- // something to bite into (pow(0, p) = 0).
- static LLCachedControl<F32> terrain_distance_floor(gSavedSettings, "TextureTerrainDistanceFloor", 0.01f);
- static LLCachedControl<F32> terrain_coverage(gSavedSettings, "TextureTerrainCoverageFraction", 0.99f);
F32 nearest = LLSurface::sNearestVisiblePatchDistance;
- F32 nearest_above_bubble = (nearest < FLT_MAX) ? llmax(nearest - bubble, 0.f) : ramp_range;
- F32 dist = llclampf(nearest_above_bubble / ramp_range);
- imagep->mMinDistanceFactor = llmax(dist, llclampf((F32)terrain_distance_floor));
- imagep->mMaxOnScreenSize = LLViewerTexture::sWindowPixelArea * llclampf((F32)terrain_coverage);
+ 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;
}
- else
+ // 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))
{
- imagep->mMinDistanceFactor = min_distance_factor;
- imagep->mMaxOnScreenSize = max_on_screen_size;
+ 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->mOnAgentAvatar = on_agent_avatar;
- // 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()))
- {
- imagep->mStalenessFactor = 0.f;
- }
- else if (LLImageGL* gli = imagep->getGLTexture())
+ // 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)
{
- static LLCachedControl<F32> bind_decay_seconds(gSavedSettings, "TextureBindDecaySeconds", 5.f);
- static LLCachedControl<F32> staleness_interval(gSavedSettings, "TextureStalenessIntervalSeconds", 5.f);
- F32 grace = llmax((F32)bind_decay_seconds, 0.f);
- F32 interval = llmax((F32)staleness_interval, 0.0001f);
-
- // Clock starts at whichever is later: the last real bind or
- // the GL-create time. The latter is the fallback for textures
- // decoded into GL but never actually rendered - without it,
- // mLastBindTime stays 0 forever and staleness can't evict.
- F32 clock_time = llmax(gli->mLastBindTime, gli->mGLCreateTime);
- bool has_clock = (clock_time > 0.f);
- F32 time_since = has_clock ? (LLImageGL::sLastFrameTime - clock_time) : 0.f;
-
- if (!has_clock || time_since <= grace)
+ LLVOVolume* volume = (*imagep->getVolumeList(LLRender::LIGHT_TEX))[vi];
+ F32 cov = llmin(volume->getSpotLightPriority(), LLViewerTexture::sWindowPixelArea);
+ if (cov > 0.f)
{
- 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;
- }
+ 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->addTextureStats(max_coverage);
+
+ // 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 0
- imagep->setDebugText(llformat("%d/%d - %d/%d -- %d/%d",
- (S32)sqrtf(max_vsize),
- (S32)sqrtf(imagep->mMaxVirtualSize),
+ imagep->setDebugText(llformat("%d/%d -- %d/%d",
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
@@ -1424,21 +1492,6 @@ 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 dbed8b5c2f..931f2ed50e 100644
--- a/indra/newview/llviewertexturelist.h
+++ b/indra/newview/llviewertexturelist.h
@@ -244,10 +244,6 @@ 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 eea8b3b74d..de8ce91bd2 100644
--- a/indra/newview/llviewerwindow.cpp
+++ b/indra/newview/llviewerwindow.cpp
@@ -5607,7 +5607,13 @@ bool LLViewerWindow::cubeSnapshot(const LLVector3& origin, LLCubeMapArray* cubea
// actually render the scene
gCubeSnapshot = true;
- display_cube_face();
+ {
+ // 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();
+ }
gCubeSnapshot = false;
}
diff --git a/indra/newview/llvocache.cpp b/indra/newview/llvocache.cpp
index 7618739b3c..f3efe3f3bb 100644
--- a/indra/newview/llvocache.cpp
+++ b/indra/newview/llvocache.cpp
@@ -488,12 +488,6 @@ void LLVOCacheEntry::updateDebugSettings()
static const F32 MIN_RADIUS = 1.0f;
F32 draw_radius = gAgentCamera.mDrawDistance;
- if (LLViewerTexture::getSystemMemoryBudgetFactor() > 1.f)
- {
- // Factor is intended to go from 1.0 to 2.0
- // For safety cap reduction at 50%, we don't want to go below half of draw distance
- draw_radius = llmax(draw_radius / LLViewerTexture::getSystemMemoryBudgetFactor(), draw_radius / 2.f);
- }
const F32 clamped_min_radius = llclamp((F32) min_radius, MIN_RADIUS, draw_radius); // [1, mDrawDistance]
sNearRadius = MIN_RADIUS + ((clamped_min_radius - MIN_RADIUS) * adjust_factor);
diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp
index 16c262d1ad..0904f69f0d 100644
--- a/indra/newview/llvovolume.cpp
+++ b/indra/newview/llvovolume.cpp
@@ -878,11 +878,15 @@ void LLVOVolume::updateTextureVirtualSize(bool forced)
LLViewerFetchedTexture* img = LLViewerTextureManager::staticCastToFetchedTexture(imagep) ;
if(img)
{
- 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;*/
+ // 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";
}
}
else if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_FACE_AREA))
@@ -928,7 +932,7 @@ void LLVOVolume::updateTextureVirtualSize(bool forced)
{
LLLightImageParams* params = getLightImageParams();
LLUUID id = params->getLightTexture();
- mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE);
+ mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
if (mLightTexture.notNull())
{
F32 rad = getLightRadius();
@@ -1797,6 +1801,41 @@ 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.
@@ -3367,7 +3406,7 @@ LLViewerTexture* LLVOVolume::getLightTexture()
{
if (mLightTexture.isNull() || id != mLightTexture->getID())
{
- mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE);
+ mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
}
}
else
@@ -5851,6 +5890,16 @@ 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 f4c121c18b..c36c5c6d29 100644
--- a/indra/newview/pipeline.cpp
+++ b/indra/newview/pipeline.cpp
@@ -9889,6 +9889,9 @@ 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;
@@ -11315,6 +11318,9 @@ 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();