summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--indra/llrender/llimagegl.cpp56
-rw-r--r--indra/llrender/llimagegl.h32
-rw-r--r--indra/llrender/llrender.cpp7
-rw-r--r--indra/newview/app_settings/settings.xml50
-rw-r--r--indra/newview/llfetchedgltfmaterial.cpp26
-rw-r--r--indra/newview/llviewertexture.cpp245
-rw-r--r--indra/newview/llviewertexture.h54
-rw-r--r--indra/newview/llviewertexturelist.cpp3
-rw-r--r--indra/newview/llviewerwindow.cpp8
-rw-r--r--indra/newview/pipeline.cpp6
10 files changed, 395 insertions, 92 deletions
diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp
index 6bcc34938c..95575c009b 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;
@@ -777,6 +779,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();
@@ -1502,6 +1505,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)
{
@@ -1511,19 +1520,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();
}
@@ -1668,6 +1688,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();
@@ -2090,12 +2111,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
@@ -2520,6 +2550,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 0869ae54fe..57ca79b3dd 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,8 +239,17 @@ public:
public:
// Various GL/Rendering options
S64Bytes mTextureMemory;
- mutable F32 mLastBindTime = 0.f; // wall-clock time at last stampBound; drives the streaming cooldown
- F32 mGLCreateTime = 0.f; // wall-clock time the GL texture was created; cooldown 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);
@@ -300,6 +310,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 +370,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 5e845fbcce..f0a1c44507 100644
--- a/indra/llrender/llrender.cpp
+++ b/indra/llrender/llrender.cpp
@@ -245,6 +245,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)
{
@@ -325,6 +330,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 864b85034e..bdcbf3b7f2 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -11896,7 +11896,7 @@
<key>TexturePixelToTexelRatio</key>
<map>
<key>Comment</key>
- <string>Global maximum pixel:texel ratio, expressed as texels per screen pixel (the "R" in 1:R). 1.0 = one texel per pixel, the best quality the streamer will allocate. A texture is sized so its most-demanding on-screen face stays at or below this many texels per pixel; everything coarser falls out by distance. VRAM pressure walks the effective ratio down from here toward 0 (no floor).</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>
@@ -11904,6 +11904,17 @@
<key>Value</key>
<real>1.0</real>
</map>
+ <key>TextureBackgroundMinRatio</key>
+ <map>
+ <key>Comment</key>
+ <string>Lowest pixel:texel ratio the streamer decays to while the viewer is backgrounded, to free VRAM for other apps. Lower frees more but re-rezzes slower on return. Restored when focus comes back.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.001</real>
+ </map>
<key>TexturePressureHighWater</key>
<map>
<key>Comment</key>
@@ -11962,13 +11973,46 @@
<key>TextureCooldownStepSeconds</key>
<map>
<key>Comment</key>
- <string>Seconds an unseen texture waits per mip level before stepping down. When a texture stops being bound (occluded / off-screen) or the window is backgrounded, its discard rises one level every this many seconds until it reaches the deepest mip, instead of snapping there immediately - so briefly-unseen content isn't thrown away and refetched (cache thrash) if it reappears. Resets the moment the texture is bound again.</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>
<string>F32</string>
<key>Value</key>
- <real>1.0</real>
+ <real>5.0</real>
+ </map>
+ <key>TextureGCStepFrames</key>
+ <map>
+ <key>Comment</key>
+ <string>Foreground GC cooldown, in rendered frames. For every this many frames a texture goes without being drawn, its mip drops by TextureGCStepMips. Resets when the texture is drawn again.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>U32</string>
+ <key>Value</key>
+ <integer>5</integer>
+ </map>
+ <key>TextureGCStepMips</key>
+ <map>
+ <key>Comment</key>
+ <string>Mip levels dropped each time a TextureGCStepFrames cooldown elapses. 1 is gentlest; higher sheds VRAM faster in coarser jumps.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>U32</string>
+ <key>Value</key>
+ <integer>1</integer>
+ </map>
+ <key>TextureFetchVisibilityFrames</key>
+ <map>
+ <key>Comment</key>
+ <string>Only fetch a texture if it was drawn within this many rendered frames; out-of-view content isn't fetched. Minimum 1 (0 is clamped up). Boosted/UI textures, avatar bakes, and callback textures are exempt.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>U32</string>
+ <key>Value</key>
+ <integer>1</integer>
</map>
<key>TextureDecodeDisabled</key>
<map>
diff --git a/indra/newview/llfetchedgltfmaterial.cpp b/indra/newview/llfetchedgltfmaterial.cpp
index 306067d2cf..d71f0c1bd4 100644
--- a/indra/newview/llfetchedgltfmaterial.cpp
+++ b/indra/newview/llfetchedgltfmaterial.cpp
@@ -75,11 +75,8 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex)
if (media_tex)
{
- // The real basecolor/emissive stay registered for coverage while
- // media hides them - stamp them so the streaming cooldown doesn't
- // fight that coverage (coarsen -> refetch tug-of-war) the whole
- // time media is playing. Blinn has no hidden-texture-under-media
- // state, so without this the two systems diverge on media faces.
+ // 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(); }
@@ -114,15 +111,10 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex)
if (!LLPipeline::sShadowRender)
{
- // Bind the normal map at whatever resolution is resident - matching
- // how Blinn-Phong normal maps degrade (soft, never absent). The old
- // "getDiscardLevel() <= 4" gate made PBR normals a cliff instead of
- // a gradient: any material the streamer legitimately sized past
- // discard 4 (distant / tiled) rendered with NO normal map while the
- // equivalent Blinn content rendered a soft one, making PBR look
- // categorically flatter. The flat-normal fallback remains only for
- // the genuinely-not-yet-loaded window, where it is the correct
- // normal-shaped default.
+ // 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);
@@ -132,10 +124,8 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex)
shader->bindTexture(LLShaderMgr::BUMP_MAP, LLViewerFetchedTexture::sFlatNormalImagep);
if (mNormalTexture.notNull())
{
- // In active use, just not loaded yet - stamp it so the
- // streaming last-bound cooldown doesn't read "unbound" as
- // "unseen" and pin it at the deepest mip before its first
- // real bind.
+ // 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();
diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp
index 42cf91b43b..47d022c854 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"
@@ -87,7 +88,12 @@ S32 LLViewerTexture::sRawCount = 0;
S32 LLViewerTexture::sAuxCount = 0;
LLFrameTimer LLViewerTexture::sEvaluationTimer;
F32 LLViewerTexture::sPixelToTexelRatio = 1.f;
-F32 LLViewerTexture::sBackgroundSeconds = 0.f;
+U32 LLViewerTexture::sGCSuspendedFrame = 0;
+S64 LLViewerTexture::sPendingAllocBytes = 0;
+S64 LLViewerTexture::sPendingFreeBytes = 0;
+U32 LLViewerTexture::sUprezRequestCount = 0;
+U32 LLViewerTexture::sDownscaleEnqueueCount = 0;
+U32 LLViewerTexture::sCooldownFlooredCount = 0;
S32 LLViewerTexture::sMaxSculptRez = 128; //max sculpt image size
constexpr S32 MAX_CACHED_RAW_IMAGE_AREA = 64 * 64;
@@ -109,6 +115,9 @@ LLViewerTexture::EDebugTexels LLViewerTexture::sDebugTexelsMode = LLViewerTextur
const F64 log_2 = log(2.0);
+// GC evict->refetch cycle samples for the 1Hz TextureStream log (main thread).
+static U32 sGCRefetchCount = 0;
+
//----------------------------------------------------------------------------------------------
//namespace: LLViewerTextureAccess
//----------------------------------------------------------------------------------------------
@@ -537,33 +546,41 @@ void LLViewerTexture::updateClass()
sFreeVRAMMegabytes = vram_target - vram_used;
const S32Megabytes free_sys_mem = getFreeSystemMemory();
- // Single VRAM-pressure knob: the global maximum pixel:texel ratio (texels
- // per screen pixel, the "R" in 1:R). It tightens (drops toward 0, no floor)
- // while used VRAM is above the high watermark,
- // relaxes back toward TexturePixelToTexelRatio below the low watermark, and
- // holds steady in the hysteresis band between - the band is what prevents
- // sawtooth. The ramp is deliberately slow so eviction (scaleDown draining
- // mDownScaleQueue) frees bytes before the next step. Lowering the ratio
- // raises every texture's desired discard, which is what actually evicts.
+ // 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> 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);
- // Unbounded downward: pressure drives the ratio all the way to 0 if it
- // has to. There is no quality floor - at 0 every texture resolves to
- // its deepest mip (computeDesiredDiscard treats zero allowed texels as
- // dim_max), and desired discard is clamped to dim_max anyway, so it
- // saturates on its own. Only the top is bounded, by the configured max.
+ // 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;
- if (vram_used > high)
+ bool in_background = (gViewerWindow && !gViewerWindow->getWindow()->getVisible()) || !gFocusMgr.getAppHasFocus();
+
+ if (in_background)
+ {
+ // 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)
+ {
+ const F32 step = llmax((F32)cooldown_step, 0.01f);
+ sPixelToTexelRatio = llmax(sPixelToTexelRatio * powf(0.25f, dt / step), bg_min);
+ }
+ }
+ else if (vram_used > high)
{
sPixelToTexelRatio -= llmax((F32)tighten_rate, 0.f) * dt;
}
@@ -574,26 +591,44 @@ void LLViewerTexture::updateClass()
// else: hold in the hysteresis band.
sPixelToTexelRatio = llclamp(sPixelToTexelRatio, 0.f, r_max);
- // Background cooldown clock: grows while backgrounded/minimized, resets in
- // foreground. Feeds the per-texture cooldown in computeDesiredDiscard so
- // backgrounding steps every texture up its mip chain over time instead of
- // snapping straight to the deepest mip.
- bool in_background = (gViewerWindow && !gViewerWindow->getWindow()->getVisible()) || !gFocusMgr.getAppHasFocus();
- sBackgroundSeconds = in_background ? (sBackgroundSeconds + dt) : 0.f;
+ // Keep the GC-suspend frame current while backgrounded. This suppresses
+ // the foreground GC now, and gives it a grace window after we come back so
+ // visible content can re-stamp its bind frames before anything is collected.
+ if (in_background)
+ {
+ sGCSuspendedFrame = LLFrameTimer::getFrameCount();
+ }
- // 1 Hz pressure log.
+ // 1 Hz pressure log. `used` units are the doubled-bytes metric
+ // (bytes/524288, see above); pending ledgers are converted to match so
+ // eff(ective) = what used will read once in-flight work settles.
static LLFrameTimer s_pressure_log_timer;
if (s_pressure_log_timer.getElapsedTimeF32() > 1.f)
{
s_pressure_log_timer.reset();
+ constexpr F32 BYTES_TO_USED_UNITS = 1.f / 524288.f;
+ F32 pend_alloc = (F32)sPendingAllocBytes * BYTES_TO_USED_UNITS;
+ F32 pend_free = (F32)sPendingFreeBytes * BYTES_TO_USED_UNITS;
LL_INFOS("TextureStream") << "pressure"
<< " ratio=" << sPixelToTexelRatio
<< " used=" << vram_used
+ << " eff=" << vram_used + pend_alloc - pend_free
+ << " pend+=" << pend_alloc
+ << " pend-=" << pend_free
<< " budget=" << vram_budget
<< " high=" << high
<< " low=" << low
<< " dsq=" << (S32)gTextureList.mDownScaleQueue.size()
+ << " uprez/s=" << sUprezRequestCount
+ << " dscale/s=" << sDownscaleEnqueueCount
+ << " cdfloor/s=" << sCooldownFlooredCount
+ << " gcref/s=" << sGCRefetchCount
+ << " gloom=" << LLImageGL::sOOMErrorCount.load()
<< LL_ENDL;
+ sUprezRequestCount = 0;
+ sDownscaleEnqueueCount = 0;
+ sCooldownFlooredCount = 0;
+ sGCRefetchCount = 0;
}
}
@@ -1237,6 +1272,7 @@ FTType LLViewerFetchedTexture::getFTType() const
void LLViewerFetchedTexture::cleanup()
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
+ setPendingByteDelta(0); // never leak ledger entries on teardown
for(callback_list_t::iterator iter = mLoadedCallbackList.begin();
iter != mLoadedCallbackList.end(); )
{
@@ -1436,15 +1472,20 @@ void LLViewerFetchedTexture::addToCreateTexture()
//just update some variables, not to create a real GL texture.
createGLTexture(mRawDiscardLevel, mRawImage, 0, false);
mNeedsCreateTexture = false;
+ setPendingByteDelta(0); // no GL bytes will be committed
destroyRawImage();
}
else if(!force_update && getDiscardLevel() > -1 && getDiscardLevel() <= mRawDiscardLevel)
{
mNeedsCreateTexture = false;
+ setPendingByteDelta(0); // nothing to create; commitment over
destroyRawImage();
}
else
{
+ // Dims are exact now (raw decoded) - refine the ledger entry to the
+ // discard level that will actually be created.
+ setPendingByteDelta(estimatedVRAMBytesAtDiscard(mRawDiscardLevel) - residentVRAMBytes());
scheduleCreateTexture();
}
return;
@@ -1569,6 +1610,61 @@ bool LLViewerFetchedTexture::preCreateTexture(S32 usename/*= 0*/)
return res;
}
+S64 LLViewerFetchedTexture::estimatedVRAMBytesAtDiscard(S32 discard) const
+{
+ if (discard < 0)
+ {
+ return 0;
+ }
+ if (mFullWidth <= 0 || mFullHeight <= 0)
+ {
+ // Dims unknown (header not fetched yet): nominal placeholder,
+ // corrected as soon as the first decode reports real dimensions.
+ return 64 * 64 * 4;
+ }
+ S32 w = llmax(1, (S32)mFullWidth >> discard);
+ S32 h = llmax(1, (S32)mFullHeight >> discard);
+ S64 bytes = (S64)w * h * 4; // GL pads most formats to 4 components
+ bytes = bytes * 4 / 3; // mip chain
+ if (LLImageGL::sCompressTextures)
+ {
+ bytes /= 4; // rough DXT ratio
+ }
+ return bytes;
+}
+
+S64 LLViewerFetchedTexture::residentVRAMBytes() const
+{
+ return mGLTexturep.notNull() ? (S64)mGLTexturep->mTextureMemory.value() : 0;
+}
+
+void LLViewerFetchedTexture::setPendingByteDelta(S64 delta)
+{
+ if (delta == mPendingByteDelta)
+ {
+ return;
+ }
+ // retire the previous contribution
+ if (mPendingByteDelta > 0)
+ {
+ sPendingAllocBytes -= mPendingByteDelta;
+ }
+ else if (mPendingByteDelta < 0)
+ {
+ sPendingFreeBytes -= -mPendingByteDelta;
+ }
+ // apply the new one
+ if (delta > 0)
+ {
+ sPendingAllocBytes += delta;
+ }
+ else if (delta < 0)
+ {
+ sPendingFreeBytes += -delta;
+ }
+ mPendingByteDelta = delta;
+}
+
bool LLViewerFetchedTexture::createTexture(S32 usename/*= 0*/)
{
if (!mNeedsCreateTexture)
@@ -1611,6 +1707,7 @@ void LLViewerFetchedTexture::postCreateTexture()
}
destroyRawImage(); // will save raw image if needed
+ setPendingByteDelta(0); // commitment realized - bytes now in LLImageGL accounting
mNeedsCreateTexture = false;
}
@@ -2139,6 +2236,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)
{
@@ -2194,6 +2315,19 @@ 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);
+
+ // Open the committed-bytes ledger entry: bytes this request will make
+ // resident minus what's resident now. Settled at postCreateTexture (or
+ // on the cancel/failure paths).
+ setPendingByteDelta(estimatedVRAMBytesAtDiscard(mRequestedDiscardLevel) - residentVRAMBytes());
+ ++sUprezRequestCount;
+
+ if (mGCEvicted)
+ {
+ // GC evict->refetch cycle; counted as gcref/s, should be ~0 when settled.
+ mGCEvicted = false;
+ ++sGCRefetchCount;
+ }
mFetchState = LLAppViewer::getTextureFetch()->getFetchState(mID, mDownloadProgress, mRequestedDownloadPriority,
mFetchPriority, mFetchDeltaTime, mRequestDeltaTime, mCanUseHTTP);
}
@@ -2242,6 +2376,12 @@ bool LLViewerFetchedTexture::updateFetch()
LL_DEBUGS("Texture") << "exceeded idle time " << FETCH_IDLE_TIME << ", deleting request: " << getID() << LL_ENDL;
LLAppViewer::getTextureFetch()->deleteRequest(getID(), true);
mHasFetcher = false;
+ if (!mNeedsCreateTexture && !mCreatePending)
+ {
+ // fetch retired without delivering anything still queued -
+ // settle the ledger (delivered data settles at postCreateTexture)
+ setPendingByteDelta(0);
+ }
}
}
@@ -2271,6 +2411,10 @@ void LLViewerFetchedTexture::forceToDeleteRequest()
mHasFetcher = false;
mIsFetching = false;
}
+ if (!mNeedsCreateTexture && !mCreatePending)
+ {
+ setPendingByteDelta(0); // request dead, nothing queued to create
+ }
resetTextureStats();
@@ -2308,6 +2452,7 @@ void LLViewerFetchedTexture::setIsMissingAsset(bool is_missing)
mFetchState = 0;
mFetchPriority = 0;
}
+ setPendingByteDelta(0); // nothing will be committed for a missing asset
}
else
{
@@ -3147,31 +3292,37 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c
desired = current; // inside the dead-band -> hold
}
- // Cooldown: don't snap an unseen texture straight to its deepest mip - step
- // it up one level per TextureCooldownStepSeconds so briefly-occluded or
- // backgrounded content isn't thrown away (and doesn't thrash the cache) when
- // we look at it again. Driven by whichever is longer: time since last bind,
- // or time backgrounded. It only raises discard and resets the moment the
- // texture is bound again. One frame interval is subtracted so a
- // continuously-visible texture (whose last bind is a frame old here, since
- // this pass runs a frame ahead of its own render) reads as zero. Avatar
- // bakes exempt.
+ // 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)
{
- static LLCachedControl<F32> cooldown_step(gSavedSettings, "TextureCooldownStepSeconds", 1.f);
- const F32 step = llmax((F32)cooldown_step, 0.01f);
- F32 unbound = 0.f;
if (LLImageGL* gli = getGLTexture())
{
- const F32 ref = llmax(gli->mLastBindTime, gli->mGLCreateTime);
- if (ref > 0.f)
+ 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();
+ mGCFloored = false;
+ if (gli->mLastBindFrame > 0 // drawn at least once
+ && now - sGCSuspendedFrame > GC_RESUME_GRACE_FRAMES) // not just back from background
{
- unbound = llmax(0.f, LLImageGL::sLastFrameTime - ref - (F32)gFrameIntervalSeconds);
+ 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);
+ mGCFloored = true;
+ ++sCooldownFlooredCount;
+ }
}
}
- const F32 cooldown_seconds = llmax(unbound, sBackgroundSeconds);
- const S32 cooldown_floor = llclamp((S32)floor(cooldown_seconds / step), 0, dim_max_i);
- desired = llmax(desired, cooldown_floor);
}
return llclamp(desired, 0, dim_max_i);
@@ -3255,6 +3406,10 @@ void LLViewerLODTexture::processTextureStats()
S32 current_discard = getDiscardLevel();
if (!avatar_bake && current_discard >= 0 && current_discard < mDesiredDiscardLevel && !mForceToSaveRawImage)
{
+ if (mGCFloored)
+ {
+ mGCEvicted = true; // eviction attributable to the visibility GC
+ }
scaleDown();
}
@@ -3283,12 +3438,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;
@@ -3303,6 +3453,9 @@ bool LLViewerLODTexture::scaleDown()
{
mDownScalePending = true;
gTextureList.mDownScaleQueue.push(this);
+ // Pending-free ledger entry: bytes decided-freed, returned when the queue drains.
+ setPendingByteDelta(estimatedVRAMBytesAtDiscard(mDesiredDiscardLevel) - residentVRAMBytes());
+ ++sDownscaleEnqueueCount;
}
return true;
diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h
index 28d896eff5..ce9a953de5 100644
--- a/indra/newview/llviewertexture.h
+++ b/indra/newview/llviewertexture.h
@@ -243,19 +243,27 @@ public:
static S32 sAuxCount;
static LLFrameTimer sEvaluationTimer;
- // The single VRAM-pressure knob: the global maximum pixel:texel ratio,
- // expressed as texels per screen pixel (the "R" in 1:R). Starts at
- // TexturePixelToTexelRatio (1.0 = one texel per pixel) and the watermark
- // controller in updateClass() walks it down toward 0 (no floor)
- // while used VRAM is above the high watermark, back up below the low
- // watermark, holding in the band between. Lowering it raises every
- // texture's desired discard, which drives scaleDown eviction. Consumed by
- // LLViewerLODTexture::computeDesiredDiscard.
+ // 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;
- // Seconds the app has been backgrounded/minimized (0 in foreground). Drives
- // the background half of the per-texture cooldown in computeDesiredDiscard.
- static F32 sBackgroundSeconds;
+ // 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;
+
+ // Committed-but-not-yet-realized VRAM, in bytes (main thread only).
+ // effective_used = resident + sPendingAllocBytes - sPendingFreeBytes.
+ // Maintained by setPendingByteDelta; shown in the 1Hz pressure log.
+ static S64 sPendingAllocBytes; // in-flight toward allocation
+ static S64 sPendingFreeBytes; // queued for release, not yet returned
+
+ // 1Hz churn counters (main thread; reset each pressure-log tick). High
+ // uprez+downscale with no memory pressure = per-texture oscillation.
+ static U32 sUprezRequestCount; // finer-mip fetch requests issued
+ static U32 sDownscaleEnqueueCount; // scaleDown enqueues
+ static U32 sCooldownFlooredCount; // desired raised by the cooldown floor
static S32 sMaxSculptRez ;
static U32 sMinLargeImageSize ;
@@ -445,6 +453,26 @@ public:
bool mCreatePending = false; // if true, this is in gTextureList.mCreateTextureList
mutable bool mDownScalePending = false; // if true, this is in gTextureList.mDownScaleQueue
+ // GC-cycle diagnostics (main thread): mGCFloored = the visibility GC
+ // raised desired on the last computeDesiredDiscard; mGCEvicted = this
+ // texture was actually evicted because of it. A subsequent uprez fetch
+ // request while mGCEvicted is a full evict->refetch cycle - the churn
+ // signature - and gets sampled into the 1Hz TextureStream log.
+ mutable bool mGCFloored = false;
+ bool mGCEvicted = false;
+
+ // --- committed-bytes ledger (main thread only) ---
+ // Estimated VRAM bytes this texture would occupy resident at `discard`
+ // (components ~4, x4/3 mip chain, /4 rough DXT when compression is on).
+ // Nominal small placeholder before dims are known.
+ S64 estimatedVRAMBytesAtDiscard(S32 discard) const;
+ // Actual bytes currently resident (LLImageGL accounting), 0 if none.
+ S64 residentVRAMBytes() const;
+ // Open/adjust/settle this texture's contribution to the global pending
+ // ledgers. delta > 0 = in-flight toward allocation; delta < 0 = queued
+ // free; 0 = settled. Replaces any previous contribution.
+ void setPendingByteDelta(S64 delta);
+
protected:
S32 getCurrentDiscardLevelForFetching() ;
void forceToRefetchTexture(S32 desired_discard = 0, F32 kept_time = 60.f);
@@ -533,6 +561,10 @@ protected:
LLFrameTimer mLastPacketTimer; // Time since last packet.
LLFrameTimer mStopFetchingTimer; // Time since mDecodePriority == 0.f.
+ // This texture's open contribution to the pending-bytes ledgers
+ // (see setPendingByteDelta). 0 = no open commitment. Main thread only.
+ S64 mPendingByteDelta = 0;
+
bool mInImageList; // true if image is in list (in which case don't reset priority!)
// This needs to be atomic, since it is written both in the main thread
// and in the GL image worker thread... HB
diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp
index 70bca3854d..8fab391997 100644
--- a/indra/newview/llviewertexturelist.cpp
+++ b/indra/newview/llviewertexturelist.cpp
@@ -64,6 +64,7 @@
#include "llviewerwindow.h"
#include "llsurface.h"
#include "llvoavatarself.h"
+#include "lldrawable.h"
#include "llvovolume.h"
#include "llviewertextureanim.h"
#include "llprogressview.h"
@@ -950,6 +951,7 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
bool bucket_used[4] = { false, false, false, false };
F32 max_coverage = 0.f;
+
U32 face_count = 0;
const U32 max_faces_to_check = 1024;
@@ -1387,6 +1389,7 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time)
img->scaleDown(image->getDesiredDiscardLevel());
}
+ image->setPendingByteDelta(0); // pending-free settled (or abandoned)
image->mDownScalePending = false;
mDownScaleQueue.pop();
diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp
index dea96e2012..e2601e6465 100644
--- a/indra/newview/llviewerwindow.cpp
+++ b/indra/newview/llviewerwindow.cpp
@@ -5597,7 +5597,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/pipeline.cpp b/indra/newview/pipeline.cpp
index 4ef88f5deb..f18ec727df 100644
--- a/indra/newview/pipeline.cpp
+++ b/indra/newview/pipeline.cpp
@@ -9461,6 +9461,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;
@@ -10845,6 +10848,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();