From e7fee9e7fb081aa41f901ea43e77688d4f69c152 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:23:15 +0300 Subject: p#604 Fix draw range fluctuations #2 --- indra/newview/llviewerdisplay.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'indra/newview/llviewerdisplay.cpp') diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 9dfa9a0efd..4773a8a555 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -217,9 +217,10 @@ void display_update_camera() { final_far *= 0.5f; } - else if (LLViewerTexture::sDesiredDiscardBias > 2.f) + // When system memory is critically low or recovering, shrink draw distance. + else if (LLViewerTexture::getSystemMemoryBudgetFactor() > 1.f) { - final_far = llmax(32.f, final_far / (LLViewerTexture::sDesiredDiscardBias - 1.f)); + final_far = llmax(32.f, final_far / LLViewerTexture::getSystemMemoryBudgetFactor()); } LLViewerCamera::getInstance()->setFar(final_far); LLVOAvatar::sRenderDistance = llclamp(final_far, 16.f, 256.f); -- cgit v1.3 From f117d33535097c3bb8d48f1f954a7a8df0449c44 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 7 Jul 2026 07:34:09 -0400 Subject: Kill the system memory stuff - it's not relevant to the new texture streaming. --- indra/llrender/llimagegl.cpp | 8 +- indra/llrender/llimagegl.h | 1 - indra/newview/app_settings/settings.xml | 11 --- indra/newview/llviewerdisplay.cpp | 5 - indra/newview/llviewermessage.cpp | 7 -- indra/newview/llviewertexture.cpp | 159 -------------------------------- indra/newview/llviewertexture.h | 22 ----- indra/newview/llvocache.cpp | 6 -- 8 files changed, 3 insertions(+), 216 deletions(-) (limited to 'indra/newview/llviewerdisplay.cpp') diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 95575c009b..c8a23d873e 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -493,7 +493,7 @@ bool LLImageGL::create(LLPointer& dest, const LLImageRaw* imageraw, b //---------------------------------------------------------------------------- LLImageGL::LLImageGL(bool usemipmaps/* = true*/, bool allow_compression/* = true*/) -: mSaveData(0), mExternalTexture(false) +: mExternalTexture(false) { init(usemipmaps, allow_compression); setSize(0, 0, 0); @@ -502,7 +502,7 @@ LLImageGL::LLImageGL(bool usemipmaps/* = true*/, bool allow_compression/* = true } LLImageGL::LLImageGL(U32 width, U32 height, U8 components, bool usemipmaps/* = true*/, bool allow_compression/* = true*/) -: mSaveData(0), mExternalTexture(false) +: mExternalTexture(false) { llassert( components <= 4 ); init(usemipmaps, allow_compression); @@ -512,7 +512,7 @@ LLImageGL::LLImageGL(U32 width, U32 height, U8 components, bool usemipmaps/* = t } LLImageGL::LLImageGL(const LLImageRaw* imageraw, bool usemipmaps/* = true*/, bool allow_compression/* = true*/) -: mSaveData(0), mExternalTexture(false) +: mExternalTexture(false) { init(usemipmaps, allow_compression); setSize(0, 0, 0); @@ -621,8 +621,6 @@ void LLImageGL::cleanup() destroyGLTexture(); } freePickMask(); - - mSaveData = NULL; // deletes data } //---------------------------------------------------------------------------- diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index 57ca79b3dd..ca75b543c0 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -256,7 +256,6 @@ private: void freePickMask(); bool isCompressed(); - LLPointer mSaveData; // used for destroyGL/restoreGL LL::WorkQueue::weak_t mMainQueue; U8* mPickMask; //downsampled bitmap approximation of alpha channel. NULL if no alpha channel U16 mPickMaskWidth; diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index bdcbf3b7f2..45a7dced5c 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -8002,17 +8002,6 @@ Value 1 - RenderMinFreeMainMemoryThreshold - - Comment - If available free physical memory is below this value textures get agresively scaled down - Persist - 0 - Type - U32 - Value - 512 - RenderLowMemMinDiscardIncrement Comment diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 4773a8a555..9f1b0d75f3 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -217,11 +217,6 @@ void display_update_camera() { final_far *= 0.5f; } - // When system memory is critically low or recovering, shrink draw distance. - else if (LLViewerTexture::getSystemMemoryBudgetFactor() > 1.f) - { - final_far = llmax(32.f, final_far / LLViewerTexture::getSystemMemoryBudgetFactor()); - } LLViewerCamera::getInstance()->setFar(final_far); LLVOAvatar::sRenderDistance = llclamp(final_far, 16.f, 256.f); gViewerWindow->setup3DRender(); diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 612af029b9..09f17fec40 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -3372,13 +3372,6 @@ void send_agent_update(bool force_send, bool send_reliable) static F32 last_draw_disatance_step = 1024; F32 memory_limited_draw_distance = gAgentCamera.mDrawDistance; - if (LLViewerTexture::getSystemMemoryBudgetFactor() > 1.f) - { - // We are critcally low on memory or recovering, - // limit requested draw distance - memory_limited_draw_distance = llmax(gAgentCamera.mDrawDistance / LLViewerTexture::getSystemMemoryBudgetFactor(), gAgentCamera.mDrawDistance / 2.f); - } - if (tp_state == LLAgent::TELEPORT_ARRIVING || LLStartUp::getStartupState() < STATE_MISC) { // Inform interest list, prioritize closer area. diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 47d022c854..7d07c95395 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -91,9 +91,6 @@ F32 LLViewerTexture::sPixelToTexelRatio = 1.f; U32 LLViewerTexture::sGCSuspendedFrame = 0; S64 LLViewerTexture::sPendingAllocBytes = 0; S64 LLViewerTexture::sPendingFreeBytes = 0; -U32 LLViewerTexture::sUprezRequestCount = 0; -U32 LLViewerTexture::sDownscaleEnqueueCount = 0; -U32 LLViewerTexture::sCooldownFlooredCount = 0; S32 LLViewerTexture::sMaxSculptRez = 128; //max sculpt image size constexpr S32 MAX_CACHED_RAW_IMAGE_AREA = 64 * 64; @@ -105,19 +102,14 @@ U32 LLViewerTexture::sMinLargeImageSize = 65536; //256 * 256. U32 LLViewerTexture::sMaxSmallImageSize = MAX_CACHED_RAW_IMAGE_AREA; F32 LLViewerTexture::sCurrentTime = 0.0f; -constexpr F32 MEMORY_CHECK_WAIT_TIME = 1.0f; constexpr F32 MIN_VRAM_BUDGET = 768.f; F32 LLViewerTexture::sFreeVRAMMegabytes = MIN_VRAM_BUDGET; F32 LLViewerTexture::sWindowPixelArea = 1.f; -F32 LLViewerTexture::sSysMemoryFactor = 1.f; LLViewerTexture::EDebugTexels LLViewerTexture::sDebugTexelsMode = LLViewerTexture::DEBUG_TEXELS_OFF; const F64 log_2 = log(2.0); -// GC evict->refetch cycle samples for the 1Hz TextureStream log (main thread). -static U32 sGCRefetchCount = 0; - //---------------------------------------------------------------------------------------------- //namespace: LLViewerTextureAccess //---------------------------------------------------------------------------------------------- @@ -489,13 +481,6 @@ void LLViewerTexture::initClass() LLImageGL::sDefaultGLTexture = LLViewerFetchedTexture::sDefaultImagep->getGLTexture(); } -S32Megabytes get_render_free_main_memory_treshold() -{ - static LLCachedControl min_free_main_memory(gSavedSettings, "RenderMinFreeMainMemoryThreshold", 512); - const U32Megabytes MIN_FREE_MAIN_MEMORY(min_free_main_memory); - return MIN_FREE_MAIN_MEMORY; -} - //static void LLViewerTexture::updateClass() { @@ -544,7 +529,6 @@ void LLViewerTexture::updateClass() // 'bias' calculation to kick in. F32 vram_target = llmax(llmin(vram_budget - 512.f, vram_budget * 0.8f), MIN_VRAM_BUDGET); sFreeVRAMMegabytes = vram_target - vram_used; - const S32Megabytes free_sys_mem = getFreeSystemMemory(); // VRAM pressure controller for the global pixel:texel ratio. Tightens above // the high watermark, relaxes below the low one, holds in the band (the band @@ -598,135 +582,7 @@ void LLViewerTexture::updateClass() { sGCSuspendedFrame = LLFrameTimer::getFrameCount(); } - - // 1 Hz pressure log. `used` units are the doubled-bytes metric - // (bytes/524288, see above); pending ledgers are converted to match so - // eff(ective) = what used will read once in-flight work settles. - static LLFrameTimer s_pressure_log_timer; - if (s_pressure_log_timer.getElapsedTimeF32() > 1.f) - { - s_pressure_log_timer.reset(); - constexpr F32 BYTES_TO_USED_UNITS = 1.f / 524288.f; - F32 pend_alloc = (F32)sPendingAllocBytes * BYTES_TO_USED_UNITS; - F32 pend_free = (F32)sPendingFreeBytes * BYTES_TO_USED_UNITS; - LL_INFOS("TextureStream") << "pressure" - << " ratio=" << sPixelToTexelRatio - << " used=" << vram_used - << " eff=" << vram_used + pend_alloc - pend_free - << " pend+=" << pend_alloc - << " pend-=" << pend_free - << " budget=" << vram_budget - << " high=" << high - << " low=" << low - << " dsq=" << (S32)gTextureList.mDownScaleQueue.size() - << " uprez/s=" << sUprezRequestCount - << " dscale/s=" << sDownscaleEnqueueCount - << " cdfloor/s=" << sCooldownFlooredCount - << " gcref/s=" << sGCRefetchCount - << " gloom=" << LLImageGL::sOOMErrorCount.load() - << LL_ENDL; - sUprezRequestCount = 0; - sDownscaleEnqueueCount = 0; - sCooldownFlooredCount = 0; - sGCRefetchCount = 0; - } - } - - // System-memory -> draw-distance factor. Separate from the VRAM ratio above: - // this is a last-resort response to running low on *system* RAM and only - // affects draw distance (via getSystemMemoryBudgetFactor, consumed by - // llviewerdisplay). Textures were mostly moved to VRAM, so this rarely fires. - bool is_sys_critically_low = isSystemMemoryCritical(); - static bool sys_was_low = false; - - // System memory factor - // sSysMemoryFactor affects draw distance - // - // We only decrement when more than 406MB is free, but increment - // when below 256MB free. This should provide a stable value - // in the 256-406MB range to avoid draw range fluctuations. - // - // Draw range reduction is a last resort, texture bias is supposed - // to free at least some memory before we get here. - // Note: textures were mostly moved to vram, we might want to - // detach texture bias from system memory. - if (is_sys_critically_low) - { - const S32Megabytes MIN_FREE_MAIN_MEMORY(get_render_free_main_memory_treshold() / 2); - // debt is a negative value since MIN_FREE_MAIN_MEMORY > free memory. - S32 sys_budget_debt = free_sys_mem - MIN_FREE_MAIN_MEMORY; - - // Leave some padding, otherwise we will crash out of memory before hitting factor 2. - const S32Megabytes PAD_BUFFER(32); - S32Megabytes budget_target = MIN_FREE_MAIN_MEMORY - PAD_BUFFER; - if (!sys_was_low) - { - // Result should range from 1 at 0 debt to 2 at -224 debt, 2.14 at -256MB - F32 new_factor = 1.f - (F32)sys_budget_debt / (F32)budget_target; - sSysMemoryFactor = llmax(sSysMemoryFactor, new_factor); - } - else - { - // Slowly ramp up factor to free memory (increasing factor decreases draw range) - constexpr F32 MAX_INCREMENT = 0.05f; - F32 increment = MAX_INCREMENT * llmax(-(F32)sys_budget_debt / (F32)budget_target, 0.f); - sSysMemoryFactor += increment * gFrameIntervalSeconds; - } - sSysMemoryFactor = llclamp(sSysMemoryFactor, 1.f, 2.f); - } - else - { - const S32Megabytes MIN_FREE_MAIN_MEMORY(get_render_free_main_memory_treshold() / 2); - // Only start ramping down when we have breathing room. - // This should be under the value of isSystemMemoryLow to not throw texture - // bias into 1.5+ territory each time we fluctuate around isSystemMemoryLow's - // treshold. - const S32Megabytes MEM_THRESHOLD = MIN_FREE_MAIN_MEMORY + S32Megabytes(150); - if (free_sys_mem > MEM_THRESHOLD && sSysMemoryFactor > 1.f) - { - // Ramp down factor over time. - constexpr F32 DECREMENT = 0.02f; - sSysMemoryFactor -= DECREMENT * gFrameIntervalSeconds; - sSysMemoryFactor = llclamp(sSysMemoryFactor, 1.f, 2.f); - } } - sys_was_low = is_sys_critically_low; -} - -//static -U32Megabytes LLViewerTexture::getFreeSystemMemory() -{ - static LLFrameTimer timer; - static U32Megabytes physical_res = U32Megabytes(U32_MAX); - - if (timer.getElapsedTimeF32() < MEMORY_CHECK_WAIT_TIME) //call this once per second. - { - return physical_res; - } - - timer.reset(); - - LLMemory::updateMemoryInfo(); - physical_res = LLMemory::getAvailableMemKB(); - return physical_res; -} - -//static -bool LLViewerTexture::isSystemMemoryLow() -{ - return getFreeSystemMemory() < get_render_free_main_memory_treshold(); -} - -//static -bool LLViewerTexture::isSystemMemoryCritical() -{ - return getFreeSystemMemory() < get_render_free_main_memory_treshold() / 2; -} - -// static -F32 LLViewerTexture::getSystemMemoryBudgetFactor() -{ - return sSysMemoryFactor; } //end of static functions @@ -2320,14 +2176,7 @@ bool LLViewerFetchedTexture::updateFetch() // resident minus what's resident now. Settled at postCreateTexture (or // on the cancel/failure paths). setPendingByteDelta(estimatedVRAMBytesAtDiscard(mRequestedDiscardLevel) - residentVRAMBytes()); - ++sUprezRequestCount; - if (mGCEvicted) - { - // GC evict->refetch cycle; counted as gcref/s, should be ~0 when settled. - mGCEvicted = false; - ++sGCRefetchCount; - } mFetchState = LLAppViewer::getTextureFetch()->getFetchState(mID, mDownloadProgress, mRequestedDownloadPriority, mFetchPriority, mFetchDeltaTime, mRequestDeltaTime, mCanUseHTTP); } @@ -3308,7 +3157,6 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c static LLCachedControl gc_step_mips(gSavedSettings, "TextureGCStepMips", 1); constexpr U32 GC_RESUME_GRACE_FRAMES = 10; const U32 now = LLFrameTimer::getFrameCount(); - mGCFloored = false; if (gli->mLastBindFrame > 0 // drawn at least once && now - sGCSuspendedFrame > GC_RESUME_GRACE_FRAMES) // not just back from background { @@ -3318,8 +3166,6 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c { const S32 step_mips = (S32)llmax((U32)gc_step_mips, 1u); desired = llclamp(desired + periods * step_mips, desired, dim_max_i); - mGCFloored = true; - ++sCooldownFlooredCount; } } } @@ -3406,10 +3252,6 @@ void LLViewerLODTexture::processTextureStats() S32 current_discard = getDiscardLevel(); if (!avatar_bake && current_discard >= 0 && current_discard < mDesiredDiscardLevel && !mForceToSaveRawImage) { - if (mGCFloored) - { - mGCEvicted = true; // eviction attributable to the visibility GC - } scaleDown(); } @@ -3455,7 +3297,6 @@ bool LLViewerLODTexture::scaleDown() gTextureList.mDownScaleQueue.push(this); // Pending-free ledger entry: bytes decided-freed, returned when the queue drains. setPendingByteDelta(estimatedVRAMBytesAtDiscard(mDesiredDiscardLevel) - residentVRAMBytes()); - ++sDownscaleEnqueueCount; } return true; diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index ce9a953de5..145661fefe 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -114,11 +114,6 @@ protected: public: static void initClass(); static void updateClass(); - static bool isSystemMemoryLow(); - static bool isSystemMemoryCritical(); - - // Ranges from 1 (no RAM deficit) to 2 (RAM deficit) - static F32 getSystemMemoryBudgetFactor(); LLViewerTexture(bool usemipmaps = true); LLViewerTexture(const LLUUID& id, bool usemipmaps) ; @@ -198,8 +193,6 @@ private: friend class LLBumpImageList; friend class LLUIImageList; - static U32Megabytes getFreeSystemMemory(); - protected: friend class LLViewerTextureList; LLUUID mID; @@ -259,12 +252,6 @@ public: static S64 sPendingAllocBytes; // in-flight toward allocation static S64 sPendingFreeBytes; // queued for release, not yet returned - // 1Hz churn counters (main thread; reset each pressure-log tick). High - // uprez+downscale with no memory pressure = per-texture oscillation. - static U32 sUprezRequestCount; // finer-mip fetch requests issued - static U32 sDownscaleEnqueueCount; // scaleDown enqueues - static U32 sCooldownFlooredCount; // desired raised by the cooldown floor - static S32 sMaxSculptRez ; static U32 sMinLargeImageSize ; static U32 sMaxSmallImageSize ; @@ -273,7 +260,6 @@ public: // estimated free memory for textures, by bias calculation static F32 sFreeVRAMMegabytes; - static F32 sSysMemoryFactor; // Viewport pixel area, refreshed once per frame. Hoisted to keep the // per-texture hot path out of gViewerWindow. static F32 sWindowPixelArea; @@ -453,14 +439,6 @@ public: bool mCreatePending = false; // if true, this is in gTextureList.mCreateTextureList mutable bool mDownScalePending = false; // if true, this is in gTextureList.mDownScaleQueue - // GC-cycle diagnostics (main thread): mGCFloored = the visibility GC - // raised desired on the last computeDesiredDiscard; mGCEvicted = this - // texture was actually evicted because of it. A subsequent uprez fetch - // request while mGCEvicted is a full evict->refetch cycle - the churn - // signature - and gets sampled into the 1Hz TextureStream log. - mutable bool mGCFloored = false; - bool mGCEvicted = false; - // --- committed-bytes ledger (main thread only) --- // Estimated VRAM bytes this texture would occupy resident at `discard` // (components ~4, x4/3 mip chain, /4 rough DXT when compression is on). diff --git a/indra/newview/llvocache.cpp b/indra/newview/llvocache.cpp index 7618739b3c..f3efe3f3bb 100644 --- a/indra/newview/llvocache.cpp +++ b/indra/newview/llvocache.cpp @@ -488,12 +488,6 @@ void LLVOCacheEntry::updateDebugSettings() static const F32 MIN_RADIUS = 1.0f; F32 draw_radius = gAgentCamera.mDrawDistance; - if (LLViewerTexture::getSystemMemoryBudgetFactor() > 1.f) - { - // Factor is intended to go from 1.0 to 2.0 - // For safety cap reduction at 50%, we don't want to go below half of draw distance - draw_radius = llmax(draw_radius / LLViewerTexture::getSystemMemoryBudgetFactor(), draw_radius / 2.f); - } const F32 clamped_min_radius = llclamp((F32) min_radius, MIN_RADIUS, draw_radius); // [1, mDrawDistance] sNearRadius = MIN_RADIUS + ((clamped_min_radius - MIN_RADIUS) * adjust_factor); -- cgit v1.3 From 1c8144b1708420b30c54472a5139d451034e89de Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:53:53 +0300 Subject: #4604 Restore system memory factor, but in LLMemory --- indra/llcommon/llmemory.cpp | 91 +++++++++++++++++++++++++++++++++++++- indra/llcommon/llmemory.h | 9 +++- indra/llcommon/llsys.cpp | 2 +- indra/llcommon/llsys.h | 8 ++-- indra/newview/llappviewerwin32.cpp | 15 ++++--- indra/newview/llviewerdisplay.cpp | 5 +++ indra/newview/llviewermessage.cpp | 8 ++++ indra/newview/llvocache.cpp | 8 ++++ 8 files changed, 133 insertions(+), 13 deletions(-) (limited to 'indra/newview/llviewerdisplay.cpp') diff --git a/indra/llcommon/llmemory.cpp b/indra/llcommon/llmemory.cpp index 31424bfed3..4e44b9a56a 100644 --- a/indra/llcommon/llmemory.cpp +++ b/indra/llcommon/llmemory.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2002&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -73,6 +73,12 @@ U32Kilobytes LLMemory::sAllocatedMemInKB(0); U32Kilobytes LLMemory::sAllocatedPageSizeInKB(0); +LLFrameTimer LLMemory::sMemoryCheckTimer; +F32 LLMemory::sSysMemoryFactor = 1.f; +U32 LLMemory::sFactorLastFrameCount = 0; + +static const S32Megabytes MEM_LOW_THRESHOLD = S32Megabytes(256); + static LLTrace::SampleStatHandle sAllocatedMem("allocated_mem", "active memory in use by application"); static LLTrace::SampleStatHandle sVirtualMem("virtual_mem", "virtual memory assigned to application"); @@ -105,12 +111,13 @@ void LLMemory::updateMemoryInfo() { LL_PROFILE_ZONE_SCOPED; + sMemoryCheckTimer.reset(); sMaxPhysicalMemInKB = gSysMemory.getPhysicalMemoryKB(); LLMemoryInfo::updateAvailableMemory(); #if LL_WINDOWS - // On windows getAvailableMemoryKB fills sAvailPhysicalMemInKB, + // On windows updateAvailableMemory fills sAvailPhysicalMemInKB, //sAllocatedMemInKB and sAllocatedPageSizeInKB sample(sVirtualMem, sAllocatedPageSizeInKB); @@ -194,6 +201,86 @@ void LLMemory::logMemoryInfo(bool update) LL_INFOS() << llformat("Current max usable memory: %.2f MB", sMaxPhysicalMemInKB / 1024.0) << LL_ENDL; } +void LLMemory::updateFreeSystemMemory() +{ + if (sMemoryCheckTimer.getElapsedTimeF32() >= 1.f) //once per second. + { + LLMemory::updateMemoryInfo(); // resets the timer + } +} + +F32 LLMemory::getSystemMemoryBudgetFactor() +{ + // Only update once per frame + U32 current_frame = LLFrameTimer::getFrameCount(); + if (sFactorLastFrameCount == current_frame) + { + return sSysMemoryFactor; + } + sFactorLastFrameCount = current_frame; + + updateFreeSystemMemory(); +#if LL_WINDOWS + S32Megabytes free_sys_mem = getAvailableCommitMemMB(); +#else + S32Megabytes free_sys_mem = getAvailableMemKB(); +#endif + bool is_sys_low = free_sys_mem < MEM_LOW_THRESHOLD; + static bool was_low = false; + + // sSysMemoryFactor affects draw distance + // + // We only decrement when more than 406MB is free, but increment + // when below 256MB free. This should provide a stable value + // in the 256-406MB range to avoid draw range fluctuations. + // + // Draw range reduction is a last resort, texture bias is supposed + // to free at least some memory before we get here. + // Note: textures were mostly moved to vram, we might want to + // detach texture bias from system memory. + if (is_sys_low) + { + // debt is a negative value since MIN_FREE_MAIN_MEMORY > free memory. + S32Megabytes sys_budget_debt = free_sys_mem - MEM_LOW_THRESHOLD; + + // Leave some padding, otherwise we will crash out of memory before hitting factor 2. + const S32Megabytes PAD_BUFFER(32); + S32Megabytes budget_target = MEM_LOW_THRESHOLD - PAD_BUFFER; + if (!was_low) + { + // Result should range from 1 at 0 debt to 2 at -224 debt, 2.14 at -256MB + F32 new_factor = 1.f - (F32)sys_budget_debt.value() / (F32)budget_target.value(); + sSysMemoryFactor = llmax(sSysMemoryFactor, new_factor); + } + else + { + // Slowly ramp up factor to free memory (increasing factor decreases draw range) + constexpr F32 MAX_INCREMENT = 0.05f; + F32 increment = MAX_INCREMENT * llmax(-(F32)sys_budget_debt.value() / (F32)budget_target.value(), 0.f); + sSysMemoryFactor += increment * LLFrameTimer::getFrameDeltaTimeF32(); + } + sSysMemoryFactor = llclamp(sSysMemoryFactor, 1.f, 2.f); + } + else + { + // Only start ramping down when we have breathing room. + // This should be under the value of isSystemMemoryLow to not throw texture + // bias into 1.5+ territory each time we fluctuate around isSystemMemoryLow's + // threshold. + const S32Megabytes MEM_THRESHOLD = MEM_LOW_THRESHOLD + S32Megabytes(150); + if (free_sys_mem > MEM_THRESHOLD && sSysMemoryFactor > 1.f) + { + // Ramp down factor over time. + constexpr F32 DECREMENT = 0.02f; + sSysMemoryFactor -= DECREMENT * LLFrameTimer::getFrameDeltaTimeF32(); + sSysMemoryFactor = llclamp(sSysMemoryFactor, 1.f, 2.f); + } + } + was_low = is_sys_low; + + return sSysMemoryFactor; +} + #if LL_WINDOWS //static U32Megabytes LLMemory::getAvailableCommitMemMB() diff --git a/indra/llcommon/llmemory.h b/indra/llcommon/llmemory.h index 290f6e03d1..efcd7aadc4 100644 --- a/indra/llcommon/llmemory.h +++ b/indra/llcommon/llmemory.h @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2002&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -27,6 +27,7 @@ #define LLMEMORY_H #include "linden_common.h" +#include "llframetimer.h" #include "llunits.h" #include "stdtypes.h" #if !LL_WINDOWS @@ -428,6 +429,7 @@ public: static void initMaxHeapSizeGB(F32Gigabytes max_heap_size); static void updateMemoryInfo() ; static void logMemoryInfo(bool update = false); + static F32 getSystemMemoryBudgetFactor(); #if LL_WINDOWS // Commit charge is a Windows-only concept, combines page file and ram @@ -437,6 +439,7 @@ public: static U32Kilobytes getMaxMemKB() ; static U32Kilobytes getAllocatedMemKB() ; private: + static void updateFreeSystemMemory(); // LLMemoryInfo directly updates memory stats friend class LLMemoryInfo; @@ -447,6 +450,10 @@ private: static U32Kilobytes sAllocatedPageSizeInKB ; static U32Kilobytes sMaxHeapSizeInKB; + + static LLFrameTimer sMemoryCheckTimer; + static F32 sSysMemoryFactor; + static U32 sFactorLastFrameCount; }; // LLRefCount moved to llrefcount.h diff --git a/indra/llcommon/llsys.cpp b/indra/llcommon/llsys.cpp index 568a6b36dc..bc48fc0fd2 100644 --- a/indra/llcommon/llsys.cpp +++ b/indra/llcommon/llsys.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2002&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public diff --git a/indra/llcommon/llsys.h b/indra/llcommon/llsys.h index 709fb29a82..0abbe047ad 100644 --- a/indra/llcommon/llsys.h +++ b/indra/llcommon/llsys.h @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -134,12 +134,12 @@ public: static U32Kilobytes getHardwareMemSize(); // Because some Mac linkers won't let us reference extern gSysMemory from a different lib. #endif - // Updates LLMemory's values (which ones is OS specific). + // Updates LLMemory's values. static void updateAvailableMemory(); // Retrieve a map of memory statistics. The keys of the map are platform- - // dependent. The values are in kilobytes to try to avoid integer overflow. - // On windows updates LLMemory values. + // dependent. + // On Windows updates LLMemory values. LLSD getStatsMap() const; // Re-fetch memory data (as reported by stream() and getStatsMap()) from the diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 5c2594e85c..8a9ddbcde6 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -82,6 +82,7 @@ #include "BugSplat.h" #include "boost/json.hpp" // Boost.Json #include "llagent.h" // for agent location +#include "llmemory.h" #include "llstartup.h" #include "llviewerregion.h" #include "llvoavatarself.h" // for agent name @@ -181,11 +182,15 @@ namespace sBugSplatSender->setAttribute(WCSTR(L"VRAM"), WCSTR(STRINGIZE(gGLManager.mVRAM))); sBugSplatSender->setAttribute(WCSTR(L"RAM"), WCSTR(STRINGIZE(gSysMemory.getPhysicalMemoryKB().value()))); - // Memory usage at crash time (can be 1s obsolete) - sBugSplatSender->setAttribute(WCSTR(L"MemAllocatedKB"), WCSTR(std::to_string(LLMemory::getAllocatedMemKB().value()))); - sBugSplatSender->setAttribute(WCSTR(L"MemAvailableKB"), WCSTR(std::to_string(LLMemory::getAvailableMemKB().value()))); - sBugSplatSender->setAttribute(WCSTR(L"MemMaxPhysicalKB"), WCSTR(std::to_string(LLMemory::getMaxMemKB().value()))); - sBugSplatSender->setAttribute(WCSTR(L"MemAvailCommitMB"), WCSTR(std::to_string(LLMemory::getAvailableCommitMemMB().value()))); + const U32 avail_kb = LLMemory::getAvailableMemKB().value(); + if (avail_kb != U32_MAX) // filter out initial values, if one is not set, all are not set + { + // Memory usage at crash time (can be 1s obsolete) + sBugSplatSender->setAttribute(WCSTR(L"MemAllocatedKB"), WCSTR(std::to_string(LLMemory::getAllocatedMemKB().value()))); + sBugSplatSender->setAttribute(WCSTR(L"MemAvailableKB"), WCSTR(std::to_string(LLMemory::getAvailableMemKB().value()))); + sBugSplatSender->setAttribute(WCSTR(L"MemMaxPhysicalKB"), WCSTR(std::to_string(LLMemory::getMaxMemKB().value()))); + sBugSplatSender->setAttribute(WCSTR(L"MemAvailCommitMB"), WCSTR(std::to_string(LLMemory::getAvailableCommitMemMB().value()))); + } if (gAgent.getRegion()) { diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 9f1b0d75f3..0d50ba6fe2 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -217,6 +217,11 @@ void display_update_camera() { final_far *= 0.5f; } + // When system memory is critically low or recovering, shrink draw distance. + else if (const F32 mem_factor = LLMemory::getSystemMemoryBudgetFactor(); mem_factor > 1.f) + { + final_far = llmax(32.f, final_far / mem_factor); + } LLViewerCamera::getInstance()->setFar(final_far); LLVOAvatar::sRenderDistance = llclamp(final_far, 16.f, 256.f); gViewerWindow->setup3DRender(); diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 09f17fec40..8863dfb501 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -40,6 +40,7 @@ #include "llinventorydefines.h" #include "lllslconstants.h" #include "llmaterialtable.h" +#include "llmemory.h" #include "llregionhandle.h" #include "llsd.h" #include "llsdserialize.h" @@ -3371,6 +3372,13 @@ void send_agent_update(bool force_send, bool send_reliable) static F32 last_draw_disatance_step = 1024; F32 memory_limited_draw_distance = gAgentCamera.mDrawDistance; + const F32 mem_factor = LLMemory::getSystemMemoryBudgetFactor(); + if (mem_factor > 1.f) + { + // We are critically low on memory or recovering, + // limit requested draw distance + memory_limited_draw_distance = llmax(gAgentCamera.mDrawDistance / mem_factor, gAgentCamera.mDrawDistance / 2.f); + } if (tp_state == LLAgent::TELEPORT_ARRIVING || LLStartUp::getStartupState() < STATE_MISC) { diff --git a/indra/newview/llvocache.cpp b/indra/newview/llvocache.cpp index f3efe3f3bb..e513a3813f 100644 --- a/indra/newview/llvocache.cpp +++ b/indra/newview/llvocache.cpp @@ -32,6 +32,7 @@ #include "lldrawable.h" #include "llviewerregion.h" #include "llagentcamera.h" +#include "llmemory.h" #include "llsdserialize.h" #include "llworld.h" // For LLWorld::getInstance() //static variables @@ -488,6 +489,13 @@ void LLVOCacheEntry::updateDebugSettings() static const F32 MIN_RADIUS = 1.0f; F32 draw_radius = gAgentCamera.mDrawDistance; + const F32 mem_factor = LLMemory::getSystemMemoryBudgetFactor(); + if (mem_factor > 1.f) + { + // Factor is intended to go from 1.0 to 2.0 + // For safety cap reduction at 50%, we don't want to go below half of draw distance + draw_radius = llmax(draw_radius / mem_factor, draw_radius / 2.f); + } const F32 clamped_min_radius = llclamp((F32) min_radius, MIN_RADIUS, draw_radius); // [1, mDrawDistance] sNearRadius = MIN_RADIUS + ((clamped_min_radius - MIN_RADIUS) * adjust_factor); -- cgit v1.3 From a937b237de3651e79cdb517f14381a5bdd4c844b Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Thu, 9 Jul 2026 08:09:24 -0400 Subject: Geenz/texture loading speed (#5985) * Add more controls for texture loading budgets. Should yield much faster loading within a given FPS target - should generally self regulate depending on your framerate. * Harden texture pipeline against stalls and OOM. Generally makes texture loading faster, at the expense of some budgeting (which we weren't doing a great job at anyways). --- indra/llrender/llgl.cpp | 6 + indra/llrender/llgl.h | 1 + indra/llrender/llimagegl.cpp | 14 +- indra/newview/app_settings/settings.xml | 37 ++- indra/newview/llface.cpp | 22 ++ indra/newview/llface.h | 18 +- indra/newview/llviewerdisplay.cpp | 31 ++- indra/newview/llviewertexture.cpp | 112 ++++++--- indra/newview/llviewertexture.h | 19 ++ indra/newview/llviewertexturelist.cpp | 426 +++++++++++++++++++------------- indra/newview/llviewertexturelist.h | 5 + 11 files changed, 482 insertions(+), 209 deletions(-) (limited to 'indra/newview/llviewerdisplay.cpp') diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index 4584ed1d86..0e59c449db 100644 --- a/indra/llrender/llgl.cpp +++ b/indra/llrender/llgl.cpp @@ -2370,6 +2370,12 @@ void clear_glerror() glGetError(); } +void drain_glerror() +{ + // bounded: a lost/reset context can return errors indefinitely + for (S32 i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {} +} + /////////////////////////////////////////////////////////////// // // LLGLState diff --git a/indra/llrender/llgl.h b/indra/llrender/llgl.h index e1ab2a49e6..3f9a9de70a 100644 --- a/indra/llrender/llgl.h +++ b/indra/llrender/llgl.h @@ -159,6 +159,7 @@ void log_glerror(); void assert_glerror(); void clear_glerror(); +void drain_glerror(); // pops ALL pending GL error flags (bounded so a lost/reset context that returns errors forever cannot hang the caller); use before an attributable glGetError check. # define stop_glerror() assert_glerror() diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index c8a23d873e..b3cd8d2896 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -1507,7 +1507,7 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt // Drain stale GL errors so an OOM detected below belongs to this alloc. // Otherwise a failed glTexImage2D is swallowed in release while // alloc_tex_image still counts the bytes, inflating the used-VRAM figure. - while (glGetError() != GL_NO_ERROR) {} + drain_glerror(); const bool use_sub_image = should_stagger_image_set(compress); if (!use_sub_image) @@ -1919,7 +1919,8 @@ bool LLImageGL::readBackRaw(S32 discard_level, LLImageRaw* imageraw, bool compre //----------------------------------------------------------------------------------------------- GLenum error ; - while((error = glGetError()) != GL_NO_ERROR) + S32 error_count = 0 ; + while((error = glGetError()) != GL_NO_ERROR && ++error_count <= 16) { LL_WARNS() << "GL Error happens before reading back texture. Error code: " << error << LL_ENDL ; } @@ -1979,7 +1980,8 @@ bool LLImageGL::readBackRaw(S32 discard_level, LLImageRaw* imageraw, bool compre LL_WARNS() << "GL Error happens after reading back texture. Error code: " << error << LL_ENDL ; imageraw->deleteData() ; - while((error = glGetError()) != GL_NO_ERROR) + error_count = 0 ; + while((error = glGetError()) != GL_NO_ERROR && ++error_count <= 16) { LL_WARNS() << "GL Error happens after reading back texture. Error code: " << error << LL_ENDL ; } @@ -2548,8 +2550,10 @@ bool LLImageGL::scaleDown(S32 desired_discard) { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; - // Don't let eviction re-arm visibility: the glGenerateMipmap re-bind below - // would otherwise stamp mLastBindFrame and keep the texture fetch-eligible. + // Don't let eviction re-arm the GC: the glGenerateMipmap re-bind below would + // otherwise stamp mLastBindFrame, so the next computeDesiredDiscard treats the + // just-evicted texture as freshly drawn, un-floors it, and re-fetches - the + // evict/refetch oscillation. LLImageGLStampBypass no_stamp; if (mTarget != GL_TEXTURE_2D diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 45a7dced5c..1c2c2d0559 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11992,16 +11992,49 @@ Value 1 - TextureFetchVisibilityFrames + TextureLoadTargetFPS Comment - Only fetch a texture if it was drawn within this many rendered frames; out-of-view content isn't fetched. Minimum 1 (0 is clamped up). Boosted/UI textures, avatar bakes, and callback textures are exempt. + Frame rate the viewer is willing to drop to while loading textures. The texture pipeline's per-frame budget is the headroom between this and the actual frame cost, so fast machines load aggressively and slow ones hold their frame rate. + Persist + 1 + Type + F32 + Value + 30.0 + + TextureLoadBudgetMaxMS + + Comment + Hard cap, in milliseconds per frame, on the adaptive texture-pipeline budget. + Persist + 1 + Type + F32 + Value + 10.0 + + TextureFetchStepMips + + Comment + Fetch refinement step, in mip levels: a texture whose resident data is coarser than desired by more than this fetches in steps of this size instead of jumping straight to the final resolution, so it sharpens progressively instead of sitting blurry then popping. 0 = jump directly. Boosted/pinned textures always jump. Persist 1 Type U32 Value + 2 + + TextureFrustumAllowance + + Comment + Falloff width for out-of-frustum texture resolution, as a fraction of screen size. Content grazing the screen edge keeps full resolution; content this far past the edge reaches the deepest mip, lerped between. Keeps barely-out-of-view textures resident so panning back doesn't refetch them. + Persist 1 + Type + F32 + Value + 0.5 TextureDecodeDisabled diff --git a/indra/newview/llface.cpp b/indra/newview/llface.cpp index e34bea63ef..c96e1bd3b0 100644 --- a/indra/newview/llface.cpp +++ b/indra/newview/llface.cpp @@ -2297,6 +2297,8 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) // don't update every frame if (gFrameTimeSeconds - mLastPixelAreaUpdate < PIXEL_AREA_UPDATE_PERIOD) { + cos_angle_to_view_dir = mLastCosAngleToViewDir; + radius = mLastRadius; return true; } @@ -2368,6 +2370,7 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) // no rigged extents, zero out bounding box and skip update mRiggedExtents[0] = mRiggedExtents[1] = LLVector4a(0.f, 0.f, 0.f); + mInFrustum = false; return false; } @@ -2422,6 +2425,7 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) if(!camera->AABBInFrustum(center, size)) { mImportanceToCamera = 0.f ; + mInFrustum = false; return false ; } if(cos_angle_to_view_dir > camera->getCosHalfFov()) //the center is within the view frustum @@ -2450,6 +2454,24 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) mImportanceToCamera = LLFace::calcImportanceToCamera(cos_angle_to_view_dir, dist) ; } + // On-screen test: does the face's projected disc overlap the screen disc? + // (Same construction as adjustPartialOverlapPixelArea.) Behind-camera faces + // get acos(cos) near pi and fall out; the generous screen radius errs toward + // "on screen" so fetch admission never starves edge content. mFrustumOverflow + // is how far past the boundary the disc sits, as a fraction of screen size - + // 0 on screen, 0.1 = 10% of a screen out - and feeds the frustum allowance + // falloff in computeDesiredDiscard. + { + F32 center_px = acosf(llclamp(cos_angle_to_view_dir, -1.f, 1.f)) * LLDrawable::sCurPixelAngle; + F32 screen_radius = (F32)llmax(gViewerWindow->getWindowWidthRaw(), gViewerWindow->getWindowHeightRaw()); + F32 past_edge = center_px - radius - screen_radius; + mInFrustum = past_edge <= 5.f; + mFrustumOverflow = llmax(past_edge - 5.f, 0.f) / screen_radius; + } + + mLastCosAngleToViewDir = cos_angle_to_view_dir; + mLastRadius = radius; + return true ; } diff --git a/indra/newview/llface.h b/indra/newview/llface.h index 71ba3d0f2f..0b0a17b8c8 100644 --- a/indra/newview/llface.h +++ b/indra/newview/llface.h @@ -269,10 +269,21 @@ public: // return mSkinInfo->mHash or 0 if mSkinInfo is null U64 getSkinHash(); - // true if face was recently in the main camera frustum according to LLViewerTextureList updates + // True if this face's projected bounding disc overlaps the screen - maintained + // by calcPixelArea() (sticky between its throttled updates). Drives per-texture + // fetch admission (LLViewerTextureList::updateImageDecodePriority -> mOnScreen). bool mInFrustum = false; + // How far past the screen boundary the projected disc sits, as a fraction of + // screen size (0 = on screen). Feeds the frustum-allowance falloff so barely + // out-of-view content keeps its resolution. Maintained with mInFrustum. + F32 mFrustumOverflow = 0.f; // value of gFrameCount the last time the face was touched by LLViewerTextureList::updateImageDecodePriority U32 mLastTextureUpdate = 0; + // Cached per-channel streaming coverage (repeat-adjusted screen pixels), + // refreshed at the mLastTextureUpdate cadence and shared by every texture + // on this face. 0 = degenerate / not yet measured. See + // update_face_stream_vsize in llviewertexturelist.cpp. + F32 mStreamVSize[LLRender::NUM_TEXTURE_CHANNELS] = {}; private: LLPointer mVertexBuffer; @@ -309,6 +320,11 @@ private: // gFrameTimeSeconds when mPixelArea was last updated F32 mLastPixelAreaUpdate = 0.f; + // Last cos-angle-to-view-dir and projected radius computed by calcPixelArea; + // reused by its throttled early-return so the overlap test gets real values. + F32 mLastCosAngleToViewDir = 1.f; + F32 mLastRadius = 0.f; + // virtual size of face in texture area (mPixelArea adjusted by texture repeats) // used to determine desired resolution of texture F32 mVSize; diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 0d50ba6fe2..be83fd0279 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -140,6 +140,27 @@ void render_disconnected_background(); void getProfileStatsContext(boost::json::object& stats); std::string getProfileStatsFilename(); +// Adaptive texture-pipeline budget: spend the frame-time headroom between the +// frame we're rendering and TextureLoadTargetFPS, clamped [2ms, max]. Headroom +// is measured (smoothed frame interval minus the pipeline's own last spend), +// so fast machines get big budgets and machines already at target hold the +// floor. Only consumed while queues have work - drain loops exit when empty. +static F32 sTexturePipelineSpent = 0.f; +static F32 texture_pipeline_budget() +{ + static LLCachedControl target_fps(gSavedSettings, "TextureLoadTargetFPS", 60.f); + static LLCachedControl max_ms(gSavedSettings, "TextureLoadBudgetMaxMS", 10.f); + static F32 smoothed_other = 0.008f; + F32 other = llmax(gFrameIntervalSeconds.value() - sTexturePipelineSpent, 0.f); + // A single multi-second hitch must not crater the budget for the following + // frames, so cap the sample before it enters the EMA. + other = llmin(other, 0.1f); + smoothed_other = smoothed_other * 0.9f + other * 0.1f; + F32 target_interval = 1.f / llclamp((F32)target_fps, 15.f, 240.f); + F32 headroom = target_interval - smoothed_other; + return llclamp(headroom, 0.002f, llclamp((F32)max_ms, 2.f, 50.f) * 0.001f); +} + void display_startup() { if ( !gViewerWindow @@ -500,9 +521,10 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) { LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("List"); - F32 max_image_decode_time = 0.050f * gFrameIntervalSeconds.value(); // 50 ms/second decode time - max_image_decode_time = llclamp(max_image_decode_time, 0.002f, 0.005f); // min 2ms/frame, max 5ms/frame) + F32 max_image_decode_time = texture_pipeline_budget(); + LLTimer tex_timer; gTextureList.updateImages(max_image_decode_time); + sTexturePipelineSpent = tex_timer.getElapsedTimeF32(); } { @@ -862,9 +884,10 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) { LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("List"); - F32 max_image_decode_time = 0.050f*gFrameIntervalSeconds.value(); // 50 ms/second decode time - max_image_decode_time = llclamp(max_image_decode_time, 0.002f, 0.005f ); // min 2ms/frame, max 5ms/frame) + F32 max_image_decode_time = texture_pipeline_budget(); + LLTimer tex_timer; gTextureList.updateImages(max_image_decode_time); + sTexturePipelineSpent = tex_timer.getElapsedTimeF32(); } { diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 80daffdbf3..90facfa333 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -571,6 +571,20 @@ void LLViewerTexture::updateClass() sPixelToTexelRatio += llmax((F32)relax_rate, 0.f) * dt; } // else: hold in the hysteresis band. + + // Allocation failures outrank the byte estimate: if setManualImage hit + // GL_OUT_OF_MEMORY since last frame, the CPU-side vram_used estimate has + // diverged from reality, so step the ratio down now regardless of the band. + static U32 last_oom_count = 0; + U32 oom_count = LLImageGL::sOOMErrorCount.load(); + if (oom_count > last_oom_count) + { + U32 new_events = llmin(oom_count - last_oom_count, (U32)5); + sPixelToTexelRatio -= llmax((F32)tighten_rate, 0.f) * 1.0f * (F32)new_events; + last_oom_count = oom_count; + LL_WARNS_ONCE("Texture") << "GL out-of-memory during texture upload triggered a pixel:texel ratio backoff." << LL_ENDL; + } + sPixelToTexelRatio = llclamp(sPixelToTexelRatio, 0.f, r_max); // Keep the GC-suspend frame current while backgrounded. This suppresses @@ -1483,6 +1497,16 @@ void LLViewerFetchedTexture::postCreateTexture() setActive(); + // Start the visibility-GC clock at creation. A texture fetched but never + // drawn (occluded, or the camera moved on) would otherwise keep + // mLastBindFrame == 0 forever, and the GC skips never-bound textures - it + // would hold residency indefinitely. Anchoring here means it ages out on + // the normal GC cooldown unless a real draw stamps it first. + if (mGLTexturep.notNull() && mGLTexturep->mLastBindFrame == 0) + { + mGLTexturep->mLastBindFrame = LLFrameTimer::getFrameCount(); + } + // rebuild any volumes that are using this texture for sculpts in case their LoD has changed for (U32 i = 0; i < mNumVolumes[LLRender::SCULPT_TEX]; ++i) { @@ -1942,7 +1966,14 @@ bool LLViewerFetchedTexture::updateFetch() S32 current_discard = getCurrentDiscardLevelForFetching(); S32 desired_discard = getDesiredDiscardLevel(); - F32 decode_priority = mMaxVirtualSize; + + // Two-tier fetch priority, constantly drained by the fetch worker (it + // sorts HTTP dispatch by this value): any on-screen texture outranks every + // off-screen one. Within the visible tier, coverage orders by size on + // screen; within the off-screen tier the same geometric coverage + // (area/dist^2) orders by proximity. addTextureStats clamps + // mMaxVirtualSize to sMaxVirtualSize, so the band offset is strict. + F32 decode_priority = mMaxVirtualSize + (mOnScreen ? sMaxVirtualSize : 0.f); if (mIsFetching) { @@ -2003,6 +2034,29 @@ bool LLViewerFetchedTexture::updateFetch() // discards are served from the GL mip pyramid via scaleDown. desired_discard = llmin(desired_discard, (S32)mCodecMaxDiscardLevel); + // Progressive refinement: when resident data is much coarser than desired, + // fetch in steps of TextureFetchStepMips instead of jumping straight to the + // final discard. Each step is small, decodes fast, and shows on screen + // while the next chains behind it (the per-frame fast pump makes chaining + // nearly free). Without this, seen-before textures (dims known, so the + // coarse first-fetch fallback never applies) sat blurry for the whole + // full-file read+decode, then popped. Boosted/pinned content still jumps. + static LLCachedControl fetch_step(gSavedSettings, "TextureFetchStepMips", 2); + const S32 step = (S32)fetch_step; + if (step > 0 + && current_discard >= 0 + && desired_discard < current_discard - step + && mBoostLevel < LLGLTexture::BOOST_HIGH + && mUseMipMaps + && !mDontDiscard + && !isAgentAvatarBoost(mBoostLevel)) + { + // Re-clamp: current can sit past codec max after scaleDown (GL + // pyramid goes deeper than the codestream) - a stepped request + // above codec max reaches the decoder with an invalid discard. + desired_discard = llmin(current_discard - step, (S32)mCodecMaxDiscardLevel); + } + bool make_request = true; if (decode_priority <= 0) { @@ -2028,30 +2082,9 @@ bool LLViewerFetchedTexture::updateFetch() LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - current < min"); make_request = false; } - else - { - // Only fetch streamed world textures the renderer is actually drawing - // (mLastBindFrame is stamped per drawn frame). Out-of-view content gets - // no residency. Exempt: boosted/UI, avatar bakes, textures with loaded - // callbacks, and bake uploads. - static LLCachedControl vis_frames(gSavedSettings, "TextureFetchVisibilityFrames", 5); - const bool visibility_gated = mBoostLevel < LLGLTexture::BOOST_HIGH - && mUseMipMaps - && !mDontDiscard - && !isAgentAvatarBoost(mBoostLevel) - && !mForceToSaveRawImage - && mLoadedCallbackList.empty(); - if (visibility_gated && mGLTexturep.notNull()) - { - const U32 last = mGLTexturep->mLastBindFrame; - const U32 now = LLFrameTimer::getFrameCount(); - if (last == 0 || now - last > llmax((U32)vis_frames, 1u)) - { - LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - not visible"); - make_request = false; - } - } - } + // No visibility gate here: off-screen content still fetches, just in the + // low-priority band (decode_priority above), so the worker services it only + // after visible work. Residency stays with the GC (computeDesiredDiscard). if (make_request) { @@ -3035,6 +3068,23 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c // (a 2048 map can't hit its own resolution on a 1080p screen), which reads // as everything being blurry. Pressure still evicts by lowering R_global. ideal = llmax(ideal, 0.f); + + // Frustum allowance: a falloff on how unloaded out-of-view content gets, + // by how far out it is. Grazing the edge keeps full resolution; at + // TextureFrustumAllowance (fraction of a screen) past the edge it reaches + // the deepest mip, lerped between. Keeps barely-out-of-view textures + // resident so a camera swing back doesn't refetch them. Applied to the + // continuous ideal so it shares the hysteresis dead-band below - applied + // after it, camera motion made desired flap a mip at a time and churned + // the fetch/scaleDown queues. + static LLCachedControl frustum_allowance(gSavedSettings, "TextureFrustumAllowance", 0.2f); + if (!avatar_bake && mFrustumOverflow > 0.f) + { + const F32 f = llclamp(mFrustumOverflow / llmax((F32)frustum_allowance, 0.01f), 0.f, 1.f); + ideal += f * ((F32)dim_max_i - ideal); + mLastOffScreenFrame = LLFrameTimer::getFrameCount(); + } + const S32 target = (S32)floor(ideal); // Hysteresis: a texture at discard C is "happy" while floor(ideal) == C, @@ -3069,7 +3119,12 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c // mip by gc_step, walking gradually toward the deepest mip instead of slamming. // Content drawn within the last cooldown stays full-res, so a fast camera pan // finds it only a step or two coarse on the way back. Resets when drawn again. - if (!avatar_bake) + // + // Out-of-frustum content is governed by the frustum allowance above instead + // (spatial falloff, not bind staleness) - without this exclusion the GC would + // walk barely-out-of-view content to the deepest mip within a second and the + // allowance would protect nothing. + if (!avatar_bake && mFrustumOverflow <= 0.f) { if (LLImageGL* gli = getGLTexture()) { @@ -3077,8 +3132,9 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c static LLCachedControl gc_step_mips(gSavedSettings, "TextureGCStepMips", 1); constexpr U32 GC_RESUME_GRACE_FRAMES = 10; const U32 now = LLFrameTimer::getFrameCount(); - if (gli->mLastBindFrame > 0 // drawn at least once - && now - sGCSuspendedFrame > GC_RESUME_GRACE_FRAMES) // not just back from background + if (gli->mLastBindFrame > 0 // drawn, or anchored at creation (postCreateTexture) + && now - sGCSuspendedFrame > GC_RESUME_GRACE_FRAMES // not just back from background + && now - mLastOffScreenFrame > GC_RESUME_GRACE_FRAMES) // re-entering content gets one grace window to be drawn and re-stamp before staleness is judged { const U32 cooldown = llmax((U32)gc_cooldown_frames, 1u); const S32 periods = (S32)((now - gli->mLastBindFrame) / cooldown); diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index cc9fbe5e48..90993ed109 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -215,6 +215,25 @@ protected: F32 mChannelCoverage[4] = { 0.f, 0.f, 0.f, 0.f }; F32 mChannelCoverageMin[4] = { 0.f, 0.f, 0.f, 0.f }; + // Any face using this texture projects onto the screen (published alongside + // the coverage above). Selects the fetch-priority band in updateFetch. + // Defaults true so unmeasured textures (fresh objects, no-face users) are + // never starved. + bool mOnScreen = true; + + // How far out of frustum the texture's least-out-of-view use sits, as a + // fraction of screen size (0 = on screen). Drives the frustum-allowance + // falloff in computeDesiredDiscard. + F32 mFrustumOverflow = 0.f; + + // Last frame this texture was out of frustum (mFrustumOverflow > 0). The + // GC in computeDesiredDiscard gives re-entering content one grace window to + // be drawn and re-stamp mLastBindFrame before its staleness is judged. + mutable U32 mLastOffScreenFrame = 0; + + // Membership flag for LLViewerTextureList::mFastFetchList (dedup). + bool mInFastFetchList = false; + ll_face_list_t mFaceList[LLRender::NUM_TEXTURE_CHANNELS]; //reverse pointer pointing to the faces using this image as texture U32 mNumFaces[LLRender::NUM_TEXTURE_CHANNELS]; LLFrameTimer mLastFaceListUpdateTimer ; diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 3e1481f8b4..ad05a0273b 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -378,6 +378,12 @@ void LLViewerTextureList::shutdown() } mFastCacheList.clear(); + for (auto& img : mFastFetchList) + { + img->mInFastFetchList = false; + } + mFastFetchList.clear(); + mUUIDMap.clear(); mImageList.clear(); @@ -871,6 +877,36 @@ void LLViewerTextureList::updateImages(F32 max_time) remaining_time -= updateImagesFetchTextures(remaining_time); remaining_time = llmax(remaining_time, min_time); + // Fast pump: advance every in-flight fetch each frame so results are + // collected and creates scheduled the frame they're ready, instead of + // one state transition per round-robin visit. Cheap - no face scans - + // and bounded by the fetch worker's own concurrency. + LLTimer fast_fetch_timer; + S32 min_count = 32; + for (size_t i = 0; i < mFastFetchList.size(); ) + { + LLViewerFetchedTexture* imagep = mFastFetchList[i]; + if (imagep->getNumRefs() > 1) + { + imagep->updateFetch(); + } + if (imagep->getNumRefs() <= 1 || (!imagep->isFetching() && !imagep->hasFetcher())) + { + imagep->mInFastFetchList = false; + mFastFetchList[i] = mFastFetchList.back(); + mFastFetchList.pop_back(); + } + else + { + ++i; + } + + if (fast_fetch_timer.getElapsedTimeF32() > remaining_time && --min_count <= 0) + { + break; + } + } + //handle results from decode threads updateImagesCreateTextures(remaining_time); @@ -915,6 +951,187 @@ void LLViewerTextureList::clearFetchingRequests() extern bool gCubeSnapshot; +// Refresh a face's cached per-channel streaming coverage (face->mStreamVSize). +// This is the most-demanding-point measurement plus each channel's own UV +// repeat source, computed ONCE per face per update cadence and shared by every +// texture registered on the face. Doing the material/transform pointer chases +// per texture visit instead made updateImageDecodePriority several times more +// expensive per face than develop's, and since the round-robin runs in a fixed +// per-frame time slice, that directly cut how many textures advance their +// load state each frame - the whole pipeline paced slower. +static void update_face_stream_vsize(LLFace* face) +{ + // Bounds on the per-face UV repeat-area divisor (mined from the old + // getTextureVirtualSize texel_area clamp [1/64, 128]): atlas/crop boost + // capped at 64x (3 mips finer), tiling penalty at 128x (3.5 mips + // coarser) so pathological UV scales can't explode either direction. + constexpr F32 MIN_REPEAT_AREA = 1.f / 64.f; + constexpr F32 MAX_REPEAT_AREA = 128.f; + + LLViewerObject* objp = face->getViewerObject(); + + // Most-demanding-point measurement: the spec is that the LOWEST pixel:texel + // ratio governs, so pixel density is evaluated at the face's NEAREST point + // and applied to the face's true world area. A whole-face average + // (bounding-disc pixel area) under-resolves perspective surfaces: on a + // floor, the tile at your feet covers far more screen than the average + // tile, and the GPU samples fine mips right there. + const LLVector4a* ext = face->isState(LLFace::RIGGED) ? face->mRiggedExtents : face->mExtents; + LLVector4a diag; + diag.setSub(ext[1], ext[0]); + // World area of the face ~ product of the two largest AABB dims (max + // pairwise product; robust for flat faces). + F32 dx = diag[0], dy = diag[1], dz = diag[2]; + F32 area_world = llmax(dx * dy, llmax(dx * dz, dy * dz)); + // Pixels per meter at the nearest point. Distance floored: nearer than + // this the screen clamp below governs anyway. + F32 dist = llmax(face->mDistanceToCamera, 0.5f); + F32 ppm = LLDrawable::sCurPixelAngle / dist; + F32 face_px = area_world * ppm * ppm; + if (face_px <= 0.f) + { + // Degenerate extents: the face hasn't been through a geometry build + // yet (or a rigged face has no rigged extents) - it isn't renderable, + // so it must not be measured. Zero marks "skip": an invented + // placeholder value would become the texture's least-demanding "use" + // and, under TextureDownrezCoverageBias, drag the whole texture to + // its deepest mip (and it poisoned BP and PBR asymmetrically, since + // the two register faces at different points in the geometry + // lifecycle). + for (U32 ch = 0; ch < LLRender::NUM_TEXTURE_CHANNELS; ++ch) + { + face->mStreamVSize[ch] = 0.f; + } + return; + } + + S32 te_offset = face->getTEOffset(); // offset is -1 if not inited + const LLTextureEntry* te = (te_offset < 0 || te_offset >= objp->getNumTEs()) ? nullptr : objp->getTE(te_offset); + + // Shared, channel-independent chases - hoisted out of the channel loop. + const LLGLTFMaterial* gltf_mat = te ? te->getGLTFRenderMaterial() : nullptr; + const LLMaterial* mat = te ? te->getMaterialParams().get() : nullptr; + + // Continuously-animated scale (llSetTextureAnim SCALE) bypasses both + // static sources via mTextureMatrix - the live animated values win. + bool anim_scale = false; + F32 anim_ss = 0.f, anim_st = 0.f; + if (te) + { + if (LLVOVolume* vvo = face->getDrawable() ? face->getDrawable()->getVOVolume() : nullptr) + { + LLViewerTextureAnim* anim = vvo->mTextureAnimp; + if (anim && (anim->mMode & LLTextureAnim::ON) && (anim->mMode & LLTextureAnim::SCALE) + && (anim->mFace < 0 || anim->mFace == te_offset)) + { + anim_scale = true; + anim_ss = anim->mScaleS; + anim_st = anim->mScaleT; + } + } + } + + // Mesh atlas sub-rect: a face whose intrinsic UVs span only part of + // [0,1]^2 shows that fraction of the image. Applies identically to all + // channels - the per-channel transforms stack on the raw face UVs. + F32 span = 1.f; + if (te) + { + if (LLVolume* vol = objp->getVolume()) + { + if (te_offset >= 0 && te_offset < vol->getNumVolumeFaces()) + { + const LLVolumeFace& vf = vol->getVolumeFace(te_offset); + F32 s = fabsf((vf.mTexCoordExtents[1].mV[0] - vf.mTexCoordExtents[0].mV[0]) + * (vf.mTexCoordExtents[1].mV[1] - vf.mTexCoordExtents[0].mV[1])); + if (s > 0.f) + { + span = s; + } + } + } + } + + // Avatar bonus: worn attachments get a coverage multiplier - avatars are + // what people look at, and rigged extents make attachment coverage + // measurement unreliable anyway. Multiplicative, not a slam. + static LLCachedControl avatar_boost(gSavedSettings, "TextureAvatarBoost", 4.f); + const F32 boost = objp->isAttachment() ? llmax((F32)avatar_boost, 1.f) : 1.f; + + for (U32 ch = 0; ch < LLRender::NUM_TEXTURE_CHANNELS; ++ch) + { + // Effective UV repeat AREA: the tiling term of texels-drawn-per- + // screen-pixel. More tiling => each tile smaller on screen => coarser + // mips suffice (penalty). Repeats < 1 (atlas/crop) => whole-image + // residency for a sub-rect legitimately demands more than its screen + // coverage (boost). + F32 repeats = 1.f; + if (te) + { + // UV scale source: every channel reads the repeat values ITS + // renderer actually applies. diffuse -> TE scale; Blinn + // normal/spec -> LLMaterial per-map repeats; PBR channels -> KHR + // texture_transform scale. Fallback is the TE scale - never a + // silent hardcoded 1. + F32 scale_s = te->getScaleS(); + F32 scale_t = te->getScaleT(); + if (ch >= LLRender::BASECOLOR_MAP) + { + // LLRender channel -> LLGLTFMaterial::TextureInfo + static const S32 gltf_info[4] = { + LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR, // BASECOLOR_MAP (3) + LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, // METALLIC_ROUGHNESS_MAP (4) + LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL, // GLTF_NORMAL_MAP (5) + LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE, // EMISSIVE_MAP (6) + }; + if (gltf_mat) + { + const LLVector2& s = gltf_mat->mTextureTransform[gltf_info[ch - LLRender::BASECOLOR_MAP]].mScale; + scale_s = s.mV[0]; + scale_t = s.mV[1]; + } + } + else if (ch == LLRender::NORMAL_MAP || ch == LLRender::SPECULAR_MAP) + { + // Blinn-Phong normal/specular maps carry their own repeats in + // LLMaterial - the renderer builds their texture matrices + // from these, NOT from the TE's diffuse scale. + if (mat) + { + if (ch == LLRender::NORMAL_MAP) + { + mat->getNormalRepeat(scale_s, scale_t); + } + else + { + mat->getSpecularRepeat(scale_s, scale_t); + } + } + } + + if (anim_scale) + { + scale_s = anim_ss; + scale_t = anim_st; + } + + repeats = fabsf(scale_s * scale_t) * span; + } + + repeats = llclamp(repeats, MIN_REPEAT_AREA, MAX_REPEAT_AREA); + + // Apply the two sides of the repeat term in the right order relative + // to the screen clamp: tiling (repeats > 1) divides the nearest-point + // footprint BEFORE the clamp (one tile can't draw more pixels than + // the screen); atlas/crop (repeats < 1) boosts AFTER it (whole-image + // residency for a crop legitimately demands more than its screen + // coverage). + F32 tiling = llmax(repeats, 1.f); + F32 crop = llmin(repeats, 1.f); + face->mStreamVSize[ch] = llmin(face_px / tiling, LLViewerTexture::sWindowPixelArea) / crop * boost; + } +} + void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imagep, bool flush_images) { llassert(!gCubeSnapshot); @@ -931,13 +1148,6 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag if (imagep->getBoostLevel() < LLViewerFetchedTexture::BOOST_HIGH) // don't bother checking face list for boosted textures { - // Bounds on the per-face UV repeat-area divisor (mined from the old - // getTextureVirtualSize texel_area clamp [1/64, 128]): atlas/crop boost - // capped at 64x (3 mips finer), tiling penalty at 128x (3.5 mips - // coarser) so pathological UV scales can't explode either direction. - constexpr F32 MIN_REPEAT_AREA = 1.f / 64.f; - constexpr F32 MAX_REPEAT_AREA = 128.f; - // Per priority bucket (0=Normal, 1=BaseColor, 2=Specular, 3=Emissive): // the HIGHEST per-face effective coverage (= the lowest texels-per-pixel // use, the most demanding variant - drives desired discard) and the @@ -950,6 +1160,9 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag F32 channel_coverage_min[4] = { FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX }; bool bucket_used[4] = { false, false, false, false }; F32 max_coverage = 0.f; + bool on_screen = false; // any face's projected disc overlaps the screen + bool any_face = false; + F32 min_overflow = FLT_MAX; // least out-of-frustum use across faces U32 face_count = 0; @@ -1000,175 +1213,29 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag F32 radius; F32 cos_angle_to_view_dir; if ((gFrameCount - face->mLastTextureUpdate) > 10) - { // only call calcPixelArea at most once every 10 frames for a given face - // this helps eliminate redundant calls to calcPixelArea for faces that have multiple textures - // assigned to them, such as is the case with GLTF materials or Blinn-Phong materials - face->mInFrustum = face->calcPixelArea(cos_angle_to_view_dir, radius); + { // refresh the face's geometry + cached coverage at most once every + // 10 frames; every texture/channel sharing this face (GLTF and + // Blinn-Phong materials) reuses the cache instead of redoing the + // measurement. (calcPixelArea maintains face->mInFrustum itself.) + face->calcPixelArea(cos_angle_to_view_dir, radius); + update_face_stream_vsize(face); face->mLastTextureUpdate = gFrameCount; } - // Most-demanding-point measurement: the spec is that the - // LOWEST pixel:texel ratio governs, so pixel density is - // evaluated at the face's NEAREST point and applied to the - // face's true world area. The previous whole-face average - // (bounding-disc pixel area) under-resolved perspective - // surfaces: on a floor, the tile at your feet covers far - // more screen than the average tile, and the GPU samples - // fine mips right there - tiled (PBR-heavy) content went - // soft while untiled content looked fine. - const LLVector4a* ext = face->isState(LLFace::RIGGED) ? face->mRiggedExtents : face->mExtents; - LLVector4a diag; - diag.setSub(ext[1], ext[0]); - // World area of the face ~ product of the two largest AABB - // dims (max pairwise product; robust for flat faces). - F32 dx = diag[0], dy = diag[1], dz = diag[2]; - F32 area_world = llmax(dx * dy, llmax(dx * dz, dy * dz)); - // Pixels per meter at the nearest point. Distance floored: - // nearer than this the screen clamp below governs anyway. - F32 dist = llmax(face->mDistanceToCamera, 0.5f); - F32 ppm = LLDrawable::sCurPixelAngle / dist; - F32 face_px = area_world * ppm * ppm; - if (face_px <= 0.f) + // Cached measurement - see update_face_stream_vsize above. + // Zero = degenerate extents / not yet through a geometry + // build: not renderable, must not be measured (a + // placeholder value would poison the per-bucket MIN bound + // and drag the texture to its deepest mip). + F32 vsize = face->mStreamVSize[i]; + if (vsize <= 0.f) { - // Degenerate extents: the face hasn't been through a - // geometry build yet (or a rigged face has no rigged - // extents) - it isn't renderable, so it must not be - // measured. Skipping matters especially for the - // per-bucket MIN bound: any invented placeholder - // value (the old fallback hit LLFace::init's 16px - // default) becomes the texture's least-demanding - // "use" and, under TextureDownrezCoverageBias, drags - // the whole texture to its deepest mip - and it - // poisoned BP and PBR asymmetrically since the two - // systems register faces at different points in the - // geometry lifecycle. continue; } - // Effective UV repeat AREA across this face: the tiling - // term of texels-drawn-per-screen-pixel. More tiling => - // each tile is smaller on screen => coarser mips suffice - // (penalty). Repeats < 1 (atlas/crop) => only a sub-rect - // of the image is shown, but discard levels are whole- - // image, so the full image must be resident at 1/repeats - // times the crop's pixel count (boost). - S32 te_offset = face->getTEOffset(); // offset is -1 if not inited - LLViewerObject* objp = face->getViewerObject(); - const LLTextureEntry* te = (te_offset < 0 || te_offset >= objp->getNumTEs()) ? nullptr : objp->getTE(te_offset); - - F32 repeats = 1.f; - if (te) - { - // UV scale source: every channel reads the repeat - // values ITS renderer actually applies, then flows - // through the identical pipeline below. Sources: - // diffuse -> TE scale - // Blinn normal/spec -> LLMaterial per-map repeats - // PBR channels -> KHR texture_transform scale - // Fallback for any missing material is the TE scale - - // never a silent hardcoded 1. - F32 scale_s = te->getScaleS(); - F32 scale_t = te->getScaleT(); - if (i >= LLRender::BASECOLOR_MAP) - { - // LLRender channel -> LLGLTFMaterial::TextureInfo - static const S32 gltf_info[4] = { - LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR, // BASECOLOR_MAP (3) - LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, // METALLIC_ROUGHNESS_MAP (4) - LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL, // GLTF_NORMAL_MAP (5) - LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE, // EMISSIVE_MAP (6) - }; - if (const LLGLTFMaterial* gltf_mat = te->getGLTFRenderMaterial()) - { - const LLVector2& s = gltf_mat->mTextureTransform[gltf_info[i - LLRender::BASECOLOR_MAP]].mScale; - scale_s = s.mV[0]; - scale_t = s.mV[1]; - } - } - else if (i == LLRender::NORMAL_MAP || i == LLRender::SPECULAR_MAP) - { - // Blinn-Phong normal/specular maps carry their own - // repeats in LLMaterial - the renderer builds their - // texture matrices from these, NOT from the TE's - // diffuse scale. Reading the diffuse scale here made - // Blinn normals scale differently than PBR normals - // (whose per-channel transform IS read above). - if (const LLMaterial* mat = te->getMaterialParams().get()) - { - if (i == LLRender::NORMAL_MAP) - { - mat->getNormalRepeat(scale_s, scale_t); - } - else - { - mat->getSpecularRepeat(scale_s, scale_t); - } - } - } - - // Continuously-animated scale (llSetTextureAnim SCALE) - // bypasses both static sources via mTextureMatrix - - // the live animated values win on either path. - if (LLVOVolume* vvo = face->getDrawable() ? face->getDrawable()->getVOVolume() : nullptr) - { - LLViewerTextureAnim* anim = vvo->mTextureAnimp; - if (anim && (anim->mMode & LLTextureAnim::ON) && (anim->mMode & LLTextureAnim::SCALE) - && (anim->mFace < 0 || anim->mFace == te_offset)) - { - scale_s = anim->mScaleS; - scale_t = anim->mScaleT; - } - } - - repeats = fabsf(scale_s * scale_t); - - // Mesh atlas sub-rect: a face whose intrinsic UVs span - // only part of [0,1]^2 shows that fraction of the - // image. Applies identically to both paths - the - // transforms above stack on the raw face UVs. - if (LLVolume* vol = objp->getVolume()) - { - if (te_offset >= 0 && te_offset < vol->getNumVolumeFaces()) - { - const LLVolumeFace& vf = vol->getVolumeFace(te_offset); - F32 span = fabsf((vf.mTexCoordExtents[1].mV[0] - vf.mTexCoordExtents[0].mV[0]) - * (vf.mTexCoordExtents[1].mV[1] - vf.mTexCoordExtents[0].mV[1])); - if (span > 0.f) - { - repeats *= span; - } - } - } - } - - repeats = llclamp(repeats, MIN_REPEAT_AREA, MAX_REPEAT_AREA); - - // Apply the two sides of the repeat term in the right - // order relative to the screen clamp: - // - tiling (repeats > 1): the per-tile footprint at the - // nearest point, THEN clamped - one tile can't draw - // more pixels than the screen. (Clamping the whole - // face first and then dividing crushed near tiles.) - // - atlas/crop (repeats < 1): boost AFTER the clamp - - // whole-image residency for a crop legitimately - // demands more than its screen coverage. - F32 tiling = llmax(repeats, 1.f); - F32 crop = llmin(repeats, 1.f); - F32 vsize = llmin(face_px / tiling, LLViewerTexture::sWindowPixelArea) / crop; - - // Avatar bonus: worn attachments get a coverage - // multiplier - avatars are what people look at, and - // rigged extents make attachment coverage measurement - // unreliable anyway. Multiplicative, not a slam: a - // nearby avatar gains ~a mip of headroom while a distant - // one still downrezzes naturally with its coverage. - // (System-avatar bakes get the same bonus in the - // no-faces branch below.) - if (objp->isAttachment()) - { - static LLCachedControl avatar_boost(gSavedSettings, "TextureAvatarBoost", 4.f); - vsize *= llmax((F32)avatar_boost, 1.f); - } + any_face = true; + on_screen = on_screen || face->mInFrustum; + min_overflow = llmin(min_overflow, face->mFrustumOverflow); if (bucket >= 0 && bucket < 4) { @@ -1242,6 +1309,15 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag imagep->mChannelCoverage[b] = channel_coverage[b]; imagep->mChannelCoverageMin[b] = (channel_coverage_min[b] == FLT_MAX) ? 0.f : channel_coverage_min[b]; } + + // Fetch admission signal: false only when faces were actually scanned and + // every one projects off screen. Textures with no scannable faces (bakes, + // spotlights, the >1024-face boost path, not-yet-built geometry) stay + // eligible - blocking them is what stalls load-in. + imagep->mOnScreen = on_screen || !any_face; + // Least out-of-frustum use governs the allowance falloff; unknown = 0 + // (no penalty), same reasoning as mOnScreen. + imagep->mFrustumOverflow = any_face ? min_overflow : 0.f; } #if 0 @@ -1510,6 +1586,18 @@ F32 LLViewerTextureList::updateImagesFetchTextures(F32 max_time) { updateImageDecodePriority(imagep); imagep->updateFetch(); + + // Fast-pump membership: textures with an active fetch get + // updateFetch every frame (in updateImages) instead of waiting + // ~a sweep period per state transition - that wait, times the + // 2-4 transitions a load needs, was the measured throughput + // ceiling. Purely additive: the sweep still pumps everything, + // so fetches started by any other path can never strand. + if ((imagep->isFetching() || imagep->hasFetcher()) && !imagep->mInFastFetchList) + { + imagep->mInFastFetchList = true; + mFastFetchList.push_back(imagep); + } } if (timer.getElapsedTimeF32() > max_time) diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h index 931f2ed50e..7004238995 100644 --- a/indra/newview/llviewertexturelist.h +++ b/indra/newview/llviewertexturelist.h @@ -34,6 +34,7 @@ #include "llviewertexture.h" #include "llui.h" #include +#include #include #include "lluiimage.h" @@ -225,6 +226,10 @@ public: image_list_t mCallbackList; image_list_t mFastCacheList; + // In-flight fetches pumped every frame (additive to the round-robin + // sweep, which remains the universal pump). See updateImages. + std::vector > mFastFetchList; + bool mForceResetTextureStats; // to make "for (auto& imagep : gTextureList)" work -- cgit v1.3 From 91c11c8d87c963f2b17e45b525396bf8c8386c26 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Wed, 22 Jul 2026 00:38:20 -0400 Subject: Revert "Geenz/texture loading speed (#5985)" This reverts commit a937b237de3651e79cdb517f14381a5bdd4c844b. --- indra/llrender/llgl.cpp | 6 - indra/llrender/llgl.h | 1 - indra/llrender/llimagegl.cpp | 14 +- indra/newview/app_settings/settings.xml | 37 +-- indra/newview/llface.cpp | 22 -- indra/newview/llface.h | 18 +- indra/newview/llviewerdisplay.cpp | 31 +-- indra/newview/llviewertexture.cpp | 112 +++------ indra/newview/llviewertexture.h | 19 -- indra/newview/llviewertexturelist.cpp | 426 +++++++++++++------------------- indra/newview/llviewertexturelist.h | 5 - 11 files changed, 209 insertions(+), 482 deletions(-) (limited to 'indra/newview/llviewerdisplay.cpp') diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index 0e59c449db..4584ed1d86 100644 --- a/indra/llrender/llgl.cpp +++ b/indra/llrender/llgl.cpp @@ -2370,12 +2370,6 @@ void clear_glerror() glGetError(); } -void drain_glerror() -{ - // bounded: a lost/reset context can return errors indefinitely - for (S32 i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {} -} - /////////////////////////////////////////////////////////////// // // LLGLState diff --git a/indra/llrender/llgl.h b/indra/llrender/llgl.h index 3f9a9de70a..e1ab2a49e6 100644 --- a/indra/llrender/llgl.h +++ b/indra/llrender/llgl.h @@ -159,7 +159,6 @@ void log_glerror(); void assert_glerror(); void clear_glerror(); -void drain_glerror(); // pops ALL pending GL error flags (bounded so a lost/reset context that returns errors forever cannot hang the caller); use before an attributable glGetError check. # define stop_glerror() assert_glerror() diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index b3cd8d2896..c8a23d873e 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -1507,7 +1507,7 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt // Drain stale GL errors so an OOM detected below belongs to this alloc. // Otherwise a failed glTexImage2D is swallowed in release while // alloc_tex_image still counts the bytes, inflating the used-VRAM figure. - drain_glerror(); + while (glGetError() != GL_NO_ERROR) {} const bool use_sub_image = should_stagger_image_set(compress); if (!use_sub_image) @@ -1919,8 +1919,7 @@ bool LLImageGL::readBackRaw(S32 discard_level, LLImageRaw* imageraw, bool compre //----------------------------------------------------------------------------------------------- GLenum error ; - S32 error_count = 0 ; - while((error = glGetError()) != GL_NO_ERROR && ++error_count <= 16) + while((error = glGetError()) != GL_NO_ERROR) { LL_WARNS() << "GL Error happens before reading back texture. Error code: " << error << LL_ENDL ; } @@ -1980,8 +1979,7 @@ bool LLImageGL::readBackRaw(S32 discard_level, LLImageRaw* imageraw, bool compre LL_WARNS() << "GL Error happens after reading back texture. Error code: " << error << LL_ENDL ; imageraw->deleteData() ; - error_count = 0 ; - while((error = glGetError()) != GL_NO_ERROR && ++error_count <= 16) + while((error = glGetError()) != GL_NO_ERROR) { LL_WARNS() << "GL Error happens after reading back texture. Error code: " << error << LL_ENDL ; } @@ -2550,10 +2548,8 @@ bool LLImageGL::scaleDown(S32 desired_discard) { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; - // Don't let eviction re-arm the GC: the glGenerateMipmap re-bind below would - // otherwise stamp mLastBindFrame, so the next computeDesiredDiscard treats the - // just-evicted texture as freshly drawn, un-floors it, and re-fetches - the - // evict/refetch oscillation. + // Don't let eviction re-arm visibility: the glGenerateMipmap re-bind below + // would otherwise stamp mLastBindFrame and keep the texture fetch-eligible. LLImageGLStampBypass no_stamp; if (mTarget != GL_TEXTURE_2D diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 1c2c2d0559..45a7dced5c 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11992,49 +11992,16 @@ Value 1 - TextureLoadTargetFPS + TextureFetchVisibilityFrames Comment - Frame rate the viewer is willing to drop to while loading textures. The texture pipeline's per-frame budget is the headroom between this and the actual frame cost, so fast machines load aggressively and slow ones hold their frame rate. - Persist - 1 - Type - F32 - Value - 30.0 - - TextureLoadBudgetMaxMS - - Comment - Hard cap, in milliseconds per frame, on the adaptive texture-pipeline budget. - Persist - 1 - Type - F32 - Value - 10.0 - - TextureFetchStepMips - - Comment - Fetch refinement step, in mip levels: a texture whose resident data is coarser than desired by more than this fetches in steps of this size instead of jumping straight to the final resolution, so it sharpens progressively instead of sitting blurry then popping. 0 = jump directly. Boosted/pinned textures always jump. + Only fetch a texture if it was drawn within this many rendered frames; out-of-view content isn't fetched. Minimum 1 (0 is clamped up). Boosted/UI textures, avatar bakes, and callback textures are exempt. Persist 1 Type U32 Value - 2 - - TextureFrustumAllowance - - Comment - Falloff width for out-of-frustum texture resolution, as a fraction of screen size. Content grazing the screen edge keeps full resolution; content this far past the edge reaches the deepest mip, lerped between. Keeps barely-out-of-view textures resident so panning back doesn't refetch them. - Persist 1 - Type - F32 - Value - 0.5 TextureDecodeDisabled diff --git a/indra/newview/llface.cpp b/indra/newview/llface.cpp index c96e1bd3b0..e34bea63ef 100644 --- a/indra/newview/llface.cpp +++ b/indra/newview/llface.cpp @@ -2297,8 +2297,6 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) // don't update every frame if (gFrameTimeSeconds - mLastPixelAreaUpdate < PIXEL_AREA_UPDATE_PERIOD) { - cos_angle_to_view_dir = mLastCosAngleToViewDir; - radius = mLastRadius; return true; } @@ -2370,7 +2368,6 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) // no rigged extents, zero out bounding box and skip update mRiggedExtents[0] = mRiggedExtents[1] = LLVector4a(0.f, 0.f, 0.f); - mInFrustum = false; return false; } @@ -2425,7 +2422,6 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) if(!camera->AABBInFrustum(center, size)) { mImportanceToCamera = 0.f ; - mInFrustum = false; return false ; } if(cos_angle_to_view_dir > camera->getCosHalfFov()) //the center is within the view frustum @@ -2454,24 +2450,6 @@ bool LLFace::calcPixelArea(F32& cos_angle_to_view_dir, F32& radius) mImportanceToCamera = LLFace::calcImportanceToCamera(cos_angle_to_view_dir, dist) ; } - // On-screen test: does the face's projected disc overlap the screen disc? - // (Same construction as adjustPartialOverlapPixelArea.) Behind-camera faces - // get acos(cos) near pi and fall out; the generous screen radius errs toward - // "on screen" so fetch admission never starves edge content. mFrustumOverflow - // is how far past the boundary the disc sits, as a fraction of screen size - - // 0 on screen, 0.1 = 10% of a screen out - and feeds the frustum allowance - // falloff in computeDesiredDiscard. - { - F32 center_px = acosf(llclamp(cos_angle_to_view_dir, -1.f, 1.f)) * LLDrawable::sCurPixelAngle; - F32 screen_radius = (F32)llmax(gViewerWindow->getWindowWidthRaw(), gViewerWindow->getWindowHeightRaw()); - F32 past_edge = center_px - radius - screen_radius; - mInFrustum = past_edge <= 5.f; - mFrustumOverflow = llmax(past_edge - 5.f, 0.f) / screen_radius; - } - - mLastCosAngleToViewDir = cos_angle_to_view_dir; - mLastRadius = radius; - return true ; } diff --git a/indra/newview/llface.h b/indra/newview/llface.h index 0b0a17b8c8..71ba3d0f2f 100644 --- a/indra/newview/llface.h +++ b/indra/newview/llface.h @@ -269,21 +269,10 @@ public: // return mSkinInfo->mHash or 0 if mSkinInfo is null U64 getSkinHash(); - // True if this face's projected bounding disc overlaps the screen - maintained - // by calcPixelArea() (sticky between its throttled updates). Drives per-texture - // fetch admission (LLViewerTextureList::updateImageDecodePriority -> mOnScreen). + // true if face was recently in the main camera frustum according to LLViewerTextureList updates bool mInFrustum = false; - // How far past the screen boundary the projected disc sits, as a fraction of - // screen size (0 = on screen). Feeds the frustum-allowance falloff so barely - // out-of-view content keeps its resolution. Maintained with mInFrustum. - F32 mFrustumOverflow = 0.f; // value of gFrameCount the last time the face was touched by LLViewerTextureList::updateImageDecodePriority U32 mLastTextureUpdate = 0; - // Cached per-channel streaming coverage (repeat-adjusted screen pixels), - // refreshed at the mLastTextureUpdate cadence and shared by every texture - // on this face. 0 = degenerate / not yet measured. See - // update_face_stream_vsize in llviewertexturelist.cpp. - F32 mStreamVSize[LLRender::NUM_TEXTURE_CHANNELS] = {}; private: LLPointer mVertexBuffer; @@ -320,11 +309,6 @@ private: // gFrameTimeSeconds when mPixelArea was last updated F32 mLastPixelAreaUpdate = 0.f; - // Last cos-angle-to-view-dir and projected radius computed by calcPixelArea; - // reused by its throttled early-return so the overlap test gets real values. - F32 mLastCosAngleToViewDir = 1.f; - F32 mLastRadius = 0.f; - // virtual size of face in texture area (mPixelArea adjusted by texture repeats) // used to determine desired resolution of texture F32 mVSize; diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index be83fd0279..0d50ba6fe2 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -140,27 +140,6 @@ void render_disconnected_background(); void getProfileStatsContext(boost::json::object& stats); std::string getProfileStatsFilename(); -// Adaptive texture-pipeline budget: spend the frame-time headroom between the -// frame we're rendering and TextureLoadTargetFPS, clamped [2ms, max]. Headroom -// is measured (smoothed frame interval minus the pipeline's own last spend), -// so fast machines get big budgets and machines already at target hold the -// floor. Only consumed while queues have work - drain loops exit when empty. -static F32 sTexturePipelineSpent = 0.f; -static F32 texture_pipeline_budget() -{ - static LLCachedControl target_fps(gSavedSettings, "TextureLoadTargetFPS", 60.f); - static LLCachedControl max_ms(gSavedSettings, "TextureLoadBudgetMaxMS", 10.f); - static F32 smoothed_other = 0.008f; - F32 other = llmax(gFrameIntervalSeconds.value() - sTexturePipelineSpent, 0.f); - // A single multi-second hitch must not crater the budget for the following - // frames, so cap the sample before it enters the EMA. - other = llmin(other, 0.1f); - smoothed_other = smoothed_other * 0.9f + other * 0.1f; - F32 target_interval = 1.f / llclamp((F32)target_fps, 15.f, 240.f); - F32 headroom = target_interval - smoothed_other; - return llclamp(headroom, 0.002f, llclamp((F32)max_ms, 2.f, 50.f) * 0.001f); -} - void display_startup() { if ( !gViewerWindow @@ -521,10 +500,9 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) { LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("List"); - F32 max_image_decode_time = texture_pipeline_budget(); - LLTimer tex_timer; + F32 max_image_decode_time = 0.050f * gFrameIntervalSeconds.value(); // 50 ms/second decode time + max_image_decode_time = llclamp(max_image_decode_time, 0.002f, 0.005f); // min 2ms/frame, max 5ms/frame) gTextureList.updateImages(max_image_decode_time); - sTexturePipelineSpent = tex_timer.getElapsedTimeF32(); } { @@ -884,10 +862,9 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) { LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("List"); - F32 max_image_decode_time = texture_pipeline_budget(); - LLTimer tex_timer; + F32 max_image_decode_time = 0.050f*gFrameIntervalSeconds.value(); // 50 ms/second decode time + max_image_decode_time = llclamp(max_image_decode_time, 0.002f, 0.005f ); // min 2ms/frame, max 5ms/frame) gTextureList.updateImages(max_image_decode_time); - sTexturePipelineSpent = tex_timer.getElapsedTimeF32(); } { diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 90facfa333..80daffdbf3 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -571,20 +571,6 @@ void LLViewerTexture::updateClass() sPixelToTexelRatio += llmax((F32)relax_rate, 0.f) * dt; } // else: hold in the hysteresis band. - - // Allocation failures outrank the byte estimate: if setManualImage hit - // GL_OUT_OF_MEMORY since last frame, the CPU-side vram_used estimate has - // diverged from reality, so step the ratio down now regardless of the band. - static U32 last_oom_count = 0; - U32 oom_count = LLImageGL::sOOMErrorCount.load(); - if (oom_count > last_oom_count) - { - U32 new_events = llmin(oom_count - last_oom_count, (U32)5); - sPixelToTexelRatio -= llmax((F32)tighten_rate, 0.f) * 1.0f * (F32)new_events; - last_oom_count = oom_count; - LL_WARNS_ONCE("Texture") << "GL out-of-memory during texture upload triggered a pixel:texel ratio backoff." << LL_ENDL; - } - sPixelToTexelRatio = llclamp(sPixelToTexelRatio, 0.f, r_max); // Keep the GC-suspend frame current while backgrounded. This suppresses @@ -1497,16 +1483,6 @@ void LLViewerFetchedTexture::postCreateTexture() setActive(); - // Start the visibility-GC clock at creation. A texture fetched but never - // drawn (occluded, or the camera moved on) would otherwise keep - // mLastBindFrame == 0 forever, and the GC skips never-bound textures - it - // would hold residency indefinitely. Anchoring here means it ages out on - // the normal GC cooldown unless a real draw stamps it first. - if (mGLTexturep.notNull() && mGLTexturep->mLastBindFrame == 0) - { - mGLTexturep->mLastBindFrame = LLFrameTimer::getFrameCount(); - } - // rebuild any volumes that are using this texture for sculpts in case their LoD has changed for (U32 i = 0; i < mNumVolumes[LLRender::SCULPT_TEX]; ++i) { @@ -1966,14 +1942,7 @@ bool LLViewerFetchedTexture::updateFetch() S32 current_discard = getCurrentDiscardLevelForFetching(); S32 desired_discard = getDesiredDiscardLevel(); - - // Two-tier fetch priority, constantly drained by the fetch worker (it - // sorts HTTP dispatch by this value): any on-screen texture outranks every - // off-screen one. Within the visible tier, coverage orders by size on - // screen; within the off-screen tier the same geometric coverage - // (area/dist^2) orders by proximity. addTextureStats clamps - // mMaxVirtualSize to sMaxVirtualSize, so the band offset is strict. - F32 decode_priority = mMaxVirtualSize + (mOnScreen ? sMaxVirtualSize : 0.f); + F32 decode_priority = mMaxVirtualSize; if (mIsFetching) { @@ -2034,29 +2003,6 @@ bool LLViewerFetchedTexture::updateFetch() // discards are served from the GL mip pyramid via scaleDown. desired_discard = llmin(desired_discard, (S32)mCodecMaxDiscardLevel); - // Progressive refinement: when resident data is much coarser than desired, - // fetch in steps of TextureFetchStepMips instead of jumping straight to the - // final discard. Each step is small, decodes fast, and shows on screen - // while the next chains behind it (the per-frame fast pump makes chaining - // nearly free). Without this, seen-before textures (dims known, so the - // coarse first-fetch fallback never applies) sat blurry for the whole - // full-file read+decode, then popped. Boosted/pinned content still jumps. - static LLCachedControl fetch_step(gSavedSettings, "TextureFetchStepMips", 2); - const S32 step = (S32)fetch_step; - if (step > 0 - && current_discard >= 0 - && desired_discard < current_discard - step - && mBoostLevel < LLGLTexture::BOOST_HIGH - && mUseMipMaps - && !mDontDiscard - && !isAgentAvatarBoost(mBoostLevel)) - { - // Re-clamp: current can sit past codec max after scaleDown (GL - // pyramid goes deeper than the codestream) - a stepped request - // above codec max reaches the decoder with an invalid discard. - desired_discard = llmin(current_discard - step, (S32)mCodecMaxDiscardLevel); - } - bool make_request = true; if (decode_priority <= 0) { @@ -2082,9 +2028,30 @@ bool LLViewerFetchedTexture::updateFetch() LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - current < min"); make_request = false; } - // No visibility gate here: off-screen content still fetches, just in the - // low-priority band (decode_priority above), so the worker services it only - // after visible work. Residency stays with the GC (computeDesiredDiscard). + else + { + // Only fetch streamed world textures the renderer is actually drawing + // (mLastBindFrame is stamped per drawn frame). Out-of-view content gets + // no residency. Exempt: boosted/UI, avatar bakes, textures with loaded + // callbacks, and bake uploads. + static LLCachedControl vis_frames(gSavedSettings, "TextureFetchVisibilityFrames", 5); + const bool visibility_gated = mBoostLevel < LLGLTexture::BOOST_HIGH + && mUseMipMaps + && !mDontDiscard + && !isAgentAvatarBoost(mBoostLevel) + && !mForceToSaveRawImage + && mLoadedCallbackList.empty(); + if (visibility_gated && mGLTexturep.notNull()) + { + const U32 last = mGLTexturep->mLastBindFrame; + const U32 now = LLFrameTimer::getFrameCount(); + if (last == 0 || now - last > llmax((U32)vis_frames, 1u)) + { + LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("vftuf - not visible"); + make_request = false; + } + } + } if (make_request) { @@ -3068,23 +3035,6 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c // (a 2048 map can't hit its own resolution on a 1080p screen), which reads // as everything being blurry. Pressure still evicts by lowering R_global. ideal = llmax(ideal, 0.f); - - // Frustum allowance: a falloff on how unloaded out-of-view content gets, - // by how far out it is. Grazing the edge keeps full resolution; at - // TextureFrustumAllowance (fraction of a screen) past the edge it reaches - // the deepest mip, lerped between. Keeps barely-out-of-view textures - // resident so a camera swing back doesn't refetch them. Applied to the - // continuous ideal so it shares the hysteresis dead-band below - applied - // after it, camera motion made desired flap a mip at a time and churned - // the fetch/scaleDown queues. - static LLCachedControl frustum_allowance(gSavedSettings, "TextureFrustumAllowance", 0.2f); - if (!avatar_bake && mFrustumOverflow > 0.f) - { - const F32 f = llclamp(mFrustumOverflow / llmax((F32)frustum_allowance, 0.01f), 0.f, 1.f); - ideal += f * ((F32)dim_max_i - ideal); - mLastOffScreenFrame = LLFrameTimer::getFrameCount(); - } - const S32 target = (S32)floor(ideal); // Hysteresis: a texture at discard C is "happy" while floor(ideal) == C, @@ -3119,12 +3069,7 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c // mip by gc_step, walking gradually toward the deepest mip instead of slamming. // Content drawn within the last cooldown stays full-res, so a fast camera pan // finds it only a step or two coarse on the way back. Resets when drawn again. - // - // Out-of-frustum content is governed by the frustum allowance above instead - // (spatial falloff, not bind staleness) - without this exclusion the GC would - // walk barely-out-of-view content to the deepest mip within a second and the - // allowance would protect nothing. - if (!avatar_bake && mFrustumOverflow <= 0.f) + if (!avatar_bake) { if (LLImageGL* gli = getGLTexture()) { @@ -3132,9 +3077,8 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c static LLCachedControl gc_step_mips(gSavedSettings, "TextureGCStepMips", 1); constexpr U32 GC_RESUME_GRACE_FRAMES = 10; const U32 now = LLFrameTimer::getFrameCount(); - if (gli->mLastBindFrame > 0 // drawn, or anchored at creation (postCreateTexture) - && now - sGCSuspendedFrame > GC_RESUME_GRACE_FRAMES // not just back from background - && now - mLastOffScreenFrame > GC_RESUME_GRACE_FRAMES) // re-entering content gets one grace window to be drawn and re-stamp before staleness is judged + if (gli->mLastBindFrame > 0 // drawn at least once + && now - sGCSuspendedFrame > GC_RESUME_GRACE_FRAMES) // not just back from background { const U32 cooldown = llmax((U32)gc_cooldown_frames, 1u); const S32 periods = (S32)((now - gli->mLastBindFrame) / cooldown); diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index 90993ed109..cc9fbe5e48 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -215,25 +215,6 @@ protected: F32 mChannelCoverage[4] = { 0.f, 0.f, 0.f, 0.f }; F32 mChannelCoverageMin[4] = { 0.f, 0.f, 0.f, 0.f }; - // Any face using this texture projects onto the screen (published alongside - // the coverage above). Selects the fetch-priority band in updateFetch. - // Defaults true so unmeasured textures (fresh objects, no-face users) are - // never starved. - bool mOnScreen = true; - - // How far out of frustum the texture's least-out-of-view use sits, as a - // fraction of screen size (0 = on screen). Drives the frustum-allowance - // falloff in computeDesiredDiscard. - F32 mFrustumOverflow = 0.f; - - // Last frame this texture was out of frustum (mFrustumOverflow > 0). The - // GC in computeDesiredDiscard gives re-entering content one grace window to - // be drawn and re-stamp mLastBindFrame before its staleness is judged. - mutable U32 mLastOffScreenFrame = 0; - - // Membership flag for LLViewerTextureList::mFastFetchList (dedup). - bool mInFastFetchList = false; - ll_face_list_t mFaceList[LLRender::NUM_TEXTURE_CHANNELS]; //reverse pointer pointing to the faces using this image as texture U32 mNumFaces[LLRender::NUM_TEXTURE_CHANNELS]; LLFrameTimer mLastFaceListUpdateTimer ; diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index ad05a0273b..3e1481f8b4 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -378,12 +378,6 @@ void LLViewerTextureList::shutdown() } mFastCacheList.clear(); - for (auto& img : mFastFetchList) - { - img->mInFastFetchList = false; - } - mFastFetchList.clear(); - mUUIDMap.clear(); mImageList.clear(); @@ -877,36 +871,6 @@ void LLViewerTextureList::updateImages(F32 max_time) remaining_time -= updateImagesFetchTextures(remaining_time); remaining_time = llmax(remaining_time, min_time); - // Fast pump: advance every in-flight fetch each frame so results are - // collected and creates scheduled the frame they're ready, instead of - // one state transition per round-robin visit. Cheap - no face scans - - // and bounded by the fetch worker's own concurrency. - LLTimer fast_fetch_timer; - S32 min_count = 32; - for (size_t i = 0; i < mFastFetchList.size(); ) - { - LLViewerFetchedTexture* imagep = mFastFetchList[i]; - if (imagep->getNumRefs() > 1) - { - imagep->updateFetch(); - } - if (imagep->getNumRefs() <= 1 || (!imagep->isFetching() && !imagep->hasFetcher())) - { - imagep->mInFastFetchList = false; - mFastFetchList[i] = mFastFetchList.back(); - mFastFetchList.pop_back(); - } - else - { - ++i; - } - - if (fast_fetch_timer.getElapsedTimeF32() > remaining_time && --min_count <= 0) - { - break; - } - } - //handle results from decode threads updateImagesCreateTextures(remaining_time); @@ -951,187 +915,6 @@ void LLViewerTextureList::clearFetchingRequests() extern bool gCubeSnapshot; -// Refresh a face's cached per-channel streaming coverage (face->mStreamVSize). -// This is the most-demanding-point measurement plus each channel's own UV -// repeat source, computed ONCE per face per update cadence and shared by every -// texture registered on the face. Doing the material/transform pointer chases -// per texture visit instead made updateImageDecodePriority several times more -// expensive per face than develop's, and since the round-robin runs in a fixed -// per-frame time slice, that directly cut how many textures advance their -// load state each frame - the whole pipeline paced slower. -static void update_face_stream_vsize(LLFace* face) -{ - // Bounds on the per-face UV repeat-area divisor (mined from the old - // getTextureVirtualSize texel_area clamp [1/64, 128]): atlas/crop boost - // capped at 64x (3 mips finer), tiling penalty at 128x (3.5 mips - // coarser) so pathological UV scales can't explode either direction. - constexpr F32 MIN_REPEAT_AREA = 1.f / 64.f; - constexpr F32 MAX_REPEAT_AREA = 128.f; - - LLViewerObject* objp = face->getViewerObject(); - - // Most-demanding-point measurement: the spec is that the LOWEST pixel:texel - // ratio governs, so pixel density is evaluated at the face's NEAREST point - // and applied to the face's true world area. A whole-face average - // (bounding-disc pixel area) under-resolves perspective surfaces: on a - // floor, the tile at your feet covers far more screen than the average - // tile, and the GPU samples fine mips right there. - const LLVector4a* ext = face->isState(LLFace::RIGGED) ? face->mRiggedExtents : face->mExtents; - LLVector4a diag; - diag.setSub(ext[1], ext[0]); - // World area of the face ~ product of the two largest AABB dims (max - // pairwise product; robust for flat faces). - F32 dx = diag[0], dy = diag[1], dz = diag[2]; - F32 area_world = llmax(dx * dy, llmax(dx * dz, dy * dz)); - // Pixels per meter at the nearest point. Distance floored: nearer than - // this the screen clamp below governs anyway. - F32 dist = llmax(face->mDistanceToCamera, 0.5f); - F32 ppm = LLDrawable::sCurPixelAngle / dist; - F32 face_px = area_world * ppm * ppm; - if (face_px <= 0.f) - { - // Degenerate extents: the face hasn't been through a geometry build - // yet (or a rigged face has no rigged extents) - it isn't renderable, - // so it must not be measured. Zero marks "skip": an invented - // placeholder value would become the texture's least-demanding "use" - // and, under TextureDownrezCoverageBias, drag the whole texture to - // its deepest mip (and it poisoned BP and PBR asymmetrically, since - // the two register faces at different points in the geometry - // lifecycle). - for (U32 ch = 0; ch < LLRender::NUM_TEXTURE_CHANNELS; ++ch) - { - face->mStreamVSize[ch] = 0.f; - } - return; - } - - S32 te_offset = face->getTEOffset(); // offset is -1 if not inited - const LLTextureEntry* te = (te_offset < 0 || te_offset >= objp->getNumTEs()) ? nullptr : objp->getTE(te_offset); - - // Shared, channel-independent chases - hoisted out of the channel loop. - const LLGLTFMaterial* gltf_mat = te ? te->getGLTFRenderMaterial() : nullptr; - const LLMaterial* mat = te ? te->getMaterialParams().get() : nullptr; - - // Continuously-animated scale (llSetTextureAnim SCALE) bypasses both - // static sources via mTextureMatrix - the live animated values win. - bool anim_scale = false; - F32 anim_ss = 0.f, anim_st = 0.f; - if (te) - { - if (LLVOVolume* vvo = face->getDrawable() ? face->getDrawable()->getVOVolume() : nullptr) - { - LLViewerTextureAnim* anim = vvo->mTextureAnimp; - if (anim && (anim->mMode & LLTextureAnim::ON) && (anim->mMode & LLTextureAnim::SCALE) - && (anim->mFace < 0 || anim->mFace == te_offset)) - { - anim_scale = true; - anim_ss = anim->mScaleS; - anim_st = anim->mScaleT; - } - } - } - - // Mesh atlas sub-rect: a face whose intrinsic UVs span only part of - // [0,1]^2 shows that fraction of the image. Applies identically to all - // channels - the per-channel transforms stack on the raw face UVs. - F32 span = 1.f; - if (te) - { - if (LLVolume* vol = objp->getVolume()) - { - if (te_offset >= 0 && te_offset < vol->getNumVolumeFaces()) - { - const LLVolumeFace& vf = vol->getVolumeFace(te_offset); - F32 s = fabsf((vf.mTexCoordExtents[1].mV[0] - vf.mTexCoordExtents[0].mV[0]) - * (vf.mTexCoordExtents[1].mV[1] - vf.mTexCoordExtents[0].mV[1])); - if (s > 0.f) - { - span = s; - } - } - } - } - - // Avatar bonus: worn attachments get a coverage multiplier - avatars are - // what people look at, and rigged extents make attachment coverage - // measurement unreliable anyway. Multiplicative, not a slam. - static LLCachedControl avatar_boost(gSavedSettings, "TextureAvatarBoost", 4.f); - const F32 boost = objp->isAttachment() ? llmax((F32)avatar_boost, 1.f) : 1.f; - - for (U32 ch = 0; ch < LLRender::NUM_TEXTURE_CHANNELS; ++ch) - { - // Effective UV repeat AREA: the tiling term of texels-drawn-per- - // screen-pixel. More tiling => each tile smaller on screen => coarser - // mips suffice (penalty). Repeats < 1 (atlas/crop) => whole-image - // residency for a sub-rect legitimately demands more than its screen - // coverage (boost). - F32 repeats = 1.f; - if (te) - { - // UV scale source: every channel reads the repeat values ITS - // renderer actually applies. diffuse -> TE scale; Blinn - // normal/spec -> LLMaterial per-map repeats; PBR channels -> KHR - // texture_transform scale. Fallback is the TE scale - never a - // silent hardcoded 1. - F32 scale_s = te->getScaleS(); - F32 scale_t = te->getScaleT(); - if (ch >= LLRender::BASECOLOR_MAP) - { - // LLRender channel -> LLGLTFMaterial::TextureInfo - static const S32 gltf_info[4] = { - LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR, // BASECOLOR_MAP (3) - LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, // METALLIC_ROUGHNESS_MAP (4) - LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL, // GLTF_NORMAL_MAP (5) - LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE, // EMISSIVE_MAP (6) - }; - if (gltf_mat) - { - const LLVector2& s = gltf_mat->mTextureTransform[gltf_info[ch - LLRender::BASECOLOR_MAP]].mScale; - scale_s = s.mV[0]; - scale_t = s.mV[1]; - } - } - else if (ch == LLRender::NORMAL_MAP || ch == LLRender::SPECULAR_MAP) - { - // Blinn-Phong normal/specular maps carry their own repeats in - // LLMaterial - the renderer builds their texture matrices - // from these, NOT from the TE's diffuse scale. - if (mat) - { - if (ch == LLRender::NORMAL_MAP) - { - mat->getNormalRepeat(scale_s, scale_t); - } - else - { - mat->getSpecularRepeat(scale_s, scale_t); - } - } - } - - if (anim_scale) - { - scale_s = anim_ss; - scale_t = anim_st; - } - - repeats = fabsf(scale_s * scale_t) * span; - } - - repeats = llclamp(repeats, MIN_REPEAT_AREA, MAX_REPEAT_AREA); - - // Apply the two sides of the repeat term in the right order relative - // to the screen clamp: tiling (repeats > 1) divides the nearest-point - // footprint BEFORE the clamp (one tile can't draw more pixels than - // the screen); atlas/crop (repeats < 1) boosts AFTER it (whole-image - // residency for a crop legitimately demands more than its screen - // coverage). - F32 tiling = llmax(repeats, 1.f); - F32 crop = llmin(repeats, 1.f); - face->mStreamVSize[ch] = llmin(face_px / tiling, LLViewerTexture::sWindowPixelArea) / crop * boost; - } -} - void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imagep, bool flush_images) { llassert(!gCubeSnapshot); @@ -1148,6 +931,13 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag if (imagep->getBoostLevel() < LLViewerFetchedTexture::BOOST_HIGH) // don't bother checking face list for boosted textures { + // Bounds on the per-face UV repeat-area divisor (mined from the old + // getTextureVirtualSize texel_area clamp [1/64, 128]): atlas/crop boost + // capped at 64x (3 mips finer), tiling penalty at 128x (3.5 mips + // coarser) so pathological UV scales can't explode either direction. + constexpr F32 MIN_REPEAT_AREA = 1.f / 64.f; + constexpr F32 MAX_REPEAT_AREA = 128.f; + // Per priority bucket (0=Normal, 1=BaseColor, 2=Specular, 3=Emissive): // the HIGHEST per-face effective coverage (= the lowest texels-per-pixel // use, the most demanding variant - drives desired discard) and the @@ -1160,9 +950,6 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag F32 channel_coverage_min[4] = { FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX }; bool bucket_used[4] = { false, false, false, false }; F32 max_coverage = 0.f; - bool on_screen = false; // any face's projected disc overlaps the screen - bool any_face = false; - F32 min_overflow = FLT_MAX; // least out-of-frustum use across faces U32 face_count = 0; @@ -1213,29 +1000,175 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag F32 radius; F32 cos_angle_to_view_dir; if ((gFrameCount - face->mLastTextureUpdate) > 10) - { // refresh the face's geometry + cached coverage at most once every - // 10 frames; every texture/channel sharing this face (GLTF and - // Blinn-Phong materials) reuses the cache instead of redoing the - // measurement. (calcPixelArea maintains face->mInFrustum itself.) - face->calcPixelArea(cos_angle_to_view_dir, radius); - update_face_stream_vsize(face); + { // only call calcPixelArea at most once every 10 frames for a given face + // this helps eliminate redundant calls to calcPixelArea for faces that have multiple textures + // assigned to them, such as is the case with GLTF materials or Blinn-Phong materials + face->mInFrustum = face->calcPixelArea(cos_angle_to_view_dir, radius); face->mLastTextureUpdate = gFrameCount; } - // Cached measurement - see update_face_stream_vsize above. - // Zero = degenerate extents / not yet through a geometry - // build: not renderable, must not be measured (a - // placeholder value would poison the per-bucket MIN bound - // and drag the texture to its deepest mip). - F32 vsize = face->mStreamVSize[i]; - if (vsize <= 0.f) + // Most-demanding-point measurement: the spec is that the + // LOWEST pixel:texel ratio governs, so pixel density is + // evaluated at the face's NEAREST point and applied to the + // face's true world area. The previous whole-face average + // (bounding-disc pixel area) under-resolved perspective + // surfaces: on a floor, the tile at your feet covers far + // more screen than the average tile, and the GPU samples + // fine mips right there - tiled (PBR-heavy) content went + // soft while untiled content looked fine. + const LLVector4a* ext = face->isState(LLFace::RIGGED) ? face->mRiggedExtents : face->mExtents; + LLVector4a diag; + diag.setSub(ext[1], ext[0]); + // World area of the face ~ product of the two largest AABB + // dims (max pairwise product; robust for flat faces). + F32 dx = diag[0], dy = diag[1], dz = diag[2]; + F32 area_world = llmax(dx * dy, llmax(dx * dz, dy * dz)); + // Pixels per meter at the nearest point. Distance floored: + // nearer than this the screen clamp below governs anyway. + F32 dist = llmax(face->mDistanceToCamera, 0.5f); + F32 ppm = LLDrawable::sCurPixelAngle / dist; + F32 face_px = area_world * ppm * ppm; + if (face_px <= 0.f) { + // Degenerate extents: the face hasn't been through a + // geometry build yet (or a rigged face has no rigged + // extents) - it isn't renderable, so it must not be + // measured. Skipping matters especially for the + // per-bucket MIN bound: any invented placeholder + // value (the old fallback hit LLFace::init's 16px + // default) becomes the texture's least-demanding + // "use" and, under TextureDownrezCoverageBias, drags + // the whole texture to its deepest mip - and it + // poisoned BP and PBR asymmetrically since the two + // systems register faces at different points in the + // geometry lifecycle. continue; } - any_face = true; - on_screen = on_screen || face->mInFrustum; - min_overflow = llmin(min_overflow, face->mFrustumOverflow); + // Effective UV repeat AREA across this face: the tiling + // term of texels-drawn-per-screen-pixel. More tiling => + // each tile is smaller on screen => coarser mips suffice + // (penalty). Repeats < 1 (atlas/crop) => only a sub-rect + // of the image is shown, but discard levels are whole- + // image, so the full image must be resident at 1/repeats + // times the crop's pixel count (boost). + S32 te_offset = face->getTEOffset(); // offset is -1 if not inited + LLViewerObject* objp = face->getViewerObject(); + const LLTextureEntry* te = (te_offset < 0 || te_offset >= objp->getNumTEs()) ? nullptr : objp->getTE(te_offset); + + F32 repeats = 1.f; + if (te) + { + // UV scale source: every channel reads the repeat + // values ITS renderer actually applies, then flows + // through the identical pipeline below. Sources: + // diffuse -> TE scale + // Blinn normal/spec -> LLMaterial per-map repeats + // PBR channels -> KHR texture_transform scale + // Fallback for any missing material is the TE scale - + // never a silent hardcoded 1. + F32 scale_s = te->getScaleS(); + F32 scale_t = te->getScaleT(); + if (i >= LLRender::BASECOLOR_MAP) + { + // LLRender channel -> LLGLTFMaterial::TextureInfo + static const S32 gltf_info[4] = { + LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR, // BASECOLOR_MAP (3) + LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, // METALLIC_ROUGHNESS_MAP (4) + LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL, // GLTF_NORMAL_MAP (5) + LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE, // EMISSIVE_MAP (6) + }; + if (const LLGLTFMaterial* gltf_mat = te->getGLTFRenderMaterial()) + { + const LLVector2& s = gltf_mat->mTextureTransform[gltf_info[i - LLRender::BASECOLOR_MAP]].mScale; + scale_s = s.mV[0]; + scale_t = s.mV[1]; + } + } + else if (i == LLRender::NORMAL_MAP || i == LLRender::SPECULAR_MAP) + { + // Blinn-Phong normal/specular maps carry their own + // repeats in LLMaterial - the renderer builds their + // texture matrices from these, NOT from the TE's + // diffuse scale. Reading the diffuse scale here made + // Blinn normals scale differently than PBR normals + // (whose per-channel transform IS read above). + if (const LLMaterial* mat = te->getMaterialParams().get()) + { + if (i == LLRender::NORMAL_MAP) + { + mat->getNormalRepeat(scale_s, scale_t); + } + else + { + mat->getSpecularRepeat(scale_s, scale_t); + } + } + } + + // Continuously-animated scale (llSetTextureAnim SCALE) + // bypasses both static sources via mTextureMatrix - + // the live animated values win on either path. + if (LLVOVolume* vvo = face->getDrawable() ? face->getDrawable()->getVOVolume() : nullptr) + { + LLViewerTextureAnim* anim = vvo->mTextureAnimp; + if (anim && (anim->mMode & LLTextureAnim::ON) && (anim->mMode & LLTextureAnim::SCALE) + && (anim->mFace < 0 || anim->mFace == te_offset)) + { + scale_s = anim->mScaleS; + scale_t = anim->mScaleT; + } + } + + repeats = fabsf(scale_s * scale_t); + + // Mesh atlas sub-rect: a face whose intrinsic UVs span + // only part of [0,1]^2 shows that fraction of the + // image. Applies identically to both paths - the + // transforms above stack on the raw face UVs. + if (LLVolume* vol = objp->getVolume()) + { + if (te_offset >= 0 && te_offset < vol->getNumVolumeFaces()) + { + const LLVolumeFace& vf = vol->getVolumeFace(te_offset); + F32 span = fabsf((vf.mTexCoordExtents[1].mV[0] - vf.mTexCoordExtents[0].mV[0]) + * (vf.mTexCoordExtents[1].mV[1] - vf.mTexCoordExtents[0].mV[1])); + if (span > 0.f) + { + repeats *= span; + } + } + } + } + + repeats = llclamp(repeats, MIN_REPEAT_AREA, MAX_REPEAT_AREA); + + // Apply the two sides of the repeat term in the right + // order relative to the screen clamp: + // - tiling (repeats > 1): the per-tile footprint at the + // nearest point, THEN clamped - one tile can't draw + // more pixels than the screen. (Clamping the whole + // face first and then dividing crushed near tiles.) + // - atlas/crop (repeats < 1): boost AFTER the clamp - + // whole-image residency for a crop legitimately + // demands more than its screen coverage. + F32 tiling = llmax(repeats, 1.f); + F32 crop = llmin(repeats, 1.f); + F32 vsize = llmin(face_px / tiling, LLViewerTexture::sWindowPixelArea) / crop; + + // Avatar bonus: worn attachments get a coverage + // multiplier - avatars are what people look at, and + // rigged extents make attachment coverage measurement + // unreliable anyway. Multiplicative, not a slam: a + // nearby avatar gains ~a mip of headroom while a distant + // one still downrezzes naturally with its coverage. + // (System-avatar bakes get the same bonus in the + // no-faces branch below.) + if (objp->isAttachment()) + { + static LLCachedControl avatar_boost(gSavedSettings, "TextureAvatarBoost", 4.f); + vsize *= llmax((F32)avatar_boost, 1.f); + } if (bucket >= 0 && bucket < 4) { @@ -1309,15 +1242,6 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag imagep->mChannelCoverage[b] = channel_coverage[b]; imagep->mChannelCoverageMin[b] = (channel_coverage_min[b] == FLT_MAX) ? 0.f : channel_coverage_min[b]; } - - // Fetch admission signal: false only when faces were actually scanned and - // every one projects off screen. Textures with no scannable faces (bakes, - // spotlights, the >1024-face boost path, not-yet-built geometry) stay - // eligible - blocking them is what stalls load-in. - imagep->mOnScreen = on_screen || !any_face; - // Least out-of-frustum use governs the allowance falloff; unknown = 0 - // (no penalty), same reasoning as mOnScreen. - imagep->mFrustumOverflow = any_face ? min_overflow : 0.f; } #if 0 @@ -1586,18 +1510,6 @@ F32 LLViewerTextureList::updateImagesFetchTextures(F32 max_time) { updateImageDecodePriority(imagep); imagep->updateFetch(); - - // Fast-pump membership: textures with an active fetch get - // updateFetch every frame (in updateImages) instead of waiting - // ~a sweep period per state transition - that wait, times the - // 2-4 transitions a load needs, was the measured throughput - // ceiling. Purely additive: the sweep still pumps everything, - // so fetches started by any other path can never strand. - if ((imagep->isFetching() || imagep->hasFetcher()) && !imagep->mInFastFetchList) - { - imagep->mInFastFetchList = true; - mFastFetchList.push_back(imagep); - } } if (timer.getElapsedTimeF32() > max_time) diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h index 7004238995..931f2ed50e 100644 --- a/indra/newview/llviewertexturelist.h +++ b/indra/newview/llviewertexturelist.h @@ -34,7 +34,6 @@ #include "llviewertexture.h" #include "llui.h" #include -#include #include #include "lluiimage.h" @@ -226,10 +225,6 @@ public: image_list_t mCallbackList; image_list_t mFastCacheList; - // In-flight fetches pumped every frame (additive to the round-robin - // sweep, which remains the universal pump). See updateImages. - std::vector > mFastFetchList; - bool mForceResetTextureStats; // to make "for (auto& imagep : gTextureList)" work -- cgit v1.3