From 140d5166e7b77060efdad6b0e011c444930966bc Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:01:07 +0300 Subject: Fix vertual memory overflow in logs --- indra/llcommon/llsys.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/indra/llcommon/llsys.cpp b/indra/llcommon/llsys.cpp index 803bab393c..dd46766cc2 100644 --- a/indra/llcommon/llsys.cpp +++ b/indra/llcommon/llsys.cpp @@ -956,15 +956,17 @@ LLSD LLMemoryInfo::loadStatsMap() state.dwLength = sizeof(state); GlobalMemoryStatusEx(&state); - DWORDLONG div = 1024; + static constexpr DWORDLONG div = 1024; stats.add("Percent Memory use", state.dwMemoryLoad/div); stats.add("Total Physical KB", state.ullTotalPhys/div); stats.add("Avail Physical KB", state.ullAvailPhys/div); stats.add("Total page KB", state.ullTotalPageFile/div); stats.add("Avail page KB", state.ullAvailPageFile/div); - stats.add("Total Virtual KB", state.ullTotalVirtual/div); - stats.add("Avail Virtual KB", state.ullAvailVirtual/div); + + static constexpr DWORDLONG mb_div = 1024 * 1024; + stats.add("Total Virtual MB", state.ullTotalVirtual/mb_div); // ~134 million MB + stats.add("Avail Virtual MB", state.ullAvailVirtual/mb_div); // SL-12122 - Call to GetPerformanceInfo() was removed here. Took // on order of 10 ms, causing unacceptable frame time spike every -- cgit v1.3 From acde733c6205f7355fcce436b8252c2e003b4a55 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:32:46 +0300 Subject: #4298 Record memory in bugsplat's attributes # Conflicts: # indra/newview/llappviewerwin32.cpp --- indra/llcommon/llmemory.cpp | 30 +++++++++------- indra/llcommon/llmemory.h | 8 +++++ indra/llcommon/llsys.cpp | 72 ++++++++++++++++++++++++++------------ indra/llcommon/llsys.h | 5 +-- indra/newview/llappviewerwin32.cpp | 7 ++++ 5 files changed, 84 insertions(+), 38 deletions(-) diff --git a/indra/llcommon/llmemory.cpp b/indra/llcommon/llmemory.cpp index ba48319a16..31424bfed3 100644 --- a/indra/llcommon/llmemory.cpp +++ b/indra/llcommon/llmemory.cpp @@ -52,6 +52,9 @@ //static +// On windows commit charge information is vital for OOM diagnosis. +U32Megabytes LLMemory::sAvailCommitMemInMB(U32_MAX); + // most important memory metric for texture streaming // On Windows, this should agree with resource monitor -> performance -> memory -> available // On OS X, this should be activity monitor -> memory -> (physical memory - memory used) @@ -104,21 +107,11 @@ void LLMemory::updateMemoryInfo() sMaxPhysicalMemInKB = gSysMemory.getPhysicalMemoryKB(); - U32Kilobytes avail_mem; - LLMemoryInfo::getAvailableMemoryKB(avail_mem); - sAvailPhysicalMemInKB = avail_mem; + LLMemoryInfo::updateAvailableMemory(); #if LL_WINDOWS - PROCESS_MEMORY_COUNTERS counters; - - if (!GetProcessMemoryInfo(GetCurrentProcess(), &counters, sizeof(counters))) - { - LL_WARNS() << "GetProcessMemoryInfo failed" << LL_ENDL; - return ; - } - - sAllocatedMemInKB = U32Kilobytes::convert(U64Bytes(counters.WorkingSetSize)); - sAllocatedPageSizeInKB = U32Kilobytes::convert(U64Bytes(counters.PagefileUsage)); + // On windows getAvailableMemoryKB fills sAvailPhysicalMemInKB, + //sAllocatedMemInKB and sAllocatedPageSizeInKB sample(sVirtualMem, sAllocatedPageSizeInKB); #elif defined(LL_DARWIN) @@ -201,6 +194,17 @@ void LLMemory::logMemoryInfo(bool update) LL_INFOS() << llformat("Current max usable memory: %.2f MB", sMaxPhysicalMemInKB / 1024.0) << LL_ENDL; } +#if LL_WINDOWS +//static +U32Megabytes LLMemory::getAvailableCommitMemMB() +{ + // Commit charge combines page file and ram, + // theoretical limit is 128TB on 64bit windows. + // Store as MB instead of KB to prevent overflow. + return sAvailCommitMemInMB; +} +#endif + //static U32Kilobytes LLMemory::getAvailableMemKB() { diff --git a/indra/llcommon/llmemory.h b/indra/llcommon/llmemory.h index adc556d180..290f6e03d1 100644 --- a/indra/llcommon/llmemory.h +++ b/indra/llcommon/llmemory.h @@ -429,10 +429,18 @@ public: static void updateMemoryInfo() ; static void logMemoryInfo(bool update = false); +#if LL_WINDOWS + // Commit charge is a Windows-only concept, combines page file and ram + static U32Megabytes getAvailableCommitMemMB(); +#endif static U32Kilobytes getAvailableMemKB() ; static U32Kilobytes getMaxMemKB() ; static U32Kilobytes getAllocatedMemKB() ; private: + // LLMemoryInfo directly updates memory stats + friend class LLMemoryInfo; + + static U32Megabytes sAvailCommitMemInMB; static U32Kilobytes sAvailPhysicalMemInKB ; static U32Kilobytes sMaxPhysicalMemInKB ; static U32Kilobytes sAllocatedMemInKB; diff --git a/indra/llcommon/llsys.cpp b/indra/llcommon/llsys.cpp index dd46766cc2..568a6b36dc 100644 --- a/indra/llcommon/llsys.cpp +++ b/indra/llcommon/llsys.cpp @@ -43,6 +43,7 @@ #include "llerrorcontrol.h" #include "llevents.h" #include "llformat.h" +#include "llmemory.h" #include "llregex.h" #include "lltimer.h" #include "llsdserialize.h" @@ -802,15 +803,13 @@ U32Kilobytes LLMemoryInfo::getPhysicalMemoryKB() const } //static -void LLMemoryInfo::getAvailableMemoryKB(U32Kilobytes& avail_mem_kb) +void LLMemoryInfo::updateAvailableMemory() { LL_PROFILE_ZONE_SCOPED_CATEGORY_MEMORY; #if LL_WINDOWS - // Sigh, this shouldn't be a static method, then we wouldn't have to - // reload this data separately from refresh() - LLSD statsMap(loadStatsMap()); - - avail_mem_kb = (U32Kilobytes)statsMap["Avail Physical KB"].asInteger(); + // On windows loadStatsMap will fill sAvailPhysicalMemInKB, + // sAvailCommitMemInMB, sAllocatedMemInKB and sAllocatedPageSizeInKB + loadStatsMap(); #elif LL_DARWIN // use host_statistics64 to get memory info @@ -822,11 +821,11 @@ void LLMemoryInfo::getAvailableMemoryKB(U32Kilobytes& avail_mem_kb) kern_return_t result = host_statistics64(host, HOST_VM_INFO64, reinterpret_cast(&vmstat), &count); if (result == KERN_SUCCESS) { - avail_mem_kb = U64Bytes((vmstat.free_count + vmstat.inactive_count) * page_size); + LLMemory::sAvailPhysicalMemInKB = U64Bytes((vmstat.free_count + vmstat.inactive_count) * page_size); } else { - avail_mem_kb = (U32Kilobytes)-1; + LLMemory::sAvailPhysicalMemInKB = (U32Kilobytes)-1; } #elif LL_LINUX @@ -880,12 +879,12 @@ void LLMemoryInfo::getAvailableMemoryKB(U32Kilobytes& avail_mem_kb) // (could also run 'free', but easier to read a file than run a program) LLSD statsMap(loadStatsMap()); - avail_mem_kb = (U32Kilobytes)statsMap["MemFree"].asInteger(); + LLMemory::sAvailPhysicalMemInKB = (U32Kilobytes)statsMap["MemFree"].asInteger(); #else //do not know how to collect available memory info for other systems. //leave it blank here for now. - avail_mem_kb = (U32Kilobytes)-1 ; + LLMemory::sAvailPhysicalMemInKB = (U32Kilobytes)-1 ; #endif } @@ -958,9 +957,13 @@ LLSD LLMemoryInfo::loadStatsMap() static constexpr DWORDLONG div = 1024; - stats.add("Percent Memory use", state.dwMemoryLoad/div); + stats.add("Percent Memory use", state.dwMemoryLoad); stats.add("Total Physical KB", state.ullTotalPhys/div); stats.add("Avail Physical KB", state.ullAvailPhys/div); + + // Despite the confusing naming "PageFile" , these values + // actually represent the committed memory limit for + // the system or the current process, whichever is smaller. stats.add("Total page KB", state.ullTotalPageFile/div); stats.add("Avail page KB", state.ullAvailPageFile/div); @@ -968,6 +971,9 @@ LLSD LLMemoryInfo::loadStatsMap() stats.add("Total Virtual MB", state.ullTotalVirtual/mb_div); // ~134 million MB stats.add("Avail Virtual MB", state.ullAvailVirtual/mb_div); + LLMemory::sAvailPhysicalMemInKB = U32Kilobytes::convert(U64Bytes(state.ullAvailPhys)); + LLMemory::sAvailCommitMemInMB = U32Megabytes::convert(U64Bytes(state.ullAvailPageFile)); + // SL-12122 - Call to GetPerformanceInfo() was removed here. Took // on order of 10 ms, causing unacceptable frame time spike every // second, and results were never used. If this is needed in the @@ -982,18 +988,38 @@ LLSD LLMemoryInfo::loadStatsMap() // specifically accepts PROCESS_MEMORY_COUNTERS*, and since this is a // classic-C API, PROCESS_MEMORY_COUNTERS_EX isn't a subclass. Cast the // pointer. - GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*) &pmem, sizeof(pmem)); - - stats.add("Page Fault Count", pmem.PageFaultCount); - stats.add("PeakWorkingSetSize KB", pmem.PeakWorkingSetSize/div); - stats.add("WorkingSetSize KB", pmem.WorkingSetSize/div); - stats.add("QutaPeakPagedPoolUsage KB", pmem.QuotaPeakPagedPoolUsage/div); - stats.add("QuotaPagedPoolUsage KB", pmem.QuotaPagedPoolUsage/div); - stats.add("QuotaPeakNonPagedPoolUsage KB", pmem.QuotaPeakNonPagedPoolUsage/div); - stats.add("QuotaNonPagedPoolUsage KB", pmem.QuotaNonPagedPoolUsage/div); - stats.add("PagefileUsage KB", pmem.PagefileUsage/div); - stats.add("PeakPagefileUsage KB", pmem.PeakPagefileUsage/div); - stats.add("PrivateUsage KB", pmem.PrivateUsage/div); + if (GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmem, sizeof(pmem))) + { + LLMemory::sAllocatedMemInKB = U32Kilobytes::convert(U64Bytes(pmem.WorkingSetSize)); + LLMemory::sAllocatedPageSizeInKB = U32Kilobytes::convert(U64Bytes(pmem.PagefileUsage)); + + stats.add("Page Fault Count", pmem.PageFaultCount); + stats.add("PeakWorkingSetSize KB", pmem.PeakWorkingSetSize / div); + stats.add("WorkingSetSize KB", pmem.WorkingSetSize / div); + stats.add("QuotaPeakPagedPoolUsage KB", pmem.QuotaPeakPagedPoolUsage / div); + stats.add("QuotaPagedPoolUsage KB", pmem.QuotaPagedPoolUsage / div); + stats.add("QuotaPeakNonPagedPoolUsage KB", pmem.QuotaPeakNonPagedPoolUsage / div); + stats.add("QuotaNonPagedPoolUsage KB", pmem.QuotaNonPagedPoolUsage / div); + stats.add("PagefileUsage KB", pmem.PagefileUsage / div); + stats.add("PeakPagefileUsage KB", pmem.PeakPagefileUsage / div); + stats.add("PrivateUsage KB", pmem.PrivateUsage / div); + } + else + { + LLMemory::sAllocatedMemInKB = U32Kilobytes(0); + LLMemory::sAllocatedPageSizeInKB = U32Kilobytes(0); + + stats.add("Page Fault Count", 0); + stats.add("PeakWorkingSetSize KB", 0); + stats.add("WorkingSetSize KB", 0); + stats.add("QuotaPeakPagedPoolUsage KB", 0); + stats.add("QuotaPagedPoolUsage KB", 0); + stats.add("QuotaPeakNonPagedPoolUsage KB", 0); + stats.add("QuotaNonPagedPoolUsage KB", 0); + stats.add("PagefileUsage KB", 0); + stats.add("PeakPagefileUsage KB", 0); + stats.add("PrivateUsage KB", 0); + } #elif LL_DARWIN diff --git a/indra/llcommon/llsys.h b/indra/llcommon/llsys.h index 827b0dc048..709fb29a82 100644 --- a/indra/llcommon/llsys.h +++ b/indra/llcommon/llsys.h @@ -134,11 +134,12 @@ public: static U32Kilobytes getHardwareMemSize(); // Because some Mac linkers won't let us reference extern gSysMemory from a different lib. #endif - //get the available memory in KiloBytes. - static void getAvailableMemoryKB(U32Kilobytes& avail_mem_kb); + // Updates LLMemory's values (which ones is OS specific). + 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. 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 2e4e9e29d5..5c2594e85c 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -181,6 +181,12 @@ 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()))); + if (gAgent.getRegion()) { // region location, when we have it @@ -193,6 +199,7 @@ namespace } LLAppViewer* app = LLAppViewer::instance(); + if (!app->isSecondInstance() && !app->errorMarkerExists()) { // If marker doesn't exist, create a marker with 'other' or 'logout' code for next launch -- 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(-) 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 95120bc676fc68793da2d4c06542cf7cb3272d00 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Fri, 26 Jun 2026 00:07:52 -0700 Subject: Update libwebrtc to m144 and fix audio device lifecycle/processing - Update libwebrtc to version m144 (autobuild.xml). - Use WebRTC's software APM exclusively; disable built-in (hardware/OS) AEC/AGC/NS, including after each device (re)deploy. - Only run the output device once a peer connection's audio is established (and bring devices up with the user's selected device at that point), fixing the buzz heard before/without an active connection. - Keep capture warm across mute/unmute to avoid the AEC cold-start hiss; stop recording 30s after a sustained mute so the OS mic indicator clears. - Reliably (re)select and (re)start capture/playout after teleport or voice restart so audio sends/records again. - Don't suspend the voice channel when entering tuning mode (Vivox-era behavior that dropped the peer connection). Co-Authored-By: Claude Opus 4.8 (1M context) --- autobuild.xml | 14 +- indra/llwebrtc/llwebrtc.cpp | 294 +++++++++++++++++++++++---- indra/llwebrtc/llwebrtc_impl.h | 52 ++--- indra/newview/llpanelvoicedevicesettings.cpp | 7 +- 4 files changed, 284 insertions(+), 83 deletions(-) diff --git a/autobuild.xml b/autobuild.xml index 1456dca104..5a08e4eeba 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -2607,11 +2607,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - 72ed1f6d469a8ffaffd69be39b7af186d7c3b1d7 + c70247d7683312ee81149dbae603574c0851e04c hash_algorithm sha1 url - https://github.com/secondlife/3p-webrtc-build/releases/download/m137.7151.04.22/webrtc-m137.7151.04.22.21966754211-darwin64-21966754211.tar.zst + https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.16/webrtc-m144.7559.06.16.28218655958-darwin64-28218655958.tar.zst name darwin64 @@ -2621,11 +2621,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - b4d0c836d99491841c3816ff93bb2655a2817bd3 + d187fd666eec8c14dbef959cdc9a6600a13736c7 hash_algorithm sha1 url - https://github.com/secondlife/3p-webrtc-build/releases/download/m137.7151.04.22/webrtc-m137.7151.04.22.21966754211-linux64-21966754211.tar.zst + https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.16/webrtc-m144.7559.06.16.28218655958-linux64-28218655958.tar.zst name linux64 @@ -2635,11 +2635,11 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors archive hash - ab2bddd77b1568b22b50ead13c1c33da94f4d59a + 47ecfec6deaa775c958fc532d7a43d186ba191f1 hash_algorithm sha1 url - https://github.com/secondlife/3p-webrtc-build/releases/download/m137.7151.04.22/webrtc-m137.7151.04.22.21966754211-windows64-21966754211.tar.zst + https://github.com/secondlife/3p-webrtc-build/releases/download/m144.7559.06.16/webrtc-m144.7559.06.16.28218655958-windows64-28218655958.tar.zst name windows64 @@ -2652,7 +2652,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors copyright Copyright (c) 2011, The WebRTC project authors. All rights reserved. version - m137.7151.04.22.21966754211 + m144.7559.06.16.28218655958 name webrtc vcs_branch diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index f4ecce63a6..ab455c9645 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -27,7 +27,7 @@ #include "llwebrtc_impl.h" #include #include - +#include "api/audio/create_audio_device_module.h" #include "api/audio_codecs/audio_decoder_factory.h" #include "api/audio_codecs/audio_encoder_factory.h" #include "api/audio_codecs/builtin_audio_decoder_factory.h" @@ -49,6 +49,12 @@ static int16_t PLAYOUT_DEVICE_DEFAULT = 0; static int16_t RECORD_DEVICE_DEFAULT = 0; #endif +// How long to keep the capture device running after a mute before stopping it. +// Keeping capture alive across brief mute/unmute cycles avoids cold-starting +// the AEC (heard as a short hiss on unmute); once the mute has been held this +// long we stop recording so the OS "mic in use" indicator clears. +static const int MUTE_STOP_RECORDING_DELAY_MS = 30000; + // // LLWebRTCAudioTransport implementation @@ -134,7 +140,9 @@ int32_t LLWebRTCAudioTransport::NeedMorePlayData(size_t number_of_frames, if (!engine) { // No engine sink; output silence to be safe. - const size_t bytes = number_of_frames * bytes_per_frame * number_of_channels; + // bytes_per_frame already accounts for all channels, so do not multiply + // by number_of_channels again (that would overrun the playout buffer). + const size_t bytes = number_of_frames * bytes_per_frame; memset(audio_data, 0, bytes); number_of_samples_out = bytes_per_frame; return 0; @@ -250,17 +258,51 @@ void LLCustomProcessor::Process(webrtc::AudioBuffer *audio) mState->setMicrophoneEnergy(std::sqrt(totalSum / (audio->num_channels() * audio->num_frames() * buffer_size))); } + +// +// LLWebRTCImpl implementation +// + +void LLWebRTCAudioDeviceModule::SetTuning(bool tuning, bool mute) +{ + tuning_ = tuning; + if (tuning) + { + int32_t hr = inner_->InitMicrophone(); + hr = inner_->InitRecording(); + hr = inner_->StartRecording(); + hr = inner_->StopPlayout(); + } + else + { + if (mute) + { + inner_->StopRecording(); + } + else + { + inner_->InitRecording(); + inner_->StartRecording(); + } + inner_->StartPlayout(); + } +} + // // LLWebRTCImpl implementation // LLWebRTCImpl::LLWebRTCImpl(LLWebRTCLogCallback* logCallback) : + mEnv(webrtc::CreateEnvironment(webrtc::CreateDefaultTaskQueueFactory())), mLogSink(new LLWebRTCLogSink(logCallback)), mPeerCustomProcessor(nullptr), mMute(true), mTuningMode(false), mDevicesDeploying(0), - mGain(0.0f) + mGain(0.0f), + mBuiltinNS(false), + mBuiltinAGC(false), + mBuiltinAEC(false) { } @@ -273,8 +315,6 @@ void LLWebRTCImpl::init() webrtc::LogMessage::SetLogToStderr(true); webrtc::LogMessage::AddLogToStream(mLogSink, webrtc::LS_VERBOSE); - mTaskQueueFactory = webrtc::CreateDefaultTaskQueueFactory(); - // Create the native threads. mNetworkThread = webrtc::Thread::CreateWithSocketServer(); mNetworkThread->SetName("WebRTCNetworkThread", nullptr); @@ -290,9 +330,17 @@ void LLWebRTCImpl::init() [this]() { webrtc::scoped_refptr realADM = - webrtc::AudioDeviceModule::Create(webrtc::AudioDeviceModule::AudioLayer::kPlatformDefaultAudio, mTaskQueueFactory.get()); + webrtc::CreateAudioDeviceModule(mEnv, webrtc::AudioDeviceModule::AudioLayer::kPlatformDefaultAudio); mDeviceModule = webrtc::make_ref_counted(realADM); mDeviceModule->SetObserver(this); + mDeviceModule->Init(); + + mBuiltinNS = mDeviceModule->BuiltInNSIsAvailable(); + mBuiltinAEC = mDeviceModule->BuiltInAECIsAvailable(); + mBuiltinAGC = mDeviceModule->BuiltInAGCIsAvailable(); + // All audio processing is done by WebRTC's software APM (configured + // below); make sure the hardware processors stay off. + workerDisableBuiltInAudioProcessing(); }); // The custom processor allows us to retrieve audio data (and levels) @@ -302,17 +350,22 @@ void LLWebRTCImpl::init() apb.SetCapturePostProcessing(std::make_unique(mPeerCustomProcessor)); mAudioProcessingModule = apb.Build(webrtc::CreateEnvironment()); + // Initial software-APM state, matching setAudioConfig() so there's no + // window where processing differs before the viewer's first config call. + // All processing is done here in software (the hardware AEC/AGC/NS is kept + // disabled), so enable echo cancellation from the very first frame. webrtc::AudioProcessing::Config apm_config; - apm_config.echo_canceller.enabled = false; - apm_config.echo_canceller.mobile_mode = false; - apm_config.gain_controller1.enabled = false; - apm_config.gain_controller2.enabled = true; - apm_config.high_pass_filter.enabled = true; - apm_config.noise_suppression.enabled = true; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kVeryHigh; - apm_config.transient_suppression.enabled = true; - apm_config.pipeline.multi_channel_render = true; - apm_config.pipeline.multi_channel_capture = false; + apm_config.echo_canceller.enabled = true; + apm_config.echo_canceller.mobile_mode = false; + apm_config.gain_controller1.enabled = false; + apm_config.gain_controller2.enabled = true; + apm_config.gain_controller2.adaptive_digital.enabled = true; // auto-level speech + apm_config.high_pass_filter.enabled = true; + apm_config.noise_suppression.enabled = true; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kVeryHigh; + apm_config.transient_suppression.enabled = true; + apm_config.pipeline.multi_channel_render = true; + apm_config.pipeline.multi_channel_capture = true; mAudioProcessingModule->ApplyConfig(apm_config); @@ -344,7 +397,6 @@ void LLWebRTCImpl::init() { if (mDeviceModule) { - mDeviceModule->EnableBuiltInAEC(false); updateDevices(); } }); @@ -382,7 +434,6 @@ void LLWebRTCImpl::terminate() mDeviceModule->Terminate(); } mDeviceModule = nullptr; - mTaskQueueFactory = nullptr; }); // In case peer connections still somehow have jobs in workers, @@ -395,47 +446,79 @@ void LLWebRTCImpl::terminate() webrtc::LogMessage::RemoveLogToStream(mLogSink); } + void LLWebRTCImpl::setAudioConfig(LLWebRTCDeviceInterface::AudioConfig config) { + // All audio processing is handled by WebRTC's software APM here. The + // platform/hardware AEC/AGC/NS is always disabled (see + // workerDisableBuiltInAudioProcessing), so these are enabled purely on the + // requested config without deferring to any built-in processor. webrtc::AudioProcessing::Config apm_config; - apm_config.echo_canceller.enabled = config.mEchoCancellation; - apm_config.echo_canceller.mobile_mode = false; - apm_config.gain_controller1.enabled = false; - apm_config.gain_controller2.enabled = config.mAGC; + apm_config.echo_canceller.enabled = config.mEchoCancellation; + apm_config.echo_canceller.mobile_mode = false; + apm_config.gain_controller1.enabled = false; + apm_config.gain_controller2.enabled = config.mAGC; apm_config.gain_controller2.adaptive_digital.enabled = true; // auto-level speech - apm_config.high_pass_filter.enabled = true; - apm_config.transient_suppression.enabled = true; - apm_config.pipeline.multi_channel_render = true; - apm_config.pipeline.multi_channel_capture = true; - apm_config.pipeline.multi_channel_capture = true; + apm_config.high_pass_filter.enabled = true; + apm_config.transient_suppression.enabled = true; + apm_config.pipeline.multi_channel_render = true; + apm_config.pipeline.multi_channel_capture = true; switch (config.mNoiseSuppressionLevel) { case LLWebRTCDeviceInterface::AudioConfig::NOISE_SUPPRESSION_LEVEL_NONE: apm_config.noise_suppression.enabled = false; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; break; case LLWebRTCDeviceInterface::AudioConfig::NOISE_SUPPRESSION_LEVEL_LOW: apm_config.noise_suppression.enabled = true; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; break; case LLWebRTCDeviceInterface::AudioConfig::NOISE_SUPPRESSION_LEVEL_MODERATE: apm_config.noise_suppression.enabled = true; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kModerate; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kModerate; break; case LLWebRTCDeviceInterface::AudioConfig::NOISE_SUPPRESSION_LEVEL_HIGH: apm_config.noise_suppression.enabled = true; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kHigh; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kHigh; break; case LLWebRTCDeviceInterface::AudioConfig::NOISE_SUPPRESSION_LEVEL_VERY_HIGH: apm_config.noise_suppression.enabled = true; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kVeryHigh; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kVeryHigh; break; default: apm_config.noise_suppression.enabled = false; - apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; + apm_config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kLow; } mAudioProcessingModule->ApplyConfig(apm_config); + + // Keep the hardware processors off; the APM above is the only processing. + PostWorkerTask([this]() { workerDisableBuiltInAudioProcessing(); }); +} + +void LLWebRTCImpl::workerDisableBuiltInAudioProcessing() +{ + if (!mDeviceModule) + { + return; + } + + // We always use WebRTC's internal (software APM) audio processing. Running + // the platform/hardware AEC, AGC, or NS alongside it causes the two to + // fight -- pumping levels, double noise suppression, and mismatched AEC + // references -- so disable any that the device exposes. + if (mBuiltinNS) + { + mDeviceModule->EnableBuiltInNS(false); + } + if (mBuiltinAGC) + { + mDeviceModule->EnableBuiltInAGC(false); + } + if (mBuiltinAEC) + { + mDeviceModule->EnableBuiltInAEC(false); + } } void LLWebRTCImpl::refreshDevices() @@ -455,8 +538,11 @@ void LLWebRTCImpl::unsetDevicesObserver(LLWebRTCDevicesObserver *observer) } } -// must be run in the worker thread. -void LLWebRTCImpl::workerDeployDevices() +// must be run in the worker thread. Selects the user's chosen capture/playout +// devices and (re)initializes and starts them. Does NOT touch per-connection +// tracks -- callers that also need mute/track state re-applied use +// workerDeployDevices(). +void LLWebRTCImpl::workerStartDevices() { if (!mDeviceModule) { @@ -500,8 +586,20 @@ void LLWebRTCImpl::workerDeployDevices() #endif mDeviceModule->InitMicrophone(); mDeviceModule->SetStereoRecording(false); + mBuiltinNS = mDeviceModule->BuiltInNSIsAvailable(); + mBuiltinAEC = mDeviceModule->BuiltInAECIsAvailable(); + mBuiltinAGC = mDeviceModule->BuiltInAGCIsAvailable(); + // A newly-selected capture device may default its hardware AEC/AGC/NS on; + // disable before InitRecording so the recording stream is configured to + // use only WebRTC's software APM. + workerDisableBuiltInAudioProcessing(); mDeviceModule->InitRecording(); + if ((!mMute && mPeerConnections.size()) || mTuningMode) + { + mDeviceModule->ForceStartRecording(); + } + int16_t playoutDevice = PLAYOUT_DEVICE_DEFAULT; int16_t playout_device_start = 0; if (mPlayoutDevice != "Default") @@ -538,15 +636,30 @@ void LLWebRTCImpl::workerDeployDevices() mDeviceModule->SetStereoPlayout(true); mDeviceModule->InitPlayout(); - if ((!mMute && mPeerConnections.size()) || mTuningMode) + // Only run playout when there's actually something to render. Starting + // playout with no peer connection leaves the output device spinning with + // no engine data, which is heard as a buzz until a connection is made. + // (Recording is gated on the same condition above.) + if (!mTuningMode && !mPeerConnections.empty()) { - mDeviceModule->ForceStartRecording(); + mDeviceModule->StartPlayout(); } +} - if (!mTuningMode) +// must be run in the worker thread. Selects/starts the devices (via +// workerStartDevices) and then re-applies per-connection mute/track state. +// Use this for device changes and tuning; for simply bringing devices up when +// a connection is established (without disturbing the connection's own +// mute/track management) call workerStartDevices() directly. +void LLWebRTCImpl::workerDeployDevices() +{ + if (!mDeviceModule) { - mDeviceModule->StartPlayout(); + return; } + + workerStartDevices(); + mSignalingThread->PostTask( [this] { @@ -740,6 +853,12 @@ void LLWebRTCImpl::intSetMute(bool mute, int delay_ms) if (mMute) { + // Keep capturing for a while after muting so quick mute/unmute cycles + // don't cold-start the AEC (and any OS capture effect such as Windows + // Voice Clarity), which is heard as a short hiss on unmute. Once the + // mute has been held this long, stop recording so the OS "mic in use" + // indicator clears. If the user unmutes or toggles before this fires, + // the sequence check turns it into a no-op and capture keeps running. mWorkerThread->PostDelayedTask( [this, current_sequence] { @@ -748,7 +867,7 @@ void LLWebRTCImpl::intSetMute(bool mute, int delay_ms) mDeviceModule->ForceStopRecording(); } }, - webrtc::TimeDelta::Millis(delay_ms)); + webrtc::TimeDelta::Millis(MUTE_STOP_RECORDING_DELAY_MS)); } else { @@ -757,6 +876,9 @@ void LLWebRTCImpl::intSetMute(bool mute, int delay_ms) { if (mDeviceModule && (current_sequence == mute_sequence.load())) { + // No-op if capture is still running (the common case, when + // unmuting within the stop delay -> no AEC cold start); + // restarts capture if a sustained mute had stopped it. mDeviceModule->InitRecording(); mDeviceModule->ForceStartRecording(); } @@ -770,8 +892,7 @@ void LLWebRTCImpl::intSetMute(bool mute, int delay_ms) LLWebRTCPeerConnectionInterface *LLWebRTCImpl::newPeerConnection() { - bool empty = mPeerConnections.empty(); - webrtc::scoped_refptr peerConnection = webrtc::scoped_refptr(new webrtc::RefCountedObject()); + webrtc::scoped_refptr peerConnection = webrtc::scoped_refptr(new webrtc::RefCountedObject(mEnv)); peerConnection->init(this); if (mPeerConnections.empty()) { @@ -779,6 +900,13 @@ LLWebRTCPeerConnectionInterface *LLWebRTCImpl::newPeerConnection() } mPeerConnections.emplace_back(peerConnection); + // The capture/playout devices are intentionally NOT started here. This + // runs when the connection is created/connecting; starting the output + // device now leaves it spinning with no decoded audio during the handshake, + // which is heard as a buzz. The devices are (re)started from + // OnConnectionChange(kConnected) instead, once audio is actually + // established (see startAudioDevices()). + peerConnection->enableSenderTracks(false); peerConnection->resetMute(); return peerConnection.get(); @@ -795,10 +923,82 @@ void LLWebRTCImpl::freePeerConnection(LLWebRTCPeerConnectionInterface* peer_conn if (mPeerConnections.empty()) { intSetMute(true); + // Last connection gone: stop capture immediately rather than + // waiting out the mute stop-delay, so the mic isn't held open after + // the call, and stop playout so the output device isn't left + // spinning with no engine data. + mWorkerThread->PostTask( + [this]() + { + if (mDeviceModule) + { + mDeviceModule->ForceStopRecording(); + mDeviceModule->StopPlayout(); + } + }); } } } +void LLWebRTCImpl::startAudioDevices() +{ + // Called when a connection's audio is established. This is the + // authoritative point that brings the devices back (with the user's + // selected devices applied) after all connections dropped (teleport, voice + // restart) or for the first call of a session. It matters because the + // WebRTC engine no-ops Start/StopRecording on our ADM wrapper -- only our + // explicit Force* calls actually drive capture -- so when the devices were + // stopped, nothing else will restart them. + // + // It's guarded on Playing()/Recording() so a second connection establishing + // won't glitch an already-running stream, and doing this at "connected" + // rather than at connection creation avoids running the output device with + // no decoded audio during the handshake. + mWorkerThread->PostTask( + [this]() + { + if (!mDeviceModule || mTuningMode) + { + return; + } + + if (!mDeviceModule->Playing()) + { + // First established connection for this call: select and start + // the user's *chosen* capture/playout devices + // (SetRecordingDevice/SetPlayoutDevice). Just calling + // InitPlayout/InitRecording here would bring the devices up on + // whatever the ADM currently has selected -- the system default + // after a cold start -- which is why a p2p call (or a call after + // teleport/voice-restart) could come up on the wrong device. + // + // We call workerStartDevices() rather than the full + // deployDevices() on purpose: deployDevices() also re-applies + // per-connection mute/track state, which races with the + // viewer's own mute setup for the freshly-establishing + // connection and can leave the sender track disabled (recording + // runs but nothing transmits after teleport). + workerStartDevices(); + } + + // Authoritatively (re)start capture whenever we're connected and not + // device-muted. This runs unconditionally -- NOT just in an else + // branch -- because workerStartDevices() above stops recording while + // re-selecting the device and only restarts it behind a gate; if + // that gate doesn't line up (or capture was stopped on a prior + // disconnect, e.g. teleport), this is what reliably brings the mic + // back. No-op if capture is already running. + if (!mMute && !mPeerConnections.empty() && !mDeviceModule->Recording()) + { + if (!mDeviceModule->RecordingIsInitialized()) + { + mDeviceModule->InitRecording(); + } + mDeviceModule->ForceStartRecording(); + } + }); +} + // // LLWebRTCPeerConnectionImpl implementation. @@ -806,7 +1006,8 @@ void LLWebRTCImpl::freePeerConnection(LLWebRTCPeerConnectionInterface* peer_conn // Most peer connection (signaling) happens on // the signaling thread. -LLWebRTCPeerConnectionImpl::LLWebRTCPeerConnectionImpl() : +LLWebRTCPeerConnectionImpl::LLWebRTCPeerConnectionImpl(const webrtc::Environment& env) : + mEnv(env), mWebRTCImpl(nullptr), mPeerConnection(nullptr), mMute(MUTE_INITIAL), @@ -1255,6 +1456,12 @@ void LLWebRTCPeerConnectionImpl::OnConnectionChange(webrtc::PeerConnectionInterf { case webrtc::PeerConnectionInterface::PeerConnectionState::kConnected: { + // Audio is established now -- (re)start the capture and playout + // devices. Doing this here rather than at connection creation + // avoids running the output device during the handshake (heard as a + // buzz), and reliably restores the devices after a full teardown + // (teleport / voice restart). + mWebRTCImpl->startAudioDevices(); mPendingJobs++; webrtc::scoped_refptr self(this); mWebRTCImpl->PostWorkerTask([self]() @@ -1267,6 +1474,7 @@ void LLWebRTCPeerConnectionImpl::OnConnectionChange(webrtc::PeerConnectionInterf }); break; } + case webrtc::PeerConnectionInterface::PeerConnectionState::kFailed: { for (auto &observer : mSignalingObserverList) diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index bd7a2e0bcf..cfb0d10c29 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -323,30 +323,8 @@ public: // tuning microphone energy calculations float GetMicrophoneEnergy() { return audio_transport_.GetMicrophoneEnergy(); } void SetTuningMicGain(float gain) { audio_transport_.SetGain(gain); } - void SetTuning(bool tuning, bool mute) - { - tuning_ = tuning; - if (tuning) - { - inner_->InitRecording(); - inner_->StartRecording(); - inner_->StopPlayout(); - } - else - { - if (mute) - { - inner_->StopRecording(); - } - else - { - inner_->InitRecording(); - inner_->StartRecording(); - } - inner_->InitPlayout(); - inner_->StartPlayout(); - } - } + + void SetTuning(bool tuning, bool mute); protected: ~LLWebRTCAudioDeviceModule() override = default; @@ -436,7 +414,6 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO // void setAudioConfig(LLWebRTCDeviceInterface::AudioConfig config = LLWebRTCDeviceInterface::AudioConfig()) override; - void refreshDevices() override; void setDevicesObserver(LLWebRTCDevicesObserver *observer) override; @@ -522,9 +499,22 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO LLWebRTCPeerConnectionInterface* newPeerConnection(); void freePeerConnection(LLWebRTCPeerConnectionInterface* peer_connection); + // (Re)start the capture and playout devices once a connection's audio is + // established. This is the authoritative point for bringing the devices + // back after all connections dropped (teleport, voice restart). Idempotent + // and safe to call from any thread (work is posted to the worker thread). + void startAudioDevices(); + protected: + const webrtc::Environment mEnv; + void workerStartDevices(); void workerDeployDevices(); + // We always rely on WebRTC's internal (software APM) audio processing, so + // any platform/hardware AEC/AGC/NS must be kept disabled. + void workerDisableBuiltInAudioProcessing(); + + LLWebRTCLogSink* mLogSink; // The native webrtc threads @@ -537,10 +527,6 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO webrtc::scoped_refptr mAudioProcessingModule; - // more native webrtc stuff - std::unique_ptr mTaskQueueFactory; - - // Devices void updateDevices(); void deployDevices(); @@ -548,6 +534,10 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO webrtc::scoped_refptr mDeviceModule; std::vector mVoiceDevicesObserverList; + bool mBuiltinNS; + bool mBuiltinAGC; + bool mBuiltinAEC; + // accessors in native webrtc for devices aren't apparently implemented yet. bool mTuningMode; std::string mRecordingDevice; @@ -580,7 +570,7 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, { public: - LLWebRTCPeerConnectionImpl(); + LLWebRTCPeerConnectionImpl(const webrtc::Environment& env); ~LLWebRTCPeerConnectionImpl(); void init(LLWebRTCImpl * webrtc_impl); @@ -659,7 +649,7 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, void gatherConnectionStats() override; protected: - + const webrtc::Environment mEnv; LLWebRTCImpl * mWebRTCImpl; webrtc::scoped_refptr mPeerConnectionFactory; diff --git a/indra/newview/llpanelvoicedevicesettings.cpp b/indra/newview/llpanelvoicedevicesettings.cpp index d8d6bcf5fd..5aaa53b732 100644 --- a/indra/newview/llpanelvoicedevicesettings.cpp +++ b/indra/newview/llpanelvoicedevicesettings.cpp @@ -338,8 +338,12 @@ void LLPanelVoiceDeviceSettings::initialize() // put voice client in "tuning" mode if (mUseTuningMode) { + // WebRTC tuning only affects the local audio device (mic-level + // monitoring and device selection); the peer connection stays up and + // its send/receive tracks are disabled for the duration. Unlike Vivox, + // there's no need to suspend (and tear down) the voice channel, which + // previously dropped the call and failed to reconnect on resume. LLVoiceClient::getInstance()->tuningStart(); - LLVoiceChannel::suspend(); } } @@ -348,7 +352,6 @@ void LLPanelVoiceDeviceSettings::cleanup() if (mUseTuningMode) { LLVoiceClient::getInstance()->tuningStop(); - LLVoiceChannel::resume(); } } -- cgit v1.3 From 4124f969ee7bbd4e0734ef0d118c60125d7bb358 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Tue, 30 Jun 2026 11:48:27 -0700 Subject: Keep capture device running for the whole voice session Mute now zeroes captured gain and disables the sender tracks instead of stopping the capture device, so unmuting no longer cold-starts the AEC (no hiss) and Bluetooth devices no longer drop/restart as they switch between mono and stereo. Capture is gated on voice being enabled rather than on mute: it starts when voice is enabled and runs across calls and mute/unmute, and is released when voice is disabled (setVoiceEnabled). Playout stays gated on there being a connection to render. As a result the OS "mic in use" indicator is on for the length of the session and only clears when voice is disabled. Co-Authored-By: Claude Opus 4.8 (1M context) --- indra/llwebrtc/llwebrtc.cpp | 292 ++++++++++++++++++---------------------- indra/llwebrtc/llwebrtc.h | 8 ++ indra/llwebrtc/llwebrtc_impl.h | 27 ++-- indra/newview/llvoicewebrtc.cpp | 8 ++ 4 files changed, 163 insertions(+), 172 deletions(-) diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index ab455c9645..6c809f2743 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -49,12 +49,6 @@ static int16_t PLAYOUT_DEVICE_DEFAULT = 0; static int16_t RECORD_DEVICE_DEFAULT = 0; #endif -// How long to keep the capture device running after a mute before stopping it. -// Keeping capture alive across brief mute/unmute cycles avoids cold-starting -// the AEC (heard as a short hiss on unmute); once the mute has been held this -// long we stop recording so the OS "mic in use" indicator clears. -static const int MUTE_STOP_RECORDING_DELAY_MS = 30000; - // // LLWebRTCAudioTransport implementation @@ -268,24 +262,19 @@ void LLWebRTCAudioDeviceModule::SetTuning(bool tuning, bool mute) tuning_ = tuning; if (tuning) { - int32_t hr = inner_->InitMicrophone(); - hr = inner_->InitRecording(); - hr = inner_->StartRecording(); - hr = inner_->StopPlayout(); - } - else - { - if (mute) - { - inner_->StopRecording(); - } - else - { - inner_->InitRecording(); - inner_->StartRecording(); - } - inner_->StartPlayout(); + // Ensure capture is running (it's normally already running -- capture is + // session-long) so the mic-level meter works, and stop rendering the + // call while tuning. The recording calls are no-ops if capture is + // already active, so this won't cold-start it. + inner_->InitMicrophone(); + inner_->InitRecording(); + inner_->StartRecording(); + inner_->StopPlayout(); } + // On exit, capture is deliberately left running (mute is handled by gain, + // not by stopping the device, so there's no AEC cold-start hiss). Playout + // is restored by the caller via workerOpenPlayout(), keeping it gated on + // there being a connection to render. } // @@ -297,6 +286,7 @@ LLWebRTCImpl::LLWebRTCImpl(LLWebRTCLogCallback* logCallback) : mLogSink(new LLWebRTCLogSink(logCallback)), mPeerCustomProcessor(nullptr), mMute(true), + mVoiceEnabled(false), mTuningMode(false), mDevicesDeploying(0), mGain(0.0f), @@ -431,7 +421,7 @@ void LLWebRTCImpl::terminate() { if (mDeviceModule) { - mDeviceModule->Terminate(); + mDeviceModule->ForceTerminate(); } mDeviceModule = nullptr; }); @@ -538,23 +528,25 @@ void LLWebRTCImpl::unsetDevicesObserver(LLWebRTCDevicesObserver *observer) } } -// must be run in the worker thread. Selects the user's chosen capture/playout -// devices and (re)initializes and starts them. Does NOT touch per-connection -// tracks -- callers that also need mute/track state re-applied use -// workerDeployDevices(). -void LLWebRTCImpl::workerStartDevices() +// must be run in the worker thread. Selects the configured capture device and +// starts recording. Capture runs the whole time voice is enabled (it's never +// stopped for mute or between calls, so the AEC never cold-starts -- there's no +// hiss on unmute), so this is a no-op when already recording. Device changes +// go through workerDeployDevices(), which stops recording first to force a +// clean re-select; voice off goes through setVoiceEnabled(false). +void LLWebRTCImpl::workerStartRecording() { - if (!mDeviceModule) + // Only run capture while voice is enabled, and never cold-start it when + // it's already running (that would cause the unmute hiss). + if (!mDeviceModule || !mVoiceEnabled || mDeviceModule->Recording()) { return; } int16_t recordingDevice = RECORD_DEVICE_DEFAULT; - int16_t recording_device_start = 0; - if (mRecordingDevice != "Default") { - for (int16_t i = recording_device_start; i < mRecordingDeviceList.size(); i++) + for (int16_t i = 0; i < mRecordingDeviceList.size(); i++) { if (mRecordingDeviceList[i].mID == mRecordingDevice) { @@ -570,8 +562,6 @@ void LLWebRTCImpl::workerStartDevices() } } - mDeviceModule->StopPlayout(); - mDeviceModule->ForceStopRecording(); #if WEBRTC_WIN if (recordingDevice < 0) { @@ -586,25 +576,32 @@ void LLWebRTCImpl::workerStartDevices() #endif mDeviceModule->InitMicrophone(); mDeviceModule->SetStereoRecording(false); - mBuiltinNS = mDeviceModule->BuiltInNSIsAvailable(); - mBuiltinAEC = mDeviceModule->BuiltInAECIsAvailable(); - mBuiltinAGC = mDeviceModule->BuiltInAGCIsAvailable(); // A newly-selected capture device may default its hardware AEC/AGC/NS on; // disable before InitRecording so the recording stream is configured to // use only WebRTC's software APM. workerDisableBuiltInAudioProcessing(); mDeviceModule->InitRecording(); + mDeviceModule->ForceStartRecording(); +} - if ((!mMute && mPeerConnections.size()) || mTuningMode) +// must be run in the worker thread. Selects the configured playout device and +// starts playout. Playout only runs while there's a connection to render +// (running the output device with no engine data is heard as a buzz), so this +// is a no-op when there are no connections or when already playing. Device +// changes go through workerDeployDevices(), which stops playout first. +void LLWebRTCImpl::workerStartPlayout() +{ + // Only run playout while voice is enabled and there's a connection to + // render (running the output device otherwise is heard as a buzz). + if (!mDeviceModule || !mVoiceEnabled || mTuningMode || mDeviceModule->Playing() || mPeerConnections.empty()) { - mDeviceModule->ForceStartRecording(); + return; } int16_t playoutDevice = PLAYOUT_DEVICE_DEFAULT; - int16_t playout_device_start = 0; if (mPlayoutDevice != "Default") { - for (int16_t i = playout_device_start; i < mPlayoutDeviceList.size(); i++) + for (int16_t i = 0; i < mPlayoutDeviceList.size(); i++) { if (mPlayoutDeviceList[i].mID == mPlayoutDevice) { @@ -635,22 +632,14 @@ void LLWebRTCImpl::workerStartDevices() mDeviceModule->InitSpeaker(); mDeviceModule->SetStereoPlayout(true); mDeviceModule->InitPlayout(); - - // Only run playout when there's actually something to render. Starting - // playout with no peer connection leaves the output device spinning with - // no engine data, which is heard as a buzz until a connection is made. - // (Recording is gated on the same condition above.) - if (!mTuningMode && !mPeerConnections.empty()) - { - mDeviceModule->StartPlayout(); - } + mDeviceModule->StartPlayout(); } -// must be run in the worker thread. Selects/starts the devices (via -// workerStartDevices) and then re-applies per-connection mute/track state. -// Use this for device changes and tuning; for simply bringing devices up when -// a connection is established (without disturbing the connection's own -// mute/track management) call workerStartDevices() directly. +// must be run in the worker thread. Used for device changes and tuning: forces +// a clean re-select of both devices, then re-applies per-connection mute/track +// state. To merely bring playout up when a connection is established (without +// disturbing the connection's own mute/track management) call +// workerOpenPlayout() directly -- see startPlayout(). void LLWebRTCImpl::workerDeployDevices() { if (!mDeviceModule) @@ -658,7 +647,13 @@ void LLWebRTCImpl::workerDeployDevices() return; } - workerStartDevices(); + // Stop first so the start helpers (which no-op when already running) will + // re-select the now-current device. + mDeviceModule->StopPlayout(); + mDeviceModule->ForceStopRecording(); + + workerStartRecording(); + workerStartPlayout(); mSignalingThread->PostTask( [this] @@ -701,6 +696,35 @@ void LLWebRTCImpl::setRenderDevice(const std::string &id) } } +void LLWebRTCImpl::setVoiceEnabled(bool enable) +{ + mVoiceEnabled = enable; + mWorkerThread->PostTask( + [this, enable]() + { + if (!mDeviceModule) + { + return; + } + if (enable) + { + // Voice on: start the capture device (it then stays running + // across calls and mute/unmute), and start playout if there's + // already a connection to render. + mDeviceModule->Init(); + workerDeployDevices(); + } + else + { + // Voice off: release both devices so the OS mic/speaker aren't + // held open. + mDeviceModule->ForceStopRecording(); + mDeviceModule->StopPlayout(); + mDeviceModule->ForceTerminate(); + } + }); +} + // updateDevices needs to happen on the worker thread. void LLWebRTCImpl::updateDevices() { @@ -749,6 +773,8 @@ void LLWebRTCImpl::updateDevices() { observer->OnDevicesChanged(mPlayoutDeviceList, mRecordingDeviceList); } + + deployDevices(); } void LLWebRTCImpl::OnDevicesUpdated() @@ -771,6 +797,13 @@ void LLWebRTCImpl::setTuningMode(bool enable) [this] { mDeviceModule->SetTuning(mTuningMode, mMute); + if (!mTuningMode) + { + // Restore playout after tuning, gated on there being a + // connection to render (so the output device isn't left + // spinning with no engine data). + workerStartPlayout(); + } mSignalingThread->PostTask( [this] { @@ -842,48 +875,16 @@ void LLWebRTCImpl::setMute(bool mute, int delay_ms) void LLWebRTCImpl::intSetMute(bool mute, int delay_ms) { + // Mute by zeroing the captured (post-APM) gain; the sender track is also + // disabled per connection (see LLWebRTCPeerConnectionImpl::setMute). The + // capture device deliberately stays running for the whole session, so + // muting/unmuting never stops or starts it -- that's what avoids the AEC + // cold-start hiss on unmute. Capture start/stop is tied to device + // selection (workerStartRecording) and shutdown, not to mute. if (mPeerCustomProcessor) { mPeerCustomProcessor->setGain(mMute ? 0.0f : mGain); } - - // Sequence counter to prevent race conditions from rapid requests to mute/unmute - static std::atomic mute_sequence(0); - uint32_t current_sequence = ++mute_sequence; - - if (mMute) - { - // Keep capturing for a while after muting so quick mute/unmute cycles - // don't cold-start the AEC (and any OS capture effect such as Windows - // Voice Clarity), which is heard as a short hiss on unmute. Once the - // mute has been held this long, stop recording so the OS "mic in use" - // indicator clears. If the user unmutes or toggles before this fires, - // the sequence check turns it into a no-op and capture keeps running. - mWorkerThread->PostDelayedTask( - [this, current_sequence] - { - if (mDeviceModule && (current_sequence == mute_sequence.load())) - { - mDeviceModule->ForceStopRecording(); - } - }, - webrtc::TimeDelta::Millis(MUTE_STOP_RECORDING_DELAY_MS)); - } - else - { - mWorkerThread->PostTask( - [this, current_sequence] - { - if (mDeviceModule && (current_sequence == mute_sequence.load())) - { - // No-op if capture is still running (the common case, when - // unmuting within the stop delay -> no AEC cold start); - // restarts capture if a sustained mute had stopped it. - mDeviceModule->InitRecording(); - mDeviceModule->ForceStartRecording(); - } - }); - } } // @@ -900,12 +901,12 @@ LLWebRTCPeerConnectionInterface *LLWebRTCImpl::newPeerConnection() } mPeerConnections.emplace_back(peerConnection); - // The capture/playout devices are intentionally NOT started here. This - // runs when the connection is created/connecting; starting the output - // device now leaves it spinning with no decoded audio during the handshake, - // which is heard as a buzz. The devices are (re)started from - // OnConnectionChange(kConnected) instead, once audio is actually - // established (see startAudioDevices()). + // Playout is intentionally NOT started here. This runs when the connection + // is created/connecting; starting the output device now leaves it spinning + // with no decoded audio during the handshake, which is heard as a buzz. + // Playout is started from OnConnectionChange(kConnected) instead, once audio + // is actually established (see startPlayout()). Capture follows + // voice-enabled state, so it's not touched here either. peerConnection->enableSenderTracks(false); peerConnection->resetMute(); @@ -923,79 +924,42 @@ void LLWebRTCImpl::freePeerConnection(LLWebRTCPeerConnectionInterface* peer_conn if (mPeerConnections.empty()) { intSetMute(true); - // Last connection gone: stop capture immediately rather than - // waiting out the mute stop-delay, so the mic isn't held open after - // the call, and stop playout so the output device isn't left - // spinning with no engine data. + // Last connection gone: stop playout (there's nothing to render). + // Capture stays running while voice is enabled so it's ready -- with + // no cold-start hiss -- when the next call comes up. But if voice + // has been disabled, stop capture now: setVoiceEnabled(false) tried + // to, but the engine's send stream was still active then (and the + // engine's own StopRecording is intentionally a no-op), so the stop + // only sticks once the connection -- and its stream -- is gone. mWorkerThread->PostTask( [this]() { if (mDeviceModule) { - mDeviceModule->ForceStopRecording(); mDeviceModule->StopPlayout(); + if (!mVoiceEnabled) + { + mDeviceModule->ForceStopRecording(); + } } }); } } } -void LLWebRTCImpl::startAudioDevices() +void LLWebRTCImpl::startPlayout() { - // Called when a connection's audio is established. This is the - // authoritative point that brings the devices back (with the user's - // selected devices applied) after all connections dropped (teleport, voice - // restart) or for the first call of a session. It matters because the - // WebRTC engine no-ops Start/StopRecording on our ADM wrapper -- only our - // explicit Force* calls actually drive capture -- so when the devices were - // stopped, nothing else will restart them. - // - // It's guarded on Playing()/Recording() so a second connection establishing - // won't glitch an already-running stream, and doing this at "connected" - // rather than at connection creation avoids running the output device with - // no decoded audio during the handshake. + // Called when a connection's audio is established. Only playout is started + // here: it's gated on there being a connection to render, because running + // the output device with no engine data is heard as a buzz. Capture is + // NOT touched here -- it follows voice-enabled state (setVoiceEnabled), so + // it's already running if voice is on and must stay off if voice is off. + // Starting it here would also let a stray kConnected during voice-disable + // teardown re-open the mic. mWorkerThread->PostTask( [this]() { - if (!mDeviceModule || mTuningMode) - { - return; - } - - if (!mDeviceModule->Playing()) - { - // First established connection for this call: select and start - // the user's *chosen* capture/playout devices - // (SetRecordingDevice/SetPlayoutDevice). Just calling - // InitPlayout/InitRecording here would bring the devices up on - // whatever the ADM currently has selected -- the system default - // after a cold start -- which is why a p2p call (or a call after - // teleport/voice-restart) could come up on the wrong device. - // - // We call workerStartDevices() rather than the full - // deployDevices() on purpose: deployDevices() also re-applies - // per-connection mute/track state, which races with the - // viewer's own mute setup for the freshly-establishing - // connection and can leave the sender track disabled (recording - // runs but nothing transmits after teleport). - workerStartDevices(); - } - - // Authoritatively (re)start capture whenever we're connected and not - // device-muted. This runs unconditionally -- NOT just in an else - // branch -- because workerStartDevices() above stops recording while - // re-selecting the device and only restarts it behind a gate; if - // that gate doesn't line up (or capture was stopped on a prior - // disconnect, e.g. teleport), this is what reliably brings the mic - // back. No-op if capture is already running. - if (!mMute && !mPeerConnections.empty() && !mDeviceModule->Recording()) - { - if (!mDeviceModule->RecordingIsInitialized()) - { - mDeviceModule->InitRecording(); - } - mDeviceModule->ForceStartRecording(); - } + workerStartPlayout(); }); } @@ -1456,12 +1420,12 @@ void LLWebRTCPeerConnectionImpl::OnConnectionChange(webrtc::PeerConnectionInterf { case webrtc::PeerConnectionInterface::PeerConnectionState::kConnected: { - // Audio is established now -- (re)start the capture and playout - // devices. Doing this here rather than at connection creation - // avoids running the output device during the handshake (heard as a - // buzz), and reliably restores the devices after a full teardown - // (teleport / voice restart). - mWebRTCImpl->startAudioDevices(); + // Audio is established now -- start playout for this connection. + // (Capture follows voice-enabled state, so it's already running and + // isn't touched here.) Doing playout here rather than at connection + // creation avoids running the output device with no decoded audio + // during the handshake (heard as a buzz). + mWebRTCImpl->startPlayout(); mPendingJobs++; webrtc::scoped_refptr self(this); mWebRTCImpl->PostWorkerTask([self]() diff --git a/indra/llwebrtc/llwebrtc.h b/indra/llwebrtc/llwebrtc.h index e76e708f0c..821400cfe8 100644 --- a/indra/llwebrtc/llwebrtc.h +++ b/indra/llwebrtc/llwebrtc.h @@ -153,6 +153,14 @@ class LLWebRTCDeviceInterface virtual void setCaptureDevice(const std::string& id) = 0; virtual void setRenderDevice(const std::string& id) = 0; + // Enable/disable the audio devices, set when voice is enabled/disabled. + // The capture (microphone) and playout (speaker) devices only run while this + // is enabled, so neither is held open when the user has voice off. While + // enabled, capture stays running across calls and mute/unmute so the AEC + // never cold-starts (no unmute hiss); playout still only runs when there's a + // connection to render. + virtual void setVoiceEnabled(bool enable) = 0; + // Device observers for device change callbacks. virtual void setDevicesObserver(LLWebRTCDevicesObserver *observer) = 0; virtual void unsetDevicesObserver(LLWebRTCDevicesObserver *observer) = 0; diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index cfb0d10c29..28d25b8d51 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -180,7 +180,7 @@ private: class LLWebRTCAudioDeviceModule : public webrtc::AudioDeviceModule { public: - explicit LLWebRTCAudioDeviceModule(webrtc::scoped_refptr inner) : inner_(std::move(inner)), tuning_(false) + explicit LLWebRTCAudioDeviceModule(webrtc::scoped_refptr inner) : inner_(inner), tuning_(false) { RTC_CHECK(inner_); } @@ -197,9 +197,15 @@ public: } int32_t Init() override { return inner_->Init(); } - int32_t Terminate() override { return inner_->Terminate(); } + int32_t Terminate() override { + // libwebrtc attempts to terminate the adm when peer connections go to zero, but we don't want that, + // now that we're keeping the adm active throughout the session. + return 0; + } bool Initialized() const override { return inner_->Initialized(); } + int32_t ForceTerminate() { return inner_->Terminate(); } + // --- Device enumeration/selection (forward) --- int16_t PlayoutDevices() override { return inner_->PlayoutDevices(); } int16_t RecordingDevices() override { return inner_->RecordingDevices(); } @@ -422,6 +428,8 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO void setCaptureDevice(const std::string& id) override; void setRenderDevice(const std::string& id) override; + void setVoiceEnabled(bool enable) override; + void setTuningMode(bool enable) override; float getTuningAudioLevel() override; float getPeerConnectionAudioLevel() override; @@ -499,16 +507,17 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO LLWebRTCPeerConnectionInterface* newPeerConnection(); void freePeerConnection(LLWebRTCPeerConnectionInterface* peer_connection); - // (Re)start the capture and playout devices once a connection's audio is - // established. This is the authoritative point for bringing the devices - // back after all connections dropped (teleport, voice restart). Idempotent - // and safe to call from any thread (work is posted to the worker thread). - void startAudioDevices(); + // Start playout once a connection's audio is established (playout is gated + // on there being a connection to render). Capture is not touched here -- + // it follows voice-enabled state, not connection state. Safe to call from + // any thread (work is posted to the worker thread). + void startPlayout(); protected: const webrtc::Environment mEnv; - void workerStartDevices(); + void workerStartRecording(); + void workerStartPlayout(); void workerDeployDevices(); // We always rely on WebRTC's internal (software APM) audio processing, so // any platform/hardware AEC/AGC/NS must be kept disabled. @@ -547,6 +556,8 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO LLWebRTCVoiceDeviceList mPlayoutDeviceList; bool mMute; + // Whether voice is enabled; gates whether the capture/playout devices run. + bool mVoiceEnabled; float mGain; LLCustomProcessorStatePtr mPeerCustomProcessor; diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index ecf963039f..126d22924b 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -1711,6 +1711,14 @@ void LLWebRTCVoiceClient::setVoiceEnabled(bool enabled) mVoiceEnabled = enabled; LLVoiceClientStatusObserver::EStatusType status; + // Gate the audio devices on voice being enabled: the capture mic and + // playout speaker only run while voice is on, and the mic isn't held + // open when voice is off. + if (mWebRTCDeviceInterface) + { + mWebRTCDeviceInterface->setVoiceEnabled(enabled); + } + if (enabled) { LL_DEBUGS("Voice") << "enabling" << LL_ENDL; -- 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(-) 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