diff options
| author | Jonathan "Geenz" Goodman <geenz@geenzo.com> | 2026-07-07 04:59:59 -0400 |
|---|---|---|
| committer | Jonathan "Geenz" Goodman <geenz@geenzo.com> | 2026-07-07 04:59:59 -0400 |
| commit | 6622a5694b5ccd1da77e1564c6475eaf81095f5b (patch) | |
| tree | 3e2e3242a3f0d81843972e253e219a3208ba325d /indra/newview | |
| parent | 3e756b57f42e61f6aad3991c7bfa0a581c894244 (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')
| -rw-r--r-- | indra/newview/app_settings/settings.xml | 50 | ||||
| -rw-r--r-- | indra/newview/llfetchedgltfmaterial.cpp | 26 | ||||
| -rw-r--r-- | indra/newview/llviewertexture.cpp | 245 | ||||
| -rw-r--r-- | indra/newview/llviewertexture.h | 54 | ||||
| -rw-r--r-- | indra/newview/llviewertexturelist.cpp | 3 | ||||
| -rw-r--r-- | indra/newview/llviewerwindow.cpp | 8 | ||||
| -rw-r--r-- | indra/newview/pipeline.cpp | 6 |
7 files changed, 313 insertions, 79 deletions
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(); |
