diff options
| author | Jonathan "Geenz" Goodman <geenz@lindenlab.com> | 2026-08-25 12:56:22 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-25 12:56:22 -0400 |
| commit | 2e9c387fee881022aec13d5c85f901539683c18f (patch) | |
| tree | d2cc89444771d96871b3126c9462f072dc059e32 | |
| parent | 92289533c1b95bc0fb2cdcc1217ae539485ea244 (diff) | |
| parent | a402fd4da91bbce03340e174505c06feede65866 (diff) | |
Merge branch 'develop' into project/lua_editor
47 files changed, 1232 insertions, 353 deletions
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6aa85f9fc7..d19c2f1c02 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -183,7 +183,7 @@ jobs: - name: Setup .NET for Velopack if: (runner.os == 'Windows' || runner.os == 'macOS') && (github.event.inputs.installer_type || 'velopack') == 'velopack' - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: '9.0.x' diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index edfe71b693..e1a88a8c25 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -12,7 +12,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v10 + - uses: actions/stale@v11 id: stale with: stale-pr-message: This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or it will be closed in 7 days diff --git a/indra/llappearance/lltexlayer.cpp b/indra/llappearance/lltexlayer.cpp index 5a02dfe716..45f3e8dcd2 100644 --- a/indra/llappearance/lltexlayer.cpp +++ b/indra/llappearance/lltexlayer.cpp @@ -1430,6 +1430,15 @@ void LLTexLayer::renderMorphMasks(S32 x, S32 y, S32 width, S32 height, const LLC // We should only be doing this when we believe something has changed with respect to the user's appearance. { LL_DEBUGS("Morph") << "gl alpha cache of morph mask not found, doing readback: " << getName() << LL_ENDL; + + // Replace the cached mask without leaking its old allocation. + alpha_cache_t::iterator cached = mAlphaCache.find(cache_index); + if (cached != mAlphaCache.end()) + { + ll_aligned_free_32(cached->second); + mAlphaCache.erase(cached); + } + // clear out a slot if we have filled our cache S32 max_cache_entries = getTexLayerSet()->getAvatarAppearance()->isSelf() ? 4 : 1; while ((S32)mAlphaCache.size() >= max_cache_entries) @@ -1466,6 +1475,7 @@ void LLTexLayer::renderMorphMasks(S32 x, S32 y, S32 width, S32 height, const LLC U8* temp = (U8*)ll_aligned_malloc_32(mem_size << 2); // allocate same size, but RGBA if (!temp) { + ll_aligned_free_32(alpha_data); LLError::LLUserWarningMsg::showOutOfMemory(); LL_ERRS() << "Failed to allocate temporary memory for morph texture readback: " << (S32)(mem_size << 2) << LL_ENDL; return; @@ -1508,6 +1518,7 @@ void LLTexLayer::renderMorphMasks(S32 x, S32 y, S32 width, S32 height, const LLC U8* temp_data = (U8*)ll_aligned_malloc_32(mem_size * TEMP_BYTES_PER_PIXEL); if (!temp_data) { + ll_aligned_free_32(alpha_data); LLError::LLUserWarningMsg::showOutOfMemory(); LL_ERRS() << "Failed to allocate temporary memory for morph texture: " << (S32)(mem_size * TEMP_BYTES_PER_PIXEL) << LL_ENDL; return; diff --git a/indra/llcommon/llprocess.cpp b/indra/llcommon/llprocess.cpp index a71589a2bf..e43f386af2 100644 --- a/indra/llcommon/llprocess.cpp +++ b/indra/llcommon/llprocess.cpp @@ -701,8 +701,25 @@ void LLProcess::launch(const LLSDOrParams& params) else stdio.err = nullptr; // inherit - // Build executable path + // Build executable path. + // Resolve bare executable names (i.e. "outleap-agent") through the + // environment's PATH boost::filesystem::path executable_path(params.executable()); + if (!executable_path.has_parent_path()) + { + boost::filesystem::path resolved = + bp::environment::find_executable(executable_path); + if (!resolved.empty()) + { + executable_path = resolved; + } + else + { + LL_WARNS("LLProcess") << "Could not locate '" << params.executable() + << "' on PATH -- launch will likely fail" << LL_ENDL; + // Let bp::process try to produce an error. + } + } // In Boost.Process v2, error_code cannot be passed as an initializer. // Use the throwing overload and catch the exception instead. diff --git a/indra/llcommon/llprocessor.cpp b/indra/llcommon/llprocessor.cpp index 9d0ef45bee..456f2b02d4 100644 --- a/indra/llcommon/llprocessor.cpp +++ b/indra/llcommon/llprocessor.cpp @@ -123,6 +123,9 @@ namespace eSSE4_1_Features = 38, eSSE4_2_Features = 39, eSSE4a_Features = 40, + eAVX_Features = 41, + eAVX2_Features = 42, + eAVX512F_Features = 43, }; const char* cpu_feature_names[] = @@ -170,6 +173,9 @@ namespace "SSE4.1 Instructions", "SSE4.2 Instructions", "SSE4a Instructions", + "AVX Instructions", // 41 + "AVX2 Instructions", // 42 + "AVX-512F Instructions", // 43 }; std::string intel_CPUFamilyName(int composed_family) @@ -283,6 +289,21 @@ public: return hasExtension(cpu_feature_names[eSSE4a_Features]); } + bool hasAVX() const + { + return hasExtension(cpu_feature_names[eAVX_Features]); + } + + bool hasAVX2() const + { + return hasExtension(cpu_feature_names[eAVX2_Features]); + } + + bool hasAVX512F() const + { + return hasExtension(cpu_feature_names[eAVX512F_Features]); + } + bool hasAltivec() const { return hasExtension("Altivec"); @@ -572,6 +593,12 @@ private: setExtension(cpu_feature_names[eSSE4_2_Features]); } + // AVX: CPUID leaf 1, ECX bit 28, OSXSAVE + XCR0[1:2] enabled + if ((cpu_info[2] & 0x18000000) == 0x18000000 && ((_xgetbv(0) & 0x6) == 0x6)) + { + setExtension(cpu_feature_names[eAVX_Features]); + } + unsigned int feature_info = (unsigned int) cpu_info[3]; for(unsigned int index = 0, bit = 1; index < eSSE3_Features; ++index, bit <<= 1) { @@ -581,6 +608,27 @@ private: } } } + else if (i == 7) + { + // AVX2/AVX-512* require AVX to be usable (OSXSAVE + XCR0 state) + if (hasExtension(cpu_feature_names[eAVX_Features])) + { + int cpu_info7[4] = { -1 }; + __cpuidex(cpu_info7, 7, 0); + // AVX2: EBX bit 5 + if (cpu_info7[1] & 0x20) + { + setExtension(cpu_feature_names[eAVX2_Features]); + } + + // AVX-512F: EBX bit 16; ZMM state in XCR0 (bits 5-7) + const unsigned long long xcr0 = _xgetbv(0); + if ((cpu_info7[1] & 0x10000) && ((xcr0 & 0xE6) == 0xE6)) + { + setExtension(cpu_feature_names[eAVX512F_Features]); + } + } + } } // Calling __cpuid with 0x80000000 as the InfoType argument @@ -779,6 +827,29 @@ private: // Not supposed to happen? setExtension(cpu_feature_names[eSSE4a_Features]); } + if (cpu_features_str.find(" AVX1.0 ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX_Features]); + } + + // AVX2 and AVX-512F are reported in machdep.cpu.leaf7_features on macOS + char cpu_leaf7_features[1024]; + len = sizeof(cpu_leaf7_features); + memset(cpu_leaf7_features, 0, len); + sysctlbyname("machdep.cpu.leaf7_features", (void*)cpu_leaf7_features, &len, NULL, 0); + + std::string cpu_leaf7_str(cpu_leaf7_features); + cpu_leaf7_str = " " + cpu_leaf7_str + " "; + + if (cpu_leaf7_str.find(" AVX2 ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX2_Features]); + } + + if (cpu_leaf7_str.find(" AVX512F ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX512F_Features]); + } } }; @@ -946,6 +1017,21 @@ private: setExtension(cpu_feature_names[eSSE4a_Features]); } + if (flags.find(" avx ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX_Features]); + } + + if (flags.find(" avx2 ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX2_Features]); + } + + if (flags.find(" avx512f ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX512F_Features]); + } + # endif // LL_X86 } @@ -1010,6 +1096,9 @@ bool LLProcessorInfo::hasSSE3S() const { return mImpl->hasSSE3S(); } bool LLProcessorInfo::hasSSE41() const { return mImpl->hasSSE41(); } bool LLProcessorInfo::hasSSE42() const { return mImpl->hasSSE42(); } bool LLProcessorInfo::hasSSE4a() const { return mImpl->hasSSE4a(); } +bool LLProcessorInfo::hasAVX() const { return mImpl->hasAVX(); } +bool LLProcessorInfo::hasAVX2() const { return mImpl->hasAVX2(); } +bool LLProcessorInfo::hasAVX512F() const { return mImpl->hasAVX512F(); } bool LLProcessorInfo::hasAltivec() const { return mImpl->hasAltivec(); } std::string LLProcessorInfo::getCPUFamilyName() const { return mImpl->getCPUFamilyName(); } std::string LLProcessorInfo::getCPUBrandName() const { return mImpl->getCPUBrandName(); } diff --git a/indra/llcommon/llprocessor.h b/indra/llcommon/llprocessor.h index b955f7f55c..da93a531f4 100644 --- a/indra/llcommon/llprocessor.h +++ b/indra/llcommon/llprocessor.h @@ -46,6 +46,9 @@ public: bool hasSSE41() const; bool hasSSE42() const; bool hasSSE4a() const; + bool hasAVX() const; + bool hasAVX2() const; + bool hasAVX512F() const; bool hasAltivec() const; std::string getCPUFamilyName() const; std::string getCPUBrandName() const; diff --git a/indra/llcommon/llsys.cpp b/indra/llcommon/llsys.cpp index b1b281001e..bc38b72004 100644 --- a/indra/llcommon/llsys.cpp +++ b/indra/llcommon/llsys.cpp @@ -580,6 +580,9 @@ LLCPUInfo::LLCPUInfo() mHasSSE41 = proc.hasSSE41(); mHasSSE42 = proc.hasSSE42(); mHasSSE4a = proc.hasSSE4a(); + mHasAVX = proc.hasAVX(); + mHasAVX2 = proc.hasAVX2(); + mHasAVX512F = proc.hasAVX512F(); mHasAltivec = proc.hasAltivec(); mCPUMHz = (F64)proc.getCPUFrequency(); mFamily = proc.getCPUFamilyName(); @@ -621,6 +624,18 @@ LLCPUInfo::LLCPUInfo() { mSSEVersions.append("4a"); } + if (mHasAVX) + { + mSIMDVersions.append("AVX"); + } + if (mHasAVX2) + { + mSIMDVersions.append("AVX2"); + } + if (mHasAVX512F) + { + mSIMDVersions.append("AVX-512F"); + } } bool LLCPUInfo::hasAltivec() const @@ -663,6 +678,21 @@ bool LLCPUInfo::hasSSE4a() const return mHasSSE4a; } +bool LLCPUInfo::hasAVX() const +{ + return mHasAVX; +} + +bool LLCPUInfo::hasAVX2() const +{ + return mHasAVX2; +} + +bool LLCPUInfo::hasAVX512F() const +{ + return mHasAVX512F; +} + F64 LLCPUInfo::getMHz() const { return mCPUMHz; @@ -678,6 +708,11 @@ const LLSD& LLCPUInfo::getSSEVersions() const return mSSEVersions; } +const LLSD& LLCPUInfo::getSIMDVersions() const +{ + return mSIMDVersions; +} + void LLCPUInfo::stream(std::ostream& s) const { // gather machine information. diff --git a/indra/llcommon/llsys.h b/indra/llcommon/llsys.h index 0abbe047ad..aeba5563ba 100644 --- a/indra/llcommon/llsys.h +++ b/indra/llcommon/llsys.h @@ -81,6 +81,7 @@ public: std::string getCPUString() const; const LLSD& getSSEVersions() const; + const LLSD& getSIMDVersions() const; bool hasAltivec() const; bool hasSSE() const; @@ -90,6 +91,9 @@ public: bool hasSSE41() const; bool hasSSE42() const; bool hasSSE4a() const; + bool hasAVX() const; + bool hasAVX2() const; + bool hasAVX512F() const; F64 getMHz() const; // Family is "AMD Duron" or "Intel Pentium Pro" @@ -103,11 +107,15 @@ private: bool mHasSSE41; bool mHasSSE42; bool mHasSSE4a; + bool mHasAVX; + bool mHasAVX2; + bool mHasAVX512F; bool mHasAltivec; F64 mCPUMHz; std::string mFamily; std::string mCPUString; LLSD mSSEVersions; + LLSD mSIMDVersions; }; //============================================================================= diff --git a/indra/llcommon/llwatchdog.h b/indra/llcommon/llwatchdog.h index a3a51113b0..9386bd6bb3 100644 --- a/indra/llcommon/llwatchdog.h +++ b/indra/llcommon/llwatchdog.h @@ -93,6 +93,11 @@ public: LLWatchdog(); ~LLWatchdog(); + LLWatchdog(const LLWatchdog&) = delete; + LLWatchdog(LLWatchdog&&) = delete; + LLWatchdog& operator=(const LLWatchdog&) = delete; + LLWatchdog& operator=(LLWatchdog&&) = delete; + // Add an entry to the watchdog. void add(LLWatchdogEntry* e); void remove(LLWatchdogEntry* e); diff --git a/indra/llcorehttp/httpstats.cpp b/indra/llcorehttp/httpstats.cpp index dc2ac35c71..f64752e91e 100644 --- a/indra/llcorehttp/httpstats.cpp +++ b/indra/llcorehttp/httpstats.cpp @@ -41,6 +41,7 @@ HTTPStats::~HTTPStats() void HTTPStats::resetStats() { + std::lock_guard<std::mutex> lock(mStatsMutex); mResutCodes.clear(); mDataDown.reset(); mDataUp.reset(); @@ -50,6 +51,7 @@ void HTTPStats::resetStats() void HTTPStats::recordResultCode(S32 code) { + std::lock_guard<std::mutex> lock(mStatsMutex); std::map<S32, S32>::iterator it; it = mResutCodes.find(code); @@ -86,6 +88,7 @@ namespace void HTTPStats::dumpStats() { + std::lock_guard<std::mutex> lock(mStatsMutex); std::stringstream out; out << "HTTP DATA SUMMARY" << std::endl; diff --git a/indra/llcorehttp/httpstats.h b/indra/llcorehttp/httpstats.h index 5c0f26d34e..08a537cda7 100644 --- a/indra/llcorehttp/httpstats.h +++ b/indra/llcorehttp/httpstats.h @@ -47,17 +47,23 @@ namespace LLCore void recordDataDown(size_t bytes) { + std::lock_guard<std::mutex> lock(mStatsMutex); mDataDown.push((F32)bytes); } void recordDataUp(size_t bytes) { + std::lock_guard<std::mutex> lock(mStatsMutex); mDataUp.push((F32)bytes); } - void recordHTTPRequest() { ++mRequests; } + void recordHTTPRequest() + { + std::lock_guard<std::mutex> lock(mStatsMutex); + ++mRequests; + } - void recordResultCode(S32 code); + void recordResultCode(S32 code); // http thread void dumpStats(); private: @@ -67,6 +73,8 @@ namespace LLCore S32 mRequests; std::map<S32, S32> mResutCodes; + + std::mutex mStatsMutex; }; diff --git a/indra/llmessage/CMakeLists.txt b/indra/llmessage/CMakeLists.txt index f143ee18cf..2306c377ba 100644 --- a/indra/llmessage/CMakeLists.txt +++ b/indra/llmessage/CMakeLists.txt @@ -165,6 +165,7 @@ set(llmessage_HEADER_FILES llxfer_mem.h llxfer_vfile.h llxorcipher.h + llzerocode.h machine.h mean_collision_data.h message.h @@ -228,6 +229,7 @@ if (LL_TESTS) LL_ADD_INTEGRATION_TEST(llservicebuilder "" "${test_libs}") LL_ADD_INTEGRATION_TEST(lltemplatemessagebuilder "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llxfer_file "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(llzerocode "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llxorcipher "" "${test_libs}") LL_ADD_INTEGRATION_TEST(message "" "${test_libs}") endif (LL_TESTS) diff --git a/indra/llmessage/lltemplatemessagebuilder.cpp b/indra/llmessage/lltemplatemessagebuilder.cpp index 758d6d343f..c4ff63bea4 100644 --- a/indra/llmessage/lltemplatemessagebuilder.cpp +++ b/indra/llmessage/lltemplatemessagebuilder.cpp @@ -29,6 +29,7 @@ #include "lltemplatemessagebuilder.h" #include "llmessagetemplate.h" +#include "llzerocode.h" #include "llmath.h" #include "llquaternion.h" #include "u64.h" @@ -492,93 +493,29 @@ void LLTemplateMessageBuilder::addUUID(const char *varname, const LLUUID& uuid) addData(varname, uuid.mData, MVT_LLUUID, sizeof(uuid.mData)); } -static S32 zero_code(U8 **data, U32 *data_size) +void LLTemplateMessageBuilder::compressMessage(U8*& buf_ptr, U32& buffer_length) { - // Encoded send buffer needs to be slightly larger since the zero - // coding can potentially increase the size of the send data. - static U8 encodedSendBuffer[2 * MAX_BUFFER_SIZE]; - - S32 count = *data_size; - - S32 net_gain = 0; - U8 num_zeroes = 0; - - U8 *inptr = (U8 *)*data; - U8 *outptr = (U8 *)encodedSendBuffer; - -// skip the packet id field - - for (U32 ii = 0; ii < LL_PACKET_ID_SIZE ; ++ii) - { - count--; - *outptr++ = *inptr++; - } - -// build encoded packet, keeping track of net size gain - -// sequential zero bytes are encoded as 0 [U8 count] -// with 0 0 [count] representing wrap (>256 zeroes) - - while (count--) + if(ME_ZEROCODED != mCurrentSMessageTemplate->getEncoding()) { - if (!(*inptr)) // in a zero count - { - if (num_zeroes) - { - if (++num_zeroes > 254) - { - *outptr++ = num_zeroes; - num_zeroes = 0; - } - net_gain--; // subseqent zeroes save one - } - else - { - *outptr++ = 0; - net_gain++; // starting a zero count adds one - num_zeroes = 1; - } - inptr++; - } - else - { - if (num_zeroes) - { - *outptr++ = num_zeroes; - num_zeroes = 0; - } - *outptr++ = *inptr++; - } + return; } - if (num_zeroes) - { - *outptr++ = num_zeroes; - } + // Encoded send buffer needs to be slightly larger since the zero + // coding can potentially increase the size of the send data. + static U8 encodedSendBuffer[2 * MAX_BUFFER_SIZE]; - if (net_gain < 0) + S32 encoded_size = LLZeroCode::encode(buf_ptr, buffer_length, + encodedSendBuffer, sizeof(encodedSendBuffer), + LL_PACKET_ID_SIZE); + if (encoded_size >= 0) { // TODO: babbage: reinstate stat collecting... //mCompressedPacketsOut++; - //mUncompressedBytesOut += *data_size; - - *data = encodedSendBuffer; - *data_size += net_gain; - encodedSendBuffer[0] |= LL_ZERO_CODE_FLAG; // set the head bit to indicate zero coding - - //mCompressedBytesOut += *data_size; - - } - //mTotalBytesOut += *data_size; + //mUncompressedBytesOut += buffer_length; + //mCompressedBytesOut += encoded_size; - return(net_gain); -} - -void LLTemplateMessageBuilder::compressMessage(U8*& buf_ptr, U32& buffer_length) -{ - if(ME_ZEROCODED == mCurrentSMessageTemplate->getEncoding()) - { - zero_code(&buf_ptr, &buffer_length); + buf_ptr = encodedSendBuffer; + buffer_length = (U32)encoded_size; } } diff --git a/indra/llmessage/llzerocode.h b/indra/llmessage/llzerocode.h new file mode 100644 index 0000000000..7246c9d946 --- /dev/null +++ b/indra/llmessage/llzerocode.h @@ -0,0 +1,211 @@ +/** + * @file llzerocode.h + * @brief Zero-code run-length compression used by the LLMessageSystem UDP protocol. + * + * $LicenseInfo:firstyear=2001&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, 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 + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLZEROCODE_H +#define LL_LLZEROCODE_H + +#include <cstring> + +#include "stdtypes.h" + +// Zero-coding compresses runs of zero bytes in a packet body, leaving the +// first header_size bytes of the buffer (the packet header - flags, +// sequence number, offset, etc.) untouched aside from the flag bit below. +// +// Runs of zero bytes in the body are replaced by a two-byte token: +// 0x00 N - represents N zero bytes, for N in 1..254 +// 0x00 0x00 N - represents (255 + N) zero bytes (wrap/overflow case, +// produced by decode()'s wire format but never emitted +// by encode(), which instead starts a fresh 0x00 token +// every 255 zero bytes) +namespace LLZeroCode +{ + // High bit of the first header byte: set by encode() and cleared by + // decode() to indicate whether the body that follows is zero-coded. + const U8 FLAG = 0x80; + + // Zero-codes src (src_size bytes, the first header_size of which are the + // packet header and are copied verbatim) into dst. + // + // dst_capacity must be at least 2 * src_size: a pathological body of + // isolated zero bytes can nearly double in size when encoded. + // + // Returns the encoded size (with FLAG set in dst[0]) if doing so made the + // packet smaller. Returns -1 if compression would not help (or the + // arguments are invalid), in which case dst is left untouched and the + // caller should keep using the original, uncompressed buffer. + inline S32 encode(const U8* src, U32 src_size, U8* dst, U32 dst_capacity, U32 header_size) + { + if (src_size < header_size || dst_capacity < 2 * src_size) + { + return -1; + } + + S32 count = (S32)(src_size - header_size); + S32 net_gain = 0; + U8 num_zeroes = 0; + + const U8* inptr = src; + U8* outptr = dst; + + // copy the header verbatim + for (U32 ii = 0; ii < header_size; ++ii) + { + *outptr++ = *inptr++; + } + + // sequential zero bytes are encoded as 0 [U8 count]; a run longer + // than 254 bytes is split into consecutive 0 [U8 count] tokens. + while (count--) + { + if (!(*inptr)) // in a zero count + { + if (num_zeroes) + { + if (++num_zeroes > 254) + { + *outptr++ = num_zeroes; + num_zeroes = 0; + } + net_gain--; // subsequent zeroes save one + } + else + { + *outptr++ = 0; + net_gain++; // starting a zero count adds one + num_zeroes = 1; + } + inptr++; + } + else + { + if (num_zeroes) + { + *outptr++ = num_zeroes; + num_zeroes = 0; + } + *outptr++ = *inptr++; + } + } + + if (num_zeroes) + { + *outptr++ = num_zeroes; + } + + if (net_gain >= 0) + { + // compression did not shrink the packet; caller should keep the original + return -1; + } + + dst[0] |= FLAG; + return (S32)src_size + net_gain; + } + + // Expands a zero-coded src (src_size bytes) into dst. + // + // If FLAG is not set in src[0], the body is not zero-coded: no work is + // done and the function returns 0. + // + // On success, returns the number of bytes written to dst (always includes + // the header_size header bytes, copied verbatim except for FLAG being + // cleared from dst[0]). + // + // If expansion would write past dst_capacity - which only a malformed or + // malicious packet should cause - decoding is aborted, *overflow is set + // true, and the returned size reflects however much (if anything) was + // salvaged; the caller should treat the packet as invalid. + inline U32 decode(const U8* src, U32 src_size, U8* dst, U32 dst_capacity, U32 header_size, bool& overflow) + { + overflow = false; + + if (src_size < header_size || !(src[0] & FLAG)) + { + return 0; + } + + S32 count = (S32)(src_size - header_size); + + const U8* inptr = src; + U8* outptr = dst; + + for (U32 ii = 0; ii < header_size; ++ii) + { + *outptr++ = *inptr++; + } + dst[0] &= ~FLAG; + + // reconstruct the body: a 0x00 byte starts a run; the byte(s) that + // follow give its length (see the wire format described above). + while (count--) + { + if (outptr > &dst[dst_capacity - 1]) + { + overflow = true; + outptr = dst; + break; + } + + if (!((*outptr++ = *inptr++))) + { + while ((count--) && (!(*inptr))) + { + if (outptr > &dst[dst_capacity - 256]) + { + overflow = true; + outptr = dst; + count = -1; + break; + } + *outptr++ = *inptr++; + memset(outptr, 0, 255); + outptr += 255; + } + + if (count < 0) + { + break; + } + else + { + if (outptr > &dst[dst_capacity - (*inptr)]) + { + overflow = true; + outptr = dst; + } + memset(outptr, 0, (*inptr) - 1); + outptr += ((*inptr) - 1); + inptr++; + } + } + } + + return (U32)(outptr - dst); + } +} + +#endif // LL_LLZEROCODE_H diff --git a/indra/llmessage/message.cpp b/indra/llmessage/message.cpp index db7405f93f..71210f7535 100644 --- a/indra/llmessage/message.cpp +++ b/indra/llmessage/message.cpp @@ -67,6 +67,7 @@ #include "llsdmessagereader.h" #include "llsdserialize.h" #include "llstring.h" +#include "llzerocode.h" #include "lltransfermanager.h" #include "lluuid.h" #include "llxfermanager.h" @@ -3060,85 +3061,6 @@ U32 LLMessageSystem::getListenPort( void ) const return mPort; } -// TODO: babbage: remove this horror! -S32 LLMessageSystem::zeroCodeAdjustCurrentSendTotal() -{ - if(mMessageBuilder == mLLSDMessageBuilder) - { - // babbage: don't compress LLSD messages, so delta is 0 - return 0; - } - - if (! mMessageBuilder->isBuilt()) - { - mSendSize = mMessageBuilder->buildMessage( - mSendBuffer, - MAX_BUFFER_SIZE, - 0); - } - // TODO: babbage: remove this horror - mMessageBuilder->setBuilt(false); - - S32 count = mSendSize; - - S32 net_gain = 0; - U8 num_zeroes = 0; - - U8 *inptr = (U8 *)mSendBuffer; - -// skip the packet id field - - for (U32 ii = 0; ii < LL_PACKET_ID_SIZE; ++ii) - { - count--; - inptr++; - } - -// don't actually build, just test - -// sequential zero bytes are encoded as 0 [U8 count] -// with 0 0 [count] representing wrap (>256 zeroes) - - while (count--) - { - if (!(*inptr)) // in a zero count - { - if (num_zeroes) - { - if (++num_zeroes > 254) - { - num_zeroes = 0; - } - net_gain--; // subseqent zeroes save one - } - else - { - net_gain++; // starting a zero count adds one - num_zeroes = 1; - } - inptr++; - } - else - { - if (num_zeroes) - { - num_zeroes = 0; - } - inptr++; - } - } - if (net_gain < 0) - { - return net_gain; - } - else - { - return 0; - } -} - - - S32 LLMessageSystem::zeroCodeExpand(U8** data, S32* data_size) { if ((*data_size ) < LL_MINIMUM_VALID_PACKET_SIZE) @@ -3159,74 +3081,18 @@ S32 LLMessageSystem::zeroCodeExpand(U8** data, S32* data_size) mCompressedPacketsIn++; mCompressedBytesIn += *data_size; - *data[0] &= (~LL_ZERO_CODE_FLAG); - - S32 count = (*data_size); - - U8 *inptr = (U8 *)*data; - U8 *outptr = (U8 *)mEncodedRecvBuffer; - -// skip the packet id field - - for (U32 ii = 0; ii < LL_PACKET_ID_SIZE; ++ii) - { - count--; - *outptr++ = *inptr++; - } - -// reconstruct encoded packet, keeping track of net size gain - -// sequential zero bytes are encoded as 0 [U8 count] -// with 0 0 [count] representing wrap (>256 zeroes) - - while (count--) + bool overflow = false; + U32 decoded_size = LLZeroCode::decode(*data, (U32)*data_size, + mEncodedRecvBuffer, sizeof(mEncodedRecvBuffer), + LL_PACKET_ID_SIZE, overflow); + if (overflow) { - if (outptr > (&mEncodedRecvBuffer[MAX_BUFFER_SIZE-1])) - { - LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size 1" << LL_ENDL; - callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE); - outptr = mEncodedRecvBuffer; - break; - } - if (!((*outptr++ = *inptr++))) - { - while (((count--)) && (!(*inptr))) - { - *outptr++ = *inptr++; - if (outptr > (&mEncodedRecvBuffer[MAX_BUFFER_SIZE-256])) - { - LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size 2" << LL_ENDL; - callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE); - outptr = mEncodedRecvBuffer; - count = -1; - break; - } - memset(outptr,0,255); - outptr += 255; - } - - if (count < 0) - { - break; - } - - else - { - if (outptr > (&mEncodedRecvBuffer[MAX_BUFFER_SIZE-(*inptr)])) - { - LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size 3" << LL_ENDL; - callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE); - outptr = mEncodedRecvBuffer; - } - memset(outptr,0,(*inptr) - 1); - outptr += ((*inptr) - 1); - inptr++; - } - } + LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size" << LL_ENDL; + callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE); } *data = mEncodedRecvBuffer; - *data_size = (S32)(outptr - mEncodedRecvBuffer); + *data_size = (S32)decoded_size; mUncompressedBytesIn += *data_size; return(in_size); diff --git a/indra/llmessage/message.h b/indra/llmessage/message.h index e23a67c04a..2e19e567e6 100644 --- a/indra/llmessage/message.h +++ b/indra/llmessage/message.h @@ -604,7 +604,6 @@ public: //void buildMessage(); S32 zeroCodeExpand(U8 **data, S32 *data_size); - S32 zeroCodeAdjustCurrentSendTotal(); // Uses ping-based retry S32 sendReliable(const LLHost &host); diff --git a/indra/llmessage/tests/llzerocode_test.cpp b/indra/llmessage/tests/llzerocode_test.cpp new file mode 100644 index 0000000000..1aba46149d --- /dev/null +++ b/indra/llmessage/tests/llzerocode_test.cpp @@ -0,0 +1,253 @@ +/** + * @file llzerocode_test.cpp + * @brief LLZeroCode test cases. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, 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 + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../llzerocode.h" + +#include "../test/lltut.h" + +#include <vector> + +namespace tut +{ + struct zerocode_data + { + }; + typedef test_group<zerocode_data> zerocode_test; + typedef zerocode_test::object zerocode_object; + tut::zerocode_test zerocode_testcase("LLZeroCode"); + + // Builds header_size bytes of header (with header[0] == header0) followed by body. + static std::vector<U8> makeBuffer(U32 header_size, U8 header0, const std::vector<U8>& body) + { + std::vector<U8> buf(header_size, 0); + if (header_size) + { + buf[0] = header0; + } + buf.insert(buf.end(), body.begin(), body.end()); + return buf; + } + + // Runs src through encode() then decode(), and (when compression is expected to help) + // asserts the round trip reproduces src exactly, including untouched header bits other + // than the zero-code flag. When compression is not expected to help, asserts encode() + // refuses it. + static void ensureRoundTrip(const char* msg, U32 header_size, U8 header0, + const std::vector<U8>& body, bool expect_compressed) + { + std::vector<U8> src = makeBuffer(header_size, header0, body); + std::vector<U8> enc(2 * src.size() + 16, 0xAA); + + S32 enc_size = LLZeroCode::encode(src.data(), (U32)src.size(), + enc.data(), (U32)enc.size(), header_size); + + if (!expect_compressed) + { + ensure(std::string(msg) + ": encode should refuse (no benefit)", enc_size < 0); + return; + } + + ensure(std::string(msg) + ": encode should succeed", enc_size >= 0); + ensure(std::string(msg) + ": encoded size should be smaller", (U32)enc_size < src.size()); + ensure(std::string(msg) + ": FLAG should be set on encoded output", (enc[0] & LLZeroCode::FLAG) != 0); + if (header_size > 0) + { + ensure_equals(std::string(msg) + ": non-flag header bits preserved on encode", + (U8)(enc[0] & ~LLZeroCode::FLAG), (U8)(header0 & ~LLZeroCode::FLAG)); + } + + std::vector<U8> dec(src.size() + 16, 0xBB); + bool overflow = false; + U32 dec_size = LLZeroCode::decode(enc.data(), (U32)enc_size, + dec.data(), (U32)dec.size(), header_size, overflow); + ensure(std::string(msg) + ": decode should not overflow", !overflow); + ensure_equals(std::string(msg) + ": decoded size should match original", dec_size, (U32)src.size()); + ensure(std::string(msg) + ": decoded bytes should match original", + memcmp(dec.data(), src.data(), src.size()) == 0); + } + + // Basic mixed body; header carries an unrelated flag bit (0x40) that must survive untouched. + template<> template<> + void zerocode_object::test<1>() + { + ensureRoundTrip("mixed body", 6, 0x40, + {0,0,0,5,0,0,0,0,0,0,7,8,9,0,0}, true); + } + + // A body with no zero bytes at all cannot benefit from zero-coding. + template<> template<> + void zerocode_object::test<2>() + { + ensureRoundTrip("no zero bytes", 1, 0x00, {1,2,3,4,5,6,7,8,9}, false); + } + + // Isolated zero-byte runs of length 1 or 2 cost as much or more than they save + // (marker + terminator == 2 bytes), so encode must refuse them. + template<> template<> + void zerocode_object::test<3>() + { + ensureRoundTrip("isolated single zero", 1, 0x00, {5, 0}, false); + ensureRoundTrip("isolated double zero", 1, 0x00, {5, 0, 0, 9}, false); + } + + // A long enough zero run followed by other data pays off. + template<> template<> + void zerocode_object::test<4>() + { + std::vector<U8> body(20, 0); + body.push_back(99); + ensureRoundTrip("long zero run", 1, 0x00, body, true); + } + + // Wrap boundary: runs are split into chunks of at most 255 zero bytes each + // (encode never emits the doubled-0x00 wire form; see test<7> for that). + // A run only shrinks the packet once it is longer than 2 bytes. + template<> template<> + void zerocode_object::test<5>() + { + static const U32 lengths[] = {1, 2, 253, 254, 255, 256, 257, 300, 509, 510, 511, 1000}; + for (U32 n : lengths) + { + std::vector<U8> body(n, 0); + body.push_back(42); // trailing nonzero byte + ensureRoundTrip("wrap boundary (with trailing byte)", 1, 0x00, body, n > 2); + } + } + + // Same boundary check, but with the zero run flushed at end-of-buffer + // (no trailing nonzero byte to force the terminator write mid-loop). + template<> template<> + void zerocode_object::test<6>() + { + static const U32 lengths[] = {1, 2, 254, 255, 256, 300}; + for (U32 n : lengths) + { + std::vector<U8> body(n, 0); + ensureRoundTrip("wrap boundary (end-of-buffer flush)", 1, 0x00, body, n > 2); + } + } + + // decode() must also accept the "0x00 0x00 N" doubled-marker wire format + // (worth +255 zero bytes per extra marker) for compatibility with any + // encoder other than this one's chunking strategy, even though encode() + // itself never emits it. + template<> template<> + void zerocode_object::test<7>() + { + // header (1 byte, FLAG set) + [0x00 marker][0x00 extra-wrap-marker][terminator=5] + // decoded body length = 1 (marker) + 1 (extra marker) + 255 (memset) + (5 - 1) = 261 + std::vector<U8> encoded = {(U8)LLZeroCode::FLAG, 0x00, 0x00, 0x05}; + std::vector<U8> dec(1024, 0xDD); + bool overflow = false; + U32 dec_size = LLZeroCode::decode(encoded.data(), (U32)encoded.size(), + dec.data(), (U32)dec.size(), 1, overflow); + ensure("wrap format: no overflow", !overflow); + ensure_equals("wrap format: decoded size", dec_size, (U32)262); + ensure_equals("wrap format: FLAG cleared on header", dec[0], (U8)0x00); + for (U32 i = 1; i < dec_size; ++i) + { + ensure_equals("wrap format: decoded byte is zero", dec[i], (U8)0); + } + } + + // decode() is a no-op (returns 0, does not touch dst) when FLAG is not set. + template<> template<> + void zerocode_object::test<8>() + { + std::vector<U8> src = makeBuffer(6, 0x00, {1,2,3,0,0,0}); + std::vector<U8> dec(64, 0xCC); + bool overflow = false; + U32 dec_size = LLZeroCode::decode(src.data(), (U32)src.size(), + dec.data(), (U32)dec.size(), 6, overflow); + ensure_equals("not zero-coded: decode returns 0", dec_size, (U32)0); + ensure("not zero-coded: no overflow", !overflow); + } + + // encode() refuses to write into an undersized destination buffer. + template<> template<> + void zerocode_object::test<9>() + { + std::vector<U8> body(50, 0); + std::vector<U8> src = makeBuffer(1, 0, body); + std::vector<U8> enc(src.size(), 0); // smaller than the required 2 * src_size + S32 r = LLZeroCode::encode(src.data(), (U32)src.size(), enc.data(), (U32)enc.size(), 1); + ensure("encode: capacity guard rejects undersized dst", r < 0); + } + + // decode() reports overflow (rather than writing out of bounds) when dst is too small, + // whether the destination is smaller than a single byte's worth of headroom... + template<> template<> + void zerocode_object::test<10>() + { + std::vector<U8> body(500, 0); + std::vector<U8> src = makeBuffer(1, 0, body); + std::vector<U8> enc(2 * src.size() + 16, 0); + S32 enc_size = LLZeroCode::encode(src.data(), (U32)src.size(), enc.data(), (U32)enc.size(), 1); + ensure("decode overflow setup: encode succeeded", enc_size >= 0); + + std::vector<U8> dec(1, 0); // capacity == header_size exactly + bool overflow = false; + U32 dec_size = LLZeroCode::decode(enc.data(), (U32)enc_size, + dec.data(), (U32)dec.size(), 1, overflow); + ensure("decode overflow (minimal capacity): overflow flagged", overflow); + ensure("decode overflow (minimal capacity): size bounded by capacity", dec_size <= dec.size()); + } + + // ...or comfortably larger than 256 bytes but still short of the true decoded size. + template<> template<> + void zerocode_object::test<11>() + { + std::vector<U8> body(500, 0); + std::vector<U8> src = makeBuffer(1, 0, body); + std::vector<U8> enc(2 * src.size() + 16, 0); + S32 enc_size = LLZeroCode::encode(src.data(), (U32)src.size(), enc.data(), (U32)enc.size(), 1); + ensure("decode overflow setup: encode succeeded", enc_size >= 0); + + std::vector<U8> dec(300, 0); // > 256, but less than the true decoded size (~501) + bool overflow = false; + U32 dec_size = LLZeroCode::decode(enc.data(), (U32)enc_size, + dec.data(), (U32)dec.size(), 1, overflow); + ensure("decode overflow (insufficient capacity): overflow flagged", overflow); + ensure("decode overflow (insufficient capacity): size bounded by capacity", dec_size <= dec.size()); + } + + // A pathological alternating zero/non-zero pattern grows under zero-coding + // (every isolated zero costs 2 output bytes for 1 input byte), so encode + // must refuse it and leave the original buffer in use. + template<> template<> + void zerocode_object::test<12>() + { + std::vector<U8> body; + for (int i = 0; i < 200; ++i) + { + body.push_back(0); + body.push_back((U8)(i + 1)); + } + ensureRoundTrip("alternating pattern", 1, 0x00, body, false); + } +} diff --git a/indra/llplugin/llpluginprocessparent.cpp b/indra/llplugin/llpluginprocessparent.cpp index 4f55d75e7d..2029497790 100644 --- a/indra/llplugin/llpluginprocessparent.cpp +++ b/indra/llplugin/llpluginprocessparent.cpp @@ -508,6 +508,16 @@ void LLPluginProcessParent::idle(void) else if (!mProcess && !mProcessCreationRequested) { mProcessCreationRequested = true; +#if LL_DARWIN + // On darwin process should be created from main thread, otherwise it can + // cause a fork-lock deadlock. Relevant to both, boost and apr. + // Alternatively can remake process creation to use posix_spawn + if (!(mProcess = LLProcess::create(mProcessParams))) + { + mProcessCreationRequested = false; + errorState(); + } +#else LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop"); // *NOTE: main_queue->postTo casts this refcounted smart pointer to a weak // pointer @@ -558,6 +568,7 @@ void LLPluginProcessParent::idle(void) errorState(); } } +#endif } if (mProcess) diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index fafc92b5d0..81743713a1 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -2583,6 +2583,12 @@ bool LLImageGL::scaleDown(S32 desired_discard) { glDrawArrays(GL_TRIANGLES, 0, 3); +#if LL_DARWIN + // On mac OS, flush before freeing, otherwise the texture may be + // freed before Metal's 'lazy' evaluation/restoration behavior triggers. + glFlush(); +#endif + free_tex_image(mTexName); glTexImage2D(mTarget, 0, mFormatInternal, desired_width, desired_height, 0, mFormatPrimary, mFormatType, nullptr); glCopyTexSubImage2D(mTarget, 0, 0, 0, 0, 0, desired_width, desired_height); diff --git a/indra/llrender/llvertexbuffer.cpp b/indra/llrender/llvertexbuffer.cpp index 9b839350ae..2d64292fe7 100644 --- a/indra/llrender/llvertexbuffer.cpp +++ b/indra/llrender/llvertexbuffer.cpp @@ -296,18 +296,21 @@ static void delete_buffers(S32 count, GLuint* buffers) LL_PROFILE_ZONE_SCOPED_CATEGORY_VERTEX; // wait a few frames before actually deleting the buffers to avoid // synchronization issues with the GPU - static std::vector<GLuint> sFreeList[4]; + constexpr U32 BUCKET_COUNT = 4; + static std::vector<GLuint> sFreeList[BUCKET_COUNT]; if (gGLManager.mInited) { - U32 idx = LLImageGL::sFrameCount % 4; + // Move current frame to free list + U32 idx = LLImageGL::sFrameCount % BUCKET_COUNT; for (S32 i = 0; i < count; ++i) { sFreeList[idx].push_back(buffers[i]); } - idx = (LLImageGL::sFrameCount + 3) % 4; + // Clear frame -3 (equals +1), this idx will be written over on the next call + idx = (LLImageGL::sFrameCount + 1) % BUCKET_COUNT; if (!sFreeList[idx].empty()) { diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp index 5f1302df88..6588c481ff 100644 --- a/indra/llui/lltexteditor.cpp +++ b/indra/llui/lltexteditor.cpp @@ -1693,28 +1693,25 @@ void LLTextEditor::pasteTextWithLinebreaks(LLWString & clean_string) std::basic_string<llwchar>::size_type start = 0; std::basic_string<llwchar>::size_type pos = clean_string.find('\n',start); - while((pos != -1) && (pos != clean_string.length() -1)) + while (pos != std::basic_string<llwchar>::npos) { - if(pos!=start) + if (pos != start) { std::basic_string<llwchar> str = std::basic_string<llwchar>(clean_string,start,pos-start); setCursorPos(mCursorPos + insert(mCursorPos, str, true, LLTextSegmentPtr())); } - addLineBreakChar(true); // Add a line break and group with the next addition. + const bool trailing_linebreak = (pos == clean_string.length() - 1); + addLineBreakChar(!trailing_linebreak); start = pos+1; pos = clean_string.find('\n',start); } - if (pos != start) + if (start < clean_string.length()) { std::basic_string<llwchar> str = std::basic_string<llwchar>(clean_string,start,clean_string.length()-start); setCursorPos(mCursorPos + insert(mCursorPos, str, false, LLTextSegmentPtr())); } - else - { - addLineBreakChar(false); // Add a line break and end the grouping. - } } // copy selection to primary diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index bdb629172d..b33cb054d4 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -396,7 +396,7 @@ void LLWebRTCImpl::init() } -void LLWebRTCImpl::terminate() +bool LLWebRTCImpl::terminate() { // Run all blocking WebRTC shutdown calls on a separate thread so that a // hung BlockingCall cannot block the viewer shutdown indefinitely. @@ -410,16 +410,55 @@ void LLWebRTCImpl::terminate() std::vector<webrtc::scoped_refptr<LLWebRTCPeerConnectionImpl>> connections; connections.swap(mPeerConnections); + // Explicitely unregister observers before shutting down the threads. + // shutdown_thread, if detached, can outlive observer. + mWorkerThread->BlockingCall([this]() + { + if (mDeviceModule) + { + mDeviceModule->SetObserver(nullptr); + } + }); + mVoiceDevicesObserverList.clear(); + + // shutdown_thread can be detached, then LLWebRTCImpl will be nulled out. + // Capture what's needed in lambda, don't rely on [this]. std::thread shutdown_thread( - [this, connections = std::move(connections), done_promise]() mutable + [networkThread = std::move(mNetworkThread), + workerThread = std::move(mWorkerThread), + signalingThread = std::move(mSignalingThread), + deviceModule = std::move(mDeviceModule), + factory = std::move(mPeerConnectionFactory), + connections = std::move(connections), + done_promise]() mutable { - mWorkerThread->BlockingCall( - [this]() + // Stop the capture/render devices alongside the connection teardown + // below rather than ahead of it. Both of these calls end in a + // WaitForSingleObject on a WASAPI thread with a 2s timeout apiece + // (AudioDeviceWindowsCore::StopRecording / StopPlayout), so blocking on + // them here can spend most of the shutdown budget before the + // connections have been touched at all -- and after an OS sleep they + // tend to hit the full timeout. + // + // This work has to stay on the worker thread: the device module was + // created there and its AudioDeviceBuffer is guarded by a sequence + // checker bound to that thread. Posting instead of blocking lets the + // signaling close below get on with its network-thread work (data + // channel close, transport teardown) while the device stop is still + // waiting on WASAPI. + // + // No explicit join is needed: Thread's task queue is FIFO and + // BlockingCall posts through it, so the ForceTerminate call at the end + // of this lambda can't run until this task has finished. Any + // worker-thread work the signaling close does is likewise ordered + // after it, so nothing sees the device module half torn down. + workerThread->PostTask( + [&deviceModule]() { - if (mDeviceModule) + if (deviceModule) { - mDeviceModule->ForceStopRecording(); - mDeviceModule->StopPlayout(); + deviceModule->ForceStopRecording(); + deviceModule->StopPlayout(); } }); @@ -434,7 +473,7 @@ void LLWebRTCImpl::terminate() // callback inline, and that callback calls back into the viewer's // signaling observers. Those observers are only valid until // llwebrtc::terminate() returns. - mSignalingThread->BlockingCall( + signalingThread->BlockingCall( [&connections]() { for (auto& connection : connections) @@ -447,22 +486,29 @@ void LLWebRTCImpl::terminate() }); // Drain anything the closes posted before dropping the factory. - mSignalingThread->BlockingCall([]() {}); + signalingThread->BlockingCall([]() {}); - mSignalingThread->BlockingCall([this]() { - mPeerConnectionFactory = nullptr; + signalingThread->BlockingCall([&factory]() { + factory = nullptr; }); - mWorkerThread->BlockingCall( - [this]() + workerThread->BlockingCall( + [&deviceModule]() { - if (mDeviceModule) + if (deviceModule) { - mDeviceModule->ForceTerminate(); + deviceModule->ForceTerminate(); } - mDeviceModule = nullptr; + deviceModule = nullptr; }); + // Explicitly clean WebRTC threads in dependency order before signalling completion. + // The connections were closed and destroyed on the signaling thread, so it's safe + // to clean. + signalingThread.reset(); + workerThread.reset(); + networkThread.reset(); + done_promise->set_value(); }); @@ -474,29 +520,29 @@ void LLWebRTCImpl::terminate() " Detaching — some WebRTC resources will be leaked."; shutdown_thread.detach(); - // Release the unique_ptrs WITHOUT joining/deleting: the detached thread - // may still be using these thread objects. - // The raw pointers are intentionally leaked — the process is exiting anyway - // and our priority is saving cache and personal data. - (void)mNetworkThread.release(); - (void)mWorkerThread.release(); - (void)mSignalingThread.release(); - + // Leave every member exactly as it is. The detached thread is still + // running the lambda above, which reads mSignalingThread, mWorkerThread, + // mDeviceModule and mPeerConnectionFactory through `this` -- clearing or + // releasing them here would pull them out from under it mid-shutdown + // (a null mSignalingThread is an immediate segfault at the next + // BlockingCall). Instead we report the failure so the caller leaks this + // object rather than deleting it; the process is exiting anyway and our + // priority is saving cache and personal data. + // // mPeerConnections is already empty -- the detached thread owns the // connections now and must be left to finish with them. + // + // The log sink is unhooked here (and deliberately not deleted, since the + // detached thread may still log) because the viewer-side log callback + // behind it doesn't outlive this call. webrtc::LogMessage::RemoveLogToStream(mLogSink); - return; + return false; } shutdown_thread.join(); - // The connections were closed and destroyed on the signaling thread before - // the shutdown thread finished, so it's safe to drop the threads now. - mNetworkThread = nullptr; - mWorkerThread = nullptr; - mSignalingThread = nullptr; - webrtc::LogMessage::RemoveLogToStream(mLogSink); + return true; } @@ -1124,17 +1170,23 @@ void LLWebRTCPeerConnectionImpl::closeOnSignalingThread() mLocalStream = nullptr; } mPeerConnection = nullptr; + } - for (auto &observer : mSignalingObserverList) - { - observer->OnPeerConnectionClosed(); - } + // Notify unconditionally, even if there was no peer connection to close -- + // a connection can be shut down before it ever finished initializing, and + // the caller is still waiting to hear that the close is done. Withholding + // this leaves the viewer's connection state machine parked in + // VOICE_STATE_WAIT_FOR_CLOSE, which has no timeout of its own. + for (auto &observer : mSignalingObserverList) + { + observer->OnPeerConnectionClosed(); } - // Nothing may call back into the viewer past this point. On shutdown the - // viewer's connection objects are torn down as soon as llwebrtc::terminate() - // returns and they deliberately don't unset themselves as observers, so any - // late callback would be reaching into freed memory. + // Nothing may call back into the viewer past this point. Connections + // closed while the viewer is still running unset themselves as observers + // when they're destroyed, but any that are left for llwebrtc::terminate() + // to close deliberately don't -- they're torn down as soon as it returns, + // so a late callback would be reaching into freed memory. mSignalingObserverList.clear(); mDataObserverList.clear(); } @@ -1759,20 +1811,20 @@ void LLWebRTCPeerConnectionImpl::OnStateChange() switch (mDataChannel->state()) { case webrtc::DataChannelInterface::kOpen: - RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State Open"; + RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State Open"; for (auto &observer : mSignalingObserverList) { observer->OnDataChannelReady(this); } break; case webrtc::DataChannelInterface::kConnecting: - RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State Connecting"; + RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State Connecting"; break; case webrtc::DataChannelInterface::kClosing: - RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State closing"; + RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State closing"; break; case webrtc::DataChannelInterface::kClosed: - RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State closed"; + RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State closed"; break; default: break; @@ -1931,8 +1983,14 @@ void terminate() { if (gWebRTCImpl) { - gWebRTCImpl->terminate(); - delete gWebRTCImpl; + if (gWebRTCImpl->terminate()) + { + delete gWebRTCImpl; + } + // Otherwise shutdown timed out and was left to a detached thread that is + // still using this object -- and the webrtc threads it owns -- so it's + // intentionally leaked. Deleting it would hand that thread a freed + // object to finish shutting down with. gWebRTCImpl = nullptr; } } diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index cee42cf19a..ae3a6a4903 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -81,19 +81,28 @@ public: { if (mCallback) { + // RTC_LOG prefixes each message with its "(file:line): " origin. Use it to demote + // libwebrtc's own chatty INFO/WARNING logging to verbose, keeping ours as-is. + static const std::string file_prefix("(llwebrtc.cpp:"); switch (severity) { case webrtc::LS_VERBOSE: mCallback->LogMessage(LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, msg); break; case webrtc::LS_INFO: - mCallback->LogMessage(LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, msg); + mCallback->LogMessage(msg.rfind(file_prefix, 0) == 0 + ? LLWebRTCLogCallback::LOG_LEVEL_INFO + : LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, + msg); break; case webrtc::LS_WARNING: - mCallback->LogMessage(LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, msg); + mCallback->LogMessage(msg.rfind(file_prefix, 0) == 0 + ? LLWebRTCLogCallback::LOG_LEVEL_WARNING + : LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, + msg); break; case webrtc::LS_ERROR: - mCallback->LogMessage(LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, msg); + mCallback->LogMessage(LLWebRTCLogCallback::LOG_LEVEL_ERROR, msg); break; default: break; @@ -413,7 +422,11 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO } void init(); - void terminate(); + // Returns true if shutdown completed cleanly and this object may be + // destroyed. Returns false if it timed out: a detached thread is still + // using this object and its webrtc threads, so it must be leaked, not + // deleted. + bool terminate(); // // LLWebRTCDeviceInterface diff --git a/indra/llxml/llcontrol.cpp b/indra/llxml/llcontrol.cpp index 84d03a6d0c..eccf672e46 100644 --- a/indra/llxml/llcontrol.cpp +++ b/indra/llxml/llcontrol.cpp @@ -192,20 +192,6 @@ LLSD LLControlVariable::getComparableValue(const LLSD& value) storable_value = false; } } - else if (TYPE_LLSD == type() && value.isString()) - { - LLPointer<LLSDNotationParser> parser = new LLSDNotationParser; - LLSD result; - std::stringstream value_stream(value.asString()); - if (parser->parse(value_stream, result, LLSDSerialize::SIZE_UNLIMITED) != LLSDParser::PARSE_FAILURE) - { - storable_value = result; - } - else - { - storable_value = value; - } - } else { storable_value = value; @@ -735,6 +721,30 @@ void LLControlGroup::setLLSD(std::string_view name, const LLSD& val) set(name, val); } +bool LLControlVariable::setValueFromNotation(const std::string& notation, bool saved_value) +{ + if (mType == TYPE_LLSD) + { + LLPointer<LLSDNotationParser> parser = new LLSDNotationParser; + LLSD result; + std::stringstream value_stream(notation); + S32 parse_count = parser->parse(value_stream, result, LLSDSerialize::SIZE_UNLIMITED); + if (parse_count != LLSDParser::PARSE_FAILURE) + { + setValue(result, saved_value); + return true; + } + LL_WARNS("Controls") << "Failed to parse LLSD notation for control '" + << mName << "': " << notation << LL_ENDL; + } + else + { + LL_WARNS("Controls") << "setValueFromNotation() called on non-LLSD control '" + << mName << "' (type " << LLControlGroup::typeEnumToString(mType) << "); ignoring." << LL_ENDL; + } + return false; +} + void LLControlGroup::setUntypedValue(std::string_view name, const LLSD& val) { if (name.empty()) diff --git a/indra/llxml/llcontrol.h b/indra/llxml/llcontrol.h index ad283c89b1..7f28c71232 100644 --- a/indra/llxml/llcontrol.h +++ b/indra/llxml/llcontrol.h @@ -127,6 +127,7 @@ public: void setPersist(ePersist); void setHiddenFromSettingsEditor(bool hide); void setComment(const std::string& comment); + bool setValueFromNotation(const std::string& notation, bool saved_value = true); private: void firePropertyChanged(const LLSD &pPreviousValue) diff --git a/indra/newview/VIEWER_VERSION.txt b/indra/newview/VIEWER_VERSION.txt index c4697fd566..bb6eac98a9 100644 --- a/indra/newview/VIEWER_VERSION.txt +++ b/indra/newview/VIEWER_VERSION.txt @@ -1 +1 @@ -26.3.0 +26.4.0 diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index b23acc6bd1..ad307f9648 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -16447,7 +16447,7 @@ <key>Persist</key> <integer>1</integer> <key>Type</key> - <string>Boolean</string> + <string>U32</string> <key>Value</key> <integer>0</integer> </map> diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 03c8d51168..a273e36b58 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -2182,6 +2182,7 @@ bool LLAppViewer::cleanup() LLWorld::deleteSingleton(); LLVoiceClient::deleteSingleton(); LLUI::deleteSingleton(); + LLGridManager::deleteSingleton(); LLWatchdog::deleteSingleton(); // It's not at first obvious where, in this long sequence, a generic cleanup @@ -3016,6 +3017,8 @@ bool LLAppViewer::initConfiguration() } } + LLGridManager::createInstance(); + LLSLURL start_slurl; if (!starting_location.empty()) { @@ -3064,6 +3067,7 @@ bool LLAppViewer::initConfiguration() // Do not save settings. // Might be smarter to have an exit code for a more reliable // "early exit, needs cleanup" case. + LLGridManager::deleteSingleton(); LLSingletonBase::deleteAll(); cleanupConsole(); delete mSettingsLocationList; @@ -3124,6 +3128,7 @@ bool LLAppViewer::initConfiguration() // Since returning 'false' is basically an error without cleanup, // do cleanup here. No need to worry about marker files here. + LLGridManager::deleteSingleton(); LLSingletonBase::deleteAll(); cleanupConsole(); return false; @@ -3889,8 +3894,12 @@ void LLAppViewer::writeSystemInfo() gDebugInfo["CPUInfo"]["CPUFamily"] = gSysCPU.getFamily(); gDebugInfo["CPUInfo"]["CPUMhz"] = (S32)gSysCPU.getMHz(); gDebugInfo["CPUInfo"]["CPUAltivec"] = gSysCPU.hasAltivec(); - gDebugInfo["CPUInfo"]["CPUSSE"] = gSysCPU.hasSSE(); - gDebugInfo["CPUInfo"]["CPUSSE2"] = gSysCPU.hasSSE2(); + gDebugInfo["CPUInfo"]["CPUSSE42"] = gSysCPU.hasSSE42(); + gDebugInfo["CPUInfo"]["CPUSSE4a"] = gSysCPU.hasSSE4a(); + gDebugInfo["CPUInfo"]["CPUAVX"] = gSysCPU.hasAVX(); + gDebugInfo["CPUInfo"]["CPUAVX2"] = gSysCPU.hasAVX2(); + gDebugInfo["CPUInfo"]["CPUAVX512F"] = gSysCPU.hasAVX512F(); + gDebugInfo["RAMInfo"]["Physical"] = LLSD::Integer(gSysMemory.getPhysicalMemoryKB().value()); gDebugInfo["RAMInfo"]["Allocated"] = LLSD::Integer(gMemoryAllocated.valueInUnits<LLUnits::Kilobytes>()); diff --git a/indra/newview/llavataractions.cpp b/indra/newview/llavataractions.cpp index c95e98982d..b6d59a4a39 100644 --- a/indra/newview/llavataractions.cpp +++ b/indra/newview/llavataractions.cpp @@ -337,6 +337,17 @@ void LLAvatarActions::showProfile(const LLUUID& avatar_id) } } +bool LLAvatarActions::myProfileVisible() +{ + LLFloater* floaterp = getProfileFloater(gAgentID); + return floaterp && floaterp->isInVisibleChain(); +} + +bool LLAvatarActions::myPicksTabVisible() +{ + return myProfileVisible() && isPickTabSelected(gAgentID); +} + // static void LLAvatarActions::showPicks(const LLUUID& avatar_id) { diff --git a/indra/newview/llavataractions.h b/indra/newview/llavataractions.h index 1f5a42ed22..90f1e96fb7 100644 --- a/indra/newview/llavataractions.h +++ b/indra/newview/llavataractions.h @@ -109,6 +109,12 @@ public: static LLFloater* getProfileFloater(const LLUUID& avatar_id); /** + * Profile helpers. + */ + static bool myProfileVisible(); + static bool myPicksTabVisible(); + + /** * Show avatar on world map. */ static void showOnMap(const LLUUID& id); diff --git a/indra/newview/llcommandlineparser.cpp b/indra/newview/llcommandlineparser.cpp index 0734b12531..84d3ff90d5 100644 --- a/indra/newview/llcommandlineparser.cpp +++ b/indra/newview/llcommandlineparser.cpp @@ -611,6 +611,11 @@ void setControlValueCB(const LLCommandLineParser::token_vector_t& value, ctrl->setValue(llsdArray, false); } + else if (ctrl->isType(TYPE_LLSD)) + { + // Command-line LLSD should support a notation format string + ctrl->setValueFromNotation(onevalue(option, value), false); + } else { ctrl->setValue(onevalue(option, value), false); diff --git a/indra/newview/llinventorybridge.cpp b/indra/newview/llinventorybridge.cpp index a8e8f122e1..cb067ef011 100644 --- a/indra/newview/llinventorybridge.cpp +++ b/indra/newview/llinventorybridge.cpp @@ -7214,8 +7214,8 @@ void LLObjectBridge::performAction(LLInventoryModel* model, std::string action) item = (LLViewerInventoryItem*)gInventory.getItem(object_id); if(item && gInventory.isObjectDescendentOf(object_id, gInventory.getRootFolderID())) { - static LLCachedControl<bool> replace_item(gSavedSettings, "InventoryAddAttachmentBehavior", false); - rez_attachment(item, NULL, ("attach" == action) ? replace_item() : true); // Replace if "Wear"ing. + static LLCachedControl<U32> add_attachment_behavior(gSavedSettings, "InventoryAddAttachmentBehavior", 0); + rez_attachment(item, NULL, ("attach" == action) ? (add_attachment_behavior() == 1) : true); // Replace if "Wear"ing. } else if(item && item->isFinished()) { @@ -8372,8 +8372,8 @@ void LLObjectBridgeAction::attachOrDetach() } else { - static LLCachedControl<bool> inventory_linking(gSavedSettings, "InventoryAddAttachmentBehavior", false); - LLAppearanceMgr::instance().wearItemOnAvatar(mUUID, true, inventory_linking()); // Don't replace if adding. + static LLCachedControl<U32> add_attachment_behavior(gSavedSettings, "InventoryAddAttachmentBehavior", 0); + LLAppearanceMgr::instance().wearItemOnAvatar(mUUID, true, add_attachment_behavior() == 1); // Don't replace if adding. } } diff --git a/indra/newview/lltexturefetch.cpp b/indra/newview/lltexturefetch.cpp index 144940c705..a0d7aac3f3 100644 --- a/indra/newview/lltexturefetch.cpp +++ b/indra/newview/lltexturefetch.cpp @@ -2527,18 +2527,26 @@ S32 LLTextureFetch::createRequest(FTType f_type, const std::string& url, const L { LL_DEBUGS("Avatar") << " requesting " << id << " " << w << "x" << h << " discard " << desired_discard << " type " << f_type << LL_ENDL; } - LLTextureFetchWorker* worker = getWorker(id); + + // Potentially we might remove a request, lock queue here instead + // of getWorker to make sure request will persist till removeRequest + lockQueue(); + LLTextureFetchWorker* worker = getWorkerAfterLock(id); if (worker) { if (worker->mHost != host) { LL_WARNS(LOG_TXT) << "LLTextureFetch::createRequest " << id << " called with multiple hosts: " << host << " != " << worker->mHost << LL_ENDL; - removeRequest(worker, true); + size_t erased_1 = mRequestMap.erase(worker->mID); + llassert_always(erased_1 > 0); + unlockQueue(); + worker->scheduleDelete(); worker = NULL; return CREATE_REQUEST_ERROR_MHOSTS; } } + unlockQueue(); S32 desired_size; std::string exten = gDirUtilp->getExtension(url); @@ -2717,9 +2725,11 @@ void LLTextureFetch::deleteAllRequests() } LLTextureFetchWorker* worker = mRequestMap.begin()->second; - unlockQueue() ; + size_t erased_1 = mRequestMap.erase(worker->mID); + llassert_always(erased_1 > 0); + unlockQueue(); - removeRequest(worker, true); + worker->scheduleDelete(); } } diff --git a/indra/newview/llviewercontrollistener.cpp b/indra/newview/llviewercontrollistener.cpp index 6f77e21fcc..f8e829deb9 100644 --- a/indra/newview/llviewercontrollistener.cpp +++ b/indra/newview/llviewercontrollistener.cpp @@ -65,6 +65,11 @@ LLViewerControlListener::LLViewerControlListener() grouphelp + replyhelp, &LLViewerControlListener::set, LLSDMap("group", LLSD())("key", LLSD())); + add("set_notation", + std::string("Set [\"group\"] TYPE_LLSD control [\"key\"] by parsing [\"value\"] as LLSD notation.\n") + + grouphelp + replyhelp, + &LLViewerControlListener::set_notation, + LLSDMap("group", LLSD())("key", LLSD())("value", LLSD())); add("toggle", std::string("Toggle [\"group\"] control [\"key\"], if boolean\n") + grouphelp + replyhelp, &LLViewerControlListener::toggle, @@ -150,6 +155,36 @@ void LLViewerControlListener::set(LLSD const & request) } //static +void LLViewerControlListener::set_notation(LLSD const& request) +{ + Info info(request); + if (!info.control) + return; + + if (!info.control->isType(TYPE_LLSD)) + { + info.response.error(STRINGIZE("set_notation requires a TYPE_LLSD control, but '" + << info.key << "' in group '" << info.groupname + << "' has type " + << LLControlGroup::typeEnumToString(info.control->type()))); + return; + } + + if (!request["value"].isString()) + { + info.response.error(STRINGIZE("set_notation requires [\"value\"] to be a string " + "containing LLSD notation, but received type: " + << LLSD::typeString(request["value"].type()))); + return; + } + + if (!info.control->setValueFromNotation(request["value"].asString())) + { + info.response.error(STRINGIZE("set_notation failed to parse notation")); + } +} + +//static void LLViewerControlListener::toggle(LLSD const & request) { Info info(request); diff --git a/indra/newview/llviewercontrollistener.h b/indra/newview/llviewercontrollistener.h index 346acac439..c5a3a31baf 100644 --- a/indra/newview/llviewercontrollistener.h +++ b/indra/newview/llviewercontrollistener.h @@ -41,6 +41,7 @@ public: private: static void set(LLSD const & event_data); + static void set_notation(LLSD const & event_data); static void toggle(LLSD const & event_data); static void get(LLSD const & event_data); static void groups(LLSD const & event_data); diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index 13d57a4ad2..87a891915e 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -4054,17 +4054,6 @@ void handle_avatar_eject(const LLSD& avatar_id) } } -bool my_profile_visible() -{ - LLFloater* floaterp = LLAvatarActions::getProfileFloater(gAgentID); - return floaterp && floaterp->isInVisibleChain(); -} - -bool picks_tab_visible() -{ - return my_profile_visible() && LLAvatarActions::isPickTabSelected(gAgentID); -} - bool enable_freeze_eject(const LLSD& avatar_id) { // Use avatar_id if available, otherwise default to right-click avatar @@ -6820,7 +6809,7 @@ class LLAvatarTogglePicks : public view_listener_t instance->setFocus(true); LLAvatarActions::showPicks(gAgent.getID()); } - else if (picks_tab_visible()) + else if (LLAvatarActions::myPicksTabVisible()) { instance->closeFloater(); } @@ -9898,6 +9887,23 @@ void show_topinfobar_context_menu(LLView* ctrl, S32 x, S32 y) LLMenuGL::showPopup(ctrl, show_topbarinfo_context_menu, x, y); } +static void register_agent_ui_callbacks() +{ + // These functions are used by menu and commands, they need to persist for the lifetime of the application + // Note: executes before LLToolBar::createButton and before menu builds + LLUICtrl::EnableCallbackRegistry::Registrar& global_enable = LLUICtrl::EnableCallbackRegistry::defaultRegistrar(); + LLUICtrl::CommitCallbackRegistry::Registrar& global_commit = LLUICtrl::CommitCallbackRegistry::defaultRegistrar(); + global_commit.add("Agent.toggleFlying", boost::bind(&LLAgent::toggleFlying)); + global_enable.add("Agent.enableFlyLand", boost::bind(&enable_fly_land)); + global_commit.add("Agent.PressMicrophone", boost::bind(&LLAgent::pressMicrophone, _2)); + global_commit.add("Agent.ReleaseMicrophone", boost::bind(&LLAgent::releaseMicrophone, _2)); + global_commit.add("Agent.ToggleMicrophone", boost::bind(&LLAgent::toggleMicrophone, _2)); + global_enable.add("Agent.IsMicrophoneOn", boost::bind(&LLAgent::isMicrophoneOn, _2)); + global_enable.add("Agent.IsActionAllowed", boost::bind(&LLAgent::isActionAllowed, _2)); + global_enable.add("Avatar.IsMyProfileOpen", boost::bind(&LLAvatarActions::myProfileVisible)); + global_enable.add("Avatar.IsPicksTabOpen", boost::bind(&LLAvatarActions::myPicksTabVisible)); +} + void initialize_edit_menu() { view_listener_t::addMenu(new LLEditUndo(), "Edit.Undo"); @@ -9955,6 +9961,8 @@ void initialize_menus() bool mMult; }; + register_agent_ui_callbacks(); + LLUICtrl::EnableCallbackRegistry::Registrar& enable = LLUICtrl::EnableCallbackRegistry::currentRegistrar(); LLUICtrl::CommitCallbackRegistry::Registrar& commit = LLUICtrl::CommitCallbackRegistry::currentRegistrar(); @@ -9970,15 +9978,6 @@ void initialize_menus() enable.add("Conversation.IsConversationLoggingAllowed", boost::bind(&LLFloaterIMContainer::isConversationLoggingAllowed)); - // Agent - commit.add("Agent.toggleFlying", boost::bind(&LLAgent::toggleFlying)); - enable.add("Agent.enableFlyLand", boost::bind(&enable_fly_land)); - commit.add("Agent.PressMicrophone", boost::bind(&LLAgent::pressMicrophone, _2)); - commit.add("Agent.ReleaseMicrophone", boost::bind(&LLAgent::releaseMicrophone, _2)); - commit.add("Agent.ToggleMicrophone", boost::bind(&LLAgent::toggleMicrophone, _2)); - enable.add("Agent.IsMicrophoneOn", boost::bind(&LLAgent::isMicrophoneOn, _2)); - enable.add("Agent.IsActionAllowed", boost::bind(&LLAgent::isActionAllowed, _2)); - // File menu init_menu_file(); @@ -10346,8 +10345,6 @@ void initialize_menus() view_listener_t::addMenu(new LLAvatarResetSkeletonAndAnimations(), "Avatar.ResetSkeletonAndAnimations"); view_listener_t::addMenu(new LLAvatarResetSelfSkeleton(), "Avatar.ResetSelfSkeleton"); view_listener_t::addMenu(new LLAvatarResetSelfSkeletonAndAnimations(), "Avatar.ResetSelfSkeletonAndAnimations"); - enable.add("Avatar.IsMyProfileOpen", boost::bind(&my_profile_visible)); - enable.add("Avatar.IsPicksTabOpen", boost::bind(&picks_tab_visible)); commit.add("Avatar.OpenMarketplace", boost::bind(&LLWeb::loadURLExternal, gSavedSettings.getString("MarketplaceURL"))); diff --git a/indra/newview/llviewernetwork.h b/indra/newview/llviewernetwork.h index 0937425a18..18c23cf7ff 100644 --- a/indra/newview/llviewernetwork.h +++ b/indra/newview/llviewernetwork.h @@ -57,13 +57,18 @@ protected: * This class maintains the currently selected grid, and provides short * form accessors for each of the properties of the selected grid. **/ -class LLGridManager : public LLSingleton<LLGridManager> +class LLGridManager : public LLSimpleton<LLGridManager> { +public: /// Instantiate the grid manager, load default grids, selects the default grid - LLSINGLETON(LLGridManager); + LLGridManager(); ~LLGridManager(); - public: + LLGridManager(const LLGridManager&) = delete; + LLGridManager(LLGridManager&&) = delete; + LLGridManager& operator=(const LLGridManager&) = delete; + LLGridManager& operator=(LLGridManager&&) = delete; + /* ================================================================ * @name Initialization and Configuration * @{ diff --git a/indra/newview/llviewerparcelmgr.cpp b/indra/newview/llviewerparcelmgr.cpp index b794047135..63eb08e4ae 100644 --- a/indra/newview/llviewerparcelmgr.cpp +++ b/indra/newview/llviewerparcelmgr.cpp @@ -309,6 +309,14 @@ void LLViewerParcelMgr::resetSegments(U8* segments) void LLViewerParcelMgr::writeHighlightSegments(F32 west, F32 south, F32 east, F32 north) { + if (west < 0 || south < 0 || east < 0 || north < 0) + { + LL_WARNS() << "Invalid parcel edge coordinates provided to writeHighlightSegments- " + << "west: " << west << ", south: " << south << ", east: " << east << ", north: " << north + << LL_ENDL; + return; + } + S32 x, y; S32 min_x = ll_round( west / PARCEL_GRID_STEP_METERS ); S32 max_x = ll_round( east / PARCEL_GRID_STEP_METERS ); diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp index a4298accc1..c37daa53a3 100644 --- a/indra/newview/llviewerregion.cpp +++ b/indra/newview/llviewerregion.cpp @@ -1891,8 +1891,17 @@ LLViewerObject* LLViewerRegion::addNewObject(LLVOCacheEntry* entry) } else { - LLViewerRegion* old_regionp = ((LLDrawable*)entry->getEntry()->getDrawable())->getRegion(); - if(old_regionp != this) + LLDrawable* drawablep = (LLDrawable*)entry->getEntry()->getDrawable(); + if (!drawablep || drawablep->isDead() || drawablep->getVObj().isNull()) + { + LL_WARNS() << "Entry: " << entry->getLocalID() << " has a dead or invalid drawable; resetting to inactive." << LL_ENDL; + mImpl->mVisibleEntries.erase(entry); + entry->setState(LLVOCacheEntry::INACTIVE); + return NULL; + } + + LLViewerRegion* old_regionp = drawablep->getRegion(); + if (old_regionp != this) { //this object exists in two regions at the same time; //this case can be safely ignored here because diff --git a/indra/newview/llviewerstats.cpp b/indra/newview/llviewerstats.cpp index 45f7058acb..742ff95f26 100644 --- a/indra/newview/llviewerstats.cpp +++ b/indra/newview/llviewerstats.cpp @@ -716,6 +716,8 @@ void send_viewer_stats(bool include_preferences) system["os"] = LLOSInfo::instance().getOSStringSimple(); system["cpu"] = gSysCPU.getCPUString(); system["cpu_sse"] = gSysCPU.getSSEVersions(); + system["cpu_simd"] = gSysCPU.getSIMDVersions(); + system["cpu_mhz"] = gSysCPU.getMHz(); system["address_size"] = ADDRESS_SIZE; system["os_bitness"] = LLOSInfo::instance().getOSBitness(); system["hardware_concurrency"] = (LLSD::Integer) std::thread::hardware_concurrency(); @@ -738,6 +740,19 @@ void send_viewer_stats(bool include_preferences) system["gpu_vendor"] = gGLManager.mGLVendorShort; system["gpu_version"] = gGLManager.mDriverVersionVendorString; system["opengl_version"] = gGLManager.mGLVersionString; + system["gpu_vram_mb"] = (S32)gGLManager.mVRAM; + system["glsl_version"] = llformat("%d.%d", gGLManager.mGLSLVersionMajor, gGLManager.mGLSLVersionMinor); + + // Resource usage + // getAvailableMemKB might be a bit stale, but that's fine, this is statistics, + // not a debug tool. + // TODO: 26.3 branch introduced getAvailableCommitMemMB, use that on windows + system["ram_avail_mb"] = (S32Megabytes)(LLMemory::getAvailableMemKB()).value(); + system["ram_allocated_mb"] = (S32Megabytes)(LLMemory::getAllocatedMemKB()).value(); + static constexpr F64 BYTES_TO_MB = 1024.0 * 1024.0; + F64 texture_bytes_alloc = LLImageGL::getTextureBytesAllocated() / BYTES_TO_MB; + F64 vertex_bytes_alloc = LLVertexBuffer::getBytesAllocated() / BYTES_TO_MB; + system["gpu_vram_tracked_mb"] = (F32)(texture_bytes_alloc + vertex_bytes_alloc); gGLManager.asLLSD(system["gl"]); diff --git a/indra/newview/llvoiceclient.cpp b/indra/newview/llvoiceclient.cpp index b91b4b180b..e005e93cfb 100644 --- a/indra/newview/llvoiceclient.cpp +++ b/indra/newview/llvoiceclient.cpp @@ -120,7 +120,7 @@ std::string LLVoiceClientStatusObserver::status2string(LLVoiceClientStatusObserv LLVoiceModuleInterface *getVoiceModule(const std::string &voice_server_type) { #ifndef DISABLE_WEBRTC - if (voice_server_type == WEBRTC_VOICE_SERVER_TYPE) + if (voice_server_type == WEBRTC_VOICE_SERVER_TYPE || voice_server_type.empty()) { return (LLVoiceModuleInterface *) LLWebRTCVoiceClient::getInstance(); } @@ -972,7 +972,7 @@ class LLViewerRequiredVoiceVersion : public LLHTTPNode LLVoiceModuleInterface *voiceModule = NULL; #ifndef DISABLE_WEBRTC - if (voice_server_type == "webrtc" || voice_server_type.empty()) + if (voice_server_type == WEBRTC_VOICE_SERVER_TYPE || voice_server_type.empty()) { voiceModule = (LLVoiceModuleInterface *) LLWebRTCVoiceClient::getInstance(); } diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index 9b4a371cec..c82a005a77 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -208,6 +208,7 @@ LLSD LLVoiceWebRTCStats::read() /////////////////////////////////////////////////////////////////////////////////////////////// bool LLWebRTCVoiceClient::sShuttingDown = false; +bool LLWebRTCVoiceClient::sWebRTCTerminated = false; LLWebRTCVoiceClient::LLWebRTCVoiceClient() : mHidden(false), @@ -233,6 +234,7 @@ LLWebRTCVoiceClient::LLWebRTCVoiceClient() : mWebRTCDeviceInterface(nullptr) { sShuttingDown = false; + sWebRTCTerminated = false; mSpeakerVolume = 0.0; @@ -301,11 +303,81 @@ void LLWebRTCVoiceClient::terminate() mVoiceEnabled = false; sShuttingDown = true; // so that coroutines won't post more work. + + drainConnections(); + + sWebRTCTerminated = true; llwebrtc::terminate(); mWebRTCDeviceInterface = nullptr; } +// Close the live peer connections before handing control to +// llwebrtc::terminate(). +// +// Anything still open when terminate() runs gets closed inline and serially on +// the signaling thread, under a single 10s budget that is also paying for the +// audio device shutdown. An estate session can hold ten live connections -- +// the current region plus up to eight neighbours, plus any group or ad-hoc +// session -- each needing a full DTLS/SCTP teardown, so that budget is not +// generous. Closing them here lets the teardown proceed asynchronously on the +// signaling thread while this thread keeps pumping. +// +// It also quiets the per-connection stats poll before terminate() runs. A +// GetStats request left in flight makes PeerConnection::Close() block in +// RTCStatsCollector::WaitForPendingRequest(), which waits on the network thread +// with no timeout at all. +// +// Best effort: whatever hasn't closed by the deadline is left to +// llwebrtc::terminate(), exactly as before. +void LLWebRTCVoiceClient::drainConnections() +{ + // Marks every session and connection as shutting down. This also stops + // estateSessionState::processConnectionStates() from spinning up + // replacement connections to neighbouring regions while we drain. + sessionState::for_each(boost::bind(predShutdownSession, _1)); + + // Long enough for a local close to complete, short enough that a wedged + // connection doesn't noticeably delay quitting. The remaining budget in + // llwebrtc::terminate() is the real backstop. + constexpr F32 DRAIN_TIMEOUT_SECONDS = 3.0f; + constexpr U32 DRAIN_POLL_MS = 10; + + // Wait on the peer connections being closed rather than on the sessions + // being reaped. A connection that still has an HTTP coroutine in flight + // holds its session alive until mOutstandingRequests unwinds, and those + // coroutines don't run from here -- but its peer connection has already + // been closed by then, which is all terminate() cares about. + LLTimer timer; + while (!sessionState::allSessionsClosed() && timer.getElapsedTimeF32() < DRAIN_TIMEOUT_SECONDS) + { + // OnPeerConnectionClosed comes back through the main queue, so it has + // to be pumped or the state machines never see connections finish. + if (auto main_queue = mMainQueue.lock()) + { + main_queue->runFor(std::chrono::milliseconds(DRAIN_POLL_MS)); + } + sessionState::processSessionStates(); + + if (!sessionState::allSessionsClosed()) + { + ms_sleep(DRAIN_POLL_MS); + } + } + + if (!sessionState::allSessionsClosed()) + { + LL_WARNS("Voice") << "Timed out draining voice connections after " + << DRAIN_TIMEOUT_SECONDS + << "s; leaving the rest to llwebrtc::terminate()." << LL_ENDL; + } + else + { + LL_INFOS("Voice") << "Voice connections drained in " + << timer.getElapsedTimeF32() << "s." << LL_ENDL; + } +} + //--------------------------------------------------- void LLWebRTCVoiceClient::cleanUp() @@ -1890,6 +1962,7 @@ void LLWebRTCVoiceClient::userAuthorized(const std::string& user_id, const LLUUI if (sShuttingDown) { sShuttingDown = false; // was terminated, restart + sWebRTCTerminated = false; initWebRTC(); } } @@ -2173,6 +2246,30 @@ void LLWebRTCVoiceClient::sessionState::processSessionStates() } } +bool LLWebRTCVoiceClient::sessionState::allConnectionsClosed() const +{ + for (const auto &connection : mWebRTCConnections) + { + if (!connection->isClosed()) + { + return false; + } + } + return true; +} + +bool LLWebRTCVoiceClient::sessionState::allSessionsClosed() +{ + for (const auto &session : sSessions) + { + if (session.second && !session.second->allConnectionsClosed()) + { + return false; + } + } + return true; +} + // process the states on each connection associated with a session. bool LLWebRTCVoiceClient::sessionState::processConnectionStates() { @@ -2425,12 +2522,18 @@ LLVoiceWebRTCConnection::LLVoiceWebRTCConnection(const LLUUID ®ionID, const s LLVoiceWebRTCConnection::~LLVoiceWebRTCConnection() { - if (LLWebRTCVoiceClient::isShuttingDown()) + if (LLWebRTCVoiceClient::isWebRTCTerminated()) { - // peer connection and observers will be cleaned up - // by llwebrtc::terminate() on shutdown. + // peer connection and observers have already been cleaned up + // by llwebrtc::terminate(). return; } + // Note this is deliberately keyed off isWebRTCTerminated() rather than + // isShuttingDown(): connections drained by drainConnections() are destroyed + // while the webrtc library is still fully alive, and must unregister + // themselves and release the peer connection like any other close. Leaving + // a freed observer registered would hand llwebrtc::terminate() a dangling + // pointer to call OnPeerConnectionClosed() on. mWebRTCPeerConnectionInterface->unsetSignalingObserver(this); llwebrtc::freePeerConnection(mWebRTCPeerConnectionInterface); } @@ -3092,8 +3195,10 @@ bool LLVoiceWebRTCConnection::connectionStateMachine() } else { - // llwebrtc::terminate() is already shuting down the connection. - setVoiceConnectionState(VOICE_STATE_WAIT_FOR_CLOSE); + // Shutting down: skip the courtesy logout to the sim (the HTTP + // round trip would just delay quitting) and go straight to + // dropping the webrtc connection. + setVoiceConnectionState(VOICE_STATE_SESSION_EXIT); } break; @@ -3104,11 +3209,10 @@ bool LLVoiceWebRTCConnection::connectionStateMachine() { setVoiceConnectionState(VOICE_STATE_WAIT_FOR_CLOSE); mOutstandingRequests++; - if (!LLWebRTCVoiceClient::isShuttingDown()) - { - mWebRTCPeerConnectionInterface->shutdownConnection(); - } - // else was already posted by llwebrtc::terminate(). + // Always drop the connection ourselves, including during shutdown: + // drainConnections() runs before llwebrtc::terminate(), so nothing + // else has posted the close yet. + mWebRTCPeerConnectionInterface->shutdownConnection(); break; } diff --git a/indra/newview/llvoicewebrtc.h b/indra/newview/llvoicewebrtc.h index 818bce5bc3..ec01b8c08c 100644 --- a/indra/newview/llvoicewebrtc.h +++ b/indra/newview/llvoicewebrtc.h @@ -79,6 +79,12 @@ public: static bool isShuttingDown() { return sShuttingDown; } + // True once llwebrtc::terminate() has been entered. Between + // isShuttingDown() and this, the webrtc library is still fully alive and + // connections must still release their peer connections normally -- see + // drainConnections() and ~LLVoiceWebRTCConnection(). + static bool isWebRTCTerminated() { return sWebRTCTerminated; } + const LLVoiceVersionInfo& getVersion() override; void updateVersion(); @@ -306,6 +312,9 @@ public: bool isEmpty() { return mWebRTCConnections.empty(); } + bool allConnectionsClosed() const; + static bool allSessionsClosed(); + virtual bool isSpatial() = 0; virtual bool isEstate() = 0; virtual bool isCallbackPossible() = 0; @@ -455,6 +464,10 @@ private: /// Clean up objects created during a voice session. void cleanUp(); + /// Close the live peer connections before handing off to + /// llwebrtc::terminate(). Bounded and best effort. + void drainConnections(); + LL::WorkQueue::weak_t mMainQueue; F32 mTuningMicGain; @@ -539,6 +552,7 @@ private: // These variables can last longer than WebRTC in coroutines so we need them as static static bool sShuttingDown; + static bool sWebRTCTerminated; LLEventMailDrop mWebRTCPump; @@ -643,6 +657,12 @@ class LLVoiceWebRTCConnection : return mShutDown; } + // True once the webrtc peer connection has finished closing. The + // connection object can outlive this while it waits for outstanding + // requests to unwind, so this -- not reaping -- is what drainConnections() + // waits on. + bool isClosed() const { return mVoiceConnectionState == VOICE_STATE_CLOSED; } + void OnVoiceConnectionRequestSuccess(const LLSD &body); void resetConnectionStats(); diff --git a/indra/newview/llwindowlistener.cpp b/indra/newview/llwindowlistener.cpp index 54eb61a6bf..69160bcd6e 100644 --- a/indra/newview/llwindowlistener.cpp +++ b/indra/newview/llwindowlistener.cpp @@ -120,6 +120,22 @@ LLWindowListener::LLWindowListener(LLViewerWindow *window, const KeyboardGetter& "Paste specified [\"text\"] into the current edit field\n" "Optional [\"path\"] specifies target UI element (must be focusable).", &LLWindowListener::pasteText); + add("cut", + "Cut selected content from the focused edit field.\n" + "Optional [\"path\"] specifies target UI element (must be focusable).", + &LLWindowListener::cut); + add("copy", + "Copy selected content from the focused edit field.\n" + "Optional [\"path\"] specifies target UI element (must be focusable).", + &LLWindowListener::copy); + add("paste", + "Paste clipboard contents into the focused edit field.\n" + "Optional [\"path\"] specifies target UI element (must be focusable).", + &LLWindowListener::paste); + add("selectAll", + "Select all content in the focused edit field.\n" + "Optional [\"path\"] specifies target UI element (must be focusable).", + &LLWindowListener::selectAll); } template <typename MAPPED> @@ -657,3 +673,77 @@ void LLWindowListener::pasteText(LLSD const & evt) LLClipboard::instance().reset(); } } + +// Helper: optionally focus a view by path; returns false and sets response error on failure. +static bool focusViewByPath(const LLSD& evt, LLEventAPI::Response& response) +{ + if (!evt.has("path")) + return true; + + std::string path(evt["path"]); + LLView* target_view = LLUI::getInstance()->resolvePath(LLUI::getInstance()->getRootView(), path); + if (!target_view) + { + response.error(STRINGIZE(evt["op"].asString() << " request specified invalid \"path\": " << path)); + return false; + } + if (!target_view->isAvailable()) + { + response.error(STRINGIZE("Target view specified by \"path\": " << path << " is not visible")); + return false; + } + gFocusMgr.setKeyboardFocus(target_view); + return true; +} + +void LLWindowListener::cut(LLSD const & evt) +{ + Response response(LLSD(), evt); + if (!focusViewByPath(evt, response)) + return; + if (!LLEditMenuHandler::gEditMenuHandler) + { + response.error(STRINGIZE(evt["op"].asString() << " request failed: no edit menu handler available")); + return; + } + LLEditMenuHandler::gEditMenuHandler->cut(); +} + +void LLWindowListener::copy(LLSD const & evt) +{ + Response response(LLSD(), evt); + if (!focusViewByPath(evt, response)) + return; + if (!LLEditMenuHandler::gEditMenuHandler) + { + response.error(STRINGIZE(evt["op"].asString() << " request failed: no edit menu handler available")); + return; + } + LLEditMenuHandler::gEditMenuHandler->copy(); +} + +void LLWindowListener::paste(LLSD const & evt) +{ + Response response(LLSD(), evt); + if (!focusViewByPath(evt, response)) + return; + if (!LLEditMenuHandler::gEditMenuHandler) + { + response.error(STRINGIZE(evt["op"].asString() << " request failed: no edit menu handler available")); + return; + } + LLEditMenuHandler::gEditMenuHandler->paste(); +} + +void LLWindowListener::selectAll(LLSD const & evt) +{ + Response response(LLSD(), evt); + if (!focusViewByPath(evt, response)) + return; + if (!LLEditMenuHandler::gEditMenuHandler) + { + response.error(STRINGIZE(evt["op"].asString() << " request failed: no edit menu handler available")); + return; + } + LLEditMenuHandler::gEditMenuHandler->selectAll(); +} diff --git a/indra/newview/llwindowlistener.h b/indra/newview/llwindowlistener.h index 537322b19b..06e4090f6d 100644 --- a/indra/newview/llwindowlistener.h +++ b/indra/newview/llwindowlistener.h @@ -49,6 +49,10 @@ public: void mouseMove(LLSD const & evt); void mouseScroll(LLSD const & evt); void pasteText(LLSD const & evt); + void cut(LLSD const & evt); + void copy(LLSD const & evt); + void paste(LLSD const & evt); + void selectAll(LLSD const & evt); private: LLViewerWindow * mWindow; diff --git a/indra/newview/tests/llslurl_test.cpp b/indra/newview/tests/llslurl_test.cpp index 3ff38ea372..bf23538201 100644 --- a/indra/newview/tests/llslurl_test.cpp +++ b/indra/newview/tests/llslurl_test.cpp @@ -147,10 +147,12 @@ namespace tut { slurlTest() { + LLGridManager::createInstance(); LLGridManager::getInstance()->initialize(std::string("")); } ~slurlTest() { + LLGridManager::deleteSingleton(); } }; diff --git a/indra/newview/tests/llviewernetwork_test.cpp b/indra/newview/tests/llviewernetwork_test.cpp index 1cbb8b50eb..6190d7052b 100644 --- a/indra/newview/tests/llviewernetwork_test.cpp +++ b/indra/newview/tests/llviewernetwork_test.cpp @@ -175,6 +175,7 @@ namespace tut { viewerNetworkTest() { + LLGridManager::createInstance(); LLFile::remove(TEST_FILENAME); gCmdLineLoginURI.clear(); gCmdLineGridChoice.clear(); @@ -185,6 +186,7 @@ namespace tut ~viewerNetworkTest() { LLFile::remove(TEST_FILENAME); + LLGridManager::deleteSingleton(); } }; |
