diff options
Diffstat (limited to 'indra')
| -rw-r--r-- | indra/llrender/llimagegl.cpp | 56 | ||||
| -rw-r--r-- | indra/llrender/llimagegl.h | 32 | ||||
| -rw-r--r-- | indra/llrender/llrender.cpp | 7 | ||||
| -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 |
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(); |
