summaryrefslogtreecommitdiff
path: root/indra/newview/llviewertexture.cpp
diff options
context:
space:
mode:
authorJonathan "Geenz" Goodman <geenz@geenzo.com>2026-07-07 04:59:59 -0400
committerJonathan "Geenz" Goodman <geenz@geenzo.com>2026-07-07 04:59:59 -0400
commit6622a5694b5ccd1da77e1564c6475eaf81095f5b (patch)
tree3e2e3242a3f0d81843972e253e219a3208ba325d /indra/newview/llviewertexture.cpp
parent3e756b57f42e61f6aad3991c7bfa0a581c894244 (diff)
Get some more wins for overall downrezzing and minimizing thrashing.
Also make sure that probes don't stamp - that creates thrashing when distant probes render. We're at a low enough resolution on those things that higher mips are likely to go unnoticed, and we bake them frequently enough that we can pick up lower mips when we actually need them.
Diffstat (limited to 'indra/newview/llviewertexture.cpp')
-rw-r--r--indra/newview/llviewertexture.cpp245
1 files changed, 199 insertions, 46 deletions
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;