diff options
| author | Erik Kundiman <erik@megapahit.org> | 2026-06-13 14:20:22 +0800 |
|---|---|---|
| committer | Erik Kundiman <erik@megapahit.org> | 2026-06-13 17:44:11 +0800 |
| commit | 191669d38d232d2bd61a0dd2252c7a3543a2b467 (patch) | |
| tree | c1783f307fd217f38b31cebbe830ba0481925f8a /indra/newview/llviewertexture.cpp | |
| parent | 47b583e9caa3388cd41f70e8de0a5a950082979d (diff) | |
| parent | 663bf4d3eba16e1d0a781ac5261541e7e2d6b4f2 (diff) | |
Merge tag 'Second_Life_Release#663bf4d3-26.3' into 26.3
Diffstat (limited to 'indra/newview/llviewertexture.cpp')
| -rw-r--r-- | indra/newview/llviewertexture.cpp | 437 |
1 files changed, 365 insertions, 72 deletions
diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 05b71b65b2..0dfc40b85f 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -87,6 +87,17 @@ S32 LLViewerTexture::sRawCount = 0; S32 LLViewerTexture::sAuxCount = 0; LLFrameTimer LLViewerTexture::sEvaluationTimer; F32 LLViewerTexture::sDesiredDiscardBias = 0.f; +F32 LLViewerTexture::sBackgroundFactor = 0.f; +F32 LLViewerTexture::sMemoryPressureMultiplier = 1.f; +F32 LLViewerTexture::sLastDitchMinDiscard = 0.f; + +//static +F32 LLViewerTexture::getMemoryPressureProgress() +{ + static LLCachedControl<F32> max_mult(gSavedSettings, "TextureMemoryPressureMaxMultiplier", 64.f); + F32 cap = llmax((F32)max_mult, 1.0001f); + return llclampf((sMemoryPressureMultiplier - 1.f) / (cap - 1.f)); +} U32 LLViewerTexture::sBiasTexturesUpdated = 0; S32 LLViewerTexture::sMaxSculptRez = 128; //max sculpt image size @@ -97,12 +108,12 @@ constexpr S32 DEFAULT_ICON_DIMENSIONS = 32; constexpr S32 DEFAULT_THUMBNAIL_DIMENSIONS = 256; U32 LLViewerTexture::sMinLargeImageSize = 65536; //256 * 256. U32 LLViewerTexture::sMaxSmallImageSize = MAX_CACHED_RAW_IMAGE_AREA; -bool LLViewerTexture::sFreezeImageUpdates = false; 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; LLViewerTexture::EDebugTexels LLViewerTexture::sDebugTexelsMode = LLViewerTexture::DEBUG_TEXELS_OFF; @@ -120,7 +131,7 @@ LLLoadedCallbackEntry::LLLoadedCallbackEntry(loaded_callback_func cb, LLViewerFetchedTexture* target, bool pause) : mCallback(cb), - mLastUsedDiscard(MAX_DISCARD_LEVEL+1), + mLastUsedDiscard(S32_MAX), mDesiredDiscard(discard_level), mNeedsImageRaw(need_imageraw), mUserData(userdata), @@ -485,6 +496,13 @@ void LLViewerTexture::updateClass() LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; sCurrentTime = gFrameTimeSeconds; + if (gViewerWindow) + { + F32 w = (F32)gViewerWindow->getWindowWidthRaw(); + F32 h = (F32)gViewerWindow->getWindowHeightRaw(); + sWindowPixelArea = llmax(w * h, 1.f); + } + LLTexturePipelineTester* tester = (LLTexturePipelineTester*)LLMetricPerformanceTesterBasic::getTester(sTesterName); if (tester) { @@ -523,6 +541,133 @@ void LLViewerTexture::updateClass() F32 over_pct = (used - target) / target; + // Predicted-VRAM pressure controller. Eviction is fast, refetch is slow, + // so feedback on instantaneous `used` sawtooths; feeding `used + + // in_flight_delta` lets mult converge to equilibrium 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 cap = llmax((F32)max_mult, 1.0001f); + F32 dt = (F32)gFrameIntervalSeconds; + + // Skip the full-list iteration when there is no pressure to react to: + // mult already at baseline, last-ditch at zero, and used well clear of + // the backoff target. Worst case the controller picks up the spike one + // frame later, from `used` alone. + bool need_predict = sMemoryPressureMultiplier > 1.001f + || sLastDitchMinDiscard > 0.f + || used > backoff_target * 0.5f; + + S64 pending_bytes_increase = 0; + S64 pending_bytes_decrease = 0; + if (need_predict) + { + LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vt - in-flight predict"); + for (auto& imagep : gTextureList) + { + if (imagep.isNull()) continue; + // Cheap inline checks first so the virtual getDiscardLevel() + // call only fires when there is a real chance of contribution. + S32 desired = imagep->getDesiredDiscardLevel(); + if (desired < 0) continue; + S32 fw = imagep->getFullWidth(); + S32 fh = imagep->getFullHeight(); + if (fw <= 0 || fh <= 0) continue; + S32 current = imagep->getDiscardLevel(); + if (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); + } + } + + // 1024 * 512 = 524288: matches the unit reduction at line 513. + 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); + + // High water mark: when used crosses budget * high_water, skip the + // smoothed convergence and slam the controller into hard-cap state. + // Recovers the historical 90% behavior - immediate aggressive + // response instead of waiting for the lerp to chase the target. + static LLCachedControl<F32> high_water(gSavedSettings, "TextureMemoryHighWaterMark", 0.8f); + bool above_high_water = used >= budget * llclamp((F32)high_water, 0.5f, 1.f); + + F32 target_mult = llclamp(powf(llmax(predicted_over, 1.f), llmax((F32)prediction_gain, 0.0001f)), 1.f, cap); + if (above_high_water) + { + target_mult = cap; + sMemoryPressureMultiplier = cap; + } + else + { + // ~63% convergence in 1/smoothing_rate seconds (default 0.25s). + 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); + + F32 progress = getMemoryPressureProgress(); + + { + 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); + // Above the high water mark, last-ditch creeps regardless of + // mult_progress: by definition we are out of normal headroom. + bool engage = above_high_water || progress >= llclampf((F32)ld_engage); + if (engage && predicted_over > 1.f) + { + sLastDitchMinDiscard += llmax((F32)ld_ramp, 0.f) * dt; + } + else if (!above_high_water && predicted_over < 1.f) + { + sLastDitchMinDiscard -= llmax((F32)ld_decay, 0.f) * dt; + } + sLastDitchMinDiscard = llclamp(sLastDitchMinDiscard, 0.f, llmax((F32)ld_max, 0.f)); + } + + // 1 Hz pressure log. + static LLFrameTimer s_pressure_log_timer; + if (s_pressure_log_timer.getElapsedTimeF32() > 1.f) + { + s_pressure_log_timer.reset(); + F32 over = used / llmax(backoff_target, 1.f); + LL_INFOS("TextureStream") << "pressure" + << " mult=" << sMemoryPressureMultiplier + << " target_mult=" << target_mult + << " progress=" << 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; + } + } + bool is_sys_low = isSystemMemoryLow(); bool is_low = is_sys_low || over_pct > 0.f; @@ -569,6 +714,10 @@ void LLViewerTexture::updateClass() // don't execute above until the slam to 1.5 has a chance to take effect sEvaluationTimer.reset(); + // Don't decay bias while downscale is still draining - those bytes + // are about to free and the loop would oscillate. + bool eviction_in_flight = !gTextureList.mDownScaleQueue.empty(); + // lower discard bias over time when at least 10% of budget is free constexpr F32 FREE_PERCENTAGE_TRESHOLD = -0.1f; constexpr U32 FREE_SYS_MEM_TRESHOLD = 100; @@ -576,7 +725,8 @@ void LLViewerTexture::updateClass() const S32Megabytes MIN_FREE_MAIN_MEMORY(min_free_main_memory() + FREE_SYS_MEM_TRESHOLD); if (sDesiredDiscardBias > 1.f && over_pct < FREE_PERCENTAGE_TRESHOLD - && getFreeSystemMemory() > MIN_FREE_MAIN_MEMORY) + && getFreeSystemMemory() > MIN_FREE_MAIN_MEMORY + && !eviction_in_flight) { static LLCachedControl<F32> high_mem_discard_decrement(gSavedSettings, "RenderHighMemMinDiscardDecrement", .1f); @@ -622,6 +772,33 @@ void LLViewerTexture::updateClass() } } + // Background-window ramp: 0 -> 1 at rate per second while backgrounded, + // snaps to 0 in foreground. Default 0.011 ~ 90s to saturate. + { + static LLCachedControl<F32> bg_factor_rate(gSavedSettings, "TextureBackgroundFactorRatePerSec", 0.011f); + if (in_background) + { + sBackgroundFactor += (F32)bg_factor_rate * gFrameIntervalSeconds; + sBackgroundFactor = llclampf(sBackgroundFactor); + } + else + { + sBackgroundFactor = 0.f; + } + } + + // Fetch-queue depth as a one-way bias floor (decay path still drops + // bias when the queue drains). Pushes bias up before VRAM overflows + // during teleport/scene-change floods. + if (LLTextureFetch* fetcher = LLAppViewer::getTextureFetch()) + { + S32 pending = fetcher->getNumRequests(); + static LLCachedControl<F32> fetch_pressure_scale(gSavedSettings, "TextureFetchPressureScale", 1000.f); + F32 scale = llmax((F32)fetch_pressure_scale, 1.f); + F32 fetch_pressure = llclamp((F32)pending / scale, 0.f, 3.f); + sDesiredDiscardBias = llmax(sDesiredDiscardBias, 1.f + fetch_pressure); + } + sDesiredDiscardBias = llclamp(sDesiredDiscardBias, 1.f, 4.f); if (last_texture_update_count_bias < sDesiredDiscardBias) { @@ -638,8 +815,6 @@ void LLViewerTexture::updateClass() // a problem. last_texture_update_count_bias = sDesiredDiscardBias; } - - LLViewerTexture::sFreezeImageUpdates = false; } //static @@ -1156,8 +1331,10 @@ void LLViewerFetchedTexture::init(bool firstinit) mRequestedDownloadPriority = 0.f; mFullyLoaded = false; mCanUseHTTP = true; - mDesiredDiscardLevel = MAX_DISCARD_LEVEL + 1; - mMinDesiredDiscardLevel = MAX_DISCARD_LEVEL + 1; + mDesiredDiscardLevel = S8_MAX; + // S8_MAX = no cap. setMinDiscardLevel takes min(current, new), so + // explicit caps from terrain / avatar self / thumbnails still apply. + mMinDesiredDiscardLevel = S8_MAX; mDecodingAux = false; @@ -1977,21 +2154,10 @@ bool LLViewerFetchedTexture::processFetchResults(S32& desired_discard, S32 curre setIsMissingAsset(); desired_discard = -1; } - else - { - //LL_WARNS() << mID << ": Setting min discard to " << current_discard << LL_ENDL; - if (current_discard >= 0) - { - mMinDiscardLevel = current_discard; - //desired_discard = current_discard; - } - else - { - S32 dis_level = getDiscardLevel(); - mMinDiscardLevel = dis_level; - //desired_discard = dis_level; - } - } + // Transient failure (decoder OOM, network blip): don't latch + // mMinDiscardLevel - that would block all future fetches via + // the make_request gate. Permanent failures are caught above + // (getDiscardLevel()<0 -> setIsMissingAsset). destroyRawImage(); } else if (mRawImage.notNull()) @@ -2072,8 +2238,18 @@ bool LLViewerFetchedTexture::updateFetch() if (mRawImage.notNull()) sRawCount--; if (mAuxRawImage.notNull()) sAuxCount--; // keep in mind that fetcher still might need raw image, don't modify original + S32 codec_levels = 0; bool finished = LLAppViewer::getTextureFetch()->getRequestFinished(getID(), fetch_discard, mFetchState, mRawImage, mAuxRawImage, - mLastHttpGetStatus); + mLastHttpGetStatus, codec_levels); + if (codec_levels > 0) + { + mCodecMaxDiscardLevel = (S8)llmin(codec_levels, (S32)S8_MAX); + if (codec_levels > 5) + { + LL_DEBUGS("TextureStream") << "Texture " << mID << " codec-reported max discard " + << codec_levels << " (above the historical hardcoded cap of 5)" << LL_ENDL; + } + } if (mRawImage.notNull()) sRawCount++; if (mAuxRawImage.notNull()) { @@ -2108,7 +2284,9 @@ bool LLViewerFetchedTexture::updateFetch() } } - desired_discard = llmin(desired_discard, getMaxDiscardLevel()); + // Clamp the fetch request to what the codestream encodes; deeper + // discards are served from the GL mip pyramid via scaleDown. + desired_discard = llmin(desired_discard, (S32)mCodecMaxDiscardLevel); bool make_request = true; if (decode_priority <= 0) @@ -2116,9 +2294,13 @@ bool LLViewerFetchedTexture::updateFetch() LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - priority <= 0"); make_request = false; } - else if (mDesiredDiscardLevel > getMaxDiscardLevel()) + else if (mDesiredDiscardLevel > (S32)mCodecMaxDiscardLevel && + current_discard >= 0) { - LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - desired > max"); + // 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; } else if (mNeedsCreateTexture || mIsMissingAsset) @@ -2570,20 +2752,21 @@ bool LLViewerFetchedTexture::doLoadedCallbacks() S32 gl_discard = getDiscardLevel(); - // If we don't have a legit GL image, set it to be lower than the worst discard level + // S32_MAX is the "no data" sentinel; real discards can now exceed + // MAX_DISCARD_LEVEL via dimDerivedMaxDiscard. if (gl_discard == -1) { - gl_discard = MAX_DISCARD_LEVEL + 1; + gl_discard = S32_MAX; } // // Determine the quality levels of textures that we can provide to callbacks // and whether we need to do decompression/readback to get it // - S32 current_raw_discard = MAX_DISCARD_LEVEL + 1; // We can always do a readback to get a raw discard + S32 current_raw_discard = S32_MAX; // We can always do a readback to get a raw discard S32 best_raw_discard = gl_discard; // Current GL quality level - S32 current_aux_discard = MAX_DISCARD_LEVEL + 1; - S32 best_aux_discard = MAX_DISCARD_LEVEL + 1; + S32 current_aux_discard = S32_MAX; + S32 best_aux_discard = S32_MAX; LLImageRaw *current_raw_image = nullptr; if (mIsRawImageValid) @@ -2683,7 +2866,7 @@ bool LLViewerFetchedTexture::doLoadedCallbacks() // // Run raw/auxiliary data callbacks // - if (run_raw_callbacks && current_raw_image != nullptr && (current_raw_discard <= getMaxDiscardLevel())) + if (run_raw_callbacks && current_raw_image != nullptr && current_raw_discard != S32_MAX) { // Do callbacks which require raw image data. //LL_INFOS() << "doLoadedCallbacks raw for " << getID() << LL_ENDL; @@ -2723,7 +2906,7 @@ bool LLViewerFetchedTexture::doLoadedCallbacks() // // Run GL callbacks // - if (run_gl_callbacks && (gl_discard <= getMaxDiscardLevel())) + if (run_gl_callbacks && gl_discard != S32_MAX) { //LL_INFOS() << "doLoadedCallbacks GL for " << getID() << LL_ENDL; @@ -3026,11 +3209,6 @@ S8 LLViewerLODTexture::getType() const return LLViewerTexture::LOD_TEXTURE; } -bool LLViewerLODTexture::isUpdateFrozen() -{ - return LLViewerTexture::sFreezeImageUpdates; -} - // This is gauranteed to get called periodically for every texture //virtual void LLViewerLODTexture::processTextureStats() @@ -3056,18 +3234,15 @@ void LLViewerLODTexture::processTextureStats() { mDesiredDiscardLevel = 0; } - // Generate the request priority and render priority - else if (mDontDiscard || !mUseMipMaps) + // HUD/UI/preview and mDontDiscard textures bypass streaming - no + // face_distance signal applies, they need native resolution. + else if (mBoostLevel >= LLGLTexture::BOOST_HIGH + || mDontDiscard + || !mUseMipMaps) { mDesiredDiscardLevel = 0; if (mFullWidth > MAX_IMAGE_SIZE_DEFAULT || mFullHeight > MAX_IMAGE_SIZE_DEFAULT) - mDesiredDiscardLevel = 1; // MAX_IMAGE_SIZE_DEFAULT = 2048 and max size ever is 4096 - } - else if (mBoostLevel < LLGLTexture::BOOST_HIGH && mMaxVirtualSize <= 10.f) - { - // If the image has not been significantly visible in a while, we don't want it - mDesiredDiscardLevel = llmin(mMinDesiredDiscardLevel, (S8)(MAX_DISCARD_LEVEL + 1)); - mDesiredDiscardLevel = llmin(mDesiredDiscardLevel, (S32)mLoadedCallbackDesiredDiscardLevel); + mDesiredDiscardLevel = 1; // 4096^2 source can't be loaded full res } else if (!mFullWidth || !mFullHeight) { @@ -3076,28 +3251,104 @@ void LLViewerLODTexture::processTextureStats() } else { - //static const F64 log_2 = log(2.0); - static const F64 log_4 = log(4.0); - F32 discard_level = 0.f; - // If we know the output width and height, we can force the discard - // level to the correct value, and thus not decode more texture - // data than we need to. + // floor(log2(max(w, h))) - both the multiplier on the normalized + // factor and the cap clamp at the bottom of this function. + S32 dim_max_for_image_i = (mFullWidth > 0 && mFullHeight > 0) + ? LLImageGL::dimDerivedMaxDiscard(mFullWidth, mFullHeight) + : (S32)mCodecMaxDiscardLevel; + F32 dim_max_for_image = (F32)dim_max_for_image_i; + if (mKnownDrawWidth && mKnownDrawHeight) { + // UI-pinned target dimensions - use pixel-area math. + static const F64 log_4 = log(4.0); S32 draw_texels = mKnownDrawWidth * mKnownDrawHeight; draw_texels = llclamp(draw_texels, MIN_IMAGE_AREA, MAX_IMAGE_AREA); - - // Use log_4 because we're in square-pixel space, so an image - // with twice the width and twice the height will have mTexelsPerImage - // 4 * draw_size discard_level = (F32)(log(mTexelsPerImage / draw_texels) / log_4); } else { - // Calculate the required scale factor of the image using pixels per texel - discard_level = (F32)(log(mTexelsPerImage / mMaxVirtualSize) / log_4); + // Two 0..1 signals composed multiplicatively: + // discard = distance_factor * size_factor * max_discard + // distance_factor: face_dist / draw_dist, shaped by + // TextureDistanceDiscardPower (default 0.5 = sqrt). + // size_factor: 1 - (mMaxOnScreenSize / window_pixels), shaped + // by TextureSizeDiscardPower. + // Either factor near 0 keeps the result fine - both have to + // be high for the texture to go deep. + static LLCachedControl<F32> distance_power(gSavedSettings, "TextureDistanceDiscardPower", 0.5f); + F32 power = llmax((F32)distance_power, 0.0001f); + F32 distance_factor = (power == 1.f) ? mMinDistanceFactor : powf(mMinDistanceFactor, power); + + static LLCachedControl<F32> size_power(gSavedSettings, "TextureSizeDiscardPower", 1.f); + F32 sz_power = llmax((F32)size_power, 0.0001f); + F32 coverage = llclampf(mMaxOnScreenSize / sWindowPixelArea); + F32 inv_cov = 1.f - coverage; + F32 size_factor = (sz_power == 1.f) ? inv_cov : powf(inv_cov, sz_power); + + 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. + // mPriorityChannel order: 0=Normal, 1=BaseColor, 2=Specular, 3=Emissive. + S32 priority_channel = (mPriorityChannel >= 0 && mPriorityChannel < 4) ? (S32)mPriorityChannel : 1; + static LLCachedControl<F32> channel_normal (gSavedSettings, "TextureChannelNormal", 1.0f); + static LLCachedControl<F32> channel_basecolor(gSavedSettings, "TextureChannelBaseColor", 0.75f); + static LLCachedControl<F32> channel_specular (gSavedSettings, "TextureChannelSpecular", 0.5f); + static LLCachedControl<F32> channel_emissive (gSavedSettings, "TextureChannelEmissive", 0.75f); + const F32 channels[4] = { + (F32)channel_normal, + (F32)channel_basecolor, + (F32)channel_specular, + (F32)channel_emissive, + }; + F32 channel_power = llmax(channels[priority_channel], 0.0001f); + if (channel_power != 1.f) + { + combined = powf(combined, channel_power); + } + + // Own-avatar boost: shave combined for rigged/animated faces + // on gAgentAvatarp. Preference, not exemption - applied + // before the staleness/background/pressure floors so heavy + // pressure can still evict. + if (mOnAgentAvatar) + { + static LLCachedControl<F32> agent_avatar_boost(gSavedSettings, "TextureAgentAvatarBoost", 0.5f); + combined *= llclampf((F32)agent_avatar_boost); + } + + // Staleness / background floors. Avatar bakes exempt from + // background to avoid the universal-cloud bug when re-foregrounding. + combined = llmax(combined, mStalenessFactor); + if (!isAgentAvatarBoost(mBoostLevel)) + { + // Background floor capped at (dim_max - offset) so we can + // keep some baseline quality while backgrounded. + static LLCachedControl<S32> bg_offset(gSavedSettings, "TextureBackgroundDiscardOffset", 2); + F32 bg = sBackgroundFactor; + if ((S32)bg_offset > 0 && dim_max_for_image > 0.f) + { + F32 cap = llmax(dim_max_for_image - (F32)(S32)bg_offset, 0.f) / dim_max_for_image; + bg = llmin(bg, cap); + } + combined = llmax(combined, bg); + } + + discard_level = combined * dim_max_for_image; } discard_level = floorf(discard_level); @@ -3106,12 +3357,43 @@ void LLViewerLODTexture::processTextureStats() if (mFullWidth > max_tex_res || mFullHeight > max_tex_res) min_discard = 1.f; - discard_level = llclamp(discard_level, min_discard, (F32)MAX_DISCARD_LEVEL); + // dim_max_for_image_i is the per-texture cap. TextureMaxDiscardOverride + // raises it (debug). Codec_max applies only to fetches, not here. + static LLCachedControl<S32> max_discard_override(gSavedSettings, "TextureMaxDiscardOverride", 0); + S32 effective_cap = (max_discard_override > 0) ? (S32)max_discard_override : dim_max_for_image_i; + discard_level = llclamp(discard_level, min_discard, (F32)effective_cap); + + mDesiredDiscardLevel = llmin(effective_cap, (S32)discard_level); + + // 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 (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)(cap_relax * room); + effective_min_cap = llmin(effective_min_cap, dim_max_for_image_i); + } + mDesiredDiscardLevel = llmin((S8)effective_min_cap, mDesiredDiscardLevel); + + // Halve the floor for bubble-resident textures (mMinDistanceFactor == 0 + // = at least one face inside the bubble) so the close-vs-far gradient + // is preserved at every pressure level. + if (!isAgentAvatarBoost(mBoostLevel)) + { + S32 forced = (S32)floorf(sLastDitchMinDiscard); + if (mMinDistanceFactor <= 0.f) forced /= 2; + forced = llclamp(forced, 0, dim_max_for_image_i); + if (forced > mDesiredDiscardLevel) + { + mDesiredDiscardLevel = (S8)forced; + } + } - // Can't go higher than the max discard level - mDesiredDiscardLevel = llmin(getMaxDiscardLevel() + 1, (S32)discard_level); - // Clamp to min desired discard - mDesiredDiscardLevel = llmin(mMinDesiredDiscardLevel, mDesiredDiscardLevel); // // At this point we've calculated the quality level that we want, @@ -3120,7 +3402,9 @@ void LLViewerLODTexture::processTextureStats() // S32 current_discard = getDiscardLevel(); - if (mBoostLevel < LLGLTexture::BOOST_AVATAR_BAKED) + // Avatar bakes exempt: shrinking mid-bake can leave the avatar + // stuck as a cloud until the next bake completes. + if (!isAgentAvatarBoost(mBoostLevel)) { if (current_discard < mDesiredDiscardLevel && !mForceToSaveRawImage) { // should scale down @@ -3128,13 +3412,6 @@ void LLViewerLODTexture::processTextureStats() } } - if (isUpdateFrozen() // we are out of memory and nearing max allowed bias - && mBoostLevel < LLGLTexture::BOOST_SCULPTED - && mDesiredDiscardLevel < current_discard) - { - // stop requesting more - mDesiredDiscardLevel = current_discard; - } mDesiredDiscardLevel = llmin(mDesiredDiscardLevel, (S32)mLoadedCallbackDesiredDiscardLevel); } @@ -3160,6 +3437,22 @@ 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. + if (!mUseMipMaps || mDontDiscard || mBoostLevel >= LLGLTexture::BOOST_HIGH) + { + return false; + } + // Avatar bakes are exempt from mid-bake eviction (cloud avatar risk). + if (isAgentAvatarBoost(mBoostLevel)) + { + return false; + } + if (!mDownScalePending) { mDownScalePending = true; |
