From 82db4943e011fc65e9a0b87990abb49b898a7782 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 12 May 2026 20:23:44 -0400 Subject: Rework texture streaming and tracking. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a big one: - Reworks the discard signal almost entirely. Now has a normalized 0..1 discard signal: distance x size x channel exponent, floored by staleness and background app state. Shaped by VRAM pressure. - Textures can now scale down to the smallest GPU mip (1×1), independent of the codec's encoded mip count. - Terrain texture LOD now works. Useful for 2K textures and PBR on terrain. Based upon camera distance to nearest terrain patch. - New texture quality setting. Low/Medium/High/Ultra - Caps texture resolution on Low to 1024, and otherwise shifts the discard signal around. Makes distance based texture LOD work a lot more predictably. - We now track last bind state for textures, and discard accordingly. We progressively discard based upon last bind time. - Avatar textures get a residency boost to stay loaded in VRAM longer under pressure. --- indra/llrender/llimagegl.cpp | 84 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 9 deletions(-) (limited to 'indra/llrender/llimagegl.cpp') diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 4a3d32c7ff..d79d13dc8b 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -66,8 +66,8 @@ static LLMutex sTexMemMutex; static std::unordered_map sTextureAllocs; static U64 sTextureBytes = 0; -// track a texture alloc on the currently bound texture. -// asserts that no currently tracked alloc exists +// Per-mip upload paths call this once per level; only free_tex_image +// removes a texture's accounting entirely. void LLImageGLMemory::alloc_tex_image(U32 width, U32 height, U32 intformat, U32 count) { U32 texUnit = gGL.getCurrentTexUnitIndex(); @@ -80,15 +80,46 @@ void LLImageGLMemory::alloc_tex_image(U32 width, U32 height, U32 intformat, U32 sTexMemMutex.lock(); - // it is a precondition that no existing allocation exists for this texture - llassert(sTextureAllocs.find(texName) == sTextureAllocs.end()); - - sTextureAllocs[texName] = size; + auto iter = sTextureAllocs.find(texName); + if (iter != sTextureAllocs.end()) + { + iter->second += size; + } + else + { + sTextureAllocs[texName] = size; + } sTextureBytes += size; sTexMemMutex.unlock(); } +// Add mip 1..N bytes to existing accounting. Use after glGenerateMipmap. +void LLImageGLMemory::account_extra_mip_bytes(U32 base_width, U32 base_height, U32 intformat) +{ + U64 extra = 0; + U32 w = base_width; + U32 h = base_height; + while (w > 1 || h > 1) + { + w = w > 1 ? w >> 1 : 1; + h = h > 1 ? h >> 1 : 1; + extra += LLImageGL::dataFormatBytes(intformat, w, h); + } + + U32 texUnit = gGL.getCurrentTexUnitIndex(); + U32 texName = gGL.getTexUnit(texUnit)->getCurrTexture(); + + sTexMemMutex.lock(); + auto iter = sTextureAllocs.find(texName); + if (iter != sTextureAllocs.end()) + { + iter->second += extra; + sTextureBytes += extra; + } + sTexMemMutex.unlock(); +} + // track texture free on given texName void LLImageGLMemory::free_tex_image(U32 texName) { @@ -838,7 +869,7 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 mMipLevels = wpo2(llmax(w, h)); //use legacy mipmap generation mode (note: making this condional can cause rendering issues) - // -- but making it not conditional triggers deprecation warnings when core profile is enabled + // - but making it not conditional triggers deprecation warnings when core profile is enabled // (some rendering issues while core profile is enabled are acceptable at this point in time) if (!LLRender::sGLCoreProfile) { @@ -864,6 +895,7 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 { LL_PROFILE_GPU_ZONE("generate mip map"); glGenerateMipmap(mTarget); + account_extra_mip_bytes(w, h, mFormatInternal); } stop_glerror(); } @@ -1461,7 +1493,12 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt LL_PROFILE_ZONE_NUM(width); LL_PROFILE_ZONE_NUM(height); - free_cur_tex_image(); + // Release prior accounting only on the base mip; per-mip iteration + // accumulates the rest via the additive alloc_tex_image. + if (miplevel == 0) + { + free_cur_tex_image(); + } const bool use_sub_image = should_stagger_image_set(compress); if (!use_sub_image) { @@ -2039,6 +2076,28 @@ S32 LLImageGL::getWidth(S32 discard_level) const return width; } +// static +S32 LLImageGL::dimDerivedMaxDiscard(S32 width, S32 height) +{ + if (width <= 0 || height <= 0) + { + return 0; + } + // max(w,h) - min() caps short on rectangular textures + // (1024x512 reaches 1x1 at discard 10, not 9). + return (S32)floorf(log2f((F32)llmax(width, 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. + if (mLastBindTime != sLastFrameTime) + { + mLastBindTime = sLastFrameTime; + } +} + S64 LLImageGL::getBytes(S32 discard_level) const { if (discard_level < 0) @@ -2468,7 +2527,12 @@ bool LLImageGL::scaleDown(S32 desired_discard) return false; } - desired_discard = llmin(desired_discard, mMaxDiscardLevel); + // GL pyramid reaches 1x1 regardless of codec levels; + // mMaxDiscardLevel is hardcapped at MAX_DISCARD_LEVEL. + S32 dim_max_discard = (mWidth > 0 && mHeight > 0) + ? dimDerivedMaxDiscard(mWidth, mHeight) + : (S32)mMaxDiscardLevel; + desired_discard = llmin(desired_discard, dim_max_discard); if (desired_discard <= mCurrentDiscardLevel) { @@ -2501,6 +2565,7 @@ bool LLImageGL::scaleDown(S32 desired_discard) LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("scaleDown - glGenerateMipmap"); gGL.getTexUnit(0)->bind(this); glGenerateMipmap(mTarget); + account_extra_mip_bytes(desired_width, desired_height, mFormatInternal); gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); } } @@ -2546,6 +2611,7 @@ bool LLImageGL::scaleDown(S32 desired_discard) { LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("scaleDown - glGenerateMipmap"); glGenerateMipmap(mTarget); + account_extra_mip_bytes(desired_width, desired_height, mFormatInternal); } gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); -- cgit v1.3 From e7263854de98868fd2b3205e09849fd6f04fb35b Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 19 May 2026 12:19:11 -0400 Subject: More high pressure and quality changes. New "high res" bubble near the camera, minimum discard settings, and discard scaling. --- indra/llrender/llimagegl.cpp | 10 +- indra/llrender/llimagegl.h | 1 + indra/newview/app_settings/settings.xml | 116 ++++++++++++++++++- indra/newview/lltextureview.cpp | 8 +- indra/newview/llviewertexture.cpp | 195 ++++++++++++++++++++++++++------ indra/newview/llviewertexture.h | 14 ++- indra/newview/llviewertexturelist.cpp | 70 +++++++++--- indra/newview/llviewertexturelist.h | 4 + 8 files changed, 355 insertions(+), 63 deletions(-) (limited to 'indra/llrender/llimagegl.cpp') diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index d79d13dc8b..a1396aba20 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -715,7 +715,10 @@ void LLImageGL::dump() //---------------------------------------------------------------------------- void LLImageGL::forceUpdateBindStats(void) const { - mLastBindTime = sLastFrameTime; + // Intentionally a no-op: mLastBindTime is written only by real bind + // paths so the staleness signal reflects actual GPU use. Callers that + // still invoke this (avatar "keep alive" sites, deleted-texture + // fallback) no longer falsely refresh staleness. } bool LLImageGL::updateBindStats() const @@ -1650,7 +1653,6 @@ bool LLImageGL::createGLTexture(S32 discard_level, const LLImageRaw* imageraw, S { destroyGLTexture(); mCurrentDiscardLevel = discard_level; - mLastBindTime = sLastFrameTime; mGLTextureCreated = false; return true ; } @@ -1766,9 +1768,7 @@ bool LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, bool data_ mTextureMemory = (S64Bytes)getMipBytes(mCurrentDiscardLevel); - - // mark this as bound at this point, so we don't throw it out immediately - mLastBindTime = sLastFrameTime; + mGLCreateTime = sLastFrameTime; checkActiveThread(); return true; diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index a02c320738..0c85446b84 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -239,6 +239,7 @@ public: // Various GL/Rendering options S64Bytes mTextureMemory; mutable F32 mLastBindTime = 0.f; // wall-clock time at last stampBound; drives streaming staleness + F32 mGLCreateTime = 0.f; // wall-clock time the GL texture was created; staleness fallback for never-bound textures private: U32 createPickMask(S32 pWidth, S32 pHeight); diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 8559fa4ac3..0c4df7a805 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11874,13 +11874,13 @@ TextureMemoryPressureRampRate Comment - Feedback rate (per second) for the VRAM-pressure factor (0..1). Higher = faster convergence on the budget; lower = gentler. + Geometric ramp/decay rate (per second) for the VRAM-pressure distance multiplier (>= 1). Higher = faster convergence on the budget; lower = gentler. Multiplier compresses the streaming distance signal: at mult=N, faces past (ramp_range / N) hit max discard. Persist 1 Type F32 Value - 3.0 + 1.0 TextureMemoryPressureBackoffStart @@ -11893,6 +11893,83 @@ Value 0.85 + TextureMemoryPressureMaxMultiplier + + Comment + Upper bound on the VRAM-pressure distance multiplier (>= 1). Mostly defensive -- at mult=64 the streaming ramp collapses to ~ramp_range/64, already extreme. Higher allows even more aggressive compression in tight-budget scenes. + Persist + 1 + Type + F32 + Value + 64.0 + + TextureLastDitchEngageProgress + + Comment + mult_progress (0..1) at which the last-ditch floor starts creeping up. The floor only advances when mult is at or above this fraction of its cap AND prediction is still over budget. Decays back toward 0 whenever prediction is under budget. + Persist + 1 + Type + F32 + Value + 0.95 + + TextureLastDitchRampRate + + Comment + Rate (discard levels/sec) at which sLastDitchMinDiscard creeps up while engaged. 0.5 = takes ~2 sec to add one discard level. Mirrors sDesiredDiscardBias ramp shape. + Persist + 1 + Type + F32 + Value + 0.5 + + TextureLastDitchDecayRate + + Comment + Rate (discard levels/sec) at which sLastDitchMinDiscard decays back to 0 when prediction is under budget. + Persist + 1 + Type + F32 + Value + 0.5 + + TextureLastDitchMinDiscardMax + + Comment + Hard ceiling on sLastDitchMinDiscard. At 13 the floor can climb all the way to the deepest meaningful mip; lower values cap how aggressive the last-ditch escalation can get before we are simply out of discards. + Persist + 1 + Type + F32 + Value + 13.0 + + TextureMemoryPressurePredictionGain + + Comment + Power exponent mapping predicted-over-budget ratio to target multiplier. target_mult = pred_over^gain. Higher gain saturates faster. + Persist + 1 + Type + F32 + Value + 10.0 + + TextureMemoryPressureSmoothingRate + + Comment + Lerp rate (1/sec) at which the pressure multiplier converges to its prediction-driven target. Higher = faster response, lower = smoother. Default 4 reaches ~63% in 0.25s. + Persist + 1 + Type + F32 + Value + 4.0 + TextureTerrainDistanceFloor Comment @@ -11952,7 +12029,7 @@ TextureCloseBubbleMeters Comment - Close-camera bubble (meters). Faces inside this distance get dist_factor = 0 (no discard contribution); the ramp to 1 spans (bubble, draw_distance]. + Close-camera bubble (meters). Faces inside this distance get dist_factor = 0 (no discard contribution); the ramp to 1 spans (bubble, draw_distance]. Shrinks toward TextureCloseBubbleMinMeters as VRAM pressure ramps the multiplier toward its cap. Persist 1 Type @@ -11960,6 +12037,39 @@ Value 5.0 + TextureCloseBubbleMinMeters + + Comment + Floor (meters) for the close-camera bubble under maximum VRAM pressure. At sMemoryPressureMultiplier = TextureMemoryPressureMaxMultiplier the bubble collapses to this value, allowing eviction of even close textures when nothing else fits. + Persist + 1 + Type + F32 + Value + 3.0 + + TextureCloseBubbleShrinkThreshold + + Comment + Bubble stays at full size until mult_progress exceeds this fraction (0..1) of its range to the cap. Above that, bubble lerps from full to TextureCloseBubbleMinMeters. Keeps the bubble out of the normal feedback loop. + Persist + 1 + Type + F32 + Value + 0.8 + + TextureCloseBubbleTrackRate + + Comment + Rate (1/sec) at which the actual bubble tracks its target. Lower = smoother, slower to react. Damps short-term multiplier swings so close textures don't yo-yo. + Persist + 1 + Type + F32 + Value + 0.5 + TextureDistanceDiscardPower Comment diff --git a/indra/newview/lltextureview.cpp b/indra/newview/lltextureview.cpp index 8cbede8303..4534db958f 100644 --- a/indra/newview/lltextureview.cpp +++ b/indra/newview/lltextureview.cpp @@ -572,9 +572,13 @@ void LLGLTexMemBar::draw() LLFontGL::getFontMonospace()->renderUTF8(text, 0, 0, v_offset + line_height*8, text_color, LLFontGL::LEFT, LLFontGL::TOP); - text = llformat("Images: %d Raw: %d (%.2f MB) Saved: %d (%.2f MB) Aux: %d (%.2f MB)", image_count, raw_image_count, raw_image_bytes_MB, + text = llformat("Images: %d Raw: %d (%.2f MB) Saved: %d (%.2f MB) Aux: %d (%.2f MB) Bubble: %.1fm PressMult: %.1fx LDMin: %.1f", + image_count, raw_image_count, raw_image_bytes_MB, saved_raw_image_count, saved_raw_image_bytes_MB, - aux_raw_image_count, aux_raw_image_bytes_MB); + aux_raw_image_count, aux_raw_image_bytes_MB, + LLViewerTextureList::sCurrentBubbleMeters, + LLViewerTexture::sMemoryPressureMultiplier, + LLViewerTexture::sLastDitchMinDiscard); LLFontGL::getFontMonospace()->renderUTF8(text, 0, 0, v_offset + line_height * 7, text_color, LLFontGL::LEFT, LLFontGL::TOP); diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 727c0510f6..e55e4016ec 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -88,7 +88,8 @@ S32 LLViewerTexture::sAuxCount = 0; LLFrameTimer LLViewerTexture::sEvaluationTimer; F32 LLViewerTexture::sDesiredDiscardBias = 0.f; F32 LLViewerTexture::sBackgroundFactor = 0.f; -F32 LLViewerTexture::sMemoryPressureFactor = 0.f; +F32 LLViewerTexture::sMemoryPressureMultiplier = 1.f; +F32 LLViewerTexture::sLastDitchMinDiscard = 0.f; U32 LLViewerTexture::sBiasTexturesUpdated = 0; S32 LLViewerTexture::sMaxSculptRez = 128; //max sculpt image size @@ -532,26 +533,119 @@ void LLViewerTexture::updateClass() F32 over_pct = (used - target) / target; - // VRAM-pressure feedback loop with progressive backoff. Ramp starts at - // backoff_start x target, not at the budget cliff. Proportional in both - // directions: ramp = (over-1)(1-factor), decay = factor; converges at - // factor = 1 - 1/over_at_backoff_target. + // VRAM-pressure controller. Drives sMemoryPressureMultiplier from + // PREDICTED VRAM (used + in-flight refetch growth - in-flight downscale + // shrinkage) rather than instantaneous used. The feedback loop chasing + // instantaneous VRAM sawtooths because eviction is fast but refetch is + // slow; the prediction lets the controller see the equilibrium directly, + // so mult converges to a target value instead of cycling. { static LLCachedControl backoff_start(gSavedSettings, "TextureMemoryPressureBackoffStart", 0.85f); + static LLCachedControl max_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f); + static LLCachedControl prediction_gain(gSavedSettings, "TextureMemoryPressurePredictionGain", 10.f); + static LLCachedControl smoothing_rate(gSavedSettings, "TextureMemoryPressureSmoothingRate", 4.f); + F32 backoff_target = target * llclamp((F32)backoff_start, 0.05f, 1.f); - F32 over = used / llmax(backoff_target, 1.f); - static LLCachedControl pressure_ramp_rate(gSavedSettings, "TextureMemoryPressureRampRate", 3.0f); - F32 dt = gFrameIntervalSeconds; - if (over > 1.f) + + // Walk the texture list once per frame, summing pending size deltas. + // Approximation: bytes(d) = (w>>d) * (h>>d) * 4 * 4/3. Units match + // `used` after the /1024/512 conversion that produced it. Coarse but + // proportionally correct - the gain knob tunes absolute magnitude. + S64 pending_bytes_increase = 0; + S64 pending_bytes_decrease = 0; { - sMemoryPressureFactor += - (over - 1.f) * (1.f - sMemoryPressureFactor) * (F32)pressure_ramp_rate * dt; + LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vt - in-flight predict"); + for (auto& imagep : gTextureList) + { + if (imagep.isNull()) continue; + S32 fw = imagep->getFullWidth(); + S32 fh = imagep->getFullHeight(); + if (fw <= 0 || fh <= 0) continue; + S32 desired = imagep->getDesiredDiscardLevel(); + S32 current = imagep->getDiscardLevel(); + if (desired < 0 || current < 0 || desired == current) continue; + + S32 wd = llmax(1, fw >> desired); + S32 hd = llmax(1, fh >> desired); + S32 wc = llmax(1, fw >> current); + S32 hc = llmax(1, fh >> current); + // bpp=4, mip pyramid overhead 4/3 + S64 size_d = (S64)wd * hd * 4 * 4 / 3; + S64 size_c = (S64)wc * hc * 4 * 4 / 3; + + if (desired < current) + pending_bytes_increase += (size_d - size_c); + else + pending_bytes_decrease += (size_c - size_d); + } } - else + + // Match the unit conversion used to produce `used` (texture_bytes_alloc + // divided by 1024/512). 1024*512 = 524288. + constexpr F32 BYTES_TO_USED_UNITS = 1.f / 524288.f; + F32 predicted_used = used + + (F32)pending_bytes_increase * BYTES_TO_USED_UNITS + - (F32)pending_bytes_decrease * BYTES_TO_USED_UNITS; + F32 predicted_over = predicted_used / llmax(backoff_target, 1.f); + + // Direct target: at over=1, mult=1; growth governed by gain power. + // Smooth toward target so per-frame prediction noise doesn't jolt + // the controller. Lerp rate set so a step change in target reaches + // ~63% in 1/smoothing_rate seconds (default 0.25s). + F32 cap = llmax((F32)max_mult, 1.0001f); + F32 target_mult = llclamp(powf(llmax(predicted_over, 1.f), llmax((F32)prediction_gain, 0.0001f)), 1.f, cap); + F32 dt = (F32)gFrameIntervalSeconds; + F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)smoothing_rate, 0.f)); + sMemoryPressureMultiplier += (target_mult - sMemoryPressureMultiplier) * alpha; + sMemoryPressureMultiplier = llclamp(sMemoryPressureMultiplier, 1.f, cap); + + // Last-ditch global discard floor. Ramps up when mult is pegged near + // cap and we are still over budget; decays toward 0 when fitting. Once + // the normal compression knobs are exhausted, this creeps the discard + // floor up integer step by step, mirroring how sDesiredDiscardBias + // pushes background textures. + { + static LLCachedControl ld_engage(gSavedSettings, "TextureLastDitchEngageProgress", 0.95f); + static LLCachedControl ld_ramp(gSavedSettings, "TextureLastDitchRampRate", 0.5f); + static LLCachedControl ld_decay(gSavedSettings, "TextureLastDitchDecayRate", 0.5f); + static LLCachedControl ld_max(gSavedSettings, "TextureLastDitchMinDiscardMax", 13.f); + F32 progress = llclampf((sMemoryPressureMultiplier - 1.f) / (cap - 1.f)); + bool mult_saturated = progress >= llclampf((F32)ld_engage); + if (mult_saturated && predicted_over > 1.f) + { + sLastDitchMinDiscard += llmax((F32)ld_ramp, 0.f) * dt; + } + else if (predicted_over < 1.f) + { + sLastDitchMinDiscard -= llmax((F32)ld_decay, 0.f) * dt; + } + sLastDitchMinDiscard = llclamp(sLastDitchMinDiscard, 0.f, llmax((F32)ld_max, 0.f)); + } + + F32 over = used / llmax(backoff_target, 1.f); // legacy alias used below + + // Throttled bisection log. Once per second. + static LLFrameTimer s_pressure_log_timer; + if (s_pressure_log_timer.getElapsedTimeF32() > 1.f) { - sMemoryPressureFactor -= sMemoryPressureFactor * (F32)pressure_ramp_rate * dt; + s_pressure_log_timer.reset(); + F32 mult_progress = llclampf((sMemoryPressureMultiplier - 1.f) / (cap - 1.f)); + LL_INFOS("TextureStream") << "pressure" + << " mult=" << sMemoryPressureMultiplier + << " target_mult=" << target_mult + << " progress=" << mult_progress + << " used=" << used + << " predicted=" << predicted_used + << " target=" << target + << " over=" << over + << " pred_over=" << predicted_over + << " in+=" << (S32)(pending_bytes_increase / 1024 / 1024) + << "MB in-=" << (S32)(pending_bytes_decrease / 1024 / 1024) + << "MB bias=" << sDesiredDiscardBias + << " ldmin=" << sLastDitchMinDiscard + << " dsq=" << (S32)gTextureList.mDownScaleQueue.size() + << LL_ENDL; } - sMemoryPressureFactor = llclampf(sMemoryPressureFactor); } bool is_sys_low = isSystemMemoryLow(); @@ -2181,12 +2275,11 @@ bool LLViewerFetchedTexture::updateFetch() make_request = false; } else if (mDesiredDiscardLevel > (S32)mCodecMaxDiscardLevel && - current_discard >= 0 && - current_discard <= (S32)mCodecMaxDiscardLevel) + current_discard >= 0) { - // scaleDown can serve this from the GL pyramid. (If current is - // already past codec_max, fall through so a zoom-in can rebuild — - // scaleDown only goes deeper.) + // Desired is past codec_max. Only scaleDown can satisfy it. + // Applies even when current is also past codec_max (post-scaleDown); + // re-fetching at codec_max then scaleDown-ing again is pure thrash. LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - desired > codec max"); make_request = false; } @@ -3177,6 +3270,16 @@ void LLViewerLODTexture::processTextureStats() F32 combined = distance_factor * size_factor; + // VRAM pressure: multiply the combined signal and clamp to 0..1. + // Compresses the effective draw range and picks up close-coverage + // textures (small combined) too. Applied before the channel + // exponent so subsequent transforms see a normalized 0..1 value. + // Avatar bakes exempt. + if (!isAgentAvatarBoost(mBoostLevel) && sMemoryPressureMultiplier > 1.f) + { + combined = llmin(combined * sMemoryPressureMultiplier, 1.f); + } + // Per-channel exponent. 1.0 = baseline; <1.0 pushes combined // toward 1 (max attenuation) faster. Edges are preserved: // pow(0, p) = 0, pow(1, p) = 1. @@ -3225,16 +3328,6 @@ void LLViewerLODTexture::processTextureStats() combined = llmax(combined, bg); } - // VRAM pressure: pow(combined, 1 - factor) bends the curve - // without flattening it - pow(0, p) = 0 so close textures - // (combined ~ 0) stay near 0 while mid/far push toward 1. - // Avatar bakes exempt. - if (!isAgentAvatarBoost(mBoostLevel) && sMemoryPressureFactor > 0.f) - { - F32 pressure_exp = llmax(1.f - sMemoryPressureFactor, 0.0001f); - combined = powf(combined, pressure_exp); - } - discard_level = combined * dim_max_for_image; } @@ -3252,21 +3345,36 @@ void LLViewerLODTexture::processTextureStats() mDesiredDiscardLevel = llmin(effective_cap, (S32)discard_level); - // Apply the setMinDiscardLevel cap, relaxed proportionally under - // VRAM pressure - at factor=1 the cap reaches dim_max so capped - // textures (terrain, etc.) participate fully in eviction. Caps of - // 0 (thumbnails) and avatar bakes are preserved. + // Apply the setMinDiscardLevel cap, relaxed under VRAM pressure + // (cap_relax = 1 - 1/mult: 0 at mult=1, ~0.5 at mult=2, ~0.9 at + // mult=10). Caps of 0 (thumbnails) and avatar bakes are preserved. S32 effective_min_cap = mMinDesiredDiscardLevel; - if (sMemoryPressureFactor > 0.f && + if (sMemoryPressureMultiplier > 1.f && mMinDesiredDiscardLevel > 0 && mMinDesiredDiscardLevel < S8_MAX && !isAgentAvatarBoost(mBoostLevel)) { + F32 cap_relax = 1.f - 1.f / sMemoryPressureMultiplier; F32 room = (F32)dim_max_for_image_i - (F32)mMinDesiredDiscardLevel; - effective_min_cap += (S32)(sMemoryPressureFactor * room); + effective_min_cap += (S32)(cap_relax * room); effective_min_cap = llmin(effective_min_cap, dim_max_for_image_i); } mDesiredDiscardLevel = llmin((S8)effective_min_cap, mDesiredDiscardLevel); + // Last-ditch global discard floor, driven by sLastDitchMinDiscard. + // That state creeps up step-by-step while mult is pegged and we are + // still over budget (see updateClass), and decays when we fit. Force + // every non-avatar-bake texture to at least floor(sLastDitchMinDiscard) + // discard, capped at the per-texture max. + if (!isAgentAvatarBoost(mBoostLevel)) + { + S32 forced = (S32)floorf(sLastDitchMinDiscard); + forced = llclamp(forced, 0, dim_max_for_image_i); + if (forced > mDesiredDiscardLevel) + { + mDesiredDiscardLevel = (S8)forced; + } + } + // // At this point we've calculated the quality level that we want, @@ -3310,6 +3418,23 @@ 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. + if (!mUseMipMaps || mDontDiscard || mBoostLevel >= LLGLTexture::BOOST_HIGH) + { + // No mip pyramid to drop into, texture is explicitly pinned full res, + // or BOOST_HIGH+ emergency-out (currently only the GLTF "force full + // res" hack hits this). + return false; + } + // Avatar bakes are exempt from mid-bake eviction (cloud avatar risk). + if (isAgentAvatarBoost(mBoostLevel)) + { + return false; + } + if (!mDownScalePending) { mDownScalePending = true; diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index e2215700c0..d40d3bc5ee 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -247,10 +247,14 @@ public: // snaps to 0 in foreground. Avatar bakes exempt. static F32 sBackgroundFactor; - // VRAM-pressure factor, 0..1. Applied in processTextureStats as - // combined = pow(combined, 1 - factor) - bends the curve without - // flattening the distance gradient. - static F32 sMemoryPressureFactor; + // VRAM-pressure distance multiplier, >= 1. Compresses the distance + // signal: dist_factor = clamp(mMinDistanceFactor * mult, 0, 1). + // Grows geometrically while over budget; decays back to 1 when fitting. + static F32 sMemoryPressureMultiplier; + // Last-ditch global discard floor. Creeps up when mult is pegged at cap + // and we are still over budget; decays back to 0 when fitting. Applied + // as a floor on mDesiredDiscardLevel for non-avatar-bake textures. + static F32 sLastDitchMinDiscard; static U32 sBiasTexturesUpdated; static S32 sMaxSculptRez ; static U32 sMinLargeImageSize ; @@ -379,7 +383,7 @@ public: void updateVirtualSize() ; - S32 getDesiredDiscardLevel() { return mDesiredDiscardLevel; } + S32 getDesiredDiscardLevel() const { return mDesiredDiscardLevel; } void setMinDiscardLevel(S32 discard) { mMinDesiredDiscardLevel = llmin(mMinDesiredDiscardLevel,(S8)discard); } void setBoostLevel(S32 level) override; diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 2e66dc2a11..ee21d6a4dd 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -71,6 +71,7 @@ void (*LLViewerTextureList::sUUIDCallback)(void **, const LLUUID&) = NULL; S32 LLViewerTextureList::sNumImages = 0; +F32 LLViewerTextureList::sCurrentBubbleMeters = 0.f; LLViewerTextureList gTextureList; @@ -937,8 +938,40 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag // dist_factor = 0 (no discard contribution). The ramp from 0 -> 1 // spans (bubble, draw_distance] rather than (0, draw_distance]. static LLCachedControl close_bubble(gSavedSettings, "TextureCloseBubbleMeters", 5.f); - F32 bubble = llclamp((F32)close_bubble, 0.f, draw_distance - 0.001f); + static LLCachedControl close_bubble_min(gSavedSettings, "TextureCloseBubbleMinMeters", 0.1f); + static LLCachedControl max_pressure_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f); + static LLCachedControl bubble_shrink_threshold(gSavedSettings, "TextureCloseBubbleShrinkThreshold", 0.8f); + static LLCachedControl bubble_track_rate(gSavedSettings, "TextureCloseBubbleTrackRate", 0.5f); + F32 bubble_full = llmax((F32)close_bubble, 0.f); + F32 bubble_min = llclamp((F32)close_bubble_min, 0.f, bubble_full); + // Target bubble: stay at full size until pressure multiplier is + // deep into its range, then collapse toward bubble_min. The bubble + // is an emergency response, not part of the normal feedback loop. + F32 mult_cap = llmax((F32)max_pressure_mult, 1.0001f); + F32 mult_progress = llclampf((LLViewerTexture::sMemoryPressureMultiplier - 1.f) / (mult_cap - 1.f)); + F32 shrink_thresh = llclampf((F32)bubble_shrink_threshold); + F32 shrink_t = (mult_progress > shrink_thresh) + ? (mult_progress - shrink_thresh) / llmax(1.f - shrink_thresh, 0.0001f) + : 0.f; + F32 target_bubble = bubble_full - (bubble_full - bubble_min) * shrink_t; + // Slow-track the actual bubble toward target so short-term multiplier + // swings don't yo-yo close textures in and out. Advance the state + // ONCE per frame, not per texture - this function runs once per + // texture so a naive per-call lerp converges in a single frame. + static F32 s_tracked_bubble = -1.f; + static U32 s_tracked_bubble_frame = 0; + if (s_tracked_bubble < 0.f) s_tracked_bubble = bubble_full; + if (s_tracked_bubble_frame != LLFrameTimer::getFrameCount()) + { + s_tracked_bubble_frame = LLFrameTimer::getFrameCount(); + F32 dt = (F32)gFrameIntervalSeconds; + F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)bubble_track_rate, 0.f)); + s_tracked_bubble += (target_bubble - s_tracked_bubble) * alpha; + s_tracked_bubble = llclamp(s_tracked_bubble, bubble_min, bubble_full); + } + F32 bubble = llclamp(s_tracked_bubble, 0.f, draw_distance - 0.001f); F32 ramp_range = llmax(draw_distance - bubble, 0.001f); + sCurrentBubbleMeters = bubble; U32 face_count = 0; U32 max_faces_to_check = 1024; @@ -1107,10 +1140,15 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag F32 grace = llmax((F32)bind_decay_seconds, 0.f); F32 interval = llmax((F32)staleness_interval, 0.0001f); - bool ever_bound = (gli->mLastBindTime > 0.f); - F32 time_since_bind = ever_bound ? (LLImageGL::sLastFrameTime - gli->mLastBindTime) : 0.f; + // Clock starts at whichever is later: the last real bind or + // the GL-create time. The latter is the fallback for textures + // decoded into GL but never actually rendered - without it, + // mLastBindTime stays 0 forever and staleness can't evict. + F32 clock_time = llmax(gli->mLastBindTime, gli->mGLCreateTime); + bool has_clock = (clock_time > 0.f); + F32 time_since = has_clock ? (LLImageGL::sLastFrameTime - clock_time) : 0.f; - if (!ever_bound || time_since_bind <= grace) + if (!has_clock || time_since <= grace) { imagep->mStalenessFactor = 0.f; } @@ -1123,7 +1161,7 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag : (S32)gli->getMaxDiscardLevel(); if (max_discard > 0) { - F32 steps = (time_since_bind - grace) / interval; + F32 steps = (time_since - grace) / interval; F32 step_size = 1.f / (F32)max_discard; imagep->mStalenessFactor = llclampf(steps * step_size); } @@ -1270,10 +1308,13 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time) // give time to downscaling first - if mDownScaleQueue is not empty, we're running out of memory and need // to free up memory by discarding off screen textures quickly - // do at least 5 and make sure we don't get too far behind even if it violates - // the time limit. If we don't downscale quickly the viewer will hit swap and may - // freeze. - S32 min_count = (S32)mCreateTextureList.size() / 20 + 5; + // Drain rate scales with both pending creates and the downscale + // queue itself. Without the queue term, a backlog of evictions + // could only drain 5/frame regardless of size, and the system + // can't actually free VRAM fast enough under pressure. + S32 min_count = (S32)mCreateTextureList.size() / 20 + + (S32)mDownScaleQueue.size() / 5 + + 5; create_timer.reset(); while (!mDownScaleQueue.empty()) @@ -1375,12 +1416,15 @@ F32 LLViewerTextureList::updateImagesFetchTextures(F32 max_time) //update MIN_UPDATE_COUNT or 5% of other textures, whichever is greater update_count = llmax((U32) MIN_UPDATE_COUNT, (U32) mUUIDMap.size()/20); - if (LLViewerTexture::sDesiredDiscardBias > 1.f + // Scale up the per-frame update window under VRAM pressure so eviction + // candidates get re-evaluated quickly. Both the legacy bias and the + // new pressure multiplier widen the window. + F32 pressure_scale = llmax(LLViewerTexture::sDesiredDiscardBias, + LLViewerTexture::sMemoryPressureMultiplier); + if (pressure_scale > 1.f && LLViewerTexture::sBiasTexturesUpdated < (U32)mUUIDMap.size()) { - // We are over memory target. Bias affects discard rates, so update - // existing textures agresively to free memory faster. - update_count = (S32)(update_count * LLViewerTexture::sDesiredDiscardBias); + update_count = (S32)(update_count * pressure_scale); // This isn't particularly precise and can overshoot, but it doesn't need // to be, just making sure it did a full circle and doesn't get stuck updating diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h index 931f2ed50e..dbed8b5c2f 100644 --- a/indra/newview/llviewertexturelist.h +++ b/indra/newview/llviewertexturelist.h @@ -244,6 +244,10 @@ private: bool mInitialized ; LLFrameTimer mForceDecodeTimer; +public: + // Current close-camera bubble in meters (frame-coherent, slow-tracked). + static F32 sCurrentBubbleMeters; + private: static S32 sNumImages; static void (*sUUIDCallback)(void**, const LLUUID &); -- cgit v1.3 From dad44ba5d67d04a73708a9a25bbe1ddec29a6a9a Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Fri, 22 May 2026 13:12:58 -0400 Subject: A few OpenGL state fixes provided by Rye from the Alchemy Viewer. --- indra/llrender/llimagegl.cpp | 2 +- indra/llrender/llrender.cpp | 15 ++++++++++++++- indra/newview/lldrawpoolwater.cpp | 8 ++++---- 3 files changed, 19 insertions(+), 6 deletions(-) (limited to 'indra/llrender/llimagegl.cpp') diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index a1396aba20..6bcc34938c 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -1895,7 +1895,7 @@ bool LLImageGL::readBackRaw(S32 discard_level, LLImageRaw* imageraw, bool compre LLGLint is_compressed = 0; if (compressed_ok) { - glGetTexLevelParameteriv(mTarget, is_compressed, GL_TEXTURE_COMPRESSED, (GLint*)&is_compressed); + glGetTexLevelParameteriv(mTarget, gl_discard, GL_TEXTURE_COMPRESSED, (GLint*)&is_compressed); } //----------------------------------------------------------------------------------------------- diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 696a0d145f..8ac906b5a1 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -197,6 +197,7 @@ void LLTexUnit::bindFast(LLTexture* texture) glActiveTexture(GL_TEXTURE0 + mIndex); gGL.mCurrTextureUnitIndex = mIndex; mCurrTexture = gl_tex->getTexName(); + mCurrTexType = gl_tex->getTarget(); // bindFast bypasses updateBindStats(); stamp directly so the staleness // signal sees per-frame use of batched textures. gl_tex->stampBound(); @@ -1732,7 +1733,16 @@ LLVertexBuffer* LLRender::genBuffer(U32 attribute_mask, S32 count) LLVertexBuffer * vb = new LLVertexBuffer(attribute_mask); vb->allocateBuffer(count, 0); - vb->setBuffer(); + // Non-Apple path uses glBufferSubData inside setXxxData, so the VBO + // must already be bound. On Apple, the VBO is lazily created in + // _unmapBuffer (LLAppleVBOPool); calling setBuffer() here would bind + // mGLBuffer == 0 and then setupVertexBuffer would issue + // glVertexAttribIPointer with a non-null offset against no bound + // GL_ARRAY_BUFFER -> GL_INVALID_OPERATION in core profile. + if (!gGLManager.mIsApple) + { + vb->setBuffer(); + } vb->setPositionData(mVerticesp.get()); @@ -1747,6 +1757,9 @@ LLVertexBuffer* LLRender::genBuffer(U32 attribute_mask, S32 count) } #if LL_DARWIN + // unmapBuffer creates the GL buffer, uploads, and leaves it bound, + // drawBuffer's later setBuffer() then runs setupVertexBuffer against + // a valid VBO. vb->unmapBuffer(); #endif vb->unbind(); diff --git a/indra/newview/lldrawpoolwater.cpp b/indra/newview/lldrawpoolwater.cpp index cdf3244389..fbecaa1583 100644 --- a/indra/newview/lldrawpoolwater.cpp +++ b/indra/newview/lldrawpoolwater.cpp @@ -204,22 +204,22 @@ void LLDrawPoolWater::renderPostDeferred(S32 pass) if (tex_a && (!tex_b || (tex_a == tex_b))) { - shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a); tex_a->setFilteringOption(filter_mode); + shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a); blend_factor = 0; // only one tex provided, no blending } else if (tex_b && !tex_a) { - shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_b); tex_b->setFilteringOption(filter_mode); + shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_b); blend_factor = 0; // only one tex provided, no blending } else if (tex_b != tex_a) { - shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a); tex_a->setFilteringOption(filter_mode); - shader->bindTexture(LLViewerShaderMgr::BUMP_MAP2, tex_b); tex_b->setFilteringOption(filter_mode); + shader->bindTexture(LLViewerShaderMgr::BUMP_MAP, tex_a); + shader->bindTexture(LLViewerShaderMgr::BUMP_MAP2, tex_b); } shader->bindTexture(LLShaderMgr::WATER_EXCLUSIONTEX, &gPipeline.mWaterExclusionMask); -- cgit v1.3 From 6622a5694b5ccd1da77e1564c6475eaf81095f5b Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 7 Jul 2026 04:59:59 -0400 Subject: 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. --- indra/llrender/llimagegl.cpp | 56 ++++++-- indra/llrender/llimagegl.h | 32 ++++- indra/llrender/llrender.cpp | 7 + indra/newview/app_settings/settings.xml | 50 ++++++- indra/newview/llfetchedgltfmaterial.cpp | 26 ++-- indra/newview/llviewertexture.cpp | 245 ++++++++++++++++++++++++++------ indra/newview/llviewertexture.h | 54 +++++-- indra/newview/llviewertexturelist.cpp | 3 + indra/newview/llviewerwindow.cpp | 8 +- indra/newview/pipeline.cpp | 6 + 10 files changed, 395 insertions(+), 92 deletions(-) (limited to 'indra/llrender/llimagegl.cpp') 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 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 #include #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 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, 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 @@ TexturePixelToTexelRatio Comment - 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). + 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. Persist 1 Type @@ -11904,6 +11904,17 @@ Value 1.0 + TextureBackgroundMinRatio + + Comment + 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. + Persist + 1 + Type + F32 + Value + 0.001 + TexturePressureHighWater Comment @@ -11962,13 +11973,46 @@ TextureCooldownStepSeconds Comment - 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. + While backgrounded, the pixel:texel ratio drops one mip toward TextureBackgroundMinRatio every this many seconds. Persist 1 Type F32 Value - 1.0 + 5.0 + + TextureGCStepFrames + + Comment + 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. + Persist + 1 + Type + U32 + Value + 5 + + TextureGCStepMips + + Comment + Mip levels dropped each time a TextureGCStepFrames cooldown elapses. 1 is gentlest; higher sheds VRAM faster in coarser jumps. + Persist + 1 + Type + U32 + Value + 1 + + TextureFetchVisibilityFrames + + Comment + 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. + Persist + 1 + Type + U32 + Value + 1 TextureDecodeDisabled 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 ratio_max(gSavedSettings, "TexturePixelToTexelRatio", 1.0f); + static LLCachedControl bg_min_ratio(gSavedSettings, "TextureBackgroundMinRatio", 0.001f); static LLCachedControl wm_high(gSavedSettings, "TexturePressureHighWater", 0.90f); static LLCachedControl wm_low(gSavedSettings, "TexturePressureLowWater", 0.70f); static LLCachedControl tighten_rate(gSavedSettings, "TexturePressureTightenRate", 0.30f); static LLCachedControl relax_rate(gSavedSettings, "TexturePressureRelaxRate", 0.10f); + static LLCachedControl 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 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 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 gc_cooldown_frames(gSavedSettings, "TextureGCStepFrames", 5); + static LLCachedControl 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(); -- cgit v1.3 From f117d33535097c3bb8d48f1f954a7a8df0449c44 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 7 Jul 2026 07:34:09 -0400 Subject: Kill the system memory stuff - it's not relevant to the new texture streaming. --- indra/llrender/llimagegl.cpp | 8 +- indra/llrender/llimagegl.h | 1 - indra/newview/app_settings/settings.xml | 11 --- indra/newview/llviewerdisplay.cpp | 5 - indra/newview/llviewermessage.cpp | 7 -- indra/newview/llviewertexture.cpp | 159 -------------------------------- indra/newview/llviewertexture.h | 22 ----- indra/newview/llvocache.cpp | 6 -- 8 files changed, 3 insertions(+), 216 deletions(-) (limited to 'indra/llrender/llimagegl.cpp') diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 95575c009b..c8a23d873e 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -493,7 +493,7 @@ bool LLImageGL::create(LLPointer& dest, const LLImageRaw* imageraw, b //---------------------------------------------------------------------------- LLImageGL::LLImageGL(bool usemipmaps/* = true*/, bool allow_compression/* = true*/) -: mSaveData(0), mExternalTexture(false) +: mExternalTexture(false) { init(usemipmaps, allow_compression); setSize(0, 0, 0); @@ -502,7 +502,7 @@ LLImageGL::LLImageGL(bool usemipmaps/* = true*/, bool allow_compression/* = true } LLImageGL::LLImageGL(U32 width, U32 height, U8 components, bool usemipmaps/* = true*/, bool allow_compression/* = true*/) -: mSaveData(0), mExternalTexture(false) +: mExternalTexture(false) { llassert( components <= 4 ); init(usemipmaps, allow_compression); @@ -512,7 +512,7 @@ LLImageGL::LLImageGL(U32 width, U32 height, U8 components, bool usemipmaps/* = t } LLImageGL::LLImageGL(const LLImageRaw* imageraw, bool usemipmaps/* = true*/, bool allow_compression/* = true*/) -: mSaveData(0), mExternalTexture(false) +: mExternalTexture(false) { init(usemipmaps, allow_compression); setSize(0, 0, 0); @@ -621,8 +621,6 @@ void LLImageGL::cleanup() destroyGLTexture(); } freePickMask(); - - mSaveData = NULL; // deletes data } //---------------------------------------------------------------------------- diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index 57ca79b3dd..ca75b543c0 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -256,7 +256,6 @@ private: void freePickMask(); bool isCompressed(); - LLPointer mSaveData; // used for destroyGL/restoreGL LL::WorkQueue::weak_t mMainQueue; U8* mPickMask; //downsampled bitmap approximation of alpha channel. NULL if no alpha channel U16 mPickMaskWidth; diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index bdcbf3b7f2..45a7dced5c 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -8002,17 +8002,6 @@ Value 1 - RenderMinFreeMainMemoryThreshold - - Comment - If available free physical memory is below this value textures get agresively scaled down - Persist - 0 - Type - U32 - Value - 512 - RenderLowMemMinDiscardIncrement Comment diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 4773a8a555..9f1b0d75f3 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -217,11 +217,6 @@ void display_update_camera() { final_far *= 0.5f; } - // When system memory is critically low or recovering, shrink draw distance. - else if (LLViewerTexture::getSystemMemoryBudgetFactor() > 1.f) - { - final_far = llmax(32.f, final_far / LLViewerTexture::getSystemMemoryBudgetFactor()); - } LLViewerCamera::getInstance()->setFar(final_far); LLVOAvatar::sRenderDistance = llclamp(final_far, 16.f, 256.f); gViewerWindow->setup3DRender(); diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 612af029b9..09f17fec40 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -3372,13 +3372,6 @@ void send_agent_update(bool force_send, bool send_reliable) static F32 last_draw_disatance_step = 1024; F32 memory_limited_draw_distance = gAgentCamera.mDrawDistance; - if (LLViewerTexture::getSystemMemoryBudgetFactor() > 1.f) - { - // We are critcally low on memory or recovering, - // limit requested draw distance - memory_limited_draw_distance = llmax(gAgentCamera.mDrawDistance / LLViewerTexture::getSystemMemoryBudgetFactor(), gAgentCamera.mDrawDistance / 2.f); - } - if (tp_state == LLAgent::TELEPORT_ARRIVING || LLStartUp::getStartupState() < STATE_MISC) { // Inform interest list, prioritize closer area. diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 47d022c854..7d07c95395 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -91,9 +91,6 @@ F32 LLViewerTexture::sPixelToTexelRatio = 1.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; @@ -105,19 +102,14 @@ U32 LLViewerTexture::sMinLargeImageSize = 65536; //256 * 256. U32 LLViewerTexture::sMaxSmallImageSize = MAX_CACHED_RAW_IMAGE_AREA; F32 LLViewerTexture::sCurrentTime = 0.0f; -constexpr F32 MEMORY_CHECK_WAIT_TIME = 1.0f; constexpr F32 MIN_VRAM_BUDGET = 768.f; F32 LLViewerTexture::sFreeVRAMMegabytes = MIN_VRAM_BUDGET; F32 LLViewerTexture::sWindowPixelArea = 1.f; -F32 LLViewerTexture::sSysMemoryFactor = 1.f; LLViewerTexture::EDebugTexels LLViewerTexture::sDebugTexelsMode = LLViewerTexture::DEBUG_TEXELS_OFF; 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 //---------------------------------------------------------------------------------------------- @@ -489,13 +481,6 @@ void LLViewerTexture::initClass() LLImageGL::sDefaultGLTexture = LLViewerFetchedTexture::sDefaultImagep->getGLTexture(); } -S32Megabytes get_render_free_main_memory_treshold() -{ - static LLCachedControl min_free_main_memory(gSavedSettings, "RenderMinFreeMainMemoryThreshold", 512); - const U32Megabytes MIN_FREE_MAIN_MEMORY(min_free_main_memory); - return MIN_FREE_MAIN_MEMORY; -} - //static void LLViewerTexture::updateClass() { @@ -544,7 +529,6 @@ void LLViewerTexture::updateClass() // 'bias' calculation to kick in. F32 vram_target = llmax(llmin(vram_budget - 512.f, vram_budget * 0.8f), MIN_VRAM_BUDGET); sFreeVRAMMegabytes = vram_target - vram_used; - const S32Megabytes free_sys_mem = getFreeSystemMemory(); // 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 @@ -598,135 +582,7 @@ void LLViewerTexture::updateClass() { sGCSuspendedFrame = LLFrameTimer::getFrameCount(); } - - // 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; - } - } - - // System-memory -> draw-distance factor. Separate from the VRAM ratio above: - // this is a last-resort response to running low on *system* RAM and only - // affects draw distance (via getSystemMemoryBudgetFactor, consumed by - // llviewerdisplay). Textures were mostly moved to VRAM, so this rarely fires. - bool is_sys_critically_low = isSystemMemoryCritical(); - static bool sys_was_low = false; - - // System memory factor - // sSysMemoryFactor affects draw distance - // - // We only decrement when more than 406MB is free, but increment - // when below 256MB free. This should provide a stable value - // in the 256-406MB range to avoid draw range fluctuations. - // - // Draw range reduction is a last resort, texture bias is supposed - // to free at least some memory before we get here. - // Note: textures were mostly moved to vram, we might want to - // detach texture bias from system memory. - if (is_sys_critically_low) - { - const S32Megabytes MIN_FREE_MAIN_MEMORY(get_render_free_main_memory_treshold() / 2); - // debt is a negative value since MIN_FREE_MAIN_MEMORY > free memory. - S32 sys_budget_debt = free_sys_mem - MIN_FREE_MAIN_MEMORY; - - // Leave some padding, otherwise we will crash out of memory before hitting factor 2. - const S32Megabytes PAD_BUFFER(32); - S32Megabytes budget_target = MIN_FREE_MAIN_MEMORY - PAD_BUFFER; - if (!sys_was_low) - { - // Result should range from 1 at 0 debt to 2 at -224 debt, 2.14 at -256MB - F32 new_factor = 1.f - (F32)sys_budget_debt / (F32)budget_target; - sSysMemoryFactor = llmax(sSysMemoryFactor, new_factor); - } - else - { - // Slowly ramp up factor to free memory (increasing factor decreases draw range) - constexpr F32 MAX_INCREMENT = 0.05f; - F32 increment = MAX_INCREMENT * llmax(-(F32)sys_budget_debt / (F32)budget_target, 0.f); - sSysMemoryFactor += increment * gFrameIntervalSeconds; - } - sSysMemoryFactor = llclamp(sSysMemoryFactor, 1.f, 2.f); - } - else - { - const S32Megabytes MIN_FREE_MAIN_MEMORY(get_render_free_main_memory_treshold() / 2); - // Only start ramping down when we have breathing room. - // This should be under the value of isSystemMemoryLow to not throw texture - // bias into 1.5+ territory each time we fluctuate around isSystemMemoryLow's - // treshold. - const S32Megabytes MEM_THRESHOLD = MIN_FREE_MAIN_MEMORY + S32Megabytes(150); - if (free_sys_mem > MEM_THRESHOLD && sSysMemoryFactor > 1.f) - { - // Ramp down factor over time. - constexpr F32 DECREMENT = 0.02f; - sSysMemoryFactor -= DECREMENT * gFrameIntervalSeconds; - sSysMemoryFactor = llclamp(sSysMemoryFactor, 1.f, 2.f); - } } - sys_was_low = is_sys_critically_low; -} - -//static -U32Megabytes LLViewerTexture::getFreeSystemMemory() -{ - static LLFrameTimer timer; - static U32Megabytes physical_res = U32Megabytes(U32_MAX); - - if (timer.getElapsedTimeF32() < MEMORY_CHECK_WAIT_TIME) //call this once per second. - { - return physical_res; - } - - timer.reset(); - - LLMemory::updateMemoryInfo(); - physical_res = LLMemory::getAvailableMemKB(); - return physical_res; -} - -//static -bool LLViewerTexture::isSystemMemoryLow() -{ - return getFreeSystemMemory() < get_render_free_main_memory_treshold(); -} - -//static -bool LLViewerTexture::isSystemMemoryCritical() -{ - return getFreeSystemMemory() < get_render_free_main_memory_treshold() / 2; -} - -// static -F32 LLViewerTexture::getSystemMemoryBudgetFactor() -{ - return sSysMemoryFactor; } //end of static functions @@ -2320,14 +2176,7 @@ bool LLViewerFetchedTexture::updateFetch() // 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); } @@ -3308,7 +3157,6 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c static LLCachedControl 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 { @@ -3318,8 +3166,6 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c { const S32 step_mips = (S32)llmax((U32)gc_step_mips, 1u); desired = llclamp(desired + periods * step_mips, desired, dim_max_i); - mGCFloored = true; - ++sCooldownFlooredCount; } } } @@ -3406,10 +3252,6 @@ 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(); } @@ -3455,7 +3297,6 @@ bool LLViewerLODTexture::scaleDown() 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 ce9a953de5..145661fefe 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -114,11 +114,6 @@ protected: public: static void initClass(); static void updateClass(); - static bool isSystemMemoryLow(); - static bool isSystemMemoryCritical(); - - // Ranges from 1 (no RAM deficit) to 2 (RAM deficit) - static F32 getSystemMemoryBudgetFactor(); LLViewerTexture(bool usemipmaps = true); LLViewerTexture(const LLUUID& id, bool usemipmaps) ; @@ -198,8 +193,6 @@ private: friend class LLBumpImageList; friend class LLUIImageList; - static U32Megabytes getFreeSystemMemory(); - protected: friend class LLViewerTextureList; LLUUID mID; @@ -259,12 +252,6 @@ public: 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 ; static U32 sMaxSmallImageSize ; @@ -273,7 +260,6 @@ public: // estimated free memory for textures, by bias calculation static F32 sFreeVRAMMegabytes; - static F32 sSysMemoryFactor; // Viewport pixel area, refreshed once per frame. Hoisted to keep the // per-texture hot path out of gViewerWindow. static F32 sWindowPixelArea; @@ -453,14 +439,6 @@ 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). diff --git a/indra/newview/llvocache.cpp b/indra/newview/llvocache.cpp index 7618739b3c..f3efe3f3bb 100644 --- a/indra/newview/llvocache.cpp +++ b/indra/newview/llvocache.cpp @@ -488,12 +488,6 @@ void LLVOCacheEntry::updateDebugSettings() static const F32 MIN_RADIUS = 1.0f; F32 draw_radius = gAgentCamera.mDrawDistance; - if (LLViewerTexture::getSystemMemoryBudgetFactor() > 1.f) - { - // Factor is intended to go from 1.0 to 2.0 - // For safety cap reduction at 50%, we don't want to go below half of draw distance - draw_radius = llmax(draw_radius / LLViewerTexture::getSystemMemoryBudgetFactor(), draw_radius / 2.f); - } const F32 clamped_min_radius = llclamp((F32) min_radius, MIN_RADIUS, draw_radius); // [1, mDrawDistance] sNearRadius = MIN_RADIUS + ((clamped_min_radius - MIN_RADIUS) * adjust_factor); -- cgit v1.3 From a937b237de3651e79cdb517f14381a5bdd4c844b Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Thu, 9 Jul 2026 08:09:24 -0400 Subject: Geenz/texture loading speed (#5985) * Add more controls for texture loading budgets. Should yield much faster loading within a given FPS target - should generally self regulate depending on your framerate. * Harden texture pipeline against stalls and OOM. Generally makes texture loading faster, at the expense of some budgeting (which we weren't doing a great job at anyways). --- indra/llrender/llgl.cpp | 6 + indra/llrender/llgl.h | 1 + indra/llrender/llimagegl.cpp | 14 +- indra/newview/app_settings/settings.xml | 37 ++- indra/newview/llface.cpp | 22 ++ indra/newview/llface.h | 18 +- indra/newview/llviewerdisplay.cpp | 31 ++- indra/newview/llviewertexture.cpp | 112 ++++++--- indra/newview/llviewertexture.h | 19 ++ indra/newview/llviewertexturelist.cpp | 426 +++++++++++++++++++------------- indra/newview/llviewertexturelist.h | 5 + 11 files changed, 482 insertions(+), 209 deletions(-) (limited to 'indra/llrender/llimagegl.cpp') diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index 4584ed1d86..0e59c449db 100644 --- a/indra/llrender/llgl.cpp +++ b/indra/llrender/llgl.cpp @@ -2370,6 +2370,12 @@ void clear_glerror() glGetError(); } +void drain_glerror() +{ + // bounded: a lost/reset context can return errors indefinitely + for (S32 i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {} +} + /////////////////////////////////////////////////////////////// // // LLGLState diff --git a/indra/llrender/llgl.h b/indra/llrender/llgl.h index e1ab2a49e6..3f9a9de70a 100644 --- a/indra/llrender/llgl.h +++ b/indra/llrender/llgl.h @@ -159,6 +159,7 @@ void log_glerror(); void assert_glerror(); void clear_glerror(); +void drain_glerror(); // pops ALL pending GL error flags (bounded so a lost/reset context that returns errors forever cannot hang the caller); use before an attributable glGetError check. # define stop_glerror() assert_glerror() diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index c8a23d873e..b3cd8d2896 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -1507,7 +1507,7 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt // 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) {} + drain_glerror(); const bool use_sub_image = should_stagger_image_set(compress); if (!use_sub_image) @@ -1919,7 +1919,8 @@ bool LLImageGL::readBackRaw(S32 discard_level, LLImageRaw* imageraw, bool compre //----------------------------------------------------------------------------------------------- GLenum error ; - while((error = glGetError()) != GL_NO_ERROR) + S32 error_count = 0 ; + while((error = glGetError()) != GL_NO_ERROR && ++error_count <= 16) { LL_WARNS() << "GL Error happens before reading back texture. Error code: " << error << LL_ENDL ; } @@ -1979,7 +1980,8 @@ bool LLImageGL::readBackRaw(S32 discard_level, LLImageRaw* imageraw, bool compre LL_WARNS() << "GL Error happens after reading back texture. Error code: " << error << LL_ENDL ; imageraw->deleteData() ; - while((error = glGetError()) != GL_NO_ERROR) + error_count = 0 ; + while((error = glGetError()) != GL_NO_ERROR && ++error_count <= 16) { LL_WARNS() << "GL Error happens after reading back texture. Error code: " << error << LL_ENDL ; } @@ -2548,8 +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. + // Don't let eviction re-arm the GC: the glGenerateMipmap re-bind below would + // otherwise stamp mLastBindFrame, so the next computeDesiredDiscard treats the + // just-evicted texture as freshly drawn, un-floors it, and re-fetches - the + // evict/refetch oscillation. LLImageGLStampBypass no_stamp; if (mTarget != GL_TEXTURE_2D diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 45a7dced5c..1c2c2d0559 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11992,16 +11992,49 @@ Value 1 - TextureFetchVisibilityFrames + TextureLoadTargetFPS Comment - 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. + Frame rate the viewer is willing to drop to while loading textures. The texture pipeline's per-frame budget is the headroom between this and the actual frame cost, so fast machines load aggressively and slow ones hold their frame rate. + Persist + 1 + Type + F32 + Value + 30.0 + + TextureLoadBudgetMaxMS + + Comment + Hard cap, in milliseconds per frame, on the adaptive texture-pipeline budget. + Persist + 1 + Type + F32 + Value + 10.0 + + TextureFetchStepMips + + Comment + Fetch refinement step, in mip levels: a texture whose resident data is coarser than desired by more than this fetches in steps of this size instead of jumping straight to the final resolution, so it sharpens progressively instead of sitting blurry then popping. 0 = jump directly. Boosted/pinned textures always jump. Persist 1 Type U32 Value + 2 + + TextureFrustumAllowance + + Comment + Falloff width for out-of-frustum texture resolution, as a fraction of screen size. Content grazing the screen edge keeps full resolution; content this far past the edge reaches the deepest mip, lerped between. Keeps barely-out-of-view textures resident so panning back doesn't refetch them. + Persist 1 + Type + F32 + Value + 0.5 TextureDecodeDisabled diff --git a/indra/newview/llface.cpp b/indra/newview/llface.cpp index e34bea63ef..c96e1bd3b0 100644 --- a/indra/newview/llface.cpp +++ b/indra/newview/llface.cpp @@ -2297,6 +2297,8 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) // don't update every frame if (gFrameTimeSeconds - mLastPixelAreaUpdate < PIXEL_AREA_UPDATE_PERIOD) { + cos_angle_to_view_dir = mLastCosAngleToViewDir; + radius = mLastRadius; return true; } @@ -2368,6 +2370,7 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) // no rigged extents, zero out bounding box and skip update mRiggedExtents[0] = mRiggedExtents[1] = LLVector4a(0.f, 0.f, 0.f); + mInFrustum = false; return false; } @@ -2422,6 +2425,7 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) if(!camera->AABBInFrustum(center, size)) { mImportanceToCamera = 0.f ; + mInFrustum = false; return false ; } if(cos_angle_to_view_dir > camera->getCosHalfFov()) //the center is within the view frustum @@ -2450,6 +2454,24 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) mImportanceToCamera = LLFace::calcImportanceToCamera(cos_angle_to_view_dir, dist) ; } + // On-screen test: does the face's projected disc overlap the screen disc? + // (Same construction as adjustPartialOverlapPixelArea.) Behind-camera faces + // get acos(cos) near pi and fall out; the generous screen radius errs toward + // "on screen" so fetch admission never starves edge content. mFrustumOverflow + // is how far past the boundary the disc sits, as a fraction of screen size - + // 0 on screen, 0.1 = 10% of a screen out - and feeds the frustum allowance + // falloff in computeDesiredDiscard. + { + F32 center_px = acosf(llclamp(cos_angle_to_view_dir, -1.f, 1.f)) * LLDrawable::sCurPixelAngle; + F32 screen_radius = (F32)llmax(gViewerWindow->getWindowWidthRaw(), gViewerWindow->getWindowHeightRaw()); + F32 past_edge = center_px - radius - screen_radius; + mInFrustum = past_edge <= 5.f; + mFrustumOverflow = llmax(past_edge - 5.f, 0.f) / screen_radius; + } + + mLastCosAngleToViewDir = cos_angle_to_view_dir; + mLastRadius = radius; + return true ; } diff --git a/indra/newview/llface.h b/indra/newview/llface.h index 71ba3d0f2f..0b0a17b8c8 100644 --- a/indra/newview/llface.h +++ b/indra/newview/llface.h @@ -269,10 +269,21 @@ public: // return mSkinInfo->mHash or 0 if mSkinInfo is null U64 getSkinHash(); - // true if face was recently in the main camera frustum according to LLViewerTextureList updates + // True if this face's projected bounding disc overlaps the screen - maintained + // by calcPixelArea() (sticky between its throttled updates). Drives per-texture + // fetch admission (LLViewerTextureList::updateImageDecodePriority -> mOnScreen). bool mInFrustum = false; + // How far past the screen boundary the projected disc sits, as a fraction of + // screen size (0 = on screen). Feeds the frustum-allowance falloff so barely + // out-of-view content keeps its resolution. Maintained with mInFrustum. + F32 mFrustumOverflow = 0.f; // value of gFrameCount the last time the face was touched by LLViewerTextureList::updateImageDecodePriority U32 mLastTextureUpdate = 0; + // Cached per-channel streaming coverage (repeat-adjusted screen pixels), + // refreshed at the mLastTextureUpdate cadence and shared by every texture + // on this face. 0 = degenerate / not yet measured. See + // update_face_stream_vsize in llviewertexturelist.cpp. + F32 mStreamVSize[LLRender::NUM_TEXTURE_CHANNELS] = {}; private: LLPointer mVertexBuffer; @@ -309,6 +320,11 @@ private: // gFrameTimeSeconds when mPixelArea was last updated F32 mLastPixelAreaUpdate = 0.f; + // Last cos-angle-to-view-dir and projected radius computed by calcPixelArea; + // reused by its throttled early-return so the overlap test gets real values. + F32 mLastCosAngleToViewDir = 1.f; + F32 mLastRadius = 0.f; + // virtual size of face in texture area (mPixelArea adjusted by texture repeats) // used to determine desired resolution of texture F32 mVSize; diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 0d50ba6fe2..be83fd0279 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -140,6 +140,27 @@ void render_disconnected_background(); void getProfileStatsContext(boost::json::object& stats); std::string getProfileStatsFilename(); +// Adaptive texture-pipeline budget: spend the frame-time headroom between the +// frame we're rendering and TextureLoadTargetFPS, clamped [2ms, max]. Headroom +// is measured (smoothed frame interval minus the pipeline's own last spend), +// so fast machines get big budgets and machines already at target hold the +// floor. Only consumed while queues have work - drain loops exit when empty. +static F32 sTexturePipelineSpent = 0.f; +static F32 texture_pipeline_budget() +{ + static LLCachedControl target_fps(gSavedSettings, "TextureLoadTargetFPS", 60.f); + static LLCachedControl max_ms(gSavedSettings, "TextureLoadBudgetMaxMS", 10.f); + static F32 smoothed_other = 0.008f; + F32 other = llmax(gFrameIntervalSeconds.value() - sTexturePipelineSpent, 0.f); + // A single multi-second hitch must not crater the budget for the following + // frames, so cap the sample before it enters the EMA. + other = llmin(other, 0.1f); + smoothed_other = smoothed_other * 0.9f + other * 0.1f; + F32 target_interval = 1.f / llclamp((F32)target_fps, 15.f, 240.f); + F32 headroom = target_interval - smoothed_other; + return llclamp(headroom, 0.002f, llclamp((F32)max_ms, 2.f, 50.f) * 0.001f); +} + void display_startup() { if ( !gViewerWindow @@ -500,9 +521,10 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) { LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("List"); - F32 max_image_decode_time = 0.050f * gFrameIntervalSeconds.value(); // 50 ms/second decode time - max_image_decode_time = llclamp(max_image_decode_time, 0.002f, 0.005f); // min 2ms/frame, max 5ms/frame) + F32 max_image_decode_time = texture_pipeline_budget(); + LLTimer tex_timer; gTextureList.updateImages(max_image_decode_time); + sTexturePipelineSpent = tex_timer.getElapsedTimeF32(); } { @@ -862,9 +884,10 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) { LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("List"); - F32 max_image_decode_time = 0.050f*gFrameIntervalSeconds.value(); // 50 ms/second decode time - max_image_decode_time = llclamp(max_image_decode_time, 0.002f, 0.005f ); // min 2ms/frame, max 5ms/frame) + F32 max_image_decode_time = texture_pipeline_budget(); + LLTimer tex_timer; gTextureList.updateImages(max_image_decode_time); + sTexturePipelineSpent = tex_timer.getElapsedTimeF32(); } { diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 80daffdbf3..90facfa333 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -571,6 +571,20 @@ void LLViewerTexture::updateClass() sPixelToTexelRatio += llmax((F32)relax_rate, 0.f) * dt; } // else: hold in the hysteresis band. + + // Allocation failures outrank the byte estimate: if setManualImage hit + // GL_OUT_OF_MEMORY since last frame, the CPU-side vram_used estimate has + // diverged from reality, so step the ratio down now regardless of the band. + static U32 last_oom_count = 0; + U32 oom_count = LLImageGL::sOOMErrorCount.load(); + if (oom_count > last_oom_count) + { + U32 new_events = llmin(oom_count - last_oom_count, (U32)5); + sPixelToTexelRatio -= llmax((F32)tighten_rate, 0.f) * 1.0f * (F32)new_events; + last_oom_count = oom_count; + LL_WARNS_ONCE("Texture") << "GL out-of-memory during texture upload triggered a pixel:texel ratio backoff." << LL_ENDL; + } + sPixelToTexelRatio = llclamp(sPixelToTexelRatio, 0.f, r_max); // Keep the GC-suspend frame current while backgrounded. This suppresses @@ -1483,6 +1497,16 @@ void LLViewerFetchedTexture::postCreateTexture() setActive(); + // Start the visibility-GC clock at creation. A texture fetched but never + // drawn (occluded, or the camera moved on) would otherwise keep + // mLastBindFrame == 0 forever, and the GC skips never-bound textures - it + // would hold residency indefinitely. Anchoring here means it ages out on + // the normal GC cooldown unless a real draw stamps it first. + if (mGLTexturep.notNull() && mGLTexturep->mLastBindFrame == 0) + { + mGLTexturep->mLastBindFrame = LLFrameTimer::getFrameCount(); + } + // rebuild any volumes that are using this texture for sculpts in case their LoD has changed for (U32 i = 0; i < mNumVolumes[LLRender::SCULPT_TEX]; ++i) { @@ -1942,7 +1966,14 @@ bool LLViewerFetchedTexture::updateFetch() S32 current_discard = getCurrentDiscardLevelForFetching(); S32 desired_discard = getDesiredDiscardLevel(); - F32 decode_priority = mMaxVirtualSize; + + // Two-tier fetch priority, constantly drained by the fetch worker (it + // sorts HTTP dispatch by this value): any on-screen texture outranks every + // off-screen one. Within the visible tier, coverage orders by size on + // screen; within the off-screen tier the same geometric coverage + // (area/dist^2) orders by proximity. addTextureStats clamps + // mMaxVirtualSize to sMaxVirtualSize, so the band offset is strict. + F32 decode_priority = mMaxVirtualSize + (mOnScreen ? sMaxVirtualSize : 0.f); if (mIsFetching) { @@ -2003,6 +2034,29 @@ bool LLViewerFetchedTexture::updateFetch() // discards are served from the GL mip pyramid via scaleDown. desired_discard = llmin(desired_discard, (S32)mCodecMaxDiscardLevel); + // Progressive refinement: when resident data is much coarser than desired, + // fetch in steps of TextureFetchStepMips instead of jumping straight to the + // final discard. Each step is small, decodes fast, and shows on screen + // while the next chains behind it (the per-frame fast pump makes chaining + // nearly free). Without this, seen-before textures (dims known, so the + // coarse first-fetch fallback never applies) sat blurry for the whole + // full-file read+decode, then popped. Boosted/pinned content still jumps. + static LLCachedControl fetch_step(gSavedSettings, "TextureFetchStepMips", 2); + const S32 step = (S32)fetch_step; + if (step > 0 + && current_discard >= 0 + && desired_discard < current_discard - step + && mBoostLevel < LLGLTexture::BOOST_HIGH + && mUseMipMaps + && !mDontDiscard + && !isAgentAvatarBoost(mBoostLevel)) + { + // Re-clamp: current can sit past codec max after scaleDown (GL + // pyramid goes deeper than the codestream) - a stepped request + // above codec max reaches the decoder with an invalid discard. + desired_discard = llmin(current_discard - step, (S32)mCodecMaxDiscardLevel); + } + bool make_request = true; if (decode_priority <= 0) { @@ -2028,30 +2082,9 @@ 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 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; - } - } - } + // No visibility gate here: off-screen content still fetches, just in the + // low-priority band (decode_priority above), so the worker services it only + // after visible work. Residency stays with the GC (computeDesiredDiscard). if (make_request) { @@ -3035,6 +3068,23 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c // (a 2048 map can't hit its own resolution on a 1080p screen), which reads // as everything being blurry. Pressure still evicts by lowering R_global. ideal = llmax(ideal, 0.f); + + // Frustum allowance: a falloff on how unloaded out-of-view content gets, + // by how far out it is. Grazing the edge keeps full resolution; at + // TextureFrustumAllowance (fraction of a screen) past the edge it reaches + // the deepest mip, lerped between. Keeps barely-out-of-view textures + // resident so a camera swing back doesn't refetch them. Applied to the + // continuous ideal so it shares the hysteresis dead-band below - applied + // after it, camera motion made desired flap a mip at a time and churned + // the fetch/scaleDown queues. + static LLCachedControl frustum_allowance(gSavedSettings, "TextureFrustumAllowance", 0.2f); + if (!avatar_bake && mFrustumOverflow > 0.f) + { + const F32 f = llclamp(mFrustumOverflow / llmax((F32)frustum_allowance, 0.01f), 0.f, 1.f); + ideal += f * ((F32)dim_max_i - ideal); + mLastOffScreenFrame = LLFrameTimer::getFrameCount(); + } + const S32 target = (S32)floor(ideal); // Hysteresis: a texture at discard C is "happy" while floor(ideal) == C, @@ -3069,7 +3119,12 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c // 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) + // + // Out-of-frustum content is governed by the frustum allowance above instead + // (spatial falloff, not bind staleness) - without this exclusion the GC would + // walk barely-out-of-view content to the deepest mip within a second and the + // allowance would protect nothing. + if (!avatar_bake && mFrustumOverflow <= 0.f) { if (LLImageGL* gli = getGLTexture()) { @@ -3077,8 +3132,9 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c static LLCachedControl gc_step_mips(gSavedSettings, "TextureGCStepMips", 1); constexpr U32 GC_RESUME_GRACE_FRAMES = 10; const U32 now = LLFrameTimer::getFrameCount(); - if (gli->mLastBindFrame > 0 // drawn at least once - && now - sGCSuspendedFrame > GC_RESUME_GRACE_FRAMES) // not just back from background + if (gli->mLastBindFrame > 0 // drawn, or anchored at creation (postCreateTexture) + && now - sGCSuspendedFrame > GC_RESUME_GRACE_FRAMES // not just back from background + && now - mLastOffScreenFrame > GC_RESUME_GRACE_FRAMES) // re-entering content gets one grace window to be drawn and re-stamp before staleness is judged { const U32 cooldown = llmax((U32)gc_cooldown_frames, 1u); const S32 periods = (S32)((now - gli->mLastBindFrame) / cooldown); diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index cc9fbe5e48..90993ed109 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -215,6 +215,25 @@ protected: F32 mChannelCoverage[4] = { 0.f, 0.f, 0.f, 0.f }; F32 mChannelCoverageMin[4] = { 0.f, 0.f, 0.f, 0.f }; + // Any face using this texture projects onto the screen (published alongside + // the coverage above). Selects the fetch-priority band in updateFetch. + // Defaults true so unmeasured textures (fresh objects, no-face users) are + // never starved. + bool mOnScreen = true; + + // How far out of frustum the texture's least-out-of-view use sits, as a + // fraction of screen size (0 = on screen). Drives the frustum-allowance + // falloff in computeDesiredDiscard. + F32 mFrustumOverflow = 0.f; + + // Last frame this texture was out of frustum (mFrustumOverflow > 0). The + // GC in computeDesiredDiscard gives re-entering content one grace window to + // be drawn and re-stamp mLastBindFrame before its staleness is judged. + mutable U32 mLastOffScreenFrame = 0; + + // Membership flag for LLViewerTextureList::mFastFetchList (dedup). + bool mInFastFetchList = false; + ll_face_list_t mFaceList[LLRender::NUM_TEXTURE_CHANNELS]; //reverse pointer pointing to the faces using this image as texture U32 mNumFaces[LLRender::NUM_TEXTURE_CHANNELS]; LLFrameTimer mLastFaceListUpdateTimer ; diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 3e1481f8b4..ad05a0273b 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -378,6 +378,12 @@ void LLViewerTextureList::shutdown() } mFastCacheList.clear(); + for (auto& img : mFastFetchList) + { + img->mInFastFetchList = false; + } + mFastFetchList.clear(); + mUUIDMap.clear(); mImageList.clear(); @@ -871,6 +877,36 @@ void LLViewerTextureList::updateImages(F32 max_time) remaining_time -= updateImagesFetchTextures(remaining_time); remaining_time = llmax(remaining_time, min_time); + // Fast pump: advance every in-flight fetch each frame so results are + // collected and creates scheduled the frame they're ready, instead of + // one state transition per round-robin visit. Cheap - no face scans - + // and bounded by the fetch worker's own concurrency. + LLTimer fast_fetch_timer; + S32 min_count = 32; + for (size_t i = 0; i < mFastFetchList.size(); ) + { + LLViewerFetchedTexture* imagep = mFastFetchList[i]; + if (imagep->getNumRefs() > 1) + { + imagep->updateFetch(); + } + if (imagep->getNumRefs() <= 1 || (!imagep->isFetching() && !imagep->hasFetcher())) + { + imagep->mInFastFetchList = false; + mFastFetchList[i] = mFastFetchList.back(); + mFastFetchList.pop_back(); + } + else + { + ++i; + } + + if (fast_fetch_timer.getElapsedTimeF32() > remaining_time && --min_count <= 0) + { + break; + } + } + //handle results from decode threads updateImagesCreateTextures(remaining_time); @@ -915,6 +951,187 @@ void LLViewerTextureList::clearFetchingRequests() extern bool gCubeSnapshot; +// Refresh a face's cached per-channel streaming coverage (face->mStreamVSize). +// This is the most-demanding-point measurement plus each channel's own UV +// repeat source, computed ONCE per face per update cadence and shared by every +// texture registered on the face. Doing the material/transform pointer chases +// per texture visit instead made updateImageDecodePriority several times more +// expensive per face than develop's, and since the round-robin runs in a fixed +// per-frame time slice, that directly cut how many textures advance their +// load state each frame - the whole pipeline paced slower. +static void update_face_stream_vsize(LLFace* face) +{ + // Bounds on the per-face UV repeat-area divisor (mined from the old + // getTextureVirtualSize texel_area clamp [1/64, 128]): atlas/crop boost + // capped at 64x (3 mips finer), tiling penalty at 128x (3.5 mips + // coarser) so pathological UV scales can't explode either direction. + constexpr F32 MIN_REPEAT_AREA = 1.f / 64.f; + constexpr F32 MAX_REPEAT_AREA = 128.f; + + LLViewerObject* objp = face->getViewerObject(); + + // Most-demanding-point measurement: the spec is that the LOWEST pixel:texel + // ratio governs, so pixel density is evaluated at the face's NEAREST point + // and applied to the face's true world area. A whole-face average + // (bounding-disc pixel area) under-resolves perspective surfaces: on a + // floor, the tile at your feet covers far more screen than the average + // tile, and the GPU samples fine mips right there. + const LLVector4a* ext = face->isState(LLFace::RIGGED) ? face->mRiggedExtents : face->mExtents; + LLVector4a diag; + diag.setSub(ext[1], ext[0]); + // World area of the face ~ product of the two largest AABB dims (max + // pairwise product; robust for flat faces). + F32 dx = diag[0], dy = diag[1], dz = diag[2]; + F32 area_world = llmax(dx * dy, llmax(dx * dz, dy * dz)); + // Pixels per meter at the nearest point. Distance floored: nearer than + // this the screen clamp below governs anyway. + F32 dist = llmax(face->mDistanceToCamera, 0.5f); + F32 ppm = LLDrawable::sCurPixelAngle / dist; + F32 face_px = area_world * ppm * ppm; + if (face_px <= 0.f) + { + // Degenerate extents: the face hasn't been through a geometry build + // yet (or a rigged face has no rigged extents) - it isn't renderable, + // so it must not be measured. Zero marks "skip": an invented + // placeholder value would become the texture's least-demanding "use" + // and, under TextureDownrezCoverageBias, drag the whole texture to + // its deepest mip (and it poisoned BP and PBR asymmetrically, since + // the two register faces at different points in the geometry + // lifecycle). + for (U32 ch = 0; ch < LLRender::NUM_TEXTURE_CHANNELS; ++ch) + { + face->mStreamVSize[ch] = 0.f; + } + return; + } + + S32 te_offset = face->getTEOffset(); // offset is -1 if not inited + const LLTextureEntry* te = (te_offset < 0 || te_offset >= objp->getNumTEs()) ? nullptr : objp->getTE(te_offset); + + // Shared, channel-independent chases - hoisted out of the channel loop. + const LLGLTFMaterial* gltf_mat = te ? te->getGLTFRenderMaterial() : nullptr; + const LLMaterial* mat = te ? te->getMaterialParams().get() : nullptr; + + // Continuously-animated scale (llSetTextureAnim SCALE) bypasses both + // static sources via mTextureMatrix - the live animated values win. + bool anim_scale = false; + F32 anim_ss = 0.f, anim_st = 0.f; + if (te) + { + if (LLVOVolume* vvo = face->getDrawable() ? face->getDrawable()->getVOVolume() : nullptr) + { + LLViewerTextureAnim* anim = vvo->mTextureAnimp; + if (anim && (anim->mMode & LLTextureAnim::ON) && (anim->mMode & LLTextureAnim::SCALE) + && (anim->mFace < 0 || anim->mFace == te_offset)) + { + anim_scale = true; + anim_ss = anim->mScaleS; + anim_st = anim->mScaleT; + } + } + } + + // Mesh atlas sub-rect: a face whose intrinsic UVs span only part of + // [0,1]^2 shows that fraction of the image. Applies identically to all + // channels - the per-channel transforms stack on the raw face UVs. + F32 span = 1.f; + if (te) + { + if (LLVolume* vol = objp->getVolume()) + { + if (te_offset >= 0 && te_offset < vol->getNumVolumeFaces()) + { + const LLVolumeFace& vf = vol->getVolumeFace(te_offset); + F32 s = fabsf((vf.mTexCoordExtents[1].mV[0] - vf.mTexCoordExtents[0].mV[0]) + * (vf.mTexCoordExtents[1].mV[1] - vf.mTexCoordExtents[0].mV[1])); + if (s > 0.f) + { + span = s; + } + } + } + } + + // Avatar bonus: worn attachments get a coverage multiplier - avatars are + // what people look at, and rigged extents make attachment coverage + // measurement unreliable anyway. Multiplicative, not a slam. + static LLCachedControl avatar_boost(gSavedSettings, "TextureAvatarBoost", 4.f); + const F32 boost = objp->isAttachment() ? llmax((F32)avatar_boost, 1.f) : 1.f; + + for (U32 ch = 0; ch < LLRender::NUM_TEXTURE_CHANNELS; ++ch) + { + // Effective UV repeat AREA: the tiling term of texels-drawn-per- + // screen-pixel. More tiling => each tile smaller on screen => coarser + // mips suffice (penalty). Repeats < 1 (atlas/crop) => whole-image + // residency for a sub-rect legitimately demands more than its screen + // coverage (boost). + F32 repeats = 1.f; + if (te) + { + // UV scale source: every channel reads the repeat values ITS + // renderer actually applies. diffuse -> TE scale; Blinn + // normal/spec -> LLMaterial per-map repeats; PBR channels -> KHR + // texture_transform scale. Fallback is the TE scale - never a + // silent hardcoded 1. + F32 scale_s = te->getScaleS(); + F32 scale_t = te->getScaleT(); + if (ch >= LLRender::BASECOLOR_MAP) + { + // LLRender channel -> LLGLTFMaterial::TextureInfo + static const S32 gltf_info[4] = { + LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR, // BASECOLOR_MAP (3) + LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, // METALLIC_ROUGHNESS_MAP (4) + LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL, // GLTF_NORMAL_MAP (5) + LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE, // EMISSIVE_MAP (6) + }; + if (gltf_mat) + { + const LLVector2& s = gltf_mat->mTextureTransform[gltf_info[ch - LLRender::BASECOLOR_MAP]].mScale; + scale_s = s.mV[0]; + scale_t = s.mV[1]; + } + } + else if (ch == LLRender::NORMAL_MAP || ch == LLRender::SPECULAR_MAP) + { + // Blinn-Phong normal/specular maps carry their own repeats in + // LLMaterial - the renderer builds their texture matrices + // from these, NOT from the TE's diffuse scale. + if (mat) + { + if (ch == LLRender::NORMAL_MAP) + { + mat->getNormalRepeat(scale_s, scale_t); + } + else + { + mat->getSpecularRepeat(scale_s, scale_t); + } + } + } + + if (anim_scale) + { + scale_s = anim_ss; + scale_t = anim_st; + } + + repeats = fabsf(scale_s * scale_t) * span; + } + + repeats = llclamp(repeats, MIN_REPEAT_AREA, MAX_REPEAT_AREA); + + // Apply the two sides of the repeat term in the right order relative + // to the screen clamp: tiling (repeats > 1) divides the nearest-point + // footprint BEFORE the clamp (one tile can't draw more pixels than + // the screen); atlas/crop (repeats < 1) boosts AFTER it (whole-image + // residency for a crop legitimately demands more than its screen + // coverage). + F32 tiling = llmax(repeats, 1.f); + F32 crop = llmin(repeats, 1.f); + face->mStreamVSize[ch] = llmin(face_px / tiling, LLViewerTexture::sWindowPixelArea) / crop * boost; + } +} + void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imagep, bool flush_images) { llassert(!gCubeSnapshot); @@ -931,13 +1148,6 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag if (imagep->getBoostLevel() < LLViewerFetchedTexture::BOOST_HIGH) // don't bother checking face list for boosted textures { - // Bounds on the per-face UV repeat-area divisor (mined from the old - // getTextureVirtualSize texel_area clamp [1/64, 128]): atlas/crop boost - // capped at 64x (3 mips finer), tiling penalty at 128x (3.5 mips - // coarser) so pathological UV scales can't explode either direction. - constexpr F32 MIN_REPEAT_AREA = 1.f / 64.f; - constexpr F32 MAX_REPEAT_AREA = 128.f; - // Per priority bucket (0=Normal, 1=BaseColor, 2=Specular, 3=Emissive): // the HIGHEST per-face effective coverage (= the lowest texels-per-pixel // use, the most demanding variant - drives desired discard) and the @@ -950,6 +1160,9 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag F32 channel_coverage_min[4] = { FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX }; bool bucket_used[4] = { false, false, false, false }; F32 max_coverage = 0.f; + bool on_screen = false; // any face's projected disc overlaps the screen + bool any_face = false; + F32 min_overflow = FLT_MAX; // least out-of-frustum use across faces U32 face_count = 0; @@ -1000,175 +1213,29 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag F32 radius; F32 cos_angle_to_view_dir; if ((gFrameCount - face->mLastTextureUpdate) > 10) - { // only call calcPixelArea at most once every 10 frames for a given face - // this helps eliminate redundant calls to calcPixelArea for faces that have multiple textures - // assigned to them, such as is the case with GLTF materials or Blinn-Phong materials - face->mInFrustum = face->calcPixelArea(cos_angle_to_view_dir, radius); + { // refresh the face's geometry + cached coverage at most once every + // 10 frames; every texture/channel sharing this face (GLTF and + // Blinn-Phong materials) reuses the cache instead of redoing the + // measurement. (calcPixelArea maintains face->mInFrustum itself.) + face->calcPixelArea(cos_angle_to_view_dir, radius); + update_face_stream_vsize(face); face->mLastTextureUpdate = gFrameCount; } - // Most-demanding-point measurement: the spec is that the - // LOWEST pixel:texel ratio governs, so pixel density is - // evaluated at the face's NEAREST point and applied to the - // face's true world area. The previous whole-face average - // (bounding-disc pixel area) under-resolved perspective - // surfaces: on a floor, the tile at your feet covers far - // more screen than the average tile, and the GPU samples - // fine mips right there - tiled (PBR-heavy) content went - // soft while untiled content looked fine. - const LLVector4a* ext = face->isState(LLFace::RIGGED) ? face->mRiggedExtents : face->mExtents; - LLVector4a diag; - diag.setSub(ext[1], ext[0]); - // World area of the face ~ product of the two largest AABB - // dims (max pairwise product; robust for flat faces). - F32 dx = diag[0], dy = diag[1], dz = diag[2]; - F32 area_world = llmax(dx * dy, llmax(dx * dz, dy * dz)); - // Pixels per meter at the nearest point. Distance floored: - // nearer than this the screen clamp below governs anyway. - F32 dist = llmax(face->mDistanceToCamera, 0.5f); - F32 ppm = LLDrawable::sCurPixelAngle / dist; - F32 face_px = area_world * ppm * ppm; - if (face_px <= 0.f) + // Cached measurement - see update_face_stream_vsize above. + // Zero = degenerate extents / not yet through a geometry + // build: not renderable, must not be measured (a + // placeholder value would poison the per-bucket MIN bound + // and drag the texture to its deepest mip). + F32 vsize = face->mStreamVSize[i]; + if (vsize <= 0.f) { - // Degenerate extents: the face hasn't been through a - // geometry build yet (or a rigged face has no rigged - // extents) - it isn't renderable, so it must not be - // measured. Skipping matters especially for the - // per-bucket MIN bound: any invented placeholder - // value (the old fallback hit LLFace::init's 16px - // default) becomes the texture's least-demanding - // "use" and, under TextureDownrezCoverageBias, drags - // the whole texture to its deepest mip - and it - // poisoned BP and PBR asymmetrically since the two - // systems register faces at different points in the - // geometry lifecycle. continue; } - // Effective UV repeat AREA across this face: the tiling - // term of texels-drawn-per-screen-pixel. More tiling => - // each tile is smaller on screen => coarser mips suffice - // (penalty). Repeats < 1 (atlas/crop) => only a sub-rect - // of the image is shown, but discard levels are whole- - // image, so the full image must be resident at 1/repeats - // times the crop's pixel count (boost). - S32 te_offset = face->getTEOffset(); // offset is -1 if not inited - LLViewerObject* objp = face->getViewerObject(); - const LLTextureEntry* te = (te_offset < 0 || te_offset >= objp->getNumTEs()) ? nullptr : objp->getTE(te_offset); - - F32 repeats = 1.f; - if (te) - { - // UV scale source: every channel reads the repeat - // values ITS renderer actually applies, then flows - // through the identical pipeline below. Sources: - // diffuse -> TE scale - // Blinn normal/spec -> LLMaterial per-map repeats - // PBR channels -> KHR texture_transform scale - // Fallback for any missing material is the TE scale - - // never a silent hardcoded 1. - F32 scale_s = te->getScaleS(); - F32 scale_t = te->getScaleT(); - if (i >= LLRender::BASECOLOR_MAP) - { - // LLRender channel -> LLGLTFMaterial::TextureInfo - static const S32 gltf_info[4] = { - LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR, // BASECOLOR_MAP (3) - LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, // METALLIC_ROUGHNESS_MAP (4) - LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL, // GLTF_NORMAL_MAP (5) - LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE, // EMISSIVE_MAP (6) - }; - if (const LLGLTFMaterial* gltf_mat = te->getGLTFRenderMaterial()) - { - const LLVector2& s = gltf_mat->mTextureTransform[gltf_info[i - LLRender::BASECOLOR_MAP]].mScale; - scale_s = s.mV[0]; - scale_t = s.mV[1]; - } - } - else if (i == LLRender::NORMAL_MAP || i == LLRender::SPECULAR_MAP) - { - // Blinn-Phong normal/specular maps carry their own - // repeats in LLMaterial - the renderer builds their - // texture matrices from these, NOT from the TE's - // diffuse scale. Reading the diffuse scale here made - // Blinn normals scale differently than PBR normals - // (whose per-channel transform IS read above). - if (const LLMaterial* mat = te->getMaterialParams().get()) - { - if (i == LLRender::NORMAL_MAP) - { - mat->getNormalRepeat(scale_s, scale_t); - } - else - { - mat->getSpecularRepeat(scale_s, scale_t); - } - } - } - - // Continuously-animated scale (llSetTextureAnim SCALE) - // bypasses both static sources via mTextureMatrix - - // the live animated values win on either path. - if (LLVOVolume* vvo = face->getDrawable() ? face->getDrawable()->getVOVolume() : nullptr) - { - LLViewerTextureAnim* anim = vvo->mTextureAnimp; - if (anim && (anim->mMode & LLTextureAnim::ON) && (anim->mMode & LLTextureAnim::SCALE) - && (anim->mFace < 0 || anim->mFace == te_offset)) - { - scale_s = anim->mScaleS; - scale_t = anim->mScaleT; - } - } - - repeats = fabsf(scale_s * scale_t); - - // Mesh atlas sub-rect: a face whose intrinsic UVs span - // only part of [0,1]^2 shows that fraction of the - // image. Applies identically to both paths - the - // transforms above stack on the raw face UVs. - if (LLVolume* vol = objp->getVolume()) - { - if (te_offset >= 0 && te_offset < vol->getNumVolumeFaces()) - { - const LLVolumeFace& vf = vol->getVolumeFace(te_offset); - F32 span = fabsf((vf.mTexCoordExtents[1].mV[0] - vf.mTexCoordExtents[0].mV[0]) - * (vf.mTexCoordExtents[1].mV[1] - vf.mTexCoordExtents[0].mV[1])); - if (span > 0.f) - { - repeats *= span; - } - } - } - } - - repeats = llclamp(repeats, MIN_REPEAT_AREA, MAX_REPEAT_AREA); - - // Apply the two sides of the repeat term in the right - // order relative to the screen clamp: - // - tiling (repeats > 1): the per-tile footprint at the - // nearest point, THEN clamped - one tile can't draw - // more pixels than the screen. (Clamping the whole - // face first and then dividing crushed near tiles.) - // - atlas/crop (repeats < 1): boost AFTER the clamp - - // whole-image residency for a crop legitimately - // demands more than its screen coverage. - F32 tiling = llmax(repeats, 1.f); - F32 crop = llmin(repeats, 1.f); - F32 vsize = llmin(face_px / tiling, LLViewerTexture::sWindowPixelArea) / crop; - - // Avatar bonus: worn attachments get a coverage - // multiplier - avatars are what people look at, and - // rigged extents make attachment coverage measurement - // unreliable anyway. Multiplicative, not a slam: a - // nearby avatar gains ~a mip of headroom while a distant - // one still downrezzes naturally with its coverage. - // (System-avatar bakes get the same bonus in the - // no-faces branch below.) - if (objp->isAttachment()) - { - static LLCachedControl avatar_boost(gSavedSettings, "TextureAvatarBoost", 4.f); - vsize *= llmax((F32)avatar_boost, 1.f); - } + any_face = true; + on_screen = on_screen || face->mInFrustum; + min_overflow = llmin(min_overflow, face->mFrustumOverflow); if (bucket >= 0 && bucket < 4) { @@ -1242,6 +1309,15 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag imagep->mChannelCoverage[b] = channel_coverage[b]; imagep->mChannelCoverageMin[b] = (channel_coverage_min[b] == FLT_MAX) ? 0.f : channel_coverage_min[b]; } + + // Fetch admission signal: false only when faces were actually scanned and + // every one projects off screen. Textures with no scannable faces (bakes, + // spotlights, the >1024-face boost path, not-yet-built geometry) stay + // eligible - blocking them is what stalls load-in. + imagep->mOnScreen = on_screen || !any_face; + // Least out-of-frustum use governs the allowance falloff; unknown = 0 + // (no penalty), same reasoning as mOnScreen. + imagep->mFrustumOverflow = any_face ? min_overflow : 0.f; } #if 0 @@ -1510,6 +1586,18 @@ F32 LLViewerTextureList::updateImagesFetchTextures(F32 max_time) { updateImageDecodePriority(imagep); imagep->updateFetch(); + + // Fast-pump membership: textures with an active fetch get + // updateFetch every frame (in updateImages) instead of waiting + // ~a sweep period per state transition - that wait, times the + // 2-4 transitions a load needs, was the measured throughput + // ceiling. Purely additive: the sweep still pumps everything, + // so fetches started by any other path can never strand. + if ((imagep->isFetching() || imagep->hasFetcher()) && !imagep->mInFastFetchList) + { + imagep->mInFastFetchList = true; + mFastFetchList.push_back(imagep); + } } if (timer.getElapsedTimeF32() > max_time) diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h index 931f2ed50e..7004238995 100644 --- a/indra/newview/llviewertexturelist.h +++ b/indra/newview/llviewertexturelist.h @@ -34,6 +34,7 @@ #include "llviewertexture.h" #include "llui.h" #include +#include #include #include "lluiimage.h" @@ -225,6 +226,10 @@ public: image_list_t mCallbackList; image_list_t mFastCacheList; + // In-flight fetches pumped every frame (additive to the round-robin + // sweep, which remains the universal pump). See updateImages. + std::vector > mFastFetchList; + bool mForceResetTextureStats; // to make "for (auto& imagep : gTextureList)" work -- cgit v1.3