summaryrefslogtreecommitdiff
path: root/indra
diff options
context:
space:
mode:
authorJonathan "Geenz" Goodman <geenz@geenzo.com>2026-05-19 12:19:11 -0400
committerJonathan "Geenz" Goodman <geenz@geenzo.com>2026-05-19 12:19:11 -0400
commite7263854de98868fd2b3205e09849fd6f04fb35b (patch)
tree0e1b19abf9009290d38a7ed6e0b378ce1b1e2027 /indra
parent1b1f59c1e21581c4c1968b9206b9d9c0dc8513a1 (diff)
More high pressure and quality changes.
New "high res" bubble near the camera, minimum discard settings, and discard scaling.
Diffstat (limited to 'indra')
-rw-r--r--indra/llrender/llimagegl.cpp10
-rw-r--r--indra/llrender/llimagegl.h1
-rw-r--r--indra/newview/app_settings/settings.xml116
-rw-r--r--indra/newview/lltextureview.cpp8
-rw-r--r--indra/newview/llviewertexture.cpp195
-rw-r--r--indra/newview/llviewertexture.h14
-rw-r--r--indra/newview/llviewertexturelist.cpp70
-rw-r--r--indra/newview/llviewertexturelist.h4
8 files changed, 355 insertions, 63 deletions
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 @@
<key>TextureMemoryPressureRampRate</key>
<map>
<key>Comment</key>
- <string>Feedback rate (per second) for the VRAM-pressure factor (0..1). Higher = faster convergence on the budget; lower = gentler.</string>
+ <string>Geometric ramp/decay rate (per second) for the VRAM-pressure distance multiplier (&gt;= 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.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>3.0</real>
+ <real>1.0</real>
</map>
<key>TextureMemoryPressureBackoffStart</key>
<map>
@@ -11893,6 +11893,83 @@
<key>Value</key>
<real>0.85</real>
</map>
+ <key>TextureMemoryPressureMaxMultiplier</key>
+ <map>
+ <key>Comment</key>
+ <string>Upper bound on the VRAM-pressure distance multiplier (&gt;= 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.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>64.0</real>
+ </map>
+ <key>TextureLastDitchEngageProgress</key>
+ <map>
+ <key>Comment</key>
+ <string>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.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.95</real>
+ </map>
+ <key>TextureLastDitchRampRate</key>
+ <map>
+ <key>Comment</key>
+ <string>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.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.5</real>
+ </map>
+ <key>TextureLastDitchDecayRate</key>
+ <map>
+ <key>Comment</key>
+ <string>Rate (discard levels/sec) at which sLastDitchMinDiscard decays back to 0 when prediction is under budget.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.5</real>
+ </map>
+ <key>TextureLastDitchMinDiscardMax</key>
+ <map>
+ <key>Comment</key>
+ <string>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.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>13.0</real>
+ </map>
+ <key>TextureMemoryPressurePredictionGain</key>
+ <map>
+ <key>Comment</key>
+ <string>Power exponent mapping predicted-over-budget ratio to target multiplier. target_mult = pred_over^gain. Higher gain saturates faster.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>10.0</real>
+ </map>
+ <key>TextureMemoryPressureSmoothingRate</key>
+ <map>
+ <key>Comment</key>
+ <string>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.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>4.0</real>
+ </map>
<key>TextureTerrainDistanceFloor</key>
<map>
<key>Comment</key>
@@ -11952,7 +12029,7 @@
<key>TextureCloseBubbleMeters</key>
<map>
<key>Comment</key>
- <string>Close-camera bubble (meters). Faces inside this distance get dist_factor = 0 (no discard contribution); the ramp to 1 spans (bubble, draw_distance].</string>
+ <string>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.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
@@ -11960,6 +12037,39 @@
<key>Value</key>
<real>5.0</real>
</map>
+ <key>TextureCloseBubbleMinMeters</key>
+ <map>
+ <key>Comment</key>
+ <string>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.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>3.0</real>
+ </map>
+ <key>TextureCloseBubbleShrinkThreshold</key>
+ <map>
+ <key>Comment</key>
+ <string>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.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.8</real>
+ </map>
+ <key>TextureCloseBubbleTrackRate</key>
+ <map>
+ <key>Comment</key>
+ <string>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.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>F32</string>
+ <key>Value</key>
+ <real>0.5</real>
+ </map>
<key>TextureDistanceDiscardPower</key>
<map>
<key>Comment</key>
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<F32> backoff_start(gSavedSettings, "TextureMemoryPressureBackoffStart", 0.85f);
+ static LLCachedControl<F32> max_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f);
+ static LLCachedControl<F32> prediction_gain(gSavedSettings, "TextureMemoryPressurePredictionGain", 10.f);
+ static LLCachedControl<F32> 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<F32> 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<F32> ld_engage(gSavedSettings, "TextureLastDitchEngageProgress", 0.95f);
+ static LLCachedControl<F32> ld_ramp(gSavedSettings, "TextureLastDitchRampRate", 0.5f);
+ static LLCachedControl<F32> ld_decay(gSavedSettings, "TextureLastDitchDecayRate", 0.5f);
+ static LLCachedControl<F32> 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<F32> close_bubble(gSavedSettings, "TextureCloseBubbleMeters", 5.f);
- F32 bubble = llclamp((F32)close_bubble, 0.f, draw_distance - 0.001f);
+ static LLCachedControl<F32> close_bubble_min(gSavedSettings, "TextureCloseBubbleMinMeters", 0.1f);
+ static LLCachedControl<F32> max_pressure_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f);
+ static LLCachedControl<F32> bubble_shrink_threshold(gSavedSettings, "TextureCloseBubbleShrinkThreshold", 0.8f);
+ static LLCachedControl<F32> 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 &);