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